Embedded

Прошивка ESP32: публикация данных датчиков в MQTT-топики

Введение

С работающим брокером (статья 312) можно перейти к реальной прошивке ESP32, публикующей показания датчика — используем DHT11 из стартового комплекта Arduino (статья 283) как знакомый пример, но теперь данные публикуются через MQTT по Wi-Fi, а не передаются по последовательному порту USB-кабеля.

Концепция

Библиотека PubSubClient — самая распространённая, простая MQTT-клиентская библиотека для Arduino-совместимых платформ, включая ESP32 — она работает поверх обычного WiFiClient (TCP-соединения) и предоставляет методы connect() для подключения к брокеру, publish() для отправки сообщения в топик, и subscribe()/обработчик callback для приёма сообщений (используется в следующей статье). Прошивка должна поддерживать оба соединения одновременно — Wi-Fi на сетевом уровне и MQTT поверх него — и явно следить за их состоянием, переподключаясь при необходимости, аналогично логике переподключения из статьи 289, но теперь применённой к сетевому, а не последовательному соединению.

Пример кода: Прошивка ESP32

// firmware/src/main.cpp — публикация показаний DHT11 в MQTT через Wi-Fi
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>

const char *WIFI_SSID = "MyHomeNetwork";
const char *WIFI_PASSWORD = "secure_password_here";
const char *MQTT_BROKER = "192.168.1.100"; // IP-адрес машины, где запущен Mosquitto
const int MQTT_PORT = 1883;
const char *DEVICE_ID = "esp32_livingroom_01"; // уникальный идентификатор конкретного устройства

#define DHT_PIN 4
DHT dht(DHT_PIN, DHT11);

WiFiClient espClient;
PubSubClient mqttClient(espClient);

void setup()
{
    Serial.begin(115200);
    dht.begin();
    connectToWiFi();
    mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
}

void loop()
{
    if (WiFi.status() != WL_CONNECTED) {
        connectToWiFi();
    }
    if (!mqttClient.connected()) {
        connectToMqtt();
    }
    mqttClient.loop(); // обязательный регулярный вызов для обработки MQTT-протокола в фоне

    static unsigned long lastPublish = 0;
    if (millis() - lastPublish >= 5000) {
        publishSensorReadings();
        lastPublish = millis();
    }
}

void connectToWiFi()
{
    if (WiFi.status() == WL_CONNECTED) return;

    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    Serial.print("Подключение к Wi-Fi");

    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    Serial.println(WiFi.status() == WL_CONNECTED ? " подключено" : " не удалось подключиться");
}

void connectToMqtt()
{
    Serial.print("Подключение к MQTT-брокеру...");
    if (mqttClient.connect(DEVICE_ID)) {
        Serial.println(" подключено");
    } else {
        Serial.print(" ошибка, код: ");
        Serial.println(mqttClient.state());
    }
}

void publishSensorReadings()
{
    const float temperature = dht.readTemperature();
    const float humidity = dht.readHumidity();

    if (isnan(temperature) || isnan(humidity)) {
        Serial.println("Ошибка считывания DHT11");
        return;
    }

    char tempStr[8], humStr[8];
    dtostrf(temperature, 4, 1, tempStr); // преобразование float в строку без использования String
    dtostrf(humidity, 4, 1, humStr);

    mqttClient.publish("home/livingroom/temperature", tempStr);
    mqttClient.publish("home/livingroom/humidity", humStr);
}

Пояснения к коду

Прошивка явно проверяет состояние обоих уровней соединения на каждой итерации loop() — WiFi.status() != WL_CONNECTED для сетевого уровня и mqttClient.connected() для MQTT-уровня поверх него — и переподключается при необходимости, что критично для устройства, развёрнутого автономно и не имеющего возможности быть вручную перезапущенным при временном сбое сети. mqttClient.loop() — обязательный регулярный вызов, аналогичный важности server.handleClient() из статьи 310 — без него библиотека не обрабатывает входящий трафик MQTT-протокола (включая поддержание соединения через keep-alive), и соединение через какое-то время будет считаться брокером разорванным.

dtostrf() — стандартная для Arduino-платформ функция преобразования числа с плавающей точкой в строку без использования более ресурсоёмкого класса String — publish() принимает обычную C-style строку (const char*), и для платформ с ограниченной памятью (хотя у ESP32 её заметно больше, чем у Arduino UNO) такой подход остаётся хорошей практикой, минимизирующей фрагментацию динамической памяти при частых вызовах.

Подводные камни

  • Совпадающие идентификаторы клиента (DEVICE_ID) у нескольких разных устройств, подключающихся к одному брокеру — MQTT-брокер обычно не позволяет двум клиентам с одинаковым ID быть одновременно подключёнными, и при подключении второго устройства с тем же ID брокер обычно принудительно отключает первое, ранее подключённое — для проекта с несколькими устройствами (как в итоговом мини-проекте статьи 320) каждое должно иметь явно уникальный идентификатор.
  • Слишком частая публикация показаний без реальной необходимости (например, каждую секунду для медленно меняющейся температуры) создаёт избыточную нагрузку на сеть и брокер при масштабировании до множества устройств — частота публикации должна соответствовать реальной скорости изменения измеряемой величины и практической потребности приложения в свежих данных, а не быть выбрана произвольно максимально часто «для надёжности».
  • Отсутствие проверки кода ошибки при неудачном подключении к MQTT (mqttClient.state() возвращает числовой код, требующий сверки с документацией библиотеки для понимания конкретной причины — неверный адрес брокера, отказ в подключении, проблема сети) — без логирования и анализа этого кода диагностика проблем подключения при разработке и отладке устройства существенно усложняется, особенно при проблемах, проявляющихся только в реальных, не лабораторных условиях эксплуатации.
  • Использование незащищённого, не аутентифицированного подключения к брокеру (как в этом базовом примере, продолжающем простую конфигурацию Mosquitto из статьи 312) в реальной домашней сети, не полностью изолированной от внешнего доступа — даже для домашнего проекта стоит хотя бы минимально задуматься о безопасности подключения (статья 318), особенно если в топики передаются команды управления реальными физическими устройствами, а не только показания неконфиденциальных датчиков.