Obsługa formatu JSON w Arduino

W dzisiejszym artykule dowiemy się w jaki sposób parsować oraz tworzyć własne obiekty JSON w programach Arduino. Pomocna okaże się nam w tym biblioteka o nazwie ArduinoJson, której nazwa może być nieco myląca, gdyż sprawdzi się ona nie tylko w połączeniu z płytkami Arduino, ale także ESP8266, Teensy, a nawet w programie kompilowanym na komputer klasy PC.

Instalacja biblioteki

Bibliotekę możemy pobrać za pomocą Menedżera płytek w Arduino IDE. Wystarczy, że wpiszemy w wyszukiwarce nazwę ArduinoJson. Należy jednak tutaj zaznaczyć, że zanim wciśniemy przycisk Instaluj musimy sprawdzić czy wersja, którą pobieramy jest stabilna. W momencie pisania artykułu ostatnia stabilna wersja to 5.13.1. Jednak jeżeli nie zmienimy wersji to menedżer domyślnie zainstaluje nam wersję 6.4.0-beta.

Kiedy już zainstalujemy bibliotekę możemy zacząć z niej korzystać dołączając do naszego programu plik nagłówkowy ArduinoJson.h

#include <ArduinoJson.h>

Alokacja pamięci

Aby móc tworzyć nowe obiekty lub parsować ciągi znaków musimy zaalokować odpowiednią ilość pamięci. W tym celu posłużymy się klasą StaticJsonBuffer lub DynamicJsonBuffer. Zanim jednak wybierzemy, którą chcemu użyć musimy wiedzieć czym się różnią. Mówiąc najprościej bufor statyczny alokowany jest na stosie, a dynamiczny na stercie. O różnicy pomiędzy stosem a stertą można poczytać tutaj. W skrócie mając do dyspozycji niewiele RAMu tak jak w Arduino lepiej wykorzystać bufor statyczny.

Pamięć zaalokować możemy w ten sposób:

StaticJsonBuffer<JSON_BUFFER_CAPACITY> jsonBuffer;

Teraz wykorzystując ten bufor możemy utworzyć lub sparsować obiekt:

Parsowanie:

JsonObject& root = jsonBuffer.parseObject(jsonString);

Tworzenie obiektu:

JsonObject& root = jsonBuffer.createObject();

Skąd jednak wiedzieć jaki rozmiar buforu? Wystarczy, że wiemy jakie obiekty chcemy parsować/tworzyć. Twórca biblioteki udostępnił nam specjalny kalkulator pod adresem https://arduinojson.org/v5/assistant/. Wystarczy, że wkleimy tam obiekt, który chcemy przetwarzać i voila! W wyniku dostajemy rozmiar buforu oraz przykładowe kody :)

Poza wyrażeniem, które możemy podstawić pod stałą reprezentującą rozmiar buforu widizmy też jaki rozmiar będzie miał bufor dla podanych danych na różnych platformach.

Parsowanie obiektów

W moim przykłądowym kodzie pokażę jak odczytać, a następnie sparsować string przez port szeregowy:

#include <ArduinoJson.h>

#define JSON_BUFFER_CAPACITY  2 * JSON_OBJECT_SIZE(3) + 40;
#define MAX_COMMAND_SIZE      200
#define TIMEOUT               100

void setup() {
  Serial.begin(9600);
}

void loop() {
  StaticJsonBuffer<JSON_BUFFER_CAPACITY> jsonBuffer;
  unsigned long startTime = millis();
  char command[MAX_COMMAND_SIZE] = "";
  char temp;
  int charPosition = 0;
  bool gotLine = false;
  
  while(millis() - startTime <= TIMEOUT) {
    while(Serial.available() > 0 && !gotLine) {
      temp = Serial.read();
      startTime = millis();
      if(temp == '\r' || temp == '\n') {
        gotLine = true;
      }
      command[charPosition++] = temp;
    }
  }
  if (gotLine) {
    JsonObject& root = jsonBuffer.parseObject(command);
    gotLine = false;
    processJsonCommand(root);
  }
}

void processJsonCommand(JsonObject& jsonRoot) {
  // Tu możemy przetworzyć sparsowany obiekt
}

Wewnątrz funkcji processJsonCommand mamy już gotowy sparsowany obiekt, który odebraliśmy przez port szeregowy (możemy do niego podłączyć np. moduł bluetooth HC-05).

Przykładowo, aby odczytać wartość pola o nazwie name należy napisać taki oto kod:

const char* name = root["name"];

Wystarczy, że przypiszemy wartość do zmiennej odpowiedniego typu i już. Biblioteka zrobi za nas całą robotę ;)

Tworzenie obiektów

Jeżeli chcemy utworzyć obiekt/tablicę należy wywołąć metodę createObject lub createArray na obiekcie bufora.

JsonObject& root = jsonBuffer.createObject();

Aby przypisać dane do odpowiedniego pola wystarczy, że odwołamy się do niego jak do tablicy asocjacyjnej:

root["name"] = "Wartosc pola name";
root["id"] = 10;

Jeżeli chcemy dodać do naszego obiektu zagnieżdżenie wystarczy, że wywołamy na nim metodę createNestedObject lub createNestedArray:

JsonObject& data = root.createNestedObject("data");
data["r"] = 120;
data["g"] = 50;
data["b"] = 255;

Tym sposobem stworzyliśmy obiekt JSON o takiej strukturze:

{
  "name": "Wartosc pola name",
  "id": 10,
  "data": {
    "r": 120,
    "g": 50,
    "b": 255
  }
}

Teraz możemy wypisać go na port szeregowy jedną z 2 metod:

root.printTo(Serial); // Wypisywanie stringu JSON bez wcięć i w jednej linii
root.prettyPrintTo(Serial); // Wypisywanie ładnie sformatowanego stringu JSON (z wcięciami i nowymi liniami)

Ograniczenia

Niezależnie czy używamy statycznego, czy dynamicznego buforu JSON należy pamiętać, że nie powinniśmy współdzielić go pomiędzy różnymi obiektami. Może to spowodować niekontrolowane zachowanie programu w przypadku skończenia się buforu. Za każdym razem, kiedy musimy stworzyć nowy JSON należy zaalokować nowy bufor, a już na pewno wystrzegać się deklarowania bufru jako globalnej zmiennej.

Należy też pamiętać, że pomimo wygody formatu JSON, nie zawsze będzie on dobrym wyborem. Szczególnie w przypadku Arduino Uno, które ma tylko 2 kb pamięci RAM. Jeżeli będziemy przetwarzali małe dane to jak najbardziej możemy sobie ułtawić życie wykorzystaniem opisanej biblioteki. W przeciwnym wypadku powinniśmy rozważyć wprowadzenie autorskiego, minimalistycznego formatu lub zmianę procesora na jakiś z większą ilością pamięci (np. Teensy lub któryś z rodziny ESP).