JSON на Arduino и ESP32: библиотека ArduinoJson 7
JSON — стандартный способ обменяться структурированными данными с браузером, сервером или соседним устройством. На микроконтроллере его почти всегда делают библиотекой ArduinoJson. Главное, что нужно знать перед началом: в седьмой версии API заметно изменился, а интернет по-прежнему полон примеров под шестую.
Что изменилось в версии 7
Раньше нужно было заранее считать, сколько памяти займёт документ, и выбирать между двумя классами. Теперь класс один, и размер он подбирает сам:
// Было в версии 6
StaticJsonDocument<256> doc;
DynamicJsonDocument doc(256);
// Стало в версии 7
JsonDocument doc;
Старый код при этом чаще всего собирается: StaticJsonDocument и
DynamicJsonDocument в седьмой версии сохранены как совместимые обёртки над
JsonDocument и помечены устаревшими — вы увидите предупреждения компилятора, но не
ошибки. Ломается не объявление документа, а те методы, которые убрали совсем.
А убрали действительно: макросы JSON_OBJECT_SIZE() и JSON_ARRAY_SIZE(), методы
capacity(), memoryUsage(), garbageCollect(), shallowCopy(), containsKey(),
а также createNestedObject() и createNestedArray(). Вот на них старый пример и
остановится.
| Задача | Версия 6 | Версия 7 |
|---|---|---|
| Документ | StaticJsonDocument<N> | JsonDocument |
| Массив по ключу | doc.createNestedArray("data") | doc["data"].to<JsonArray>() |
| Объект по ключу | doc.createNestedObject("info") | doc["info"].to<JsonObject>() |
| Объект в массив | array.createNestedObject() | array.add<JsonObject>() |
| Проверка наличия ключа | doc.containsKey("a") | doc["a"].is<int>() |
| Проверка переполнения | по capacity() | doc.overflowed() |
Мнемоника простая: to<T>() — по ключу, заменяет значение; add<T>() — в
массив, добавляет элемент.
Ещё одно изменение ломает код тихо, без ошибки компиляции: serializeJson() теперь
заменяет содержимое строки, а не дописывает в конец. Строчки output = "" перед
вызовом больше не нужны — а если код полагался на дописывание, поведение изменится.
Собрать JSON
#include <ArduinoJson.h>
void setup() {
Serial.begin(115200);
JsonDocument doc;
doc["device"] = "roboarm";
doc["uptime"] = millis() / 1000;
JsonArray angles = doc["angles"].to<JsonArray>();
angles.add(90);
angles.add(45);
angles.add(0);
JsonObject status = doc["status"].to<JsonObject>();
status["connected"] = true;
status["temperature"] = 36.6;
serializeJson(doc, Serial);
Serial.println();
}
void loop() {
}
Результат:
{"device":"roboarm","uptime":12,"angles":[90,45,0],"status":{"connected":true,"temperature":36.6}}
Куда писать результат — определяет второй аргумент. Serial пишет прямо в порт,
String — в строку, буфер char[] — в массив:
String payload;
serializeJson(doc, payload);
char buffer[256];
serializeJson(doc, buffer, sizeof(buffer));
Есть и serializeJsonPretty() — с отступами и переносами. Читать удобнее, но
объём вырастает в полтора-два раза, так что для обмена данными берут обычный вариант.
Разобрать JSON
const char* json =
"{\"device\":\"roboarm\",\"angles\":[90,45,0],\"status\":{\"connected\":true}}";
JsonDocument doc;
DeserializationError error = deserializeJson(doc, json);
if (error) {
Serial.print("Не удалось разобрать JSON: ");
Serial.println(error.f_str());
return;
}
const char* device = doc["device"];
int firstAngle = doc["angles"][0];
bool connected = doc["status"]["connected"];
Serial.print(device);
Serial.print(", первый угол ");
Serial.print(firstAngle);
Serial.print(", связь ");
Serial.println(connected);
Проверка ошибки обязательна. DeserializationError приводится к bool, где истина
означает сбой, а f_str() даёт текстовое описание — например, IncompleteInput, если
строка оборвалась, или NoMemory, если данные не поместились.
Обращение к отсутствующему ключу не приводит к падению: вернётся нулевое значение для запрошенного типа. Если важно отличить «ноль» от «ключа нет», проверяйте тип:
if (doc["angle"].is<int>()) {
int angle = doc["angle"];
}
Перебор массивов и объектов
for (JsonVariant value : doc["angles"].as<JsonArray>()) {
Serial.println(value.as<int>());
}
for (JsonPair item : doc["status"].as<JsonObject>()) {
Serial.print(item.key().c_str());
Serial.print(" = ");
Serial.println(item.value().as<String>());
}
Чтение прямо из ответа сервера
deserializeJson() принимает не только строку, но и любой поток. Это позволяет
разбирать ответ сервера, не собирая его целиком в память:
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
void fetchWeather() {
HTTPClient http;
http.begin("http://api.example.com/weather");
if (http.GET() == 200) {
JsonDocument doc;
DeserializationError error = deserializeJson(doc, http.getStream());
if (!error) {
float temperature = doc["main"]["temp"];
Serial.printf("Температура: %.1f\n", temperature);
}
}
http.end();
}
Сетевые потоки не буферизованы, и библиотека читает их по одному байту — на больших
ответах это заметно медленно. Автор библиотеки рекомендует оборачивать поток в
ReadBufferingStream из своей же библиотеки StreamUtils. Для ответов в пару
килобайт разница несущественна.
Отдельный приём для больших ответов — фильтрация: попросить библиотеку сохранить только нужные поля, а остальное пропустить мимо. Это резко снижает расход памяти, когда сервер отдаёт много лишнего.
Сколько это стоит по памяти
Седьмая версия работает исключительно через динамическое выделение памяти, и на восьмибитных платах это ощутимо: по замерам автора, пример парсера на AVR стал примерно на 40% больше, чем в шестой версии.
Поэтому официальная рекомендация звучит так:
- Arduino UNO, Nano, Mega — оставайтесь на ArduinoJson 6. Она поддерживается и работает.
- ESP32, ESP8266, UNO R4 и другие 32-битные — берите версию 7. Там тот же прирост составляет пару процентов от общего размера прошивки, а API удобнее.
В менеджере библиотек нужную версию можно выбрать в выпадающем списке перед установкой — см. Библиотеки Arduino IDE.
На плате с двумя килобайтами ОЗУ JSON вообще стоит применять с осторожностью:
документ на десяток полей вместе с исходной строкой легко съедает половину памяти.
Если обмен идёт между двумя вашими же устройствами, текстовый протокол вида
go x=10 y=20 обойдётся в разы дешевле. Про расход ОЗУ — в статье
Память Arduino и ESP32.
Частые проблемы
'class JsonDocument' has no member named 'createNestedObject' и подобные ошибки
про containsKey, capacity или JSON_OBJECT_SIZE. Пример написан под версию 6, а
установлена 7: эти методы удалены. Замены — в таблице выше. Само объявление
StaticJsonDocument<256> при этом соберётся, только с предупреждением.
NoMemory при разборе. На ESP32 обычно означает, что документ действительно
огромный — примените фильтрацию. На AVR это штатная ситуация: памяти не хватает.
Числа с плавающей точкой теряют знаки. По умолчанию сериализуется несколько
значащих цифр. Нужную точность задают вторым аргументом: doc["value"] = serialized(String(x, 4)).
Кириллица в значениях. JSON работает с UTF-8, и библиотека передаёт байты как есть. Проблемы обычно на стороне получателя, а не платы.
Что дальше
- Отдача JSON из веб-интерфейса — Веб-сервер на ESP32.
- Установка и выбор версии библиотеки — Библиотеки Arduino IDE.
- Сколько памяти реально доступно — Память Arduino и ESP32.