В процессе поисков более легковесного протокола, похожего на полюбившийся мне MQTT для проекта беспроводных датчиков отслеживания положения на базе ESP8266 - оказалось, что существует, но пока не сильно распространена, версия протокола с названием MQTT For Sensor Networks (MQTT-SN).
“MQTT-SN спроектирован как можно более похожим на MQTT, но адаптирован к особенностям беспроводной среды передачи данных, таким как низкая пропускная способность, высокие вероятность сбоя в соединениях, короткая длина сообщения и т.п. Также оптимизирован для дешевых устройствах с аккумуляторным питанием и ограниченными ресурсами по обработке и хранения.”
В интернете довольно мало информации о данном протоколе, ковыряние в которой и стало основой для написание данной заметки.
Основные отличия MQTT-SN от “старшего брата” это уменьшение размера сообщения , в основном, за счет сокращения “служебной” информации, особенно интересна реализация QOS -1 - когда клиент отправляет сообщение без подтверждения о подтверждении доставки, и возможность использование отличного от TCP протокола, можно встретить реализации для UDP, UDP6, ZigBee, LoRaWAN, Bluetooth.
Не буду сильно погружаться в описание - кому интересно, можете ознакомиться со спецификацией MQTT-SN в OASIS. Приведу лишь пару схем из стандарта:
Первое что бросается в глаза - наличие MQTT брокера на схеме, помимо клиентов, шлюзов и форвардер MQTT-SN (не придумал как перевести, написал по аналогии с DNS). А это значит, что для функционирования протокола “сети сенсоров” полноценный MQTT брокер обязателен и необходим.
Если рассмотреть функции каждого участника обмена то получается следующее:
MQTT-SN клиенты (как принимающие так и передающие сообщения) - подключаются к MQTT брокеру, через MQTT-SN шлюзы.
MQTT-SN шлюз - основная функция двусторонняя "синтаксическая" трансляция MQTT-SN - MQTT.
MQTT-SN форвардер - если клиентам недоступен шлюз, они могут посылать и принимать сообщения через него.
MQTT брокер - сервер, своеобразное ядро системы, который тем и занимается что пересылает сообщения.
Здесь на картинке тоже можно видеть полноценный взрослый 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” из домашней директории, при этом у нас будет полный вывод всего происходящего в консоли:
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”. Просто, быстро и не очень трудоемко - нужно делать, но лень. Для полномасштабного теста подожду уже готовые платки.
Для начала решил проверить, а все ли сообщения доходят, мы то используем в качестве транспорта UDP, который не гарантирует “обеспечение надежности, упорядочивания или целостности данных”. На протяжении 5 минут делал следующее - скриптом захватывал все сообщения и записывал время приема в файл, параллельно другим скриптом захватывал сообщения из последовательного порта и так же записывал время приема в другой файл. Получилось больше 13200 строк и соответствие в 100%, то есть сколько контроллер отправил сообщений, столько и было получено. Диспетчер задач показывал среднюю нагрузку сетевого интерфейса на получение 24-48 Кбит/с и 170-200 Кбит/с на отдачу. При таком же тестировании но с протоколом MQTT нагрузка на сетевой интерфейс составила 48-64 и 200-300 соответственно. Можете мне не верить и проверить все сами:) Как говорится налицо преимущества, но это для одного только датчика.
Кому интересно - ссылка на этот весь говнокод репозиторий. Продолжение следует...
antsam
Для апгрейда старых устройств — MQTT-S может быть интересен, но как по мне подключение сенсоров по WiFi — уже прошлый век.
Сейчас для этого есть более подходящие протоколы и устройства — zigbee или zwave. Не знаю как для zwave, но для zigbee, например, уже реализована библиотека для подключения к MQTT брокеру
kharlashkin Автор
MQTT-SN умеет работать поверх и ZigBee, и LoRaWAN, и Bluetooth, и др. О чем я внятно сразу в статье и написал. То что уже довольно много реализовано «шлюзов» между перечисленными мною выше протоколами и MQTT-брокерами, говорит о высокой популярности MQTT, простоте и как следствие распространенности. Использование MQTT-SN поверх Ethernet/WiFi/ZigBee/LoRaWAN/Bluetooth/BLE и других, позволит просто существенно упростить разработку и взаимодействие устройств — так как в этом случае можно оперировать одинаковыми сущностями, а в случае Ethernet/WiFi еще и использовать стандартное сетевое активное и беспроводное оборудование для построения сетей.