Ни для кого не секрет, что IoT‑устройства собирают огромное количество информации, в том числе личные данные пользователей и фиксируют их местоположение. А также они жёстко привязаны к конкретному производителю, т.е. пользователь зависим от конкретного производителя или бренда. Гаджеты совместимы с парой основных систем, но кроссплатформенность отсутствует. Устройства разных производителей часто не могут взаимодействовать друг с другом без дополнительных шлюзов. Универсального протокола связи устройств тоже нет. Из стандартов: Zigbee, Matter, Z‑Wave, Bluetooth, Wi-Fi — как следствие умные устройства плохо дружат между собой.
Есть ли выход из подобной ситуации?
Конечно да.
В прошлой статье я говорил о экосистеме MixRech с поисковиком и мессенджером. Ссылка тут. И там есть мессенджер MixChat, который управляет умными устройствами с помощью системного чат бота SmarMix в самом же MixChat.
SmartMix объединяет умные устройства в один стандарт MQTT
Мессенджер MixChat и микроконтроллер ESP32 взаимодействуют друг с другом по протоколу MQTT.
Вам потребуется собственный MQTT сервер (например, на Raspberry Pi или в облаке), но после разового подключения, вам не нужно будет настраивать SmartMix.
Как это работает?
Зайдя в чат со SmartMix, вы вводите параметры MQTT сервера по кнопке вверху и сохраняете. Потом вы подключаетесь к своему серверу и можете отправлять команды, для управления ESP32: включать/выключать светодиод, выводить температуру на экран.


SmartMix не привязан к конкретному производителю. А это означает, что вы можете управлять разными устройствами без дополнительных настроек.
ESP32 не собирает данные пользователей
Когда вы отправляете сообщение SmartMix, ваши личные данные: пароли, номера телефонов, электронные почты или цифровые ID людей не используются для передачи.
Когда вы вводите параметры своего MQTT сервера, в.т.ч. и топики для отправки и получения, они шифруются и сохраняются в кэш по API запросу:
class SaveMqttView(generics.GenericAPIView): serializer_class = SaveMqttSerializer authentication_classes = [JWTAuthentication, CsrfExemptSessionAuthentication] permission_classes = [IsAuthenticated] def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = request.user.id server = serializer.validated_data['server'] port = serializer.validated_data['port'] client_id = serializer.validated_data['client_id'] mqtt_username = serializer.validated_data['mqtt_username'] mqtt_password = serializer.validated_data['mqtt_password'] topic_sent = serializer.validated_data['topic_sent'] topic_subscribe = serializer.validated_data['topic_subscribe'] mqtt_values = list(request.session.values()) for value in mqtt_values: if value == server or value == mqtt_username: return Response({"message": "Ошибка. Пользователь не может подключиться" " к другому серверу"}, status=403) key = gen_key(user, salt) request.session[f'server_{user}'] = encrypt_text(server, key).decode('utf-8') request.session[f'port_{user}'] = port request.session[f'client_id_{user}'] = client_id request.session[f'mqtt_username_{user}'] = encrypt_text(mqtt_username, key).decode('utf-8') request.session[f'mqtt_password_{user}'] = encrypt_text(mqtt_password, key).decode('utf-8') request.session[f'topic_sent_{user}'] = topic_sent request.session[f'topic_subscribe_{user}'] = topic_subscribe return Response({"message": "Данные MQTT успешно сохранены"})
Данные MQTT-сервера расшифровываются только на сервере при подключении, после чего вы подключаетесь к серверу.
В боте за подключение к серверу отвечает класс MQTT. Вот его ключевые методы:
connection() — устанавливает связь с сервером.
publish(msg) — отправляет команду в топик topic_sent.
subscribe() — слушает топик topic_subscribe, чтобы получить ответ от ESP32.
Полный код класса выглядит так:
Скрытый текст
import os import time import sys from dotenv import load_dotenv from paho.mqtt import client as mqtt_client from .cifer import gen_key, decrypt_text load_dotenv() salt = os.getenv('CIFER_SALT') class MQTT: def __init__(self, mqtt_dict, client, user_id, exit_flag, msg): key = gen_key(user_id, salt) self.client = client self.exit_flag = exit_flag self.msg = msg self.server = decrypt_text(mqtt_dict[f'server_{user_id}'], key).decode('utf-8') self.port = mqtt_dict[f'port_{user_id}'] self.mqtt_username = decrypt_text(mqtt_dict[f'mqtt_username_{user_id}'], key).decode('utf-8') self.mqtt_password = decrypt_text(mqtt_dict[f'mqtt_password_{user_id}'], key).decode('utf-8') self.topic_sent = mqtt_dict[f'topic_sent_{user_id}'] self.topic_subscribe = mqtt_dict[f'topic_subscribe_{user_id}'] def connection(self): self.client.username_pw_set(self.mqtt_username, self.mqtt_password) # self.client.on_connect = "Connected to MQTT Broker!" if self.server and self.port: self.client.connect(self.server, int(self.port)) print("Connect to server") else: print("Ошибка отправки. Не верные данные сервера") def publish(self, msg): # Отправить сообщение в MQTT if self.topic_sent: result = self.client.publish(self.topic_sent, msg) status = result[0] if status == 0: print(f"Отправлено `{msg}` в топик `{self.topic_sent}`") else: print(f"Не удалось отправить сообщение в топик {self.topic_sent}") else: print("Ошибка. Неверный топик для отправки") def on_connect(self, client, userdata, flags, rc): if rc == 0: self.client.subscribe(self.topic_subscribe) else: print("Failed to connect, return code %d\n", rc) def subscribe(self): try: if self.server and self.port: self.client.username_pw_set(self.mqtt_username, self.mqtt_password) self.client.on_connect = self.on_connect self.client.on_message = self.on_message self.client.connect(self.server, int(self.port)) self.client.loop_start() # Запускаем цикл в фоне start = time.time() while not self.exit_flag: time.sleep(0.1) self.exit_flag = False print(time.time() - start) self.client.loop_stop() # Останавливаем цикл print(self.client.is_connected) self.client.disconnect() else: print("Ошибка получения. Не верные данные сервера") except KeyboardInterrupt: print("Соединение прервано") sys.exit() def on_message(self, client, userdata, message): self.msg = message.payload.decode() print('MQTT MESSAGE:', message.payload.decode()) self.exit_flag = True def print_message(self): print(self.msg) return self.msg def return_mqtt(mqtt_dict, user_id): mqtt = MQTT(mqtt_dict, mqtt_client.Client(mqtt_client.CallbackAPIVersion.VERSION1, mqtt_dict[f'client_id_{user_id}']), user_id,False, '') return mqtt
Полный код всего проекта с поисковиком и мессенджером здесь.
После подключения к MQTT-серверу вы отправляете сообщение по MQTT:
mqtt_dict = dict(request.session.items()) send_to_mqtt(mqtt_dict, sender_id, content)
def send_to_mqtt(mqtt_dict, user_id, msg): mqtt = return_mqtt(mqtt_dict, user_id) mqtt.connection() mqtt.publish(msg)
Сообщение доходит до ESP32, срабатывает внутренняя логика на ESP32. С ESP32 вы получаете только технические показатели (например, значение температуры).
ESP32 также подключается к MQTT и использует следующие методы и функции:
connect - устанавливает связь с сервером.
set_callback - обрабатывает полученное сообщение и отправляет ответ в топик topic_subscribe.
on_message - ловит полученную команду от бота SmartMix
subscribe - слушает топик topic_sent для получения сообщения от бота SmartMix.
check_msg - проверяет полученные сообщения из топика topic_sent в бесконечном цикле.
Полный код работы ESP32 с MQTT:
Скрытый текст
import os import network import time from umqtt.simple import MQTTClient from micro import flashing_led, led_off, show_temperature print(os.getcwd()) print(os.listdir()) server = 'MQTT_SERVER' port = 'MQTT_PORT' topic = 'MQTT_TOPIC' topic_sub = 'MQTT_TOPIC_ANSWER' client_id = 'MQTT_CLIENT_ID' username = 'MQTT_USERNAME' password = 'MQTT_PASSWORD' sta_if = network.WLAN(network.STA_IF) if not sta_if.isconnected(): # Подключение к Wi-Fi print("Connect to Wi-Fi...") sta_if.active(True) print('Wi-Fi Interface states:') print('Active:', sta_if.active()) print('Connected:', sta_if.isconnected()) timeout = 20 start = time.time() sta_if.connect('WIFI_SSID', 'WIFI_PASSWORD') while not sta_if.isconnected(): print("Wating...") time.sleep(1) if time.time() - start > timeout: print("Connection is timeout") break print("Connected. IP:", sta_if.ifconfig()) else: # Подключение к Wi-Fi установлено print("Already connected, IP:", sta_if.ifconfig()) def on_message(topic, msg): answer = '' # Обработка сообщений print(msg) if msg == b'led_on': flashing_led() answer = 'Светодиод включён' client.publish(topic_sub, bytes(answer, 'utf-8')) print(answer) elif msg == b'led_off': led_off() answer = 'Светодиод выключен' client.publish(topic_sub, bytes(answer, 'utf-8')) print(answer) elif msg == b'temp': show_temperature() answer = show_temperature() print(answer) client.publish(topic_sub, bytes(answer, 'utf-8')) client = MQTTClient(client_id, server, port, username, password) client.connect() client.set_callback(on_message) client.subscribe(topic) last_ping = time.time() ping_interval = 30 while True: client.check_msg() now = time.time() if now - last_ping > ping_interval: client.ping() last_ping = now
Полный код для всего ESP32 здесь.
И в конце вы получаете зашифрованное сообщение от ESP32: "Светодиод включён", "Светодиод выключен" или значение температуры.
salt = os.getenv('CIFER_SALT') key = gen_key(chat_id, salt) message_content = get_mqtt(mqtt_dict, sender_id) message_bot = Message( sender_bot_id=bot.id, content=encrypt_text(message_content, key), chat_id=chat_id, )
def get_mqtt(mqtt_dict, user_id): mqtt = return_mqtt(mqtt_dict, user_id) mqtt.subscribe() return mqtt.print_message()
Для хранения данных сервера в кэше я использовал Redis, который развёрнут в контейнере и защищён паролем. Контейнер с Redis запускается вместе с поисковиком, мессенджером и Nginx.
Полный код:
Скрытый текст
version: '3.8' services: mixrech: build: ./mixrech command: sh -c "python manage.py makemigrations && python manage.py migrate && python manage.py collectstatic --noinput && gunicorn mixrech.wsgi:application --bind 0.0.0.0:8000 --timeout 120" volumes: - .:/app - mixrech_static:/mixrech/staticfiles/ env_file: - .env ports: - "8000:8000" mixchat: build: ./mixchat command: sh -c "python manage.py migrate && python manage.py collectstatic --noinput --verbosity 2 && gunicorn mixchat.wsgi:application --bind 0.0.0.0:8001 --timeout 120" volumes: - .:/app - mixchat_static:/mixchat/staticfiles/ - media_data:/mixchat/main/media_v4/ environment: - MEDIA_ROOT=/mixchat/main/media_v4/ env_file: - .env ports: - "8001:8001" redis: image: redis:7-alpine container_name: mixchat_redis restart: unless-stopped ports: - "6379:6379" volumes: - redis_data:/data command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes networks: - default environment: - REDIS_PASSWORD=${REDIS_PASSWORD} nginx: image: nginx:1.25.3 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - media_data:/app/media/ - mixrech_static:/app/staticfiles/mixrech/ - mixchat_static:/app/staticfiles/mixchat/ - /etc/letsencrypt/live/mixrech.com/fullchain.pem:/etc/letsencrypt/live/mixrech.com/fullchain.pem - /etc/letsencrypt/live/mixrech.com/privkey.pem:/etc/letsencrypt/live/mixrech.com/privkey.pem depends_on: - mixrech - mixchat ports: - "80:80" - "443:443" volumes: mixrech_static: mixchat_static: media_data: redis_data:
Архитектура шифрования
Соль (CIFER_SALT) для генерации ключа хранится на сервере в переменных окружениях и недоступна клиенту. Это исключает утечку паролей.
Шифрование симметричное и происходит оно во всех чатах.
import base64 from dotenv import load_dotenv from cryptography.fernet import Fernet from cryptography.hazmat.primitives.kdf.scrypt import Scrypt load_dotenv() def gen_key(id, salt): kdf = Scrypt(salt=salt.encode('utf-8'), length=32, n=2**14, r=8, p=1) key = kdf.derive(str(id).encode()) return base64.urlsafe_b64encode(key) def encrypt_text(text, key): fer = Fernet(key) encrypt_data = fer.encrypt(text.encode('utf-8')) return encrypt_data def decrypt_text(text, key): fer = Fernet(key) decrypt_data = fer.decrypt(text) return decrypt_data def encrypt_file(file_path, key): fer = Fernet(key) with open(file_path, 'rb') as f: data = f.read() encrypt_data = fer.encrypt(data) with open(file_path, 'wb') as f: f.write(encrypt_data) def decrypt_file(file_path, key): fer = Fernet(key) with open(file_path, 'rb') as f: data = f.read() decrypt_data = fer.decrypt(data) with open(file_path, 'wb') as f: f.write(decrypt_data)
Итог
Чат-бот SmartMix управляет устройством без привязки к производителю, а само устройство не собирает данные пользователей.
Буду рад вопросам!
