22 KiB
GSON
Парсер и сборщик данных в формате JSON для Arduino
- В 6 раз быстрее и сильно легче ArduinoJSON
- Парсинг JSON с обработкой ошибок
- Линейная сборка JSON-пакета
- Экранирование "опасных" символов
- Работает на базе AnyText (StringUtils)
- Работает с 64 битными числами
- Встроенный механизм хэширования ключей
- Библиотека не подходит для хранения и изменения данных! Только парсинг и сборка пакетов
Совместимость
Совместима со всеми Arduino платформами (используются Arduino-функции)
Зависимости
- Библиотека StringUtils
Содержание
Документация
AnyText
Под типом AnyText
понимается строка в любом формате:
"const char"
- строкиchar[]
- строкиF("f-строки")
String
- строки
gson::Parser
// конструктор
gson::Parser();
gson::Parser(размер);
gson::ParserStatic<размер>();
// методы
uint16_t length(); // получить количество элементов
uint16_t size(); // получить размер документа в оперативной памяти (байт)
void hashKeys(); // хешировать ключи всех элементов (операция необратима)
bool hashed(); // проверка были ли хешированы ключи
Entry get(AnyText key); // доступ по ключу (главный контейнер - Object)
Entry get(size_t hash); // доступ по хэшу ключа (главный контейнер - Object)
Entry get(int index); // доступ по индексу (главный контейнер - Array или Object)
AnyText key(int idx); // прочитать ключ по индексу
size_t keyHash(int idx); // прочитать хэш ключа по индексу
AnyText value(int idx); // прочитать значение по индексу
int8_t parent(int idx); // прочитать родителя по индексу
Type type(int idx); // получить тип по индексу
const __FlashStringHelper* readType(uint16_t idx); // прочитать тип по индексу
// парсить. Вернёт true при успешном парсинге. Можно указать макс. уровень вложенности
bool parse(String& json, uint8_t maxdepth = 16);
bool parse(const char* json, uint8_t maxdepth = 16);
// вывести в Print с форматированием
void stringify(Print* p);
// обработка ошибок
bool hasError(); // есть ошибка парсинга
Error getError(); // получить ошибку
uint16_t errorIndex(); // индекс места ошибки в строке
const __FlashStringHelper* readError(); // прочитать ошибку
Настройки
Объявляются перед подключением библиотеки
// отключить поддержку хэширования ключей (экономит 4 байта на элемент при парсинге)
#define GSON_NO_HASH
Лимиты
- После парсинга один элемент весит 8 (10 с хэшем) байт на AVR и 8 (12 с хэшем) байт на ESP
- Максимальное количество элементов: 255 на AVR и 512 на ESP
- Максимальная длина json-строки: 65 536 символов
- Максимальная длина ключа: 32 символа
- Максимальная длина значения: 32768 символов
- Максимальный уровень вложенности элементов задаётся в функции
parse()
. Парсинг - рекурсивный, каждый уровень добавляет несколько байт в оперативку
Тесты
Тестировал на ESP8266, пакет сообщений из телеграм бота - 3600 символов, 147 "элементов". Получал значение самого отдалённого и вложенного элемента, в GSON - через хэш. Результат:
Либа | Flash | SRAM | FreeHeap | Parse | Get |
---|---|---|---|---|---|
ArduinoJson | 297433 | 31628 | 41104 | 10686 us | 299 us |
GSON | 279349 | 31400 | 43224 | 1744 us | 296 us |
Таким образом GSON в 6 раз быстрее при парсинге, значения элементов получает с такой же скоростью. Сама библиотека легче на 18 кБ во Flash и 2.1 кБ в Heap. В других тестах (AVR) на получение значения GSON с хэшем работал в среднем в 1.5 раза быстрее.
gson::Type
None
Object
Array
String
Int
Float
Bool
gson::Error
None
Alloc
TooDeep
NoParent
NotContainer
UnexComma
UnexColon
UnexToken
UnexQuotes
UnexOpen
UnexClose
UnknownToken
BrokenToken
BrokenString
BrokenContainer
EmptyKey
IndexOverflow
LongPacket
LongKey
EmptyString
gson::Entry
Также наследует всё из AnyText
, документация здесь
Entry get(AnyText key); // получить элемент по ключу
bool includes(AnyText key); // содержит элемент с указанным ключом
Entry get(size_t hash); // получить элемент по хэшу ключа
bool includes(size_t hash); // содержит элемент с указанным хэшем ключа
Entry get(int index); // получить элемент по индексу
bool valid(); // проверка корректности (существования)
uint16_t length(); // получить размер (для объектов и массивов. Для остальных 0)
Type type(); // получить тип элемента
AnyText key(); // получить ключ
size_t keyHash(); // получить хэш ключа
AnyText value(); // получить значение
gson::string
String s; // доступ к строке
void clear(); // очистить строку
bool reserve(uint16_t res); // зарезервировать строку
// прибавить gson::string. Будет добавлена запятая
string& add(string& str);
// добавить ключ (строка любого типа)
string& addKey(AnyText key);
// прибавить текст (строка любого типа) без кавычек и запятой
string& addText(AnyText str);
// прибавить текст (строка любого типа) без кавычек и запятой с escape символов
string& addTextEsc(AnyText str);
// добавить строку (строка любого типа)
string& addString(AnyText key, AnyText value);
string& addString(AnyText value);
string& addStringRaw(AnyText value); // без запятой
// добавить строку (строка любого типа) с escape символов
string& addStringEsc(AnyText key, AnyText value);
string& addStringEsc(AnyText value);
string& addStringRawEsc(AnyText value); // без запятой
// добавить bool
string& addBool(AnyText key, const bool& value);
string& addBool(const bool& value);
string& addBoolRaw(const bool& value); // без запятой
// добавить float
string& addFloat(AnyText key, const double& value, uint8_t dec = 2);
string& addFloat(const double& value, uint8_t dec = 2);
string& addFloatRaw(const double& value, uint8_t dec = 2); // без запятой
// добавить int
string& addInt(AnyText key, const AnyValue& value);
string& addInt(const AnyValue& value);
string& addIntRaw(const AnyValue& value); // без запятой
string& beginObj(AnyText key = ""); // начать объект
string& endObj(); // завершить объект
string& beginArr(AnyText key = ""); // начать массив
string& endArr(); // завершить массив
string& end(); // завершить пакет
Использование
Парсинг
Библиотека не дублирует строку в памяти и работает с исходной строкой: запоминает позиции текста, исходную строку не меняет. Отсюда следует, что:
- Строка должна существовать в памяти на всём протяжении работы с json документом
- Если исходная строка -
String
- она категорически не должна изменяться программой до окончания работы с документом
Создание документа:
gson::Parser p; // динамический парсер
gson::Parser p(10); // динамический парсер, зарезервирован под 10 элементов
gson::ParserStatic<10> p; // статический парсер, зарезервирован под 10 элементов
Смысл как у String
-строки: динамический парсер будет увеличиваться в процессе парсинга в динамической памяти МК, если размер документа неизвестен. Можно заранее зарезервировать размер, чтобы парсинг происходил быстрее. Статический - выделяется статически, используя меньше памяти на слабых платформах.
// получили json
char json[] = R"raw({"key":"value","int":12345,"obj":{"float":3.14,"bool":false},"arr":["hello",true]})raw";
String json = "{\"key\":\"value\",\"int\":12345,\"obj\":{\"float\":3.14,\"bool\":false},\"arr\":[\"hello\",true]};";
// парсить
p.parse(json);
// обработка ошибок
if (p.hasError()) {
Serial.print(p.readError());
Serial.print(" in ");
Serial.println(p.errorIndex());
} else Serial.println("done");
После парсинга можно вывести весь пакет с типами, ключами, значениями в виде текста и родителем:
for (uint16_t i = 0; i < p.length(); i++) {
// if (p.type(i) == gson::Type::Object || p.type(i) == gson::Type::Array) continue; // пропустить контейнеры
Serial.print(i);
Serial.print(". [");
Serial.print(p.readType(i));
Serial.print("] ");
Serial.print(p.key(i));
Serial.print(":");
Serial.print(p.value(i));
Serial.print(" {");
Serial.print(p.parent(i));
Serial.println("}");
}
Значения можно получать в типе AnyText
, который может конвертироваться в другие типы и выводиться в порт:
- Ключом может быть строка в любом виде (
"строка"
,F("строка")
) - Можно обращаться ко вложенным объектам по ключу, а к массивам по индексу
Serial.println(p["key"]); // value
Serial.println(p[F("int")]); // 12345
int val = p["int"].toInt16(); // конвертация в указанный тип
val = p["int"]; // авто конвертация
float f = p["obj"]["float"]; // вложенный объект
Serial.println(p["arr"][0]); // hello
Serial.println(p["arr"][1]); // true
// проверка типа
p["arr"].type() == gson::Type::Array;
// вывод содержимого массива
for (int i = 0; i < p["arr"].length(); i++) {
Serial.println(p["arr"][i]);
}
// а лучше - так
gson::Entry arr = p["arr"];
for (int i = 0; i < arr.length(); i++) {
Serial.println(arr[i]);
}
// Пусть json имеет вид [[123,456],["abc","def"]], тогда ко вложенным массивам можно обратиться:
Serial.println(p[0][0]); // 123
Serial.println(p[0][1]); // 456
Serial.println(p[1][0]); // abc
Serial.println(p[1][1]); // def
Каждый элемент можно вывести в тип gson::Entry
по имени (из объекта) или индексу (из массива) и использовать отдельно, чтобы не "искать" его заново:
gson::Entry e = p["arr"];
Serial.println(e.length()); // длина массива
Serial.println(e[0]); // hello
Serial.println(e[1]); // true
Хэширование
GSON нативно поддерживает хэш-строки из StringUtils, работа с хэшами значительно увеличивает скорость доступа к элементам JSON документа по ключу. Строка, переданная в функцию SH
, вообще не существует в программе и не занимает места: хэш считается компилятором на этапе компиляции, вместо него подставляется число. А сравнение чисел выполняется быстрее, чем сравнение строк. Для этого нужно:
- Хэшировать ключи после парсинга:
p.parse(json);
p.hashKeys();
- Обращаться к элементам по хэшам ключей, используя функцию
sutil::SH
:
using sutil::SH;
void foo() {
Serial.println(p[SH("int")]);
Serial.println(p[SH("obj")][SH("float")]);
Serial.println(p[SH("array")][0]);
}
Примечание: для доступа по хэшу используется перегрузка
[size_t]
, а для доступа к элементу массива -[int]
. Поэтому для корректного доступа к элементам массива нужно использовать именноint
, а неuint8
иuint16
! Иначе компилятор может вызвать доступ по хэшу вместо обращения к массиву.
gson::Entry arr = p["arr"];
for (int i = 0; i < arr.length(); i++) { // счётчик int
Serial.println(arr[i]);
}
Сборка
JSON строка собирается линейно в обычную String
-строку, что очень просто и приятно для микроконтроллера:
gson::string gs; // создать строку
gs.beginObj(); // начать объект 1
gs.addString("str1", F("value"));// добавить строковое значение
gs["str2"] = "value2"; // так тоже можно
gs["int"] = 12345; // целочисленное
gs.beginObj("obj"); // вложенный объект 2
gs.addFloat(F("float"), 3.14); // float
gs["float2"] = 3.14; // или так
gs["bool"] = false; // Bool значение
gs.endObj(); // завершить объект 2
gs.beginArr("array"); // начать массив
gs.addFloat(3.14); // в массив - без ключа
gs += "text"; // добавить значение (в данном случае в массив)
gs += 12345; // добавить значение (в данном случае в массив)
gs += true; // добавить значение (в данном случае в массив)
gs.endArr(); // завершить массив
gs.endObj(); // завершить объект 1
gs.end(); // ЗАВЕРШИТЬ ПАКЕТ (обязательно вызывается в конце)
Serial.println(gs); // вывод в порт
Serial.println(gs.s); // вывод в порт (или так)
Версии
- v1.0
- v1.1 - улучшен парсер, добавлено хэширование ключей и обращение по хэш-кодам
- v1.2 - оптимизация под StringUtils 1.3
- v1.3 - оптимизация парсера, ускорение чтения значений из Parser
- v1.4 - оптимизация парсера, ускорение чтения, изначальная строка больше не меняется парсером
- v1.4.1 - поддержка ядра esp8266 v2.x
- v1.4.2 - добавлены Raw методы в string
Установка
- Библиотеку можно найти по названию GSON и установить через менеджер библиотек в:
- Arduino IDE
- Arduino IDE v2
- PlatformIO
- Скачать библиотеку .zip архивом для ручной установки:
- Распаковать и положить в C:\Program Files (x86)\Arduino\libraries (Windows x64)
- Распаковать и положить в C:\Program Files\Arduino\libraries (Windows x32)
- Распаковать и положить в Документы/Arduino/libraries/
- (Arduino IDE) автоматическая установка из .zip: Скетч/Подключить библиотеку/Добавить .ZIP библиотеку… и указать скачанный архив
- Читай более подробную инструкцию по установке библиотек здесь
Обновление
- Рекомендую всегда обновлять библиотеку: в новых версиях исправляются ошибки и баги, а также проводится оптимизация и добавляются новые фичи
- Через менеджер библиотек IDE: найти библиотеку как при установке и нажать "Обновить"
- Вручную: удалить папку со старой версией, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!
Баги и обратная связь
При нахождении багов создавайте Issue, а лучше сразу пишите на почту alex@alexgyver.ru
Библиотека открыта для доработки и ваших Pull Request'ов!
При сообщении о багах или некорректной работе библиотеки нужно обязательно указывать:
- Версия библиотеки
- Какой используется МК
- Версия SDK (для ESP)
- Версия Arduino IDE
- Корректно ли работают ли встроенные примеры, в которых используются функции и конструкции, приводящие к багу в вашем коде
- Какой код загружался, какая работа от него ожидалась и как он работает в реальности
- В идеале приложить минимальный код, в котором наблюдается баг. Не полотно из тысячи строк, а минимальный код