К третьему году умного дома у меня накопилось одиннадцать плат, и я однажды решил собрать все температуры одним запросом: mosquitto_sub с маской home/+/temp, где плюс подставляет любой уровень. Увидел два значения из пяти. Остальные три термометра жили в топиках temp, kitchen-temp и eshome/8CA2_temp — каждый по-своему правильных, потому что скетчи я писал в разное время и каждый раз называл вещи по-новому.
Маска — беспощадный инструмент: она сопоставляет строки, а не смысл. home/+/temp ловит ровно три уровня: home, что угодно, temp. kitchen-temp — один уровень вместо трёх, temp — вообще один, а MAC-вариант eshome/8CA2_temp спрятал и место, и тип в одном уровне с нижним подчёркиванием. Пока плат три, расхождение живёт в голове; на одиннадцати оно расползлось по дашбордам, и каждая новая автоматизация начиналась с получаса археологии по логам. Я тогда нарисовал на бумаге дерево всех одиннадцати плат и ужаснулся: три схемы именования в одном доме — по месту, по устройству и по MAC — мирно сосуществовали и мешали друг другу.
В уроке про устройство протокола MQTT топик назван API дома — и это не метафора: структура имён решает, какие вопросы вы сможете задать дому одной маской. Здесь я разберу правила именования, которые выработал на своих граблях, антипаттерны, в которых погорел лично, и схему, которая пережила рост с пяти плат до двадцати без единого переименования.
Анатомия: уровни, слэши, регистр
Топик — строка UTF-8, уровни в ней разделяет слэш: home/livingroom/temp состоит из трёх. Первое правило — строчные буквы латиницей: temp и Temp — два разных топика, брокер сравнивает байты, а не смысл, и дашборд честно нарисует два ряда вместо одного. Формально протокол разрешает кириллицу, но я попробовал один раз и хватило: в консоли такое имя приходится брать в кавычки и надеяться на локаль, часть веб-инструментов кодирует его в проценты, а переключение раскладки превращает опечатку в квест на вечер. Мой алфавит: строчные буквы, цифры, дефис. Подчёркивание тоже безопасно для брокера, но я его не люблю: в консоли его не видно, и soil_1 рядом с soil-1 из соседнего скетча выглядит как два одинаковых топика — а это два разных.
Второе правило — слэш только как разделитель. Ведущий слэш создаёт пустой первый уровень: /home/temp — это уровни пустой, home, temp, и маска home/# его не увидит; я поймал это на собственном скетче, когда из красивости приписал слэш в начале — полвечера думал, почему брокер не видит плату, а в логе всё было идеально. Хвостовой слэш делает пустой последний уровень — формально валидно и бесполезно. Дефис внутри уровня безопасен и читаем: soil-1, soil-2 — так я нумерую экземпляры.
Третье — длина. Имя топика едет в каждом пакете публикации, поэтому я держу его в пределах тридцати — сорока символов: home/livingroom/temp — девятнадцать байтов, и при периоде в десять секунд это восемь тысяч шестьсот повторов имени в сутки — арифметику периодов мы считали в уроке про публикацию по millis. Локальной сети всё равно, а плате на батарейке с GSM каждый байт имени умножается на число сообщений и превращается в статью расходов.
| Плохой топик | Чем плох | Хороший |
|---|---|---|
| temp | нет ни места, ни типа: через полгода не вспомнить, чей он; два таких с разных плат конфликтуют в одном дашборде | home/livingroom/temp |
| /home/livingroom/temp | ведущий слэш: пустой первый уровень, маски home/# и home/+/temp топик не ловят | home/livingroom/temp |
| Датчик температуры спальни | кириллица и пробелы: боль в консоли, проценты в URL, опечатки с раскладки | home/bedroom/temp |
| home/livingroom/temp/C | единица отдельным уровнем: маска home/+/temp остаётся с носом | home/livingroom/temp — единицы живут вне payload |
#include <PubSubClient.h> WiFiClient net;PubSubClient client(net);String rooms[3];unsigned long lastPub = 0; void publishTemp(String room, int t) { String topic = "home/" + room + "/temp"; client.publish(topic, String(t));} void setup() { Serial.begin(9600); WiFi.begin("HomeNet", "12345678"); while (WiFi.status() != WL_CONNECTED) { delay(300); } rooms[0] = "livingroom"; rooms[1] = "bedroom"; rooms[2] = "balcony"; client.setServer("192.168.1.50", 1883); client.connect("esp32-livingroom"); Serial.println("shema: home/room/temp");} void loop() { client.loop(); unsigned long now = millis(); if (now - lastPub >= 10000) { lastPub = now; for (int i = 0; i < 3; i++) { publishTemp(rooms[i], 15 + random(0, 10)); } }}Масштаб: от трёх плат до двадцати
Именование по месту (home/livingroom/temp) и по устройству (esp32-13/temp) — два лагеря, и у меня на этот счёт мнение с границей: до десятка плат ведите схему по месту, потому что дом спрашивает «что в гостиной», а не «что у платы номер тринадцать». Когда платы множатся и переезжают между комнатами, добавляйте тип и номер экземпляра: home/livingroom/light-1/state, home/livingroom/light-2/state. MAC-адрес в имени — рабочий вариант из сообщества ESP8266 (eshome/8CA2_temp): уникальность бесплатна, но маска home/+/temp такой топик не поймает, а через полгода вы не вспомните, где физически живёт 8CA2.
Отдельный вопрос — глубина. Двух уровней мало: home/temp не отличает комнаты. Пяти — уже тесно для человека: в четвёртом уровне я однажды забыл слово и получил топика-призрака, который никто не узнаёт. Моя рабочая глубина — три-четыре уровня: дом, комната, тип, при необходимости номер экземпляра. Каждому уровню — свой смысл и никогда два смысла в одном уровне: склеенное подчёркиванием место-тип маской уже не разлепить.
Команды и факты разводите суффиксами: light/set — сюда пишут желания, light/state — сюда устройство публикует факт. В уроке про subscribe и callback мы разбирали, почему устройство обязано подтверждать исполнение отдельным топиком: set — просьба, state — правда. Если в планах Home Assistant, загляните заранее в его конвенции имён: переименование топиков на живом доме — вечер археологии, я проходил.
И главное — карта топиков. У меня это секция в README: строка на устройство — топик, комната, плата, дата прошивки. Новое устройство я не придумываю, а вписываю в схему; сломавшееся — нахожу по топику за секунды. Проверка схемы помещается в одну строку: mosquitto_sub с маской на настроенном брокере — если под home/+/temp видны все термометры, схема живая; если нет, у вас не схема, а набор привычек.
Проверка match в мониторе — упрощение настоящей работы брокера с масками: плюс подставляет ровно один уровень, решётка — сколько угодно, и обе работают только в подписке, не в публикации. Принцип тот же, что в основах pub-sub: маска не угадывает намерения, она сопоставляет строки. Один лишний уровень — и устройство выпадает из всех дашбордов молча, без единой строчки в журнале.
Чек-лист перед вшиванием топика у меня теперь короткий: строчные латинские буквы, ни одного лишнего слэша, место и тип на своих уровнях, имя короче сорока символов, строка в карте топиков. Двадцать плат проходят его за вечер. Обратная миграция с хаоса на схему заняла два вечера: каждую плату пришлось перепрошивать и заново подключать к дашборду — дешевле сразу.
И про честность названий: топик обещает ровно то, что внутри. livingroom, который на самом деле коридор, жил у меня полгода — плату перенесли, а топик остался. В конце концов я вписал в карту топиков колонку «где физически», и она спасла больше нервов, чем вся схема. Схема — это не наказание, а память: устройство без читаемого адреса в логе — устройство без паспорта.
- Топик — API дома: уровни место-тип-экземпляр решают, какие вопросы дому можно задать одной маской
- Регистр важен: temp и Temp — разные топики; мои правила — строчные ASCII, дефис внутри уровня, слэш только разделитель
- Ведущий слэш создаёт пустой уровень, маски его не видят; $-топики под маски-подписки не попадают
- Команды и факты разводите суффиксами: set — просьба, state — факт от устройства
- Держите карту топиков в README и проверяйте схему маской в консоли до вшивания в плату
- 1. Плата публикует в /home/livingroom/temp — с ведущим слэшем. Что получит подписчик с маской home/+/temp?
- 2. Почему имя eshome/8CA2_temp неудобно, хоть и уникально?
- 3. Что вернёт проверка по маске home/+/temp для имени home/livingroom/temp/extra?
- 4. Куда, по моему правилу, публикуется факт, а куда просьба?
- 5. Плата на батарейке с GSM публикует раз в 10 секунд. Зачем я режу имя топика до тридцати — сорока символов?
Частые вопросы по теме урока
- Плата публикует в /home/livingroom/temp — с ведущим слэшем. Что получит подписчик с маской home/+/temp?
- Ведущий слэш создаёт пустой первый уровень, а маска требует на этом месте слово home. Строки не совпали — брокер не доставит ничего и не пожалуется. Ничего: пустой первый уровень не совпадает с home
- Почему имя eshome/8CA2_temp неудобно, хоть и уникально?
- Маска подставляет один уровень между двумя слэшами, а здесь и место, и тип сидят в одном уровне. Уникальность без структуры ломает групповые подписки. Маска home/+/temp его не поймает: место и тип склеены в один уровень
- Что вернёт проверка по маске home/+/temp для имени home/livingroom/temp/extra?
- Плюс — ровно один уровень: home, любой, temp. Четвёртый уровень extra выводит имя из-под маски. Для многих уровней есть решётка, и то только в конце. Ноль: уровней больше трёх, а плюс подставляет ровно один
- Куда, по моему правилу, публикуется факт, а куда просьба?
- set — входящий канал желаний, state — публичная правда устройства. Дашборд читает state, а не догадывается, исполнился ли set. Просьба в set, факт в state
- Плата на батарейке с GSM публикует раз в 10 секунд. Зачем я режу имя топика до тридцати — сорока символов?
- Топик — часть каждого PUBLISH. На локалке это неважно, на тарифицируемом канале сорок лишних байтов на сообщение — восемь с лишним мегабайтов в месяц только имён. Имя едет в каждом пакете: байты умножаются на восемь тысяч шестьсот сообщений в сутки