Code Minimal Réseau - (3) Mon D1 Mini récupère des données sur Internet (Json)

Auteur avatarPhilippe Blusseau | Dernière modification 7/09/2023 par Philby

Code Minimal R seau - 3 Mon D1 Mini r cup re des donn es sur Internet Json Le D1 Mini R cup re des Donn es.png
Utilisation des fonction réseau des cartes compatibles Arduino, possédant une puce Wi-Fi
=== Episode n° 3 : Mon D1 Mini récupère des données sur Internet (Json) ===
Difficulté
Technique
Durée
2 heure(s)
Disciplines scientifiques
Arduino, Informatique
<languages />
Licence : Attribution (CC-BY)

Introduction

Cette expérience fait partie d'une série de 4 épisodes, présentant différentes façons de bénéficier des capacités de communication des cartes compatibles Arduino possédant une puce Wi-Fi (Wemos D1 mini, ESP32, ...). On suppose (Cf. "Expériences ré-requises" ci-après) que vous avez déjà manipulé une carte Arduino et son environnement de développement. Ces 4 épisodes sont les suivants :


  1. Connecter le Wemos D1 Mini à internet en Wi-Fi.
  2. Héberger un site web sur mon Wemos D1 Mini.
  3. Mon Wemos D1 Mini récupère des données sur Internet  (format Json) --- cette page.
  4. Mes Wemos D1 Mini discutent sur Internet avec MQTT.


Il est nécessaire de commencer par l'épisode 1, par contre les épisodes suivants peuvent être consultés dans n'importe quel ordre.


Dans la même philosophie que les expériences "Code minimal des capteurs pour Arduino" et "Code minimal des actionneurs pour Arduino", nous fournirons ici uniquement le code relatif à nos besoins de connexion, sans mettre au point une quelconque application. Donc, ici, pas besoin de connecter une led ou un capteur, donc pas de schéma de montage : vous branchez simplement votre carte en USB sur votre ordinateur, et les résultats seront visibles en mode texte dans le moniteur série de l'environnement de développement Arduino.
  • Expériences pré-requises
  • Fichiers

Étape 1 - JSON ? qu'est-ce que c'est ?

Nous allons maintenant nous intéresser à la récupération de données sur Internet (informations sur la météo, sur la pollution, sur les derniers recensements, ...). De nombreux serveurs de données, et en particulier les serveurs "Open Data" (offrant des données libres de droit), sont accessibles en mode web. C'est-à-dire qu'une simple requête dans la barre d'adresse de votre navigateur, permet de récupérer les informations souhaitées.


Et, encore mieux, dans la plupart des cas, la réponse revient sous une forme standardisée de type JSON (JavaScript Objet Notation), que les navigateurs récents sont capables de décoder. A titre d'exemple, ouvrez un nouvel onglet dans votre navigateur, et recopiez dans la barre d'adresse ce qui suit ...


https://data.rennesmetropole.fr//api/records/1.0/search/?dataset=etat-du-trafic-en-temps-reel&q=rocade

... et vous devriez avoir en retour un texte de ce type :

{"nhits": 63, "parameters": {"dataset": "etat-du-trafic-en-temps-reel", "q": "rocade", "rows": 10, "start": 0, "format": "json", "timezone": "UTC"}, "records": [{"datasetid": "etat-du-trafic-en-temps-reel", "recordid": "c8cd4fc9d2a9f1840170322c834f827fc100cc75", "fields": {"traveltimereliability": 100, "traveltime": 55, "predefinedlocationreference": "30023", "averagevehiclespeed": 91, "datetime": "2022-11-29T15:11:00+01:00", "gml_id": "v_rva_troncon_fcd.fid-722fb9f8_184c264cda5_453f", "trafficstatus": "freeFlow", "func_class": 666, "geo_point_2d": [48.14130932076887, -1.6781068587055177], (...)

... mais que votre navigateur va quasi-immédiatement immédiatement reconnaître comme un format JSON, et afficher sous une forme plus structurée :

Exemple de réponse JSON

Nous avons fait ici appel au serveur Open Data de la ville de Rennes, et avons fait une requête demandant l'état du trafic sur la rocade principale. Ce même serveur propose un tas d'autres données libres, et on peut trouver sur Internet une multitude d'autres serveurs "Open Data" en mode JSON.

Étape 2 - Récupération de données JSON

... bon, ok, mais mon D1 mini n'a pas de navigateur ?


C'est là où deux bibliothèques vont nous être utiles :

  • la première pour permettre à notre carte se connecter au serveur de données en mode sécurisé (car la plupart des sites web ont une adresse 'https://www...') : WiFiClientSecure. Celle-ci est intégrée de base dans l'environnement de développement Arduino.
  • la seconde pour décoder le format JSON et extraire facilement les éléments de réponse qui nous intéressent : ArduinoJson. Celle-ci doit être récupérée dans le gestionnaire de bibliothèques :
    Bibliothèque ArduinoJSON

Les possibilités sont multiples, et l'exploitation des données JSON par les cartes D1 mini ou ESP32, peut prendre des formes très sympathiques : voir par exemple les réalisations "Voir Demain" et "Hawaiiiii" issues d'un hackathon organisé en décembre 2021 par Les Petits Débrouillards Grand Ouest et L'Edulab de l'Université de Rennes 2.


Fonctions JSON


Pour connaître toutes les autres possibilités de cette bibliothèque, voir sa référence, ici.


Code minimal :


Bon, en fait, pas tout à fait "minimal" :

  • pour des raisons de clarté, nous avons défini deux fonctions : serverRequest pour générer la requête auprès du serveur et récupérer la réponse, et showJSONAnswer pour analyser la réponse (décodage des informations JSON).
  • pour faciliter la réutilisation de ce code, plutôt que de tout traiter dans le setup(), nous activerons ces fonctions régulièrement, depuis la boucle loop(), ce qui est le mode de fonctionnement habituel.

  1 /* =========================================================================================================
  2  * 
  3  *                              CODE MINIMAL RESEAU - ETAPE 5 : Données JSON
  4  *          
  5  * ---------------------------------------------------------------------------------------------------------
  6  * Les petits Débrouillards - décembre 2022 - CC-By-Sa http://creativecommons.org/licenses/by-nc-sa/3.0/
  7  * ========================================================================================================= */
  8 
  9 // Bibliothèques requises
 10 // ATTENTION AUX MAJUSCULES & MINUSCULES ! Sinon d'autres bibliothèques, plus ou moins valides, seraient utilisées.
 11 
 12 #include <WiFiManager.h>                          // Gestion de la connexion Wi-Fi (recherche de points d'accès)  
 13 #include <WiFiClientSecure.h>                     // Gestion de la connexion (HTTP) à un serveur de données
 14 #include <ArduinoJson.h>                          // Fonctions de décodage JSON des réponses du serveur. 
 15 
 16 
 17 
 18 // Variables globales
 19 
 20 WiFiManager myWiFiManager;                        // Création de mon instance de WiFiManager.
 21 WiFiClientSecure myWiFiClient;                    // Création de mon instance de client WiFi.
 22 const char* mySSID   = "AP_PetitDeb" ;            // Nom de la carte en mode Point d'Accès.
 23 const char* mySecKey = "PSWD1234" ;               // Mot de passe associé, 8 caractères au minimum.
 24 
 25 char* Data_HOST = "data.rennesmetropole.fr";      // Serveur web hébergeant les données qui nous intéressent
 26 int   Data_PORT = 443;                            // Port sur lequel envoyer la requête
 27 char* Data_REQUEST =                              // Requête (sur cet exemple : demande de l'état du trafic au point
 28                                                   // 31553, correspondant à la porte de Saint-Malo de la rocade de Rennes 
 29       "/api/records/1.0/search/?dataset=etat-du-trafic-en-temps-reel&q=31553";  
 30 
 31 
 32 const int MAX_RESPONSE_SIZE = 6000 ;              // Taille max de la réponse attendue d'un serveur. A modifier en fonction du besoin.
 33 char Data_Response[MAX_RESPONSE_SIZE] ;           // Buffer qui contiendra la réponse du serveur.
 34   
 35 #define TEN_SECONDS 10000                         // On appelera le serveur de données toutes les 10000 ms = 10 secondes.
 36 unsigned long myWakeUp ;                          // Timer mis en place pour limiter le nombre d'appels au serveur de données.
 37 
 38 /* --------------------------------------------------------------------------------------------------------------
 39  *  serverRequest() : Envoi requête HTTP au serveur et récupération de la réponse
 40  *  paramètres : 
 41  *    - pHost     : nom du serveur ; 
 42  *    - pPort     : port sur lequel est appelé le serveur ; 
 43  *    - pRequest  : requête au serveur.
 44  *    - pResponse : endroit où stocker la réponse
 45  *    - pRespMax  : nombre max de caractères autorisés pour la réponse
 46  *  valeur de retour : 
 47  *      -2 = réponse tronquée (trop de caractères) ;
 48 *       -1 = pas de réponse ;
 49          0 = pas de connexion au serveur ;
 50  *       1 = ok.
 51  *  ------------------------------------------------------------------------------------------------------------- */
 52 int serverRequest(char* pHost, int pPort, char* pRequest, char *pResponse, int pRespMax) {
 53     
 54     const int API_TIMEOUT = 15000;      // Pour être sûr de recevoir l'en-tête de la réponse client.
 55 
 56     // Comme la connexion est sécurisée (protocole HTTPS), il faudrait indiquer le certificat du site web.
 57     // Pour simplifier, on va utiliser l'option magique ".setInsecure()", ce qui n'est pas important dans 
 58     // notre exemple, où les données échangées ne sont pas confidentielles.
 59 
 60     myWiFiClient.setInsecure();
 61     myWiFiClient.setTimeout(API_TIMEOUT);
 62 
 63     // Connexion au serveur (on essaie 5 fois, avec un intervalle d'une seconde)
 64 
 65     Serial.print("--- Connexion au serveur [" + String(pHost) + "] "); 
 66     int nbTries = 1;
 67     while(!myWiFiClient.connect(pHost, pPort)) {
 68         Serial.print(".");
 69         if (++nbTries > 5) {
 70             Serial.println("--- Connexion impossible :-(");
 71             myWiFiClient.stop();
 72             return(0);
 73         }
 74         delay(1000);
 75     }   
 76 
 77     // Connecté à notre serveur ! --> Envoi de la requête URL. Il faut envoyer en fait une suite de lignes : 
 78     //        "GET <notre requête> HTTP/1.1"
 79     //        "Host: <nom du serveur>"
 80     //        "Connection: close"
 81     //        <ligne vide>
 82     // Cet envoi se fait simplement grâce à la fonction println du client WiFi, similaire à celle que 
 83     // l'on utilise pour envoyer des données au moniteur série pour nos traces.
 84 
 85     String myURL = String(pRequest);
 86     Serial.println() ; 
 87     Serial.println("--- Connexion OK ! --> Envoi requête URL - " + myURL);
 88     myWiFiClient.println("GET " + myURL + " HTTP/1.1") ;
 89     myWiFiClient.println("Host: " + String(pHost)) ;
 90     myWiFiClient.println("Connection: close") ;
 91     myWiFiClient.println() ;
 92     
 93     // Attente de la réponse ....(on essaie 50 fois, avec un intervalle de 100ms, donc 5 secondes en tout)
 94      
 95     nbTries = 1;
 96     while(!myWiFiClient.available()){
 97         if (++nbTries > 50) {
 98             Serial.println("--- Pas de réponse :-(");
 99             myWiFiClient.stop();
100             return(-1);
101         }
102         delay(100);
103     }
104 
105     // Récupération de l'en-tête de la réponse (dont on ne fera rien)
106     // Cette entête est une suite de caractères, composant un certain nombre de lignes (ie se terminant par '\n'), 
107     // la dernière ligne de l'entête n'est composée que du caractère "\r" (suivie du '\n') ;
108      
109     Serial.println("--- Réponse OK --> Récupération de l'en-tête ...");
110     String myLine ;
111     while (myWiFiClient.connected()) {
112         myLine = myWiFiClient.readStringUntil('\n');
113         if (myLine == "\r") {
114             break;
115         }
116     }
117 
118     // Entête reçue ! On va alors recopier dans pResponse tous les caractères qui suivent 
119     // en faisant attention à ne pas dépasser la taille du buffer.
120 
121     Serial.println("--- Entête ok --> Récupération des données ...");
122     int myIndex = 0 ;
123     while(myWiFiClient.available() && myWiFiClient.connected() ){
124 
125         char myResp = myWiFiClient.read();
126         pResponse[myIndex] = myResp;     
127         if (myIndex++ >= pRespMax) {
128              Serial.println("*** Réponse trop longue : " + String(pRespMax) + "caractères, et ne peut pas être traitée") ;
129              myWiFiClient.stop();
130              return(-2);
131         }
132         pResponse[myIndex] = '\0';            // Vu sur forums : conseillé d'ajouté 'fin de chaîne' systématiquement
133         delay(1) ;                            // Et également d'ajouter ce tout petit délai pour éviter des plantages.
134         
135     }
136 
137     // Tout s'est bien passé ! On arrête notre client WiFi
138 
139     Serial.println("--- Récupération des données ok (" + String(myIndex) + " caractères).") ;
140     myWiFiClient.stop();
141     return(1) ;
142 
143 }
144 
145 /* --------------------------------------------------------------------------------------------------------
146  *  showJSONAnswer : Décodage de la structure de données JSON
147  *  Paramètres :
148  *    - pResponse : endroit se trouve la réponse (au format JSON) du serveur
149  *    - pRespMax  : nombre max de caractères autorisés pour la réponse
150  * -------------------------------------------------------------------------------------------------------- */
151 void showJSONAnswer(char *pResponse, int pRespMax) {
152 
153     // Création de notre structure JSON
154     // Le besoin en mémoire (capacity) doit être vérifié sur l'assistant https://arduinojson.org/v6/assistant/
155     // 1) dans la première page de l'assistant, sélectionnez le processeur (par exemple "ESP8266"), le mode
156     //    "Deserialize", et le type d'entrée "char*", puis cliquez sur le bouton "Netx:JSON"
157     // 2) Lancez votre requête depuis un navigateur. Dans notre exemple, tapez dans la barre d'adresse :
158     //      "https://data.rennesmetropole.fr/api/records/1.0/search/?dataset=etat-du-trafic-en-temps-reel&q=31553"
159     // 3) Recopiez la réponse obtenue - sous sa forme "Données Brutes" du navigateur vers l'assistant
160     // 4) L'assistant va alors préconiser le bon objet à créer (StaticJsonDocument ou DynamicJsonDocument),
161     //    ainsi que la taille à réserver. L'assistant va même proposer un exemple de programme exploitant toutes 
162     //    les informations de la structure JSON. 
163     // Pour notre exemple, l'assistant a proposé la définition qui suit.
164     
165     StaticJsonDocument<768> doc;
166 
167     // Décodage de la réponse JSON.
168     // La fonction deserializeJson va transformer la réponse "texte" du serveur, en une structure de données recopiée
169     // dans la variable 'doc', où il sera ensuite facile d'aller chercher les informations souhaitées.
170      
171     DeserializationError error = deserializeJson(doc, pResponse, pRespMax);
172     if (error) {
173         Serial.println("--- Décodage réponse JSON KO, code " + String(error.f_str())) ;
174         return;
175     }
176     Serial.println("--- Décodage réponse JSON OK !") ;
177 
178     // Nous pouvons maintenant extraire facilement les informations qui nous intéressent,
179     // en n'oubliant pas le niveau de profondeur de la donnée au sein de la structure JSON. 
180     // Ce niveau de profondeur est incrémenté par le nombre de '{' ou '[' rencontrés, et 
181     // décrémenté lors de la rencontre des ']' et '}'. Sur notre exemple 'rocade de Rennes',
182     // cela donne ceci :
183     //       +-----------------------------------------------------------------+
184     //       |   {                                                             | ... Entrée niveau 1
185     //       |      "nhits": 1,                                                |
186     //       |      "parameters": {                                            | ... Entrée niveau 2
187     //       |          "dataset": "etat-du-trafic-en-temps-reel",             |
188     //       |          (...)                                                  |
189     //       |      },                                                         | ... Retour niveau 1
190     //       |      "records": [                                               | ... Début d'un tableau : niveau 2
191     //       |          {                                                      | ... Entrée niveau 3
192     //       |              (...)                                              |                                                           |
193     //       |              "fields": {                                        | ... Entrée niveau 4
194     //       |                   (...)                                         |
195     //       |                   "averagevehiclespeed": 88,                    |
196     //       |                   (...)                                         |
197     //       |                   "datetime": "2022-11-30T11:57:00+01:00",      |
198     //       +-----------------------------------------------------------------+
199     // ... et donc :
200     //  - (1er niveau) --------- doc["nhits"] donnera la valeur 1,
201     //  - (2ème niveau) -------- doc["parameters"]["dataset"] donnera la valeur "etat-du-trafic-en-temps-reel"
202     //  - (4ème niveau) -------- doc["records"][0]["fields"]["averagevehiclespeed"] donnera la valeur 88
203 
204     // Extraction et affichage sur le port série de trois  valeurs
205 
206     String myLocRef = String(doc["records"][0]["fields"]["predefinedlocationreference"]) ;
207     String myTime = String(doc["records"][0]["fields"]["datetime"]) ;
208     int mySpeed    = doc["records"][0]["fields"]["averagevehiclespeed"] ;
209     
210     Serial.print("Vitesse au point " + myLocRef + " ") ;
211     Serial.print("le " +  myTime.substring(8,10) + "/" + myTime.substring(5,7) + "/" + myTime.substring(0,4) + " ") ;
212     Serial.print("à " +  myTime.substring(11,13) + "h" + myTime.substring(14,16) + " ") ;
213     Serial.println(" : " + String(mySpeed) + " km/h.") ; 
214 
215 }
216 
217 /* --------------------------------------------------------------------------------------------------------
218  *  SETUP : Initialisation
219  * -------------------------------------------------------------------------------------------------------- */
220 void setup() {
221 
222     // Initialisation de la liaison série, affichage 1er message
223 
224     Serial.begin(115200);
225     delay(100) ;
226     Serial.println(); 
227     Serial.println("-----------------------") ;
228     Serial.println("Exemple extraction JSON") ;
229     Serial.println("-----------------------") ;
230 
231     // Tentative de connexion au Wi-Fi. Si la carte n'a pas réussi  se connecter au dernier Point d'Accès connu,
232     // alors elle va se positionner en mode Point d'Accès, demandera sur l'adresse 192.168.4.1 quel nouveau
233     // Point d'Accès choisir. Par défaut, on restera bloqué tant que l'utilisateur n'aura pas fait de choix.
234     
235     Serial.println("Connexion au Wi-Fi ...");
236     if (myWiFiManager.autoConnect(mySSID, mySecKey)) {
237         Serial.println(); Serial.print("Connecté ! Adresse IP : ");
238         Serial.println(WiFi.localIP());
239     }
240     else {
241         Serial.println("Connexion Wi-Fi KO :-(");     
242     }
243 
244     // Initialisation du timer qui sera testé dans loop() - pour faire appel au serveur seulement toutes les 10 secondes 
245     // millis() est une fonction système donnant le nombre de ms depuis le lancement ou la réinitialisation de la carte.
246 
247     unsigned long myWakeUp = millis() + TEN_SECONDS ;
248 
249 }
250 
251 /* --------------------------------------------------------------------------------------------------------------
252  *  LOOP : fonction appelée régulièrement par le système
253  *  ------------------------------------------------------------------------------------------------------------- */
254 void loop() { 
255 
256     unsigned long myNow = millis() ;
257     if (myNow >= myWakeUp) {
258         Serial.println("Wake Up ! Nouvelle demande au serveur ...") ;
259         if (serverRequest(Data_HOST, Data_PORT, Data_REQUEST, Data_Response, MAX_RESPONSE_SIZE) == 1) {
260             Serial.println("Réponse reçue du serveur, lancement analyse JSON ...") ;
261             showJSONAnswer(Data_Response, MAX_RESPONSE_SIZE) ;
262         }
263         myWakeUp = myNow + TEN_SECONDS ;         
264     }
265 
266 }


Étape 3 - La suite, la suite ! ... :-)


Dernière modification 7/09/2023 par user:Philby.

Commentaires

Published