Терморегулятор в моей теплице третий год живёт с меню из трёх разделов, и все три года добавление одного пункта выглядело как экспедиция: ноутбук, кабель, сборка проекта, прошивка, проверка, что ничего не отломал. Между тем само устройство менять не требовалось — менялась только картинка на экране, список названий и их порядок. Такая работа просится на сторону данных, и формат для неё напрашивается тот же, что у конфига: JSON.
Идея урока простая до дерзости: меню — тоже конфигурация. Разделы, пункты и их порядок описываются JSON-документом; плата разбирает его и листает, как листала бы свои константы. Пакет пришёл — меню стало другим, перепрошивки не случилось. У конфига с порогами, где плата живёт по присланным числам, был свой урок (arduinojson-konfig-json) — здесь по присланной структуре живёт интерфейс.
План такой: собрать дерево разделов и пунктов через createNestedArray, напечатать его serializeJsonPretty, приладить навигацию одной кнопкой с коротким и длинным нажатием — и подменить меню без единого байта в скетче. Всё это прогоняется в симуляторе: кнопка на макетке настоящая, пакет — тот, что на плате пришёл бы от сервера.
Дерево меню: два этажа и цена каждого
Структура двухэтажная: массив sections, в нём объект на каждый раздел, у объекта — имя name и массив пунктов items. В v6 это createNestedArray("sections") снаружи и createNestedObject() на каждый раздел внутри; в v7 та же кройка пишется через to<JsonArray>() и add(). Чтение по индексам — doc["sections"][0]["items"][1] — мы тренировали на массивах и вложенности, здесь ничего нового, кроме применения: список строк вместо чисел.
Цена вопроса радует. Раздел из трёх пунктов по двенадцать символов — около семидесяти байтов пакета; три раздела — под двести; документ с запасом хватает 768 байтов. Даже на Uno с её 2 КБ SRAM меню живёт свободно, а на ESP32 таких документов можно держать десяток — старое и новое параллельно, для подмены без остановки. Ограничение не в памяти, а в экране: строка на 16 символов режет длинные названия, и это лучше решить в данных — короткие пункты, — чем в коде.
Глазам на этапе сборки нравится serializeJsonPretty: тот же документ, но с отступами — два этажа дерева читаются с листа. Компактная форма остаётся каналу: между платой и сервером отступы — чистые байты впустую, на пакете в двести байтов экономия смешная, на пакете в четыре килобайта уже заметная.
#include <ArduinoJson.h> DynamicJsonDocument menu(768); void sobratMenyu() { JsonArray sections = menu.createNestedArray("sections"); const char* imena[3] = {"Vlaznost", "Svet", "Servis"}; const char* punkty[3][3] = { {"Sejchas", "Sutki", "Poliv"}, {"Vkl/Vykl", "Porog", "Grafik"}, {"Karta SD", "Zvuk", "Sbros"} }; for (int k = 0; k < 3; k++) { JsonObject razdel = sections.createNestedObject(); razdel["name"] = imena[k]; JsonArray items = razdel.createNestedArray("items"); for (int p = 0; p < 3; p++) items.add(punkty[k][p]); } serializeJsonPretty(menu, Serial); // дерево с отступами: смотрим глазами Serial.println();} void setup() { Serial.begin(9600); sobratMenyu();} void loop() {}Обратите внимание, чем стал список названий: двумерный массив строк, из которого за два цикла собирается документ. До JSON-версии этот же массив жил впрямую в скетче, и добавить четвёртый раздел значило править размеры в четырёх местах — счётчик разделов, границы циклов, массив имён, массив пунктов. Сборка через createNestedArray держит все размеры в одном месте: правка данных больше не трогает логику.
Пока sobratMenyu() вызывается из setup, мы всё ещё перепрошиваем ради каждого нового пункта — просто теперь это аккуратнее выглядит. Следующий шаг делает данные по-настоящему чужими: строку меню присылают, а не собирают. Тогда sobratMenyu() уходит на пенсию, а её место занимает deserializeJson пакета — и именно этот вариант крутится в симуляторе ниже.
| Источник меню | Как правится | Что требуется от платы |
|---|---|---|
| Константы в скетче | перепрошивкой, с ноутбуком и кабелем | ничего; меню вечно, пока не зальёшь новое |
| JSON-строка по каналу | сервером в любой момент, без остановки устройства | валидация пакета и SRAM под документ |
| Файл на SD-карте | редактированием файла на компьютере | модуль SD и чтение файла строкой |
| EEPROM-копия | никак — это запас после подмены | порядка сотни байтов ячеек |
Навигация одной кнопкой — компромисс, за который платим порогами. Короткое нажатие листает пункты, удержание дольше 700 мс — разделы: две координаты на одной ножке. Механика чистая millis: запомнили момент фронта, сравнили с порогом, флаг uzhDolgo не даёт длинному нажатию сработать как короткому на отпускании. Считать время нажатия через delay нельзя в принципе — опрос должен жить каждый проход loop, как учил урок про millis вместо delay. Порог 700 мс подобрал под руку: 400 мс ловятся случайно, секунда — уже тягомотно.
Светодиод здесь не декорация: вспышка на 80 мс при каждом переходе читается боковым зрением, когда экран не в поле зрения — руки в перчатках, глаза на растении. На реальном приборе на место вспышки встаёт щелчок зуммера; механика та же, только вывод другой. И заметьте, что сам скетч не знает ни одного названия: разделы и пункты читаются из документа по индексам — вы вправе прислать три раздела по пять пунктов, ограничения только в константах порогов.
Честная оговорка про движок: полноценное дерево с объектами внутри массива он не читает, поэтому в симуляторе пункты раздела лежат плоскими ключами m0/m1/m2 рядом с массивом sections. На плате форма из code-блока честнее — items у каждого раздела, и скетч урока от этого не меняется: разбор вы проверяете на своей строке, а не на моей.
Подмена меню: строка вместо прошивки
Канал доставки меню — любой, что умеет передавать строку: Serial с сервера, Bluetooth с телефона, файл menu.json на SD-карте — как конфиг из урока про настройки на карте. Приёмник не знает, откуда строка, и знать не должен: сначала данные, потом применение. Порядок подмены я держу такой же жёсткий, как для порогов: пришедший пакет разбирается в отдельный временный документ, проверяется целиком — разделы на месте, пункты не пустые, размер влезает, — и только потом рабочая переменная начинает указывать на новое меню. Меню, распавшееся на середине канала, не должно оставить устройство с половиной разделов.
Проверку размеров делайте до подмены, а не после: measureJson временного документа против бюджета — строка, и вы знаете, что пакет на 4 килобайта для Uno приговор, ещё до первого чтения пункта. У меня в пакете меню есть ключ rev — номер версии; он печатается в лог при каждой подмене, и через месяц переписки с «а оно точно обновилось?» этот байт окупил себя десятки раз.
Почему JSON, а не свой компактный формат из однобайтовых кодов пунктов? Потому что меню правит человек с блокнотом на объекте, а не программист с документацией. Читаемость глазам — часть интерфейса обслуживания: инженер в теплице видит пакет, в котором словами написано Poliv, и правит его без единой таблицы кодов. Экономия сотни байтов против работы с документацией — сделка, которую я не подписываю.
- Меню — структура данных: массив sections, у каждого раздела name и массив пунктов items
- Сборка: createNestedArray и createNestedObject в v6, to<JsonArray>() и add() в v7; чтение — doc["sections"][0]["items"][1]
- Две координаты на одной кнопке: короткое нажатие — пункт, удержание от 700 мс — раздел, пороги на millis
- Подмена без перепрошивки: разбор в временный документ, проверки containsKey и длины, затем переключение указателя
- serializeJsonPretty — взгляд на дерево глазами при сборке, компактная форма — для канала
- 1. Плата получила меню из JSON вместо констант в скетче. Что главное это даёт?
- 2. Кнопку удержали 800 мс при пороге 700 мс. Что сделает скетч урока?
- 3. В присланном пакете у раздела нет ключа items. Что напечатает скетч без проверки?
- 4. Что печатает serializeJsonPretty по сравнению с serializeJson?
- 5. Как безопасно подменить меню пришедшей строкой?
Частые вопросы по теме урока
- Плата получила меню из JSON вместо констант в скетче. Что главное это даёт?
- Смысл подмены данных вместо прошивки: устройство уже на объекте, а правка меню — частое событие. Память и скорость тут ни при чём, а ограничение экрана остаётся ограничением экрана. новые пункты и их порядок правятся на сервере без перепрошивки
- Кнопку удержали 800 мс при пороге 700 мс. Что сделает скетч урока?
- Удержание перешагнуло порог 700 мс — сработал переход раздела, а флаг uzhDolgo запретил считать отпускание коротким нажатием. Два действия на одном нажатии не смешиваются именно из-за флага. перейдёт в следующий раздел один раз
- В присланном пакете у раздела нет ключа items. Что напечатает скетч без проверки?
- Разбор такого пакета проходит успешно: JSON корректен, не хватает поля. Отсюда правило из урока про конфиг: containsKey и проверка длины до применения, а не после. пустоту: раздел входит, пунктов нет, экран молчит
- Что печатает serializeJsonPretty по сравнению с serializeJson?
- Pretty-форма — для человека: два этажа меню с отступами видны с листа. Каналу отступы не нужны — между платой и сервером они просто байты, поэтому в обмене живёт компактная форма. документ с отступами — читается глазами при сборке дерева
- Как безопасно подменить меню пришедшей строкой?
- Порядок «сначала все проверки, потом все изменения» пришёл из урока про конфиг: половинчатое меню хуже старого. Временный документ стоит нескольких байтов, а устройство никогда не остаётся без разделов. разобрать во временный документ, проверить целиком и только затем переключить указатель