Кнопку «уходя, гаси свет» в моём первом дашборде я собрал честно: три публикации подряд — home/bedroom/light, home/kitchen/light, home/hall/light. Работало, пока ламп было три. На четвёртой комнате кнопку я забыл дописать — свет в прихожей горел до утра, и увидел я это из окна двора, возвращаясь домой. Когда ламп стало восемь, кнопка превратилась в портянку из восьми публикаций, каждая — отдельный пакет со своим шансом потеряться.
Спасение лежало в устройстве протокола, о котором мы говорили ещё в уроке про устройство MQTT: топик — просто строка, и ничто не заставляет привязывать её к одной комнате. Я завёл home/all/light — «свет вообще весь» — и кнопка сжалась до одной публикации. Число с условием: пакет этой команды весит те же 50–60 байтов, что и одиночная, — арифметику веса сообщения мы считали в уроке про публикацию показаний, — а доезжает он до всех адресатов за один заход: копии по подпискам раздаёт брокер, а не дашборд. В HTTP-схеме те же восемь ламп — это восемь запросов по двести-триста байтов и восемь шансов, что последний уйдёт с опозданием от первого.
В этом уроке — групповая механика целиком: две подписки на плате, приоритет локальной команды над групповой и главная ловушка групповых топиков, на которой я однажды сжёг вечер, — команда, вернувшаяся эхом в свою же группу. И обратный вопрос сразу: групповые топики — территория команд. Телеметрию в группу я не свожу никогда: «температура всех комнат» — это не команда, а пять независимых потоков, и смешивать их в одном адресе значит ломать себе графики и пороги.
Две подписки: своё плюс групповой
На стороне платы почти ничего нового, кроме количества: каждое устройство подписано дважды — на свой командный топик и на групповой. Subscribe() вызывается два раза после каждого успешного connect: home/bedroom/light/cmd и home/all/light/cmd; напоминание про чистую сессию и место подписок в reconnect — в уроке про callback и команды. Callback остаётся один, внутри — знакомое ветвление по имени топика: пришло в cmd спальни — управляем спальней, пришло в all — всем, что у этого устройства есть. Две подписки — это и две записи в журнале брокера при каждом подключении: по ним видно, что плата сделала после соединения, без чтения её скетча.
Белый список payload здесь нужнее, чем где-либо: групповой топик слушают много устройств, и мусорное значение размножается на все. Я сравниваю payload только с ровно 1 и ровно 0 — всё прочее молча игнорируется. Отчёты о выполнении публикуются в state-ветку, и на неё не подписан ни один исполнитель. Почему это критично — история ниже, в самой дорогой ловушке урока.
Кому положено публиковать в групповой топик — вопрос с жёстким ответом в моём доме: только дашборд и автоматизации, никогда — сами устройства. Платы в группе — слушатели и исполнители, их право голоса ограничено state-веткой. Проверять payload на месте callback я умею по-разному, но основной приём — тот же, что в командном парсере через Serial: строка разбирается на части, каждая часть проходит через список допустимых значений, и только потом происходит действие.
#include <PubSubClient.h> WiFiClient net;PubSubClient client(net);const int RELAY = 7; int wantLight = -1; // флаг из callback, исполнение в loopunsigned long overrideUntil = 0; // до этого момента групповые пропускаемconst unsigned long OVERRIDE_MS = 300000; // пять минут void callback(char* topic, byte* payload, unsigned int length) { String msg = ""; for (unsigned int i = 0; i < length; i++) { msg += (char)payload[i]; } String t = String(topic); int v = -1; if (msg == "1") { v = 1; } if (msg == "0") { v = 0; } if (v < 0) { return; } // белый список: прочее в молчание if (t == "home/bedroom/light/cmd") { overrideUntil = millis() + OVERRIDE_MS; // ночник не трогать 5 минут wantLight = v; } else if (t == "home/all/light/cmd") { if (millis() < overrideUntil) { Serial.println("GRP propusk: deystvuet override nochnika"); return; } wantLight = v; }} void loop() { client.loop(); if (wantLight >= 0) { // исполнение и отчёт — не в callback digitalWrite(RELAY, wantLight ? HIGH : LOW); client.publish("home/bedroom/light/state", wantLight ? "1" : "0", true); wantLight = -1; }} // в reconnect() после каждого успешного connect:// client.subscribe("home/bedroom/light/cmd");// client.subscribe("home/all/light/cmd");Приоритет — вопрос, который я недооценил в первый же вечер групповых команд. Поставил ночник в спальне на тусклый, лёг читать, а через минуту кто-то из домочадцев нажал «включи весь свет» — групповая команда перещёлкнула мой ночник на полную. Мнение с границей: групповая команда обязана быть слабее локальной, но не навсегда, а на несколько минут — иначе «выключи всё на ночь» перестанет доставать до комнат, где свет трогали вручную днём.
Механика держится на millis: локальная команда ставит метку overrideUntil = millis() + 300000, и пять минут любые групповые для этой комнаты пропускаются с честной печатью в журнал. Границу я проверил на своей квартире: пять минут хватает для режима «читаю-сплю», а в коридоре, где свет дёргают каждые две минуты, пришлось бы сокращать до минуты. Callback обязан закончиться за миллисекунды, поэтому внутри ставится только метка, а исполнение и публикация отчёта живут в loop() — тот же принцип, что в уроке про millis вместо delay.
Проверяется override без помощников, двумя командами из консоли: mosquitto_pub в личный cmd с единицей, затем сразу групповая — ночник должен молчать и напечатать GRP propusk, а через пять минут та же групповая обязана его взять. Одна деталь, которую я сперва пропустил: метка живёт в памяти, и перезагрузка платы её сбрасывает. Это осознанный выбор, а не недосмотр: плату вручную перезагружают редко, и требовать после этого «а куда делся override» — значит усложнять то, что должно быть простым.
Маска или отдельный топик
Альтернатива отдельному топику — подписка маской: home/+/light/cmd ловит одним вызовом и спальню, и кухню, и групповой all, потому что плюс заменяет ровно один уровень иерархии. Работает, но у цены два слагаемых. Первое: устройство начинает слышать команды чужих комнат — лишний трафик и лишние сравнения в callback на каждую чужую команду. Второе: в логе брокера подписка-маска выглядит одинаково для всех плат, и по ней не видно, кто именно обязан исполнять группу. Моё правило: маски — инструмент наблюдателей и дашбордов, а исполнительные платы подписываются поимённо — в логе такой список читается как должностная инструкция.
И последнее про payload групповых: он обязан быть абсолютным. QoS 1 для команд я беру как и раньше — дубликат безвреден для реле с командой «включи» или «выключи»: то же состояние просто выставится дважды. Но «переключи» в группе запрещено: один потерянный PUBACK — и половина из двадцати ламп окажется в другом состоянии, чем до команды. Групповые топики — территория абсолютных значений; это правило я больше не нарушал после истории с прихожей из начала урока.
Сколько групп заводить — вопрос, на который я отвечаю потребностями людей, а не красотой схемы. Моя квартира живёт с двумя: home/all/light для «весь свет» и home/all/power для «выключи всё питание, ухожу» — во вторую, кроме ламп, входят реле обогревателя и насоса. Одно устройство может слушать обе группы и свою личную cmd — три подписки на плату; ветвление в callback вырастает до трёх строк, и таблица подписок живёт комментарием в начале скетча: три строки, которые спасают через полгода, когда «почему-то не сработала группа».
| Схема | Как выглядит | Когда беру |
|---|---|---|
| Один общий топик | home/all/light/cmd плюс личные cmd у каждой платы | по умолчанию для «весь свет»: одна кнопка — одна публикация |
| Группы по зонам | home/floor1/light/cmd и home/all/light/cmd рядом | дом с этажами: «выключи второй этаж» без глобальной кнопки |
| Подписка маской | home/+/light/cmd одним вызовом subscribe | дашборды и наблюдатели: им нужен весь дом, исполнять они ничего не будут |
| Групповая с override | групповой топик плюс метка времени у локальных команд | если командами пользуются люди, а не только автоматика |
- Групповая команда — одна публикация в home/all/light/cmd: один пакет 50–60 байтов, копии адресатам раздаёт брокер
- Плата подписана дважды — свой cmd плюс групповой; callback один, ветвление по имени топика, белый список payload
- Локальная команда сильнее групповой на срок override: у меня пять минут, для часто дёргаемых комнат — минута
- Команды и отчёты живут в разных ветках cmd и state: эхо-отчёт в cmd запускает бесконечную волну исполнений
- Payload групповых только абсолютный (1 или 0) при QoS 1: команда-переключатель при дубликате рассинхронизирует половину устройств
- 1. Чем одна публикация в home/all/light/cmd лучше восьми отдельных запросов дашборда к лампам?
- 2. Ночник выставлен вручную, через минуту прилетает home/all/light/cmd 1. Как не затереть настройку?
- 3. Устройство отчиталось публикацией 1 в тот же home/all/light/cmd. Что произойдёт?
- 4. Какой payload нельзя класть в групповой топик при QoS 1?
- 5. Чем плоха подписка исполнительной платы маской home/+/light/cmd?
Частые вопросы по теме урока
- Чем одна публикация в home/all/light/cmd лучше восьми отдельных запросов дашборда к лампам?
- Пакет весит те же 50–60 байтов, но он один; раздачу копий по подпискам делает брокер. Шифрование и особые подтверждения тут ни при чём — QoS вы выбираете сами. Уходит один пакет 50–60 байтов, копии раздаёт брокер одновременно всем
- Ночник выставлен вручную, через минуту прилетает home/all/light/cmd 1. Как не затереть настройку?
- Override с таймером на millis: локальная команда защищает комнату на заданный срок. Полный отказ от групповых обесценивает кнопку «всё выключить», а QoS на приоритет не влияет. Локальная команда ставит override на несколько минут, групповая в это время пропускается
- Устройство отчиталось публикацией 1 в тот же home/all/light/cmd. Что произойдёт?
- Для брокера отчёт неотличим от команды: подписчики исполнят её снова, их отчёты пойдут третьей волной. Отсюда правило веток: cmd для команд, state для отчётов. Копии уйдут всем подписчикам — начнётся вторая волна исполнений
- Какой payload нельзя класть в групповой топик при QoS 1?
- QoS 1 гарантирует минимум одну доставку и допускает дубликаты. Для абсолютных 1 и 0 дубликат безвреден, а «переключи» от дубликата меняет состояние второй раз. Команду-переключатель: дубликат перещёлкнет часть устройств обратно
- Чем плоха подписка исполнительной платы маской home/+/light/cmd?
- Плюс заменяет один уровень — под него попадают все комнаты, и плата получает чужие команды. Поимённые подписки честнее: в логе брокера видно, кто за что отвечает. Плата слышит команды чужих комнат, а в логе не видно, кто обязан исполнять группу