En esta entrada vamos a ver cómo trabajar (serializar y deserializar) con ficheros Json con un microprocesor compatible con el ecosistema de Arduino gracias a la genial librería Arduino Json.
La mayoría de lenguajes disponen de librerías para escribir (serializar) o leer/parsear (deserializar) ficheros Json (por ejemplo, ya vimos un ejemplo en C#). El entorno no es una excepción, y disponemos de la Arduino Json que incorpora funciones para serializar y deserializar objetos de forma sencilla.
Intercambiar información como texto plano puede parecer extraño ya que supone un trabajo adicional de interpretación (parseo) de los datos frente, por ejemplo, a recibir directamente un stream binario. Sin embargo, aporta la ventaja de compatibilizar sistemas diferentes. Se acabo eso de preocuparse de si “mi integer son 32 bits, y tu integer son 16bits”.
Arduino Json es compatible con múltiples placas de desarrollo (Arduino Uno, Nano, Mega, Micro, Leonardo, Due, ESP8266, ESP32…). En las placas con más pequeñas (8 bits) va “un poco justa”. Pero, por ejemplo, encaja perfectamente con el ESP8266 y el ESP32.
La librería Arduino Json es Open Source y podéis añadirla a vuestro proyecto de forma cómoda desde el gestor de librerías del Arduino IDE. Su código está disponible en https://arduinojson.org.
Vamos a profundizar y ver ejemplos de esta interesante librería, que nos permitirá emplear el formato Json en nuestros proyectos de Arduino.
¿Qué es un fichero Json?
Json (JavaScript Object Notation) es un formato de texto plano para almacenar datos estructurados que se ha impuesto como sistema “casi” estándar de intercambio de información en sistemas de comunicación. De hecho, es un componente habitual en el funcionamiento de las mayoría de páginas web.
Json ha ganado presencia frente a otros formatos como XML por su ligereza y sencillez. Esto se traduce en un menor consumo de banda y menor carga para el servidor y el cliente. Además es un formato que es “más o menos” sencillo de leer incluso por una persona.
De forma resumida, Json dispone de 4 tipos de objetos básicos (números, booleanos, texto, y null), que pueden agruparse en arrays o en objetos.
Un objeto es una colección de “clave/valor”. Es decir, una colección de objetos identificado por una clave de texto. El acceso a los objetos se realiza a través de su clave.
{
"id": 12,
"description": "demoItem",
"value": 50.4
}
Por su parte un array es una lista de objetos ordenados en forma secuencial. El acceso a los objetos se realiza a través de su posición.
[
"banana",
12,
3.1416
]
Por supuesto, ambos tipos (array y objetos) se pueden componer para crear objetos anidados (“nested”). Por ejemplo, el siguiente Json simboliza un objeto de una persona con Id 12, y nombre Peter, con un array con las frutas que le gustan, y un objeto anidado con su dirección.
{
"id": 12,
"name": "peter",
"fruits": [
"banana",
"orange",
"raspberry"
],
"house": {
"address": "white citadel",
"city": "gondor"
}
}
Usando Arduino Json
La “magia” de Arduino Json, en su versión 6, reside en el objeto JsonDocument que abstrae un documento Json y dispone de las herramientas para facilitar su serialización y deserialización.
Internamente, la librería dispone objetos para representar Arrays (JsonArray), objetos (JsonObject) y las relaciones clave/valor (JsonVariant) que admiten String, números enteros, números con coma flotante, booleanos y null. Sin embargo, en la mayoría de ocasiones, es transparente durante su uso.
En primer lugar creamos un JsonDocument que puede ser de tipo StaticJsonDocument o DynamicJsonDocument (veremos la diferencia a continuación).
StaticJsonDocument<200> doc;
DynamicJsonDocument doc(1024);
Trabajando con JsonDocument
Al generar un fichero JsonDocument este está “vacío”. Internamente se convertirá en un Array o en un Objeto en función de como lo usemos por primera vez.
Así, si empleamos una propiedad clave/valor, el JsonDocument pasa a ser un objeto,
doc["hola"] = "mundo";
Mientras que si usamos ‘add()’ para añadir un objeto, el JsonDocument pasa a ser un array,
doc.add("hola mundo");
Así mismo, podemos crear objetos o arrays anidados. Así crearíamos un objeto anidado,
JsonObject obj = doc.createNestedObject();
obj["hello"] = "world";
O arrays anidados de la siguiente forma. En este caso, el parámetro proporcionado es opcional, y representa el nombre que queramos que tenga el array dentro del objeto “padre”.
JsonArray obj = doc.createNestedArray("nested");
obj.add("hola mundo");
En este caso el parámetro proporcionado es opcional, y representa el nombre que queramos que tenga el array dentro del objeto “padre”.
Dynamic o Static JsonDocument
Una duda que surge frecuentemente al usar Arduino Json es la diferencia y cuando usar StaticJsonDocument o DynamicJsonDocument.
StaticJsonDocument se genera durante la compilación y se almacena en la pila. Pare generarlo, se emplea templating para indicar la memoria disponible para el fichero Json.
StaticJsonDocument<200> doc;
Mientras que DynamicJsonDocument emplea memoria dinámica y se almacena en la memoria de variables (‘heap’). Para generarlo se emplea el constructor, que recibe como parámetro la memoria máxima disponible para el Json.
DynamicJsonDocument doc(1024);
Esta diferencia tiene consideraciones sobre su uso. En forma resumida son:
- DynamicJsonDocument es un poco más lento
- DynamicJsonDocument permite generar ficheros más grandes
Por tanto, el propio desarrollador de Arduino Json aconseja emplear StaticJsonDocument para ficheros de menos de 1KB, y DynamicJsonDocument para documentos mayores de 1KB.
Memoria del JsonDocument
Como hemos visto, al generar cualquiera de las dos variantes del JsonDocument tenemos que indicar la memoria máxima disponible. Este debe ser superior al tamaño del objeto que queremos codificar y, en general, es difícil de calcular porque depende del tipo de variable, del anidamiento, y de la placa que estemos usada.
Para su cálculo exacto tenemos tres opciones. En forma resumida, una es emplear un tamaño superior, y usar la función ‘memoryUsage()’ para determinar el tamaño real. Otra, es usar ciertas constantes proporcionadas por la librería y hacer “la suma”. Y finalmente, la preferida es usar el asistente proporcionado por el desarrollador disponible en https://arduinojson.org/v6/assistant/.
Sin embargo, durante la fase de desarrollo, tampoco os importe demasiado simplemente usar un tamaño suficientemente grande y ajustar el tamaño posteriormente cuando paséis la fase de prototipo y si vuestro proyecto realmente lo necesita.
El asistente proporciona muchas más herramientas, como un “generador” de código para serializar y deserializar el código.
Serializar y deserializar
Podemos serializar el JsonDocument mediante una de las sobrecargas del método ‘serializeJson(…)‘.
serializeJson(const JsonDocument& doc, char* output, size_t outputSize);
serializeJson(const JsonDocument& doc, char output[size]);
serializeJson(const JsonDocument& doc, Print& output);
serializeJson(const JsonDocument& doc, String& output);
serializeJson(const JsonDocument& doc, std::string& output);
serializeJson(const JsonDocument& doc, std::ostream& output);
O deeserializar un fichero Json a un objeto mediante una de las sobrecargas del método ‘deserializeJson(…)’, que devuelve un valor indicando si la conversión se ha realizado correctamente.
DeserializationError deserializeJson(JsonDocument& doc, const char* input);
DeserializationError deserializeJson(JsonDocument& doc, const char* input, size_t inputSize);
DeserializationError deserializeJson(JsonDocument& doc, const __FlashStringHelper* input);
DeserializationError deserializeJson(JsonDocument& doc, const __FlashStringHelper* input, size_t inputSize);
DeserializationError deserializeJson(JsonDocument& doc, const String& input);
DeserializationError deserializeJson(JsonDocument& doc, const std::string& input);
DeserializationError deserializeJson(JsonDocument& doc, Stream& input);
DeserializationError deserializeJson(JsonDocument& doc, std::istream& input);
Configuración adicional
La librería Arduino Json tiene distintas opciones que podemos modificar durante la compilación para variar su comportamiento.
ARDUINOJSON_DECODE_UNICODE Permite emplear caracteres unicode (\uXXXX)
ARDUINOJSON_DEFAULT_NESTING_LIMIT: Define el límite de anidado
ARDUINOJSON_ENABLE_NAN Emplear ‘NaN’ o ‘null’
ARDUINOJSON_ENABLE_INFINITY Emplear ‘Infinity’ o ‘null’
ARDUINOJSON_NEGATIVE_EXPONENTIATION_THRESHOLD Usar notación científica para números pequeños
ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD Usar notación científica para números grandes1
ARDUINOJSON_USE_LONG_LONG Emplar ‘long’ o ‘long long’ para números enteros
ARDUINOJSON_USE_DOUBLE Emplear ‘float’ o ‘double’ para números en coma flotante
Para más información leer la documentación de la librería
Ejemplos de Arduino Json
Como de costumbre, lo mejor es que veamos unos cuantos ejemplos para ilustrar el uso de Arduino Json. Vamos a ver tres sencillos, uno para ilustrar el uso como objetos, otro para uso como array, y un ejemplo final más complejo que muestre el uso de propiedades anidadas.
Ejemplo objeto
El siguiente código muestra la serialización y deserialización de un Json sencillo como objeto.
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>
void SerializeObject()
{
String json;
StaticJsonDocument<300> doc;
doc["text"] = "myText";
doc["id"] = 10;
doc["status"] = true;
doc["value"] = 3.14;
serializeJson(doc, json);
Serial.println(json);
}
void DeserializeObject()
{
String json = "{\"text\":\"myText\",\"id\":10,\"status\":true,\"value\":3.14}";
StaticJsonDocument<300> doc;
DeserializationError error = deserializeJson(doc, json);
if (error) { return; }
String text = doc["text"];
int id = doc["id"];
bool stat = doc["status"];
float value = doc["value"];
Serial.println(text);
Serial.println(id);
Serial.println(stat);
Serial.println(value);
}
void setup()
{
Serial.begin(115200);
Serial.println("===== Object Example =====");
Serial.println("-- Serialize --");
SerializeObject();
Serial.println();
Serial.println("-- Deserialize --");
DeserializeObject();
}
void loop()
{
}
El fichero Json que hemos generado/leído con es el siguiente.
{
"text": "myText",
"id": 10,
"status": true,
"value": 3.14
}
Si ejecutamos el código veremos el siguiente resultado por puerto serie.
Ejemplo array
El siguiente código muestra la serialización y deserialización de un Json sencillo como array.
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>
void SerializeArray()
{
String json;
StaticJsonDocument<300> doc;
doc.add("B");
doc.add(45);
doc.add(2.1728);
doc.add(true);
serializeJson(doc, json);
Serial.println(json);
}
void DeserializeArray()
{
String json = "[\"B\",45,2.1728,true]";
StaticJsonDocument<300> doc;
DeserializationError error = deserializeJson(doc, json);
if (error) { return; }
String item0 = doc[0];
int item1 = doc[1];
float item2 = doc[2];
bool item3 = doc[3];
Serial.println(item0);
Serial.println(item1);
Serial.println(item2);
Serial.println(item3);
}
void setup()
{
Serial.begin(115200);
Serial.println("===== Array Example =====");
Serial.println("-- Serialize --");
SerializeArray();
Serial.println();
Serial.println("-- Deserialize --");
DeserializeArray();
Serial.println();
}
void loop()
{
}
El fichero Json que hemos generado/leído con es el siguiente.
["B", 45, 2.1728, true]
Si ejecutamos el código veremos el siguiente resultado por puerto serie.
Ejemplo complejo
El siguiente código muestra la serialización y deserialización de un Json con un array y un objeto anidados.
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>
void SerializeComplex()
{
String json;
StaticJsonDocument<300> doc;
doc["text"] = "myText";
doc["id"] = 10;
doc["status"] = true;
doc["value"] = 3.14;
JsonObject obj = doc.createNestedObject("nestedObject");
obj["key"] = 40;
obj["description"] = "myDescription";
obj["active"] = true;
obj["qty"] = 1.414;
JsonArray arr = doc.createNestedArray("nestedArray");
arr.add("B");
arr.add(45);
arr.add(2.1728);
arr.add(false);
serializeJson(doc, json);
Serial.println(json);
}
void DeserializeComplex()
{
String json = "{\"text\":\"myText\",\"id\":10,\"status\":true,\"value\":3.14,\"nestedObject\":{\"key\":40,\"description\":\"myDescription\",\"active\":true,\"qty\":1.414},\"nestedArray\":[\"B\",45,2.1728,true]}";
StaticJsonDocument<300> doc;
DeserializationError error = deserializeJson(doc, json);
if (error) { return; }
String text = doc["text"];
int id = doc["id"];
bool stat = doc["status"];
float value = doc["value"];
Serial.println(text);
Serial.println(id);
Serial.println(stat);
Serial.println(value);
int key = doc["nestedObject"]["key"];
String description = doc["nestedObject"]["description"];
bool active = doc["nestedObject"]["active"];
float qty = doc["nestedObject"]["qty"];
Serial.println(key);
Serial.println(description);
Serial.println(active);
Serial.println(qty);
String item0 = doc["nestedArray"][0];
int item1 = doc["nestedArray"][1];
float item2 = doc["nestedArray"][2];
bool item3 = doc["nestedArray"][3];
Serial.println(item0);
Serial.println(item1);
Serial.println(item2);
Serial.println(item3);
}
void setup()
{
Serial.begin(115200);
Serial.println("===== Complex Example =====");
Serial.println("-- Serialize --");
SerializeComplex();
Serial.println();
Serial.println("-- Deserialize --");
DeserializeComplex();
Serial.println();
}
void loop()
{
}
El fichero Json que hemos generado/leído con es el siguiente.
{
"text": "myText",
"id": 10,
"status": true,
"value": 3.14,
"nestedObject": {
"key": 40,
"description": "myDescription",
"active": true,
"qty": 1.414
},
"nestedArray": ["B", 45, 2.1728, true]
}
Si ejecutamos el código veremos el siguiente resultado por puerto serie.
Conclusión
La librería Arduino Json es una pequeña maravilla Open Source. Arduino Json destaca es en proyectos que incorporan comunicación Web. Pero no está exclusivamente restringido a ello y podemos usarlo, por ejemplo, para enviar la información de posición de un robot por Bluetooth, los registros de un datalogger, o la configuración u órdenes a un sistema.
Sin embargo, pese al esfuerzo de los desarrolladores en conseguir una librería muy potente y eficiente, el proceso de conversión siempre va a suponer un consumo de memoria y procesador. Por tanto, su uso no tiene porque ser adecuada para todos los proyectos. En particular, no será una buena solución para aplicaciones con altas velocidades de transmisión.
A un con todo, debería ser una de vuestras primeras opciones a la hora de intercambiar información, sobre todo entre sistemas diferentes, y una de vuestras librerías favoritas. En el futuro la usaremos de forma intensiva en la sección sobre el ESP8266 para procesar información en comunicaciones web.
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.