Введение
С работающим брокером (статья 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), особенно если в топики передаются команды управления реальными физическими устройствами, а не только показания неконфиденциальных датчиков.