Узел полива отчитывался серверу строкой вида temp=21;hum=58;pump=1 — формат придумал я сам и на сервере сам же его разбирал. Через полгода к проекту подключился второй человек, для которого точка с запятой между полями была загадкой, а сторонний сервис вообще отказался принимать что-либо кроме JSON. Пришлось признать: свой текстовый протокол — это вечные разборы на обоих концах, а JSON — формат, который читают все без конвенций.
Свой формат я собирал конкатенацией: String s = "temp="; s += t; и так до конца пакета. Пока в имени узла не оказалась точка с запятой — парсер сервера увидел лишнее поле, а я увидел пустую телеметрию. Сравнение подходов к склейке строк мы делали в уроке про String-конкатенацию: там же вывод, к которому я пришёл на практике, — строки для протоколов собирает библиотека, а не ваши руки. Для JSON этот вопрос закрыт окончательно: serializeJson экранирует и пунктирует сама, а с тех пор в моих протоколах только JSON.
Сборка через документ у нас уже была — в уроке про свой JSON и отправку на сервер и в разборе serializeJson с приёмниками печати (отдельный урок). Здесь добираем низ: само присваивание doc["key"] = value — типы значений, вложенность, массивы, перезапись и очистка. API читается как объявление: положил значение под ключ — ключ появился; положил под существующий — обновился.
План такой: механика присваивания и типы, этажи — вложенные объекты и массивы — с отличиями v6 и v7, жизненный цикл документа при повторных сборках. К концу урока у вас будет полный цикл сборки: от пустого документа до напечатанной телеметрии — и понимание, где в этом цикле живут грабли. В конце — симулятор, где присваивание работает как на плате, и таблица-шпаргалка по типам.
Присваивание: ключ слева, тип определит библиотека
Базовая форма — три строки: doc["temp"] = 21.4; doc["node"] = "dacha1"; doc["pump"] = true;. Слева ключ, справа значение любого поддержанного сорта: число целое или с точкой, строка, true/false, null. Тип библиотека выводит из выражения справа и сама оформит печать: число без кавычек, строка в кавычках, булево словом true или false. Дробь — только с точкой: 21,4 с запятой запишется как два значения, та же ловушка, что и при разборе из первого урока синтаксиса JSON.
Перезапись ключа обновляет значение: doc["temp"] = 20.9 после 21.4 оставит один ключ temp с новым числом — так обновляют телеметрию без пересборки. Порядок полей в печатном JSON — порядок первого появления ключа: документ получается предсказуемым и в логах читается глазами. Удалить поле можно doc.remove("key") — пригождается, когда поле стало неактуальным, а пустая пара смутит принимающего. Положить явный null — doc["rain"] = nullptr: в печати будет "rain":null, и читатель отличит «данных нет» от честного нуля — та же дисциплина, что при чтении через isNull в уроке про чтение полей. И не забывайте про тип слева при чтении: собранный документ читается теми же doc["key"], что и разобранный из сети.
Разница версий минимальна: присваивание скаляров в v6 и v7 одинаково. Расходятся этажи — вложенные объекты и массивы, — там дам оба варианта записи.
Про строки одна тонкость, о которой стоит знать заранее: в седьмой версии библиотека копирует строки внутрь документа, и присвоенная String может спокойно умереть — значение остаётся. В шестой версии const char* хранился указателем: строка-литерал жила вечно, а вот String или буфер — только до первого изменения. Мы ловили это в уроке про чтение полей на примере повисшего указателя; при сборке правило простое: положили String в v6 — не держите указатель на неё, а лучше сразу v7. Документу всё равно, откуда строка — литерал, String или буфер: форма присваивания одна.
#include <ArduinoJson.h> JsonDocument doc; // v6: StaticJsonDocument<384> doc;char buf[192]; // под печать с запасом void buildTelemetry(float t, int h, bool pump, unsigned long up) { doc.clear(); // новая порция — с чистого листа doc["node"] = "poliv1"; doc["pump"] = pump; // true/false библиотека напечатает сама JsonObject sens = doc["sens"].to<JsonObject>(); // v6: doc.createNestedObject("sens") sens["temp"] = t; sens["humi"] = h; JsonArray day = doc["day"].to<JsonArray>(); // v6: doc.createNestedArray("day") day.add(18); day.add(20); day.add(22); doc["uptime"] = up; // секунды с включения} void setup() { Serial.begin(9600); buildTelemetry(21.4, 58, true, millis() / 1000); Serial.print("bytes="); // длина до печати — как при отправке в буфер Serial.println(measureJson(doc)); serializeJson(doc, Serial); Serial.println();} void loop() {}Этажи: вложенные объекты и массивы
Вложенный объект собирается цепочкой присваиваний: doc["sens"]["temp"] = 21.4 — промежуточный объект sens создастся сам. В v6 явный путь: JsonObject sens = doc.createNestedObject("sens"); и дальше sens["temp"] = 21.4. Явная форма ценна, когда этаж надо передать в функцию или наполнить в цикле — держите ссылку и работайте с ней.
Массив: в v6 — JsonArray day = doc.createNestedArray("day"); day.add(18); day.add(20);. В v7 то же самое одной строкой: JsonArray day = doc["day"].to<JsonArray>(); — после чего add() хоть в цикле. Правка элемента по индексу — doc["day"][1] = 99; — работает после создания. Журнал показаний за сутки собирают массивом из объектов: взяли массив, добавили в него объекты — в v7 это doc.add<JsonObject>() для элемента-объекта, в v6 — createNestedObject() уже у массива.
Повторные сборки: перед новой порцией данных — doc.clear(). Перезапись обновляет существующие ключи, но состав полей не меняет: если в прошлой сборке было поле vent, а в новой его нет — оно уедет на сервер молча, и неделю можно искать, откуда взялся чужой режим. clear стоит две буквы в коде и делает сборку честной: новый документ с нуля, никакого прошлого. Замер measureJson перед печатью — та же страховка, что при отправке в буфер: сначала увидели длину, потом печатали.
И про производительность: сборка из сотни присваиваний занимает на Uno микросекунды — сотни тактов на ключ. Дольше всё, что после: печать в Serial, отправка в сеть, запись на карту. Поэтому оптимизируют не присваивания, а канал: compact-печать вместо pretty для сети, pretty — для отладки; это различие мы уже считали в уроке про serializeJson.
Сборка в симуляторе
Присваивание движок книги ведёт как на плате: кладите значения ключами, печатайте serializeJson и serializeJsonPretty, замеряйте measureJson, проверяйте containsKey, чистите clear. Одна форма записи в сим-скетче — точечная: doc.temp = 21.4 — движок понимает только точку; на плате привычнее doc["temp"], они равнозначны. Этажи — вложенные объекты и массивы — в симе не собираются: их путь показал code-блок для настоящей платы. Зато плоская сборка в симе настоящая: что положили присваиванием — то и напечаталось.
Скетч ниже проходит весь жизненный цикл за один setup: сборка телеметрии, печать в двух форматах, замер длины, перезапись показания, проверка ключа — и вторая сборка с чистого листа для соседнего узла.
Числа в скетче реальные: компактная печать телеметрии — 51 байт, после перезаписи показания столько же, состав-то не менялся; pretty-версия раздувается на отступах и переводах строк; сборка второго узла после clear — 31 байт. Перезапись temp видно глазами: в первой печати 21.4, во второй 20.9; флаг pump уехал в JSON словом true — библиотека оформила булево сама.
| Значение | Присваивание | В печатном JSON | Грабли |
|---|---|---|---|
| Целое число | doc["humi"] = 58 | 58 | дробная часть пропадёт — для неё float |
| Дробное число | doc["temp"] = 21.4 | 21.4 | запятая вместо точки ломает запись |
| Строка | doc["node"] = "dacha1" | "dacha1" в кавычках | кавычки внутри значения экранируются сами |
| Булево | doc["pump"] = true | true | не строка "true": сравнение на плате будет другим |
| null | doc["rain"] = nullptr | null | отличимо от нуля при чтении isNull |
- doc["key"] = value — создание и обновление в одной форме: нового ключа нет, ключ появился; перезапись меняет значение, не состав
- Тип печати выводится из выражения: число без кавычек, строка в кавычках, true/false словами, nullptr кладёт явный null
- Этажи: doc["sens"]["temp"] = 21.4 в v7, createNestedObject/createNestedArray в v6; в v7 массив — doc["day"].to<JsonArray>()
- Перед повторной сборкой doc.clear(): состав полей честный, чужие ключи не уезжают на сервер
- measureJson до печати и serializeJson в приёмник — длина известна заранее, печать идёт куда угодно: Serial, буфер, клиент
- 1. Выполнены doc["temp"] = 21.4 и затем doc["temp"] = 20.9. Что в документе?
- 2. Как в седьмой версии библиотеки завести массив day внутри документа?
- 3. Как положить в документ явный null?
- 4. Зачем doc.clear() перед повторной сборкой, если перезапись и так обновляет значения?
- 5. Документ печатается в Serial при 9600 бод и занимает 60 байт. Сколько времени уйдёт на печать?
Присваивание замыкает арифметику раздела: чужие ответы читаем фильтрами с лимитом на массивах, свои собираем ключами с clear между сборками. Остаётся применить: собранный документ печатается в брокера строкой serializeJson(doc, client) — рецепт из урока про публикацию в MQTT — или уезжает телом запроса, как в уроке про HTTP POST. Домашнее задание: соберите телеметрию своей платы — два показания, булев флаг и имя узла — и отправьте по MQTT или POST; принимающую сторону проверьте печатью в Serial с замером measureJson до отправки. Если принимающая сторона — вторая плата, разбор своего пакета вы уже умеете: deserializeJson из первых уроков раздела. Полный круг «собрал — отправил — разобрал» стоит прогнать хотя бы между двумя скетчами: на этом замыкается вся арифметика раздела.
Частые вопросы по теме урока
- Выполнены doc["temp"] = 21.4 и затем doc["temp"] = 20.9. Что в документе?
- Перезапись обновляет значение существующего ключа. Состав полей при этом не меняется — обновление телеметрии делают именно так. Один ключ temp со значением 20.9
- Как в седьмой версии библиотеки завести массив day внутри документа?
- В v7 массив берут через to<JsonArray>() и наполняют add(). createNestedArray — форма v6; литерал массива в присваивании не поддержан — массив заводят только через to<JsonArray>() или createNestedArray. JsonArray day = doc["day"].to<JsonArray>(); затем day.add(18);
- Как положить в документ явный null?
- nullptr даёт "rain":null в печати. Ноль — это число, читатель не отличит его от честного нулевого значения; различие подробно разбирали в уроке про чтение полей. doc["rain"] = nullptr;
- Зачем doc.clear() перед повторной сборкой, если перезапись и так обновляет значения?
- Перезапись обновляет только те ключи, которые вы кладёте заново; устаревшие остаются. clear обнуляет документ целиком — новая сборка с чистого листа. Чтобы состав полей был честным: ключи прошлой сборки не уедут с новыми данными
- Документ печатается в Serial при 9600 бод и занимает 60 байт. Сколько времени уйдёт на печать?
- При 9600 бод каждый байт стоит около миллисекунды: 60 байт — 60–65 мс. Сборка документа занимает доли миллисекунды — медленная часть всегда канал. Около 60 мс — примерно миллисекунда на байт