Метеоузел на Uno висит на окне и раз в десять минут ходит в погодный сервис. Ответ — полный /data/2.5/weather — в моём городе весит около двух килобайт: координаты, группа main с восемью полями, ветер, облака, восход, системный блок с кодами страны. Нужны мне из этого три вещи: имя города и два числа из main — температура и влажность. Полгода узел разбирал ответ целиком и держал в ОЗУ всё: и то, что рисуется на экране, и то, что никогда не понадобится ни мне, ни серверу. Свободной ОЗУ, по бесплатному freeRam() в журнале, оставалось меньше полутора килобайт — и половина её жила в пуле документа.
Документ-образец, срезающий поля ещё на разборе, мы завели в уроке про фильтры: третий аргумент deserializeJson — DeserializationOption::Filter(filter). Там — плоский фильтр на верхние ключи и общая механика «что берём, что пропускаем». Здесь следующий этаж: вложенный фильтр на группу main вида {"main":["temp","humidity"],"name":true}, арифметика выигрыша через memoryUsage() и грабля, о которую я споткнулся первым же днём: отфильтрованное поле не пустое — его в документе нет вовсе, и is<T>() на нём честно возвращает false. Механику проверки типов мы разбирали отдельно; здесь она выстреливает с фильтром, потому что границы документа становятся жёсткими: ключ либо есть целиком, либо его нет совсем.
Погоду целиком разбирали в уроке про OpenWeather; здесь я вырезаю из того же ответа половину ещё на входе. Границы применимости — когда ответ не влезает в ОЗУ даже с фильтром — считал соседний урок про большие ответы, его бюджет повторять не буду: здесь добавляется измеритель, а не новая граница. И один сквозной вопрос: сколько байт вы получаете за список из трёх строк — меряем честно, на своём пакете, без обещаний из документации: экономия без числа — просто вера.
Фильтр с адресами: main, temp, humidity
Вложенный фильтр описывается тем же вложенным документом. filter["main"] = true взяло бы группу main целиком; вместо этого я перечисляю нужные члены: filter["main"]["temp"] = true; filter["main"]["humidity"] = true; — и из восьми полей группы в документ попадают два. Короткая запись того же — значение-массив: filter["main"] = ["temp","humidity"] работает как белый список имён. Ключ верхнего уровня filter["name"] = true забирает имя города как есть. Читается фильтр как маршрут: что перечислено — то и приехало, остальное парсер пропускает, не заводя под него слотов. Вложенность в фильтре повторяет вложенность ответа один в один: группа в группе — два этажа, список имён — всегда последний этаж, дальше белого списка адресация не растёт.
В седьмой версии filter — обычный JsonDocument, растущий сам; в шестой его заводили StaticJsonDocument со счётом байтов, и под три строки хватало сотни с четвертью. Заполняется он один раз в setup() и живёт глобально: под него тоже выделено место, поэтому фанатизм вреден — фильтр на сорок ключей ради двух полей удержит больше, чем сэкономит. Мой ориентир: фильтр на порядок-два меньше ожидаемого полного ответа — тогда он окупается с первого же разбора. Отдельный плюс — читаемость: по одному взгляду на filter видно, какие поля узел потребляет, без чтения всего кода разбора. Такой фильтр я правлю вместе с протоколом: новая версия ответа — новая строка в образце, оба изменения в одном коммите.
#include <ArduinoJson.h> // Ответ сервиса; в реальном скетче приходит по HTTP и весит около 2 КБ.// Здесь укорочен до нужных полей и пары лишних, чтобы скетч читался.String packet = "{\"name\":\"Omsk\",\"timezone\":18000,\"wind\":{\"speed\":4.2}," " \"main\":{\"temp\":-3.5,\"feels_like\":-9.1,\"humidity\":88,\"pressure\":1013}}"; JsonDocument filter; // v6: StaticJsonDocument<192> filter;JsonDocument doc; // v6: StaticJsonDocument<192> doc; void setup() { Serial.begin(9600); // Маршрут: имя города как есть, из группы main — только два члена filter["name"] = true; filter["main"]["temp"] = true; filter["main"]["humidity"] = true; DeserializationError err = deserializeJson(doc, packet, DeserializationOption::Filter(filter)); if (err) { Serial.print("razbor sorvan: "); Serial.println(err.c_str()); return; } Serial.print("osadki: "); Serial.print((const char*)doc["name"]); Serial.print(" t = "); Serial.print((float)doc["main"]["temp"], 1); Serial.print(" h = "); Serial.println((int)doc["main"]["humidity"]); Serial.print("pechat: "); Serial.print(measureJson(doc)); Serial.println(" bajt"); Serial.print("pul: "); Serial.print(doc.memoryUsage()); Serial.println(" bajt");} void loop() {}Печать результата — пятьдесят с небольшим байт: {"name":"Omsk","main":{"temp":-3.5,"humidity":88}}. Против двух килобайт исходной строки выигрыш по документу — в десятки раз, и это не магия, а прямое следствие механики: значения, которых нет в фильтре, не копируются в пул, слоты под них не заводятся. Замер memoryUsage() после полного разбора того же пакета давал мне порядка тысячи трёхсот байт, после отфильтрованного — несколько десятков. Ваши цифры будут своими: сервисы пересобирают пакеты без предупреждений, поэтому меряйте на своём ответе, а не верьте чужим. Ориентир для прикидки остаётся прежним: результат в полтора-два раза длиннее своих ключей и значений — вот столько пул и займёт с накладными на слоты.
Замер встроен прямо в скетч: memoryUsage() печатается рядом с длиной печати, и пара цифр обновляется на каждом разборе. Приёмы снятия показаний те же, что в уроке про measureJson; фильтр добавляет в пару вторую строку. В шестой версии тот же выигрыш выражался в ёмкости: StaticJsonDocument<2048> ужимался до StaticJsonDocument<192> — четверть килобайта вместо двух, и на Uno это разница между «влезает с запасом» и «влезает впритык к стеку». Замер я снял на v7.0 на Uno; на ESP32 те же цифры никого не волнуют — там бюджет решают другие статьи расходов, и фильтр включают ради чистоты состава, а не ради байтов.
Что экономится, а что нет
| Что измеряем | Без фильтра | С фильтром |
|---|---|---|
| Значения неперечисленных полей | копируются в пул: слоты и строки на всё | не заводятся вовсе — главный выигрыш |
| Проход по строке | от первой скобки до последней | тот же: структуру парсер читает целиком |
| Трафик и время приёма | ответ едет по сети целиком | без изменений: канал фильтр не сжимает |
| Пул под сам фильтр | не нужен | десятки байт постоянно — цена списка желаний |
Фильтр не сжимает канал: два килобайта трафика остаются двумя килобайтами, время приёма не сокращается ни на миллисекунду. Не исчезает и проход по строке — дешевле становится не чтение, а размещение: память под документ и время на копирование строк в пул. На ESP32 с её сотнями килобайт экономия незаметна, на Uno решает: узел, который не влезал в два килобайта, начинает жить с запасом. Полный выигрыш собирается связкой «поток плюс фильтр»: deserializeJson читает прямо из http.getStream(), и опция Filter передаётся туда же третьим аргументом — ни полной строки, ни полного документа память не держит. Эту связку мы разбирали на ESP32 в уроке про JSON по HTTP; на Uno приём обычно идёт в String, и там фильтр хотя бы срезает вторую копию — документ, а не строку. Время разбора фильтр подрезает по-настоящему: не читать строку быстрее нельзя, а вот не копировать — можно, и на Uno копирование строк было половиной всей работы. Если приём всё же идёт в String, держите одну и ту же строку для повторных разборов: переиспользование буфера дешевле, чем новая копия на каждый цикл.
Движок книги опцию Filter не эмулирует — в симуляторе та же выборка выполняется руками: полный пакет разбирается в первый документ, из него во второй переносятся только два поля, и обе печати считаются measureJson-ом. Арифметика та же, что у настоящего фильтра, только перенос выполняет скетч, а не библиотека; на плате роль выборчика исполняет код из первого блока. LED на D12 горит, пока разница печатах больше двадцати байт, кнопка D2 чистит оба документа. Движок честно считает длины через measureJson — те же вызовы, что на плате, поэтому разница в байтах показывается настоящая, а не нарисованная.
Пакет в подсказке — шесть полей; в жизни их за два десятка, и разница на каждом разборе пропорциональна. Напишите свой пакет длиннее — и посмотрите, как строка «sdelali legche» растёт без единой правки кода: выборка берёт только temp и humi, всё остальное игнорирует. Состав отобранного удобно печатать целиком и сверять глазами: в отфильтрованном документе нет даже следов ветра и давления. Состав после фильтра — готовая форма для валидации из урока про проверку входа: обязательные ключи перечислены один раз — в образце.
- 1. Фильтр {"main":["temp","humidity"],"name":true}. Что окажется в документе?
- 2. Поле вырезано фильтром. Что вернёт is<T>() по этому ключу?
- 3. Что фильтр НЕ экономит?
- 4. Полный разбор погодного ответа дал memoryUsage() порядка 1300 байт. Отфильтрованный на два поля — порядка…
- 5. Под сам документ-фильтр память…
- Фильтр — второй JsonDocument-образец: deserializeJson(doc, s, DeserializationOption::Filter(filter)) кладёт в doc только перечисленное
- Вложенный белый список: filter["main"] = ["temp","humidity"] берёт из группы два члена; true забирает ветку целиком
- Выигрыш измеряется, а не обещается: memoryUsage() порядка 1300 байт на полный ответ против десятков на отфильтрованном (мой пакет, v7)
- Отфильтрованного поля нет в документе: is<T>() и containsKey вернут false — сначала проверьте filter, потом сеть
- Канал фильтр не сжимает; полный выигрыш — со связкой «поток плюс фильтр» без промежуточной String
Частые вопросы по теме урока
- Фильтр {"main":["temp","humidity"],"name":true}. Что окажется в документе?
- true под ключом берёт значение как есть, значение-массив в группе — белый список имён членов. Всё, что не перечислено, в документ не попадает. Имя города плюс только temp и humidity из группы main
- Поле вырезано фильтром. Что вернёт is<T>() по этому ключу?
- Отфильтрованное поле не заводится в пуле: чтение даёт ноль, is<T>() и containsKey — false. Ищите опечатку в filter, а не промахи сети. false: поля в документе нет вовсе
- Что фильтр НЕ экономит?
- Канал фильтр не сжимает: два килобайта ответа остаются двумя килобайтами в проводе. Срезается память под документ и время размещения значений. Трафик: ответ приходит по сети целиком
- Полный разбор погодного ответа дал memoryUsage() порядка 1300 байт. Отфильтрованный на два поля — порядка…
- Под выбранные поля заводятся единицы слотов и пара строк: десятки байт. Точную цифру снимайте на своём ответе — пакеты сервисы пересобирают без предупреждений. Десятков байт
- Под сам документ-фильтр память…
- Фильтр — обычный документ: под него выделено место на весь сеанс. Фанатизм вреден — список на сорок ключей ради двух полей съест больше, чем сэкономит. Тратится один раз и живёт постоянно, поэтому узкий фильтр лучше жирного