Мой первый телеметрический узел слал на сервер каждую запись отдельно: показал температуру — запрос, влажность — ещё запрос. Работало, пока я не посчитал: заголовки HTTP у короткого запроса весят не меньше самого тела, и полезная нагрузка тонула в обвязке. Серверщик сформулировал коротко: «шли раз в минуту один пакет — метаданные отдельно, показания массивом». Пакет собрался за вечер, трафик упал в разы, а запись на сервере стала атомарной: минута либо есть целиком, либо её нет.
Этот урок — про скелет такого пакета. Плоские поля для метаданных мы уже собирали присваиваниями в уроке про создание JSON; новое здесь — вложенные структуры: массив показаний и вложенный объект, в шестой версии за это отвечают createNestedArray и createNestedObject, в седьмой — вызов to<JsonArray>() и to<JsonObject>() на поле. Плюс бюджет: сколько байт ест пакет, как это считать до отправки и зачем в пакете поле с версией протокола.
Скелет пакета: метаданные плюс массив
Рабочая структура выглядит так: в корне плоские поля device, fw, up — имя узла, версия прошивки, время работы; рядом поле readings, куда ложится массив значений за период. В шестой версии библиотеки массив создаёт createNestedArray: JsonArray readings = doc.createNestedArray("readings");, значения добавляет readings.add(x). В седьмой версии эти методы остались псевдонимами и дают предупреждение о deprecated — новый синтаксис короче: JsonArray readings = doc["readings"].to<JsonArray>();. Пул один и тот же, различие в имени, но переезжая на v7 — о чём был урок про миграцию — берите новую запись сразу.
Вложенный объект кладётся тем же приёмом: createNestedObject("place") или doc["place"].to<JsonObject>(), дальше поля присваиваются как обычно. Где это нужно — пример из практики: пакет с несколькими датчиками, и в place уезжает этаж и комната установки. Итерация по такому массиву на принимающей стороне разбиралась в уроке про массивы JSON, цепочки вложений — в уроке про вложенные объекты; здесь мы собираем отправителя.
Поле fw в метаданных — не украшение. Протокол живёт: завтра сервер захочет поле заряда батареи, послезавтра — смену единиц. Версия в каждом пакете позволяет серверу выбирать ветку обработки до разбора массива, а вам — расширять формат, не ломая старые приёмники. Один байт метаданных против вечера отладки «а что сломалось после обновления» — сделка, которую я заключаю не думая.
#include <ArduinoJson.h>#include <DHT.h> DHT dht(2, DHT11);DynamicJsonDocument doc(768); // v7: JsonDocument doc; const int N = 8;float buf[N];int head = 0; unsigned long last = 0;const unsigned long PERIOD = 5000; void setup() { Serial.begin(9600); pinMode(12, OUTPUT); dht.begin();} void loop() { if (millis() - last < PERIOD) return; last = millis(); buf[head] = dht.readTemperature(); head = (head + 1) % N; doc.clear(); doc["device"] = "room-1"; doc["fw"] = 2; doc["up"] = last / 1000; JsonArray readings = doc["readings"].to<JsonArray>(); // v6: doc.createNestedArray("readings") for (int i = 0; i < N; i++) readings.add(buf[(head + i) % N]); serializeJson(doc, Serial); Serial.println(); Serial.print("bayt="); Serial.println(measureJson(doc)); float sred = 0; for (int i = 0; i < N; i++) sred += buf[i]; sred /= N; digitalWrite(12, sred > 24.0 ? HIGH : LOW);}Скетч копит показания в кольцевом буфере на восемь значений и раз в пять секунд собирает пакет: три плоских поля метаданных, массив из восьми температур, печать и счётчик байт. Первые пять секунд массив заполнен нулями — буфер ещё не набрал историю, для отладки это даже удобно: видно начало работы. Светодиод показывает вердикт по среднему за окно — тот же агрегат, что в уроке про разбор журнала частями, только на отправляющей стороне.
Бюджет считается до отправки. measureJson перед печатью — точная длина компактного пакета: она известна ещё в воздухе, и рост протокола виден в цифрах. Время на связи прикидывается делением: 9600 бод — около миллисекунды на байт, пакет в двести байт занимает канал на пятую часть секунды. На ESP32 с HTTP-POST'ом экономия виднее всего: заголовки едут с каждым запросом, и восемь значений одним пакетом вместо восьми запросов — это семь заголовков, которых не случилось. Отправка телеметрии POST'ом целиком разобрана в уроке про HTTP-телemетрию, а упаковка тела — в уроке про JSON в POST.
| Вариант | Размер на Uno | Когда выбирать |
|---|---|---|
| Одиночная запись на каждое показание | самый дешёвый пакет, но заголовки HTTP — с каждым | Записи раз в час и реже |
| Пакет с массивом на 8 показаний | около 150 байт тела | Телеметрия раз в минуту, сервер пишет блоками |
| Тот же пакет в pretty | вдвое-втрое длиннее | Только глазами в мониторе, не в канал |
| NDJSON: запись на строку | как пакет, но приём проще | Когда приёмник не умеет массивы — из урока про чанки |
Симулятор: пакет в эфире
Симулятор держит макетку с тремя датчиками: DHT на D2 даёт температуру и влажность, фоторезистор на A1 — освещённость, потенциометр на A0 — порог для светодиода. Плоский пакет собирается присваиваниями каждый цикл: устройство, время работы, показания, порог — и уходит компактной печатью со счётчиком байт. Это точный скелет реального отправителя, только без HTTP-обвязки.
Честная оговорка про массив: движок книги не строит вложенные структуры, поэтому серверный пакет с readings прилетает в симулятор готовым фрагментом — как будто его уже собрал createNestedArray на другой стороне. Проверка containsKey на readings и pretty-печать этого пакета показывают принимающую сторону: массив в документе выглядит так же, как если бы вы его вложили сами. На настоящей плате массив собирает скетч из code-блока — расхождение только в способе сборки, формат один и тот же.
- 1. Узел шлёт 60 одиночных записей в час по HTTP. Что даёт переход на пакет с массивом readings?
- 2. Проект на ArduinoJson 7. Как вложить массив readings в документ?
- 3. measureJson(doc) перед отправкой показал 89 байт. Что вы узнали?
- 4. Куда в пакете положить версию протокола, чтобы сервер видел её до разбора массива?
- 5. В симуляторе серверный пакет с readings прилетел готовым фрагментом. Как этот же массив строится на настоящей плате?
- Один пакет в минуту бьёт россыпь записей: заголовки HTTP не едут с каждым значением
- Скелет: плоские метаданные device, fw, up; показания — массивом readings
- v6: doc.createNestedArray("readings"); v7: doc["readings"].to<JsonArray>() и add() на каждое значение
- measureJson перед отправкой: длина печати известна до эфира, канал и буфер посчитаны заранее
- В симуляторе readings прилетает готовым фрагментом — на плате его собирает createNestedArray или to<JsonArray>
Частые вопросы по теме урока
- Узел шлёт 60 одиночных записей в час по HTTP. Что даёт переход на пакет с массивом readings?
- Запросы склеиваются, заголовки перестают ездить с каждым значением, сервер пишет минуту целиком. SRAM растёт на документ и кольцевой буфер, но никак не в восемь раз. Меньше запросов, меньше заголовков, запись на сервере атомарна
- Проект на ArduinoJson 7. Как вложить массив readings в документ?
- В седьмой версии идиома — to<JsonArray>() на поле: старые createNestedArray и createNestedObject остались псевдонимами и дают предупреждение о deprecated. Пул один и тот же, различие в имени. JsonArray readings = doc["readings"].to<JsonArray>(); — createNestedArray устарел
- measureJson(doc) перед отправкой показал 89 байт. Что вы узнали?
- 89 байт — это печать, не память. Умножьте на 1.04 мс для 9600 бод — узнаете, сколько пакет занимает канал, ещё до отправки. Ёмкость считает отдельная история из урока про расчёт памяти. Длину компактной печати: время на 9600 бод — около 93 мс
- Куда в пакете положить версию протокола, чтобы сервер видел её до разбора массива?
- Версия — метаданные: одно поле fw рядом с device. Сервер читает его до массива и выбирает ветку обработки; расширение формата перестаёт ломать старые приёмники. Отдельным плоским полем fw в метаданных
- В симуляторе серверный пакет с readings прилетел готовым фрагментом. Как этот же массив строится на настоящей плате?
- Движок книги отдаёт фрагмент как есть — так видно принимающую сторону. Плата строит массив сама: вложить структуру, потом add() на каждое показание; формат пакета в обоих случаях один. createNestedArray или to<JsonArray>(), значения — по add() на каждое