Три датчика на трёх пинах и три Serial.println в loop — и монитор превращается в кашу: температура приезжает между двумя строчками света, уставка теряется в потоке, а сопоставить показания по времени можно только глазами. Один документ на все датчики чинит это структурой: {"up":120,"sens":{"t":23,"lux":55,"ust":40}} — один println, показания лежат рядом как соседи одной группы, и принимающей стороне не надо гадать, что с чем сравнивать.
Соседний приём я показывал в уроке про пакет для сервера: там массив readings собирал историю одного датчика плюс имя узла и версию протокола. Здесь другой срез — три разных датчика в одном документе, каждый в своей группе, и дисциплина переиспользования документа: то, из-за чего пакетная сборка чаще всего ломается на Uno.
Группа на каждый датчик
В шестой версии группы создаёт createNestedObject: JsonObject sens = doc.createNestedObject("sens"); sens["t"] = t; sens["lux"] = lux; — вложенный объект заведён в корне, ссылка на него лежит в переменной, внутри имена короткие. В седьмой та же запись выглядит как doc["sens"].to<JsonObject>() — детали переезда в уроке про миграцию. Плоский вариант с префиксами room_t и room_lux короче на пару скобок, но имена пухнут с каждым датчиком и однажды коллизятся; группа держит имена плоскими внутри своего этажа.
Цена вопроса — две скобки. На пакете из этого урока группа sens добавляет пару байт, а выигрывают оба конца: в мониторе видно, что t и lux из одной пробы, в разборе на приёмнике — один доступ к группе вместо сравнения префиксов. Пятый датчик добавит пять коротких пар, а не пять переименований всех существующих ключей. Единицы измерения и оговорки прячутся в легенду при старте — менять состав полей на живом приёмнике дороже, чем пара байт на ключе, поэтому состав продумывают до первого пакета, а не после.
Бюджет: где документ кончается
DynamicJsonDocument doc(768) занимает 768 байт SRAM с момента создания и до конца скетча — треть из двух килобайт Uno, и рядом живут Serial-буфер, стек и переменные. В шестой версии пул фиксирован: разбор, который не влез, возвращает код NoMemory — методика расчёта ёмкости в уроке про NoMemory; а при сборке переполнение молча выбрасывает значения, без кода и реплики. В седьмой версии документ растёт сам, но растёт из кучи — на Uno кучи нет, и потолок тот же SRAM. Точный размер строки даёт measureJson — методика в уроке про оценку памяти.
Три меры держат пакет в бюджете. Первая — doc.clear() перед каждой сборкой: документ живёт между пакетами, и без очистки ключи копятся от пакета к пакету. Вторая — measureJson рядом с каждой печатью: рост протокола виден в цифрах прямо в мониторе, а не по письму от сервера. Третья — короткие ключи: sens вместо sensors, t вместо temperature экономят по 5-10 байт на пакете; граница — читаемость: ключ, который вы сами не расшифруете через полгода, не экономия, а закладка.
Суточная арифметика на пакете из урока: строка выходит на 45-60 байт, плюс строка bayt= — около 55 байт каждые две секунды, то есть 43200 пакетов и порядка двух с половиной мегабайт в сутки. Канал 9600 бод тянет до 83 МБ в сутки — запас в тридцать с лишним раз; укоротите период до полусекунды — и канал занят уже на десятую часть, а запас сжался примерно до восьми. Это не запрет, а вилка: чем быстрее период, тем ближе момент, когда печать начинает вытеснять сами измерения.
#include <ArduinoJson.h> DynamicJsonDocument doc(768); unsigned long last = 0;const unsigned long PERIOD = 2000; // пакет раз в две секунды void setup() { Serial.begin(9600); pinMode(12, OUTPUT);} void loop() { if (millis() - last < PERIOD) return; last = millis(); int t = map(analogRead(A2), 0, 1023, 15, 35); // термистор: температура int lux = map(analogRead(A1), 0, 1023, 0, 100); // фоторезистор: свет, % int ust = analogRead(A0) / 10; // потенциометр: уставка doc.clear(); // старые ключи не должны доживать до нового пакета doc["up"] = millis() / 1000; JsonObject sens = doc.createNestedObject("sens"); sens["t"] = t; sens["lux"] = lux; sens["ust"] = ust; serializeJson(doc, Serial); // один пакет — одна строка Serial.println(); Serial.print("bayt="); Serial.println(measureJson(doc)); // рост пакета виден в цифрах digitalWrite(12, lux < ust ? HIGH : LOW);}Разбор по шагам. Расписание держит millis-пару из урока про millis вместо delay — период в две секунды без единого delay. doc.clear() стоит перед сборкой, а не после печати: после тоже можно, но перед надёжнее — у сборки нет веток возврата, где очистка могла бы потеряться. Три analogRead с map-переводом в физические единицы, группа sens, одна печать serializeJson и счётчик байт. Светодиод — пример применения: сравнение света с уставкой берётся из тех же переменных, что легли в пакет.
Приёмник читает строку целиком: serializeJson отправляет пакет одним куском, println завершает его. Печать полями через Serial.print рвёт пакет пополам на таймаутах приёмной стороны — приёмник соберёт половины в два разных объекта. Построчный разбор на второй плате разобран в уроке про JSON через UART.
Проба в симуляторе: подождите два-три пакета и сравните bayt= у строк — длина меняется на единицу-другую, когда число на ручке переходит из 9 в 10 или из 99 в 100. Это и есть рост протокола в миниатюре: поле с плавающей точкой или длинным именем добавит больше, и счётчик покажет это сразу. Группа sens в строке занимает два байта скобок — поискайте их глазами в пакете, они всегда на месте.
Честная оговорка про движок: вложенные группы присваиваниями он не строит, поэтому пакет склеивается строкой и разбирается deserializeJson — результат в мониторе совпадает с тем, что на плате даёт createNestedObject. Проверить можно сравнением: код-блок для платы и симулятор печатают одинаковый JSON при одинаковых значениях. Расхождение только в способе сборки — формат один и тот же.
Что будет, если датчиков станет десять? Документ растёт по ключам, а не по значениям: пять коротких пар на датчик — порядка тридцати байт, и на десяти датчиках строка выйдет на 180-200 байт, в 768 байт она влезает с запасом. А вот история не влезет: сто точек в документе — это десятки килобайт, на Uno место кончилось ещё в начале расчёта; кольцевой буфер значений в собственном массиве и пакет-срез — так я решал это в уроке про график из ста точек. Правило получается двухэтажное: срез в одном документе — да, история — нет; документ вмещает текущее состояние, а не прошлое.
И последнее про Serial как транспорт: печать bayt= после каждого пакета — это тоже канал. На пакете раз в две секунды реплика незаметна, на периоде в сто миллисекунд она съедает четверть пропускной способности — и счётчик становится тормозом. Мой маркер: на быстрых периодах печатаю bayt= один раз из двадцати пакетов, при старте и после смены состава полей; в остальное время пакет уходит один, без обвязки. После каждой правки сборки делаю контрольную посылку с печатью — одна строка и одна реплика показывают, чист ли документ.
| Вариант пакета | Длина | Когда выбирать |
|---|---|---|
| Плоский с префиксами: room_t, room_lux | самый короткий, минус пара скобок | один-два датчика, имена точно не пересекутся |
| Группа sens через createNestedObject | около 45-50 байт на три датчика | баланс байт и читаемости, датчики будут добавляться |
| Тот же пакет в pretty | вдвое-втрое длиннее | только глазами в мониторе, не в канал |
| Массив readings из урока про пакет | пакет за период одного датчика | история на сервере, а не мгновенный срез |
- 1. Что делает doc.createNestedObject("sens")?
- 2. doc.clear() забыт, пакет собирается каждые 2 с. Что покажет measureJson через десять пакетов?
- 3. Сколько SRAM Uno занимает DynamicJsonDocument doc(768)?
- 4. Пул документа в v6 кончился при разборке большой строки. Что вернёт deserializeJson?
- 5. Зачем measureJson рядом с каждой печатью пакета?
- Один пакет на все датчики: группа sens через createNestedObject, один println вместо трёх
- doc.clear() перед каждой сборкой — иначе ключи копятся, и measureJson растёт по прямой
- 768 байт документа — треть SRAM Uno; в v6 пул фиксирован: не влезло — NoMemory или молча пропавшее значение
- measureJson в каждой печати: рост протокола виден в цифрах сразу, а не по письму от сервера
- Короткие ключи экономят байты, но граница — читаемость; легенда полей печатается при старте
Частые вопросы по теме урока
- Что делает doc.createNestedObject("sens")?
- Метод заводит вложенный объект в корне и отдаёт ссылку: дальше поля пишутся в неё — sens["t"] = t. В седьмой версии та же запись выглядит как doc["sens"].to<JsonObject>(). Заводит в корне вложенный объект и возвращает на него ссылку
- doc.clear() забыт, пакет собирается каждые 2 с. Что покажет measureJson через десять пакетов?
- Без очистки ключи всех прошлых пакетов остаются в документе, и serializeJson печатает всю накопленную историю. Рост по прямой в счётчике байт — самый ранний признак забытого clear. Длину, растущую с каждым пакетом
- Сколько SRAM Uno занимает DynamicJsonDocument doc(768)?
- Документ держит весь пул в SRAM постоянно, а не только под разбором. Это треть памяти платы — рядом живут Serial-буфер, стек и переменные, поэтому размер считается заранее. 768 байт из 2 КБ — с момента создания и до конца скетча
- Пул документа в v6 кончился при разборке большой строки. Что вернёт deserializeJson?
- В шестой версии пул фиксирован: не влезло — разбор возвращает NoMemory, и ветка обработки честно откажется от строки. При сборке переполнение ведёт себя хуже — значения выбрасываются молча. Код NoMemory
- Зачем measureJson рядом с каждой печатью пакета?
- Счётчик байт в каждой печати превращает рост протокола из сюрприза на сервере в цифру в мониторе. Добавили поле или сменили датчик — сразу видно, на сколько байт потяжелел пакет. Рост пакета виден в цифрах до того, как он упрётся в пул