Часть 1. Знакомство с ESP32
Часть 2. Подключение к WiFi
Часть 3. Получение даты и времени
Полная статья на сайте.
4. Подключение к MQTT-брокеру
Для удаленного управления ESP32 можно использовать множество различных сетевых протоколов, например:
- HTTP / HTTPS
Протоколы для обмена данными по модели клиент-сервер, часто прячется под псевдонимом WEB-интерфейс. - MQTT
Лёгкий publish/subscribe протокол для IoT, оптимален для обмена короткими сообщениями между устройствами и сервером (брокером). - UDP
Протокол передачи данных без установления соединения, подходит для быстрой передачи небольших пакетов, но без гарантии доставки. - CoAP
Лёгкий протокол, похожий на HTTP, но оптимизированный для устройств с ограниченными ресурсами и работы в ненадёжных сетях.
Так почему я предпочитаю использовать MQTT-протокол, а не Web-интерфейс, к примеру? Ведь Web-интерфейс значительно проще? А потому, что Web-интерфейс проще только для пользователя вашей системы, но обладает значительными недостатками:
- Web-интерфейс нельзя использовать вне локальной сети без “белого IP”, проброса портов или других ухищрений. Для меня, когда я живу в режиме “5 город / 2 деревня”, а устройства есть и там и там – это является самой критичной проблемой.
- Для создания грамотного web-интерфейса вам потребуется изучить не только C/C++, но и языки разметки HTML, CSS и хотя бы немного стать web-дизнайнером.
- HTTP(S) Web-сервер заметно сильнее нагружает процессор, чем легкий и быстрый MQTT.
Разумеется, идеальных вещей в нашем мире не существует, MQTT тоже имеет свои недостатки. Например необходимость устанавливать на смартфон дополнительное приложение и настраивать его. Но лично для меня это не столь критично.
Именно поэтому в данной статье я буду рассматривать только управление через MQTT. Если вы не знакомы с этим протоколом, рекомендую вам предварительно ознакомиться со следующими статьями по данной теме:
Какие данные нам понадобятся?
Для подключения к MQTT-брокеру нам понадобятся:
- URI-адрес сервера или его IP-адрес
- Номер порта для подключения, стандартно это 1883 или 8883, но могут быть варианты
- Логин и пароль учетной записи на сервере, по именем которой мы будем получать доступ к данным
- Для защищенных SSL/TLS-соединений дополнительно потребуется корневой СА-сертификат для проверки подлинности сервера. О том, что это такое и зачем это нужно – я писал в другой статье: HTTPS, SSL/TLS соединения на ESP32 и ESP-IDF
Какую библиотеку будем использовать?
Фреймворк Arduino ESP32 , в отличие от ESP-IDF, не имеет встроенной поддержки MQTT-протокола. Посему нам придется использовать сторонние библиотеки. В предыдущей версии статьи “шаг за шагом” для ESP8266, если вы её читали, я использовал для подключения к MQTT-брокеру довольно известную библиотеку PubSubClient: https://github.com/knolleary/pubsubclient. Это, наверное, одна из самых известных и популярных библиотек среди Arduinо-сообщества. Поэтому не будем изменять традициям, и повторим этот опыт.
Но лично мне эта идея не очень нравится. Почему? Да потому что PubSubClient, на мой взгляд, плохо приспособлена для работы в составе FreeRTOS:
- Для обеспечения функционирования клиента необходимо в каждом цикле loop() и, желательно, как можно чаще, вызывать метод mqttClient.loop(). Ну а мы же теперь понимаем, исходя из раздела “Особенности программирования в многозадачной среде RTOS“, что этого лучше избегать, чтобы дать возможность работать задачам с меньшим приоритетом.
- PubSubClient совсем не рассчитана на работу в многопоточных операционных системах и никак не обеспечивает потокобезопасность. Поэтому при необходимости отправки или получения данных из разных задач придется придется самим обеспечивать защиту совместного доступа.
- PubSubClient обладает довольно скромными возможностями по сравнению с средствами, предоставляемыми “встроенным” ESP32 MQTT Client API, особенно в плане SSL/TLS подключений.
Пока вы используете PubSubClient имея в скетче только одну-единственную задачу “по умолчанию”, она, конечно же будет работать совершенно нормально. Но как только вам понадобится создать ещё одну или несколько задач и поручить общаться с брокером, то вам придется обеспечивать защиту от попыток одновременного доступа, да и “обслуживать” PubSubClient становится непонятно как.
PubSubClient будет отличным решением, если вы предпочтете отказаться от функций обратного вызова, и организовать проверку подключения к WiFi в цикле loop(), как это было описано выше в разделе 2.3.1. “Отслеживаем состояние WiFi в рабочем цикле”.
В остальных вариантах, на мой взгляд, на ESP32 гораздо рациональнее и оптимальнее использовать встроенный в ESP-IDF клиент – ESP32 MQTT Client API. Да и вообще: зачем городить огород из разных сторонних библиотек, когда Espressif давно позаботились об этом и обеспечили нам отличный API esp-mqtt для работы с MQTT, “из коробки” поддерживающий многозадачность, очередь отправки, SSL/TLS, максимально “заточенный” под ESP32 и к тому же достаточно удобный и простой. Но есть маленькая проблема.
Компонент ESP-MQTT был создан для работы в составе фреймворка ESP-IDF. И для его работы может понадобиться его конфигурирование через menuconfig. A в Arduino ESP32, как мы помним, в общем случае не поддерживается menuconfig. Но это не беда – стандартных настроек ESP-MQTT по умолчанию вполне достаточно в нашем проекте, поэтому даже не будем с этим заморачиваться.
Поэтому я решил в данной статье предложить вам оба варианта, а вы сами решите, какой вам больше подходит.
И не стоит также забывать, что кроме этого, имеется и еще множество альтернативных вариантов, например библиотеки-“обертки” ESP-MQTT, специально адаптированные под экосистему Arduino, например:
- Johboh/MQTTRemote – совместимая с Arduino (с использованием Arduino IDE или PlatformIO) и ESP-IDF (с использованием Espressif IoT Development Framework или PlatformIO) библиотека MQTT-оболочка для настройки MQTT-подключения.
- cyijun/ESP32MQTTClient – потокобезопасный MQTT-клиент для ESP-IDF или Arduino ESP32. Эта библиотека совместима с C++ arduino-esp32v2/v3+ и ESP-IDFv4.x/v5.x.
Эти обертки построены на базе “стандартного” esp-mqtt, но позволяют упростить процесс настройки. Вы можете воспользоваться этими вариантами, возможно они покажутся вам проще. Но в данной статье мы будем кушать конфеты без фантиков.
4.1. Подключение к MQTT серверу с помощью PubSubClient
Существует две популярные версии данной библиотеки (хотя может быть и больше):
Чем они отличаются? В основной версии значение QoS можно указать только при подписке. А при публикации – нет! Я считаю, что это не есть хорошо (к слову, разработчики MQTT клиента для ESP-IDF считают так же, поскольку в ESP32 реализован “по второму типу”).
Лично я предпочитаю пользоваться версией от Imroy. Но в данном примере я буду использовать основную ветку. Просто потому что во 99.9% примеров в сети используется именно она – не будем нарушать традиции.
Вначале рассмотрим вариант подключения к MQTT серверу в открытом виде, то есть без TLS-шифрования; а затем уже перейдем к чуть более сложному, защищенному соединению. О том, что это такое и зачем это нужно – рекомендую почитать в другой статье: HTTPS, SSL/TLS соединения на ESP32 и ESP-IDF, здесь я не буду подробно останавливаться на “теоретических” вопросах.
4.1.1. Подключаем библиотеку PubSubClient к проекту
Подключение библиотеки к проекту зависит от того, какую IDE вы использовали для своей работы.
Для Arduino IDE все просто: откройте менеджер библиотек, введите в поиске “PubSubClient”, и нажмите “Установка” под нужной библиотекой:
Для PlatformIO IDE ситуация выглядит несколько сложнее: вам необходимо открыть в Visual Studio Code файл platformio.ini и отредактировать его следующим образом:
При первой компиляции PlatformIO сам скачает библиотеку и подключит её к проекту:
Есть и другие варианты подключить библиотеку, подробнее об них можно почитать здесь: Подключение библиотек к проекту PlatformIO
Для ESP-IDF + Arduino как компонент ситуация самая сложная – всё придется делать “руками”:
- Откройте каталог проекта через проводник, Total Commander или любой другой файловый менеджер
- Создайте в каталоге проекта подкаталог components и перейдите в него
- Скачайте zip-архив с библиотекой (https://github.com/knolleary/pubsubclient/archive/refs/heads/master.zip) и распакуйте его в каталог components
- Переименуйте папку pubsubclient-master в просто pubsubclient (данный шаг не обязателен)
- Перейдите в каталог pubsubclient и создайте в нем текстовый файл с именем CMakeLists.txt со следующим содержимым:
4.1.2. Класс PubSubClient и с чем его едят
Вначале необходимо разобраться, что из себя представляет этот самый PubSubClient.
Конструкторов у класса довольно много, я даже не стал копировать все в текст статьи, ибо по сути нам пока интересны первые два, а точнее – второй, где мы должны передать единственный аргумент – ссылку на экземпляр WiFiClient.
Можно, конечно, при объявлении переменной (и вызове конструктора) ничего не передавать, но тогда придется это сделать потом отдельно, с помощью метода setClient(Client& client):
Раз уж мы вспомнили про setClient, наверное логично будет сразу же упомянуть и о других “настроечных” методах данного класса:
Подключение к серверу
С помощью следующей группы методов осуществляется подключение к MQTT-серверу (брокеру) – их несколько, с разным набором аргументов, вы можете использовать подходящий вам вариант:
Здесь, наверное, необходимы пояснения:
- const char* id – идентификатор клиента, должен быть уникальным в пределах одного сервера (см. примечание)
- const char* user – логин пользователя
- const char* pass – пароль пользователя
- const char* willTopic – топик Last Will and Testament (Последняя воля и завещание), в который сервер опубликует willMessage сообщение, когда клиент неожиданно отключится от сервера
- uint8_t willQos – уровень обслуживания для LWT-сообщения
- boolean willRetain – должен ли сервер хранить последнее LWT-сообщение после публикации
- const char* willMessage – содержимое LWT-сообщения
- boolean cleanSession – при подключении начинать “чистый” сеанс, то есть клиент заново возобновляет все подписки; а сервер в свою очередь, отправляет клиенту все retained сообщения, на которые он подписался
Если вам не знакомы эти термины, прошу прочитать эту статью: Что такое MQTT и с чем его едят?
Примечание 1. Во многих примерах ClientID генерируется “на лету” из MAC-адреса устройства, но я не вижу необходимости тратить на это свободную память кучи. Ибо нефиг. И задаю его как const char* – так и проще и “легче”. Только не забывайте менять его от проекта к проекту (он должен быть уникальным в пределах одного сервера).
Примечание 2. LWT можно задать только при подключении к серверу и только один раз, что противоречит спецификации протокола MQTT. Впрочем это почти во всех клиентах так.
Если по каким-либо причинам необходимо отключиться от сервера “штатно”, необходимо вызвать disconnect():
Проверить состояние подключения к серверу можно с помощью функции connected():
boolean connected();
Функционирование клиента
Поскольку PubSubClient создавался для классической однозадачной экосистемы Arduino, необходимо постоянно вызывать метод loop() для его функционирования:
Он выполняет всю внутреннюю работу по обмену данными с сервером и поддержанию соединения. По большому счету, можно создать отдельную задачу для работы PubSubClient, и поместить вызов этого метода в рабочий цикл этой задачи.
Отправка данных на сервер (публикация)
Для отправки сообщения на сервер необходимо воспользоваться одним из методов, указанных ниже:
Обратите внимание – топик и сами данные должны быть представлены как const char* или набор байт uint8_t*. Как видите, в данной реализации при отправке сообщения нельзя указать QoS, что, в принципе, противоречит описанию протокола. Я не знаю, почему автор так сделал.
Отправка осуществляется только если клиент подключен к северу, иначе данные будут отброшены.
Получение входящих данных
С помощью указанных ниже методов можно подписаться на интересующие нас топики:
Подписаться можно не только на конкретный топик, но и на шаблон топиков, например “#“.
Когда в топике, на который мы подписались, появиться новое входящее сообщение, будет вызвана функция обратного вызова, которую мы должны были предварительно зарегистрировать с помощью метода setCallback(). Этот метод должен быть создан по следующему прототипу:
Например:
Теперь мы можем попробовать подключиться к серверу, для начала без шифрования.
4.1.3. Подключение к MQTT без шифрования
Вначале необходимо подключить библиотеку к скетчу:
#include "PubSubClient.h"
Объявим константы, в которых будут храниться параметры подключения к серверу:
Ещё нам понадобятся парочка глобальных переменных:
Теперь, используя знания, полученные в предыдущем разделе, напишем функцию подключения к MQTT-серверу:
и функцию отключения:
Куда будем их добавлять? Хороший вопрос!
4.1.3.1. Неправильный мёд подход
Если вы использовали синхронный способ проверки состояния подключения к WiFi в рабочем цикле (раздел 2.3.1), то можно легко впихнуть написанные функции в wifiCheck():
Не забываем добавить обработку клиента MQTT в основной цикл скетча:
это будет хорошо и достаточно надежно работать! Вы можете использовать этот способ в этом случае, но есть одна проблемка (см. ниже)…
Но если вы использовали асинхронный подход из раздел 2.3.2 с использованием функций обратного вызова, то возникает соблазн поступить точно также:
Но тут стоит вспомнить про то, что callback вызывается из другой задачи с другим контекстом, и это может легко привести к конфликтам!
Хорошо, путь даже так, проверим это:
Вроде бы работает, но подключение к MQTT серверу происходит быстрее, чем успевает синхронизироваться время. Мы запускаем синхронизацию времени и подключение к серверу практически одновременно, а синхронизация времени требует некоторого времени… Непорядок!
Пока мы подключаемся к MQTT-серверу без TLS-шифрования, в этом нет никакой особой проблемы, ну разве что на mqtt может улететь “кривое” время запуска, например. Но как только мы попытается использовать для подключения к MQTT шифрование, да ещё и с проверкой сертификата сервера – это станет проблемой. Поэтому данную проблему лучше исправить сразу и навсегда, дабы не искать потом причину неудач.
4.1.3.2. Правильный мёд подход
Как это можно исправить?
Я могу предложить свой, но это далеко не единственный вариант, вы можете сделать лучше. Для такого простого клиента, как PubSubClient, я просто “переместил” весь код работы в основной цикл скетча loop() с проверкой всех состояний.
Для этого пишем примерно следующее:
из обработчика cbWiFiEvent() убираем всё “лишнее”, и чуть-чуть подправим рабочий цикл:
Можно проверить, все работает хорошо, даже при асинхронном подходе:
Все работает как положено и вся работа с клиентом пока осуществляется из контекста основной задачи скетча. Задача выполнена.
4.1.4. Подключение к MQTT с использованием TLS-шифрования
TLS (Transport Layer Security) соединения используются для обеспечения безопасности передачи данных между устройством и удалённым сервером. TLS защищает данные от перехвата, подделки и других угроз, которые возможны при передаче по незащищённым каналам. В частности, TLS обеспечивает:
- Шифрование передаваемых данных — чтобы никто не мог прочитать передаваемую по каналу связи информацию.
- Аутентификацию сервера — чтобы убедиться, что устройство подключается именно к тому серверу, которому доверяет (например, с помощью проверки сертификата CA).
- Целостность данных — чтобы данные не были изменены в процессе передачи.
Использование TLS рекомендуется для всех внешних коммуникаций устройства, например, при работе с MQTT или при обновлениях по воздуху (OTA), чтобы предотвратить атаки типа “человек посередине” и другие угрозы безопасности, подробнее смотри в документации: ESP32: TLS (Transport Layer Security) And IoT Devices и ESP-IDF Security Overview.
Если вы читали мою предыдущую статью HTTPS, SSL/TLS соединения на Arduino и ESP8266, то, наверное, помните, что на ESP8266 не получиться поддерживать несколько защищенных соединений одновременно. На ESP32 такой проблемы нет – вы вполне можете одновременно подключаться к нескольким ресурсам с использованием TLS, и свободная память еще останется. Кроме того, фреймворк Arduino ESP32 имеет расширенные возможности TLS по сравнению ESP8266. Поэтому можно смело рекомендовать использование SSL/TLS везде, где только можно.
Прежде чем мы продолжим, оставлю здесь ссылки на дополнительные статьи по данной теме:
4.1.4.1. Библиотека NetworkClientSecure, mbedTLS и все, все, все
В Arduino ESP32 за “безопасные” TLS-соединения отвечает библиотека NetworkClientSecure. Она реализует защищённые соединения по протоколу TLS 1.2 с использованием библиотеки mbedTLS (на уровне ESP-IDF). Её можно использовать не только для подключения к MQTT-брокеру, но и для отправки HTTPS-запросов, о чем и будет рассказано ниже.
Для проверки подлинности сервера и установки защищённого соединения можно использовать следующие методы:
- setCACert – для проверки сертификата сервера (тот ли он, за кого себя выдает),
- setCertificate и setPrivateKey – для проверки сертификата клиента (его обычно используют вместо пары логин/пароль),
- а также поддерживается работа с набором корневых сертификатов и режимом PSK (pre-shared key) для MQTT и других протоколов
Более подробную информацию можно почерпнуть из официальной документации: NetworkClientSecure. Поскольку это довольно обзорная статья, я не буду сильно углубляться в детали. Поскольку я в примере рассматриваю подключение к публичному серверу, то я и буду рассматривать только этот вариант.
Проверка сертификата сервера
Для установки защищенного соединения с сервером необходимо проверить валидность его сертификата – а тот ли это сервер, к которому мы пытаемся подключиться. Сертификат это своего рода удостоверение личности сервера. Но серверов много, хранить сертификаты для каждого сервера в интернете никто не будет – это были бы очень большие массивы информации. Поэтому на стороне клиента обычно хранят только сертификаты корневых центров сертификации, которые выпускает сертификаты для дочерних центров сертификации, а те, в свою очередь, выдают сертификаты конечным серверам.
Сертификат любого сервера и центра сертификации имеет определенный срок действия, после которого он заменяется новым. Именно для проверки срока действия нам и нужна была актуальная дата.
При установке TLS-соединения сервер предоставляет нам свой сертификат вместе со всей восходящей цепочкой сертификатов центров сертификации, включая корневой. Если корневой сертификат присутствует в нашей базе данных и мы можем его проверить и доверять ему, значит мы можем доверять и конечному серверу. На “взрослых” компьютерах для хранения списков доверенных корневых сертификатов имеется специальная “база данных”. На ESP32 тоже имеется возможность подключения набора корневых сертификатов – bundle, но в данном примере мы поступим попроще.
То есть, для корректной проверки сертификата сервера перед установкой защищенного соединения нам потребуется:
- сертификат корневого (первого в списке) центра сертификации, который в конечном итоге выдал сертификат серверу, к которому мы хотим подключиться
- корректные дата и время для проверки
Как получить файл сертификата корневого центра сертификации с помощью браузера – я рассказывал здесь: HTTPS, SSL/TLS соединения на Arduino и ESP8266.
Но срок действия любых сертификатов всегда ограничен, и спустя какое то время он станет не действителен. После этого ваше устройство перестанет подключаться к серверу через TLS-соединение. Обычно сроки действия сертификатов CA достаточно большие (десятки лет). Кстати, один и тот же центр сертификации может выдать сертификаты сразу нескольким серверам, которые вы хотите использовать – и тогда корневой сертификат понадобиться только один.
Как обновить сертификат? Обновить сертификат можно, только заменив его в скетче. И даже если вы задействуете набор сертификатов от mozilla, встроенный в ESP-IDF, его тоже придется обновлять перепрошивкой.
Но есть альтернативный вариант: использовать небезопасные безопасные соединения – setInsecure(). В этом случае клиент просто не проверяет сертификат сервера на его валидность и безусловно доверяет любому серверу. После чего устанавливает с сервером зашифрованный канал связи. То есть передаваемые данные передаются как при “нормальном” TLS-соединении, но сервер могут и подменить – “однако в процессе пути собака могла подрасти“. Зато – не нужно заботиться о сроках действия сертификата.
Как поступить в каждом конкретном случае – решать только вам! Например я сильно сомневаюсь, чтобы кто-то попытался подменить сервер MQTT. Но стараюсь, чтобы все “было по правилам”. В качестве альтернативы можно предложить такой способ: в обычном случае использовать нормальный способ (с проверкой сертификата), а в случае 3-х (5, 10…) неудачных попыток соединения попытаться подключиться без проверки сертификата, и если это удалось – отправить уведомление автору скетча о необходимости сменить сертификат.
4.1.4.2. Добавляем TLS-подключение в функцию подключения к MQTT серверу
Предполагается, что файл корневого сертификата вы уже получили. Для сервера wqtt.ru, который я использовал для проверки примеров в статье, вы найдете его содержимое немного ниже – файл вам уже не понадобиться.
Интегрировать библиотеку NetworkClientSecure в наш скетч очень просто:
#include <WiFiClientSecure.h>
Затем необходимо заменить переменную для WiFi клиента, которую используем для MQTT-клиента, на более “безопасную” версию:
Если вы хотите иметь полноценную проверку сертификата сервера, то добавим содержимое сертификата в виде строки const char:
Если вы не хотите выполнять проверку сертификата сервера, то добавлять это в скетч не обязательно.
Ну и наконец, в функции подключения к MQTT серверу перед строчкой mqtt_client.connect(...) добавим ещё одну:
Либо, если вы не хотите выполнять проверку сертификата сервера, то необходимо добавить другую команду:
И не забудьте заменить порт в настройках сервера на “безопасный”:
Этого вполне достаточно, проверим:
Вуаля, всё успешно работает. Теперь передаваемые данные зашифрованы, и сосед – хозяин роутера, через который мы выходим в сеть интернет, не сможет перехватить наши данные.
4.1.4.3. Переключение режимов сборки скетча с помощью условных макросов
Помните, я выше рассказывал, что можно управлять сборкой с помощью макросов препроцессора? Давайте провернем этот нехитрый фокус.
Объявим два макроса, с помощью первого будем управлять переключением TLS-режима, а с помощью второго – будем ли мы проверять сертификат или нет.
#define CONFIG_MQTT_USE_TLS 1 // Будем ли мы использовать mbedTLS для MQTT-подключения
#define CONFIG_MQTT_USE_SERVER_CERT 1 // Будем ли мы проверять сертификат сервера
Тогда с помощью условных макросов #if ... #endif, мы можем “насовсем” отключать или включать нужные участки кода:
Изменением двух символов (с 0 на 1 и наоборот) в макросах теперь можно легко управлять тем, каким именно способом будет осуществляться подключение к MQTT-брокеру. И зверей убивать не надо © Простоквашино. Пользуйтесь на здоровье!
4.1.5. Публикация исходящих данных на MQTT-сервере
Для публикации данных на сервер можно воспользоваться одним из методов, указанных ниже:
Обратите внимание – топик и сами данные должны быть представлены как const char* или набор байт uint8_t*. Поэтому, если вы используете динамические строки класса String, то необходимо привести их к нужному виду, например так:
4.1.6. Оформление подписки и получение входящих данных от MQTT-сервера
Если вам нужно получать какие-либо данные с других устройств или с панели управления, сразу после подключения к серверу можно оформить подписку на интересующие ваше устройство топики. Для этого воспользуйтесь методами:
Допустим, у нас есть некая переменная, для простоты так и назовем её config_value, а значения для её изменения будем ждать в топике “dzen/esp32_artuino/config“. Тогда мы можем написать такой код:
Далее, мы должны написать функцию обратного вызова, которая будет срабатывать при поступлении новых данных:
И, наконец, регистрируем её в функции mqttConnect() сразу после подключения к серверу:
Проверим работу:
Подписываться можно не на конкретный топик, а например на все топики устройства, например так: “dzen/esp32_artuino/+“, впрочем я об этом уже писал здесь: Что такое MQTT и с чем его едят?
4.1.7. LWT
В заключение модифицируем наш код так, чтобы при неожиданном отключении устройства от сервера, в топке “dzen/esp32_artuino/status” само собой волшебным образом появилось сообщение offline. Сделать это очень просто:
Вуаля! Теперь если отключить ESP32, спустя несколько секунд в топике status вместо сообщения online появиться offline. Теперь мы всегда точно знаем, включено ли наше устройство на ESP32 и можно ли им управлять.
4.2. Подключение к MQTT серверу с помощью ESP MQTT Client
В состав ESP-IDF входит довольно мощная и удобная библиотека для работы с MQTT – esp-mqtt. Можно также воспользоваться ей для работы с MQTT, хотя это и немного более сложный путь, чем описан в предыдущем разделе. Но ведь «у самурая нет цели, есть только путь». Вы же не боитесь трудностей?
Что это дает:
- Поддержка всех типов соединений (TCP, TLS, WebSocket) для подключения к серверу и множества настраиваемых параметров, полностью соответствует протоколу MQTT
- ESP32 MQTT Client запускает для своей работы отдельную задачу, поэтому из loop() вашего скетча не нужно будет тратить время на его “обслуживание” – он работает в фоне и незаметно для вас
- Полностью асинхронный программный интерфейс, можно безопасно настраивать подключение к серверу прямо в callback-е WiFi Event
- ESP32 MQTT API потокобезопасен и может совместно использоваться несколькими задачами одновременно
- ESP32 MQTT API имеет исходящую очередь отправки, поэтому сообщения не будут потеряны, даже если клиент отключен от сервера (только с QoS > 0)
- Для работы не нужно подключать WiFiClientSecure и создавать переменных типа WiFiClient – все работает на уровне netif, с любыми протоколами транспортного уровня полностью автоматически.
Из недостатков можно отметить:
- Сравнительно высокую сложность разработки кода
- Невозможность конфигурования клиента с помощью menuconfig (за исключением режима сборки “ESP-IDF & Arduino as component”), так что работать придется с настройками по умолчанию.
- Мне не удалось отключить проверку сертификата сервера. То есть вы можете использовать либо обычный TCP (без шифрования), либо “нормальный” TLS, промежуточных вариантов не дано. Это можно было бы настроить через menuconfig, но он не доступен.
Прежде всего я советовал бы вам прочитать другую статью ESP32 MQTT Client API, в которой подробно объясняется работа с этим API. В целом подходы, изложенные в ней, соответствуют тому, что можно применять в Arduino ESP32. Но… Arduino ESP32 пока поддерживает более старую версию ESP32 MQTT API, так что настроить MQTT будет даже немного проще, а в остальном ничего не изменилось. Впрочем, это может со временем измениться, и вам придется воспользоваться примерами настройки подключения из статьи ESP32 MQTT Client API.
4.2.1. Подключение к MQTT серверу
Подключаем API к проекту:
#include "mqtt_client.h"
Для общения с брокером нам понадобиться только одна переменная типа esp_mqtt_client_handle_t, её лучше инициализировать как NULL:
В данном варианте я не буду отдельно рассматривать варианты подключения с шифрованием и без оного – статья и без этого получилась слишком длинной. Дальше в примере доступны оба варианта, выбрать один из которых можно с помощью условного макроса, как это описано выше в разделе 4.1.4.3.
В библиотеке mbedTLS для варианта ESP-IDF, по умолчанию запрещено пропускать проверку сертификата сервера, поэтому придется добавлять и проверять корневой сертификат сервера:
SP MQTT API рассчитано на работу с использованием системного цикла событий, который должен быть предварительно запущен. Но нам ничего запускать не потребуется, так как тот же самый цикл событий используется драйвером WiFi и к этому моменту должен уже работать.
MQTT API имеет свой собственный класс событий – MQTT_EVENTS. Он включает в себя несколько разных идентификаторов событий, на которые мы можем по разному реагировать. или игнорировать. Для этого мы должны создать обработчик событий MQTT_EVENTS.
Я создал такой обработчик:
Как оно работает?
- При получении события MQTT_EVENT_CONNECTED (подключение выполнено) выводим сообщение в лог, публикуем состояние “online” в топике “dzen/esp32_arduino/status” и подписываемся на топик настроек “dzen/esp32_arduino/config“
- При получении события MQTT_EVENT_DATA (входящие данные) сравниваем топики и записываем новое значение в переменную
- В остальных случаях просто выводим отладочное сообщение для информации
Теперь можно написать функцию запуска MQTT-клиента. Не подключения, а именно запуска клиента!
Её можно разбить на два этапа: инициализации клиента и его непосредственного запуска. Настройка MQTT-клиента, конечно, выглядит гораздо сложнее, чем в случае с PubSubClient, зато все параметры в одном месте:
Затем создаем задачу под MQTT-клиент:
Затем, если все прошло успешно, можно уже запустить клиент:
В случае потери доступа в сеть необходимо приостановить работу клиента:
Обе этих функции можно и нужно запихнуть в обработчик событий WiFi-подключения – именно там им и место:
А вот рабочий цикл loop() мы полностью освободили для прикладных задач:
Красота!
Проверяем:
Всё работает как часы. Хотя, судя по логу, синхронизация времени прошла позже, чем подключился клиент, но это скорее всего связано с асинхронностью работы разных задач.
Код, хоть выглядит и несколько громоздким, зато надежен. Как видите, не так уж было и сложно, “не так уж и страшен ESP-IDF, как его малюют”. И вполне можно использовать из экосистемы Arduino.
Продолжение следует...