Третья плата приехала на дачу в августе: термометр в погребе дополнил теплицу и мастерскую, и моя пара из первого урока превратилась в сетку из трёх точек — по-прежнему без единого роутера. Показания при этом нужны были не на плате, а в телефоне, а телефон живёт в домашнем WiFi. Так появилась четвёртая роль: шлюз, плата, которая одной стороной слушает ESP-NOW, а другой держит связь с роутером и складывает показания в MQTT.
Урок о том, как из пар собирается звезда: кто спит, кто держит сеть, почему broadcast не гарантирует доставки и какая канальная дисциплина спасает от немой гибели всей сети. Радио симулятор не эмулирует — звезда рисуется моделью в Serial с пометкой [model]. Полный конвейер показания от датчика до облачного графика разобран в уроке про IoT-датчик; здесь — участок между датчиками и роутером.
Звезда, лимит пиров и broadcast
Топология, которая работает годами: узлы-датчики живут на батарейках, спят глубоким сном — бюджет микроампер и таймер пробуждения разобраны в уроке про deep sleep, — просыпаются, отправляют один пакет шлюзу и снова засыпают. Шлюз всегда на питании: принимает уникаст-пакеты, ведёт журнал давности, переправляет показания дальше. Связи между узлами нет вовсе — они друг про друга не знают, вся разводка идёт через шлюз: для датчиков это честная цена простоты.
Роль узла скучна до идеала: плата, батарейка, датчик и три поля в пакете. Период сна выбирается от задачи: грунт в горшке меняется за часы — сон на десять минут; протечка под мойкой — сон на полминуты и полный бодрый цикл. Каждый просыпающийся узел проходит один и тот же ритуал: WiFi.mode(WIFI_STA), инициализация ESP-NOW, один send, сон. Всё вместе укладывается в сотни миллисекунд, остальное время чип проводит в микроамперах — поэтому узел с датчиком и тремя батарейками живёт сезон, а не неделю.
Второй режим — broadcast: отправка на адрес FF:FF:FF:FF:FF:FF, которую слышат все на канале. Он удобен тикером от шлюза: время, запрос показаний, смена режима — одним пакетом вместо двадцати персональных. Broadcast тоже добавляют в таблицу пиров как обычную запись, но статус из колбэка получает другой смысл: SUCCESS значит «пакет ушёл в эфир», а не «все услышали» — квитанций при broadcast нет, кто спал или был за дальностью, тот пропустил. Таблица пиров держит около двадцати адресатов, шифрованных меньше; звезды на пятьдесят-сто узлов без шифрования — рабочая практика чужих проектов, мои семь узлов живут в таблице с запасом.
#include <WiFi.h>#include <PubSubClient.h>#include <esp_now.h> typedef struct { uint8_t id; // номер узла: 2 = теплица, 3 = погреб... int16_t temp10; // температура x10 uint8_t battery; // процент батарейки} Paket; const char *SSID = "DachaNet";const char *PASS = "superParol123";WiFiClient net;PubSubClient mqtt(net);unsigned long poslednij[4]; // давность последнего пакета узла void prinyato(const uint8_t *mac, const uint8_t *buf, int len) { if (len != sizeof(Paket)) return; Paket p; memcpy(&p, buf, sizeof(Paket)); poslednij[p.id] = millis(); float t = p.temp10 / 10.0; char tema[48]; char znach[24]; snprintf(tema, sizeof(tema), "dom/uzel%d/temp", p.id); snprintf(znach, sizeof(znach), "%.1f", t); mqtt.publish(tema, znach); Serial.print("uzel "); Serial.print(p.id); Serial.print(": publish "); Serial.println(tema);} void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); // канал станет каналом роутера! WiFi.begin(SSID, PASS); while (WiFi.status() != WL_CONNECTED) { delay(400); Serial.print("."); } Serial.println(WiFi.localIP()); mqtt.setServer(IPAddress(192, 168, 1, 50), 1883); esp_now_init(); esp_now_register_recv_cb(prinyato);} void loop() { mqtt.loop(); // раз в минуту: кто не писал дольше пяти минут — тревога в брокер}Разбор. Пакет у всех узлов один — раскладка с id в первом байте, тема в брокере собирается из номера узла, и вся карта тем живёт в одном snprintf. Это продолжение схемы тем из раздела MQTT, которую я разбирал раньше: шлюз не выдумывает свою адресацию, а переводит радиопакеты в уже понятную. Обратная дорога — команды от дома к узлам — тоже через шлюз: тикер broadcast для всех и точечный уникаст конкретному узлу, например перевод в радиопакет команды с аргументом из того же раздела.
Статистика — то, за что звезда получает глаза. От каждого узла шлюз помнит время последнего пакета: узел молчит дольше, чем обещал при пробуждении, — повод для тревоги в брокере, а не тихого ожидания. Статусы колбэка я складываю в счётчики: рост промахов показывает деградацию эфира раньше, чем пропадут показания. У меня шлюз раз в час печатает три числа — отправлено, доставлено, промахов, — и по их наклону я узнаю о разряде батарейки узла раньше самого узла.
Обратные команды живут по короткому правилу: некритичные — в broadcast, критичные — уникастом и с повтором в следующем цикле. У узла нет очереди входящих: проснулся, услышал тикер, исполнил, уснул — пропущенный broadcast не восстанавливается. Поэтому смену периода сна я кладу в каждый тикер, пока узел не подтвердит её первым же пакетом: повтор по циклам стоит нуля и делает сеть самовосстанавливающейся, а шлюз заодно видит по подтверждению, что узел жив.
Отдельное слово о версиях пакета. Раскладку Paket не меняйте в одиночку: новое поле только у узлов — и шлюз читает смещения мимо, поле только у шлюза — узлы шлют старый формат, а len-проверка молча отбрасывает. Держите определение структуры в одном файле на все платы, а при неизбежной смене поднимите номер версии прямо в пакете: байт ver, и шлюз на переходный месяц принимает обе раскладки. Про шифрование честно: включать PMK/LMK стоит, когда по эфиру идут команды actuators — закрыть чужой гараж; для термометров в огороде у меня шифрование выключено и таблица пиров потому вместительнее. Живой пример: в четвёртый узел добавил влажность — новым узлам ver=2, шлюз месяц принимал обе раскладки по байту версии, старые узлы перепрошились без спешки, и ни одно показание по дороге не пропало.
Канал решает: грабля, которая убивает сеть молча
Теперь главная грабля кластера. Пока шлюз не подключён к роутеру, вся компания живёт на канале 1 — совпадение бесплатно, как в паре из первого урока. Но WiFi.mode(WIFI_STA) плюс WiFi.begin переводят шлюз на канал роутера — у моего дома это 6. Узлы остались на 1, шлюз слушает на 6, и звезда умирает без единой строки в логе: пакеты уходят, промахов в колбэках нет, приёмник молчит. У меня этот вечер занял два часа и одну фразу с форума: ESP-NOW общается только внутри одного канала.
Дисциплина, которая лечит: канал роутера фиксируется в его настройках, чтобы не гулял при перезагрузках; узлы знают этот канал заранее и задают его в поле channel у записи пира — пакет уйдёт на нужном канале; приёмник обязан слушать тот же. Проверка за пять минут: печать WiFi.channel() на шлюзе и у узла, цифры должны совпасть. Третья роль WiFi у шлюза — отладочная: на даче я поднимаю на нём собственную точку доступа рядом со станцией, как в уроке про AP и STA вместе, и конфигурирую узлы телефоном без роутера вовсе.
Проверка канала заводится в каждую плату разом: при старте печатаю строку загрузки с WiFi.channel() и MAC — на шлюзе, у узлов, везде. Один взгляд на три окна монитора — и рассинхрон виден раньше, чем звезда успеет замолчать. Сам канал и раскладку пакета я держу константами в общем заголовочном файле проекта: единственная копия на все платы, менять — сразу у всех.
| Сообщение | Кому | Квитанция | Зачем в моём проекте |
|---|---|---|---|
| уникаст узел → шлюз | одному адресату | статус в колбэке | основной поток показаний раз в цикл сна |
| broadcast шлюза | всем на канале | только «ушёл в эфир» | тикер времени и запрос показаний всем сразу |
| уникаст шлюз → узел | одному узлу | статус в колбэке | команда конкретному узлу: период, сброс счётчика |
Итог: из пар ESP-NOW собирается звезда, где узлы спят и шлют один пакет, а шлюз держит канал роутера и переводит радиопакеты в MQTT. Broadcast разгружает тикер, но не даёт квитанций; уникаст даёт честный статус; таблица пиров ограничивает адресатов, шифрованных — сильнее. И всё держится на канале: едином, зафиксированном, известном каждому участнику.
Домашнее задание: добавьте к звезде четвёртый узел и неделю собирайте таблицу давности последнего пакета. Энергобюджет узла посчитайте заранее — батарейка и сон расписывают миллиамперы, а счётчики между снами удобнее держать в RTC-памяти.
- 1. Шлюз подключили к роутеру — звезда замолчала. Первая версия?
- 2. Колбэк после broadcast вернул SUCCESS. Что это гарантирует?
- 3. Куда шлюз переправляет показания узлов?
- 4. Сколько адресатов держит таблица пиров без шифрования?
- 5. Кто в звезде обязан держать постоянную связь с роутером?
- Звезда: узлы спят и шлют один уникаст-пакет, шлюз всегда в сети и переводит пакеты в MQTT
- Broadcast FF:FF:FF:FF:FF:FF слышат все на канале; SUCCESS в колбэке — «ушёл в эфир», не «услышали»
- Таблица пиров — около двадцати адресатов, шифрованных меньше; звёзды на десятки узлов ходят без шифрования
- ESP-NOW живёт в одном канале: шлюз в WiFi меняет канал — фиксируйте канал роутера и прописывайте channel у пиров
- Глаза сети — статистика: давность последнего пакета узла и счётчики SUCCESS/FAIL из колбэка
Частые вопросы по теме урока
- Шлюз подключили к роутеру — звезда замолчала. Первая версия?
- WiFi.begin переводит шлюз на канал роутера, а ESP-NOW живёт только внутри одного канала. Сверьте WiFi.channel() на шлюзе и у узлов — это мой первый чек. Канал шлюза сменился на роутерный, узлы шлют мимо
- Колбэк после broadcast вернул SUCCESS. Что это гарантирует?
- При broadcast SUCCESS означает лишь отправку в эфир: спящий или далёкий узел пакет пропустит без каких-либо отметок в колбэке. Пакет ушёл в эфир на канале
- Куда шлюз переправляет показания узлов?
- Тема собирается из номера узла: dom/uzel2/temp и далее. Узлы не знают про брокера ничего — их мир ограничен радиопакетом до шлюза. В темы MQTT-брокера по номеру узла
- Сколько адресатов держит таблица пиров без шифрования?
- Протокол держит порядка двадцати пиров; каждая шифрованная запись съедает ресурс ключей, поэтому за плотными сетями ходят без шифрования. Около двадцати, шифрованных меньше
- Кто в звезде обязан держать постоянную связь с роутером?
- Узлы живут на батарейках и видят только шлюз. Роутер, брокер и остальной мир — забота шлюза, всегда включённого в розетку. Только шлюз — узлы спят и говорят раз в цикл