Конфиг поливалки в ключах и значениях жил спокойно, пока у меня не появилась вторая зона полива. Порогов стало два, потом интервалов тоже два, потом каждой зоне завели флаг ручного режима. В файле выросли zone1_threshold=700, zone2_threshold=450, zone1_interval=10, zone2_interval=25 — имена-конструкторы, парсер оброс if-ами на каждое имя, а сосед однажды поправил не тот интервал — и одна зона поливалась по режиму другой неделю. Формат упёрся в потолок: у данных появилась структура — зоны, вложенные блоки, массивы, — а ключ-значение структуры не держит.
JSON закрывает это без имён-конструкторов: массив zones, в нём объекты по зонам, у каждого свои threshold и interval — один уровень вложенности, и десять параметров перестают путаться. Разбор берёт на себя ArduinoJson — мы уже гоняли его на ответах серверов в уроке про первый разбор, здесь подключаем к нему файл с карты.
План: цепочка от чтения файла до переменных с валидацией на трёх ступенях, изоляция мусора, цена RAM на Uno — и честное сравнение с key=value, чтобы понять, когда JSON оправдан, а когда избыточен.
От файла к переменным: три ступени валидации
Цепочка короткая. Открыли config.json в FILE_READ — и передали дескриптор прямо в deserializeJson(doc, file): парсер вычитает поток сам, и строка целиком в оперативке не появится — на Uno это экономия в килобайты. Результат — код разбора: Ok — едем дальше, NoMemory — документ мал, InvalidInput — файл вообще не JSON. Проверка кода не обсуждается: битый файл оставляет документ пустым, и каждое чтение поля молча вернёт ноль — все шесть кодов мы разобрали в уроке про диагностику DeserializationError.
Вторая ступень — containsKey. Поле есть — читаем и досматриваем; поля нет — дефолт и строка в Serial. На плате есть и более короткая запись: оператор | подставляет замену для отсутствующего поля, мы разбирали его в уроке про чтение полей. Я всё равно пишу containsKey: печатная телеметрия «поле отсутствовало» стоит одной строки, а искать, почему порог съехал до нуля, — половины дня. Разница между «поле равно нулю» и «поля нет» в конфиге стоит того, чтобы её различать.
Третья ступень — диапазоны, и это частая дыра. JSON гарантирует структуру, но не смысл: interval=-5 разберётся без единой жалобы и поедет прямо в таймер, threshold=9999 пройдёт как ни в чём не бывало. Все ограничения из прошлого урока про key=value остаются на месте: интервал не меньше секунды, порог в границах, подрезка с печатью. Правило трёх ступеней: код разбора, обязательные поля, диапазоны — мусор ловится на входе, а не в середине работающего устройства.
Ступени проверяют в порядке дешевизны: код разбора — одна переменная, containsKey — поиск по документу, диапазоны — сравнение чисел. И печатайте причину отказа поимённо: нет поля, вне диапазона, не разобралось — на объекте эти строки в Serial экономят поездку к ноутбуку. Документ после разбора живёт до следующего deserializeJson — свежий конфиг в тот же doc затирает старые ключи без всякого clear(), как почтовый ящик из урока про повторный разбор.
| Критерий | key=value | JSON |
|---|---|---|
| Вложенность и массивы | нет: имена-конструкторы zone1_interval | да: зоны как объекты, поля читаются по цепочке |
| Валидация | руками: проверка каждой строки | код разбора + containsKey + диапазоны |
| RAM на разбор | ноль сверх строки | 512-1024 байта под документ на Uno |
| Файл глазами человека | короткий и плоский | длиннее, зато структура видна |
| Цена опечатки | строка мимо — парсер молчит | битый JSON — код разбора назовёт причину |
Цена RAM: документ живёт в куче
DynamicJsonDocument doc(768) — просьба выделить 768 байт под рабочий документ, и берутся они из кучи malloc-ом: на Uno из тех же 2048 байт, из которых глобальные переменные и стек уже отщипнули своё. Мой рабочий коридор — 512-1024 байта: doc(512) проглатывает плоский конфиг из шести-восьми полей, doc(1024) — конфиг с массивом из пары зон. Динамический документ чуть медленнее статического — malloc и free стоят тактов, — но конфиг читается один раз при старте, и разница умещается в микросекунды. Половина RAM под документ — это много, поэтому документ объявляют внутри функции loadConfig: после выхода из неё куча возвращается скетчу.
Ёмкость фиксирована: документ не растёт по потребности. Не хватило — deserializeJson вернёт NoMemory, и ловится он как любой другой код разбора. Прикинуть запас можно measureJson(doc): вернёт длину будущего JSON в байтах. Впритык к ёмкости — документ мал, потому что парсер держит и значения, и имена ключей, и служебные узлы, которых в файле не видно.
Отдельно про String: файл на пару килобайт, прочитанный целиком в String, съест столько же в RAM, а String при росте копирует себя заново и фрагментирует кучу — на Uno это заканчивается обрезанными строками и падениями, о чём мы говорили в уроке про экономию RAM макросом F. Отсюда правило: отдавайте File прямо в deserializeJson; String — только для маленьких файлов, которые вы контролируете.
Почему для конфига беру динамический документ, а не статический на стеке: loadConfig вызывается из setup, и стек ещё неглубокий, но у меня есть проекты, где конфиг перечитывается по кнопке из глубины основного цикла — там 768 байт на стеке рискуют столкнуться с локальными переменными, и проявляется это случайными числами вместо порогов. Куча освобождается точно по выходу из функции, стек не раздувается — динамический документ за такт-другой на malloc окупается предсказуемостью.
#include <SPI.h>#include <SD.h>#include <ArduinoJson.h> const int chipSelect = 10; int interval = 5;int threshold = 600;int zoneCount = 1;String deviceName = "polivalka-1"; void loadConfig() { File f = SD.open("config.json", FILE_READ); if (!f) { Serial.println("config.json net - defolty"); return; } DynamicJsonDocument doc(768); // 768 байт из кучи, живут до return DeserializationError err = deserializeJson(doc, f); // поток прямо в парсер f.close(); if (err) { Serial.println("razbor sorvalsya - defolty"); // err.c_str() назовёт код return; } if (doc.containsKey("interval")) { int v = doc["interval"]; // скобки и точка здесь равнозначны interval = (v < 1) ? 1 : v; // не меньше одной секунды } if (doc.containsKey("threshold")) { threshold = constrain(doc["threshold"], 100, 900); } if (doc.containsKey("name")) { deviceName = doc["name"].as<String>(); } if (doc.containsKey("zones")) { zoneCount = doc["zones"].size(); // массив: сколько зон приехало } Serial.println("konfig primenen");} void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("SD not ready"); while (true) { } } loadConfig(); Serial.print("interval="); Serial.println(interval); Serial.print("threshold="); Serial.println(threshold); Serial.print("zones="); Serial.println(zoneCount);} void loop() { }Четыре проверки на десяток строк — open, код разбора, containsKey, диапазоны — и устройство с любым битым конфигом едет на дефолтах: без карты, с файлом-мусором, с отсутствующими полями, с безумными числами. Каждый отказ напечатан поимённо — по Serial вы поймёте, что файл не применился, за десять секунд, а не через неделю по странному поведению.
Когда JSON оправдан: у данных появилась структура — зоны, вложенные блоки, массивы однотипных записей, — или конфигом обмениваются программы, которые и так говорят JSON: скрипты, серверы, умный дом. Когда избыточен: три плоских числа, которые редактирует человек, — key=value короче, читается без кавычек и не тащит библиотеку на полтора десятка килобайт флеша. Моё правило: как только в именах параметров появились цифры — zone1, zone2 — переходите на массив в JSON.
Обратная дорога тоже работает: соберите документ программно и serializeJson(doc, файл) — порог, откалиброванный кнопками на месте, устройство запишет в тот же config.json, и к следующему пуску он приедет уже как настройка. Сборку JSON руками мы проходили в уроке про сериализацию. Так замыкается цикл: читаем при старте, пишем по результатам работы.
Последнее, про типы полей. Свои файлы вы контролируете, а чужая программа может прислать threshold как строку «700» вместо числа — as<int>() вернёт ноль, и досмотр поймает его как ноль, а не как семьсот. Если конфиг пишет кто-то другой, добавьте проверку типа threshold.is<int>() перед чтением: не то — печать и дефолт. В своих файлах я это опускаю и читаю напрямую — экономия двух строк оправдана, когда автор файла и автор скетча один и тот же человек.
В симуляторе карта не подключается, зато сам ArduinoJson в нём настоящий: deserializeJson, containsKey, чтение полей. Ниже скетч подставляет три варианта строки config.json — полный, без поля, вне диапазона — и три ступени валидации видно вживую: смотрите, как каждый отказ напечатан поимённо и какое значение живёт в переменных после каждого варианта. Одна оговорка о движке: поля читаются точечной формой doc.interval — на плате она равнозначна квадратным скобкам.
- 1. Чтение отсутствующего поля из разобранного документа вернёт:
- 2. deserializeJson вернул NoMemory. Что случилось?
- 3. Зачем досмотр диапазонов, если разбор прошёл успешно?
- 4. Где DynamicJsonDocument doc(768) берёт память?
- 5. Что экономит передача File прямо в deserializeJson(doc, file)?
- Цепочка: FILE_READ — deserializeJson — containsKey обязательных полей — диапазоны; битый конфиг не доезжает до переменных
- Отсутствующее поле читается нулём молча — containsKey отличает «поля нет» от «поле равно нулю»
- JSON окупается структурой: массив зон вместо имён-конструкторов zone1_threshold и zone2_threshold
- DynamicJsonDocument(768) — 768 байт из кучи Uno; коридор 512-1024; NoMemory — ёмкости мало
- Отдавайте File прямо в deserializeJson: String с большим файлом фрагментирует кучу
- Три плоских числа — key=value; цифры в именах параметров — сигнал переходить на JSON
Частые вопросы по теме урока
- Чтение отсутствующего поля из разобранного документа вернёт:
- Пустой документ на любое чтение отвечает нулём молча. Поэтому отсутствующее поле отличают через containsKey и едут на дефолт. Ноль — молча, без предупреждений
- deserializeJson вернул NoMemory. Что случилось?
- NoMemory — документ мал: парсер держит значения, имена ключей и служебные узлы. Оцените нужное через measureJson и увеличьте ёмкость. Ёмкости документа не хватило на разбор
- Зачем досмотр диапазонов, если разбор прошёл успешно?
- Валидный JSON бывает бессмысленным: отрицательный интервал разборится честно и поедет в таймер. Диапазоны — третья ступень проверки. Разбор гарантирует структуру, но не смысл: interval=-5 пройдёт без жалоб
- Где DynamicJsonDocument doc(768) берёт память?
- Динамический документ выделяет пул из кучи. На Uno мой коридор — 512-1024 байта; объявляйте документ в функции, чтобы куча вернулась после выхода. Из кучи malloc-ом: 768 байт из двух килобайт RAM Uno
- Что экономит передача File прямо в deserializeJson(doc, file)?
- Парсер вычитает поток сам, и String из всего файла не появляется. На Uno это килобайты RAM и целая гора проблем с фрагментацией кучи. RAM: строка целиком не копируется в оперативку