Журнал наблюдений за теплицей я неделю вёл в блокноте: час — строка, температура, свет. Когда дошли руки до платы, та же таблица перекочевала в JSON по образцу пакета с массивом readings — такой скелет мы собирали в уроке про пакет для сервера. Проблема всплыла через сутки работы: на Uno документ начинал задыхаться, хотя строка пакета выглядела скромной — полкилобайта с хвостом. Разгадка сидела в форме записей: каждая строка журнала стала объектом, и каждый объект притащил в пул свои слоты — одни лишь ключи повторились двадцать четыре раза по три штуки.
Механику сборки мы уже проходили: плоские поля — присваиваниями в уроке про создание JSON, вложенные группы — в уроке про вложенную сериализацию. Новое здесь — цена: сколько байтов пула стоит этаж вложенности и почему массив объектов на сутки упирается в потолок Uno, при том что его строка на вид безобидна.
План такой: скелет суточного журнала из объектов; сборка кольца на двадцать четыре записи на настоящей плате; арифметика слотов на пальцах; симулятор с окном, которое растёт ручкой, — и таблица решений, когда вложенность оправдана, а когда дешевле плоские массивы.
Скелет суток: массив объектов
Журнал выглядит так: в корне один ключ rows, под ним массив, а в массиве — записи-объекты {"h":13,"t":24.6,"lux":51}. h — час от начала суток, t и lux — показания. Форма дружелюбна к принимающей стороне: сервер читает запись целиком, поля не связаны позицией, новая колонка добавляется без переезда всего протокола. Плоский вариант — два массива t и lux рядом — короче на проводе, но связность теряется: пятая температура и пятое освещение держатся только на общем индексе, и после первой же дыры в потоке вы будете гадать, что именно пропустило кольцо.
В шестой версии библиотеки массив создаёт createNestedArray, запись-объект — createNestedObject на массиве; в седьмой та же работа пишется короче: doc["rows"].to<JsonArray>() и rows.add<JsonObject>(). Один уровень мы уже строили, здесь добавляется глубина: объект лежит не в корне, а внутри массива, и каждый его член платит слотами в общем пуле. Сколько именно — считаем сразу после кода.
#include <ArduinoJson.h> const int N = 24; // сутки почасовоint hH[N];int tH[N];int luxH[N];int head = 0;int zapol = 0;unsigned long last = 0;const unsigned long CHAS = 3600000UL; DynamicJsonDocument doc(1536); // v7: JsonDocument doc; void setup() { Serial.begin(9600);} void loop() { if (millis() - last < CHAS) return; last = millis(); tH[head] = analogRead(A2) / 10; // свои замеры вместо ручек luxH[head] = analogRead(A1) / 10; hH[head] = zapol; head = (head + 1) % N; if (zapol < N) zapol++; doc.clear(); JsonArray rows = doc["rows"].to<JsonArray>(); // v6: doc.createNestedArray("rows") for (int i = 0; i < zapol; i++) { int idx = (head - zapol + i + N) % N; JsonObject row = rows.add<JsonObject>(); // v6: rows.createNestedObject() row["h"] = hH[idx]; row["t"] = tH[idx]; row["lux"] = luxH[idx]; } serializeJson(doc, Serial); Serial.println(); Serial.print("bayt_json="); Serial.println(measureJson(doc));}Кольцо — тот же приём, что в уроке про график из ста точек: свежие записи пишутся поверх старых, head шагает по модулю, zapol помнит, сколько записей набралось. До первых суток пакет честно короткий — везёт только то, что успело скопиться. Ключевая строка — doc.clear() перед сборкой: без него в v6 копятся прошлые составы, и пакет растёт сам по себе. Такт держит millis без delay, и константа часа обязана быть длинной: 3600000 не влезает в int, суффикс UL тут страховка от сюрпризов арифметики с millis.
Пачка печатается целиком и сразу: serializeJson в Serial, следом счётчик measureJson. Для суток почасово строка выходит около полукилобайта — при 9600 бод это полсекунды канала раз в час, вполне терпимо. Но строка — не память: следом считаем пул, и там цифры жёстче в несколько раз.
Цена этажа: арифметика слотов
Пул документа живёт слотами: на 32-битных платах слот стоит порядка шестнадцати байт, на восьмибитных AVR — примерно вдвое меньше. Элемент массива занимает один слот. Член объекта — два: слот на ключ и слот на значение, даже когда ключ-литерал хранится ссылкой и своей строки в пуле не держит. Строковые значения добавляют сверху свою длину с терминатором, числа сидят прямо в слоте. Та же бухгалтерия лежит в основе урока про расчёт ёмкости — здесь доводим её до вложенности.
Считаем наш журнал на AVR. Массив из двадцати четырёх элементов — двадцать четыре слота. Каждая запись — три члена по два слота, шесть на запись; на сутки — сто сорок четыре. Плюс ключ rows в корне — два слота. Итого около ста семидесяти слотов, на AVR это порядка тысячи трёхсот байтов пула. Плоская пара массивов — сорок восемь слотов на элементы плюс пара ключей: около четырёхсот. Разница примерно втрое, и платит её ключ: двенадцать повторов трёх коротких слов на каждый час суток.
Провод дорожает по той же причине: пакет-объекты — около пятисот тридцати байтов против ста пятидесяти у плоских массивов, цену точки мы уже считали в уроке про график. Вывод у меня простой: массив объектов — формат для приёмника, который разбирает сам и на большой памяти; плоские массивы — для Uno на отправке. В седьмой версии макросы JSON_OBJECT_SIZE и JSON_ARRAY_SIZE остались достоянием шестой — ёмкость теперь меряют живьём: собираете макет журнала и печатаете measureJson, цифра заменяет салфетку. Методика с запасом на слоты разобрана в уроке про размер документа.
На больших платах вся эта бухгалтерия — спорт ради привычки. На ESP8266 со свободными сорока килобайтами журнал на сутки объектами уже честный, на ESP32 можно копить неделю и не заметить: слот вдвое шире, но куча перестаёт его считать. Спрашивать цену этажа стоит там, где плата маленькая — ровно там, где вложенность красивее всего смотрится в коде. Привязывайте расчёт к плате, а не к языку: на Mega с восемью килобайтами тот же скелет живёт без окна и без плоских компромиссов.
Разбор симулятора: окно из шести записей — это около сорока четырёх слотов, на AVR порядка трёхсот пятидесяти байтов пула, и в строке это видно: bayt_json колеблется около ста сорока. Передвиньте окно на единицу — пакет подрастает на двадцать с небольшим байтов, по цене одной записи с тремя членами. Кнопка pretty показывает этажи: два пробела на уровень — и видно, что каждый объект записи сидит внутри массива, а массив внутри корня.
Честная оговорка про движок: строить вложенные структуры присваиваниями он не умеет, поэтому пакет склеивается строкой и разбирается deserializeJson — печатаемый документ совпадает с тем, что на плате собирают to<JsonArray>() и add<JsonObject>() из код-блока. Светодиод загорается, когда окно набрано и разбор сошёлся; в коде плата ставит его по тем же условиям. Красивая лесенка хороша в мониторе, а цену красоты в байтах мы считали отдельно в уроке про serializeJsonPretty — в канал она не едет.
| Скелет журнала | Пул на AVR | Когда выбирать |
|---|---|---|
| Два плоских массива по 24 | около 400 байтов | графики и статистика: самый дешёвый скелет |
| Массив объектов {h,t,lux} на 24 | порядка 1300 байтов | сервер разбирает сам; Uno платит втрое |
| Окно из 6 записей объектами | около 350 байтов | свежий хвост в объектах, история — отдельно |
| Тот же пакет в pretty | провод растёт в два-три раза | только глаза в мониторе, не канал |
- 1. Во сколько примерно раз массив из 24 объектов с тремя полями дороже двух плоских массивов по 24 числа в пуле документа?
- 2. На Uno документ DynamicJsonDocument(1024), журнал копит сутки объектами {h,t,lux}. Что произойдёт?
- 3. Куда уходят слоты под ключи "h", "t", "lux", повторённые 24 раза?
- 4. Замеры идут строго каждый час. Какое поле записи-объекта можно выбросить первым?
- 5. В седьмой версии макросов JSON_OBJECT_SIZE и JSON_ARRAY_SIZE нет. Как прикинуть ёмкость журнала до прошивки?
- Журнал-массив объектов дружелюбен к приёмнику: запись атомарна, поля не связаны позицией
- Слот на AVR — около восьми байт: элемент массива — один слот, член объекта — два, ключ плюс значение
- Сутки объектами {h,t,lux} — порядка 170 слотов и 1300 байтов пула; плоские массивы — около 400: разница втрое
- В v7 ёмкость меряют макетом и measureJson: макросы JSON_OBJECT_SIZE остались в шестой версии
- doc.clear() перед сборкой и сверка zapisej с bayt_json — так ловится тихая обрезка окна
Частые вопросы по теме урока
- Во сколько примерно раз массив из 24 объектов с тремя полями дороже двух плоских массивов по 24 числа в пуле документа?
- Элемент массива — один слот, член объекта — два: ключ плюс значение. На сутки объектами выходит порядка 170 слотов против 52 у плоской пары — разница примерно втрое, и почти вся она — оплаченные ключи. Примерно втрое: член объекта — два слота, элемент массива — один
- На Uno документ DynamicJsonDocument(1024), журнал копит сутки объектами {h,t,lux}. Что произойдёт?
- Строка и пул — разные величины: сутки объектами стоят порядка 170 слотов, на AVR это около 1300 байтов против килобайта ёмкости. В v6 хвосты пропадут молча, в v7 документ поползет по куче. Пул попросит порядка 1400 байтов — окно придётся резать или брать плоские массивы
- Куда уходят слоты под ключи "h", "t", "lux", повторённые 24 раза?
- Ключ-литерал хранится ссылкой и строки в пуле не копирует, но слот под себя требует в каждой записи. Двадцать четыре повтора трёх ключей — это 72 слота только на имена. В тот же пул документа: слот на ключ и слот на значение каждой записи
- Замеры идут строго каждый час. Какое поле записи-объекта можно выбросить первым?
- При фиксированном шаге время держится позицией: i-я запись и есть i-й час. Экономия — слот пула и байты провода на каждой записи; при неровном шаге поле h возвращает уже как гарантию. h — номер часа совпадает с позицией в массиве
- В седьмой версии макросов JSON_OBJECT_SIZE и JSON_ARRAY_SIZE нет. Как прикинуть ёмкость журнала до прошивки?
- Практика v7 — замер вместо декларации: макет журнала собирают один раз, measureJson даёт длину строки, а пул оценивают по слотам с запасом. Салфетку это заменяет полностью. Собрать макет документа и распечатать measureJson, слоты считать с запасом