Введение
Симметрично публикации данных из предыдущей статьи, разберём подписку на топики — приём команд управления от Qt-приложения (или любого другого MQTT-клиента) для удалённого управления исполнительным устройством, подключённым к ESP32, например реле или светодиодом.
Концепция
PubSubClient принимает подписку через subscribe(topic), после чего все сообщения, опубликованные в этот топик любым другим клиентом сети, доставляются текущему устройству через зарегистрированную callback-функцию, вызываемую библиотекой автоматически при получении нового сообщения — внутри этой функции прошивка разбирает полученные данные и выполняет соответствующее действие, аналогично обработке команд через Serial из основного Arduino-цикла, но теперь источником команд является сеть, а не физический кабель.
Пример кода: Прошивка ESP32
// firmware/src/main.cpp — подписка на топик управления реле через MQTT
#include <WiFi.h>
#include <PubSubClient.h>
const char *WIFI_SSID = "MyHomeNetwork";
const char *WIFI_PASSWORD = "secure_password_here";
const char *MQTT_BROKER = "192.168.1.100";
const char *DEVICE_ID = "esp32_livingroom_01";
const char *COMMAND_TOPIC = "home/livingroom/light/set";
const char *STATE_TOPIC = "home/livingroom/light/state";
#define RELAY_PIN 5
WiFiClient espClient;
PubSubClient mqttClient(espClient);
void setup()
{
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
connectToWiFi();
mqttClient.setServer(MQTT_BROKER, 1883);
mqttClient.setCallback(onMqttMessage); // регистрация обработчика входящих сообщений
}
void loop()
{
if (WiFi.status() != WL_CONNECTED) connectToWiFi();
if (!mqttClient.connected()) connectToMqtt();
mqttClient.loop();
}
void connectToWiFi()
{
if (WiFi.status() == WL_CONNECTED) return;
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(500);
}
void connectToMqtt()
{
if (mqttClient.connect(DEVICE_ID)) {
// Подписка выполняется ПОСЛЕ успешного подключения, и должна повторяться
// при каждом переподключении — подписка не сохраняется автоматически между сессиями
mqttClient.subscribe(COMMAND_TOPIC);
publishState(); // сообщаем текущее состояние сразу после восстановления связи
}
}
void onMqttMessage(char *topic, byte *payload, unsigned int length)
{
// payload приходит как массив байт без завершающего нуля — явное преобразование в строку обязательно
String message;
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
if (String(topic) == COMMAND_TOPIC) {
if (message == "ON") {
digitalWrite(RELAY_PIN, HIGH);
} else if (message == "OFF") {
digitalWrite(RELAY_PIN, LOW);
}
publishState(); // подтверждаем фактическое состояние после выполнения команды
}
}
void publishState()
{
const bool isOn = digitalRead(RELAY_PIN) == HIGH;
mqttClient.publish(STATE_TOPIC, isOn ? "ON" : "OFF", true); // retain=true — см. пояснения ниже
}
Пояснения к коду
mqttClient.setCallback(onMqttMessage) регистрирует функцию, которая будет вызвана библиотекой автоматически при получении любого сообщения по любому из подписанных топиков — важно, что подписка (mqttClient.subscribe()) выполняется именно после успешного connect(), и при каждом повторном подключении после разрыва связи (внутри connectToMqtt()) подписку нужно выполнить заново, поскольку, в отличие от постоянного хранения данных в базе, MQTT-сессия и её подписки не сохраняются автоматически между разрывом и восстановлением соединения без специальной, отдельно настраиваемой персистентной сессии.
publish(STATE_TOPIC, isOn ? "ON" : "OFF", true) — третий аргумент true устанавливает флаг retain, означающий, что брокер сохраняет последнее опубликованное в этот топик сообщение и немедленно отправляет его любому новому подписчику сразу при подписке, даже если это сообщение было опубликовано до того, как новый подписчик подключился — это важно для топиков состояния (light/state): Qt-приложение, запущенное или переподключившееся позже момента изменения состояния, сразу узнает текущее состояние света, не дожидаясь следующего реального изменения.
Подводные камни
- Забытая повторная подписка после переподключения к брокеру — если код подписывается на топик только один раз при самом первом успешном подключении (а не при каждом восстановлении соединения после разрыва, как в примере), после временного сбоя сети и последующего автоматического переподключения устройство перестанет получать команды управления, даже формально оставаясь «подключённым» к брокеру — подписка должна повторяться при каждом успешном
connect(), а не один единственный раз за всё время работы устройства. - Обработка
payloadбез явного учёта того, что это просто массив байт без завершающего нуля (в отличие от привычных C-строк) — попытка напрямую интерпретироватьpayloadкак обычную null-terminated строку (например, прямойString((char*)payload)без учёта длиныlength) может прочитать за пределы реальных полученных данных, захватив случайный «мусор» из памяти после конца сообщения; явный цикл поlengthбайт, как в примере, — корректный, безопасный способ преобразования. - Отправка команды без последующего реального подтверждения её выполнения (если бы прошивка просто выполняла команду без вызова
publishState()после) — без отдельного топика состояния, обновляемого именно после фактического выполнения действия, Qt-приложение, отправившее команду «включить свет», не имеет надёжного способа узнать, действительно ли команда была получена и выполнена устройством, а не просто потерялась где-то по дороге — явное разделение команды и состояния (статья 311) и обязательная публикация состояния после каждого изменения — важная практика надёжного MQTT-проектирования. - Использование
retain=trueбез понимания его долгосрочных последствий — retained-сообщение продолжает храниться брокером даже после отключения публиковавшего его устройства, и новый подписчик получит это, возможно, уже устаревшее состояние, потенциально не отражающее реальное текущее физическое состояние устройства, если оно было выключено или заменено без явной публикации актуального состояния перед отключением — для критичных состояний стоит явно продумать механизм очистки или обновления retained-сообщений при ожидаемом отключении устройства (например, через Last Will and Testament — отдельную возможность MQTT, публикующую заданное сообщение от имени клиента при обнаружении его неожиданного отключения брокером).