Как стать автором
Обновить

MQTT-SN + ESP8266

Время на прочтение9 мин
Количество просмотров11K

В процессе поисков более легковесного протокола, похожего на полюбившийся мне 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” из домашней директории, при этом у нас будет полный вывод всего происходящего в консоли:

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”. Просто, быстро и не очень трудоемко - нужно делать, но лень. Для полномасштабного теста подожду уже готовые платки.

Фото 1. Первенец с недочетами
Фото 1. Первенец с недочетами

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

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

Теги:
Хабы:
+3
Комментарии2

Публикации

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн