Тридцать узлов в доме, и однажды я обновил сервер: разбор телеметрии стал строже, сценарии умнее, панель красивее. Через сутки выяснилось, что четырнадцать узлов сидят на прошивке годичной давности, шлют поля в старом формате, и половина новых функций для них тихо не работает. Кто на чём — не знал никто: лестница в подвал, крыша, гараж. Обегать тридцать коробок с ноутбуком и USB-кабелем я не захотел и больше не собираюсь.
Лечение скромное, но системное: каждая телеметрия несёт поле fw с версией прошивки — fw:"1.4.0" рядом с показаниями. Десяток байтов в пакете, а сервер сразу видит, кто на чём: панель красит старые узлы, рассылка обновлений уходит по списку, а не по памяти. Структуру JSON-payload мы строили в уроке про структуру payload — здесь добавляется измерение времени: поле, которое переживёт десяток релизов и сервера, и прошивки.
Разберём semver и сравнение версий, не обманувшись строками; правило совместимости «добавляй, не переименовывай», которое спасает старые серверы от новых полей; и миграцию топиков через префиксы v1/v2 на случай, когда без ломающих изменений не обошлось.
Semver: major, minor, patch
Версия в формате semver — три числа через точку: 1.4.0. Major меняется, когда контракт ломается: переименовали поле, сменили структуру. Minor — добавили поле или команду, не тронув старое: прежний сервер обязан жить как жил. Patch — починили расчёт, ничего не меняя снаружи. Три числа — словарь между прошивкой и сервером: 1.4.0 и 1.5.0 говорят на одном языке, 2.0.0 — уже нет.
Сравнивать версии строками — ловушка, в которую я попал лично: строка «1.10.0» лексикографически меньше «1.4.0», потому что посимвольно единица после точки идёт раньше четвёрки. Панель честно считала самые свежие узлы самыми старыми и просила их обновить. Считайте числа: major умножить на десять тысяч, minor на сто, плюс patch — и сравнивайте целые. 1.10.0 даёт 10100, 1.4.0 даёт 10400 — порядок честный, и код этот одинаково короток на плате и на сервере.
Где живёт версия: константа в прошивке, в одном месте, и никак иначе. Рядом держу дату сборки; номер поднимаю руками при каждом выкладывании — скучно, зато поле fw, которое забыли поднять, врёт всем панелям сразу, и это хуже отсутствия поля. Вес копеечный: шесть-восемь символов в пакете; бюджет будущего payload целиком, если хочется знать заранее, посчитает measureJson из урока про оценку памяти документа.
Совместимость работает в обе стороны, и это стоит проговаривать: сервер 2.0 обязан читать payload узла 1.4.0 — потому что поле t никто не переименовывал. Правило «добавляй, не переименовывай» защищает не только старые серверы от новых узлов, но и новые серверы от старых узлов — второе важнее в первые месяцы после обновления, когда апгрейд прошивок ещё едет по сети. Я проверяю пару «новый сервер — старый узел» на стенде до выкладывания: пакет 1.4.0 из архива телеметрии — в новый разбор, если прошёл — сервер готов, если нет — сервер не готов, и это выясняется за минуту, а не по звонкам из кухни.
#include <WiFi.h>#include <PubSubClient.h> WiFiClient net;PubSubClient client(net); const char* WIFI_SSID = "HomeNet";const char* WIFI_PASS = "12345678";const char* BROKER = "192.168.1.50";const char* FW_VERSION = "1.4.0"; // единое место правдыconst int PIN_RELAY = 7; void publishTelemetry(float t) { char payload[96]; snprintf(payload, sizeof(payload), "{\"t\":%.1f,\"r\":%d,\"fw\":\"%s\"}", t, digitalRead(PIN_RELAY), FW_VERSION); client.publish("home/kotel/tele", payload); Serial.println(payload);} void setup() { Serial.begin(115200); pinMode(PIN_RELAY, OUTPUT); WiFi.begin(WIFI_SSID, WIFI_PASS); client.setServer(BROKER, 1883); Serial.print("firmware: "); Serial.println(FW_VERSION); // версия и в Serial при старте} void loop() { static unsigned long lastPub = 0; if (!client.connected()) { if (client.connect("kotel-tele")) { client.publish("home/kotel/fw", FW_VERSION, true); // retain } delay(2500); return; } client.loop(); if (millis() - lastPub > 30000) { lastPub = millis(); publishTelemetry(21.4); }}Поле fw едет в каждом пакете телеметрии, а при каждом connect версия уходит отдельной публикацией в home/kotel/fw с retain: панель узнаёт версию ещё до первой телеметрии, брокер хранит её между перезапусками панели. Новых префиксов я для этого не завожу — тот же префикс узла, суффикс fw; схему префиксов и цену решётки в подписках обсуждали в уроке про схему топиков.
Бюджет поля — десять-четырнадцать байтов: на ESP32 незаметно, на Uno с буфером 256 уже осознанный расход, и если место кончается, режу ключи, а не версию — экономия на ключах считалась в уроке про буфер. Сырые поля вместо JSON экономят больше, но поле fw тогда превращается в число после запятой — и теряется совместимость с сервером, который ждёт JSON. Когда какой конверт уместен — урок про payload: JSON против сырого.
Добавляй, не переименовывай
Старый сервер читает знакомые поля и игнорирует незнакомые — терпимость, которую стоит поддерживать на своей стороне разбора сознательно. Добавили diag:1 — сервер версии 1.3 пропустит его молча, ничего не сломается. Это и есть правило «добавляй, не переименовывай»: minor-изменения совместимы в обе стороны, пока вы только дописываете и ничего не трогаете из старого.
Переименование ломает: t стало temperature — сервер 1.3 читает t, не находит, пишет null или роняет разбор пакета целиком. Удаление поля ломает тем же способом, только молча. Отсюда мой порядок ухода старого поля: сначала сервер учится читать новое и игнорировать старое (minor сервера), потом прошивки переходят, и лишь через два релиза сервера старое поле выбрасывается. Пока хотя бы один живой сервер его читает — поле остаётся.
Когда без смены структуры не обойтись — это major, и топики переезжают префиксами: home/kotel/v1/tele и рядом home/kotel/v2/tele. Прошивка 2.0 переходный месяц публикует в оба, сервер переезжает на v2 своим темпом, потом прошивка перестаёт писать в v1. Двойная публикация стоит байтов, зато переезд идёт без паузы в данных: я так поднимал сеть с 1.x на 2.0 на двенадцати узлах — ноль потерянных замеров. Дисциплина миграции библиотечного этажа показана в уроке про переезд на ArduinoJson v7 — та же схема, другой уровень.
Пограничный случай — единицы измерения: яркость 0-255 стала 0-100, число то же, смысл другой. Переименование тут не спасает: сервер обязан знать, какие единицы ему привезли. Мой рецепт — соседнее поле: рядом со значением едет bright:150 и units:pwm, новые прошивки пишут units:proc — сервер смотрит на units, а не угадывает по номеру версии. Поле стоит четыре-шесть байтов и снимает целый класс «а почему у меня свет на треть, а не на полтора».
| Изменение | Версия | Совместимость | Что делаю |
|---|---|---|---|
| Починили расчёт температуры | 1.4.1 | полная | patch: поднять номер, поле fw само расскажет |
| Добавили поле diag | 1.5.0 | старые серверы игнорируют | minor: добавляй, старое не трогай |
| Переименовали t в temperature | 2.0.0 | ломающее | major: топики v1/v2, двойная публикация |
| Сменили единицы яркости 0-255 на 0-100 | 2.1.0 | ломающее по смыслу | major и новое поле с единицами |
Поле fw окупается на первой же рассылке обновлений: сервер собирает список узлов с версией меньше целевой и шлёт им команду апдейта — мне не надо помнить, где чей узел. Панель к тому же отличает «узел молчит» от «узел молчит, и он на 1.2.x со своей известной жадностью к памяти»: во втором случае первым делом смотришь в буфер. Диагностика по версии — скучный инструмент, который стреляет раз в квартал и каждый раз экономит вечер.
Инвентаризация собирается одним подписчиком: home/+/fw отдаёт версии всех узлов одним потоком, решётка не нужна, опрос узлов не нужен. Мой журнал версий живёт в таблице на сервере: узел, версия, дата последнего fw-пакета — последняя колонка сама находит узлы, которые давно не перезагружались, а значит и не подтверждали версию. А когда дойдёт до автоконфигурации панели — Home Assistant собирает устройства из discovery-топиков, и там те же правила совместимости: как генерируется такой конфиг из кода, разобрано в уроке про discovery-конфиг Home Assistant.
Чего поле fw не делает — не защищает от забывчивости автора. Неподнятый перед сборкой номер честно соврёт всем панелям. Мой минимум дисциплины: версия поднимается тем же коммитом, что и изменения, дата сборки печатается рядом, и перед выкладыванием я читаю в Serial строку firmware: — сверка занимает десять секунд и ловит «забыл» на месте.
И единый словарь полей на весь дом: у меня он — одна страница в журнале, где против каждого поля записан смысл, единицы и версия, с которой поле появилось. t — градусы Цельсия, float, с 1.0.0; diag — байт-флаг диагностики, с 1.5.0; units — строка, с 2.1.0. Словарь — то, по чему новый узел пишется за вечер без археологии по чужим скетчам: посмотрел страницу — собрал payload, который панель поймёт с первого пакета. Из трёх прошивок разного возраста словарь собирается за один взгляд, а поле fw в каждом пакете говорит, какой строкой словаря этот узел живёт.
- 1. Зачем поле fw в каждом пакете телеметрии?
- 2. Какая версия больше: 1.10.0 или 1.4.0?
- 3. Старый сервер получил payload с новым полем diag. Что будет?
- 4. Переименовали поле t в temperature в прошивке. Какая это версия?
- 5. Как переезжать на новую структуру payload без потери данных?
- Поле fw в каждом payload — копеечный вес, зато сервер видит, кто на какой прошивке, без обхода узлов.
- Semver: major — сломал контракт, minor — добавил, patch — починил; сравнение только числами по частям.
- Строка «1.10.0» меньше «1.4.0» лексикографически: major*10000+minor*100+patch и сравнивайте целые.
- Правило совместимости: добавляй, не переименовывай; удаление поля — только после двух релизов сервера.
- Ломающие изменения — major и топики v1/v2 с двойной публикацией на переходный период.
Частые вопросы по теме урока
- Зачем поле fw в каждом пакете телеметрии?
- Десяток байтов — и рассылка обновлений идёт по списку версий, а не по обходу узлов с ноутбуком. Сервер видит, кто на какой прошивке, и фильтрует узлы
- Какая версия больше: 1.10.0 или 1.4.0?
- Строка «1.10.0» лексикографически меньше «1.4.0»: сравнение по частям даёт 10100 против 10400. 1.10.0 — сравнивайте числа по частям, строкой сравнение соврёт
- Старый сервер получил payload с новым полем diag. Что будет?
- Терпимый разбор пропускает незнакомые поля: добавление безопасно в обе стороны. Поле игнорируется, остальное читается
- Переименовали поле t в temperature в прошивке. Какая это версия?
- Старый сервер читает t и не находит его: контракт сломан — major, топики v1/v2, двойная публикация. major: ломающее изменение
- Как переезжать на новую структуру payload без потери данных?
- Префиксы v1/v2 живут рядом переходный период: двойная публикация стоит байтов, паузы в данных нет. Публиковать в v1 и v2 параллельно, сервер переезжает своим темпом