На майских праздниках выдалось достаточно много свободного времени, потому было просто необходимо продолжить эксперимент начатый в предыдущей статье. Ранее мне удалось получить данные температуры и влажности из датчика Xiaomi, теперь же была поставлена задача научиться отправлять эти данные MQTT-брокеру.
 Для начала стоит отметить, что в код получения данных с датчиков пришлось внести несколько изменений.  После некоторого количества тестов выяснилось, что по неизвестной (хотелось сказать неисследованной, но от этого она менее известной не становится, потому оставим так) причине advertising BLE пакеты начинают разрываться. Т.е. если ранее в одном пакете приходило и имя датчика и его данные, то теперь данные приходят в отдельных пакетах. По этой причине пришлось отказаться от проверки на имя устройства: После удаления этой проверки появилась другая, можно сказать забавная проблема. Если бы мне 10 лет назад сказали, что когда я буду программировать микроконтроллер с целью получить данные температуры с датчиков по Bluetooth, а отвечать мне будет моя зубная щетка, то я бы попытался вызвать санитаров. Тем не менее, все происходило именно так. Контроллер получал пакеты от зубной щетки и принимал их за нужные датчики. Оказалось, что она тоже имеет данные в пакете с идентификатором 0xfe95. Очевидно, что данные температуры и влажности щетка сообщить не в состоянии, потому хотелось бы её пакеты игнорировать. Потому пршлось добавить проверку следующего за 0xfe95 байта (data+4): Дальнейшие тесты показали, что теперь вроде бы всё хорошо и можно продолжать дальше. Для начала нужно было установить MQTT-брокер. Чтобы долго не думать над выбором и не регистрироваться ни в каких сервисах было прнято решение использовать Eclipse Mosquitto. Установить и запустить его оказалось просто до безумия благодаря тому, что он поставляется в том числе и в виде docker-контейнера. У меня уже имелся домашний Linux-сервер (хотя запустить контейнер сейчас можно и на Windows машине), так что оставалось только набрать волшебную команду: Поскольку это пока только эксперименты, то никаких дополнительных настроек добавлять смысла нет. Достаточно всего того, что есть по умолчанию (даже авторизация не нужна).  Для просмотра данных в брокере был использован первый попавшийся клиент для Windows — MQTT Explorer. В Интернете можно найти огромное количество материала по вопросам отправки данных в MQTT-брокер с ESP32 (например, вот). Зачастую в них используется библиотека PubSubClient, которую в Arduino IDE можно установить через Library Manager. Работать с этой библиотекой относительно просто. Достаточно иметь активное Wifi подключение (про это тоже можно найти тонну руководств, например вот), а дальше все должно пройти буквально в несколько строчек: В нашем случае, всё пришлось немного усложнить, но об этом далее. Особенностью нашего подхода к получению данных с датчиков является то, что контроллер пассивно слушает всё, что они отправляют в эфир. Не очень бы хотелось потерять какие-то пакеты только потому, что контроллер в этот момент занят какой-то другой работой (например, отправкой данный по MQTT). К тому же наш котроллер имеет 2-х ядерный процессор (модули и спецификации). Потому было просто необходимо разобраться с многозадачностью. Благо программная библиотека ESP32 поддерживает FreeRTOS функции. Запустить новую задачу очень просто — достаточно вызвать функцию xTaskCreate(). Для решения текущей ситуации понадобилось 2 задачи: для работы с BLE и для работы с Wifi. Запускаются задачи простым кодом: Код задачи для работы с BLE очевиден — достаточно просто запускать сканирование с необходимым интервалом: А вот задача для работы с Wifi и MQTT получилась несколько сложнее. Казалось бы, причем тут конечный автомат, но почему-то захотелось реализовать работу с сетью именно так. Всему виной неблокирующая функция WiFi.begin(), которая требует активного ожидания подключения к Wifi с помощью функции WiFi.status() (пример). А поскольку связь с Wifi-роутером может обрываться, то нужно поддерживать возможность пересоединения. В этом случае конечный автомат позволит реализовать эту логику относительно просто. Для Arduino можно найти несколько библиотек для реализации подобных решений (например, вот или вот), но и без них реализовать нужное поведение достаточно просто. Хватит и оператора switch. Для начала был определен набор состояний автомата: Ну и к этим состояниям прилагается следующая логика переходов:
 Выглядит сложно, но в реализации всё достаточно просто. Как уже омечалось ранее достаточно иметь бесконечный цикл и оператор switch: В приведенном выше коде переменная state содержит текущее состояние, соответственно для перехода в новое состояние достаточно просто присвоить новое значение в эту переменную. В каждом состоянии приходится проверять состояние Wifi-соединения и переходить к переподключению при необходимости. Этот код можно было не дублировать, но хотелось оставить всю логику переходов внутри обработки каждого состояния. Также как можно заметить из кода, вся работа по отправке данных в MQTT-брокер происходит в функциях prepareSendBuffer() и publishEvents(). Осталось пояснить как они работают. Для того, чтобы организовать отправку данных в нашем случае необходимо организовать буфер для принимаемых по Bluetooth сообщений. Потому был создан класс Event, который хранит в себе информацию о полученном сообщении (тип сообщения, адрес устройства, значение). Сам же буффер принимаемых сообщений это просто vector указателей на объекты класса Event. Код приёма и разбора Bluetooth сообщений добавляет сообщения в этот буфер следующим кодом: Задаче отправки в MQTT-брокер остается только взять события из этого буфера и обработать. Однако, важно не забыть о синхронизации параллельно выполняемых задач, поскольку в случае использования буфера двумя задачами одновременно возможны серьезные проблемы. Для этого достаточно использовать простейший примитив синхронизации доступный во FreeRTOS — мьютекс.  Работать с мьютексом очень просто. Достаточно создать его при помощи функции xSemaphoreCreateMutex(), а далее использовать 2 функции для его занятия xSemaphoreTake() и для освобождения xSemaphoreGive(). При каждом обращении к буферу сообщений нам необходимо занимать мьютекс, что позволит нам синхронизировать обращения к нему из разных задач. Оданако, если задача будет держать мьютекс заблокированным все время отправки (которое может быть не маленьким), это приведет к тому, что некоторые принимаемые сообщения могут быть пропущены. Потому критически важно как можно быстрее освобождать буфер для записи сообщений. Для этого применяется дополнительный буфер для отправляемых событий, который заполняется функцией prepareSendBuffer(): Эта функция переписывает события в буфер отправки, а также убирает дублирующиеся сообщения для одного устройства (потому кода так много). После переписывания в буфер отправки эта функция удаляет ненужные сообщения и очищает буфер приёма. Синхронизация треубется только на этом этапе поскольку далее с буфером отправки будет работать только одна задача. Непосредственно отпавка происходит в функции publishEvents(), код который относительно прост: Функция обрабатывает сообщения в буфере отправки последовательно, формирует название MQTT топика и отправляет значение в брокер. Обработанное сообщение затем удалеяется, а буфер отправки очищается. В результате мы получаем в достаточной степени автономный модуль, который способен принимать сообщения от датчиков Xiaomi и отправлять их в MQTT-брокер. Выглядит это как-то так:  В дальнейшем данные из MQTT-брокера можно использовать как угодно (например, отображать в каком-нибудь виджете способном работать с MQTT). И надеюсь, мне удасться продолжить эксперименты и показать интересный способ работы с ними. Полный код из данной заметки можно найти на Github. UPD: В Github меня немного поправили (спасибо MikalaiR). Во FreeRTOS задачах не стоит так просто пытаться выйти из функции, потому как нужно вызывать vTaskDelete(). Вот здесь об этом подробнее. Код на Github и здесь я поправил.