Оригинальная статья: https://kotyara12.ru/iot/esp32-mqtt-client/
Дисклеймер: основной площадкой для публикации моих статей на текущий момент является мой сайт https://kotyara12.ru. Дзен я не могу воспринимать всерьез из-за его постоянно меняющейся политики и того, что он совсем не подходит для технических текстов. Поэтому здесь статьи иногда могут быть с сокращениями или ошибками в оформлении из-за некорректного переноса. И если тема статьи вам интересна - прочтите её полностью по ссылке выше, там и код примеров скопипастить можно. Благодарю за понимание.
Доброго здравия, уважаемые читатели!
В данной статье обсудим встроенный в ESP-IDF модуль MQTT-клиента – для чего он нужен, как использовать, настраивать и как использовать в своих проектах. Компонент MQTT-клиента называется esp-mqtt и доступен не только из собственно фреймворка ESP-IDF, но и из фреймворка ArduinoEspressif32. То есть если вы используете в вашем проекте ESP32, то нет нужды использовать сторонние библиотеки (например PubSubClient или аналоги) для подключения к MQTT-серверу, даже если вы пишете скетч под Arduino – все уже есть и встроено, достаточно изучить и начать пользоваться. Чем мы, собственно, сейчас и займемся.
Если вы не знакомы с MQTT протоколом, рекомендую вам вначале ознакомиться с другой статьей “Что такое MQTTи с чем его едят“ – там вы узнаете об основных компонентах MQTT-транспорта и принципах его работы. Здесь же я лишь в самых кратких словах остановлюсь на этом вопросе.
Осторожно – многа букав!
Обзор
ESP-MQTT — это реализация клиента протокола MQTT, который является облегченным протоколом обмена сообщениями на основе концепции «публикация/подписка». Теперь (теперь – а это с коих пор? – прим.авт.) ESP-MQTT поддерживает не только MQTT v3.1.1, но и MQTT v5.0.
ESP-MQTT является полнофункциональным клиентом и поддерживает следующие функции:
- Подключение к серверу посредством любого типа подключения: MQTT over TCP, SSL with Mbed TLS, MQTT over WebSocket, и MQTT over WebSocket Secure
- Одновременная работа нескольких экземпляров (например для подключения к разным серверам)
- Поддерживаются все стандартные операции протокола: подписка, публикация, аутентификация, LWT-сообщения, keep alive pings и все три уровня QoS.
Отправка сообщений с разными уровнями QoS
В ESP-MQTT реализован механизм повторной отложенной отправки сообщений с использованием очереди исходящих сообщений (outbox).
Существует два способа создания нового сообщения MQTT:
- С помощью блокирующей функции esp_mqtt_client_publish() – эта функция пытается отправить сообщение немедленно.
- С помощью её неблокирующего аналога esp_mqtt_client_enqueue() – эта функция всегда ставит сообщение в очередь отправки.
При этом поведение клиента зависит от уровня QoS, заданного при отправке:
- Сообщения с QoS 0 всегда отправляются только один раз.
- Сообщения с QoS 1 и 2 ведут себя по-разному, поскольку протокол требует дополнительных шагов для завершения процесса. Библиотека ESP-MQTT всегда повторно передает неподтвержденные сообщения публикации QoS 1 и 2, чтобы избежать потери сообщений при сбоях в соединениях (хотя спецификация MQTT требует повторной передачи только при повторном подключении, когда флаг «Clear Session» установлен на 0).
Таким образом, сообщения с QoS 1 и 2, которым может потребоваться повторная передача, всегда ставятся в очередь. Но если для создания сообщения была использована функция esp_mqtt_client_publish(), то первая попытка передачи произойдет немедленно. Повторная попытка передачи для неподтвержденных сообщений произойдет после времени, заданного в message_retransmit_timeout при создании подключения.
После истечения времени, заданного через menuconfig в макросе CONFIG_MQTT_OUTBOX_EXPIRED_TIMEOUT_MS, сообщения считаются устаревшими и удаляются, даже если они не были отправлены. При этом, если был включен (enabled) макрос CONFIG_MQTT_REPORT_DELETED_MESSAGES , то будет сгенерировано соответствующее событие для уведомления пользователя.
Использование ESP MQTT API
MQTT-клиент – это отдельная задача FreeRTOS, которая работает “в фоне”, а общение с ней происходит посредством вызова API-шных функций, а также событий и функций обратного вызова. ESP MQTT API имеет потоко-безопасность, и его можно использовать из разных задач одновременно.
Конфигурация ESP MQTT
Прежде чем переходить непосредственно к коду, стоит заглянуть в меню конфигурации ESP-IDF, например как описано здесь. Настроек ESP MQTT не очень много и все они находятся в разделе Component config → ESP-MQTT Configurations:
Здесь вы можете выбрать:
- Какие версии протокола MQTT вы собираетесь использовать в проекте. Конечно, можно оставить обе версии, но это может увеличить размер полученного бинарника, поэтому все что не нужно – смело “выпиливаем”.
- Enable MQTT over SSL – нужна ли вам поддержка защищенных соединений. Я советую так: если сервер “внешний” – то обязательно нужна, если локальный (внутри сети, например HA или на роутере) – то не обязательна.
- Use Incremental Message Id – Установите [*], чтобы идентификатор сообщения генерировался как инкрементное число, а не как случайное значение. По умолчанию используется случайное значение.
- Skip publish if disconnected – Активация этой опции заставляет API отбрасывать любые сообщения, если клиент не подключен к серверу. Если эта опция выключена, то сообщения с QoS > 0 будут в любом случае добавлены во внутренний исходящий почтовый ящик для публикации позже, даже если клиент отключен. Это может привести к переполнению памяти в некоторых случаях. Параметр MQTT_SKIP_PUBLISH_IF_DISCONNECTED позволяет приложениям переопределять это поведение и не ставить пакеты публикации в очередь в отключенном состоянии.
- Report deleted messages – Установите [*], чтобы генерировались соответствующие события для сообщений, которые были удалены из папки «исходящие» до того, как они были корректно отправлены и подтверждено их получение.
- MQTT Using custom configurations – разрешить пользователю самому настроить конфигурацию MQTT-брокера.
- Default MQTT over TCP port и Default MQTT over SSL port – здесь вы можете указать порты, используемые для подключения к брокеру “по умолчанию”. Лично я предпочитаю указать их явно при настройке подключения.
- Default MQTT Buffer Size – укажите здесь размер буфера, который используется как для передачи, так и для приема сообщений. Это не размер исходящего почтового ящика (очереди)!
- MQTT task stack size – размер стека для задачи MQTT-клиента. Тут все зависит от ваших потребностей. Например при настройках по умолчанию 6144 ( 6 кБ ) у меня стабильно остается чуть больше 2 кБ стека “не занято”. Поэтому, когда придется идти на режим “жестой экономии” памяти, можно смело уменьшить это значение до 5 кБ ( 5120 ).
- Disable API locks – настройки по умолчанию используют блокировки для защиты внутренних структур. Можно отключить эти блокировки, если пользовательский код не обращается к MQTT API из нескольких параллельных задач, чем ускорите работу и сэкономите память.
- MQTT task priority – приоритет для задачи MQTT-клиента. MQTT не такая важная задача, чтобы ставить ее важнее прикладной. Я обычно не изменяю значение по умолчанию. Если прикладная задача написана правильно, с периодическим уходом в спячку, проблем не будет.
- MQTT transport poll read timeut – таймаут в миллисекундах для операции чтения TCP/IP. Для нормальных сетей 1000 мс – более чем достаточно, но для каких-нибудь 2G – можно попробовать увеличить это значение.
- Number of queued events – количество событий в очереди. Для меня это на текущий момент загадка – help по этому поводу “молчит”, но это явно не размер исходящего ящика.
- Enable MQTT task core selection – разрешает выбор пользователю ядра, на котором будет работать задача MQTT. По умолчанию это CORE0 – системное ядро.
- Use external memory for outbox data – очень полезная опция, если у вас в модуле присутствует дополнительная “внешняя” память (SPIRAM). Позволяет организовать исходящий почтовый ящик не в основной куче, а на внешней памяти. Поскольку сетевые операции передачи сообщений все равно медленнее, чем чтение по QSPI, это практически не повлияет на производительность клиента.
- Enable custom outbox implementation – с помощью этой опции вы можете включить возможность самому написать свою реализацию очереди исходящих сообщений, например с лимитами и приоритетами. Не пользовался пока что, не знаю.
- Outbox message expired timeout[ms] – период времени в миллисекундах, после истечения которого не отправленные сообщения в очереди исходящих сообщений считаются устаревшими и подлежат немедленному уничтожению. По умолчанию это 30 секунд – если за 30 секунд не удалось отправить сообщение по любой причине, то все, оно потеряно.
Не забудьте сохранить настройки перед выходом из утилиты.
Настройка и запуск MQTT – клиента
Прежде чем начинать пользоваться данным API, необходимо включить в проект заголовочный файл:
#include "mqtt_client.h"
Далее необходимо настроить подключение к серверу. Самая сложная часть работы, далее вы поймете почему. Для этого используем функцию esp_mqtt_client_init():
esp_mqtt_client_handle_t esp_mqtt_client_init(const esp_mqtt_client_config_t *config)
Для этого ей необходимо передать ссылку на довольно сложную структуру esp_mqtt_client_config_t, посмотрите как она выглядит:
Как говорится – “зацените масштаб проблемы”. Немножко громоздко??? Таки да. Что ж, придется со всем этим разбираться.
Указанная структура esp_mqtt_client_config_t содержит внутри себя несколько вложенных структур поменьше, как в матрешке:
- broker_t – параметры брокера, в том числе его адрес и параметры SSL
- credentials_t – параметры учетной записи, с помощью которой вы собираетесь подключаться к серверу
- session_t – параметры сессии
- network_t – параметры транспортного уровня (сети передачи данных)
- task_t – параметры задачи MQTT-клиента
- buffer_t – параметры буферов приема / передачи транспортного уровня
- outbox_config_t – параметры исходящей очереди (“почтового ящика”)
Некоторые из параметров “перекрывают” параметры, заданные в menuconfig. Если их не заполнять, то значения будут подтянуты из настроек проекта.
Совет: чтобы не заполнять все подряд, в том числе и то, что вам абсолютно не нужно, можно сделать так (один из вариантов):
esp_mqtt_client_config_t mqttCfg;
memset(&mqttCfg, 0, sizeof(esp_mqtt_client_config_t));
1. Параметры брокера
Структура broker_t выглядит так:
В свою очередь, здесь две вложенных структуры – address_t и verification_t.
1. address_t отвечает за настройку адреса и других параметров сервера:
где:
- uri – полный путь к брокеру с обязательным указанием префикса протокола и номера порта – например ssl://server.com:8883. Ну или если порт не указан, то конфигуратор возьмет его из настроек sdkconfig.h, которые мы рассматривали выше.
или есть альтернативный вариант:
- hostname – имя хоста или его адрес в локальной сети или сети интернет
- transport – тип транспортного протокола, который будет использоваться для передачи пакетов: MQTT_TRANSPORT_OVER_TCP, MQTT_TRANSPORT_OVER_SSL, MQTT_TRANSPORT_OVER_WS, MQTT_TRANSPORT_OVER_WSS
- path – дополнительный путь к скрипту на сервере, если он вдруг используется (относительно хоста)
- port – номер порта брокера, если указать 0, то будет взят в соответствии с параметрами конфигурации, которые мы рассматривали выше.
Чтобы не запутаться – у вас есть два пути:
- настроить только uri – в этом случае система сама распарсит uri и вытащит оттуда все остальные поля самостоятельно.
- настроить все поля, кроме uri, по отдельности
Что вы предпочитаете – решать только вам.
Пример заполнения:
2. verification_t – параметры SSL, если оно используется
Что это такое, SSL и TLS, и зачем оно вообще хоть кому-то надо, я уже рассказывал в другой статье. Если вы впервые сталкиваетесь с этим – рекомендую ознакомиться.
Здесь же отмечу только два факта: а) вопреки расхожему мнению, что ESP32 “не тянет” TLS, то могу сказать , что таки “тянет”, и довольно успешно; и б) “никто этим все равно не пользуется” – а вот это вы определенно зря. Но – если сервер находится в локальной сети за NAT-ом и доступа “наружу” нет, то данную структуру действительно можно не заполнять.
где:
- use_global_ca_store – указывает, следует ли использовать глобальное хранилище сертификатов, в противном случае следует заполнить поле certificate.
- (*crt_bundle_attach)(void *conf) – указатель на функцию, подключающее глобальный сборник корневых сертификатов из хранилища корневых сертификатов Mozilla NSS – вам не нужно будет заботится о подключении корневого сертификата сервера, но взамен вы расплачиваетесь размером свободной памяти
- certificate – указатель на первый байт корневого сертификата сервера
- certificate_len – длина корневого сертификата сервера
- psk_hint_key – указатель на структуру PSK для включения аутентификации PSK (как альтернативы проверке сертификата). PSK используется только в том случае, если нет других способов проверки брокера. Он не копируется и не освобождается клиентом, пользователю необходимо удалить его из памяти при необходимости.
- skip_cert_common_name_check – позволяет пропустить проверку сервера по сертификату. То есть как бы SSL, но не совсем. Не рекомендую этого делать без крайней необходимости и только тогда, когда вы понимаете для чего вы это делаете – это не способ исправления ваших ошибок.
- alpn_protos – список поддерживаемых протоколов шифрования, завершающийся нулем, для использования в ALPN. Не заполняйте это, если не знаете для чего оно нужно. Я – не знаю 😉
- common_name – Указатель на строку, содержащую общее имя сертификата сервера. Если не NULL, CN сертификата сервера должен соответствовать этому имени, Если NULL, CN сертификата сервера должен соответствовать имени хоста. Это игнорируется, если skip_cert_common_name_check=true
Я обычно заполняю только поля certificate и certificate_len, ну и use_global_ca_store при необходимости.
2. Параметры учетной записи
Далее нам предстоит заполнить данные вашей учетной записи, с помощью которой будем подключаться к серверу. Все “серьезные” брокеры работают только через логин / пароль, ну или бывает токен безопасности. Конечно, есть варианты, на которых авторизация не требуется, но они только для целей тестирования.
Структура credentials_t также, как и в предыдущем случае, содержит вложенную структуру authentication_t:
где:
- username – имя пользователя MQTT-сервера должно быть указано в любом случае
- client_id – уникальный идентификатор клиента. Если оставить NULL, то API сгенерирует его автоматически по шаблону ESP32_%CHIPID%, где %CHIPID% – последние три цифры MAC-адреса в HEX формате. Данный параметр игнорируется, если set_null_client_id == true.
- set_null_client_id – позволяет вообще оставить пустым client_id, если сервер его не требует.
и данные для дополнительной аутентификации authentication_t, заполняемые опционально:
- authentication.password – пароль пользователя (если есть)
- authentication.certificate – сертификат клиента для взаимной SSL-аутентификации, если таковая требуется. Должен быть указан вместе с key.
- authentication.certificate_len – длина буфера клиентского сертификата, если он указан
- authentication.key – приватный ключ для для взаимной SSL-аутентификации, если таковая требуется.
- authentication.key_len – длина буфера приватного ключа
- authentication.key_password – пароль для расшифровки клиентского ключа, это не PEM и не DER.
- authentication.key_password_len – длина буфера пароль для расшифровки клиентского ключа
- authentication.use_secure_element – для ESP32-ROOM-32SE можно использовать встроенный аппаратный secure element для ускорения расчетов
- authentication.ds_data – хендл дескриптора для цифровой подписи, периферийное устройство цифровой подписи доступно в некоторых устройствах Espressif
В реальных проектах мне пока приходилось использовать всего три поля, этого вполне достаточно:
или так
3. Параметры сессии
Параметры сессии включают в себя и LWT-сообщение. То есть, несмотря на то, что стандартом MQTT предусмотрена возможность задать LWT-сообщение для нескольких топиков (тем) одновременно, в данной реализации клиента это возможно сделать только единожды:
где:
- disable_clean_session – отключить “чистую сессию“. Флаг “чистый сеанс” намеренно инвертирован, чтобы при инициализации структуры нулями чистая сессия была по умолчанию.
- keepalive – интервал пинга, с помощью которого клиент оповещает сервер, что “жив”, по умолчанию 120 секунд. Установка значения в 0 не отключает механизм keepalive, а использует значение по умолчанию.
- disable_keepalive – отключает механизм keepalive совсем
- protocol_ver – задайте версию протокола, которую следует использовать для подключения к серверу: MQTT_PROTOCOL_UNDEFINED, MQTT_PROTOCOL_V_3_1, MQTT_PROTOCOL_V_3_1_1 или MQTT_PROTOCOL_V_5
- message_retransmit_timeout – укажите интервал в миллисекундах, через который будет предпринята попытка повторной отправки сообщений с QoS > 0
и параметры LWT-сообщения:
- last_will.topic – топик ( тема )
- last_will.msg – содержимое завещания
- last_will.msg_len – длина сообщения завещания
- last_will.qos – качество обслуживания: 0, 1 или 2
- last_will.retain – сохранять это сообщение на сервере
для будущих потомковили нет
Пример заполнения:
4. Параметры транспортного уровня (сети передачи данных)
Здесь все проще:
где:
- reconnect_timeout_ms – интервал в миллисекундах, через который будет предпринята попытка повторного подключения к брокеру, если автоматическое повторное подключение не отключено (по умолчанию 10 с)
- timeout_ms – таймаут для сетевых операций (чтение / запись сокета) (по умолчанию 10 с)
- refresh_connection_after_ms – обновить соединение после этого значения (в миллисекундах)
- disable_auto_reconnect – с помощью этого параметра можно отключить автоматическое переподключение к брокеру (например если вы сами его восстанавливаете)
- transport – указатель на хендл транспортного сетевого протокола.
- if_name – имя сетевого NETIF-интерфейса, если их несколько. Например, если у вас в системе два подключения – WiFi и Ethernet, то можно заставить работать MQTT только через один из них.
Пример заполнения:
5. Параметры задачи MQTT-клиента
При необходимости, здесь можно задать параметры задачи MQTT-клиента, отличные от того, что заданы через menuconfig:
Я думаю эти параметры в особом пояснении не требуются.
Пример заполнения:
Можно оставить эту структуру не заполненной, тогда значения будут взяты из файла sdkconfig.h.
6. Параметры буферов приема / передачи транспортного уровня
Здесь, как я уже упоминал, можно задать размеры буферов передачи и приема данных по сети:
Например:
Можно оставить эту структуру не заполненной, тогда значения будут взяты из файла sdkconfig.h.
7. Параметры исходящей очереди (“почтового ящика”)
На текущий момент тут всего один параметр – uint64_tlimit, который указывается в байтах:
Этот параметр отвечает за то, насколько большим может быть исходящий почтовый ящик. При превышении этого лимита все сообщения, даже с QoS > 0, будут отброшены. Это может быть полезно в условиях нестабильной связи с сервером, когда отправить сообщения зачастую не удается и свободная память “тает на глазах”. К слову, эту опцию разработчикам предложил я пару-тройку лет назад, когда “боролся со связью”, и мое issue было принято.
Просто не заполняйте это, если не желаете устанавливать никаких лимитов.
Итоговый вариант
Устали читать? Что ж, давайте сведем все вышеизложенное в один пример настройки MQTT-клиента.
Допустим мы хотим подключиться к публичному серверу где-то в этих ваших ыньтерьнетах, по протоколу TCP/IP с использованием SSL и авторизации. Тогда процедура инициализации может выглядеть так:
Ура! Мы сделали это! Это было непросто, но мы справились.
На самом деле, если говорить серьезно, все эти параметры нужны и важны, просто в некоторых популярных библиотеках для Arduino мы их “не видим” и не может как-то изменить. Здесь же API предоставляет вам возможность для более тонкой настройки.
Используя полученный хендл esp_mqtt_client_handle_t mqtt, мы потом сможем взаимодействовать с сервером – подписываться и отправлять сообщения. Как это сделать, мы и рассмотрим ниже. Но…
Мы только что создали, но еще не запустили, задачу MQTT-клиента, и это отнюдь не означает, что он уже подключился к серверу и уже можно начинать подписываться или отправлять сообщения. Задача пока ждет, чтобы её запустили с помощью esp_mqtt_client_start(client):
// Создаем задачу MQTT клиента
esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqttCfg);
// Запускаем клиент
esp_mqtt_client_start(client);
Но делать это пока что несколько преждевременно.
Дело в том, что вся работа клиента происходит асинхронно (это же отдельная задача FreeRTOS), и сразу после выхода из esp_mqtt_client_start() мы не можем быть уверены – есть подключение к серверу или нет. Например, он может не успеть подключиться, а может и того хуже – вообще нет подключения к сети.
Возникает извечный русский вопрос: а что делать? Можно взять муравья… А очень даже просто – необходимо создать обработчик событий MQTT-клиента, который и будет уведомлять нас о наступлении того или иного события в клиенте. Чем мы и займемся далее.
Продолжение зедсь: https://kotyara12.ru/iot/esp32-mqtt-client/