MQTT-SN

от автора

В процессе поисков более легковесного протокола, похожего на полюбившийся мне MQTT для проекта беспроводных датчиков отслеживания положения на базе ESP8266 — оказалось, что существует, но пока не сильно распространена, версия протокола с названием MQTT For Sensor Networks (MQTT-SN).

“MQTT-SN спроектирован как можно более похожим на MQTT, но адаптирован к особенностям беспроводной среды передачи данных, таким как низкая пропускная способность, высокие вероятность сбоя в соединениях, короткая длина сообщения и т.п. Также оптимизирован для дешевых устройствах с аккумуляторным питанием и ограниченными ресурсами по обработке и хранения.”

В интернете довольно мало информации о данном протоколе, ковыряние в которой и стало основой для написание данной заметки.

Основные отличия MQTT-SN от “старшего брата” это уменьшение размера сообщения , в основном, за счет сокращения “служебной” информации, особенно интересна реализация QOS -1 — когда клиент отправляет сообщение без подтверждения о подтверждении доставки, и возможность использование отличного от TCP протокола, можно встретить реализации для UDP, UDP6, ZigBee, LoRaWAN, Bluetooth.

Не буду сильно погружаться в описание — кому интересно, можете ознакомиться со спецификацией MQTT-SN в OASIS. Приведу лишь пару схем из стандарта:

Рис.1 “Архитектура MQTT-SN”.
Рис.1 “Архитектура MQTT-SN”.

Первое что бросается в глаза — наличие MQTT брокера на схеме, помимо клиентов, шлюзов и форвардер MQTT-SN (не придумал как перевести, написал по аналогии с DNS). А это значит, что для функционирования протокола “сети сенсоров” полноценный MQTT брокер обязателен и необходим. 

Если рассмотреть функции каждого участника обмена то получается следующее:

  • MQTT-SN клиенты (как принимающие так и передающие сообщения) — подключаются к MQTT брокеру, через MQTT-SN шлюзы.

  • MQTT-SN шлюз — основная функция двусторонняя «синтаксическая» трансляция MQTT-SN ↔ MQTT.

  • MQTT-SN форвардер — если клиентам недоступен шлюз, они могут посылать и принимать сообщения через него.

  • MQTT брокер — сервер, своеобразное ядро системы, который тем и занимается что пересылает сообщения.

Рис.2 Прозрачные и агрегирующие шлюзы.
Рис.2 Прозрачные и агрегирующие шлюзы.

Здесь на картинке тоже можно видеть полноценный взрослый MQTT-брокер и два режима работы MQTT-SN шлюзов:

  • В прозрачном режиме для каждого клиента шлюз устанавливает и поддерживает отдельное соединение с MQTT брокером. Это соединение зарезервировано исключительно для сквозного и прозрачного обмена сообщениями между клиентом и брокером. Шлюз выполняет трансляцию между протоколами. Ну и поскольку весь обмен сообщениями осуществляется сквозным образом, все функции и возможности, которые реализуются, могут быть использованы клиентами.

  • В режиме агрегации, шлюз будет иметь только одно соединение с MQTT брокером. Все сообщения остаются между клиентами и шлюзом, а уже шлю решает что отправлять брокеру или какому клиенту принятое сообщения от MQTT брокера передать.

Ну что же — перейдем к реализации, в качестве операционной системы я использовал Ubuntu 20.04 со статическим адресом 10.10.10.10/24.

MQTT

Устанавливаем Eclipse Mosquitto:

sudo apt install mosquitto

Для тестирования нам не понадобятся какие-то настройки в отношении безопасности и т.п. Но я крайне не рекомендую так делать в производстве. Хотя если вы решили использовать MQTT/MQTT-SN на промышленном уровне все необходимые инструменты имеются. После установки давайте проверим как пересылаются сообщения — я использую Python для этого. Установим библиотеку paho-mqtt.

pip install paho-mqtt
Скрипт, передающий в топик “habr” сообщение “Hello Habrahabr!”:
import paho.mqtt.publish as publish  msg = "Hello Habrahabr!"  publish.single("habr", msg, hostname="10.10.10.10", port=1883)
Скрипт, подписывается на топик “habr” и принимает все сообщения:
import paho.mqtt.client as mqtt  def on_connect(client, userdata, flags, rc):     client.subscribe("habr/#")  def on_message(client, userdata, msg):     print(msg.topic + ' ' + str(msg.payload))  client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message  client.connect("10.10.10.10", 1883, 60) client.loop_forever()

Чтобы более подробнее познакомится с MQTT очень рекомендую блог Steve’s Internet Guide, ну и поиск не только по хабру, конечно.

MQTT-SN

Убедившись что наш брокер работает, переходим к следующему этапу. Я буду использовать шлюз из репозитория paho.mqtt-sn.embedded-c, повторим действия для компиляции шлюза в нашей ОС.

Для начала установим необходимые пакеты для сборки:

sudo apt-get install build-essential libssl-dev

Клонируем репозиторий себе в систему, переходим в папку со шлюзом и компилируем:

git clone -b develop https://github.com/eclipse/paho.mqtt-sn.embedded-c cd paho.mqtt-sn.embedded-c/MQTTSNGateway make install make clean

По умолчанию пакет ставится в директорию, куда вы клонировали репозиторий — если вы как и не особо пока заморачивались — то в домашнюю 😉 Для первого запуска нужно отредактировать конфигурацию шлюза и запустить его с правами sudo. В дальнейшем можно запускать уже обычно. Наша простая конфигурация (для большего упрощения я убрал закомментировать строки).

Наша простая конфигурация (для большего упрощения я убрал закомментировать строки):
BrokerName=localhost BrokerPortNo=1883 BrokerSecurePortNo=8883  ClientAuthentication=NO AggregatingGateway=NO QoS-1=NO Forwarder=NO PredefinedTopic=NO GatewayID=1 GatewayName=Paho-MQTT-SN-Gateway KeepAlive=900 # UDP GatewayPortNo=10000 MulticastIP=225.1.1.1 MulticastPortNo=1885 MulticastTTL=1

Как и писалось выше первый запуск делаем с “sudo” из домашней директории, при этом у нас будет полный вывод всего происходящего в консоли:

gateway.conf
sudo ./MQTT-SNGateway   ***************************************************************************  * MQTT-SN Gateway  * Part of Project Paho in Eclipse  * (http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt-sn.embedded-c.git/)  *  * Author : Tomoaki YAMAGUCHI  * Version: 1.4.0  ***************************************************************************  20210404 224219.274 Paho-MQTT-SN-Gateway has been started.   ConfigFile: ./gateway.conf  SensorN/W:  UDP Multicast 225.1.1.1:1885 Gateway Port 10000 TTL: 1  Broker:     localhost : 1883, 8883  RootCApath: (null)  RootCAfile: (null)  CertKey:    (null)  PrivateKey: (null)

Давайте теперь что-нибудь уже отправим нашему шлюзу, который это сообщение передаст MQTT-брокеру, для отправки будем использовать все тот же Python и MQTT-SN client for Python 3 and Micropython. В репозитории есть примеры для отправки и приема сообщений, немного подправив их, мы сможем уже отправлять и принимать сообщения как из MQTT сегмента куда-либо, так и из MQTT-SN сегмента.

mqttsn_publisher.py
from mqttsn.MQTTSNclient import Client import struct import time import sys  class Callback:      def published(self, MsgId):         print("Published")  def connect_gateway():     try:         while True:             try:                 aclient.connect()                 print('Connected to gateway...')                 break             except:                 print('Failed to connect to gateway, reconnecting...')                 time.sleep(1)     except KeyboardInterrupt:         print('Exiting...')         sys.exit()  def register_topic():     global topic     topic = aclient.register("habr")     print("topic registered.")  aclient = Client("mqtt_sn_client", "10.10.10.10", port=10000) aclient.registerCallback(Callback()) connect_gateway()  topic = None register_topic()  payload = ‘Hello Habrahabr!’  pub_msgid = aclient.publish(topic, payload, qos=0)  aclient.disconnect() print("Disconnected from gateway.")

Не буду здесь приводить много простого кода — если до этих пор все у вас получалось — думаю разберетесь и дальше 😉

ESP8266

Теперь пришло время настоящего веселья. Будем применять протокол, по моему мнению, на наиболее подходящих для него микроконтроллерах esp8266.

На самом деле готовых реализаций несколько и ни одна из них у меня корректно не завелась “без доработки напильником». Наиболее логичной реализацией мне показалась у MQTT-SN клиента у некоего Gabriel Nikol в репозитории arduino-mqtt-sn-client на GitHub.

Проблема 1. Тестовый пример, возможно в авторской реализации шлюза и работает (я не пробовал), но с Paho ни в какую не хочет. Ну что же, в запросах на репозитории висят похожие проблемы, будем пробовать решать. Исправляем, как указано здесь в запросе некорректный параметр типов топика и — все получилось! Сообщения отправляются — красота.

Проблема 2. Но при подписывании на топики — мы наблюдаем, что у нас к каждому сообщению добавляется “0x00”, который считается признаком конца строки, но почему-то у нас во всех других способах отправки сообщений ничего подобного нет. Пробежавшись по спецификации протокола, я и правда не нашел, что у нас сообщение обязательно должно заканчиваться так — вырезаем это в отправке сообщений. Еще стали на шаг ближе! Что мы стараемся отправить, то и получаем.

Проблема 3. Для обычной реализации MQTT протокола, я использовал для передачи кватерниона массив байт — так меньше сообщение, 16 (4 числа типа float) вместо 32 (если считать один знак до и 6 после запятой). Оказалось, что в данной реализации используется символьный тип данных char, где каждый байт интерпретируется как ASCII-символ. Давайте добавим и такую возможность — отправлять массив байтов.

Проблема 4. Заметил довольно длинный временной промежуток между подключением к беспроводной сети микроконтроллера и первым соединением со шлюзом. Давайте посмотрим в чем дело. Оказывается — при подключении, наш микроконтроллер спит при первом подключении 10 секунд, на втором 20. Исправим на 50 мс для первой и соответственно 100 для второй попытки — на данном этапе я думаю этого хватит — я проблем не заметил, но на всякий случай увеличил количество попыток до 5 (разумеется для использования в “реальном мире” нужно пересматривать этот таймаут).

Ну больше каких-то таких проблем в использовании я не нашел, и если кто-то что-то найдет — создавайте запросы автору (как то “меня терзают смутные сомнения”, что он продолжает поддерживать свое творение, но за спрос — не бьют в нос).

main.cpp
#include <I2Cdev.h> #include <MPU9250_9Axis_MotionApps41.h> #include <ESP8266WiFi.h> #include <WiFiUdp.h> #include <WiFiUdpSocket.h> #include <MqttSnClient.h> #include <ArduinoOTA.h>  const char* ssid     = "habr"; const char* password = "Hello Habrahabr!";  MPU9250 mpu;  #define SDA 4 #define SCL 5  IPAddress ip(10, 10, 10, 30); IPAddress gateway(10, 10, 10, 1); IPAddress subnet(255, 255, 255, 0); IPAddress gatewayIPAddress(10, 10, 10, 100); uint16_t localUdpPort = 10000;  WiFiUDP udp; WiFiUdpSocket wiFiUdpSocket(udp, localUdpPort); MqttSnClient<WiFiUdpSocket> mqttSnClient(wiFiUdpSocket);  const char* clientId = "thigh_l"; char* subscribeTopicName = "main"; char* publishTopicName = "adam/thigh_l"; String messageMQTT;  uint16_t packetSize; uint16_t fifoCount; uint8_t fifoBuffer[48]; bool blinkState = false; bool sendQuat = false;  Quaternion q;  int8_t qos = 0;  void mqttsn_callback(char *topic, uint8_t *payload, uint16_t length, bool retain) {   for (uint16_t i = 0; i < length; i++) {     messageMQTT += (char)payload[i];   }   if (messageMQTT == "start1"){     sendQuat = true;     messageMQTT = "";   }   else if (messageMQTT == "stop") {     sendQuat = false;     messageMQTT = "";   } }  void setup_wifi() {   delay(10);   WiFi.setSleepMode(WIFI_NONE_SLEEP);   WiFi.mode(WIFI_STA);   WiFi.config(ip, gateway, subnet);   WiFi.begin(ssid, password);   while (WiFi.status() != WL_CONNECTED) {     delay(50);   } }  void convertIPAddressAndPortToDeviceAddress(IPAddress& source, uint16_t port, device_address& target) {   target.bytes[0] = source[0];   target.bytes[1] = source[1];   target.bytes[2] = source[2];   target.bytes[3] = source[3];   target.bytes[4] = port >> 8;   target.bytes[5] = (uint8_t) port ; }  void setup() {   Wire.begin(SDA, SCL);   Wire.setClock(400000);   Serial.begin(115200);   setup_wifi();   mpu.initialize();   mpu.dmpInitialize();   mpu.setDMPEnabled(true);   packetSize = mpu.dmpGetFIFOPacketSize();   fifoCount = mpu.getFIFOCount();   ArduinoOTA.onStart([]() {   });   ArduinoOTA.onEnd([]() {   });   ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {   });   ArduinoOTA.onError([](ota_error_t error) {     if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");     else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");     else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");     else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");     else if (error == OTA_END_ERROR) Serial.println("End Failed");   });   ArduinoOTA.begin();   pinMode(LED_BUILTIN, OUTPUT);   mqttSnClient.begin();   device_address gateway_device_address;   convertIPAddressAndPortToDeviceAddress(gatewayIPAddress, localUdpPort, gateway_device_address);   mqttSnClient.connect(&gateway_device_address, clientId, 180);   mqttSnClient.setCallback(mqttsn_callback);   mqttSnClient.subscribe(subscribeTopicName, qos); }  void loop() {   fifoCount = mpu.getFIFOCount();     if (fifoCount == 1024) {       mpu.resetFIFO();       }     else if (fifoCount % packetSize != 0) {       mpu.resetFIFO();       }     else if (fifoCount >= packetSize  && sendQuat) {         mpu.getFIFOBytes(fifoBuffer, packetSize);         fifoCount -= packetSize;         mpu.dmpGetQuaternion(&q, fifoBuffer);         mqttSnClient.publish((uint8_t*)&q, publishTopicName, qos);         blinkState = !blinkState;         digitalWrite(LED_BUILTIN, blinkState);         mpu.resetFIFO();       }   ArduinoOTA.handle();   mqttSnClient.loop(); }

Тестирование

Вот теперь действительно началось самое интересное. Так ли уже хорош MQTT-SN против MQTT, ведь предназначен именно для беспроводного подключения.

У меня есть 15 датчиков с микроконтроллерами, и в своем тестовом проекте по захвату движений я использовал MQTT, в качестве старта передачи данных использовались сообщения в топик “main” и у меня была проверка на изменение кватерниона (т.е. новое сообщение отправлялось, когда предыдущий кватернион отличался от настоящего примерно на 0,5⁰). Не сложно будет изменить прошивки, чтобы для каждого микроконтроллера была своя команда старта передачи + передавать данные с частотой 50 Гц без проверки на отличия предыдущего и настоящего кватернионов.

Для этого напишем пару скриптов. Я себе представляю алгоритм тестирования следующим образом: Передаем сообщение для старта передачи с датчика, считываем среднее значение полученных сообщений в секунду, каждую секунду пишем в файл полученное количество сообщений, до запуска передачи от следующего датчика смотрим в диспетчере задач сколько потребляет трафика процесс “MQTTSN-Gateway”. Просто, быстро и не очень трудоемко — нужно делать, но лень. Для полномасштабного теста подожду уже готовые платки.

Рис3. Первенец с недочетами
Рис3. Первенец с недочетами

Для начала решил проверить, а все ли сообщения доходят, мы то используем в качестве транспорта UDP, который не гарантирует “обеспечение надежности, упорядочивания или целостности данных”. На протяжении 5 минут делал следующее — скриптом захватывал все сообщения и записывал время приема в файл, параллельно другим скриптом захватывал сообщения из последовательного порта и так же записывал время приема в другой файл. Получилось больше 13200 строк и соответствие в 100%, то есть сколько контроллер отправил сообщений, столько и было получено. Диспетчер задач показывал среднюю нагрузку сетевого интерфейса на получение 24-48 Кбит/с и 170-200 Кбит/с на отдачу. При таком же тестировании но с протоколом MQTT нагрузка на сетевой интерфейс составила 48-64 и 200-300 соответственно. Можете мне не верить и проверить все сами:) Как говорится налицо преимущества, но это для одного только датчика.

Кому интересно — ссылка на этот весь говнокод репозиторий. Продолжение следует…

ссылка на оригинал статьи https://habr.com/ru/post/550902/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *