Szybkie i niezawodne parsowanie JSON – przykład z UART #2

Witaj w kolejnym artykule na temat przetwarzania danych w formacie JSON! Dzisiaj wracam do Ciebie z drugą częścią, która skupi się na bardziej rozbudowanych i praktycznych przykładach analizy danych JSON.

W poprzedniej części przewodnika, omówiłem podstawy parsowania JSON i zaprezentowałem proste przykłady dostosowane do początkujących projektów. Teraz nadszedł czas, aby sięgnąć po bardziej zaawansowane przekształcania danych JSON, które są niezbędne w rzeczywistych projektach programistycznych.

W tym materiale pokażę Ci, jak radzić sobie z bardziej złożonymi strukturami danych JSON, obsługiwać błędy w parsowaniu, pracować z danymi tablicowymi oraz integrować analizę JSON z rzeczywistymi scenariuszami aplikacji. Mam nadzieję, że dostarczy Ci praktycznych narzędzi i wskazówek, które pomogą lepiej zrozumieć i wykorzystywać JSON w projektach.

Przydatne funkcje biblioteki lwjson

W poprzedniej części materiału korzystaliśmy w zasadzie tylko z obiektów typu string. Jednak w rzeczywistości, aplikacje często wymagają przesyłania różnego rodzaju danych, takich jak liczby, wartości logiczne true/false oraz null. Biblioteka parsująca JSON, tak jak ta, którą omawiamy, udostępnia narzędzia do obsługi różnych typów danych.

Parsowanie liczb

Jeśli potrzebujesz odczytać liczby z danych JSON, możesz skorzystać z funkcji takich jak lwjson_get_val_int() do odczytywania wartości całkowitych oraz lwjson_get_val_real() do odczytywania liczb rzeczywistych. To pozwala na precyzyjne odczytanie danych numerycznych z treści JSON.

Wartości logiczne i null

Wartości logiczne (true/false) oraz wartość null są przechowywane nieco inaczej. Nie znajdziesz ich w polu „value”, ale zamiast tego w polu „type” w tokenie. Może ono przyjąć jedną z trzech wartości: LWJSON_TYPE_TRUE, LWJSON_TYPE_FALSE lub LWJSON_TYPE_NULL. Dzięki temu jesteś w stanie identyfikować te specyficzne wartości w przesyłanych danych JSON.

Obsługa tokenów

Biblioteka parsująca JSON oferuje również zaawansowany interfejs do obsługi tokenów. W poprzedniej części omówiliśmy funkcję lwjson_find() do odnajdywania tokena o konkretnej nazwie. Jednakże, aby radzić sobie z bardziej skomplikowanymi strukturami JSON, jak tablice, możesz skorzystać z funkcji takich jak lwjson_find_ex() oraz lwjson_get_first_child(), które pozwalają na przeszukiwanie zagnieżdżonych danych.

Analiza obiektów typu „Array”

O ile odczytywanie stringów, liczb czy wartości logicznych nie powinno sprawić żadnych problemów, to analiza tablic może być trochę problematyczna. Dlaczego? Spróbujmy przeanalizować, jak przechowywane są dane w tokenach. Najpierw rzućmy okiem jak wyglądać będzie JSON, którym zajmiemy się w przykładzie.

{
  "BLINK_TIME":100,
  "LED_COLOR":
  [
	{
	  "NUMBER":0,
	  "RED":255,
	  "GREEN":0,
	  "BLUE":0
	},
	{
	  "NUMBER":1,
	  "RED":0,
	  "GREEN":255,
	  "BLUE":0
	},
	{
	  "NUMBER":2,
	  "RED":0,
	  "GREEN":0,
	  "BLUE":255
	},
	{
	  "NUMBER":3,
	  "RED":255,
	  "GREEN":255,
	  "BLUE":255
	}
  ]
}

Przechowuje on dane o kolorach czterech diod RGB w formie tablicy „LED_COLOR” (typ „array”). Oprócz tego przesyłamy informację o czasie pomiędzy kolejnymi mignięciami diod.

Podstawowym problemem, jaki rzuca się w oczy już na początku, jest to, że nazwy „NUMBER”, „RED”, „GREEN” oraz „BLUE” występują kilka razy. A to oznacza, że zwykłe wyszukiwanie za pomocą funkcji lwjson_find() nie zadziała, bo nie wyszuka nam wszystkich obiektów. Drugą kwestią jest odpowiednie nawigowanie w algorytmie analizy danych po tablicy „LED_COLOR”. Widzimy, że są w niej umieszczone obiekty, a dopiero w tych obiektach znajdują się dane.

Na screenach poniżej wrzuciłem podgląd z tablicy tokenów po przeprowadzeniu parsowania za pomocą biblioteki lwjson.

W jaki sposób w takim razie napisać program, aby poprawnie pobrać dane o LED-ach? Musimy w odpowiedni sposób użyć funkcji lwjson_find_ex() oraz przełączać się pomiędzy kolejnymi elementami w tablicy. Teraz opiszę jedynie kwestię analizy tablicy (array) o nazwie LED_COLOR. Resztę projektu omówimy w dalszej części materiału. Spróbujmy przeanalizować to po kolei.

Odczytane dane o kolorach poszczególnych barw RGB będziemy przechowywali w prostej tablicy struktur.

typedef struct
{
	uint8_t red;
	uint8_t green;
	uint8_t blue;
} led_rgb_t;

led_rgb_t led[NUMBER_OF_LEDS];

W pierwszej kolejności musimy znaleźć tablicę LED_COLOR. W tym celu wywołamy znaną już funkcję lwjson_find().

t = lwjson_find(&lwjson, "LED_COLOR");

Następnie musimy pobrać pierwszy obiekt z tej tablicy. W bibliotece lwjson nazywane są one dziećmi (child).

tmp = lwjson_get_first_child(t);

Teraz możemy przejść do analizy pierwszego obiektu. Dane będziemy przeglądać w pętli for dopóki będą tam znajdowały się kolejne dane o LED-ach.

for(uint32_t i=0; i<NUMBER_OF_LEDS; i++)

Najpierw szukamy pola o nazwie NUMBER, żeby dowiedzieć się, której diody dane dotyczą. Przy przesyłaniu JSON nie mamy gwarancji, że diody będą przesłane po kolei (mogą być, ale nie muszą – to zależy od aplikacji na PC). Taka elastyczność to ogromna zaleta formatu JSON, ale sprawia, że musimy też elastycznie dane przetwarzać. Dlatego najpierw szukamy numeru diody. Jak już wspomniałem, pola NUMBER występują w całym JSON-ie kilka razy, dlatego szukamy tylko w danym elemencie tablicy za pomocą funkcji lwjson_find_ex().

t = lwjson_find_ex(&lwjson, tmp, "NUMBER");

Jeżeli znajdziemy takie pole (token nie jest pusty), zapisujemy sobie numer i szukamy danych o kolorach. Jeżeli token jest pusty, oznacza to, że w tablicy nie ma obiektu i możemy zakończyć szukanie.

if (t != NULL)
{
	nbr = lwjson_get_val_int(t);
}
else
{
	break;
}

Teraz szukamy kolejno pól o nazwie RED, GREEN i BLUE. Jak znajdziemy, to zapisujemy sobie dale o danym kolorze.

t = lwjson_find_ex(&lwjson, tmp, "RED");

if (t != NULL)
{
	led[nbr].red = lwjson_get_val_int(t);
}

t = lwjson_find_ex(&lwjson, tmp, "GREEN");

if (t != NULL)
{
	led[nbr].green = lwjson_get_val_int(t);
}

t = lwjson_find_ex(&lwjson, tmp, "BLUE");

if (t != NULL)
{
	led[nbr].blue = lwjson_get_val_int(t);
}

Teraz musimy przejść do kolejnego elementu tablicy. Autor biblioteki w każdym tokenie zapisuje wskaźnik na kolejny element, dlatego wystarczy wywołać kod jak poniżej.

tmp = tmp->next;

W ten sposób w kolejnej iteracji peli for mamy już następny obiekt i go analizujemy. W ten sposób przechodzimy przez wszystkie elementy tablicy LED_COLOR – bez znaczenie czy będą tam jedna, dwie, czy trzy diody.

Przykład z diodami RGB

Wiemy już, w jaki sposób przeglądać bardziej złożone dane, jak tablice obiektów. Teraz przygotujmy pełny projekt, na którym przetestujemy działanie algorytmu.

Konfiguracja projektu

Rozpocznijmy od utworzenia nowego projektu w programie CubeMX. Wybieramy opcję „File -> New -> STM32 Project” i nadajemy projektowi odpowiednią nazwę. Warto zaznaczyć, że zalecane jest pozostawienie domyślnej inicjalizacji układów peryferyjnych.

W naszym projekcie wykorzystamy interfejs USART w trybie asynchronicznym z prędkością 115200 bps. Interfejs USART2 (podłączony do portu USB) mamy już włączony dzięki domyślnej inicjalizacji peryferiów. Aby w wygodny sposób odbierać dane, włączymy jeszcze kontroler DMA1 na kanale 6 oraz przerwania. Docelowo chciałbym wykorzystać w projekcie przerwanie od zakończenie transmisji (IDLE), dzięki czemu otrzymamy przerwanie po przesłaniu całego polecenia JSON.

Obrazek posiada pusty atrybut alt; plik o nazwie CubeMX-UART-DMA-2.jpg
Konfiguracja DMA 1 Channel 6
Włączenie przerwań od USART2

W dzisiejszym przykładzie potrzebujemy kilku diod LED RGB. Postanowiłem wykorzystać pasek z adresowanymi LED-ami WS2813. Poniżej przedstawię tylko konfigurację pinu i timera, do którego podłączamy linie danych. Pełen opis działania tego typu diod znajdziesz w jednym w moim wcześniejszych wpisów pod tytułem „Adresowane diody WS2813, czyli PWM i DMA w akcji”. Wykorzystamy TIMER 2 i kanał 1 jako wyjście PWM.

Włączenie kanału 1 TIMER 2 w trybie wyjścia PWM

Aby otrzymać sygnał o częstotliwości ok. 800 kHz, który wymagany jest do sterowania WS2813, musimy odpowiednio dobrać wartość Prascalera i Period. Ze względu na to, że STM32L476RG pracuje z maksymalną częstotliwością 80 MHz, musimy go podzielić przez 100. Przykładowo możemy więc zastosować Preskaler o wartości 0 oraz Period o wartości 100. Kanał 1 będzie pracował w trycie PWM1 oraz polaryzacją High.

Konfiguracja parametrów TIMER2 i kanału 1

Chcemy wykorzystać kontroler DMA do przesyłania danych. Aby go skonfigurować przechodzimy do zakładki DMA Settings. Wybieramy opcję Add i wybieramy kierunek przepływu danych Memory to Peripheral. Zaznaczamy opcję inkrementacji adresów przy Memory oraz szerokość danych jako Word przy Peripheral (rejestr CCR1 w TIMER 2 jest 32 bitowy) oraz Byte przy Memory (będziemy dane przesyłać bajtami).

Kanał 1 TTIMER2 dostępny jest na pinie PA0. Na Nucleo znajdziesz go na złączu CN8. Poniżej znajduje się schemat podłączenia taśmy/paska LED. Warto wykorzystać tutaj dodatkowo konwerter poziomów napięć, aby zapewnić prawidłowe napięcia przy stanie wysokim dla WS2813.

Pasek LED z diodami należy podłączyć do płytki Nucleo zgodnie ze schematem poniżej. Warto pamięć o dobrej jakości połączeniach i niezbyt długich przewodach, aby zakłócenia nie powodowały nam błędów w przesyłanych danych.

Schemat podłączenia taśmy LED z diodami WS2813

Kod programu

Możemy wygenerować projekt (Alt+K) i przejść do napisania kodu obsługi komunikacji i sterowania diodami. Do projektu musimy dodać bibliotekę lwjson. Dokładny opis znajdziesz w poprzednim artykule pt. „Szybkie i niezawodne parsowanie JSON – przykład z UART #1„. Dodajemy również bibliotekę do obsługi diod WS2813 zgodnie z opisem „Adresowane diody WS2813, czyli PWM i DMA w akcji”.

Odbieranie danych przez USART2

W pierwszej kolejności obsłużymy odbieranie danych przez interfejs szeregowy USART2. Przed funkcją main() dodajemy tablicę, w której będziemy przechowywali odebrane polecenia JSON oraz zmienną, która poinformuje o odebraniu danych.

uint8_t rx_buffer[256];
bool data_received = false;

Jak już wspominałem, do odbierania pełnych poleceń wykorzystamy przerwanie od wolnej linii (IDLE). Zostanie wywołane w momencie, gdy na linii RX interfejsu USART2 zakończy się transmisja. W funkcji main(), ale przed pętlą while(1) wywołujemy odbieranie danych.

HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buffer, 256);

Obsługę przerwania umieściłem w funkcji HAL_UARTEx_RxEventCallback(). Przy jej wywołaniu przez bibliotekę HAL-a na końcu bufora z danymi dodajemy znak '\0′. Biblioteka lwjson pracuje na stringach, dlatego nasze polecenie musi kończyć się tym znakiem. Poza tym ustawiamy flagę data_received oraz ponownie wywołujemy odbierania danych.

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	if (huart->Instance == USART2)
	{
		rx_buffer[Size] = '\0';
		data_received = true;
		HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buffer, 256);
	}
}

Parsowanie danych z lwjson

W pliku „main.c” załączamy bibliotekę lwjson, ws2813b oraz biblioteki do obsługi zmiennych bool i stringów, ponieważ będziemy z nich korzystać w projekcie.

#include <stdbool.h>
#include <string.h>


#include "lwjson/lwjson.h"
#include "led_ws2813b.h"

W funkcji main() utworzyłem zmienne pomocnicze.

  led_state_t led_state = LED_OFF;
  uint32_t blink_time = 1000;
  led_rgb_t led[NUMBER_OF_LEDS] = {0};
  uint32_t stimer = 0;
  uint32_t nbr;

Teraz możemy przystąpić do inicjalizacji biblioteki lwjson. Potrzebne nam będą dwie zmienne. Jedna to tablica typu lwjson_token_t. Jest to tablica przechowująca obiekty (tokeny) sparsowane przez bibliotekę. Druga to struktura typu lwjson_t, gdzie przechowywane są zmienne pomocnicze niezbędne w procesie parsowania.

  lwjson_token_t tokens[128];
  lwjson_t lwjson;

Bibliotekę inicjalizujemy, wywołując funkcję lwjson_init(). Jako argumenty podajemy obiekty typu lwjson_t, tablicę z tokenami oraz rozmiar tej tablicy.

lwjson_init(&lwjson, tokens, LWJSON_ARRAYSIZE(tokens));

W tym miejscu warto także zainicjalizować obsługę ledów.

led_init();

Do migania diodami zgodnie z czasem przesłanym w JSON-ie wykorzystamy timer programowy. Zmienna stimer posłuży nam do przechowywania ticków timera systemowego. Przed wejściem w pętlę while(1) pobieramy aktualny czas.

stimer = HAL_GetTick();

W petli while czekamy na przyjście danych z UART, a następnie analizujemy je zgodnie z opisanym już wcześniej algorytmem.

if (data_received == true)
{
  data_received = false;


  if (lwjson_parse(&lwjson, (char*)rx_buffer) == lwjsonOK)
  {
	  const lwjson_token_t* t;
	  const lwjson_token_t* tmp;

	  if ((t = lwjson_find(&lwjson, "BLINK_TIME")) != NULL)
	  {
		  blink_time = lwjson_get_val_int(t);
	  }

	  t = lwjson_find(&lwjson, "LED_COLOR");
	  tmp = lwjson_get_first_child(t);

	  for(uint32_t i=0; i<NUMBER_OF_LEDS; i++)
	  {
			t = lwjson_find_ex(&lwjson, tmp, "NUMBER");

			if (t != NULL)
			{
				nbr = lwjson_get_val_int(t);
			}
			else
			{
				break;
			}

			t = lwjson_find_ex(&lwjson, tmp, "RED");

			if (t != NULL)
			{
				led[nbr].red = lwjson_get_val_int(t);
			}

			t = lwjson_find_ex(&lwjson, tmp, "GREEN");

			if (t != NULL)
			{
				led[nbr].green = lwjson_get_val_int(t);
			}

			t = lwjson_find_ex(&lwjson, tmp, "BLUE");

			if (t != NULL)
			{
				led[nbr].blue = lwjson_get_val_int(t);
			}

			tmp = tmp->next;
	  }

	  lwjson_free(&lwjson);
  }
}

Następnie sterujemy diodami LED. Za pomocą stanu led_state generujemy miganie diod.

if ((HAL_GetTick() - stimer) > blink_time)
{
	stimer = HAL_GetTick();

	if (led_state == LED_OFF)
	{
		led_state = LED_ON;

		for (int i=0; i<NUMBER_OF_LEDS; i++)
			led_set_one_led_colors(i, led[i].green, led[i].red, led[i].blue);

		led_send_led_colors();
	}
	else if (led_state == LED_ON)
	{
		led_state = LED_OFF;

		led_set_all_led_colors(0, 0, 0);
		led_send_led_colors();
	}
}

Po wgraniu programu na płytkę wystarczy wysłać JSON za pomocą terminalu szeregowego np. Realterm. Uproszczone (bez białych znaków), przykładowe polecenie do 4 diod LED zamieszczam poniżej.

{"BLINK_TIME":100,"LED_COLOR":[{"NUMBER":0,"RED":255,"GREEN":0,"BLUE":0},{"NUMBER":1,"RED":0,"GREEN":255,"BLUE":0},{"NUMBER":2,"RED":0,"GREEN":0,"BLUE":255},{"NUMBER":3,"RED":255,"GREEN":255,"BLUE":255}]}	

Podsumowanie

W ten sposób obsłużyliśmy sterowanie kilkoma diodami LED na podstawie danych z tablicy JSON. Analizowanie typu array w JSON-ach może być początkowo trochę zawiłe, ale mam nadzieję, że artykuł pomoże Ci szybko dodać także bardziej rozbudowane JSON-y do własnych aplikacji.

Im częściej używam JSON-ów we własnych aplikacjach do wysyłania komunikatów, tym bardziej doceniam elastyczność i prostotę tego rozwiązania. JSON-y pozwalają w łatwy sposób dodawać oraz usuwać w trakcie rozwoju fragmenty danych. Jednocześnie możemy przesyłać różnej długości polecenia dotyczące tych samych elementów – to daje dużą swobodę działania.

Oczywiście JSON-y nie są rozwiązaniem na wszystkie problemy. Często w systemach embedded zależy nam przede wszystkim na przesyłaniu jak najmniejszej ilości danych ze względu na różne ograniczenia interfejsów lub czas transmisji. Wtedy musimy stosować mniej przejrzyste, ale krótsze protokoły binarne. Jeżeli jednak nie zależy nam aż tak bardzo na czasie komunikacji, warto pomyśleć o zastosowaniu formatu JSON.

Na dzisiaj to wszystko. W kolejnym wpisie będę chciał pokazać, w jaki sposób analizować bardziej rozbudowane JSON-y. Jeżeli podobał Ci się wpis lub masz jakieś własne przemyślenia, zostaw komentarz. Pamiętaj też, aby polubić mój profil na Facebook-u oraz zasubskrybować kanał na YouTube.

Repozytorium GitHub

Materiały dodatkowe

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *