Le format JSON permet d’organiser, stocker et transmettre des données. Très utilisé dans le monde informatique il trouve également sa place dans l’univers des micro-contrôleurs. Parmi toutes les librairies disponibles pour Arduino, ESP32 et ESP8266, ArduinoJSON est la plus populaires et la plus aboutie.
Dans ce tutoriel nous allons également apprendre comment formater puis envoyer sur le port série des données sans avoir besoin d’aucune librairie avec du code Arduino standard.
Sommaire
- 1 C’est quoi le format JSON ?
- 2 Le vocabulaire JSON
- 3 Comment formater des données au format JSON sans librairie Arduino
- 4 Comment tester en ligne les chaînes au format JSON
- 5 Dans quel cas utiliser (ou pas) une librairie Arduino JSON ?
- 6 Installer la librairie ArduinoJson sur l’IDE Arduino
- 7 Comment déterminer la taille du buffer ?
- 8 Créer un objet JSON de type StaticJsonDocument
- 9 Comment modifier une valeur dans l’objet JSON
- 10 Sérialiser l’objet JSON, le convertir en une chaîne de caractère
- 11 Désérialiser (Parser), comment convertir une chaine en objet JSON
- 12 Comment extraire des données stockées dans un objet JSON ?
- 13 Tutoriels et projets pour aller plus loin avec JSON sur micro-contrôleurs
- 14 Mises à jour
C’est quoi le format JSON ?
JSON (mour JavaScript Object Notation) est un format de données textuelle (wikipedia) qui permet de structurer les données pour le stockage et le transfert. Il remplace très avantageusement le format XML. C’est le format de données de prédilection du langage Javascript.
Il est souvent utilisé dans des services tels que les API (interfaces de programmation d’application) et les services Web qui fournissent des données publiques.
Sans trop rentrer dans les détails, voici comment ça fonctionne :
- Les données sont représentées par des paires clé:valeur
- Le signe deux-points : sépare la clé de la valeur
- Le signe guillemet ” permet d’encadrer une chaîne de caractères
- les paires clé:valeur sont séparées par des virgules , (sauf la dernière ligne)
- Les accolades {} contiennent des objets. On peut y stocker ce que l’on souhaite à condition de respecter le formaliste JSON ci-dessus
- Les crochets [] contiennent des tableaux
Ce qui donne par exemple une structure
{
"cle1":"valeur1",
"cle2":"[1,2,3,4]",
"cle3":{
"clea":"valeura",
"cleb":"['a','b','c']"
}
}
Ou un tableau
[{
"temperature": 18.6,
"humidity": 52.5
},{
"temperature": 19.1,
"humidity": 52.3
}]
Dernière remarque, le format JSON n’est rien d’autre qu’une chaîne de caractères, ce qui donne pour l’exemple précédent
[{"temperature":"18.6","humidity":"52.5"},{"temperature":"19.1","humidity":"52.3" }]
Le vocabulaire JSON
Avant d’aller plus loin, voyons un peu de vocabulaire que l’on rencontre très souvent
Sérialiser | Serializer transforme un objet JSON en chaine de caractère
Désérialiser | Deserializer transforme une chaine en objet JSON
Parser identique à désérialiser
Comment formater des données au format JSON sans librairie Arduino
En dehors de la mémoire, les objets JSON sont stockés (le plus souvent) sous la forme de chaine de caractères.
Donc si vous n’avez pas besoin de manipuler les données dans votre projet, inutile de vous encombrer d’une librairie consommatrice de ressource.
Prenons par exemple ce projet ou l’on collecte le taux d’humidité du sol avec une sonde. Il est très facile d’envoyer sur le port série les mesures pour les afficher ensuite sur Node-RED sous la forme d’un graphique.
Voici deux exemples de code Arduino. Le code fait exactement la même chose :
- A gauche, on envoie au fur et à mesure sur le port série la chaîne JSON.
- A droite, on prépare la chaine JSON et ensuite on l’envoie sur le port série
Ce qui est important dans les deux cas, c’est de terminer par un println() de façon à envoyer le caractère de contrôle “\n” de retour à la ligne. De cette façon Node-RED saura détecter les nouvelles mesures entrantes.
Si le projet vous intéresse, voici le lien pour continuer la lecture
A quoi sert le caractère échappatoire \ ?
Certains langages tel que le C++ n’autorisent que le guillemet ” pour définir une chaîne de caractère. Malheureusement, le format JSON nécessite d’encadrer les clés entre guillemets ainsi que les valeurs dès que c’est une chaîne de caractères.
C’est là qu’intervient le caractère échappatoire \ . Il suffit de le placer devant de guillemet comme ceci \” pour éviter les erreurs de compilation.
Comment tester en ligne les chaînes au format JSON
Nous avons vu précédemment de petites exemples. Dans un projet réel, vous risquez de rapidement devoir traiter des objets de grande taille. Mis à plat, c’est parfaitement illisible et difficile (impossible) à débogguer.
Pour tester vos JSON, il existe de nombreux outils en ligne. Je vous conseille jsonlint
Dans quel cas utiliser (ou pas) une librairie Arduino JSON ?
Le language Arduino permet de créer des structures contenant des données dans différents formats. C’est un équivalent du format JSON.
Cependant, il peut s’avérer utilise d’utiliser une librairie permettant de manipuler des objets JSON dans plusieurs cas :
- Lorsque les données doivent être stockées (sur une carte SD ou la mémoire Flash) et rechargées
- Lorsqu’on récupère des données dans ce format, par exemple des données ou des paramètres depuis un serveur
- Lorsqu’on doit manipuler les données
Et éviter de l’utiliser dans les cas suivants :
- Un système d’acquisition de données. Il est préférable de “pousser” les données vers un serveur ou un fichier sur une carte SD ou sur la mémoire Flash (LittleFS ou SPIFFS)
Il existe plusieurs librairies pour manipuler des objets JSON. La librairie ArduinoJSON de Benoît Blanchon reste la référence.
Installer la librairie ArduinoJson sur l’IDE Arduino
La librairie ArduinoJson développée par Benoît Blanchon est la librairie de référence sur Arduino
Vous pouvez installer la librairie ArduinoJson directement depuis le gestionnaire de librairie de l’IDE Arduino. Utilisez le sélecteur pour choisir la version souhaitée.
Très bien documentée, elle dispose même d’un site internet totalement dédié ici.
Seul bémol, manipuler des objets JSON n’est pas sans conséquences sur le code Arduino. En effet, l’objet JSON est entièrement stocké dans la RAM du micro-contrôleur, ce qui peut limiter les performances et les cas d’utilisation.
La version 6 améliore toutefois les choses.
Différences entre les ArduinoJson 5 et ArduinoJson 6 : migration de code
Tous les exemples donnés dans ce tutoriel utilisent ArduinoJson 6, la version la plus récente. ArduinoJson est une version de rupture qui introduit de nouveaux concepts et demande une migration du code existant.
Benoît Blanchon a également réalisé une vidéo (en anglais) qui explique comment migrer du code depuis la version 5.
Voici les principaux changements apportés.
Version 5 | Version 6 |
Objet JSON en mémoire | |
JsonBuffer
Avec ArduinoJson 5, il était difficile d’utiliser un JsonObject ou un JsonArray en tant que membre de la classe car il fallait s’assurer que le JsonBuffer restait également en mémoire. L’astuce consistait également à ajouter le JsonBuffer en tant que membre de la classe, mais c’était plus compliqué. |
JsonDocument
ArduinoJson 6 remplace le concept de JsonBuffer par le concept de JsonDocument. Le JsonDocument est stocké en mémoire et contient la racine de l’arborescence d’objets. C’est en gros la combinaison de JsonBuffer et JsonVariant. Étant donné qu’un JsonDocument peut contenir n’importe quel type de valeur, vous devez le convertir pour lire le contenu. |
il existe deux versions du JsonDocument.
Il est recommandé d’utiliser un objet StaticJsonDocument pour un document inférieur à 1 Ko et DynamicJsonDocument pour un document supérieur à 1 Ko. |
|
Mesurer la taille d’un objet JSON | |
|
|
Désérialiser | |
|
|
Sérialiser | |
|
|
Les Piles (stack) et les Tas (heap), avantages et inconvénients, comment choisir
ArduioJSON est capable d’utiliser les piles (Stack) ou les Tas (Heap) des micro-contrôleurs. Voici les principaux avantages et inconvénients et surtout lequel choisir.
Stack (pile) | Heap (tas) |
Une pile est une zone spéciale de la mémoire du micro-contrôleur (ou de l’ordinateur) allouée par le processeur qui stocke les variables temporaires créées par une fonction.
Dans la pile, les variables sont déclarées, stockées et initialisées pendant l’exécution. Le dernier élément arrivé dans la pile est le premier que l’on peut retirer. Il n’est pas nécessaire de se souvenir de l’emplacement d’un élément au sein de la pile. Lorsque la tâche est terminée, la mémoire de la variable est automatiquement effacée. La taille de la pile est attribuée par le système d’exploitation au moment du lancement de la tâche, elle ne peut pas varier. La pile est supprimée lors de la fin de l’exécution de la tâche. |
Le tas est un emplacement de la mémoire utilisé pour les allocations dynamiques. Contrairement à la pile, n’importe quel bloc de cet espace peut être alloué ou libéré à n’importe quel moment. La gestion du tas est plus complexe et moins rapide car il est nécessaire de connaitre en permanence quel bloc est alloué.
Le tas est créé au démarrage d’un processus et est lié à celui-ci. Cela signifie que plusieurs tâches peuvent accéder à un même bloc dans cette mémoire. La taille du tas varie et peut augmenter si le programme a besoin de plus de mémoire. |
Avantages et inconvénients |
|
Taille < 1024 bytes (octets) | Taille > 1024 bytes (octets) |
Objet JSON stocké dans la pile (Stack) | Objet JSON stocké dans le tas (heap) |
Rapide, petite taille | Plus lent, grande dimension |
|
|
Plus d’informations sur guru99.com et le journaldunet.
Comment déterminer la taille du buffer ?
Une partie de la mémoire du micro-contrôleur doit être réservée à l’objet JSON. La première chose à faire avant de vous lancer dans le projet est donc de déterminer la quantité de mémoire à réserver.
Pour cela, il existe deux assistants en ligne qui permettent d’estimer la taille du buffer
Deux exemples d’objets JSON renvoyés par les API d’OpenWeatherMap et Weather Underground permettent de mieux comprendre comment la taille du buffer doit être calculée.
Je vous conseille donc de commencer par utiliser cet outil pour estimer la taille du buffer et de réserver au moins 20% d’espace mémoire supplémentaire afin de tenir compte des variations éventuelles.
Comme vous pouvez le constater, la taille du buffer varie également en fonction de la plateforme. L’outil est ligne permet d’estimer la taille du buffer pour Arduino, ESP32, ESP8266, SAMD21 et STM32.
Est-il possible d’allouer toute la mémoire disponible à un JsonDocument ?
oui c’est possible mais il est conseillé de libérer la mémoire dès que possible afin de ne pas bloquer les autres processus. Cette astuce peut entraîner une fragmentation de la mémoire. Plus d’infos ici.
Créer un objet JSON de type StaticJsonDocument
Comme pour n’importe quelle librairie, on commence par déclarer la librairie ArduinoJSON au début du programme
#include
La première chose à faire est donc de déclarer un objet de type StaticJsonDocument. Ce document stocké dans la mémoire du micro-contrôleur contiendra l’objet JSON. On pourra le manipuler (écrire, lire, modifier une valeur).
Nous allons débuter avec un objet simple qui va stocker la température, l’humidité et la pression atmosphérique d’un capteur BME280. En utilisant l’outil en ligne, on obtient entre 77 octets (Arduino) et 111 octets (ESP32). On va couper la poire en deux et attribuer 100 octets.
Comme on est largement en dessous de 1Ko (1024 octets), on utilisera un StaticJsonDocument.
L’ajout de données est maintenant similaire au Javascript. Il suffit d’indiquer entre crochets [] la clé et la valeur à lui attribuer sans se soucier du type de données. La librairie ArduinoJson se charge de tout.
Par exemple
firstJSON["key"] = "value"
Ce qui donne pour un capteur BME280 cet objet (évidemment ce sont des valeurs fictives).
Objet en mémoire | Représentation réelle |
|
|
Comment modifier une valeur dans l’objet JSON
Depuis la version 6 de la librairie ArduinoJson, il est très facile de modifier la valeur d’une clé. Il suffit d’utiliser l’opérateur crochets exactement comme nous avons fait précédemment pour attribuer la première valeur
firstJSON["temperature"] = 23.1;
C’est aussi simple que ça !
Sérialiser l’objet JSON, le convertir en une chaîne de caractère
Dans beaucoup de situation, on voudra stocker les données (sur la mémoire flash ou une carte microSD), ou les envoyer vers un logiciel tiers (ou un serveur). Pour cela, il faut convertir l’objet JSON en une chaîne de caractères.
C’est l’opération sérialiser (ou serialize en anglais)
La chaîne de caractère obtenue doit être récupérée dans un buffer (mémoire tampon) dont la taille est identique à l’objet JSON
buffer[100];
Ensuite, pour obtenir la chaîne JSON, il suffit d’appeler la fonction serializeJson(objet_Json, buffer).
char buffer[firstJsonSize];
serializeJson(firstJSON, buffer);
Serial.println(buffer);
On obtient l’objet JSON sous la forme d’une chaîne sur le moniteur série
{"sensor":"BME280","temperature":22.6,"humidity":22.6,"pressure":998.5}
Remarque. La chaîne obtenue est minifiée, c’est à dire que tous les espaces et sauts de ligne ont été supprimés. Dans cet exemple, ce n’est pas un problème, mais lorsque le JSON est de grande dimension cela devient difficile (impossible) à lire.
Vous pouvez utiliser un outil en ligne tel que jsonlint présenté précédemment ou utiliser la fonction serializeJsonPretty qui fait la même chose. Attention, n’utilisez cette fonction que pour la mise au point.
Téléverser le code de l’exemple
Voici le code source complet qui vous permettra de tester ce que nous venons de découvrir.
Si vous utilisez l’IDE Arduino, supprimez la première ligne (#include ) destinée à PlatformIO.
Sur ESP32, il est possible de connaître la taille du tas (Heap) restant à tout moment en appelant la méthode ESP.getFreeHeap().
#include
// load library
#include #include
#define firstJsonSize 100
#define displayFreeHeap false
void setup() {
Serial.begin(115200);
Serial.println("\n");
#ifdef ESP32
// Display free heap on startup | affiche la taille du tas (heap) au démarrage
Serial.print("ESP32 free Heap at startup: ");
Serial.println(ESP.getFreeHeap());
#endif
// Uncomment type of storage: dynamic (heap) or static (stack)
// Decommenter le type de stockage : dynamic (heap) ou static (stack)
//DynamicJsonDocument firstJSON;
StaticJsonDocument firstJSON;
firstJSON["sensor"] = "BME280";
firstJSON["temperature"] = 22.6;
firstJSON["humidity"] = 22.6;
firstJSON["pressure"] = 998.5;
char buffer[firstJsonSize];
serializeJson(firstJSON, buffer);
// Or for human reading | Ou la version pour une lecture par un humain
//serializeJsonPretty(firstJSON, buffer);
Serial.print("Data serialised: ");
Serial.println(buffer);
// Update Temperature value | actualise la temperature
firstJSON["temperature"] = 23.1;
serializeJson(firstJSON, buffer);
Serial.print("Data udpated: ");
Serial.println(buffer);
#ifdef ESP32
if ( displayFreeHeap ) {
Serial.print("ESP32 free Heap at startup: ");
Serial.println(ESP.getFreeHeap());
}
#endif
}
void loop() {
}
Désérialiser (Parser), comment convertir une chaine en objet JSON
Nous avons vu comment créer un objet JSON avec du code Arduino et insérer puis modifier des valeurs.
Dans de nombreux cas, on sera amené à récupérer des données ou des paramètres depuis un serveur tiers ou un fichier stocké sur une carte micro-SD. Dans ce cas on récupère une chaîne de caractère qu’on va devoir convertir en objet JSON utilisable par la librairie. C’est désérialiser ou Parser une chaîne.
Créer un compte IQAir
Pour ce tutoriel, nous allons utiliser l’API du service IQAir qui permet de connaître la qualité de l’air et la météo dans de nombreuses villes du monde entier. Le compte gratuit permet de réaliser jusqu’à 1 million de requêtes par mois.
Allez sur cette page pour créer votre compte.
Confirmer votre adresse email
Ouvrez votre compte et allez sur l’onglet API puis nouvelle clé. Choisissez le plan Community et valider
Copier votre clé d’API.
Il existe de nombreux points de sortie (endpoints) détaillés ici
Pour construire la requête, il faudra connaître le pays (country), la région (state) et la ville (city).
Utilisez ces requêtes en remplaçant à chaque fois les paramètres indiqués entre accolades (en supprimant les accolades) :
- http://api.airvisual.com/v2/countries?key={{YOUR_API_KEY}} liste des pays
- http://api.airvisual.com/v2/states?country={{COUNTRY_NAME}}&key={{YOUR_API_KEY}} liste des régions pour le pays indiqué
- http://api.airvisual.com/v2/cities?state={{STATE_NAME}}&country={{COUNTRY_NAME}}&key={{YOUR_API_KEY}} liste des villes disponibles dans la région indiquée
Voici par exemple la requête pour Paris (France)
http://api.airvisual.com/v2/city?city=paris&state=Ile-de-France&country=france&key={{YOUR_API_KEY}}
Ce qui donne la réponse au format JSON suivant
{"status":"success","data":{"city":"Paris","state":"Ile-de-France","country":"France","location":{"type":"Point","coordinates":[2.351666,48.859425]},"current":{"weather":{"ts":"2020-09-04T18:00:00.000Z","tp":25,"pr":1019,"hu":57,"ws":4.6,"wd":340,"ic":"01d"},"pollution":{"ts":"2020-09-04T18:00:00.000Z","aqius":6,"mainus":"n2","aqicn":17,"maincn":"n2"}}}}
Désérialiser (parser) la réponse renvoyée par l’AI JSON de IQAir
Créer un nouveau croquis sur l’IDE Arduino ou un nouveau projet PlatformIO et coller le code suivant.
Modifier les paramètres suivants dans le code :
- Identifiants de connexion au réseau WiFi
- Clé d’API
Le code ci-dessous est compatible ESP32 et ESP8266 et détecte automatiquement la plateforme
#include
// load libraries
#include
#ifdef ESP32
#include
#include
#else
#include
#include
#endif
#define docSize 800
#define displayFreeHeap false
String iqair_api_key = "enter_your_apikey";
String city = "Paris";
String state = "Ile-de-France";
String country = "france";
int heap_at_startup = 0;
// Replace with your network credentials
const char* SSID = "enter_your_ssid";
const char* PASSWORD = "enter_your_password";
String jsonBuffer;
void setup() {
Serial.begin(115200);
Serial.println("\n");
Serial.print("Connecting to ");
Serial.println(SSID);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// Print local IP address
Serial.println("");
Serial.println("WiFi connected at IP address:");
Serial.println(WiFi.localIP());
StaticJsonDocument aiqAir;
// Start Web Server
if(WiFi.status()== WL_CONNECTED){
String serverPath = "http://api.airvisual.com/v2/city?city=" + city + "&state=" + state + "&country=" + country + "&key=" + iqair_api_key;
HTTPClient http;
http.begin(serverPath.c_str());
int httpResponseCode = http.GET();
if ( httpResponseCode == 200 ) {
Serial.print("HTTP Response code: ");
Serial.println(httpResponseCode);
String payload = http.getString();
Serial.println(payload);
deserializeJson(aiqAir, payload);
}
}
if ( aiqAir.isNull() ) {
Serial.println("! No Data returned by server");
} else {
Serial.print("Memory used (bytes): ");
Serial.println(aiqAir.memoryUsage());
// All data | contient toutes les données
JsonObject data = aiqAir["data"];
// Only pollution | uniquement la pollution
JsonObject data_current_pollution = data["current"]["pollution"];
const char* data_current_pollution_ts = data_current_pollution["ts"]; // "2020-09-04T18:00:00.000Z"
int data_current_pollution_aqius = data_current_pollution["aqius"]; // 6
const char* data_current_pollution_mainus = data_current_pollution["mainus"]; // "n2"
int data_current_pollution_aqicn = data_current_pollution["aqicn"]; // 17
const char* data_current_pollution_maincn = data_current_pollution["maincn"]; // "n2"
Serial.print("Pollution at "); Serial.print(city); Serial.println(": ");
Serial.print("IAQ "); Serial.print(data_current_pollution_aqius);
Serial.print(" Polluting "); Serial.println(data_current_pollution_maincn);
}
#ifdef ESP32
// Display free heap on startup | affiche la taille du tas (heap) au démarrage
Serial.print("ESP32 free Heap at startup: ");
heap_at_startup = ESP.getFreeHeap();
Serial.println(heap_at_startup);
#endif
#ifdef ESP32
if ( displayFreeHeap ) {
Serial.print("Heap memory used");
Serial.println(ESP.getFreeHeap() - heap_at_startup);
}
#endif
}
void loop() {
}
Comment fonctionne le code
On charge les librairies permettant de se connecter au réseau WiFi et créer un client HTTP
#ifdef ESP32
#include
#include
#else
#include
#include
#endif
L’assistant ArduinoJson préconise entre 430 (Arduino) et 630 octets (ESP32 / ESP8266). Par sécurité, on assigne 800 octets.
#define docSize 800
Comme précédemment, on créé un objet de type StaticJsonDocument (taille < 1Ko)
StaticJsonDocument aiqAir;
On construit l’url qui pointe vers le point de sortie (endpoint) du serveur IQAIr
String serverPath = "http://api.airvisual.com/v2/city?city=" + city + "&state=" + state + "&country=" + country + "&key=" + iqair_api_key;
On créé un objet qui contiendra le client HTTP
HTTPClient http;
http.begin(serverPath.c_str());
On stocke le numéro de la réponse.
int httpResponseCode = http.GET();
Si la réponse est égale à 200, c’est que le serveur à bien renvoyé une réponse. Pour connaître tous les codes des réponses HTTP allez ici.
if ( httpResponseCode == 200 ) {
...
}
On récupère le contenu de la réponse directement au format JSON à l’aide de la fonction http.getString() de la librairie HTTPClient() sur ESP32 et sur ESP8266
String payload = http.getString();
Voici un exemple de réponse renvoyée par l’API JSON de IQAir
{"status":"success","data":{"city":"Paris","state":"Ile-de-France","country":"France","location":{"type":"Point","coordinates":[2.351666,48.859425]},"current":{"weather":{"ts":"2020-09-04T18:00:00.000Z","tp":25,"pr":1019,"hu":57,"ws":4.6,"wd":340,"ic":"01d"},"pollution":{"ts":"2020-09-04T18:00:00.000Z","aqius":45,"mainus":"p2","aqicn":22,"maincn":"o3"}}}}
Il suffit maintenant d’appliquer la méthode deserializeJson() de la librairie ArduinoJson sur le payload pour convertir la réponse (parser) et la placer dans l’espace mémoire réservée précédemment.
deserializeJson(aiqAir, payload);
La méthode isNull() permet de vérifier si l’objet est vide ou contient des données.
La méthode memoryUsage() permet de connaître l’espace réellement occupé dans la mémoire du micro-contrôleur (Arduino, ESP32, ESP8266 ou STM32).
Comment extraire des données stockées dans un objet JSON ?
L’accès aux données est similaire au language Javascript.
Prenons un extrait de la réponse de IQAir et imaginons qu’on souhaite récupérer la longitude. La longitude est stockée dans un tableau (à l’indice 1) qui se trouve dans data:location:coordinates.
C’est facultatif mais plus pratique d’extraire les portions de données, ici on extrait la racine
JsonObject data = aiqAir["data"];
Puis on accède à la clé désirée. Ici la position 1 du tableau de coordonnées GPS.
Attention, il faut spécifier à chaque fois le type de données. Ici la longitude est de type float.
float data_location_coordinates_1 = data["location"]["coordinates"][1];
Si vous n’êtes pas à l’aise avec l’accès aux données dans un JSON, n’oubliez pas que l’assistant peut vous mâcher le travail 💪
Tutoriels et projets pour aller plus loin avec JSON sur micro-contrôleurs
Voici quelques projets et tutoriels qui utilisent le format JSON pour stocker ou transmettre les données.
Mises à jour
7/09/2020 Publication de l’article
- ESP32, broches GPIO et fonctions associées. I/O, PWM, RTC, I2C, SPI, ADC, DAC
- ESP32-CAM. Broches et équipements ESP-EYE, AI Thinker, TTGO T-Camera, M5Stack Timer Camera…
- ESP32-CAM. Quel modèle choisir ? ESP-EYE, AI Thinker, TTGO T-Camera, M5Stack Timer Camera…
- M5Stack Atomic GPS. Tracker ESP32 TinyGPS++, export GPX sur carte SD, visualisation sur Google Maps ou VSCode
- Home Assistant. Installer le snap sur NAS Synology sur une machine virtuelle Ubuntu
Avez-vous aimé cet article ?