Dans mon article précédent, je vous présentai la carte LILYGO T-Display. Je vous présente ici sa grande sœur, la LILYGO T-Display S3. Cette carte de développement basée sur un microcontrôleur ESP32-S3 est équipée d’un écran LCD de 1.9″ offrant une résolution de 170 x 320 pixels.
Voici un rapide comparatif entre ces deux cartes :
LILYGO T-Display
LILYGO T-Display S3
Microcontrôleur
ESP32 (Tensilica Xtensa LX6)
ESP32S3 (Tensilica Xtensa LX7)
Écran
135 x 240
170 x 320
SRAM
448 Ko
512 Ko
FLASH
4 Mo
16 Mo
PSRAM
–
8 Mo
TFT Driver
ST7789V
ST7789
TFT Interface
SPI
SPI + Parallèle (8 bits)
Schéma des broches
Voici la schéma des broches de la carte, qui met en évidence les broches utilisées la communication parallèle avec l’écran ST7789 (LCD_D0 à LCD_D7, LCD_WR et LCD_RD). La commande du rétroéclairage est contrôlée via la broche LCD_BL (GPIO38) et l’allumage de l’écran est contrôlé par la broche LCD_Power_On (GPIO15).
Installation du gestionnaire de cartes ESP32
La carte étant basé sur le microcontrôleur ESP32 il faut installer le support des cartes Espressif ESP32 dans l’IDE Arduino.
Dans le menu préférences de l’IDE Arduino, ajoutez l’URL suivante dans le gestionnaire de carte :
Installez le paquet de gestion de cartes esp32 de Espressif Systems.
Une fois le gestionnaire de cartes installé, sélectionnez la carte ESP32S3 Dev Module dans la liste des cartes esp32 disponibles, et appliquer cette configuration :
Note : avec la configuration Lilygo T-Display S3, la PSRAM ne semble pas correctement configurée.
Éditer le fichier User_Setup_Select.h qui se trouve dans le sous-répertoire Librairies du répertoire Arduino, et appliquer les modifications suivantes :
Commenter la ligne suivante :
C++
//#include <User_Setup.h> // Default setup is root library folder
Et dé-commenter la ligne suivante :
C++
#include<User_Setups/Setup206_LilyGo_T_Display_S3.h>// For the LilyGo T-Display S3 based ESP32S3 with ST7789 170 x 320 TFT
Afficher un texte sur l’écran du LilyGo T-Display
Pour vérifier que les bibliothèques et la configuration de la carte sont correctement installées, nous allons afficher sur l’écran la taille des mémoires Flash et PSRAM détectées :
C++
// LilyGo T-Display S3// Site : https://tutoduino.fr/// Licence : Copyleft 2025// Inclusion de la bibliothèque TFT_eSPI pour gérer l'affichage TFT// Cette bibliothèque permet de contrôler les écrans TFT compatibles avec les contrôleurs comme le ST7789V#include<TFT_eSPI.h>// Création d'un objet TFT_eSPI nommé "tft" pour interagir avec l'écran// Cet objet encapsule toutes les fonctions nécessaires pour dessiner sur l'écranTFT_eSPI tft = TFT_eSPI();// -----------------------------------------------------------------------------// Mise en veille profonde de l'ESP32 et de l'ecran LCD// -----------------------------------------------------------------------------voidenterDeepSleep() { // Configure la sortie de veille par appui sur le boutonesp_sleep_enable_ext0_wakeup((gpio_num_t)14, LOW); // Éteindre écran et désactiver son alimentationdigitalWrite(38, LOW);digitalWrite(15, LOW); // Activer la veille profonde du microcontrolleuresp_deep_sleep_start();}voidsetup() { // Configuration de la broche 38 (rétroéclairage) en sortiepinMode(38, OUTPUT); // Allumer le rétroéclairage (HIGH = allumé)digitalWrite(38, HIGH); // Configuration de la broche 15 (activation de l'écran) en sortiepinMode(15, OUTPUT); // Activer l'écran (HIGH = activé)digitalWrite(15, HIGH); // Initialisation de l'écran TFTtft.init(); // Rotation de l'écran (horizontal connecteur usb à gauche)tft.setRotation(3); // Effacer l'écran en le remplissant de noirtft.fillScreen(TFT_BLACK); // Affichage des différents messages sur l'écrantft.setTextColor(TFT_GREEN);tft.setTextSize(2);tft.setCursor(10, 20);tft.println("https://tutoduino.fr");tft.setTextColor(TFT_MAGENTA);tft.setCursor(10, 50);tft.println("Lilygo T-Display S3"); // Lire et afficher la taille de la mémoire Flashuint32_t flashSize = ESP.getFlashChipSize();tft.setTextColor(TFT_ORANGE);tft.setCursor(10, 80);tft.print("Flash: ");tft.print(flashSize / (1024 * 1024)); // Conversion en Mo (1 Mo = 1024 Ko = 1024 * 1024 octets)tft.println(" Mo"); // Vérifier si la PSRAM est présenteif (psramFound()) { // Lire la taille de la PSRAM et l'afficher sur l'écranuint32_t psramSize = ESP.getPsramSize();tft.setCursor(10, 110);tft.setTextColor(TFT_CYAN);tft.print("PSRAM: ");tft.print(psramSize / (1024 * 1024)); // Conversion en Motft.println(" Mo"); } else { // Si la PSRAM n'est pas détectée, afficher un messagetft.setCursor(10, 110);tft.setTextColor(TFT_CYAN);tft.println("No PSRAM detected"); }}voidloop() { // Attendre 1 minute et entrer en veille profondedelay(60000);enterDeepSleep();}
Si tout se déroule normalement, votre Lilygo T-Display S3 devrait afficher les messages suivants :
Mesure de la tension de la batterie
Le Lilygo T-Display S3 peut être alimenté par une batterie Lithium-Ion de 3,7 V par son connecteur JST PH 2.0 (JST 2 broches avec un pas de 2 mm). Une batterie de 800 mAh peut être positionnée à l’intérieur du boîtier.
Batterie Li-Ion de 3,7 V connecté au Lilygo T-Display S3 via son connecteur JST PH 2.0
Il est possible de mesurer la tension de la batterie via la broche LCD_BAT_VOLT (GPIO4), ce qui permet d’estimer son niveau de charge (4,2 V =100% et 3,0 V = 0%).
La tension fournie par la batterie dépasse la plage de mesure admissible d’une entrée analogique de l’ESP32. Afin de permettre sa mesure, un pont diviseur de tension est intégré au circuit, ce qui abaisse la tension à un niveau compatible avec l’entrée ADC utilisée (GPIO4 dans ce cas).
Le schéma électrique de la carte indique que ce pont diviseur présente un rapport de division de 2. Toutefois, en raison des tolérances des composants et des imprécisions propres à la référence ADC de l’ESP32, un ajustement empirique a été nécessaire. Un coefficient correctif ADC_CORR de 1,051 a ainsi été appliqué afin d’obtenir une correspondance fidèle entre la tension réelle mesurée au multimètre et la valeur calculée par l’ESP32 sur le module LilyGO T-Display S3.
Voici le code qui permet d’afficher la tension de la batterie sur l’écran :
C++
// LilyGo T-Display S3// Site : https://tutoduino.fr/// Licence : Copyleft 2025// Inclusion de la bibliothèque pour gérer l'écran TFT#include<TFT_eSPI.h>// Création de l'objet pour contrôler l'écran TFTTFT_eSPI tft = TFT_eSPI();// Définition des broches utilisées#define LCD_BAT_VOLT 4 // Broche de mesure de la tension batterie#define PIN_BUTTON 14 // Broche du bouton#define TFT_ON 15 // Broche d'activation de l'écran#define TFT_BL 38 // Broche du rétroéclairage#define ADC_REF 3.30#define ADC_CORR 1.051 // calibration terrain#define ADC_MAX 4095.0#define DIVIDER 2.0// Configuration de la mesure de tensionvoidinitVoltageMeasurement() { // Résolution 12 bits et atténuation 11 dB (mesure jusqu'à 2600 mV) // Il s'agit des valeurs par défaut, objectif uniquement pédagogiqueanalogSetPinAttenuation(LCD_BAT_VOLT, ADC_11db);analogReadResolution(12);}// Lecture de la tension batteriefloatreadBatteryVoltage() {int adcValue = analogRead(LCD_BAT_VOLT);return (adcValue / ADC_MAX) * ADC_REF * DIVIDER * ADC_CORR;}voidsetup() { // Configurer rétroéclairage et écran en sortiepinMode(TFT_BL, OUTPUT);digitalWrite(TFT_BL, HIGH); // Allumer rétroéclairagepinMode(TFT_ON, OUTPUT);digitalWrite(TFT_ON, HIGH); // Activer écran // Initialiser la mesure de tensioninitVoltageMeasurement(); // Initialiser l'écran TFTtft.init(); // Rotation de l'écran (connecteur USB à gauche)tft.setRotation(3);}voidloop() { // Mesurer et afficher la tension batterie toutes les 10 secondestft.fillScreen(TFT_BLACK);tft.setCursor(10, 50);tft.setTextColor(TFT_CYAN);tft.printf("VBAT = %.2f V", readBatteryVoltage());delay(10000);}
Pourquoi chiffrer la mémoire Flash de vos objets connectés IoT ?
Le chiffrement consiste à rendre les données illisibles pour toute personne ne possédant pas la clé de déchiffrement, assurant ainsi leur confidentialité.
Ce principe vous est sans doute familier grâce au petit cadenas affiché dans la barre d’adresse de votre navigateur : il indique que la communication entre votre appareil et le site web utilise le protocole sécurisé HTTPS. Ainsi, même si un attaquant intercepte le trafic, il ne pourra pas en lire le contenu.
Toutefois, la protection des données ne se limite pas aux échanges réseau. Dans le cas des objets connectés, il est tout aussi crucial de chiffrer la mémoire Flash, afin d’empêcher l’accès ou la modification non autorisés des informations stockées localement.
Pourquoi chiffrer la mémoire Flash d’un objet connecté IoT ?
Le “cerveau” d’un objet connecté IoT (Internet of Things) est un micro-contrôleur, qui stocke ses données persistantes (programme, configuration Wi-Fi…) dans une mémoire Flash. Ce type de mémoire conserve en effet les données même sans alimentation.
Une personne ayant un accès physique à un objet connecté peut dès lors lire le contenu de cette mémoire et en extraire des secrets, informations cruciales pour la sécurité de votre réseau (certificats de sécurité, mot de passe de l’application, mots de passe Wi-Fi…). Il est donc indispensable de chiffrer cette mémoire afin de rendre ces secrets illisibles pour un attaquant. Il existe d’ailleurs maintenant des puces spécialisées pour le stockage des secrets (ex : TPM).
La plupart des objets connectés “grand public”, comme les capteurs de température ou les caméras de vidéo-surveillance, ne sont pas équipés de puce TPM et leur mémoire Flash est rarement chiffrée. Or ces objets sont généralement connectés à un réseaux Wi-Fi.
Les communications sur les réseaux Wi-Fi sont maintenant relativement sécurisés avec une clé de chiffrement WPA2-PSK. Mais un attaquant qui a un accès physique à un objet connecté (un capteur dans une usine, une caméra dans votre jardin), peut facilement extraire cette clé de la mémoire Flash non chiffrée de cet objet.
Voici un exemple minimaliste de code pour un objet connecté basé sur un microcontrôleur ESP32, qui se connecte au réseau Wi-Fi TP-Link_21B7 (SSID) avec le mot de passe R3seauS3cur1t3IoT! :
C++
// Example used to show how Wi-Fi password// can be extracted from Flash memory//// https://tutoduino.fr/#include<WiFi.h>constchar *ssid = "TP-Link_21B7";constchar *passphrase = "R3seauS3cur1t3IoT!";voidsetup() {Serial.begin(115200);delay(100);WiFi.begin(ssid, passphrase);Serial.println();Serial.println();Serial.print("Waiting for WiFi... ");while (WiFi.status() != WL_CONNECTED) {delay(500);Serial.print("."); }Serial.println("");Serial.println("WiFi connected");Serial.print("IP address: ");Serial.println(WiFi.localIP());}voidloop() {delay(1000);}
Le nom du réseau Wi-Fi (ssid) et son mot de passe (passphrase) sont des constantes qui vont être stockées dans la mémoire Flash du microcontrôleur.
Après avoir connecté l’objet via son port USB à un ordinateur sous Linux, il est trivial d’extraire les identifiants Wi-Fi de sa mémoire Flash :
Exemple d’extraction de mot de passe Wi-Fi de la mémoire Flash d’un ESP32
Voici cet exemple illustré en vidéo :
Comment chiffrer la mémoire Flash sur un ESP32 ?
Le framework ESP-IDF permet de chiffrer la mémoire flash des ESP32 avec une procédure relativement simple.
Voici les principales étapes :
#1. Générer une clé de chiffrement
Vous pouvez utiliser la clé de chiffrement matérielle intégrée à l’ESP32 (unique pour chaque puce) ou générer votre propre clé de chiffrement personnalisée. Chaque méthode présente ses avantages et ses inconvénients. Dans ce tutoriel, à des fins pédagogiques, j’ai choisi de générer ma propre clé de chiffrement.
Voici comment générer votre propre clé de chiffrement :
Cette clé a une taille de 256 bits, qui permettra de chiffrer avec un bon niveau de sécurité vos firmwares. Attention cette clé sera nécessaire pour toute les futures modifications de firmware de votre objet connecté, elle doit être conservée précieusement.
#2. Brûlez-la clé de chiffrement dans un eFuse du micro-contrôleur
Votre clé de chiffrement doit être écrite dans l’ESP32. Afin de garantir la confidentialité absolue de la clé et d’empêcher toute lecture ou modification ultérieure, le dispositif utilise le mécanisme de programmation eFuse.
Un eFuse (fusible électronique) est un composant matériel intégré directement dans la puce de l’ESP32. Contrairement à la mémoire flash standard, c’est une mémoire OTP (One-Time Programmable) : une fois un bit activé, il ne peut plus être modifié ni effacé. Cette opération est donc permanente et irréversible.
Voici le résultat de cette commande si tout se déroule bien :
Bash
steph@F15:~/esp/esp-idf-5.5.1$espefuse--port/dev/ttyUSB0burn_keyflash_encryptionmy_key.binespefusev4.11.dev2Connecting.....Detectingchiptype...ESP32=== Run"burn_key"command===Sensitivedatawillbehidden (see --show-sensitive-info)Burnkeystoblocks:-BLOCK1 -> [?? ??????????????????????????????????????????????????????????????]ReversingthebyteorderDisablingreadtokeyblockDisablingwritetokeyblockBurnkeysinefuseblocks.Thekeyblockwillbereadandwriteprotected (no furtherchangesorreadback) Checkallblocksforburn...idx,BLOCK_NAME,Conclusion[00] BLOCK0 is not empty (written ):0x0000000400000000000014380000b00000a5840d8e19b63400000000 (towrite):0x00000000000000000000000000000000000000000000000000010080 (codingscheme=NONE)[01] BLOCK1 is empty, will burn the new value.Thisisanirreversibleoperation!Type'BURN' (all capitals) to continue.BURNBURNBLOCK1-OK (write block==readblock)BURNBLOCK0-OK (all writeblockbitsareset)Readingupdatedefuses...Successful
Votre clé de chiffrement de 256 bits est stockée dans le BLOCK1 des fusibles, elle est protégée en lecture/écriture.
Nous pouvons vérifier par la suite que nous n’avons aucun accès en lecture à la clé de chiffrement :
Bash
espefuse--port/dev/ttyUSB0summary
Renvoie des points d’interrogation pour BLOCK1, la clé de chiffrement est correctement protégée en lecture.
#3. Activer le chiffrement de la flash dans le micro-contrôleur (attention, ceci est irréversible)
Deux eFuse spécifiques doivent être brûlés pour activer le chiffrement de la flash :
FLASH_CRYPT_CNT :Compteurqui limite le nombre de tentatives pour activer le chiffrement, empêchant toute réinitialisation non autorisée du chiffrement. Sa valeur doit être impair pour activer le chiffrement.
FLASH_CRYPT_CONFIG : Définit les paramètres du chiffrement. La valeur 0x0F active le chiffrement AES-256 pour toutes les données stockées dans la mémoire Flash (y compris le bootloader, l’application et les données utilisateur).
La partition NVS (Non-Volatile Storage) est une partition spéciale de la flash. Son rôle principal est de stocker des données de configuration persistantes qui doivent survivre à un redémarrage ou à une coupure d’alimentation. La partition NVS doit être chiffrée afin de protéger les données sensibles comme les mots de passe.
Le chiffrement de la partition NVS se fait de la manière suivante dans l’IDE Arduino :
Sélectionner le schéma de partition “Custom“
Créer le fichier “partitions.csv” dans le répertoire où est stocké votre croquis sur votre ordinateur
Configurer le schéma de partition souhaité au format csv dans ce fichier
Ajouter le flag “encrypted” pour la partition NVS
Compiler le programme dans l’IDE Arduino (il peut être utile de supprimer auparavant le répertoire cache arduino)
Exemple de schéma de partition avec la partition NVS chiffrée pour un ESP32
#5. Chiffrer le firmware
Après avoir compilé le programme sous l’IDE Arduino, il faut exporter les fichiers binaires générés.
Cela positionne tous les fichiers nécessaires dans le sous-répertoire build du répertoire où est stocké votre croquis Arduino.
Le fichier flash_args contient le nom des fichiers qu’il va falloir chiffrer et à quel offset il va falloir ensuite les flasher dans la mémoire de l’ESP32 :
Chiffrer tous ces fichiers binaires avec votre clé de chiffrement en indiquant en paramètre leur adresse dans la mémoire Flash.
Le chiffrement assure la confidentialité des données !
En répétant la procédure du début de ce tutoriel, il devient impossible d’extraire les données en clair de la mémoire Flash.
La partition NVS est chiffrée et le SSID et le mot de passe de Wi-Fi ne sont plus lisibles en clair
Le firmware est également chiffré, il n’est plus possible de réaliser d’ingénierie inverse dessus :
Le firmware est également chiffré, rendant impossible tout reverse engineering
Conclusion
Le chiffrement de la mémoire Flash assure la confidentialité des données qu’elle stocke et apporte de nombreux bénéfices aux niveau de la cybersécurité de votre objet connecté IoT :
✅ Bootloader chiffré
Empêche l’exécution de code malveillant au démarrage.
Protège contre les attaques par modification du bootloader (ex. : injection de malware).
Garantit l’intégrité du processus de démarrage.
✅ Application protégée
Rend le code illisible sans la clé de chiffrement.
Empêche l’ingénierie inverse (reverse engineering) pour voler la propriété intellectuelle.
Sécurise les algorithmes sensibles (ex. : protocoles de communication, logiques métiers).
✅ Mot de passe Wi-Fi inextractible
Empêche la récupération du mot de passe même avec un accès physique à la mémoire.
Réduit les risques de piratage du réseau via l’extraction des identifiants.
Protège la confidentialité des données transmises sur le réseau.
✅ Clone du firmware impossible sans la clé de chiffrement
Empêche la duplication non autorisée du firmware (protection contre la contrefaçon).
Garantit l’authenticité du matériel (seuls les appareils légitimes peuvent fonctionner).
Protège les revenus et la réputation en évitant les copies illégales.
En résumé : Le chiffrement de la mémoire Flash renforce la sécurité, protège la propriété intellectuelle et limite les risques de piratage ou de contrefaçon. Un must pour les appareils connectés ! 🔒
PSRAM & partitions pour les ESP32 dans l’IDE Arduino
Avez-vous déjà remarqué les options de l’IDE Arduino permettant d’activer la PSRAM ou de sélectionner un schéma de partitionnement pour un ESP32 ? Savez-vous à quoi elles servent ?
Dans ce tutoriel, consacré à l’ESP32-C5, je vous expliquerai leur signification et comment les utiliser efficacement. Je présenterai d’abord les principaux types de mémoire (FLASH, SRAM, PSRAM et EEPROM), puis les différents schémas de partitionnement.
Le partitionnement est crucial car une configuration bien pensée permet d’exploiter pleinement les fonctionnalités de l’ESP32, comme le stockage de fichiers (par exemple, des pages web pour serveurs embarqués), les mises à jour OTA du firmware et la persistance des paramètres. Une mauvaise configuration peut entraîner des problèmes d’espace disque insuffisant, la perte de fonctionnalités ou une corruption de la mémoire, comme on peut le constater lors de débordements de tampon dans des programmes.
Avant d’explorer les fonctionnalités avancées de gestion de la mémoire de l’ESP32, nous reviendrons sur les bases à l’aide de l’exemple Arduino Uno afin de comprendre où sont stockées les données et comment optimiser l’espace disponible pour chaque type de mémoire.
Comprendre les mémoires d’un microcontrôleur avec l’exemple de l’Arduino Uno
Les microcontrôleurs utilisent différents types de mémoires à des fins différentes :
EEPROM : mémoire non volatile qui conserve les données même sans alimentation. Ce type de mémoire de petite capacité est optimisée pour des écritures fréquentes (100.000 cycles).
FLASH : comme la mémoire EEPROM, la mémoire FLASH est non volatile et conserve les données même sans alimentation. Ce type de mémoire a généralement une plus grande capacité, mais permet moins de cycles d’écriture (10.000 cycles).
SRAM : mémoire volatile ultra-rapide qui s’efface dès coupure alimentation.
L’arduino UNO R3 est basé sur le microcontrôleur ATMEGA 328P, équipé des mémoires suivantes selon la documentation de ce microcontrôleur :
32 ko de mémoire FLASH
1 ko de mémoire EEPROM
2 ko de mémoire SRAM
Arduino UNO R3 et son microcontrôleur ATMEGA 238P
Prenons par exemple le programme suivant, et expliquons l’utilisation de la mémoire :
C++
// Exemple illustrant l'utilisation des differentes memoires// d'un Arduino UNO R3//// https://tutoduino.fr/char* pointeur = NULL; // le compilateur reserve 2 octets en SRAM pour stocker ce pointeurvoidsetup() {Serial.begin(115200); pointeur = (char*)malloc(200 * sizeof(char)); // allocation dynamique de 200 octets en SRAMif (pointeur == NULL) {Serial.println("Erreur d'allocation mémoire!"); } else {free(pointeur); pointeur = NULL; }}voidloop() {delay(1000);}
Une fois compilé, le programme occupe 2264 octetsdeFlash. Les variables globales utilisent 228 octets de SRAM, laissant 1820 octets de disponibles sur les 2 ko de mémoire SRAM totale.
Plaintext
Sketch uses 2264 bytes (7%) of program storage space. Maximum is 32256 bytes.Global variables use 228 bytes (11%) of dynamic memory, leaving 1820 bytes for local variables. Maximum is 2048 bytes.
Note : Sur les 32 768 octets (32 ko) de Flash, 512 octets sont réservés au bootloader Arduino, laissant 32256 octets maximum pour le programme utilisateur.
Si le programme essaie d’allouer plus de mémoire que disponible en SRAM, l’allocation va échouer. Exemple ici avec une tentative d’allocation de 1900 octets :
Exemple d’erreur d’allocation mémoire liée à la taille de la SRAM
Il n’y a pas de schéma de partition configurable dans l’IDE Arduino pour l’Arduino UNO R3. En effet, son microcontrôleur utilise une mémoire flash fixe de 32 Ko, sans système de fichiers ni support OTA natif. Mais commençons par comprendre les différents types de mémoire utilisées par les microcontrôleurs :
Mémoires du XIAO ESP32-C5
Le module XIAO ESP32-C5 est basé sur le micro-contrôleur ESP32-C5 équipé d’une SRAM externe de 8 Mo ainsi que d’une FLASH externe de 8 Mo.
Le micro-contrôleur ESP32-C5 possède la mémoire interne suivante :
384 ko de SRAM haute performance (HP), utilisée par le cœur haute performance (HP core), qui fonctionne à une fréquence élevée (240 MHz).
16 ko de SRAM basse consommation (LP), associée au cœur basse consommation (LP core), qui fonctionne à une fréquence beaucoup plus basse (40 MHz).
320 ko de ROM, mémoire immuable en lecture seule, dédiée aux fonctions de base et au démarrage du système.
Seeed Studio XIAO ESP32-C5
L’adresse virtuelle est mappée à l’espace d’adressage physique de la mémoire externe via l’unité de gestion de la mémoire (MMU). Le tableau suivant présente les adresses virtuelles des différentes mémoires de l’ESP32-C5.
Note : Sur le XIAO ESP32-C5, la PSRAM externe et la Flash partagent l’espace d’adresses virtuelles 0x42000000–0x43FFFFFF car le SoC ESP32-C5 utilise un sous-système cache/MMU pour mapper les mémoires externes dans un espace d’adresses virtuelles commun. La Flash et la PSRAM sont mappées dynamiquement dans cette plage via les mappings de pages MMU plutôt que d’occuper des régions d’adresses virtuelles distinctes.
Mémoire SRAM interne et PSRAM externe du XIAO ESP32-C5
Pour revenir au début de ce tutoriel, rappelons que l’allocation dynamique de mémoire s’effectue dans la SRAM. Or, la SRAM interne du microcontrôleur ESP32-C5 est limitée à 384 Ko.
Si votre programme nécessite une allocation dynamique de mémoire plus importante, la PSRAM de 8 Mo du module XIAO ESP32-C5 entre en jeu. La PSRAM est une mémoire externe, connectée via SPI, conçue pour émuler la SRAM mais utilisant la technologie DRAM (Dynamic RAM).
Cependant, la PSRAM n’est pas activée par défaut dans la configuration ESP32 de l’IDE Arduino ; vous devez l’activer explicitement dans les paramètres de l’IDE.
Cet exemple démontre comment l’ESP32-C5 sélectionne automatiquement la SRAM pour les petites allocations (1 octet) et la PSRAM pour les grandes (1 Mo), les grandes allocations échouant lorsque la PSRAM est désactivée.
C++
// Example illustrating the use of SRAM and PSRAM// on a XIAO ESP32C5// https://tutoduino.fr/en/partition-esp32-arduino-en/#include<Arduino.h>#include"esp_heap_caps.h"#include"esp_system.h"/** * Function to display the memory address of any variable * @paramvariableName Name of the variable for display * @paramvariable Pointer to the variable to check */voidprintAddress(constchar*variableName, void*variable) {Serial.print("Memory address of '");Serial.print(variableName);Serial.print("' is: 0x");Serial.println((uintptr_t)variable, HEX);}voidsetup() {int* one_byte_pointer; // Pointer for small allocation test (1 byte)int* one_mbyte_pointer; // Pointer for large allocation test (1 MB)Serial.begin(115200);while (!Serial) ; // Wait for Serial Monitor to connectSerial.println("\n=== SRAM vs PSRAM usage on ESP32C5 ==="); // CHECK PSRAM AVAILABILITY AND STATUSsize_t psram_total = ESP.getPsramSize();size_t psram_free = ESP.getFreePsram();if (psram_total == 0) {Serial.println("❌ PSRAM not enabled or not detected");Serial.println(" Enable PSRAM in Arduino IDE Tools → PSRAM: 'Enabled'"); } else {Serial.println("✅ PSRAM enabled and detected");Serial.printf(" PSRAM Total: %d bytes (%.1f MB)\n", psram_total, psram_total / 1024.0 / 1024.0);Serial.printf(" PSRAM Free: %d bytes (%.1f MB)\n", psram_free, psram_free / 1024.0 / 1024.0); }Serial.println("\n--- Testing small allocation (1 byte) ---"); // TRY TO ALLOCATE 1 BYTE - Should use internal SRAM (384KB on ESP32-C5) one_byte_pointer = (int*)malloc(0x1); // Allocate 1 byteif (one_byte_pointer != NULL) {Serial.println("✅ Small allocation (1 byte) succeeded - Uses SRAM");printAddress("one_byte_pointer", (void*)one_byte_pointer); psram_free = ESP.getFreePsram();Serial.printf("\PSRAM Free: %d bytes (%.1f MB)\n", psram_free, psram_free / 1024.0 / 1024.0); } else {Serial.println("❌ Memory allocation failed for one_byte_pointer"); }Serial.println("\n--- Testing large allocation (1 MB) ---"); // TRY TO ALLOCATE 1MB - Should use PSRAM (8MB on XIAO ESP32C5) one_mbyte_pointer = (int*)malloc(0x100000); // Allocate 1 megabyte (1048576 bytes)if (one_mbyte_pointer != NULL) {Serial.println("✅ Large allocation (1 MB) succeeded - Uses PSRAM");printAddress("one_mbyte_pointer", (void*)one_mbyte_pointer); psram_free = ESP.getFreePsram();Serial.printf("\PSRAM Free: %d bytes (%.1f MB)\n", psram_free, psram_free / 1024.0 / 1024.0); // Free the large allocation to avoid memory leaksfree(one_mbyte_pointer); one_mbyte_pointer = NULL; } else {Serial.println("❌ Memory allocation failed for one_mbyte_pointer");Serial.println(" This happens if PSRAM is not enabled or allocation exceeds available space"); }}voidloop() { // Empty loop - all tests run once in setup()delay(1000);}
Lorsque la PSRAM est désactivée, l’allocation d’1 octet réussit dans la SRAM interne, mais l’allocation d’1 Mo échoue :
Plaintext
=== SRAM vs PSRAM usage on ESP32C5 ===❌ PSRAM not enabled or not detected Enable PSRAM in Arduino IDE Tools → PSRAM: 'Enabled'--- Testing small allocation (1 byte) ---✅ Small allocation (1 byte) succeeded - Uses SRAMMemory address of 'one_byte_pointer' is: 0x4085C834 (internal SRAM memory)PSRAM Free: 0 bytes (0.0 MB)--- Testing large allocation (1 MB) ---❌ Memory allocation failed for one_mbyte_pointer This happens if PSRAM is not enabled or allocation exceeds available space
Lorsque la PSRAM est activée, l’allocation de 1 octet s’effectue dans la SRAM interne tandis que l’allocation de 1 Mo s’effectue dans la PSRAM externe :
Plaintext
=== SRAM vs PSRAM usage on ESP32C5 ===✅ PSRAM enabled and detected PSRAM Total: 8388608 bytes (8.0 MB) PSRAM Free: 8386296 bytes (8.0 MB)--- Testing small allocation (1 byte) ---✅ Small allocation (1 byte) succeeded - Uses SRAMMemory address of 'one_byte_pointer' is: 0x4085C868 (internal SRAM memory)PSRAM Free: 8386296 bytes (8.0 MB)--- Testing large allocation (1 MB) ---✅ Large allocation (1 MB) succeeded - Uses PSRAMMemory address of 'one_mbyte_pointer' is: 0x42050908 (external PSRAM memory)PSRAM Free: 7337704 bytes (7.0 MB)
L’allocation de 1 Mo utilise bien de la PSRAM externe, et c’est bien confirmé par sa plage d’adresses (0x42000000-0x43FFFFFF).
Le schéma de partition de la mémoire Flash dans l’IDE Arduino
Le menu Partition Scheme de l’IDE Arduino permet de définir comment doit être utilisée la mémoire FLASH externe des microcontrôleurs ESP32.
Menu Partition Scheme de l’IDE Arduino pour un ESP32-C5
Les différents schémas de partitions pour ESP32-C5
Pour bien choisir un schéma de partition sur un ESP32, il est essentiel de comprendre les acronymes et les rôles de chaque section de la mémoire flash :
SPIFFS (Serial Peripheral Interface Flash File System) est un système de fichiers qui permet de créer, lire, modifier et supprimer des fichiers directement dans la mémoire flash. Ce système de fichiers gère l’usure, la fragmentation et la persistance des données, spécifique à la technologie flash. Ce système ne permet pas d’organiser les fichiers en dossiers (tous les fichiers sont à la racine).
FATFS est un système de fichiers est une implémentation légère du système de fichiers FAT (File Allocation Table) conçue pour les systèmes embarqués. Permet d’organiser les fichiers en dossiers et sous-dossiers et permet de gérer des fichiers et des partitions bien plus grandes que SPIFFS.
APP (Application) est la partition dédiée au stockage du firmware (programme compilé) de l’ESP32. La partition APP peut être divisée en plusieurs partitions (ex : app0, app1) pour permettre des mises à jour du firmware à distance (OTA). La partition app0 contient le firmware principal (version actuelle ou précédente) alors que la partition app1 contient une seconde copie du firmware. Pendant qu’une nouvelle version est téléchargée dans app1, app0 continue de fonctionner. Après la mise à jour, l’ESP32 redémarre sur app1.
OTA (Over-The-Air) est la partition qui stocke les métadonnées pour les mises à jourOTA, comme l’état de la dernière mise à jour ou la partition active (app0 ou app1).
Les schémas de partition sont stockés dans le répertoire d’installation du core ESP32 pour Arduino. Par exemple sous Linux :
Offset : Offset en hexadécimal dans la mémoire flash.
Size : Taille de la partition en hexadécimal.
Flags : Options supplémentaires (ex : encrypted).
Voici par exemple le contenu du fichier default_8MB.csv
L’IDE Arduino utilise le fichier de partition (.csv) sélectionné pour générer une table de partition binaire, intégrée au firmware. Cette table est écrite dans la mémoire flash de l’ESP32, à l’adresse 0x8000, lors du téléversement. Le bootloader de l’ESP32 lit cette table lors de son exécution pour savoir où se trouvent les partitions (APP, SPIFFS, OTA, etc.).
Exemple de problème lié aux schémas de partition de la Flash
Vous souhaitez développer un projet de lampe connectée basée sur un ESP32-C5, vous allez exploiter les capacités de ce microcontrôleur pour créer un appareil compatible avec Home Assistant, en utilisant le protocole Matter sur Thread. Ce choix vous permet de concevoir un objet connecté sécurisé, interopérable et facilement intégrable dans un écosystème domotique existant. Je vous invite à lire mon tutoriel Intégrer un appareil Matter (Wi-Fi et Thread) dans Home Assistant.
Pour ce projet, vous partez de l’exemple MatterOnOffLight, fourni par Espressif. Cet exemple est spécialement conçu pour les applications de type “lampe connectée” et intègre la prise en charge du protocole Matter sur Thread, ce qui simplifie grandement l’intégration avec Home Assistant.
Essayons d’utiliser le schéma de partition Minimal pour ce programme :
Ce schéma de partition minimal.csv réserve une partition app0 de type app et de taille 0x140000 (1310720 octets) :
Une erreur apparaît lors de la compilation du programme. En effet, le programme compilé occupe 2193968 octets, ce qui est supérieur à la taille de la partition app configurée par le schéma de partition Minimal sélectionné (1310720 octets) :
Plaintext
Le croquis utilise 2193968 octets (167%) de l'espace de stockage de programmes. Le maximum est de 1310720 octets.
Le fichier README de cet exemple indique bien qu’il faut sélectionner le schéma de partition Huge APP (3MB No OTA/1MB SPIFFS).
La partition app0 de type app est en effet bien configurée avec une taille 0x300000 (3145728 octets) dans le schéma de partition huge_app.csv :
Une fois compilé, le firmware occupe 2193976 octets de la mémoire flash. Ce qui est bien inférieur au maximum de 3145728 octets (3 Mo) réservé pour notre partition APP.
Plaintext
Le croquis utilise 2193976 octets (69%) de l'espace de stockage de programmes. Le maximum est de 3145728 octets.Les variables globales utilisent 100740 octets (30%) de mémoire dynamique, ce qui laisse 226940 octets pour les variables locales. Le maximum est de 327680 octets.
Afficher l’heure des prochains passages de votre bus IdFM sur un LilyGo T-Display
Vous voulez savoir combien de temps il vous reste avant l’arrivée de votre prochain bus ?
Si les panneaux d’affichage aux arrêts ou certaines applications smartphone permettent déjà de consulter ces informations, les avoir directement sur un petit écran dédié chez vous est encore plus pratique : vous pouvez partir au dernier moment et éviter toute attente inutile.
Dans cet article, je vous explique comment afficher en temps réel les horaires des prochains passages de bus à votre arrêt, directement sur un LilyGo T-Display.
Mais pour commencer, je vous invite à lire mon article d’introduction au LilyGo T-Display pour apprendre les bases de sa programmation.
Utilisation de la plateforme PRIM d’Île de France Mobilité
Il est nécessaire de vous créer un compte sur cette plateforme et de générer votre jeton authentification.
Il faut ensuite chercher les identifiants de l’arrêt et des lignes de bus et pour lesquelles vous souhaitez connaître les horaires des prochains passages :
Les identifiants ID_Line de vos lignes de bus sont disponibles dans le référentiel des lignes
Exemple de recherche de l’identifiant de l’arrêt Pierre Semard dans la ville de Châtillon :
ArRid de l’arrêt Pierre Semard à Chatillon est 28607
Il y a généralement deux arrêts qui portent le même nom. Afin de distinguer l’arrêt correspond au sens de circulation du bus pour atteindre votre destination, je vous conseille de chercher l’identifiant de l’arrêt à partir de la carte.
Exemple de recherche de l’identifiant de la ligne de bus 195 :
L’ID_Line de la ligne 195 est C01215
Une fois votre jeton d’authentification obtenu et l’identifiant de votre arrêt trouvé, vous pouvez interroger l’API PRIM pour récupérer les horaires des prochains passages de bus à cet arrêt.
Voici par exemple une commande curl permettant d’obtenir les prochains passages à l’arrêt Pierre Sémard (remplacez YOUR-TOKEN par votre clé API PRIM) :
Résultat affiché après l’exécution de la commande curl sous Linux :
La plateforme PRIM retourne les informations au format JSON. Vous trouvez dans le champ MonitoredStopVisit les informations relatives à l’arrêt (MonitoringRef). Dont la liste des bus avec leur identifiant de ligne (LineRef) ainsi que l’heure prévue du prochain départ (ExpectedDepartureTime).
Exemple dans lequel on voit que l’horaire de passage du prochain bus sera à 14h53 (UTC)
Notez que les horaires sont fournis en heure universelle (UTC). Il conviendra donc d’ajuster ce horaire avec l’heure locale en prenant en compte le décalage entre l’heure d’été et l’heure d’hiver.
Le croquis pour IDE Arduino
Voici le croquis qui permet d’afficher les horaires des prochains passages des bus 195 et 388 l’arrêt Pierre Semard dans la ville de Chatillon.
C++
// Recuperation des horaires des prochains passages de bus a un arret en Ile de France// Les informations sont recuperees de la plateform PRIM geree par IdFM// Affichage de ces informations sur un LilyGo T-Display// https://tutoduino.fr/// Copyleft 2025#include<Adafruit_GFX.h>#include<Adafruit_ST7789.h>#include<Fonts/FreeSansBold18pt7b.h>#include<Fonts/FreeSansBold12pt7b.h>#include<time.h>#include<WiFi.h>#include<HTTPClient.h>#include<ArduinoJson.h>#include<esp_sleep.h>#include"secrets.h"#include"line_mapping.h"// Bouton qui permet de sortir de veille profonde#define BUTTON_PIN 35// Broches du ST7789 sur le LilyGo T-Display#define TFT_MOSI 19#define TFT_SCLK 18#define TFT_CS 5#define TFT_DC 16#define TFT_RST 23#define TFT_BL 4// Objet sur l'ecran du LilyGo T-DisplayAdafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);// Nombre de prochains passages a afficher#define NB_BUS_DISPLAYED 3// Nombre de prochains passages à lire dans la réponse du serveur PRIM#define MAX_NB_BUS_SCHEDULE 6// Structure pour stocker l'heurestructmyTime_t {int heure;int minute;};// Structure pour stocker les informations sur les prochains passagesstructbusSchedule_t { String busLine;myTime_t busTime;};// URL du service, il doit etre completee par l'identifiant de l'arret de bus qui est stocke dans "secrets.h"constchar* serviceUrl = "https://prim.iledefrance-mobilites.fr/marketplace/stop-monitoring?MonitoringRef=";// Variable globale utilisée pour la mise en veilleuint8_t loopCounter;// ---------------------------------------------------------------------------// Vérifie si time1 est superieur ou egal a time2 (seulement heure/minute)// ---------------------------------------------------------------------------boolisMyTimeGreaterOrEqual(myTime_ttime1, tmtime2) { // Compare heuresif (time1.heure != time2.tm_hour) {returntime1.heure > time2.tm_hour; } // Heures égales → compare minutesreturntime1.minute >= time2.tm_min;}// ---------------------------------------------------------------------------// Synchronisation de l'heure NTP + fuseau France (DST auto)// ---------------------------------------------------------------------------voidsetupTime() { // Fuseau France normalisé : CET-1CEST,M3.5.0/2,M10.5.0/3configTzTime("CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org");Serial.println("Heure synchronisée (France)");}// ---------------------------------------------------------------------------// Convertit l'heure UTC en heure locale// ---------------------------------------------------------------------------myTime_tconvertUTCtoLocal(myTime_tutcTime) {structtmlocalTime = { 0 };charbuffer[6];myTime_t myLocalTime; // Recuperation de l'heure locale en UTC pour vérifier si // nous sommes en heures d'hiver ou heure d'ete (isdst)if (!getLocalTime(&localTime)) {Serial.println("Erreur : impossible d'obtenir l'heure locale !");return utcTime; // fallback } myLocalTime = utcTime;switch (localTime.tm_isdst) {case0: // heure d'hiver = UTC + 1myLocalTime.heure += 1;break;case1: // heure d'ete = UTC + 2myLocalTime.heure += 2;break;default: // indetermine ou erreur, retourne l'heure UTCbreak; } // Gestion dépassement 24hif (myLocalTime.heure >= 24) {myLocalTime.heure -= 24; }if (myLocalTime.heure < 0) {myLocalTime.heure += 24; }return myLocalTime;}// ---------------------------------------------------------------------------// Extrait l'heure HH:MM d'une chaîne ISO 8601 "YYYY-MM-DDTHH:MM:SS.000Z"// ---------------------------------------------------------------------------myTime_tgetTimeHHMM(constchar*isoString) {myTime_t myTime; // JJ et MM sont a des positions fixes dans ISO 8601myTime.heure = (isoString[11] - '0') * 10 + (isoString[12] - '0');myTime.minute = (isoString[14] - '0') * 10 + (isoString[15] - '0');return myTime;}// -----------------------------------------------------------------------------// Appel de l'API PRIM pour obtenir les horaires des passages des prochains bus// Retourne le nombre d'horaires contenant des informations sur les lignes// qui nous interessent.// -----------------------------------------------------------------------------intgetExpectedDepartureTime(busSchedule_tschedules[MAX_NB_BUS_SCHEDULE]) { // Nombre d'horaire retournéint nbScheduleInfo = 0; // Initialise les MAX_NB_BUS_SCHEDULE prochains busfor (int i = 0; i < MAX_NB_BUS_SCHEDULE; i++) {schedules[i].busLine = "";schedules[i].busTime.heure = 0;schedules[i].busTime.minute = 0; } // Verifie que le Wi-Fi est bien connecteif (WiFi.status() != WL_CONNECTED) {Serial.println("ERROR: Wi-Fi not connected.");return0; } // Contruit la requete HTTP pour l'arret concerne HTTPClient http; String fullUrl = String(serviceUrl) + String(stopPointRef);http.begin(fullUrl);http.addHeader("apikey", apiKey);http.addHeader("accept", "application/json"); // Envoir la requete et recupere la reponse (payload)int httpCode = http.GET();if (httpCode != HTTP_CODE_OK) {Serial.print("HTTP Error: ");Serial.println(httpCode);Serial.println(http.getString());http.end();return0; } String payload = http.getString();http.end();Serial.println(payload); // La reponse au format JSON peut être volumineuse en fonction du nombre de lignes de bus a cet arret DynamicJsonDocument doc(40 * 1024);if (deserializeJson(doc, payload, DeserializationOption::NestingLimit(20))) {Serial.println("JSON parsing error !");return0; } // Tableau de N elements contenant les informations sur le passage // dont le numero de la ligne et l'heure de départ JsonArray visits =doc["Siri"]["ServiceDelivery"]["StopMonitoringDelivery"][0]["MonitoredStopVisit"]; // Recupere les informations (ligne de bus et heure de depart) pour les // prochains passages, en se limiant à MAX_NB_BUS_SCHEDULE elementsfor (JsonObject visit : visits) {if (nbScheduleInfo >= MAX_NB_BUS_SCHEDULE) {Serial.println("Maximum number of information collected, skip following ones");break; } // Identifiant de la ligne de busconstchar* lineRef = visit["MonitoredVehicleJourney"]["LineRef"]["value"]; // Horaire du prochain departconstchar* expectedDepartureTime =visit["MonitoredVehicleJourney"]["MonitoredCall"]["ExpectedDepartureTime"];if (!lineRef || !expectedDepartureTime) {Serial.println("No data");continue; } // Vérifie que nous souhaitons les informations concernant cette ligne de busbool isKnownLine = false;for (int i = 0; i < lineMappingsSize; i++) {if (lineMappings[i].ref == String(lineRef)) { isKnownLine = true;break; } } // Rertourner l'heure du prochain depart et le numero de la ligne si cette ligne // nous interesseif (isKnownLine) {schedules[nbScheduleInfo].busLine = mapLineRefToNumber(lineRef);schedules[nbScheduleInfo].busTime = convertUTCtoLocal(getTimeHHMM(expectedDepartureTime)); nbScheduleInfo++; } }return nbScheduleInfo;}// -----------------------------------------------------------------------------// Mise en veille profonde de l'ESP32 et de l'ecran LCD// -----------------------------------------------------------------------------voidenterDeepSleep() { // Configure la sortie de veille par appui sur boutonesp_sleep_enable_ext0_wakeup((gpio_num_t)BUTTON_PIN, LOW); // Éteindre écran et le mettre en veilledigitalWrite(TFT_BL, LOW);tft.writeCommand(0x10); // Activer la veille profonde du microcontrolleuresp_deep_sleep_start();}// ---------------------------------------------------------------------------// SETUP// ---------------------------------------------------------------------------voidsetup() {Serial.begin(115200);delay(1000); // Le bouton est une entrée qui permet de sortir de veillepinMode(BUTTON_PIN, INPUT_PULLUP); // Connexion Wi-FiWiFi.begin(wifiSsid, wifiPassword);Serial.print("Connexion Wi-Fi");while (WiFi.status() != WL_CONNECTED) {delay(500);Serial.print("."); }Serial.println("\nConnecté au Wi-Fi"); // Configure la recuperation de l'heure et le fuseau horairesetupTime(); // NTP + TZ // Allume le rétroéclairagepinMode(TFT_BL, OUTPUT);digitalWrite(TFT_BL, HIGH); // Initialisation écrantft.init(135, 240);tft.setRotation(1);tft.setFont(&FreeSansBold18pt7b);delay(1000); loopCounter = 0;}// ---------------------------------------------------------------------------// BOUCLE PRINCIPALE// ---------------------------------------------------------------------------voidloop() {busSchedule_tnextBuses[MAX_NB_BUS_SCHEDULE];structtmtimeinfo;chartimeString[6];charbuf[15]; // Met l'ESP32 en veille profonde et eteind l'ecran // après 1 minutes (30 sec par loop) loopCounter++;if (loopCounter > 2) {enterDeepSleep(); }tft.fillScreen(ST77XX_BLACK); // Affiche l'heure localeif (getLocalTime(&timeinfo)) {strftime(timeString, sizeof(timeString), "%H:%M", &timeinfo);Serial.print("Heure locale: ");Serial.println(timeString);tft.setTextColor(ST77XX_WHITE);tft.setCursor(70, 25);tft.print(timeString);tft.drawLine(0, 32, 240, 32, ST77XX_WHITE); } // Récupère les horaires des prochains busint numBuses = getExpectedDepartureTime(nextBuses);Serial.printf("numBuses = %d\n", numBuses);if (numBuses > MAX_NB_BUS_SCHEDULE) {Serial.printf("Erreur\n");return; } // Affichage de la ligne et de l'heure de départ des prochains bus // Le nombre de bus affiches est limite a NB_BUS_DISPLAYED // Seuls les bus dont l'horaire de depart est posterieur a // l'heure courante sont affichesint nbBusDisplayed = 0;int nbBus = 0;while ((nbBus <= numBuses) && (nbBusDisplayed < NB_BUS_DISPLAYED)) { // N'affiche que les bus dont l'heure de depart est superieur a l'heure actuelleif (isMyTimeGreaterOrEqual(nextBuses[nbBus].busTime, timeinfo)) {Serial.printf("Ligne %s%02d:%02d\n",nextBuses[nbBus].busLine,nextBuses[nbBus].busTime.heure,nextBuses[nbBus].busTime.minute);snprintf(buf, sizeof(buf), "%s%02d:%02d",nextBuses[nbBus].busLine, nextBuses[nbBus].busTime.heure, nextBuses[nbBus].busTime.minute);tft.setTextColor(ST77XX_CYAN);tft.setCursor(0, 65 + 30 * nbBusDisplayed);tft.print(buf); nbBusDisplayed++; } nbBus++; } // Réactualise toutes les 30 secondesdelay(30 * 1000);}
Les identifiants de votre arrêt de bus ainsi que ceux des lignes à surveiller doivent être renseignés dans le fichier line_mapping.h.
C++
#ifndef LINEMAPPING_H#define LINEMAPPING_H#include<Arduino.h>// pour String// Identifiant de l'arretconstchar* stopPointRef = "STIF:StopPoint:Q:28607:";// Structure pour stocker le mappingstructLineMapping { String ref; // LineRef PRIM String number; // numero de la ligne};// Tableau de correspondance entre les references PRIM des lignes et le numero des lignesstaticconst LineMapping lineMappings[] = { // Cette table contient la liste des lignes de bus s'arretant a l'arret stop_point_ref // et dont vous souhaitez les horaires des prochains passages { "STIF:Line::C01215:", "195" }, { "STIF:Line::C01314:", "388" },};// Taille du tableaustaticconstint lineMappingsSize = sizeof(lineMappings) / sizeof(LineMapping);// Fonction pour récupérer le numéro public de la ligne de bus à partir de son identifiant LineRefinlineStringmapLineRefToNumber(constString&lineRef) {for (int i = 0; i < lineMappingsSize; i++) {if (lineMappings[i].ref == lineRef) {returnlineMappings[i].number; } }return"XXX"; // inconnu}#endif // LINEMAPPING_H
Il est recommandé de stocker votre jeton d’authentification PRIM ainsi que le SSID et le mot de passe de votre réseau Wi-Fi dans le fichier secrets.h.
Mon programme utilise les librairies suivantes avec l’IDE Arduino :
Exemple d’affichage sur le LilyGo T-Display des prochains passages à l’arrêt Pierre Sémard, à Châtillon, pour les lignes 195 et 388.
Quota de requêtes sur une API PRIM
Le nombre de requêtes par API sur la plateforme PRIM est limité, et le suivi de votre consommation est disponible sur cette page.
Par exemple, le nombre de requêtes unitaires pour récupérer les horaires des prochains passages est limité à 500 par jour (il s’agit de quota journalier).
Il est cependant possible d’augmenter ce quota pour cette API.
Le code de ce projet est disponible sur mon GitHub.
LILYGO T-Display sous IDE Arduino
Le LILYGO T-Display est une carte de développement basée sur un microcontrôleur ESP32, équipée d’un écran LCD ST7789V de 1.14″ offrant une résolution de 135 x 240 pixels.
Schéma des broches
Voici la schéma des broches de la carte, qui met en évidence les broches utilisées la communication SPI avec l’écran ST7789 (MOSI, SCLK, CS, DC, RST) ainsi que la commande du rétroéclairage via la broche GPIO4.
Installation du gestionnaire de cartes ESP32
La carte étant basé sur le microcontrôleur ESP32 il faut installer le support des cartes Espressif ESP32 dans l’IDE Arduino.
Dans le menu préférences de l’IDE Arduino, ajoutez l’URL suivante dans le gestionnaire de carte :
Installez le paquet de gestion de cartes esp32 de Espressif Systems.
Une fois le gestionnaire de cartes installé, sélectionnez la carte LilyGo T-Display dans la liste des cartes esp32 disponibles.
Installation des librairies pour l’écran LCD
Installez la librairie Adafruit ST7735 and ST7789 Library.
Afficher un texte sur l’écran du LilyGo T-Display
Pour vérifier que les bibliothèques et la configuration de la carte sont correctement installées, nous allons afficher un message de test sur l’écran à l’aide du code suivant :
Une fois le code compilé et téléversé sur le TTGO, et si tout s’est bien déroulé, vous devriez voir s’afficher Tutoduino sur son écran.
Pour aller plus loin, découvrez mon tutoriel complet sur l’affichage en temps réel des horaires de bus Île-de-France avec le LilyGo T-Display.
Capteur de température ESP32-C6 avec ESPHome
Dans mon tutoriel précédent Créer un capteur de température Zigbee sur batterie avec un ESP32C6, j’explique en détail comment programmer un capteur de température, basé sur un microcontrôleur ESP32-C6 et un capteur BME280, communiquant via le protocole Zigbee avec son serveur Home Assistant.
Capteur de température sur batterie basé sur un BME280 et un ESP32C6
Dans ce nouveau tutoriel, je vous montre comment configurer ce capteur avec ESPHome. ESPHome est un framework open-source qui permet de générer le firmware de microcontrôleur comme les ESP32 ou ESP8266 à partir de simples fichiers de configuration YAML, sans nécessiter la moindre compétence en programmation.
Comme la prise en charge de Zigbee n’est pas encore intégrée à ESPHome, le capteur communiquera avec le serveur Home Assistant via Wi-Fi.
Le capteur fonctionnant sur batterie, j’ai concentré mes efforts sur l’optimisation de sa consommation énergétique.
Voici la configuration permettant au capteur de relever la température, la pression et l’hygrométrie toutes les 15 minutes, puis de transmettre ces données au serveur Home Assistant via le Wi-Fi :
YAML
esphome:name: capteur-temp-esp32c6friendly_name: capteur-temp-esp32c6esp32:board: esp32-c6-devkitc-1framework:type: esp-idf# Enable logging only for warningslogger:level: WARN# Enable Home Assistant APIapi:encryption:key: "your-key"ota: - platform: esphomepassword: "your-pwd"wifi:ssid: !secretwifi_ssidpassword: !secretwifi_passwordfast_connect: true# Disable Wi-Fi scanning, direct connection to ssid on_connect:# Update sensors - component.update: bme280_measure - component.update: battery_voltage - delay: 5s# Delay to ensure that data is sent to HA - deep_sleep.enter: deep_sleep_1switch: - platform: gpiopin: GPIO3id: rf_switchname: "RF Switch"internal: truerestore_mode: ALWAYS_OFF - platform: gpiopin: GPIO14id: antenna_selectname: "Antenna Select"internal: truerestore_mode: ALWAYS_ON# Uses external antennai2c:sda: GPIO22scl: GPIO23scan: Falsesensor: - platform: bme280_i2cid: bme280_measuretemperature:name: "Temperature BME280"pressure:name: "Pressure BME280"humidity:name: "Humidity BME280"address: 0x76update_interval: never - platform: adcpin: GPIO0name: "Battery Voltage"id: battery_voltageupdate_interval: neverattenuation: 12db# For ADC inputs > 1.1V (extends range to ~2.45V)filters: - multiply: 2.0# Voltage divider (div 2) compensation# Deep sleep configuration to save powerdeep_sleep:id: deep_sleep_1sleep_duration: 15min# Sleep duration between wake-ups
Optimisation de la configuration Wi-Fi
Dans cette configuration ESPHome, la définition de fast_connect sur true permet de désactiver le scan du réseau Wi-Fi et ainsi accélérer la connexion à votre réseau :
YAML
fast_connect: true# Disable Wi-Fi scanning, direct connection to ssid
Le bloc on_connect permet d’exécuter des actions dès que la connexion Wi-Fi est établie. L’instruction component.update déclenche la mise à jour du capteur concerné. Ainsi, les mesures du BME280 ainsi que la tension de la batterie sont actualisées automatiquement grâce à cette configuration :
Cette mise à jour est suivie d’une temporisation de 5 secondes, le temps nécessaire pour que le capteur transmette ses données au serveur Home Assistant via le Wi-Fi. L’ESP32-C6 entre ensuite en veille profonde afin de minimiser au maximum la consommation de la batterie.
YAML
- delay: 5s# Delay to ensure that data is sent to HA- deep_sleep.enter: deep_sleep_1
Configuration de la veille profonde
Généralement la veille profonde (deep_sleep) est configurée par deux paramètres :
run_duration : Durée pendant laquelle l’ESP32C6 doit être actif, c’est-à-dire exécuter du code.
sleep_duration : La durée pendant laquelle l’ESP32C6 reste en mode de sommeil profond.
Ici, seul le paramètre sleep_duration est configuré. En effet, la durée d’activité ne doit pas être définie si l’on souhaite que l’ESP32C6 passe en veille profonde via l’instruction deep_sleep.enter.
YAML
# Deep sleep configuration to save powerdeep_sleep:id: deep_sleep_1sleep_duration: 15min# Sleep duration between wake-ups
Mesure de la puissance du signal Wi-Fi
Afin de valider la bonne réception du signal Wi-Fi par le capteur, il est utile de configurer la remontée du RSSI dans Home Assistant.
Cette information est fournie par ESPHome, et a pour unité le dBm, voir mon article “C’est quoi… un dBm ?” pour en savoir plus. Pour l’obtenir, il suffit de déclarer le capteur wifi_signal dans la configuration d’ESPHome :
YAML
sensor: - platform: wifi_signalname: "Signal WiFi"id: wifi_signal_strengthupdate_interval: never
Le rafraîchissement de la mesure est faite lorsque l’ESP32-C6 est connectée au réseau Wi-Fi :
YAML
wifi:ssid: !secretwifi_ssidpassword: !secretwifi_passwordfast_connect: true# Disable Wi-Fi scanning, direct connection to ssid on_connect:# Update sensors - component.update: bme280_measure - component.update: battery_voltage - component.update: wifi_signal_strength - delay: 5s# Delay to ensure that data is sent to HA - deep_sleep.enter: deep_sleep_1
Configuration de l’antenne externe
Comme expliqué dans la Getting Started de l’ESP32C6, l’activation de l’antenne externe nécessite une configuration spécifique. Par défaut, l’ESP32C6 utilise son antenne céramique interne pour le Wi-Fi.
Le choix entre l’antenne interne et une antenne externe se fait via la broche GPIO14.
GPIO14 à l’état bas (paramètre par défaut) : l’appareil utilise l’antenne céramique intégrée.
GPIO14 à l’état haut : l’appareil bascule vers l’antenne externe.
Pour que cette sélection soit effective, il faut d’abord placer la broche GPIO3 à l’état bas, ce qui active le contrôle du commutateur RF.
Voici la configuration pour utiliser une antenne externe :
Pour utiliser l’antenne interne, il faut configurer restore_mode sur ALWAYS_OFF pour la GPIO14.
La remontée du RSSI vers Home Assistant permet de confirmer l’apport bénéfique de l’usage d’une antenne externe.
Avec l’utilisation de l’antenne interne, le RSSI est de –70 dBm.
Avec l’utilisation d’une antenne externe, le RSSI atteint -52 dBm.
Consommation électrique
La capture suivante illustre la consommation du capteur lorsqu’il sort de veille, effectue les mesures, les transmet au serveur Home Assistant via le Wi-Fi, puis retourne en veille profonde.
On observe que lorsque le capteur est actif et qu’il communique ses données au serveur Home Assistant en Wi-Fi, sa consommation oscille entre 26-54 mA. Lorsque l’ESP32C6 est en veille profonde, sa consommation est réduite à 23μA.
J’espère que ce tutoriel vous a été utile. N’hésitez pas à me faire part de votre avis en cliquant sur les étoiles ci-dessous ou en laissant un commentaire.
C’est quoi… un dBm ?
Avant d’aborder le concept de dBm (décibel-milliwatt), il est essentiel de comprendre le concept de décibel.
Le décibel (dB)
Le décibel (dB) est une unité logarithmique qui exprime le rapport entre deux puissances.
Par exemple, si nous avons 2 puissances P1 = 100 mW et P0 = 50 mW, le rapport entre ces deux puissances est de 2, ce qui correspond à environ 3 dB.
Vous entendez ou entendrez souvent parler de ce nombre -3 dB. En effet, diviser par deux la puissance d’un signal correspond à une diminution de 3 dB. On dit qu’une atténuation de -3 dB signifie une puissance du signal divisée par 2.
Le décibel-milliwatt (dBm)
Le dBm (décibel-milliwatt) est une unité utilisée pour mesurer la puissance d’un signal électrique, en prenant comme référence fixe une puissance de 1 milliwatt (mW).
Cette unité est très largement utilisé pour mesurer la puissance des signaux mobiles (3/4/5G, Wi-Fi, LoRa…) afin de garantir une bonne réception et des performances optimales.
Exemple sur Linux, la commande iwconfig vous indique les informations sur votre connexion Wi-Fi (carte réseau Wi-Fi wlp45s0 dans cet exemple). On y voit la puissance du signal reçue qui est de -63 dBm.
On voit que mon PC reçoit un signal Wi-Fi de bonne qualité (-63 dBm) me permettant un débit élevé (234 Mbit/s).
En approchant le PC de ma box internet, le signal est de bien meilleur qualité (- 31 dBm) et permet des débits supérieurs. Au contraire, le signal devient de mauvaise qualité (-80 dBm) lorsque j’éloigne le PC de ma box, et le débit s’effondre à 6,5 Mb/s.
Voici quelques exemples de puissance de signal reçu et leur interprétation :
Puissance du signal reçu
Interprétation
-30 dBm
Signal très fort, garanti une excellente connexion
-50 dBm
Signal excellent, adapté à la plupart des usages exigeants (streaming…)
-65 dBm
Signal acceptable pour une connexion stable et fiable
-90 dBm
Signal très faible ne permettant pas une connexion stable
Vous voyez que la distance entre l’émetteur et le récepteur joue un rôle considérable dans la puissance du signal reçu. En effet, la perte en puissance varie avec le carré de la distance. Aussi avec le doublement de la distance entre l’émetteur et le récepteur, la puissance reçue est divisée par 4. Cette perte dépend également de la fréquence du signal.
Exemple de calcul de puissance de réception d’un signal Wi-Fi
Prenons comme exemple une box internet placée à 10 mètres de mon ordinateur et qui émet sur la fréquence 2,4 GHz.
avec d = distance en mètres ; f = fréquence du signal en hertz ; c = célérité de la lumière en mètre par seconde
En appliquant cette formule, nous calculons une perte de –60 dBm pour une distance de 10 m avec une fréquence de 2400000000 Hz.
La puissance d’émission de la livebox est 100 mW, soit +20 dBm. En effet, PdB=10*log(100 mW/1 mW)=10*2=20 dBm.
Le signal à l’arrivée sera donc de -40 dBm (+20dBm – 60 dBm).
Vous trouverez sur le site sosh.fr des exemples d’atténuation liés aux matériaux (cloison de plâtre = -7 dBm, mur porteur = -15 dBm…).
Home Assistant : Afficher les données Météo-France sur un reTerminal E1001 avec ESPHome
Dans ce tutoriel, je vais vous montrer comment afficher les données de l’intégration Météo‑France de Home Assistant sur un reTerminal E1001 à l’aide du framework ESPHome. Vous apprendrez à récupérer les informations météorologiques (température, conditions, prévisions) depuis Home Assistant et à les afficher sur l’écran e‑paper du reTerminal.
Affichage des prévisions Météo-France sur le reTerminal E1001
Commencez par ajouter l’intégration Météo-France dans Home Assistant, puis sélectionnez la ville correspondant à votre lieu de résidence ou celle pour laquelle vous souhaitez obtenir les prévisions météorologiques (Nantes par exemple).
L’intégration Météo‑France prend en charge les deux plateformes suivantes dans Home Assistant : Weather et Sensor, qui permettent d’accéder aux conditions et prévisions météorologiques :
Weather : fournit des données météorologiques complètes, incluant la météo actuelle, les prévisions horaires détaillées ainsi que les prévisions journalières sur 2 semaines.
Sensor : expose une série de capteurs spécifiques qui délivrent des mesures précises sur les conditions locales. Parmi ces capteurs, on retrouve la température, la pression atmosphérique, la vitesse et la direction du vent, le taux d’humidité, la probabilité de neige, ainsi que les alertes météorologiques officielles.
Nous allons d’abord vérifier les informations fournies par cette intégration, en utilisant l’onglet « États » des Outils de développement. En saisissant weather.nantes comme filtre, vous pouvez constater que cette entité possède la condition météorologique (rainy) comme état et plusieurs attributs (temperature, humidity, etc.). Il s’agit ici des informations exposée par la plateforme Sensor de l’intégration Météo-France.
Pour accéder aux prévisions météo il faut récupérer les données de la plateforme Weather de l’intégration Météo-France. Pour cela il faut aller dans l’onglet Actions et exécuter l’action weather.get_forecasts sur l’entité weather.nantes et en indiquant le type daily (pour les prévisions journalières) ou hourly (pour les prévisions horaires).
Voici la configuration YAML de cette action pour récupérer les prévisions journalières :
Les données de la plateforme Weather de l’intégration Météo-France s’afficheront, incluant les prévisions météorologiques de prochains jours : condition, temperature (max), templow (min), precipitation, humidity.
Affichage de la température extérieure
Pour afficher la température extérieure, j’utilise la température actuelle fournie par la plateforme Sensor de l’intégration Météo-France.
Affichage de la température extérieure
L’entité temperature de l’intégration Météo-France n’est pas activée par défaut. Il faut donc l’activer dans la configuration de l’intégration. Notez l’identifiant de cette entité, qui dans cet exemple est : sensor.nantes_temperature.
Il faut déclarer cette entité en tant que capteur (sensor) dans la configuration YAML de ESPHome :
YAML
# Declaration du capteur home assistant sensor.nantes_temperaturesensor: - platform: homeassistantid: temperature_nantesentity_id: sensor.nantes_temperature
Vous pouvez afficher la valeur de ce capteur sur l’écran du reTerminal E1001 via la configuration ESPHome suivante :
Pour afficher la température intérieure, j’utilise le capteur interne STH40 du reTerminal.
Température intérieure mesurée par le capteur interne du reTerminal E1001
Le microcontrôleur ESP32-S3 du reTerminal E1001 communique avec le capteur STH40 au travers d’un bus I2C. Son adresse I2C est 0x44, et le bus I2C utilise les broches suivantes :
Serial Data (SDA) : GPIO19
Serial Clock (SCL) : GPIO20
Voici la configuration YAML qui permet de récupérer la température et l’humidité depuis ce capteur, à copier juste après la ligne captive_portal: du fichier de configuration ESPHome :
YAML
# define I2C interfacei2c:sda: GPIO19scl: GPIO20scan: false# temperature and humidity sensorsensor: - platform: sht4xtemperature:name: "Temperature"id: sht4x_temperaturehumidity:name: "Humidity"id: sht4x_humidityaddress: 0x44update_interval: 60s
Vous pouvez afficher la valeur de ce capteur sur l’écran du reTerminal E1001 via la configuration ESPHome suivante :
Notez que le formatage %.1f indique qu’il faut afficher la température en nombre flottant avec 1 chiffre derrière la virgule (ex: 18.1).
Affichage de la température d’une autre pièce
Pour afficher la température d’une autre pièce, ici ma serre, j’utilise un capteur de température externe connecté en Zigbee à mon routeur Home Assistant.
Température mesurée par un capteur de température relié à Home Assistant en Zigbee
Il est très simple de récupérer la valeur de ce capteur, il suffit de repérer son ID d’identité (ici sensor.lumi_lumi_weather_temperature_2) dans la configuration Home Assistant.
Entity_id du capteur de température externe
Puis de déclarer ce capteur dans la configuration ESPHome.
Il est possible d’afficher les températures minimum et maximum mesurées par ce capteur au cours des dernières 48h. Il suffit d’ajouter une intégration de type Statistics (helper) :
Ajout d’une intégration de type statistics
Vous pouvez ensuite créer un capteur de statistiques en indiquant l’entité auquel la statistique va s’appliquer (ici notre capteur de température de la serre) :
Puis configurer la caractéristique de la statistique souhaitée (ici sa valeur minimale) :
Et enfin la configuration de ce capteur statistique. Ici par exemple nous configurons l’age maximum à 48h, c’est à dire que ce capteur statistique va stocker la température minimale de la serre sur les 48 dernières heure :
Affichage des prévisions journalières
Je souhaite afficher les prévisions météorologiques journalières, incluant le jour, la condition, la température minimale/maximale et les précipitations pour aujourd’hui et les quatre prochains jours.
Prévisions météorologiques (condition, température maximale/minimale et précipitation) quotidiennes
Pour afficher les prévisions quotidiennes, il est nécessaire de modifier le fichier de configuration de Home Assistant (configuration.yaml) afin de créer une automatisation qui exécute l’action weather.get_forecasts, ainsi qu’un modèle de capteur personnalisé (template sensor) pour récupérer les données de prévision météo.
Voici un exemple de configuration du capteur meteo_jour_nantes et de son automatisation, qui récupère les prévisions météo (jour, températures minimales et maximales, et conditions) pour aujourd’hui et les trois prochains jours :
Pour vérifier la bonne configuration de ces capteurs, redémarrer Home Assistant et vérifier leur état en utilisant l’outil de développement.
Notez que l’attribut « jour » est affiché sous forme de nom du jour de la semaine grâce à l’argument %A de la fonction timestamp_custom(). Consultez la documentation du composant Time pour plus de détails.
Une fois que les capteurs sont configurés dan Home Assistant, il faut les déclarer dans la configuration ESPHome.
Comme le jour et les conditions météorologiques sont fournies sous forme de texte (par exemple, « Lundi », « ensoleillé »), elles doivent être définies comme text_sensor.
Nous souhaitons afficher les conditions météorologiques horaires, la température et les précipitations pour les douze prochaines heures.
Prévisions météorologiques (condition, température et précipitation) horaires
Comme pour les prévisions quotidiennes, l’affichage des prévisions horaires nécessite de modifier le fichier de configuration de Home Assistant (configuration.yaml) afin de créer un capteur personnalisé meteo_heure_nantes et son automatisation :
Voici un exemple de configuration d’un capteur personnalisé et de son automatisation, qui récupère les prévision pour les heures H et H+1.
Pour vérifier la bonne configuration de ces capteurs, redémarrer Home Assistant et vérifier leur état en utilisant l’outil de développement.
Notez que l’heure est au format heure locale grâce au second argument de timestamp_custom() positionné à true.
Une fois que les capteurs sont configurés dans Home Assistant, il faut les déclarer dans la configuration ESPHome.
Comme l’heure et les conditions météorologiques sont fournies sous forme de texte (par exemple, « 11:00 », « ensoleillé »), elles doivent être définies comme text_sensor.
Vous pouvez ensuite afficher les valeurs de ces capteurs sur l’écran du reTerminal E1001 via la configuration ESPHome suivant :
YAML
// Condition meteo H0 (texte)it.printf(100, 50, id(myFont), "%s", id(condition0).state.c_str());// Precipitations H0 (float, avec une décimale)it.printf(100, 100, id(myFont), "%.1f", id(precipitation0).state);// Temperature H0 (float, pas de décimale)it.printf(100, 150, id(myFont), "%.0f", id(temperature0).state);
Affichage de la charge de la batterie
Pour récupérer le niveau de batterie du reTerminal, il faut activer la mesure de la tension de la batterie en configurant le GPIO21 (VBAT ENABLE) lors du boot du reTerminal. La lecture s’effectue ensuite sur le GPIO1 (VBAT ADC), qui est relié en interne à la batterie via un circuit de mesure.
En lisant la valeur analogique sur ce GPIO1, nous récupérons la tension de la batterie interne. La documentation technique montre que la tension est mesurée à l’aide d’un pont diviseur de tension, il faut donc appliquer un filtre pour compenser la division de la tension par deux liée à ce pont diviseur constitué de 2 résistances de 10kΩ.
Schéma montrant la mesure de la tension de la batterie interne sur GPIO1 via un pont diviseur de tension
Il faut configurer l’atténuation à 12 dB pour les entrées ADC lorsque la tension mesurée dépasse 1,1 V sur GPIO1. Ce réglage permet à l’ADC de l’ESP32-C6 de mesurer avec précision des niveaux de tension plus élevés, en étendant la plage d’entrée au-delà du défaut (atténuation à 0 dB) de 0 à 1,1 V. Une atténuation de -12 dB permet de mesurer des tensions de 0 à 2.5 V. Voir la documentation de l’ESP32 ou d’ESPHome pour plus de détails.
La tension aux bornes de la batterie interne permet d’en déduire son niveau de charge. La batterie lithium est considérée comme déchargée si sa tension est inférieure à 3.3 V. Une batterie bien chargée lorsque sa tension est supérieure à 4.0 V.
Affichage de la tension de la batterie interne
Afficher la puissance du signal Wi-Fi
L’affichage de la puissance de réception du signal Wi-Fi par le reTerminal peut être utile.
Cette information est fournie par ESPHome, et a pour unité le dBm, voir mon article “C’est quoi… un dBm ?” pour en savoir plus. Pour l’obtenir, il suffit de déclarer le capteur wifi_signal dans la configuration d’ESPHome :
YAML
sensor: - platform: wifi_signalname: "Signal WiFi ReTerminal"id: wifi_signal_strengthupdate_interval: never
Comme le Wi-Fi n’est pas disponible au démarrage, j’ai décidé de mettre à jour sa mesure lorsque le reTerminal reçoit les prévisions météorologiques quotidiennes. À ce moment-là, la connexion Wi-Fi est déjà établie. C’est également à ce moment-là que je commence à actualiser l’affichage, voir plus loin dans le chapitre “Rafraîchissement de l’affichage“.
YAML
- platform: homeassistantname: "Precipitation11"entity_id: sensor.meteo_heure_antonyattribute: precipitation11id: precipitation11accuracy_decimals: 1on_value:then:# Refresh the wifi signal measure and e-paper display whenever this sensor updates (wifi connected) - delay: 5s - component.update: wifi_signal_strength - component.update: epaper_display
Affichage des icônes
J’ai choisi d’afficher les icônes en n’utilisant pas d’image mais plutôt la police materialdesignicons-webfont.ttf. Il faut télécharger cette police et la placer dans le répertoire fonts de ESPHome avec le module complémentaire File Editor par exemple. Pour choisir vos propres icônes, vous pouvez utiliser le site pictogrammers.com et noter la référence de l’icône souhaitée.
Il faut ensuite déclarer les glyphcorrespondant aux icônes dans la font. Ici par exemple sur une font de taille 20 nous souhaitons utiliser les icônes représentant une batterie, une goutte d’eau, un thermomètre et une horloge.
Le reTerminal E1001 est configuré pour être en veille profonde pendant 30 minutes. Au bout de ces 3à minutes, il se réveille pendant 30 secondes afin de récupérer les informations météo et mettre à jour son écran. Il retourne ensuite en veille profonde pour 30 minutes.
Cette veille profonde est configurée de la sorte dans le fichier de configuration YAML de ESPHome :
YAML
# Deep sleep configuration to save powerdeep_sleep:id: deep_sleep_1# ID for deep sleep componentrun_duration: 30s# Time to stay awake after wake-upsleep_duration: 30min# Time to sleep between wake-upswakeup_pin: GPIO3# GPIO pin to wake up devicewakeup_pin_mode: INVERT_WAKEUP# Inverted wake-up logic
Rafraîchissement de l’affichage
Lors de la sortie de veille profonde du reTerminal, il adopte le même comportement qu’au démarrage. Il faut donc assurer une chronologie précise des événement. L’écran du reTerminal doit être rafraichi uniquement une fois que les données du capteur interne et de Home Assistant sont récupérées (le WiFi met du temps à s’activer et donc les données de Home Assistant ne sont pas disponible immédiatement au boot).
Mettre à jour au moment du boot la mesure de la tension de la batterie et la mesure du capteur interne de température et d’humidité :
YAML
on_boot:priority: 600then:# Turn on the GPIO output that powers the battery measurement circuit - output.turn_on: bsp_battery_enable# Wait a short delay to allow power stabilization - delay: 500ms# Manually update battery sensors (voltage and percentage) - component.update: battery_voltage# Manually read the internal temperature/humidity sensor - component.update: sht4x_component
Désactiver la mise à jour automatic (update_interval: never) pour le capteur interne ST4X :
YAML
# Internal SHT4x temperature/humidity sensor - platform: sht4x# Sensor platformid: sht4x_component# ID for the componenttemperature: # Temperature sensorname: "Temperature"# Sensor nameid: sht4x_temperature# ID for temperature sensorhumidity: # Humidity sensorname: "Humidity"# Sensor nameid: sht4x_humidity# ID for humidity sensoraddress: 0x44# I2C addressupdate_interval: never# Disable automatic updates (manual read only)
Désactiver le rafraichissement automatique (update_interval: never) de l’écran :
Et enfin rafraîchir l’écran une fois qu’une donnée Home Assistant est reçue :
YAML
- platform: homeassistantname: "TemperatureJ0"entity_id: sensor.meteo_jour_nantesattribute: temperature0id: temperatureJ0accuracy_decimals: 1on_value:then:# Refresh the e-paper display whenever this sensor updates - component.update: epaper_display
Conclusion
Grâce au reTerminal E1001 et à son intégration dans Home Assistant, j’ai enfin la station météo parfaitement adaptée à mes attentes. Le framework ESPHome demande une courte phase d’apprentissage pour en maîtriser les principes, mais une fois ces notions acquises, il se révèle remarquablement puissant et flexible.
L’ensemble de la configuration Home Assistant et ESPHome utilisée dans ce tutoriel est disponible sur mon GitHub.
J’espère que ce tutoriel vous aura intéressé. N’hésitez pas à donner votre avis en cliquant sur les étoiles ci-dessous ou en laissant un commentaire.
Programmer le reTerminal E1002 sous IDE Arduino
Seeed Studio vient de présenter le reTerminal E1002, un appareil prêt à l’emploi doté d’un boîtier métallique robuste et d’un écran e-paper couleur de 7,3″ affichant en 800×480 pixels. Basé sur un microcontrôleur ESP32-S3, il intègre plusieurs fonctionnalités matérielles : trois boutons, un buzzer, une LED de statut (en plus de la LED d’alimentation), un capteur de température et de pression, un microphone ainsi qu’un lecteur de carte MicroSD. L’ensemble est alimenté par une batterie interne de 2000 mAh offrant jusqu’à trois mois d’autonomie.
Approche 3 : Par programmation (IDE Arduino, C/C++)
C’est cette dernière approche que je vais utiliser dans cet article, car elle a l’avantage de ne pas vous imposer les limites des deux autres approches.
Configuration de l’IDE Arduino pour le reTerminal E1002
Il faut tout d’abord intégrer le support pour les microcontrôleurs ESP32 dans l’IDE Arduino, en ajoutant l’URL suivante dans le champ Additional Boards Manager URLs du menu File > Preferences :
Il faut ensuite installer le package ESP32, via le menu Tools > Board > Boards Manager, en recherchant “esp32” et installant le package esp32 by Espressif Systems (disponible sur github espressif).
Téléchargez ensuite la librairie GxEPD2 dans son format ZIP :
Et l’ajouter à l’IDE Arduino via le menu Sketch > Include Library > Add .ZIP Library…
Et pour finir, installer cette librairie GxEPD2 by Jean-Marc Zingg via le menu Tools > Manage libraries.
Premier programme “Hello World”
Voici le code qui affiche le célèbre “Hello World” sur l’écran du reTerminal E1002, qu’il faut installer sur l’appareil après avoir sélectionné la carte XIAO_ESP32S3 via le menu Tools > Board > ESP32 Arduino et choisi le port sur lequel le reTerminal est connecté.
C++
/** * @file hello_world.ino * @brief Display "Hello World!" on Seeed reTerminal E1002 e-paper. * * https://tutoduino.fr/ */#include<GxEPD2_7C.h>// Include library for 7-color ePaper display management#include<Fonts/FreeMonoBold12pt7b.h>// Include a bold monospaced font at 12pt (for text display)// Define SPI connection pins between the microcontroller and the ePaper display#define EPD_SCK_PIN 7 // SPI clock pin#define EPD_MOSI_PIN 9 // SPI data pin (MOSI)#define EPD_CS_PIN 10 // SPI Chip Select for the ePaper display#define EPD_DC_PIN 11 // Data/Command control pin#define EPD_RES_PIN 12 // Display Reset pin#define EPD_BUSY_PIN 13 // Display Busy status pin// Define the ePaper display driver and class (for a 7.3" color model)#define GxEPD2_DISPLAY_CLASS GxEPD2_7C#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEP073E01 // Specific to the 7.3" Color display// Set up a maximum display buffer size for RAM management#define MAX_DISPLAY_BUFFER_SIZE 16000// Macro to compute the maximum height, given display size and RAM constraints#define MAX_HEIGHT(EPD) \ (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) \ ? EPD::HEIGHT \ : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))// Instantiate the ePaper display object with the chosen driver and pinoutsGxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)>display(GxEPD2_DRIVER_CLASS(/*CS=*/EPD_CS_PIN, /*DC=*/EPD_DC_PIN, /*RST=*/EPD_RES_PIN, /*BUSY=*/EPD_BUSY_PIN));// Create a dedicated SPI bus object for display communicationSPIClasshspi(HSPI);/** * @brief Draws "Hello World!" in red at coordinates (100, 100) on the ePaper display. */voidhelloWorld(){display.setRotation(0); // No rotation, natural orientationdisplay.setFont(&FreeMonoBold12pt7b); // Use the bold 12pt monospace fontdisplay.setTextColor(GxEPD_RED); // Set text color to red (one of supported display colors)display.setFullWindow(); // Use the full display area for drawingdisplay.firstPage(); // Start first drawing page (paged drawing saves RAM)do {display.fillScreen(GxEPD_WHITE); // Fill the screen with white (background)display.setCursor(100, 100); // Set the cursor to (100, 100) for text positiondisplay.print("Hello World!"); // Print "Hello World!" on the screen }while (display.nextPage()); // Continue until all pages are rendered (for paged displays)}/** * @brief Arduino setup function. Initializes the display and shows "Hello World!". */voidsetup(){pinMode(EPD_RES_PIN, OUTPUT); // Configure reset pin as outputpinMode(EPD_DC_PIN, OUTPUT); // Configure data/command pin as outputpinMode(EPD_CS_PIN, OUTPUT); // Configure chip-select pin as output // Initialize SPI bus for display with specified pins, speed, and settingshspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0)); // Initialize the ePaper display hardware and driverdisplay.init(0); // Display "Hello World!" messagehelloWorld(); // Put the display into low-power (hibernate) mode after renderingdisplay.hibernate();}// No loop code needed. The display only updates once at startup in this demo.voidloop() {};
Voici le résultat après téléversement sur le reTerminal :
Le célèbre « Hello World !» s’affiche sur le reTerminal E1002 programmé sur l’IDE Arduino
Récupération d’une donnée depuis le serveur Home Assistant
L’objectif de ce tutoriel est d’afficher sur le reTerminal E1002 la température du capteur qui est disponible dans le serveur Home Assistant. Cette donnée sera récupérée via l’API RESTful du serveur Home Assistant disponible via l’URL http://ADRESSE_IP_HA:8123/api/. Cette API accepte et renvoie uniquement les objets codés en JSON. Nous utiliserons pour cela la librairie Arduinojson by Benoit Blanchon, que nous installons depuis le menu Tools > Manage Libraries de l’IDE Arduino.
Il faut récupérer, depuis la page web de Home Assistant, l’ID de l’entité que nous souhaitons afficher sur le reTerminal. Ici par exemple, la température du capteur correspond à l’entité dont l’ID est: sensor.tutoduino_esp32c6tempsensor_temperature.
ID de l’entité de Home Assistant à récupérer
Pour pouvoir récupérer des données de Home Assistant, notre programme va avoir besoin d’un jeton (Token) de Home Assistant afin de pouvoir l’interroger via son API. Il faut le faire à partir de la page web de votre serveur Home Assistant, en allant sur la page de votre profile et en cliquant sur le bouton Créer un jeton depuis le menu Sécurité.
Création d’un Jeton dans Home Assistant
Une fois le jeton créé, il suffit de l’indiquer dans votre programme sous l’IDE Arduino. Je recommande toujours de stocker vos secrets (mot de passe wifi, jeton home assistant…) dans un fichier secret.h qui sera inclus dans votre programme C. Cela évite par exemple de les faire fuiter sur Github après un copier-coller malencontreux…
Stockage du jeton Home Assistant dans un fichier secret.h dans l’IDE Arduino
Voici un exemple de programme qui va récupérer via l’API Home Assistant la température de notre capteur et l’afficher sur l’écran du reTerminal E1002. Notez que la lecture sur le serveur Home Assistant se fait toutes les 10 minutes et qu’entre deux lectures, le reTerminal se met en veille profonde afin d’économiser sa batterie. Il est cependant possible de réveiller à tout moment le reTerminal en cliquant sur son bouton vert.
C++
/** * @file TemperatureDisplay.ino * @brief Fetches temperature data from a Home Assistant server and displays it on a reTerminal E1002 e-paper screen. * The device can enter deep sleep and wake up via a button press or timer. */#include<WiFi.h>// Wi-Fi library for ESP32#include<HTTPClient.h>// HTTP client for making requests#include<ArduinoJson.h>// JSON parsing library#include<GxEPD2_7C.h>// E-paper display library for 7-color displays#include<Fonts/FreeMonoBold12pt7b.h>// Custom font for the display#include"secrets.h"// Custom file containing sensitive data (Wi-Fi credentials, API tokens)// --- Pin Definitions ---// Serial1 communication pins for debugging#define SERIAL_RX 44#define SERIAL_TX 43// SPI pins for e-paper display communication#define EPD_SCK_PIN 7 // SPI Clock pin#define EPD_MOSI_PIN 9 // MOSI (Master Out Slave In) pin#define EPD_CS_PIN 10 // Chip Select pin#define EPD_DC_PIN 11 // Data/Command pin#define EPD_RES_PIN 12 // Reset pin#define EPD_BUSY_PIN 13 // Busy pin// Button pin definitions according to the hardware schematic#define GREEN_BUTTON 3 // KEY0 - GPIO3, connected to the green button// --- E-paper Display Configuration ---// Display class for 7.3" color e-paper#define GxEPD2_DISPLAY_CLASS GxEPD2_7C#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEP073E01// Maximum display buffer size in bytes (limits memory usage)#define MAX_DISPLAY_BUFFER_SIZE 16000// Calculate the maximum display height based on the buffer size#define MAX_HEIGHT(EPD) \ (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE * 8) / EPD::WIDTH ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE * 8) / EPD::WIDTH)// --- Device Initialization ---// Initialize SPI interface for the display (HSPI = Host SPI)SPIClasshspi(HSPI);// Initialize the display object with the specified driver and buffer sizeGxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)>display(GxEPD2_DRIVER_CLASS(EPD_CS_PIN, EPD_DC_PIN, EPD_RES_PIN, EPD_BUSY_PIN));/** * @brief Displays the temperature on the e-paper screen. * @paramtemperature A string representing the temperature to display. */voiddisplayTemperature(constchar*temperature) {Serial1.print("Temperature to display: ");Serial1.println(temperature); // Configure display settingsdisplay.setRotation(0); // Set display orientation to default (0 degrees)display.setFont(&FreeMonoBold12pt7b); // Set font to FreeMonoBold12pt7bdisplay.setTextColor(GxEPD_RED); // Set text color to reddisplay.setFullWindow(); // Use the full display window for rendering // Start the display update processdisplay.firstPage();do {display.fillScreen(GxEPD_WHITE); // Clear the screen with white backgrounddisplay.setCursor(100, 100); // Set cursor position to (100, 100)display.print("Temperature = ");display.print(temperature); // Print the temperature valuedisplay.print(" C"); // Print the degree symbol and 'C' for Celsius } while (display.nextPage()); // Repeat until the display update is complete}/** * @brief Fetches the temperature from the Home Assistant server. * @return A dynamically allocated string containing the temperature or "Error" on failure. * @note The caller is responsible for freeing the returned string using free(). */char*getTemperature() { // Check if Wi-Fi is connectedif (WiFi.status() != WL_CONNECTED) {Serial1.println("ERROR: Wi-Fi not connected.");returnstrdup("Error"); // Return an error message } // Initialize HTTP client and configure the request HTTPClient http;http.begin(ha_url); // Set the target URL from secrets.hhttp.addHeader("Authorization", "Bearer " + String(ha_token)); // Add authorization headerhttp.addHeader("Content-Type", "application/json"); // Set content type to JSON // Send a GET request to the Home Assistant APIint httpCode = http.GET();if (httpCode != HTTP_CODE_OK) {Serial1.print("HTTP Error: ");Serial1.println(httpCode);Serial1.println(http.getString()); // Print the error responsehttp.end(); // Close the connectionreturnstrdup("Error"); // Return an error message } // Read the server response String payload = http.getString();Serial1.println("Server response:");Serial1.println(payload); // Parse the JSON response DynamicJsonDocument doc(1024); // Create a JSON document with a capacity of 1024 bytes DeserializationError error = deserializeJson(doc, payload);http.end(); // Close the HTTP connectionif (error) {Serial1.print("JSON parsing error: ");Serial1.println(error.c_str());returnstrdup("Error"); // Return an error message if JSON parsing fails } // Check if the "state" field exists in the JSON responseif (!doc.containsKey("state")) {Serial1.println("ERROR: 'state' field missing in JSON response.");returnstrdup("Error"); // Return an error message if "state" is missing }constchar* state = doc["state"]; // Extract the temperature value from the JSONSerial1.print("Temperature = ");Serial1.println(state); // Return a dynamically allocated copy of the temperature stringreturnstrdup(state);}/** * @brief Prints the reason for waking up from deep sleep. */voidprintWakeupReason() {esp_sleep_wakeup_cause_t wakeupReason = esp_sleep_get_wakeup_cause();switch (wakeupReason) {case ESP_SLEEP_WAKEUP_EXT0:Serial1.println("Wakeup caused by external signal (EXT0 - button press)");break;case ESP_SLEEP_WAKEUP_EXT1:Serial1.println("Wakeup caused by external signal (EXT1)");break;case ESP_SLEEP_WAKEUP_TIMER:Serial1.println("Wakeup caused by timer");break;case ESP_SLEEP_WAKEUP_TOUCHPAD:Serial1.println("Wakeup caused by touchpad");break;default:Serial1.printf("Wakeup not caused by deep sleep: %d\n", wakeupReason);break; }}/** * @brief Initial system setup. Runs once at startup. */voidsetup() { // Initialize Serial1 for debuggingSerial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);delay(1000); // Wait for serial port to initializeSerial1.println("System starting...");printWakeupReason(); // Print the reason for waking up // Configure the green button as an input with pull-up resistorpinMode(GREEN_BUTTON, INPUT_PULLUP); // Enable wakeup from deep sleep when the button is pressed (LOW signal)esp_sleep_enable_ext0_wakeup((gpio_num_t)GREEN_BUTTON, LOW); // Configure display control pins as outputspinMode(EPD_RES_PIN, OUTPUT);pinMode(EPD_DC_PIN, OUTPUT);pinMode(EPD_CS_PIN, OUTPUT); // Initialize SPI interface for the displayhspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0)); // Connect to Wi-FiWiFi.begin(wifi_ssid, wifi_password);Serial1.print("Connecting to Wi-Fi");while (WiFi.status() != WL_CONNECTED) {delay(500);Serial1.print("."); }Serial1.println("\nConnected to Wi-Fi"); // Initialize the e-paper displaydisplay.init(0);}/** * @brief Main loop. Runs repeatedly after setup(). */voidloop() { // Fetch the temperature from Home Assistantchar* temperature = getTemperature();if (temperature != nullptr) {displayTemperature(temperature); // Display the temperaturefree(temperature); // Free the dynamically allocated memory } else {Serial1.println("ERROR: Failed to get temperature.");displayTemperature("Error"); // Display an error message } // Put the display into hibernation to save powerdisplay.hibernate();delay(1000); // Wait for 1 seconds before entering deep sleep // Configure deep sleep wakeup after 10 minutesesp_sleep_enable_timer_wakeup(10 * 60 * 1000000); // 10 minutes in microsecondsesp_deep_sleep_start(); // Enter deep sleep mode}
Voici le rendu sur le reTerminal E1002
Récupération de données sur Internet à l’aide d’une API RESTful
Grâce aux bibliothèques Wi-Fi, HTTP et JSON installées, il est très simple de récupérer des données sur Internet via l’API RESTful.
Voici un exemple pour obtenir la valeur d’un Bitcoin en USD :
C++
// Bitcoin URL should be stored in secret.h header fileconstchar* btc_url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd";/** * @brief Get Bitcoin price (USD) from CoinGecko API. * @return Bitcoin price as integer, 0 if not available. */intgetBTC() {int btc = 0;if (WiFi.status() == WL_CONNECTED) { HTTPClient http;http.begin(btc_url);int httpCode = http.GET();if (httpCode == HTTP_CODE_OK) { DynamicJsonDocument doc(1024);if (!deserializeJson(doc, http.getString())) btc = doc["bitcoin"]["usd"].as<int>(); }http.end(); }return btc;}
Vous pouvez tester l’API au préalable. Copiez/collez simplement l’URL dans votre navigateur et le résultat s’affichera directement au format JSON :
Check the API directly from web browser (JSON format)
Affichage des icônes météo sur le reTerminal
J’ai décidé de télécharger des icônes météo depuis Internet et de les inclure directement dans l’IDE Arduino sous forme de tableau de caractères non signés stocké en mémoire programme (PROGMEM) afin d’optimiser l’utilisation de la RAM. Ces données restent dans la mémoire flash du microcontrôleur plutôt que dans la RAM volatile. J’ai utilisé l’excellent outil en ligne image2cpp pour convertir les images en tableaux d’octets.
Pour déterminer quelle icône météo doit être affichée, il faut utiliser le code météo renvoyé par l’API (ici weather_code = 2).
Code météo renvoyé par l’API open-meteo.com
Vous devez ensuite afficher l’icône correspondante en fonction des codes d’interprétation météorologique de l’OMM fournis dans la documentation open-Meteo.
Interprétation des codes météo d’open-meteo.com
Je crée la fonction suivante pour cette interprétation :
Il est alors facile de dessiner l’icône 50×50 sur l’écran du reTerminal à la position x,y :
C++
display.drawBitmap(x, y, epd_bitmap_allArray[iconNb], 50, 50, GxEPD_BLACK);
Affichage du caractère degré (°) sur le reTerminal avec les polices GFX
Par défaut, le caractère « degré » n’existe pas sur les polices GFX. J’ai trouvé une discussion à ce sujet, mais seule la police 12 points était disponible. J’ai donc décidé d’approfondir mes recherches et trouvé cet excellent outil de personnalisation de polices en ligne : https://tchapi.github.io/Adafruit-GFX-Font-Customiser. Grâce à cet outil, j’ai remplacé le caractère «`» (0x60) par « ° » dans les polices FreeSans 18 et 17 points.
Personnalisateur de police en ligne pour remplacer le caractère ‘`’ (0x60) par ‘°’
Il est simple d’afficher 25.4° sur l’écran du reTerminal en ajoutant ‘`’ ou 0x60 pour afficher le caractère ‘°’, voici un exemple avec la police FreeSans12pt7b modifiée :
Récupération du capteur de température interne du reTerminal
Le reTerminal Série E intègre un capteur de température et d’humidité SHT4x connecté via I2C. Vous devez installer la bibliothèque Sensirion I2C SHT4xvia le gestionnaire de bibliothèques Arduino (Outils > Gérer les bibliothèques).
Les valeurs de température et d’humidité peuvent ensuite être obtenues à l’aide du code suivant :
C++
#include<Wire.h>#include<SensirionI2cSht4x.h>// I2C pins for reTerminal E Series#define I2C_SDA 19#define I2C_SCL 20// Object to manage onboard SHT4x sensor (I2C temperature/humidity)SensirionI2cSht4x sht4x;voidsetup() { // Initialize I2C with custom pinsWire.begin(I2C_SDA, I2C_SCL);uint16_t error; // Initialize the sensorsht4x.begin(Wire, 0x44);}voidloop() {uint16_t error;float temperature;float humidity; // Measure temperature and humidity with medium precision error = sht4x.measureMediumPrecision(temperature, humidity);}
Récupération du niveau de batterie du reTerminal
La série reTerminal E permet de surveiller la tension de la batterie via son convertisseur analogique-numérique (ADC) connectée à un circuit diviseur de tension. Le niveau de charge interne de la batterie peut être mesuré à l’aide du code suivant :
C++
// Battery monitoring pins#define BATTERY_ADC_PIN 1 // GPIO1 - Battery voltage ADC#define BATTERY_ENABLE_PIN 21 // GPIO21 - Battery monitoring enable/** * @brief Calculates the battery charge percentage based on the measured voltage. * @parambatteryVoltage Measured battery voltage in volts. * @return Estimated battery charge percentage (0 to 100%). */intgetBatteryPercent(floatbatteryVoltage) {if (batteryVoltage > 4.20)return100;elseif (batteryVoltage > 3.96)return90;elseif (batteryVoltage > 3.91)return80;elseif (batteryVoltage > 3.85)return70;elseif (batteryVoltage > 3.80)return60;elseif (batteryVoltage > 3.75)return50;elseif (batteryVoltage > 3.60)return40;elseif (batteryVoltage > 3.40)return20;elseif (batteryVoltage > 3.20)return10;elseif (batteryVoltage > 3.00)return5;elsereturn0;}/** * @brief Read actual battery voltage from ADC. * @return Battery voltage in Volts. */floatgetBatteryVoltage() {digitalWrite(BATTERY_ENABLE_PIN, HIGH);delay(5);int mv = analogReadMilliVolts(BATTERY_ADC_PIN);digitalWrite(BATTERY_ENABLE_PIN, LOW);return ((mv / 1000.0) * 2); // Correction for voltage divider}voidsetup() { // Configure battery monitoringpinMode(BATTERY_ENABLE_PIN, OUTPUT);digitalWrite(BATTERY_ENABLE_PIN, HIGH); // Enable battery monitoring // Configure ADC for batteryanalogReadResolution(12); // 12-bit ADCanalogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);}voidloop() {uint8_t vBatPercentage;float vBat; // Measure battery voltage vBat = getBatteryVoltage(); vBatPercentage = getBatteryPercent(vBat);}
Programme complet
Grâce à toutes les fonctionnalités décrites dans cet article, j’ai construit une station météo, programmée avec l’IDE Arduino et affichée sur l’écran e-paper couleur de 7,3″ du reTerminal E1002.
Station météo sur reTerminal E1002 programmée sous Arduino IDE pour afficher des données provenant de plusieurs sources
Ce projet transforme le reTerminal E1002 en une station de données multi-sources, combinant les relevés de capteurs locaux avec des API en ligne :
Récupère les prévisions météorologiques depuis l’API Open-Meteo (url dans le fichier secret.h)
Lit les capteurs de température de Home Assistant via son API REST.
Récupère le cours du Bitcoin (USD) depuis CoinGecko.
Surveille le niveau de batterie, la température interne et l’humidité du reTerminal grâce à des capteurs intégrés.
Affiche toutes les informations avec des icônes et du texte clairs sur l’écran couleur haute résolution.
Met en veille prolongée pour économiser l’énergie, avec réveil déclenché par une simple pression sur un bouton ou une minuterie.
C++
/** * @file reTerminalE1002_HA.ino * @brief Weather, Home Assistant, and Bitcoin dashboard on Seeed reTerminal E1002 e-paper. * * - Fetches weather data from Open-Meteo API. * - Fetches Home Assistant sensor temperatures via REST API. * - Fetches Bitcoin and Ethereum price in USD from CoinGecko. * - Displays all data on reTerminal E1002 7.3" color e-paper. * - Handles deep sleep and wake-up via button or timer. * https://tutoduino.fr/ */#include<WiFi.h>#include<HTTPClient.h>#include<ArduinoJson.h>#include<GxEPD2_7C.h>#include<GxEPD2_BW.h>#include<FreeSans12pt7b_mod.h>#include<FreeSans18pt7b_mod.h>#include<FreeSans24pt7b_mod.h>#include<Fonts/FreeSans9pt7b.h>#include<Wire.h>#include<SensirionI2cSht4x.h>#include"icons_50x50.h"#include"icons_100x100.h"#include"icons.h"#include"secrets.h"#include"language.h"#define uS_TO_S_FACTOR 1000000ULL /* Conversion factor for micro seconds to seconds */// Battery monitoring pins#define BATTERY_ADC_PIN 1 // GPIO1 - Battery voltage ADC#define BATTERY_ENABLE_PIN 21 // GPIO21 - Battery monitoring enable// Serial communication pins for debugging#define SERIAL_RX 44#define SERIAL_TX 43// SPI pinout for ePaper display (verify for your hardware)#define EPD_SCK_PIN 7#define EPD_MOSI_PIN 9#define EPD_CS_PIN 10#define EPD_DC_PIN 11#define EPD_RES_PIN 12#define EPD_BUSY_PIN 13#define GREEN_BUTTON 3 // Deep sleep wake-up button// I2C pins for reTerminal E Series#define I2C_SDA 19#define I2C_SCL 20// Select the ePaper driver to use// 0: reTerminal E1001 (7.5'' B&W)// 1: reTerminal E1002 (7.3'' Color)#define EPD_SELECT 0#if (EPD_SELECT ==0)#define GxEPD2_DISPLAY_CLASS GxEPD2_BW#define GxEPD2_DRIVER_CLASS GxEPD2_750_GDEY075T7#define BOX_TEXT_COLOR GxEPD_WHITE#define BOX_FILL_COLOR GxEPD_BLACK#elif (EPD_SELECT == 1)#define GxEPD2_DISPLAY_CLASS GxEPD2_7C#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEP073E01#define BOX_TEXT_COLOR GxEPD_WHITE#define BOX_FILL_COLOR GxEPD_GREEN#endif#define MAX_DISPLAY_BUFFER_SIZE 16000#define MAX_HEIGHT(EPD) \ (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) \ ? EPD::HEIGHT \ : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)>display(GxEPD2_DRIVER_CLASS(/*CS=*/EPD_CS_PIN, /*DC=*/EPD_DC_PIN, /*RST=*/EPD_RES_PIN, /*BUSY=*/EPD_BUSY_PIN));// Global variable for SPI communicationSPIClasshspi(HSPI);// Global weather variablesint D0MinTemp, D0MaxTemp, D1MinTemp, D1MaxTemp, D2MinTemp, D2MaxTemp, D3MinTemp, D3MaxTemp, D4MinTemp, D4MaxTemp;int D0Code, D1Code, D2Code, D3Code, D4Code;String localTime = "2025-01-01 00:00";int currentTemp;int g_x_start, g_y_start;// Object to manage onboard SHT4x sensor (I2C temperature/humidity)SensirionI2cSht4x sht4x;constfloat sht4xCalibration = -1; // SHT4x calibration offset/** * @brief Fetch temperature from Home Assistant REST API. * @paramentityId - Home Assistant sensor entity (e.g., "sensor.lumi_lumi_weather_temperature") * @return Temperature value or 0.0 if error. */floatgetHomeAssistantSensorState(StringentityId) {if (WiFi.status() != WL_CONNECTED) {Serial1.println("ERROR: Wi-Fi not connected.");return0.0; } HTTPClient http; String url = String(ha_url) + entityId;http.begin(url);http.addHeader("Authorization", "Bearer " + String(ha_token));http.addHeader("Content-Type", "application/json");int httpCode = http.GET();if (httpCode != HTTP_CODE_OK) {Serial1.print("HTTP Error: ");Serial1.println(httpCode);Serial1.println(http.getString());http.end();return0.0; } DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, http.getString());http.end();if (error) {Serial1.print("JSON parsing error: ");Serial1.println(error.c_str());return0.0; }if (!doc.containsKey("state")) {Serial1.println("ERROR: 'state' field missing in JSON response.");return0.0; }float state = doc["state"].as<float>();return state;}/** * @brief Fetch weather data from Open-Meteo API (current and next 4 days). * @return 0 on success, <0 on error. */intfetchWeatherData() {if (WiFi.status() != WL_CONNECTED) {Serial1.println("WiFi not connected");return -1; } HTTPClient http;http.begin(weather_url);int httpCode = http.GET();if (httpCode != HTTP_CODE_OK) {Serial1.print("HTTP Error: ");Serial1.println(httpCode);Serial1.println(http.getString());http.end();return -2; } DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, http.getString());http.end();if (error) {Serial1.print("JSON parsing error: ");Serial1.println(error.c_str());return -3; } localTime = doc["current"]["time"].as<String>(); D0Code = doc["current"]["weather_code"].as<int>(); currentTemp = doc["current"]["temperature"].as<int>(); D0MinTemp = doc["daily"]["temperature_2m_min"][0].as<int>(); D0MaxTemp = doc["daily"]["temperature_2m_max"][0].as<int>(); D1MinTemp = doc["daily"]["temperature_2m_min"][1].as<int>(); D1MaxTemp = doc["daily"]["temperature_2m_max"][1].as<int>(); D1Code = doc["daily"]["weather_code"][1].as<int>(); D2MinTemp = doc["daily"]["temperature_2m_min"][2].as<int>(); D2MaxTemp = doc["daily"]["temperature_2m_max"][2].as<int>(); D2Code = doc["daily"]["weather_code"][2].as<int>(); D3MinTemp = doc["daily"]["temperature_2m_min"][3].as<int>(); D3MaxTemp = doc["daily"]["temperature_2m_max"][3].as<int>(); D3Code = doc["daily"]["weather_code"][3].as<int>(); D4MinTemp = doc["daily"]["temperature_2m_min"][4].as<int>(); D4MaxTemp = doc["daily"]["temperature_2m_max"][4].as<int>(); D4Code = doc["daily"]["weather_code"][4].as<int>();return0;}/** * @brief Get Bitcoin price (USD) from CoinGecko API. * @return Bitcoin price as integer, 0 if not available. */intgetBTC() {int btc = 0;if (WiFi.status() == WL_CONNECTED) { HTTPClient http;http.begin(btc_url);int httpCode = http.GET();if (httpCode == HTTP_CODE_OK) { DynamicJsonDocument doc(1024);if (!deserializeJson(doc, http.getString())) btc = doc["bitcoin"]["usd"].as<int>(); }http.end(); }return btc;}/** * @brief Get Ethereum price (USD) from CoinGecko API. * @return Ethereum price as integer, 0 if not available. */intgetETH() {int eth = 0;if (WiFi.status() == WL_CONNECTED) { HTTPClient http;http.begin(eth_url);int httpCode = http.GET();if (httpCode == HTTP_CODE_OK) { DynamicJsonDocument doc(1024);if (!deserializeJson(doc, http.getString())) eth = doc["ethereum"]["usd"].as<int>(); }http.end(); }return eth;}/** * @brief Zeller's congruence to compute day of week (0=Sunday). * @paramy Year * @paramm Month * @paramd Day * @return Weekday index */intdayOfTheWeek(inty, intm, intd) {if (m < 3) { m += 12; y -= 1; }int K = y % 100;int J = y / 100;int h = (d + 13 * (m + 1) / 5 + K + K / 4 + J / 4 + 5 * J) % 7;return ((h + 6) % 7); // 0=Sunday}/** * @brief Formats ISO date string as US date for display. * @paramdateStr "YYYY-MM-DD HH:MM" * @return "Saturday, October 4, 2025" style string. */StringformatDateEN(StringdateStr) {int y = dateStr.substring(0, 4).toInt();int m = dateStr.substring(5, 7).toInt();int d = dateStr.substring(8, 10).toInt();int wday = dayOfTheWeek(y, m, d);returnString(String(days[wday]) + ", " + String(months[m - 1]) + " " + String(d) + ", " + String(y));}/** * @brief Formats ISO date string as French date for display. * @paramdateStr "YYYY-MM-DD HH:MM" * @return "Samedi 04 Octobre" style string. */StringformatDateFR(StringdateStr) {int y = dateStr.substring(0, 4).toInt();int m = dateStr.substring(5, 7).toInt();int d = dateStr.substring(8, 10).toInt();int wday = dayOfTheWeek(y, m, d);returnString(days[wday]) + " " + (d < 10 ? "0" : "") + String(d) + " " + months[m - 1];}/** * @brief Maps Open-Meteo weather code to icon index. * @paramweatherCode Open-Meteo code * @return Icon bitmap index */intweatherCodeToIcon(intweatherCode) {switch (weatherCode) {case0: return8; // Suncase1:case2: return3; // Some cloudscase3: return5; // Cloudycase45:case48: return2; // Fogcase51:case53:case55:case56:case57: return7; // Drizzlecase61:case63:case65:case66:case67: return1; // Raincase71:case73:case75:case77: return4; // Snowcase80:case81:case82: return6; // Showerscase85:case86: return4; // Snowcase95:case96:case99: return0; // Thunderstormdefault: return5; // Default to Cloudy }}/** * @brief Display forecast for one day at (x, y) with weather icon. */voiddisplayForecast(intx, inty, intday, intmin, intmax, inticonNb) {int16_t x1, y1;uint16_t w, h;display.drawBitmap(x + 30, y + 50, epd_bitmap_allArray[iconNb], 50, 50, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.getTextBounds(days[day], x, y, &x1, &y1, &w, &h);display.setCursor(x + 55 - w / 2, y + 40);display.print(days[day]);display.setFont(&FreeSans12pt7b);display.getTextBounds(String(max) + "`", x, y, &x1, &y1, &w, &h);display.setCursor(x + 55 - w / 2, y + 130);display.print(max, 0);display.write(0x60);display.getTextBounds(String(min) + "`", x, y, &x1, &y1, &w, &h);display.setCursor(x + 55 - w / 2, y + 160);display.print(min, 0);display.write(0x60);}/** * @brief Displays today's weather with large icon and temperature. */voiddisplayCurrent(intx, inty, floatcurrent, intmin, intmax, inticonNb) {int16_t x1, y1;uint16_t w, h;display.drawBitmap(x + 30, y + 50, epd_bitmap2_allArray[iconNb], 100, 100, GxEPD_BLACK);display.setFont(&FreeSans24pt7b);display.setCursor(x + 150, y + 130);display.print(current, 0);display.write(0x60);display.setFont(&FreeSans12pt7b);display.getTextBounds(String(max) + "`", 0, 0, &x1, &y1, &w, &h);display.setCursor(x + 80 - w / 2, y + 170);display.print(max, 0);display.write(0x60);display.getTextBounds(String(min) + "`", 0, 0, &x1, &y1, &w, &h);display.setCursor(x + 80 - w / 2, y + 200);display.print(min, 0);display.write(0x60);}/** * @brief Read actual battery voltage from ADC. * @return Battery voltage in Volts. */floatgetBatteryVoltage() {digitalWrite(BATTERY_ENABLE_PIN, HIGH);delay(5);int mv = analogReadMilliVolts(BATTERY_ADC_PIN);digitalWrite(BATTERY_ENABLE_PIN, LOW);return ((mv / 1000.0) * 2); // Correction for voltage divider}/** * @brief Calculates the battery charge percentage based on the measured voltage. * @parambatteryVoltage Measured battery voltage in volts. * @return Estimated battery charge percentage (0 to 100%). */intgetBatteryPercent(floatbatteryVoltage) {if (batteryVoltage > 4.20)return100;elseif (batteryVoltage > 3.96)return90;elseif (batteryVoltage > 3.91)return80;elseif (batteryVoltage > 3.85)return70;elseif (batteryVoltage > 3.80)return60;elseif (batteryVoltage > 3.75)return50;elseif (batteryVoltage > 3.60)return40;elseif (batteryVoltage > 3.40)return20;elseif (batteryVoltage > 3.20)return10;elseif (batteryVoltage > 3.00)return5;elsereturn0;}/** * @brief Arduino setup: serial, display, WiFi, sensors. */voidsetup() {Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);delay(500);pinMode(GREEN_BUTTON, INPUT_PULLUP); // Prepare deep sleep wake-up on button pressesp_sleep_enable_ext0_wakeup((gpio_num_t)GREEN_BUTTON, LOW); // SPI and display initializationhspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));display.init(0);display.setRotation(0); // Connect to WiFiWiFi.begin(wifi_ssid, wifi_password);Serial1.print("Connecting to WiFi...");unsignedlong start = millis();while (WiFi.status() != WL_CONNECTED && (millis() - start) < 15000) {delay(500);Serial1.print("."); }if (WiFi.status() != WL_CONNECTED) {Serial1.println("[ERROR] WiFi connection failed, restart required.");ESP.restart(); } // Initialize I2C with custom pinsWire.begin(I2C_SDA, I2C_SCL); // Initialize onboard SHT4x sensorsht4x.begin(Wire, 0x44); // Configure battery monitoringpinMode(BATTERY_ENABLE_PIN, OUTPUT);digitalWrite(BATTERY_ENABLE_PIN, HIGH); // Enable battery monitoring // Configure ADC for batteryanalogReadResolution(12); // 12-bit ADCanalogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);}/** * @brief Main loop: fetch sensors, display dashboard, go to deep sleep. */voidloop() {int x, y;int x_ha_box, y_ha_box, ha_box_w, ha_box_h; // home assistant boxint x_current_box, y_current_box, current_box_w, current_box_h; // current weather boxint x_forecast_box, y_forecast_box, forecast_box_w, forecast_box_h; // forecast boxint x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, bitcoin_box_h; // bitcoin boxint x_battery_box, y_battery_box, battery_box_w, battery_box_h; // battery boxint16_t x1, y1;uint16_t w, h;esp_err_t esp_error;float internalTemperatureSensor, externalTemperatureSensor, greenHouseTemperature;float min, max; String icon;float sht4xTemperature, sht4xHumidity;uint8_t vBatPercentage;float vBat; // Measure reTerminal internal SHT4 sensoruint16_t error = sht4x.measureMediumPrecision(sht4xTemperature, sht4xHumidity); sht4xTemperature += sht4xCalibration; // Measure battery voltage vBat = getBatteryVoltage(); vBatPercentage = getBatteryPercent(vBat); // Get Bitcoind valueint btc = getBTC();int eth = getETH(); // Fetch weather data for current and next 4 daysif (fetchWeatherData() != 0) {Serial1.print("fetchWeatherData failed!");return; }#ifdef LANGUAGE_FR String date = formatDateFR(localTime); // French formatted date#else String date = formatDateEN(localTime); // French formatted date#endifint today = dayOfTheWeek(localTime.substring(0, 4).toInt(), localTime.substring(5, 7).toInt(), localTime.substring(8, 10).toInt()); // Fetch Home Assistant sensor states internalTemperatureSensor = getHomeAssistantSensorState("sensor.tutoduino_esp32c6tempsensor_temperature"); externalTemperatureSensor = getHomeAssistantSensorState("sensor.lumi_lumi_weather_temperature"); greenHouseTemperature = getHomeAssistantSensorState("sensor.lumi_lumi_weather_temperature_2");display.setFullWindow();display.firstPage();do {display.fillScreen(GxEPD_WHITE); // Display the timestamp of the last weather updatedisplay.setTextColor(GxEPD_BLACK);display.setFont(&FreeSans9pt7b);display.setCursor(600, 470);display.print(localTime); // Display date on topdisplay.setFont(&FreeSans18pt7b);display.getTextBounds(date, 0, 2, &x1, &y1, &w, &h);display.setCursor(400 - w / 2, 40);display.print(date); // Current weather box x_current_box = 30; y_current_box = 60; current_box_w = 240; current_box_h = 210;display.fillRect(x_current_box, y_current_box, current_box_w, 40, BOX_FILL_COLOR);display.drawRect(x_current_box, y_current_box, current_box_w, current_box_h, GxEPD_BLACK);display.drawRect(x_current_box, y_current_box, current_box_w, 40, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.getTextBounds(today_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_current_box + current_box_w / 2 - w / 2, y_current_box + 30);display.setTextColor(BOX_TEXT_COLOR);display.print(today_text);display.setTextColor(GxEPD_BLACK);displayCurrent(x_current_box, y_current_box, currentTemp, D0MinTemp, D0MaxTemp, weatherCodeToIcon(D0Code)); // Forecast box for next 4 days x_forecast_box = 300; y_forecast_box = 60; forecast_box_w = 480; forecast_box_h = 210;display.fillRect(x_forecast_box, y_forecast_box, forecast_box_w, 40, BOX_FILL_COLOR);display.drawRect(x_forecast_box, y_forecast_box, forecast_box_w, forecast_box_h, GxEPD_BLACK);display.drawRect(x_forecast_box, y_forecast_box, forecast_box_w, 40, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.getTextBounds(forecast_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_forecast_box + forecast_box_w / 2 - w / 2, y_forecast_box + 30);display.setTextColor(BOX_TEXT_COLOR);display.print(forecast_text);display.setTextColor(GxEPD_BLACK);displayForecast(x_forecast_box + 10, y_forecast_box + 40, (today + 1) % 7, D1MinTemp, D1MaxTemp, weatherCodeToIcon(D1Code));displayForecast(x_forecast_box + 120, y_forecast_box + 40, (today + 2) % 7, D2MinTemp, D2MaxTemp, weatherCodeToIcon(D2Code));displayForecast(x_forecast_box + 240, y_forecast_box + 40, (today + 3) % 7, D3MinTemp, D3MaxTemp, weatherCodeToIcon(D3Code));displayForecast(x_forecast_box + 360, y_forecast_box + 40, (today + 4) % 7, D4MinTemp, D4MaxTemp, weatherCodeToIcon(D4Code)); // Home Assistant sensors box x_ha_box = 30; y_ha_box = 300; ha_box_w = 400; ha_box_h = 150;display.fillRect(x_ha_box, y_ha_box, ha_box_w, 40, BOX_FILL_COLOR);display.drawRect(x_ha_box, y_ha_box, ha_box_w, ha_box_h, GxEPD_BLACK);display.drawRect(x_ha_box, y_ha_box, ha_box_w, 40, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.getTextBounds(ha_sensor_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_ha_box + ha_box_w / 2 - w / 2, y_ha_box + 30);display.setTextColor(BOX_TEXT_COLOR);display.print(ha_sensor_text);display.setTextColor(GxEPD_BLACK); // Internal temperature from SHT4display.setFont(&FreeSans12pt7b);display.getTextBounds(indoor_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_ha_box + ha_box_w / 6 - w / 2, y_ha_box + 70);display.print(indoor_text);display.setFont(&FreeSans18pt7b);display.getTextBounds(String(sht4xTemperature, 1) + "`", 0, 0, &x1, &y1, &w, &h);display.setCursor(x_ha_box + ha_box_w / 6 - w / 2, y_ha_box + 120);display.print(sht4xTemperature, 1);display.write(0x60); // Outdoor temperature (Home Assistant)display.setFont(&FreeSans12pt7b);display.getTextBounds(outdoor_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_ha_box + ha_box_w / 2 - w / 2, y_ha_box + 70);display.print(outdoor_text);display.setFont(&FreeSans18pt7b);display.getTextBounds(String(externalTemperatureSensor, 1) + "`", 0, 0, &x1, &y1, &w, &h);display.setCursor(x_ha_box + ha_box_w / 2 - w / 2, y_ha_box + 120);display.print(externalTemperatureSensor, 1);display.write(0x60); // Greenhouse temperature (Home Assistant)display.setFont(&FreeSans12pt7b);display.getTextBounds(other_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_ha_box + 5 * ha_box_w / 6 - w / 2, y_ha_box + 70);display.print(other_text);display.setFont(&FreeSans18pt7b);display.getTextBounds(String(greenHouseTemperature, 1) + "`", 0, 0, &x1, &y1, &w, &h);display.setCursor(x_ha_box + 5 * ha_box_w / 6 - w / 2, y_ha_box + 120);display.print(greenHouseTemperature, 1);display.write(0x60); // Bitcoin price box x_bitcoin_box = 460; y_bitcoin_box = 300; bitcoin_box_w = 150; bitcoin_box_h = 150;display.fillRect(x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, 40, BOX_FILL_COLOR);display.drawRect(x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, bitcoin_box_h, GxEPD_BLACK);display.drawRect(x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, 40, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.getTextBounds(crypto_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_bitcoin_box + bitcoin_box_w / 2 - w / 2, y_bitcoin_box + 30);display.setTextColor(BOX_TEXT_COLOR);display.print(crypto_text);display.setTextColor(GxEPD_BLACK); // Bitcoindisplay.drawBitmap(x_bitcoin_box + 10, y_bitcoin_box + 60, epd_bitmap3_allArray[0], 30, 30, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.setCursor(x_bitcoin_box + 40, y_bitcoin_box + 80);display.print(btc);display.print(" $"); // Ethereumdisplay.drawBitmap(x_bitcoin_box + 10, y_bitcoin_box + 95, epd_bitmap3_allArray[2], 30, 30, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.setCursor(x_bitcoin_box + 40, y_bitcoin_box + 120);display.print(eth);display.print(" $"); // Battery percentage box x_battery_box = 640; y_battery_box = 300; battery_box_w = 140; battery_box_h = 150;display.fillRect(x_battery_box, y_battery_box, battery_box_w, 40, BOX_FILL_COLOR);display.drawRect(x_battery_box, y_battery_box, battery_box_w, battery_box_h, GxEPD_BLACK);display.drawRect(x_battery_box, y_battery_box, battery_box_w, 40, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.getTextBounds(battery_text, 0, 0, &x1, &y1, &w, &h);display.setCursor(x_battery_box + battery_box_w / 2 - w / 2, y_battery_box + 30);display.setTextColor(BOX_TEXT_COLOR);display.print(battery_text);display.setTextColor(GxEPD_BLACK);display.drawBitmap(x_battery_box + 10, y_battery_box + 70, epd_bitmap3_allArray[1], 40, 40, GxEPD_BLACK);display.setFont(&FreeSans12pt7b);display.setCursor(x_battery_box + 50, y_battery_box + 80);display.print(vBatPercentage);display.print(" %");display.setCursor(x_battery_box + 50, y_battery_box + 120);display.print(vBat);display.print(" V"); } while (display.nextPage());display.hibernate();delay(1000);uint64_t sleepTime = 60 * 60 * uS_TO_S_FACTOR; // 60 minutes esp_error = esp_sleep_enable_timer_wakeup(sleepTime);if (esp_error != ESP_OK) {Serial1.print("Error to enter deep sleep: ");Serial1.println(esp_error); } else {Serial1.println("Enter deep sleep for 60min...");esp_deep_sleep_start(); }}
Le code source complet est disponible sur mon GitHub, y compris les icônes et les polices utilisées dans cet exemple.
reTerminal E1002 programmé sous IDE Arduino pour afficher de données provenant de multiples sources
Le code fonctionne sur le reTerminal E1002 (écran couleur) mais également le sur reTerminal E1001 (écran noir & blanc). L’avantage du reTerminal E1001 est la rapidité du rafraîchissement de son écran.
Comparaison du temps de rafraîchissement des écrans du reTerminal E1002 et E1001
Je vous recommande également la lecture de mon tutoriel Home Assistant : Afficher les données Météo-France sur un reTerminal E1001 avec ESPHome.
J’espère que cet article vous aura intéressé. N’hésitez pas à donner votre avis en cliquant sur les étoiles ci-dessous.
Mesure de tension et de courant avec un module INA3221
Le composant INA3221 est un circuit intégré de mesure de courant et de tension fabriqué par Texas Instruments. Il est principalement utilisé pour surveiller le courant et la tension dans des systèmes électroniques.
Dans cet article je vais détailler son principe et donner pour exemple la surveillance de la charge d’une batterie par un panneau solaire.
Principe du circuit intégré INA3221
Le principe de fonctionnement du composant INA3221 repose sur la mesure du courant au moyen d’une résistance de shunt, qui est une résistance de très faible valeur placée en série avec la charge. Le composant amplifie puis mesure la tension aux bornes de cette résistance. Il détermine ensuite la valeur du courant en divisant la tension mesurée par la valeur connue de cette résistance de shunt.
Imaginons que nous souhaitions mesurer le courant qui circule dans la résistance R (la charge utile) dans le schéma ci-dessous.
Nous allons introduire une résistance de shunt dans ce circuit afin de mesurer ce courant. En effet, le courant qui circule dans la charge utile (résistance R) est le même que celui qui circule dans la résistance de shunt (Rshunt). La mesure de la tension aux bornes de la résistance de shunt (Vshunt = VIN+ – VIN-) permet d’en déduire ce courant, en appliquant la loi d‘Ohm (voir les bases de l’électronique).
I = Vshunt / Rshunt
Notez que le composant IN3221 mesure à la fois la tension aux bornes de la résistance de shunt (Vshunt), mais aussi la tension aux bornes de la charge utile (Vbus).
Module INA3221 triple canal
Pour réaliser ce tutoriel, j’ai utilisé un module INA3221 à trois canaux, capable de mesurer le courant et la tension sur trois voies distinctes.
INA3221 vues recto et verso
Ce module INA3221 intègre une résistance de shunt R100 (dont la valeur est 0.1 Ω) pour chaque canal.
Pour mesurer le courant qui circule dans la résistance R, nous allons utiliser le premier canal (CH1) de l’INA3221. Pour cela il suffit de connecter :
La broche VIN1+ à l’alimentation (côté positif, avant la résistance de shunt).
La broche VIN1- à la charge (côté positif, après la résistance de shunt).
La broche GND à la masse du système (côté négatif de l’alimentation et de la charge utile).
Alimentation du module et communication I2C avec l’Arduino
L’alimentation du module se fait via ses broches GND et VS, avec une tension comprise entre 2.7 V et 5.5 V.
Le module communique avec l’Arduino via un bus I2C. Il suffit donc de relier les broches SCL et SDA du module à celles de l’Arduino.
Par défaut, le module est configuré avec l’adresse I2C égale à 0x40. Mais il est possible de configurer l’adresse I2C du module en faisant un pont de soudure entre la borne A0 et l’une des broches suivantes :
Modification manuelle de l’adresse I2C du module INA3221 en faisant un pont de soudure
Exemple de croquis pour Arduino
Voici l’exemple de croquis fourni avec la librairie Adafruit INA3221 qui permet d’afficher la tension et le courant sur les 3 canaux.
Attention, il est nécessaire de modifier la valeur de la résistance de shunt en fonction de votre module. Le module INA3221 d’Adafruit est équipé de résistance de shunt de 0.5 Ω alors que notre module est équipé de résistance de shunt de 0.1 Ω.
C++
// Mesure de tension et courant avec module INA3221// https://tutoduino.fr/// Copyleft 2025#include"Adafruit_INA3221.h"#include<Wire.h>#define SHUNT_RESISTANCE 0.1#define CH1 0#define CH2 1#define CH3 2// Create an INA3221 objectAdafruit_INA3221 ina3221;voidsetup() {Serial.begin(115200);while (!Serial)delay(10); // Wait for serial port to connect on some boardsSerial.println("Adafruit INA3221 simple test"); // Initialize the INA3221if (!ina3221.begin(0x40, &Wire)) { // can use other I2C addresses or busesSerial.println("Failed to find INA3221 chip");delay(10); }Serial.println("INA3221 Found!");ina3221.setAveragingMode(INA3221_AVG_4_SAMPLES); // Set shunt resistances for all channelsfor (uint8_t i = 0; i < 3; i++) {ina3221.setShuntResistance(i, SHUNT_RESISTANCE); }}voidloop() { // Display voltage and current for channel 1float bus_voltage1 = ina3221.getBusVoltage(CH1);float current1 = ina3221.getCurrentAmps(CH1) * 1000; // Convert to mASerial.print("CH1 Battery : Bus voltage = ");Serial.print(bus_voltage1, 2);Serial.print(" V, Current = ");Serial.print(current1, 2);Serial.println(" mA"); // Display voltage and current for channel 2float bus_voltage2 = ina3221.getBusVoltage(CH2);float current2 = ina3221.getCurrentAmps(CH2) * 1000; // Convert to mASerial.print("CH2 Solar panel : Bus voltage = ");Serial.print(bus_voltage2, 2);Serial.print(" V, Current = ");Serial.print(current2, 2);Serial.println(" mA"); // Display voltage and current for channel 3float bus_voltage3 = ina3221.getBusVoltage(CH3);float current3 = ina3221.getCurrentAmps(CH3) * 1000; // Convert to mASerial.print("CH3 Load : Bus voltage = ");Serial.print(bus_voltage3, 2);Serial.print(" V, Current = ");Serial.print(current3, 2);Serial.println(" mA"); Serial.println(""); // Delay for 1s before the next readingdelay(1000);}
Surveillance de la charge d’une batterie par un panneau solaire
Le montage suivant utilise un module CN3791, qui est un contrôleur de charge MPPT (Maximum Power Point Tracking) conçu pour charger des batteries lithium-ion à partir de panneaux solaires.
Le module INA3221 mesure la tension de la batterie sur le premier canal (CH1), permettant de connaître l’état de charge de la batterie. Le second canal (CH2) est utilisé pour mesurer le courant généré par le panneau solaire. Le troisième canal (CH3) est utilisé pour mesurer le courant consommé par la charge utile.
Exemple de mesures
Les mesures suivantes ont été réalisées avec une charge utile consommant environ 90 mA sous 4 V. J’ai effectué plusieurs mesures dans des conditions d’ensoleillement différentes.
Ensoleillement fort :
Dans cet exemple, le panneau génère un courant de 318 mA. Ce courant est suffisant pour alimenter la charge utile en plus de charger la batterie avec un courant de 257 mA.
Plaintext
CH1 Battery : Bus voltage = 4.05 V, Current = 257.60 mACH2 Solar panel : Bus voltage = 5.52 V, Current = 318.40 mACH3 Load : Bus voltage = 4.02 V, Current = 93.50 mA
Ensoleillement faible :
Lorsque le soleil est faible, le panneau génère peu de courant. En cette fin d’après-midi nuageuse il génère un courant de 27 mA. Ce courant est insuffisant pour alimenter la charge utile, la batterie est donc mise à contribution pour l’alimenter. Le courant mesuré au niveau de la batterie est négatif, car elle fournit un courant de 65 mA pour alimenter la charge utile.
Plaintext
CH1 Battery : Bus voltage = 4.02 V, Current = -65.20 mACH2 Solar panel : Bus voltage = 5.23 V, Current = 27.60 mACH3 Load : Bus voltage = 3.99 V, Current = 94.00 mA
Aucun ensoleillement :
La nuit, le panneau ne génère plus de courant et la charge utile est uniquement alimentée par la batterie.
Plaintext
CH1 Battery : Bus voltage = 4.01 V, Current = -95.20 mACH2 Solar panel : Bus voltage = 3.59 V, Current = -0.40 mACH3 Load : Bus voltage = 3.98 V, Current = 94.00 mA
Comparatif autonomie de nœuds Meshtastic
J’ai découvert récemment Meshtastic, la plateforme open-source de communication sans fil basée sur LoRa, qui permet de créer des réseaux maillés décentralisés et autonomes. Dans cet article, je compare la consommation électrique de quelques nœuds Meshtastic que j’utilise. La consommation des nœuds est importante, car elle détermine l’autonomie que vous pourrez atteindre en fonction de la capacité de la batterie que vous envisagez d’utiliser.
Plusieurs critères importants sont pris en compte dans ce protocole de test :
Wi-Fi : l’activation du Wi-Fi permet au nœud de communiquer en MQTT (possible également via une liaison BLE avec un Smartphone)
BLE : l’activation du Bluetooth Low Energy (BLE) permet au nœud de communiquer avec l’application Meshtastic sur un smartphone, pour envoyer et recevoir des messages par exemple.
LoRa : par définition la liaison LoRa d’un nœud Meshtastic est régulière. Mais en fonction de son rôle (client, routeur, répéteur…) et du volume de échanges radio, la consommation du module LoRa est très variable. Pour mesurer la consommation d’une émission radio LoRa, j’ai configuré le LoRa avec le préréglage modem LONG_SLOW afin que la durée d’émission permette d’effectuer la mesure.
Écran : certains nœuds sont équipés de petits écrans OLED. Si ils peuvent être pratiques pour lire certaines informations directement sur les nœud sans avoir à ouvrir l’application sur le smartphone, ce type d’écran consomme environ une dizaine de milliampères.
Rôle du nœud : le rôle du nœud va impacter fortement sa consommation électrique. En effet, un nœud CLIENT_MUTE va beaucoup moins solliciter le module radio LoRa qu’un nœud ROUTER ou REPEATER.
GPS : le module GPS permet au nœud de déterminer sa position de manière autonome. Si le nœud n’est pas mobile, il est préférable de définir une position fixe et d’éviter d’utiliser un module GPS pour réduire sa consommation électrique. Si le nœud est mobile il est également possible que la position soit fournie au nœud par le smartphone.
La mesure de la consommation électrique a été réalisée avec une alimentation RIDEN RD6006, les nœud ont été alimentés en 5V via leur port USB-C.
Module HELTEC V3
Le module HELTEC V3 est un noeud Meshtastic équipé d’un écran et doté d’une communication LoRa, Wi-Fi et Bluetooth Low Energy (BLE).
Considérons l’hypothèse d’utilisation suivante : utilisation de l’écran, connexion à un smartphone en Bluetooth, émission d’un message LoRa toute les minutes, en considérant la durée de transmission égale à 1 seconde (il faudrait affiner cette partie pour une estimation plus juste). Dans cette hypothèse, la consommation moyenne du module est d’environ 130 mA. Équipé d’une batterie 2500 mAh, le module aura une autonomie d’environ 19h heures.
Module XIAO ESP32S3 & Wio-SX1262
Le module XIAO ESP32S3 & Wio-SX1262 ne possède pas d’écran, il est doté d’une communication LoRa, Wi-Fi et Bluetooth Low Energy (BLE).
Module XIAO nRF52840 & Wio-SX1262
Le moduleXIAO nRF52840 & Wio-SX1262 ne possède pas d’écran et n’a pas de connectivité Wi-Fi , il est uniquement doté d’une connectivité LoRa et Bluetooth Low Energy (BLE). Le module nRF52840 est remarquable, la consommation liée à l’activation du Bluetooth n’est même pas mesurable sur ce module.
Sensibiliser aux risques des clés USB avec un Lilygo T-Dongle-S3
Les clés USB sont pratiques pour le stockage et le transfert de données. Mais elles présentent également plusieurs risques potentiels pour la sécurité. Au delà des logiciels malveillants qu’elles peuvent contenir, certaines clés (comme la célèbre Rubber Ducky) peuvent être programmées pour se comporter comme un clavier. Permettant ainsi à un attaquant d’injecter des commandes malveillantes dans le système hôte.
Vous souhaitez sensibiliser vos équipes à ce risque ? Vous avez un budget de moins de 20€ ? Cela tombe bien car c’est l’objectif de cet article !
Pour rendre ce petit tutoriel accessible à tous, j’utilise une clé Lilygo T-Dongle-S3 que je programme avec l’IDE Arduino.
Installation du gestionnaire de cartes ESP32
Le dongle Lilygo étant basé sur le microcontrôleur ESP32-S3 il faut installer le support des cartes Espressif ESP32 dans l’IDE Arduino.
Dans le menu préférences de l’IDE Arduino, ajoutez l’URL suivante dans le gestionnaire de carte :
Installez le paquet de gestion de cartes esp32 by Espressif. Attention, à date (15/02/2025) il faut installer la version 2.0.14 car la librairie RFT_eSPI que nous allons utiliser ne fonctionne pas avec les versions ultérieures sur l’ESP32-S3 (voir TFT_eSPI/issue3329).
Une fois le gestionnaire de cartes installé, sélectionnez la carte ESP32S3 Dev Module et configurez la taille de la mémoire Flash à 16MB.
Installation des librairies pour l’écran LCD et la LED RGB
Installez ensuite la librairie FastLED pour la gestion de la LED RGB, et la librairie TFT_eSPI pour la gestion de l’écran LCD.
Il est indispensable de modifier le fichier User_Setup_Select.h dans le répertoire de la librairie TFT_eSPI pour que la librairie soit configurée pour notre dongle Lilygo-T-Dongle-S3.
Commenter la ligne 27 du fichier User_Setup_Select.h :
C++
//#include <User_Setup.h> // Default setup is root library folder
Dé-commenter la ligne 135 du fichier User_Setup_Select.h :
C++
#include<User_Setups/Setup209_LilyGo_T_Dongle_S3.h>// For the LilyGo T-Dongle S3 based ESP32 with ST7735 80 x 160 TFT
Voici à quoi doit ressembler ce fichier User_Setup_Select.h après ces 2 modifications :
Un premier croquis pour vérifier que tout fonctionne bien
Pour vérifier que votre installation et configuration est correcte, voici un croquis qui affiche un message à l’écran et fait clignoter la LED RGB.
C++
// LilyGo T-Dongle-S3 ESP32// https://tutoduino.fr/// Copyleft 2025#include<FastLED.h>#include<SPI.h>#include<TFT_eSPI.h>TFT_eSPI tft = TFT_eSPI(); // Invoke custom library#define DATA_PIN 40#define CLOCK_PIN 39CRGB rgb_led[1];voidsetup() { // Init RGB LED in BGR orderingFastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, BGR>(rgb_led, 1); // Init TFT screentft.init(); // Set screen in landscape mode for right side laptop usb port (turn 270°)tft.setRotation(3); // Set screen background to BLACKtft.fillScreen(TFT_BLACK); // Set "cursor" at top left corner of display (0,0) and select font 4tft.setCursor(0, 0, 4); // Set text color to whitetft.setTextColor(TFT_WHITE);tft.println("Welcome to");tft.println("Tutoduino"); }voidloop() { // Set LED color to blue, then pausergb_led[0] = CRGB::Blue;FastLED.show();delay(500); // Set LED color to white, then pausergb_led[0] = CRGB::White;FastLED.show();delay(500); // Set LED color to red, then pausergb_led[0] = CRGB::Red;FastLED.show();delay(500); // Set LED color to black, then pausergb_led[0] = CRGB::Black;FastLED.show();delay(500);}
Si le téléversement se déroule bien, au démarrage la LED RGB devrait clignoter Bleu-Blanc-Rouge et le texte “Welcome to Tutoduino” devrait s’afficher sur l’écran.
Le croquis de sensibilisation aux risques liés aux clés USB
Ce programme s’exécute automatiquement dès que le dongle USB est inséré dans un PC. Il est prévu pour un PC fonctionnant sous Windows, mais il peut être adapté très facilement pour un PC sous Linux (“Windows+r” doit simplement être remplacé par “CTRL+ALT+t”).
Lorsque il est connecté à un PC sous Windows, le dongle USB émule pendant 30 secondes un stockage de masse USB contenant un fichier au format csv. Après 30 secondes, le stockage de masse USB est éjecté et le dongle émule alors un clavier USB. Ce clavier USB envoi alors la combinaison de touche “Windows + r” qui ouvre la boite de dialogue “exécuter” puis y entre la commande “cmd” (“c;d” pour un clavier azerty) afin d’ouvrir un invité de commande (Command Prompt). La commande “dir” est ensuite exécutée dans cet invité de commande, affichant le contenu du répertoire courant…
Un message est également affiché sur l’écran du dongle USB, avec pour objectif de rendre plus visuel ce type d’attaque et de renforcer l’efficacité de la sensibilisation.
C++
/* * Sensibilisation aux risques liés à l'utilisation des clés USB * ------------------------------------------------------------- * * Ce programme configure un dongle USB à base de ESP32 pour se comporter * d'abord comme une clé USB, puis comme un clavier qui ouvre automatiquement * un terminal. * * ATTENTION : Ce programme est à des fins éducatives uniquement. Il démontre * comment un dispositif peut émuler une clé USB et un clavier, ce qui peut * potentiellement être utilisé à des fins malveillantes. * * Risques associés : * 1. Exécution de commandes non autorisées : En se comportant comme un clavier, * le dispositif peut injecter des commandes dans le système hôte. * 2. Vol de données : Le dispositif peut copier des fichiers sensibles sur lui-même * lorsqu'il est connecté en tant que clé USB. * 3. Installation de logiciels malveillants : Le dispositif peut installer des * logiciels malveillants sur le système hôte. * * Précautions à prendre : * - Ne jamais connecter de clés USB non fiables à votre ordinateur. * - Utiliser des logiciels de sécurité pour analyser les clés USB avant de les utiliser. * - Désactiver l'exécution automatique des fichiers à partir des clés USB. * * L'auteur de ce programme décline toute responsabilité en cas d'utilisation * abusive ou de dommages causés par ce programme. * * https://tutoduino.fr/ * Copyleft 2025*/#include"USB.h"#include"USBMSC.h"#include<FastLED.h>#include<SPI.h>#include<TFT_eSPI.h>#include"USBHIDKeyboard.h"// KeyboardUSBHIDKeyboard Keyboard;// LCD screenTFT_eSPI tft = TFT_eSPI(); // Invoke custom library// RGB LED#define RGB_DATA_PIN 40#define RGB_CLOCK_PIN 39CRGB rgb_led[1];USBCDC USBSerial;USBMSC MSC;#define FAT_U8(v) ((v) & 0xFF)#define FAT_U16(v) FAT_U8(v), FAT_U8((v) >> 8)#define FAT_U32(v) FAT_U8(v), FAT_U8((v) >> 8), FAT_U8((v) >> 16), FAT_U8((v) >> 24)#define FAT_MS2B(s,ms) FAT_U8(((((s) & 0x1) * 1000) + (ms)) / 10)#define FAT_HMS2B(h,m,s) FAT_U8(((s) >> 1)|(((m) & 0x7) << 5)), FAT_U8((((m) >> 3) & 0x7)|((h) << 3))#define FAT_YMD2B(y,m,d) FAT_U8(((d) & 0x1F)|(((m) & 0x7) << 5)), FAT_U8((((m) >> 3) & 0x1)|((((y) - 1980) & 0x7F) << 1))#define FAT_TBL2B(l,h) FAT_U8(l), FAT_U8(((l >> 8) & 0xF) | ((h << 4) & 0xF0)), FAT_U8(h >> 4)#define README_CONTENTS "Asset,Date,Time,Production\nPROD_LINE_A,05/02/25,14:28:00,180\nPROD_LINE_G,05/02/25,15:05:00,245\nPROD_LINE_U,05/02/25,14:28:00,180"staticconstuint32_t DISK_SECTOR_COUNT = 2 * 8; // 8KB is the smallest size that windows allow to mountstaticconstuint16_t DISK_SECTOR_SIZE = 512; // Should be 512staticconstuint16_t DISC_SECTORS_PER_TABLE = 1; //each table sector can fit 170KB (340 sectors)staticuint8_tmsc_disk[DISK_SECTOR_COUNT][DISK_SECTOR_SIZE] ={ //------------- Block0: Boot Sector -------------// { // Header (62 bytes)0xEB, 0x3C, 0x90, //jump_instruction'M' , 'S' , 'D' , 'O' , 'S' , '5' , '.' , '0' , //oem_nameFAT_U16(DISK_SECTOR_SIZE), //bytes_per_sectorFAT_U8(1), //sectors_per_clusterFAT_U16(1), //reserved_sectors_countFAT_U8(1), //file_alloc_tables_numFAT_U16(16), //max_root_dir_entriesFAT_U16(DISK_SECTOR_COUNT), //fat12_sector_num0xF8, //media_descriptorFAT_U16(DISC_SECTORS_PER_TABLE), //sectors_per_alloc_table;//FAT12 and FAT16FAT_U16(1), //sectors_per_track;//A value of 0 may indicate LBA-only accessFAT_U16(1), //num_headsFAT_U32(0), //hidden_sectors_countFAT_U32(0), //total_sectors_320x00, //physical_drive_number;0x00 for (first) removable media, 0x80 for (first) fixed disk0x00, //reserved0x29, //extended_boot_signature;//should be 0x29FAT_U32(0x1234), //serial_number: 0x1234 => 1234'T' , 'i' , 'n' , 'y' , 'U' , 'S' , 'B' , ' ' , 'M' , 'S' , 'C' , //volume_label padded with spaces (0x20)'F' , 'A' , 'T' , '1' , '2' , ' ' , ' ' , ' ' , //file_system_type padded with spaces (0x20) // Zero up to 2 last bytes of FAT magic code (448 bytes)0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //boot signature (2 bytes)0x55, 0xAA }, //------------- Block1: FAT12 Table -------------// {FAT_TBL2B(0xFF8, 0xFFF), FAT_TBL2B(0xFFF, 0x000) // first 2 entries must be 0xFF8 0xFFF, third entry is cluster end of readme file }, //------------- Block2: Root Directory -------------// { // first entry is volume label'U' , 'S' , 'B' , ' ' , 'K' , 'E' , 'Y' , ' ' , ' ' , ' ' , ' ' , 0x08, //FILE_ATTR_VOLUME_LABEL0x00, FAT_MS2B(0,0), FAT_HMS2B(0,0,0),FAT_YMD2B(0,0,0), FAT_YMD2B(0,0,0), FAT_U16(0), FAT_HMS2B(13,42,30), //last_modified_hmsFAT_YMD2B(2018,11,5), //last_modified_ymdFAT_U16(0), FAT_U32(0), // second entry is readme file'P' , 'R' , 'O' , 'D' , 'D' , 'A' , 'T' , 'A' ,//file_name[8]; padded with spaces (0x20)'T' , 'X' , 'T' , //file_extension[3]; padded with spaces (0x20)0x20, //file attributes: FILE_ATTR_ARCHIVE0x00, //ignoreFAT_MS2B(1,980), //creation_time_10_ms (max 199x10 = 1s 990ms)FAT_HMS2B(13,42,36), //create_time_hms [5:6:5] => h:m:(s/2)FAT_YMD2B(2018,11,5), //create_time_ymd [7:4:5] => (y+1980):m:dFAT_YMD2B(2020,11,5), //last_access_ymdFAT_U16(0), //extended_attributesFAT_HMS2B(13,44,16), //last_modified_hmsFAT_YMD2B(2019,11,5), //last_modified_ymdFAT_U16(2), //start of file in clusterFAT_U32(sizeof(README_CONTENTS) - 1) //file size }, //------------- Block3: Readme Content -------------// README_CONTENTS};unsignedlong start_time = 0;staticint32_tonWrite(uint32_tlba, uint32_toffset, uint8_t*buffer, uint32_tbufsize){memcpy(msc_disk[lba] + offset, buffer, bufsize);return bufsize;}staticint32_tonRead(uint32_tlba, uint32_toffset, void*buffer, uint32_tbufsize){memcpy(buffer, msc_disk[lba] + offset, bufsize);return bufsize;}staticboolonStartStop(uint8_tpower_condition, boolstart, boolload_eject){returntrue;}staticvoidusbEventCallback(void*arg, esp_event_base_tevent_base, int32_tevent_id, void*event_data){}voidawarness_on() { // Eject USB keyMSC.end(); // Emulate keyboard and open a terminalKeyboard.begin();Keyboard.press(KEY_RIGHT_GUI);Keyboard.press('r');Keyboard.releaseAll();delay(500);Keyboard.println("c;d");delay(500);Keyboard.println("dir");Keyboard.println("echo Zhqt could go zrongM"); // Display awarness message on LCD screentft.setRotation(3); tft.setCursor(0, 0, 2); // Set text color to whitetft.setTextColor(TFT_RED);tft.println("USB keys:");tft.println("Small but sneaky.");tft.println("Be worry!"); // Set LED color to redrgb_led[0] = CRGB::Red;FastLED.show(); // Infinite loopwhile (1) {rgb_led[0] = CRGB::Red;FastLED.show();delay(1000);rgb_led[0] = CRGB::Black;FastLED.show();delay(1000); };}voidsetup() { // Init USB keyUSB.onEvent(usbEventCallback);MSC.vendorID("ESP32");//max 8 charsMSC.productID("USB_MSC");//max 16 charsMSC.productRevision("1.0");//max 4 charsMSC.onStartStop(onStartStop);MSC.onRead(onRead);MSC.onWrite(onWrite);MSC.mediaPresent(true);MSC.begin(DISK_SECTOR_COUNT, DISK_SECTOR_SIZE); //USBSerial.begin();USB.begin(); // Init TFT screen and set it to blacktft.init();tft.fillScreen(TFT_BLACK); // Init RGB LED in BGR ordering and set it to blackFastLED.addLeds<APA102, RGB_DATA_PIN, RGB_CLOCK_PIN, BGR>(rgb_led, 1);rgb_led[0] = CRGB::Black;FastLED.show(); start_time = millis();}voidloop() { // start awarness session after 20 seconds ;)if ((millis() - start_time) > 30000) {awarness_on(); }}
Il faut configurer le USB Mode sur USB-OTG avant de téléverser le croquis sur le dongle.
Note : si vous n’arrivez pas à téléverser le croquis sur le dongle, il faut le débrancher et le reconnecter tout en appuyant sur le bouton Boot (au dos de la clé, côté opposé à l’écran). Relâcher le bouton lorsque la clé est insérée dans le PC, puis téléverser le croquis.
Voici en vidéo ce que l’utilisateur verra sur son écran lorsqu’il va insérer la clé USB dans son PC, et l’affichage sur l’écran de la clé USB lorsque l’attaque sera terminée.
Utilisation d’une clé Waveshare ESP32-S3-LCD-1.47
Vous pouvez également utiliser la clé USB Waveshare ESP32-S3-LCD-1.47, il suffit de modifier la configuration de l’écran et de la LED.
Voici le contenu du fichier de configuration pour cet écran de résolution 172×320, qu’il suffit de créer dans la librairie TFT_eSPI et d’inclure dans le fichier User_Setup_Select.h :
C++
// ST7789 172 x 320 display with no chip select line#define USER_SETUP_ID 210#define ST7789_DRIVER // Configure all registers#define TFT_WIDTH 172#define TFT_HEIGHT 320//#define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue//#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red#define TFT_INVERSION_ON//#define TFT_INVERSION_OFF#define TFT_BACKLIGHT_ON 1#define TFT_BL 48 // LED back-light#define TFT_MISO -1 // Not connected#define TFT_MOSI 45#define TFT_SCLK 40#define TFT_CS 42 #define TFT_DC 41#define TFT_RST 39 // Connect reset to ensure display initialises#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:.#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.//#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts#define SMOOTH_FONT// #define SPI_FREQUENCY 27000000#define SPI_FREQUENCY 40000000#define SPI_READ_FREQUENCY 20000000
Pour mettre un peu plus de piquant dans la sensibilisation, j’ai apporté une légère modification par rapport au programme précédent. L’événement qui transforme la clé d’un stockage de masse en clavier n’est plus une temporisation de 30 secondes, mais un message envoyé par un smartphone via une connexion BLE (Bluetooth Low Energy). Il suffit de se connecter à la clé “ESP32-USB” depuis son smartphone avec un logiciel comme LightBlue (voir cet article) et d’écrire “ON” (en UTF-8 String) dans le paramètre “beb5483e-36e1-4688-b7f5-ea07361b26a8”.
Voici le croquis correspondant pour le Waveshare ESP32-S3-LCD-1.47 :
C++
/* * Sensibilisation aux risques liés à l'utilisation des clés USB * ------------------------------------------------------------- * * Ce programme configure un dongle USB à base de ESP32 pour se comporter * d'abord comme une clé USB, puis comme un clavier qui ouvre automatiquement * un terminal. * * ATTENTION : Ce programme est à des fins éducatives uniquement. Il démontre * comment un dispositif peut émuler une clé USB et un clavier, ce qui peut * potentiellement être utilisé à des fins malveillantes. * * Risques associés : * 1. Exécution de commandes non autorisées : En se comportant comme un clavier, * le dispositif peut injecter des commandes dans le système hôte. * 2. Vol de données : Le dispositif peut copier des fichiers sensibles sur lui-même * lorsqu'il est connecté en tant que clé USB. * 3. Installation de logiciels malveillants : Le dispositif peut installer des * logiciels malveillants sur le système hôte. * * Précautions à prendre : * - Ne jamais connecter de clés USB non fiables à votre ordinateur. * - Utiliser des logiciels de sécurité pour analyser les clés USB avant de les utiliser. * - Désactiver l'exécution automatique des fichiers à partir des clés USB. * * L'auteur de ce programme décline toute responsabilité en cas d'utilisation * abusive ou de dommages causés par ce programme. * * https://tutoduino.fr/ * Copyleft 2025*/#include"USB.h"#include"USBMSC.h"#include<FastLED.h>#include<SPI.h>#include<TFT_eSPI.h>#include"USBHIDKeyboard.h"#include<BLEDevice.h>#include<BLEUtils.h>#include<BLEServer.h>// BLE#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"// KeyboardUSBHIDKeyboard Keyboard;// LCD screenTFT_eSPI tft = TFT_eSPI(); // Invoke custom library// RGB LED#define PIN_NEOPIXEL 38USBCDC USBSerial;USBMSC MSC;#define FAT_U8(v) ((v) & 0xFF)#define FAT_U16(v) FAT_U8(v), FAT_U8((v) >> 8)#define FAT_U32(v) FAT_U8(v), FAT_U8((v) >> 8), FAT_U8((v) >> 16), FAT_U8((v) >> 24)#define FAT_MS2B(s,ms) FAT_U8(((((s) & 0x1) * 1000) + (ms)) / 10)#define FAT_HMS2B(h,m,s) FAT_U8(((s) >> 1)|(((m) & 0x7) << 5)), FAT_U8((((m) >> 3) & 0x7)|((h) << 3))#define FAT_YMD2B(y,m,d) FAT_U8(((d) & 0x1F)|(((m) & 0x7) << 5)), FAT_U8((((m) >> 3) & 0x1)|((((y) - 1980) & 0x7F) << 1))#define FAT_TBL2B(l,h) FAT_U8(l), FAT_U8(((l >> 8) & 0xF) | ((h << 4) & 0xF0)), FAT_U8(h >> 4)#define README_CONTENTS "Asset,Date,Time,Production\nPROD_LINE_A,05/02/25,14:28:00,180\nPROD_LINE_G,05/02/25,15:05:00,245\nPROD_LINE_U,05/02/25,14:28:00,180"staticconstuint32_t DISK_SECTOR_COUNT = 2 * 8; // 8KB is the smallest size that windows allow to mountstaticconstuint16_t DISK_SECTOR_SIZE = 512; // Should be 512staticconstuint16_t DISC_SECTORS_PER_TABLE = 1; //each table sector can fit 170KB (340 sectors)staticuint8_tmsc_disk[DISK_SECTOR_COUNT][DISK_SECTOR_SIZE] ={ //------------- Block0: Boot Sector -------------// { // Header (62 bytes)0xEB, 0x3C, 0x90, //jump_instruction'M' , 'S' , 'D' , 'O' , 'S' , '5' , '.' , '0' , //oem_nameFAT_U16(DISK_SECTOR_SIZE), //bytes_per_sectorFAT_U8(1), //sectors_per_clusterFAT_U16(1), //reserved_sectors_countFAT_U8(1), //file_alloc_tables_numFAT_U16(16), //max_root_dir_entriesFAT_U16(DISK_SECTOR_COUNT), //fat12_sector_num0xF8, //media_descriptorFAT_U16(DISC_SECTORS_PER_TABLE), //sectors_per_alloc_table;//FAT12 and FAT16FAT_U16(1), //sectors_per_track;//A value of 0 may indicate LBA-only accessFAT_U16(1), //num_headsFAT_U32(0), //hidden_sectors_countFAT_U32(0), //total_sectors_320x00, //physical_drive_number;0x00 for (first) removable media, 0x80 for (first) fixed disk0x00, //reserved0x29, //extended_boot_signature;//should be 0x29FAT_U32(0x1234), //serial_number: 0x1234 => 1234'T' , 'i' , 'n' , 'y' , 'U' , 'S' , 'B' , ' ' , 'M' , 'S' , 'C' , //volume_label padded with spaces (0x20)'F' , 'A' , 'T' , '1' , '2' , ' ' , ' ' , ' ' , //file_system_type padded with spaces (0x20) // Zero up to 2 last bytes of FAT magic code (448 bytes)0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //boot signature (2 bytes)0x55, 0xAA }, //------------- Block1: FAT12 Table -------------// {FAT_TBL2B(0xFF8, 0xFFF), FAT_TBL2B(0xFFF, 0x000) // first 2 entries must be 0xFF8 0xFFF, third entry is cluster end of readme file }, //------------- Block2: Root Directory -------------// { // first entry is volume label'U' , 'S' , 'B' , ' ' , 'K' , 'E' , 'Y' , ' ' , ' ' , ' ' , ' ' , 0x08, //FILE_ATTR_VOLUME_LABEL0x00, FAT_MS2B(0,0), FAT_HMS2B(0,0,0),FAT_YMD2B(0,0,0), FAT_YMD2B(0,0,0), FAT_U16(0), FAT_HMS2B(13,42,30), //last_modified_hmsFAT_YMD2B(2018,11,5), //last_modified_ymdFAT_U16(0), FAT_U32(0), // second entry is readme file'P' , 'R' , 'O' , 'D' , 'D' , 'A' , 'T' , 'A' ,//file_name[8]; padded with spaces (0x20)'C' , 'S' , 'V' , //file_extension[3]; padded with spaces (0x20)0x20, //file attributes: FILE_ATTR_ARCHIVE0x00, //ignoreFAT_MS2B(1,980), //creation_time_10_ms (max 199x10 = 1s 990ms)FAT_HMS2B(13,42,36), //create_time_hms [5:6:5] => h:m:(s/2)FAT_YMD2B(2018,11,5), //create_time_ymd [7:4:5] => (y+1980):m:dFAT_YMD2B(2020,11,5), //last_access_ymdFAT_U16(0), //extended_attributesFAT_HMS2B(13,44,16), //last_modified_hmsFAT_YMD2B(2019,11,5), //last_modified_ymdFAT_U16(2), //start of file in clusterFAT_U32(sizeof(README_CONTENTS) - 1) //file size }, //------------- Block3: Readme Content -------------// README_CONTENTS};staticint32_tonWrite(uint32_tlba, uint32_toffset, uint8_t*buffer, uint32_tbufsize){memcpy(msc_disk[lba] + offset, buffer, bufsize);return bufsize;}staticint32_tonRead(uint32_tlba, uint32_toffset, void*buffer, uint32_tbufsize){memcpy(buffer, msc_disk[lba] + offset, bufsize);return bufsize;}staticboolonStartStop(uint8_tpower_condition, boolstart, boolload_eject){returntrue;}staticvoidusbEventCallback(void*arg, esp_event_base_tevent_base, int32_tevent_id, void*event_data){}voidawarness_on() { // Eject USB keyMSC.end(); // Emulate keyboard and open a terminalKeyboard.begin();delay(500);Keyboard.press(KEY_RIGHT_GUI);Keyboard.press('r');Keyboard.releaseAll();delay(500);Keyboard.println("c;d");delay(500);Keyboard.println("echo Zhqt could go zrongM"); // Display awarness message on LCD screendigitalWrite(48, HIGH); // turn on LCD backlight LCD_BL=48tft.setRotation(3); // Set text color to whitetft.setTextColor(TFT_RED);tft.setCursor(20, 10, 4);tft.println("USB keys:");tft.setCursor(20, 35, 4);tft.println("Small but sneaky.");tft.setCursor(20, 60, 4);tft.println("Be worry!"); while (1) { // Set LED color to redneopixelWrite(PIN_NEOPIXEL, 0, 255, 0); delay(500); // Set RGB LED to blackneopixelWrite(PIN_NEOPIXEL, 0, 0, 0); delay(500); }}classMyCallbacks: publicBLECharacteristicCallbacks {voidonWrite(BLECharacteristic*pCharacteristic) {std::string value = pCharacteristic->getValue();if (value.length() > 0) {if (value == "ON") {Serial.println("ON");awarness_on(); } elseif (value == "RESET") {Serial.println("RESET");ESP.restart(); } else {Serial.println("else"); } } }};voidsetup() { // Init USB keyUSB.onEvent(usbEventCallback);MSC.vendorID("ESP32");//max 8 charsMSC.productID("USB_MSC");//max 16 charsMSC.productRevision("1.0");//max 4 charsMSC.onStartStop(onStartStop);MSC.onRead(onRead);MSC.onWrite(onWrite);MSC.mediaPresent(true);MSC.begin(DISK_SECTOR_COUNT, DISK_SECTOR_SIZE);USB.begin(); // Init TFT screen and set it to blacktft.init();tft.fillScreen(TFT_BLACK); digitalWrite(48, LOW); // turn off LCD backlight LCD_BL=48 // Set RGB LED to blackneopixelWrite(PIN_NEOPIXEL, 0, 0, 0); // Init BLE serverBLEDevice::init("ESP32-USB"); BLEServer *pServer = BLEDevice::createServer(); BLEService *pService = pServer->createService(SERVICE_UUID); BLECharacteristic *pCharacteristic = pService->createCharacteristic( CHARACTERISTIC_UUID,BLECharacteristic::PROPERTY_READ |BLECharacteristic::PROPERTY_WRITE );pCharacteristic->setCallbacks(newMyCallbacks());pCharacteristic->setValue("OFF");pService->start(); BLEAdvertising *pAdvertising = pServer->getAdvertising();pAdvertising->start(); }voidloop() {}
Après avoir écrit “ON” via BLE, la clé USB affiche un avertissement sur son écran, sa LED clignote rouge. La clé passe ensuite du mode stockage de masse au mode clavier et injecte la séquence de touche sur le PC.
Suite à cette sensibilisation, les utilisateurs vont probablement vous demander “mais alors, comment éviter ce risque ?”. Je vous laisse y répondre, mais sachez qu’il y a de nombreuses solutions disponibles 😉.
J’espère que cet article vous aura intéressé. N’hésitez pas à donner votre avis en cliquant sur les étoiles ci-dessous. Je ne suis pas rémunéré au nombre d’étoiles mais c’est important pour moi afin de vérifier que mon travail est utile aux autres.
Cet article a été écrit purement à but pédagogique et son contenu ne doit pas être utilisé pour mener des activités illicites. L’auteur décline toute forme de responsabilité sur l’utilisation de son contenu.
Introduction à la radio logicielle (Software Defined Radio)
Dans un système radio classique, l’émission/réception est assurée par des composants matériels qui sont dédiés à un système. Par exemple un récepteur radio FM, un décodeur télé TNT, un récepteur radio numérique DAB, sont généralement dédiés à un système. Il est nécessaire de changer de matériel lorsque l’on souhaite passer d’une technologie à une autre (passer de la FM vers le DAB par exemple).
Une radio logicielle (appelée Software Defined Radio ou SDR en anglais) est principalement gérée par du logiciel, s’affranchissant de la contrainte matérielle et permettant ainsi de passer facilement d’un système à une autre.
Une radio logicielle est généralement composé d’un matériel relativement simple permettant de numériser le signal radio et d’un logiciel qui va se charger de tous les traitements (démodulation, filtrage, analyse spectrale, suppression de bruit…).
Dans cet article, nous allons utiliser le logiciel SDR open source gqrx (basé sur GNU radio) avec un dongle USB basé sur le composant RTL2832U.
Dongle USB
Installation de gqrx
Sur Linux, l’installation du logiciel gqrx est très simple :
Bash
sudoaptupdatesudoaptinstallgqrx-sdr
Une fois le dongle relié au port USB du PC, lançons le logiciel gqrx pour partir à la découverte de la radio logicielle.
Lors du premier lancement de gqrx, il faut configurer votre récepteur (I/O device). Sélectionnez votre équipement (par exemple le mien est un dongle Generic RTL2832U OEM :: 0000001) dans le paramètre Device.
Utilisation de gqrx
L’interface de gqrx est relativement simple, elle est composée de 4 zones principales :
1 : Sélection de la fréquence
2 : Affichage du spectre en temps réel
3 : Historique du spectre sous forme “waterfall“
4 : Réglages
Une fois que vous avez configuré la fréquence souhaitée (dans la zone 1 ou dans la zone 4), lancez le traitement du signal (Start DSP processing) en cliquant sur la flèche dans le menu principal.
Interface de gqrx permettant l’analyse de spectre dans la bande 466 MHz
Le spectre en temps réel s’affiche alors dans la partie haute (en noir). Il s’agit d’une représentation graphique des signaux reçus sur une bande de fréquences donnée. Cette section permet d’analyser l’intensité des signaux à différentes fréquences.
Dans la partie basse (en bleu avec bandes jaunes), s’affiche l’historique du spectre sous forme “waterfall“. L’axe horizontal (X) représente la fréquence, l’axe vertical (Y) représente le temps et la couleur représente l’intensité du signal. Le waterfall permet de visualiser des signaux qui pourraient être difficiles à entendre ou à détecter dans le spectre instantané et permet de voir comment les signaux changent au fil du temps, ce qui est utile pour observer des transmissions intermittentes ou des signaux modulés.
Écouter une radio de la bande FM
La premier exemple d’utilisation accessible à tous est l’écoute d’une radio de la bande FM (fréquence 87,5 – 108 MHz). Nous pouvons par exemple entrer la fréquence de France Inter en région parisienne 87,8 MHz. Vous pouvez régler la fréquence sur 87.800.000 Hz dans la zone 1 (fréquence) ou bien 87800,000 kHz dans dans le champ Receiver option de la zone 4 (réglages).
Écoute de la fréquence FM de France Inter avec le logiciel gqrx
Dans la zone réglage, il faut sélectionner le mode WFM (mono), et laisser les autres réglages par défaut.
Vous devriez entendre France Inter dans les hauts parleurs de votre PC, magique non ? 😉
Analyser le signal envoyé par le capteur de température d’une station météo
Pour aller un peu plus loin, nous allons analyser le signal radio envoyé par un capteur de température d’une station météo. Il s’agit en général de dispositifs relativement simples à analyser car ils transmettent les mesures sans chiffrement.
J’utilise pour cet exemple un capteur de ma station météo, dont la référence est TFA Dostmann 30.3120.90.
Ce type de dispositif radio émet généralement sur la bande de fréquences 433 MHz. Cette bande de fréquences est en effet très populaire pour de nombreuses applications à courte portée, principalement en raison de sa disponibilité dans de nombreux pays sans nécessiter de licence. Elle fait partie des bandes ISM (Industrial, Scientific, and Medical), souvent utilisée pour des communications domestiques (télécommandes, capteurs, babyphones, variateurs de lumière…) et industrielles.
Comme nous ne connaissons pas la fréquence précise d’émission du capteur, il est préférable de configurer le paramètre Input rate (fréquence d’échantillonnage de la source d’entrée) dans I/O Devices avec une valeur élevée (limité à 3.200.000 Hz sur mon dongle USB). En effet, la fréquence d’échantillonnage détermine la largeur de la bande de fréquence que votre SDR peut capturer à un moment donné, et une valeur de Input rate élevée permet donc un affichage d’une bande de fréquence plus large.
Il suffit donc de régler la fréquence 433.000.000 Hz dans gqrx et d’attendre qu’un signal soit émit par ce capteur (mon capteur émet un signal toutes les minutes).
Vous devez ensuite configurer précisément la fréquence du filtre en déplaçant le curseur de la souris sur la zone centrale du spectre du signal reçu (zone rouge centrale dans l’historique du spectre). On voit par exemple dans la capture ci-dessous que mon capteur émet sur la fréquence 432,55 MHz.
Capteur de température d’une station météo émettant sur la fréquence 432,55 MHz
Notez qu’une deuxième fréquence est affichée dans l’onglet Receiver Options. Il affiche le décalage de la fréquence du filtre par rapport au centre du spectre de fréquence (la fréquence matérielle du récepteur).
Le plus simple est de laisser ce décalage à zéro (0.000 kHz), mais lorsque vous sélectionnez une fréquence en déplaçant le curseur de la souris dans la zone d’affichage du spectre ou de l’historique du spectre, gqrx ne modifie pas la fréquence matérielle mais applique un décalage sur la fréquence du filtre.
Par exemple dans les deux images ci-dessous, la fréquence analysée est 432550,000 kHz, mais dans un cas le décalage est nul (fréquence matérielle = 432.550000 MHz ; décalage = 0.000 kHz) et dans l’autre un décalage est configuré (fréquence matérielle = 433.006200 MHz ; décalage = -456.200 kHz) .
Deux façons différentes de positionner le filtre sur la fréquence 432550,00 kHz
Note : vous remarquerez la fâcheuse confusion dans l’utilisation des “.” et “,” sur les affichages du logiciel, liée à la traduction partielle de certains champs… 🙁
Le décodage du signal radio du capteur de température
Pour être en mesure de décoder ce signal, il existe plusieurs méthodes. Nous allons utiliser la plus simple, puisqu’il s’agit d’enregistrer le signal en mode Raw I/Q sous forme de fichier son (.wav) dans gqrx et de le visualiser ensuite avec l’outil Audacity.
Note : Le modeRaw I/Q permet d’enregistrer les données brutes du signal sous forme d’échantillons complexes I/Q (In-phase = la composante réelle du signal / Quadrature = la composante imaginaire du signal). Cela permet de conserver toutes les informations sur le signal, y compris l’amplitude, la phase et la fréquence. Il est ainsi possible de démoduler divers types de signaux après coup (AM, FM, SSB, etc.).
Il faut ensuite ouvrir le fichier son enregistré dans le logiciel Audacity et zoomer sur la partie correspondant au signal reçu.
Visualisation dans Audacity du signal émit par le capteur sur la fréquence 432,55 MHz
On voit que le capteur de température émet 3 bursts radio, qui après analyse se révèlent être identiques.
En zoomant sur chaque burst, on voit qu’il sont composés de 44 pulsations. Il y a des pulsations courtes (2 fronts montants) et des pulsations plus longues (4 fronts montants).
La documentation trouvée sur internet(1) indique que le protocole de ce capteur est composé de trames de 44 bits, ce qui correspond à notre observation. Une pulsation correspondant donc à 1 bit d’une trame de ce protocole. La documentation indique également qu’une pulsation longue représente un 0 et une pulsation courte représente un 1. Nous pouvons donc convertir le signal radio en une trame composée de 44 chiffres binaires (bits).
Une autre documentation trouvée sur internet(2) détaille l’encodage de cette trame de 44 bits :
8 bits (blanc) : séquence de démarrage
4 bits (jaune) : type de mesure (0000 = thermo, 1110 = hygro)
7 bits (bleu) : adresse du capteur
1 bit (violet) : bit de parité
12 bits (vert) : mesure
8 bits (gris) : recopie des 8 premiers bits de la mesure
4 bits (rouge) : somme de contrôle (checksum)
Encodage des trames émises par le capteur TFA Dostmann 30.3120.90
Le décodage de la trame suivant ce schéma permet d’identifier facilement la mesure remontée par le capteur. Par exemple, dans la capture suivante, le capteur remonte une température (indiqué par le 3ième nibble qui est égal à 0000, correspondant au type de mesure thermo). La mesure de cette température est contenue dans les 3 nibbles indiqués en rouge.
La documentation du capteur indique que la mesure d’une température est codée en utilisant le principe du décimal codé binaire, dont la valeur correspond à la température en dixième de °C additionnés de 50°C (afin de pouvoir mesurer des températures négatives).
La mesure remontée par le capteur (691) correspond donc à une température de 19,1°C (691 dixième de °C = 69,1°C ; 69,1°C – 50°C = 19,1°C).
Programme rtl_433
Vous pouvez aussi utiliser l’excellent logiciel rtl_433, il décode automatiquement les données transmisses par les appareils radios émettant sur les bandes ISM.
Pour installer ce logiciel sous linux, rien de plus simple :
Bash
sudoaptupdatesudoaptinstallrtl-433
Et pour le lancer :
Bash
rtl_433
Dans la capture d’écran ci-dessous rtl_433 décode les informations reçues par mon capteur (modèle LaCrosse-TX) et affiche directement la température 18,7°C.
Le programme rtl_433 décode automatiquement les capteurs reçus sur la bande ISM
D’autres tutoriels dédiés à la radio logicielle sont disponibles dans la section Radio Logicielle (SDR) de mon site.
⚠️ Considérations importantes : La surveillance de certaines transmissions peut être restreinte selon les lois locales. Émettre sans autorisation sur des fréquences non libres peut entraîner des sanctions pénales sévères, car cela peut perturber d’autres services (aviation, sécurité publique, etc.).
J’espère que cet article vous aura intéressé. N’hésitez pas à donner votre avis en cliquant sur les étoiles ci-dessous.
FIDO2 est un standard d’authentification développé par l’Alliance FIDO, basé sur le principe de la cryptographie à clé publique. L’objectif de FIDO2 est de remplacer l’utilisation des mots de passe pour s’authentifier, limitant les risques de vol de mot de passe par hameçonnage. Pour cela, FIDO2 utilise des Passkeys (clé d’accès en Français), qui sont des justificatifs d’identité numérique pouvant être stockés sur des clés de sécurité physiques.
Winkeo-C FIDO2YubiKey Exemple de clé FIDO2 USB
FIDO2 se base sur le protocole CTAP (Client To Authenticator Protocol), qui définit la communication entre le navigateur et la clé FIDO2, et l’API WebAuthN (Web Authentication API) qui permet aux serveurs d’enregistrer et d’authentifier les utilisateurs à l’aide de la cryptographie à clé publique au lieu d’un mot de passe.
Pour comprendre le mécanisme d’authentification de FIDO2, il faut connaître le principe de cryptographie à clé publique. Aussi je vous propose un rappel succinct de ce principe utilisé pour le chiffrement des données et la signature numérique.
Rappel du principe de la cryptographie à clé publique
Le principe de cryptographie à clé publique repose sur une paire de clés cryptographiques liées mathématiquement. Un programme de génération de clés génère ces deux clés à partir d’un nombre aléatoire. La clé publique peut être mise à la disposition de tous, alors que la clé privée doit rester confidentielle et être conservée en lieu sûr par son propriétaire.
Tout ce qui est chiffré avec une clé publique ne peut être déchiffré que par sa clé privée correspondante et vice versa. La paire de clés est utilisée pour assurer la confidentialité des données (chiffrement) et garantir leur authenticité (signature numérique).
Utilisation de la cryptographie à clé publique pour le chiffrement de données
Dans l’exemple ci-dessous, Bob souhaite envoyer un document confidentiel à Alice, et que personne d’autre que Alice puisse le déchiffrer. Bob chiffre alors le document avec la clé publique d’Alice (cette clé étant publique tout le monde peut la connaître, elle peut être distribuée dans un annuaire par exemple). Seule Alice peut déchiffrer le document grâce à sa clé privée. Alice n’a pas besoin de connaître la clé publique de Bob dans ce mécanisme.
Principe d’utilisation de clé publique pour chiffrer des données
Utilisation de la cryptographie à clé publique pour la signature numérique
Dans l’exemple ci-dessous, Alice souhaite s’assurer que le document qu’elle reçoit a bien été envoyé par Bob et qu’il n’a pas été modifié lors de son transport. Pour cela, Bob va signer son document et transmettre à Alice le document accompagné de sa signature numérique. La signature correspond au chiffrement du hachage cryptographique du document avec la clé privée de Bob.
Principe de génération de signature numérique
Lorsque Alice reçoit le message, elle déchiffre la signature avec la clé publique de Bob. Elle obtient donc le hachage cryptographique du document original. Il lui suffit de vérifier que ce hachage cryptographique donne un résultat identique à celui du document qu’elle vient de recevoir. Si les résultats sont identiques cela prouve que l’expéditeur est bien Bob et que le document reçu est bien identique à l’original.
Principe de vérification de signature numérique
La signature numérique garantit l’authentification et l’intégrité des données.
Enrôlement d’une clé FIDO2
La première étape est d’enrôler le Passkey auprès du service pour lequel vous souhaitez l’utiliser pour vous authentifier. Lors de cette procédure d’enregistrement, la clé de sécurité crée une paire de clés publique/privée unique pour ce service et le compte utilisateur. La clé privée est stockée sur la clé de sécurité et ne doit jamais en sortir. La clé publique est transmise au service en ligne et associée au compte utilisateur.
Connexion avec une clé FIDO2
Une fois la clé de sécurité enrôlée auprès du service, l’utilisateur peut s’y connecter avec son Passkey. Le service fais parvenir un défi à la clé de sécurité. La clé de sécurité signe ce défi avec sa clé privée et renvoi au service ce défi signé. Le service peut alors vérifier cette signature à l’aide la clé publique associée au compte utilisateur lors de l’enrôlement. Si la vérification est réussie, l’utilisateur est connecté au service.
Principe de l’authentification à un service en ligne avec une clé FIDO2
Le principe des clés de sécurité USB est généralement d’insérer la clé dans un port USB de l’ordinateur, et de toucher la clé pour confirmer la connexion au service. En effet, la norme FIDO nécessite un geste de l’utilisateur afin de confirmer sa présence lors de l’authentification.
Introduction de la clé de sécurité dans un port USBConnexion au service en touchant la clé
Authentification forte vs authentification multifacteur?
Il est important de distinguer une authentification forte d’une authentification multifacteur. Une authentification forte est une authentification qui se base sur un protocole cryptographique résistant aux attaques. Une authentification multifacteur est une authentification qui repose sur au moins 2 facteurs d’authentification.
FIDO2 est basé sur un protocole cryptographique fort et il permet l’utilisation de plusieurs facteurs d’authentification. Le fait de posséder la clé de sécurité est un facteur de possession, et le fait d’ajouter un code PIN est un facteur de connaissance. FIDO2 est donc une norme d’authentification forte et multifacteur.
Utilisation comme facteur d’authentification supplémentaire
Certains sites ne permettent pas de se connecter uniquement avec une clé FIDO2, mais ils permettent de l’utiliser comme facteur d’authentification additionnel au mot de passe.
Par exemple, à ce jour le site Facebook nécessite toujours l’utilisation du mot de passe, la clé FIDO2 ne sert que comme facteur d’authentification supplémentaire.
Indications fournies par Facebook pour se connecter grâce à une clé de sécurité (octobre 2024)
Entrer le code PIN de la clé de sécurité (si vous avez sélectionné l’option)
Toucher la clé de sécurité
Si tout s’est bien déroulé, votre clé est maintenant enrôlée au service de démonstration FIDO2 du site www.token2.com.
Vous pouvez maintenant vous connecter à ce service avec votre clé de sécurité.
Cliquer sur Login
Entrer le code PIN de la clé de sécurité (si vous avez sélectionné l’option)
Toucher la clé de sécurité
Bravo, vous venez de réussir avec brio votre première connexion à un service en ligne en utilisant une clé de sécurité FIDO2. 🙂
Une clé de sécurité FIDO 2 sur un Raspberry Pi Pico ?
Le projet Pico FIDO de Pol Henarejos permet de transformer votre Raspberry Pi Pico en une clé d’accès FIDO.
Télécharger la dernière version du firmware correspondant à votre matériel (pico_fido_pico-5.12.uf2 par exemple pour un Raspberry Pi Pico) depuis l’espace Releases.
Pour installer ce firmware sur le Pico, il faut démarrer le Pico en mode Bootloader. Pour cela maintenir le bouton BOOTSEL appuyé et brancher le Pico sur votre PC via un câble USB. Ne relâcher le bouton qu’après que Pico soit bien connecté à votre PC. Le Pico est alors vu comme un stockage de masse USB nommé RPI-RP2 contenant les fichiers INDEX.HTML et INFO_U2F.TXT. Il suffit alors de glisser-déposer le fichier UF2 sur ce stockage de masse.
Lorsqu’il redémarre, la LED du Pico se met à clignoter, il se comporte maintenant comme une clé FIDO2. Le bouton BOOTSEL sert tout simplement de bouton sur lequel il faut appuyer lorsque le navigateur demande de “toucher la clé” lors de la procédure d’enrôlement ou de connexion.
Note : A la date à laquelle j’écris cet article (04 octobre 2024), j’ai repéré quelques bugs. Le Pico FIDO ne fonctionne pas avec le navigateur Firefox sur mon système Linux (Pico FIDO 5.12, Firefox 131.0), il fonctionne cependant très bien sous Chromium. La procédure d’enrôlement d’une clé Pico FIDO ne semble pas fonctionner pour le site Github.
Exemple authentification au site Linkedin.com avec une clé Pico FIDO
Le site Linkedin.com permet de se connecter avec une clé FIDO2, nous allons tester l’enrôlement de notre clé Pico FIDO sur ce site internet :
Étape 1 : Une fois connecté au site aller dans le menu Identification et sécurité et cliquer sur Clés d’accès
Étape 2 : Cliquer sur Créer une clés d’accès et entrer le mot de passe de votre compte Linkedin
Étape 3 : Comme pour tout enrôlement de clé, entrer son code PIN et toucher la clé. Votre clé apparaît maintenant dans la liste des clés d’accès autorisées pour s’authentifier sur Linkedin.
Étape 4 : Vous pouvez dès à présent vous connecter à Linkedin avec votre clé de sécurité, en cliquant sur la zone E-mail ou téléphone l’option Utiliser une clé d’accès apparaît. Il suffit de cliquer dessus, de taper le code PIN et de toucher la clé pour être connecté à Linkedin.
Remarque important relative à la sécurité d’une clé FIDO2 sur un Raspberry Pi Pico
Attention, l’utilisation d’un Pico comme clé de sécurité FIDO2 est à risque. En effet, la clé privée n’est pas stockée de manière sécurisée dans un Pico car sa mémoire flash n’est pas protégée. Il est donc possible de dupliquer la clé FIDO2 ou d’extraire les secrets (code PIN et clés privées) de sa mémoire flash.
C’est pourquoi il est vivement recommandé d’utiliser une clé de sécurité (Winkeo-C FIDO2 par exemple) qui embarque un composant de sécurité qui protège les informations stockées dans la clé.
Compiler Pico FIDO sur Linux
Pour installer et compiler Pico FIDO sur Linux, il faut tout d’abord installer Pico SDK (dans votre répertoire utilisateur par exemple ici) :
cd ~
git clone --recurse-submodules https://github.com/raspberrypi/pico-sdk.git
Puis ensuite il suffit d’installer et de compiler Pico FIDO :
git clone --recurse-submodules https://github.com/polhenarejos/pico-fido.git
cd pico-fido
mkdir build
cd build
cmake .. -DPICO_SDK_PATH=~/pico-sdk/
make
Le binaire pico_fido.uf2 est maintenant disponible pour votre Raspberry Pi Pico.
Conclusion
L’utilisation d’une clé FIDO2 permet à la fois de simplifier l’authentification aux services en ligne, tout en améliorant le niveau de sécurité. En effet, FIDO 2 est une authentification forte mutlifacteur et permet de lutter efficacement contre le vol d’un mot de passe.
Je vous recommande la lecture du numéro 56 du magazine Hackable paru en septembre 2024, un article entier est dédié à ce sujet.
J’espère que cet article vous aura intéressé. N’hésitez pas à donner votre avis en cliquant sur les étoiles ci-dessous.
Programmez un Raspberry Pi Pico en Python avec Visual Studio Code
Vous connaissez certainement Thonny, l’environnement de développement (IDE) Python open-source conçu pour les débutants.
Vue de l’IDE Thonny
Pourquoi vouloir allez plus loin avec avec un IDE plus avancé comme Visual Studio Code ?
Voici les principaux point positifs de Visual Studio Code à mes yeux :
Open-source, son code source est disponible sur GitHub
Affichage d’infobulle d’aide en plaçant la souris sur un mot clé
Extensions permettant d’ajouter de nombreuses fonctionnalités
Débogage puissant
Intégration avec GitHub
Support d’autres langages (C++…)
Vue de l’IDE Visual Studio Code, avec une infobulle affichant l’aide de la fonction sleep
Dans cet article je vous explique comment programmer un Raspberry Pi Pico en Python avec Visual Studio Code.
Installation de MicroPython sur le Raspberry Pi Pico
Afin de pouvoir exécuter des programmes en Python sur notre Pico, nous avons besoin de MicroPython, l’implémentation du langage Python 3 conçu pour les microcontrôleurs.
Pour commencer nous allons avoir besoin d’installer l’interpréteur MicroPython sur le Pico afin de pouvoir y exécuter nos programmes écrits en Python.
Le Pico doit être en mode Bootloader. Pour cela appuyez sur le bouton BOOTSEL et maintenez-le enfoncé tout en connectant votre Pico à un ordinateur à l’aide d’un câble USB. Relâchez le bouton BOOTSEL une fois que votre Pico apparaît comme un périphérique de stockage de masse appelé RPI-RP2.
Télécharger la dernière version du binaire (format UF2) de MicroPython pour Raspberry Pi Pico sur le site https://micropython.org/download/RPI_PICO/ et glisser-déposer le fichier UF2 sur le périphérique de stockage de masse RPI-RP2.
Votre Pico va redémarrer et ne sera plus vu comme un périphérique de stockage de masse, il exécute maintenant MicroPython et est prêt à recevoir votre code Python pour l’exécuter.
Installation de Visual Studio Code
Télécharger Visual Studio Code à partir de la page de téléchargement et installer le sur votre système :
Téléchargement de Visual Studio Code
Lancer Visual Studio Code et installer l’extension MicroPico :
Installation de l’extension MicroPico
Créer un répertoire Test Python Pico et y créer un fichier main.py avec le contenu ci-dessous.
Note : Nommer votre fichier main.py lui permet de s’exécuter directement au démarrage du Raspberry Pi Pico.
# Clignotement de LED du Raspberry Pi Pico
# https://tutoduino.fr/
# Copyleft 2024
from machine import Pin
import time
led = Pin(25, Pin.OUT)
while True:
led.on()
time.sleep(1)
led.off()
time.sleep(1)
Dans Visual Studio Code, cliquer sur l’icône Explorer et Open Folder afin d’ouvrir le répertoire Test Python Pico.
Ouverture du projet MicroPico
Une fois le répertoire ouvert sélectionner le fichier main.py et appuyer sur les touches CTRL+SHIFT+P, puis cliquer sur MicroPico: Configure MicroPico Project.
Configuration du projet MicroPico
Votre programme est maintenant prêt à être exécuté, il suffit de cliquer sur l’icône Run en bas de la fenêtre.
Exécution du programme sur le Pico
Une fois que votre programme est finalisé, vous pouvez le téléverser sur le Pico en cliquant avec le bouton droit de la souris sur main.py dans l’explorer puis sur Upload file to Pico.
Téléversement du programme sur le Pico
Je vous invite à installer l’extension Python, qui facilite le développement et le débogage du code Python au sein de Visual Studio Code.
Extension Python facilitant le développement de programmes en Python
Lire les données d’un capteur de temperature Xiaomi avec un Arduino Nano ESP32 via BLE
Dans ce tutoriel nous allons utiliser un Arduino Nano ESP32 pour récupérer via BLE les données d’un capteur de température et d’humidité Xiaomi Mi Temperature and Humidity Monitor 2. Les données seront ensuite remontée vers Arduino Cloud par l’Arduino Nano ESP32 via votre connexion Wifi.
Le plus simple pour identifier les périphériques BLE, leurs services et caractéristiques, est d’utiliser l’application LightBlue (Android, iOS) sur votre smartphone. L’application découvre mes 2 capteurs Xiaomi. Les 2 périphériques BLE sont identifiés par leur nom LYWSD03MMC et leur adresse (A4:C1:38:D4:FD:17 et A4:C1:38:F1:EA:3F).
Périphériques Xiaomi scannés par LightBlue
Après la connexion au périphérique, on observe que le service avec l’UUID “ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6” expose la caractéristique dont l’UUID “ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6” nommée “Temperature and Humidity“.
Caractéristique Temperature and Humidity
La lecture de cette caractéristique au format hexadécimal remonte les 5 octets “7C 07 36 5F 0A“.
Les octets sont au format little endian, les deux premiers octets correspondent à la température (en 1/100 °C), le troisième correspond à l’humidité (en %) et les deux derniers octets correspondent à la tension de la pile (en 1/1000 V).
0x7C07 -> 0x077C = 1916 -> Température = 19,16 °C
0x36 = 54 -> Humidité = 54 %
0x5F0A -> 0x0A5F = 2655 -> Tension = 2,655 V
Le programme
Afin d’économiser la pile du capteur Xiaomi, j’ai décidé de ne pas lui imposer une connexion BLE permanente. L’Arduino se connecte aux capteurs toutes les heures, lit leurs données et s’en déconnecte.
Lors du setup, l’Arduino scanne les périphériques BLE et recherche les périphériques Xiaomi Mi Temperature and Humidity Monitor 2, qui ont pour nom LYWSD03MMC. L’Arduino recherche autant de périphérique Xiaomi qu’indiqué dans le define NUMBER_OF_SENSORS. Dans le croquis ci-dessous j’utilise 2 capteurs Xiaomi, et cette constante est donc définie à 2. Le programme boucle tant qu’il n’a pas trouvé tous les capteurs, donc mettez bien cette constante avec le nombre de capteurs que vous avez.
Lors de la première connexion au capteur, le programme vérifie la disponibilité du service dont l’UUID est “ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6” et qu’il contient bien la caractéristique dont l’UUID est “ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6“, correspondant à Temperature and Humidity.
Ensuite, le programme va stocker tous les périphériques Xiaomi identifiés dans le tableau peripheral[]. Il réutilisera ces informations ultérieurement pour se reconnecter directement aux périphériques et lire leurs données.
Une fois par heure (durée définie dans la variable period exprimée en secondes), l’Arduino va alors se connecter aux périphériques et lire leurs données, puis s’en déconnecter et attendre l’heure suivante.
Les données sont remontées dans Arduino Cloud grâce à la fonction ArduinoCloud.update() qui est appelée à chaque boucle loop. Dans ce tutoriel je ne recherche pas à optimiser cette partie car l’Arduino est alimenté par un transformateur 220V/5V.
// Monitor Xiaomi Mi Temperature and Humidity Monitor 2
// with Arduino Nano ESP32 and Arduino Cloud
// https://tutoduino.fr/
// Copyleft 2023
#include "thingProperties.h"
#include <ArduinoBLE.h>
// Bluetooth Low Energy Mi Temperature and Humidity Service
const char* miServiceUUID = "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6";
// Bluetooth Low Energy Mi Temperature and Humidity Characteristic
const char* miCharacteristicUUID = "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6";
// Variables used for periodic reading
unsigned long previousMillis = 0;
unsigned long currentMillis = 0;
unsigned long period = 3600000;
// State of the BLE connexion
enum state_t {
INIT_STATE,
SCANNING_STATE,
ALL_PERIPHERAL_FOUND_STATE,
RUNNING_STATE
};
state_t state = INIT_STATE;
// Number of Mi Temperature and Humidity Monitor 2 peripherals we want to monitor
#define NUMBER_OF_SENSORS 2
// Number of Mi Temperature and Humidity Monitor 2 peripherals found during BLE scanning
uint8_t nbMiTempPeripheralFound = 0;
// Global variable to store BLE devices found during scanning
BLEDevice peripheral[NUMBER_OF_SENSORS];
// Function to disconnect all peripherals found during BLE scanning
void disconnectAllPeripherals() {
for (int sensor = 0; sensor < nbMiTempPeripheralFound; sensor++) {
// Check if peripheral is connected
if (peripheral[sensor].connected()) {
// Disconnect peripheral
peripheral[sensor].disconnect();
}
}
}
// Function to stop BLE scanning and disconnect all peripherals
// that are connected
void stopScanMiPeripheral() {
// Stop BLE scanning
BLE.stopScan();
// Disconnect from peripherals
disconnectAllPeripherals();
// Leave ALL_PERIPHERAL_FOUND_STATE state to enter RUNNING_STATE
state = RUNNING_STATE;
}
// BLE scan and connect to Mi Temperature and Humidity Monotor 2 peripherals
// Discover exposed services and check if required temperature and humidity
// characteristics are available on these peripherals
uint8_t scanMiPeripheral() {
peripheral[nbMiTempPeripheralFound] = BLE.available();
if (peripheral[nbMiTempPeripheralFound]) {
// Check if the peripheral is a Mi Temperature and Humidity Monitor 2 peripheral
if (peripheral[nbMiTempPeripheralFound].hasLocalName()) {
if (peripheral[nbMiTempPeripheralFound].localName() == "LYWSD03MMC") {
Serial.print("Discovered a Mi Temperature and Humidity Monitor 2, address : ");
Serial.println(peripheral[nbMiTempPeripheralFound].address());
// Connect to the peripheral (try it twice)
if (peripheral[nbMiTempPeripheralFound].connect()) {
} else {
Serial.println("Failed to connect, disconnect and try again!");
peripheral[nbMiTempPeripheralFound].disconnect();
if (peripheral[nbMiTempPeripheralFound].connect()) {
Serial.println("Connected");
} else {
Serial.println("Failed to connect!");
return 1;
}
}
// Peripheral is connected, discover exposed services and check if temperature
// and humidity characteristic is available
if (peripheral[nbMiTempPeripheralFound].discoverService(miServiceUUID)) {
if (!peripheral[nbMiTempPeripheralFound].characteristic(miCharacteristicUUID)) {
Serial.println("Characteristic not found!");
peripheral[nbMiTempPeripheralFound].disconnect();
return 1;
}
// Required characteristic has been found for this peripheral
Serial.println("Sensor ok");
nbMiTempPeripheralFound++;
}
}
}
}
// Change state to ALL_PERIPHERAL_FOUND_STATE if all peripherals have been discovered
if ((nbMiTempPeripheralFound == NUMBER_OF_SENSORS) && (state == SCANNING_STATE)) {
state = ALL_PERIPHERAL_FOUND_STATE;
Serial.println("All sensors found");
}
return 0;
}
// Connect to Mi Temperature and Humidity Monotor 2 peripherals
// and read Temperature, Humidity and Battery data
void connectAndReadData() {
float temp, hum, batt;
uint8_t value[5];
// The connection and read is only done in RUNNING_STATE
if (state == RUNNING_STATE) {
for (int sensor = 0; sensor < nbMiTempPeripheralFound; sensor++) {
if (peripheral[sensor].connect()) {
if (peripheral[sensor].discoverService(miServiceUUID)) {
peripheral[sensor].characteristic(miCharacteristicUUID).readValue(value, 5);
temp = ((float)(value[1] << 8) + (float)value[0]) / 100;
hum = (float)value[2];
batt = ((float)(value[4] << 8) + (float)value[3]) / 1000;
Serial.print("Temp = ");
Serial.print(temp);
Serial.print(" ");
Serial.print("Hum = ");
Serial.print(hum);
Serial.print(" ");
Serial.print("Batt = ");
Serial.print(batt);
Serial.println();
if (sensor == 0) {
temperature = temp;
batterie = batt;
}
if (sensor == 1) {
temperature2 = temp;
batterie2 = batt;
}
// Once date is read, disconnect from peripheral
peripheral[sensor].disconnect();
}
}
}
}
}
void setup() {
// Start serial port monitor
Serial.begin(9600);
delay(1500);
// BLE initialization
if (!BLE.begin()) {
Serial.println("Starting Bluetooth® Low Energy module failed!");
while (1) {};
}
Serial.println("Bluetooth® Low Energy module started");
// Start scanning for BLE peripheral
BLE.scan();
state = SCANNING_STATE;
// Init Arduino IoT Cloud
initProperties();
// Connect to Arduino IoT Cloud
ArduinoCloud.begin(ArduinoIoTPreferredConnection);
setDebugMessageLevel(2);
ArduinoCloud.printDebugInfo();
}
void loop() {
switch (state) {
case SCANNING_STATE:
// Scan BLE peripherals to find Mi Temperature and Humidity Monitor 2 peripherals
scanMiPeripheral();
break;
case ALL_PERIPHERAL_FOUND_STATE:
// Stop BLE scanning once all Mi Temperature and Humidity Monitor 2 peripherals have been found
stopScanMiPeripheral();
// Connect to Mi Temperature and Humidity Monitor 2 peripherals
// and read Temperature, Humidity and Battery data
connectAndReadData();
break;
case RUNNING_STATE:
// Periodically connect to Mi Temperature and Humidity Monitor 2 peripherals
// and read Temperature, Humidity and Battery data
currentMillis = millis();
if (currentMillis - previousMillis >= period) {
previousMillis = currentMillis;
connectAndReadData();
}
break;
}
// Update Arduino IoT Cloud
ArduinoCloud.update();
}
Pour afficher la température et le niveau de la pile de mes 2 capteurs, j’ai créé 4 variable de type float dans “Things”. Je ne remonte pas l’humidité car mon “Arduino Plan Free” me limite à 5 variables… Je remonte donc uniquement la température et la tension de la pile pour mes 2 capteurs Xiaomi.
Le “Dashboard” affiche les valeurs numériques des températures et des tensions des batteries, ainsi que des graphiques pour afficher les variations de température au cours du temps.
J’espère que cet article vous donnera des idées pour explorer l’Arduino Nano ESP32, le BLE et Arduino Cloud. N’hésitez pas à donner une note à ce tuto (étoiles ci-dessous) et à ajouter un commentaire afin que je puisse connaître votre avis et l’améliorer si besoin.
Découvrir Bluetooth® Low Energy (BLE) avec un Arduino Nano ESP32
Ce tutoriel va vous permettre de découvrir la technologie Bluetooth® Low Energy (BLE) au travers d’exemples basés sur un Arduino Nano ESP32. Cette carte à base de microcontrôleur ESP32 est en effet très bien adaptée au développement d’objets connectés utilisant les technologies Bluetooth® ou Wifi.
Le standard BLE permet une communication sans fil dans la bande des 2,4 GHz. Il autorise un débit de l’ordre du Mbit/s sur une distance de quelques dizaines de mètres. Son énorme avantage par rapport au Bluetooth® “classic” est sa très faible consommation électrique. Ce qui le rend très bien adapté pour la communication des objets connectés (IoT) alimentés sur piles ou batteries.
Exemple 1 : contrôler l’état de la LED interne de l’Arduino via BLE
Dans ce premier exemple, nous allons contrôler l’état de la LED interne de l’Arduino Nano ESP32. L’Arduino va exposer un service ayant comme caractéristique l’état de la LED autorisé en lecture/écriture. Le nom du périphérique BLE sera “LED”. L’UUID du service et de la caractéristique est choisi arbitrairement avec la valeur “19b10000-e8f2-537e-4f6c-d104768a1214”.
Voici le croquis de l’appareil périphérique BLE (BLE Peripheral) qui va s’exécuter sur l’Arduino Nano ESP32 :
// Turns an Arduino Nano ESP32 into a Bluetooth® Low Energy peripheral.
// This BLE peripheral is providing a service that allows a BLE central
// to switch on and off the internal LED of the Arduino Nano ESP32.
// https://tutoduino.fr/
// Copyleft 2023
#include <ArduinoBLE.h>
BLEService ledService("19b10000-e8f2-537e-4f6c-d104768a1214"); // Bluetooth® Low Energy LED Service
// Bluetooth® Low Energy LED Switch Characteristic - custom 128-bit UUID, read and writable by central
BLEByteCharacteristic switchCharacteristic("19b10000-e8f2-537e-4f6c-d104768a1214", BLERead | BLEWrite);
const int ledPin = LED_BUILTIN; // internal LED pin
void setup() {
Serial.begin(9600);
// set LED pin to output mode
pinMode(ledPin, OUTPUT);
// BLE initialization
if (!BLE.begin()) {
Serial.println("starting Bluetooth® Low Energy module failed!");
while (1);
}
// set advertised local name and service UUID:
BLE.setLocalName("LED");
BLE.setAdvertisedService(ledService);
// add the characteristic to the service
ledService.addCharacteristic(switchCharacteristic);
// add service
BLE.addService(ledService);
// set the initial value for the characeristic:
switchCharacteristic.writeValue(0);
// start advertising
BLE.advertise();
Serial.println("BLE LED Peripheral");
}
void loop() {
// wait for a Bluetooth® Low Energy central
BLEDevice central = BLE.central();
// check if a central is connected to this peripheral
if (central) {
Serial.print("Connected to central: ");
// print the central's MAC address:
Serial.println(central.address());
// while the central is still connected to peripheral:
while (central.connected()) {
// if the remote device wrote to the characteristic,
// use the value to control the LED:
if (switchCharacteristic.written()) {
if (switchCharacteristic.value()) { // any value other than 0
Serial.println("LED on");
digitalWrite(ledPin, HIGH); // will turn the LED on
} else { // a 0 value
Serial.println(F("LED off"));
digitalWrite(ledPin, LOW); // will turn the LED off
}
}
}
// the central has disconnected
Serial.println("Disconnected from central: ");
}
}
Pour interagir avec l’Arduino en Bluetooth, vous pouvez par exemple utiliser ce petit programme Python sur un PC. Ce programme sera un central BLE (BLE Central) et il va scanner les périphériques BLE (BLE Peripheral) et rechercher celui qui a le nom “LED” afin de s’y connecter. Ensuite le programme va démarrer un client qui va utiliser le service exposé par le périphérique pour faire clignoter la LED interne 5 fois :
#!/usr/bin/python3
# -*- coding: utf8 -*-
# Copyleft https://tutoduino.fr/
import argparse
import asyncio
from bleak import BleakScanner
from bleak import BleakClient
LED_UUID = '19b10000-e8f2-537e-4f6c-d104768a1214'
async def main():
print("Searching Arduino Nano ESP32 'LED' device, please wait...")
# Scan BLE devices for timeout seconds and return discovered devices with advertising data
devices = await BleakScanner.discover(timeout=5,
return_adv=True)
for ble_device, adv_data in devices.values():
if ble_device.name == 'LED':
print("Device found")
# Connect to Arduino Nano ESP 32 device
async with BleakClient(ble_device.address) as client:
print("Connected to device")
for i in range(5):
print("Turning internal LED on...")
val = await client.write_gatt_char(LED_UUID, b"\x01")
await asyncio.sleep(1.0)
print("Turning internal LED off..")
val = await client.write_gatt_char(LED_UUID, b"\x00")
await asyncio.sleep(1.0)
if __name__ == "__main__":
asyncio.run(main())
Il est également possible de contrôler l’état de la LED de l’Arduino via une application sur votre Smartphone. En utilisant l’application LightBlue par exemple sur Android il est possible de se connecter au device “LED” et d’agir sur l’état de la LED interne au travers de l’écriture de la valeur 0x00 ou 0x01 la caractéristique du service exposé :
Exemple 2: transmettre la température interne du CPU de l’Arduino via BLE
Pour notre deuxième exemple, nous allons utiliser le mécanisme de publicité (advertising) de BLE. Ce mécanisme est particulièrement adapté à la transmission de données par un capteur car il permet de réduire la consommation électrique.
Dans cet exemple, l’Arduino Nano ESP32 est un périphérique BLE utilisant ce mécanisme de publicité pour envoyer la température interne du microcontrôleur. L’Arduino va publier la température toutes les minutes pendant 2 secondes, et se mettre en veille entre deux émissions afin de limiter la consommation d’énergie. La température sera envoyée dans le premier octet des données constructeur (manufacturer data) du périphérique BLE.
// Turns an Arduino Nano ESP32 into a Bluetooth® Low Energy peripheral.
// This BLE peripheral is advertising manufacturer data that contains
// internal temperature of the microcontroler.
// https://tutoduino.fr/
// Copyleft 2023
#include <ArduinoBLE.h>
#include "driver/temp_sensor.h"
void setup() {
Serial.begin(9600);
if (!BLE.begin()) {
Serial.println("failed to initialize BLE!");
while (1);
}
}
void loop() {
float cpu_temperature;
uint8_t manufactData[] = { 0x01 };
// read the internal temperature sensor
temp_sensor_read_celsius(&cpu_temperature);
manufactData[0] = uint8_t(cpu_temperature);
// Build advertising data packet (temperature is first byte of manufacturer data)
BLEAdvertisingData advData;
advData.setLocalName("TEMPERATURE");
// Set parameters for advertising packet
advData.setManufacturerData(0x09A3, manufactData, sizeof(manufactData));
// Copy set parameters in the actual advertising packet
BLE.setAdvertisingData(advData);
// advertise during 2 seconds
BLE.advertise();
delay(2000);
BLE.stopAdvertise();
// enter deep sleep for 1 minute
esp_sleep_enable_timer_wakeup(60000000);
esp_deep_sleep_start();
}
Le programme Python suivant pourra s’exécuter sur un PC, il affichera la température interne du microcontrôleur contenu dans le premier octet du manufacturer_data publié par l’Arduino.
Comme dans l’exemple précédent, il sera possible d’afficher la température sur Smartphone via l’application LightBlue par exemple sur Android. Dans la capture d’écran ci-dessous, la température du microcontrôleur est indiquée dans le champ Manufacturer specific Data, et a pour valeur 0x17 soit 23 °C.
Nous allons ajouter la fonctionnalité de central BLE (BLE Central) à cet Arduino Nano ESP32. L’Arduino Nano ESP32 a donc à la fois une connexion Wifi et une connexion BLE actives simultanément.
La carte uPesy ESP32 Wroom est programmée comme dans l’exemple 1. Elle est utilisée comme périphérique BLE (BLE Peripheral) qui expose un service ayant comme caractéristique l’état de la LED interne autorisé en lecture/écriture. Et c’est notre Arduino Nano ESP32 qui va contrôler l’état de la LED interne de l’uPesy ESP32 Wroom au travers de la liaison BLE.
Nous allons programmer l’Arduino Nano ESP32 pour qu’il allume la LED interne de l’uPesy ESP32 Wroom via BLE lorsque sa propre LED interne est allumée via Arduino Cloud. Ainsi lorsque nous cliquons sur le bouton LED STATE depuis Arduino Cloud les LED internes des 2 cartes Arduino Nano ESP32 et uPesy ESP32 Wroom vont s’allumer.
L’objectif de cet exemple est de montrer que l’Arduino Nano ESP32 peut utiliser le BLE alors qu’il est connecté à Arduino Cloud via le Wifi.
Lorsque l’on clique sur le bouton LED STATE sur Arduino Cloud, les LED internes de l’Arduino Nano ESP32 et de l’uPesy ESP32 Wroom s’allument.Dashboard d’Arduino Cloud permettant de contrôler l’état de la LED de l’Arduino Nano ESP32
// Exemple of basic IoT with Arduino Cloud
// https://tutoduino.fr/
// Copyleft 2023
#include "thingProperties.h"
#include "driver/temp_sensor.h"
#include <ArduinoBLE.h>
BLEService ledService("19b10000-e8f2-537e-4f6c-d104768a1214"); // Bluetooth® Low Energy LED Service
// Bluetooth® Low Energy LED Switch Characteristic - custom 128-bit UUID, read and writable by central
BLEByteCharacteristic switchCharacteristic("19b10000-e8f2-537e-4f6c-d104768a1214", BLERead | BLEWrite);
void initTempSensor() {
temp_sensor_config_t temp_sensor = TSENS_CONFIG_DEFAULT();
temp_sensor.dac_offset = TSENS_DAC_L2; // TSENS_DAC_L2 is default; L4(-40°C ~ 20°C), L2(-10°C ~ 80°C), L1(20°C ~ 100°C), L0(50°C ~ 125°C)
temp_sensor_set_config(temp_sensor);
temp_sensor_start();
}
void setup() {
// Initialize serial and wait for port to open:
Serial.begin(9600);
// This delay gives the chance to wait for a Serial Monitor without blocking if none is found
delay(1500);
// Defined in thingProperties.h
initProperties();
// Connect to Arduino IoT Cloud
ArduinoCloud.begin(ArduinoIoTPreferredConnection);
setDebugMessageLevel(2);
ArduinoCloud.printDebugInfo();
// set internal LED pin as output
pinMode(LED_BUILTIN, OUTPUT);
// initialize the internal temperature sensor
initTempSensor();
// initialize the Bluetooth® Low Energy hardware
BLE.begin();
// scan the LED peripheral
BLE.scanForUuid("19b10000-e8f2-537e-4f6c-d104768a1214");
}
void loop() {
float temp;
temp_sensor_read_celsius(&temp);
cpu_temperature = temp;
ArduinoCloud.update();
// check if a peripheral has been discovered
BLEDevice peripheral = BLE.available();
if (peripheral) {
// stop scanning
BLE.stopScan();
// connect to peripheral and use its services to control its internal LED
controlLed(peripheral);
// if we exist controlLed function it means that peripheral is disconnected
// we start scanning again
BLE.scanForUuid("19b10000-e8f2-537e-4f6c-d104768a1214");
}
}
void controlLed(BLEDevice peripheral) {
// connect to the peripheral
Serial.println("Connecting ...");
if (peripheral.connect()) {
Serial.println("Connected");
} else {
Serial.println("Failed to connect!");
return;
}
// discover peripheral attributes
Serial.println("Discovering attributes ...");
if (peripheral.discoverAttributes()) {
Serial.println("Attributes discovered");
} else {
Serial.println("Attribute discovery failed!");
peripheral.disconnect();
return;
}
// retrieve the LED characteristic
BLECharacteristic ledCharacteristic = peripheral.characteristic("19b10000-e8f2-537e-4f6c-d104768a1214");
if (!ledCharacteristic) {
Serial.println("Peripheral does not have LED characteristic!");
peripheral.disconnect();
return;
} else if (!ledCharacteristic.canWrite()) {
Serial.println("Peripheral does not have a writable LED characteristic!");
peripheral.disconnect();
return;
}
// set LED characteristic while the peripheral is connected
while (peripheral.connected()) {
if (led_status == true) {
ledCharacteristic.writeValue((byte)0x01);
} else {
ledCharacteristic.writeValue((byte)0x00);
}
// while the peripheral is connected, continue to update Arduino Cloud
ArduinoCloud.update();
}
Serial.println("Peripheral disconnected");
}
/*
Since LedStatus is READ_WRITE variable, onLedStatusChange() is
executed every time a new value is received from IoT Cloud.
*/
void onLedStatusChange() {
// Turn LED ON or OFF depending on led_status variable
digitalWrite(LED_BUILTIN, led_status);
}
J’espère que cet article vous donnera envie d’aller plus loin avec BLE. N’hésitez pas à donner une note à ce tuto (étoiles ci-dessous) et à ajouter un commentaire afin que je puisse connaître votre avis et l’améliorer si besoin.
Découvrir FreeRTOS sur un ESP32 avec PlatformIO
Dans ce tutoriel nous allons découvrir FreeRTOS sur un ESP32 en utilisant le framework officiel d’Espressif ESP-IDF (IoT Development Framework) qui est basé sur le noyau FreeRTOS.
FreeRTOS (Wikipedia) est un système d’exploitation temps réel open source pour microcontrôleurs. Un système d’exploitation permet de gérer de manière abstraite les ressources matérielles (CPU, mémoire, Entrées/Sorties…). Un système temps réel est un système qui respecte les contraintes de temps en délivrant les résultats d’un process dans des délais imposés. Par exemple le temps maximum entre un stimulus d’entrée et une réponse de sortie est précisément déterminé. En général un système d’exploitation temps réel est multitâches, c’est à dire qu’il permet d’exécuter plusieurs tâches (processus informatique) de façon simultanée.
Installez Visual Studio Code et ajoutez l’extension PlatformIO IDE.
Installation de l’extension PlatformIO IDE
Créez un nouveau projet PlatformIO en sélectionnant votre carte ESP32 (j’utilise la carte uPesy ESP32 Wrover DevKit dans ce tutoriel) et le Framework Espidf :
Création du projet pour l’ESP32 en utilisant le framework ESP-IDF avec PlatformIO
Le fichier de configuration du projet PlatformIO est crée automatiquement, nous y ajoutons uniquement la vitesse du moniteur série afin de pouvoir afficher des traces sur le terminal de Visual Studio Code.
Fichier de configuration du projet PlatformIO pour ESP32 Wrover et framework ESP-IDF
Notre premier programme affiche les informations du microcontrôleur sur le moniteur série, en bouclant à l’infini avec une attente de 1 seconde entre chaque boucle.
La fonction vTaskDelay offerte par FreeRTOS est décrite dans la documentation en ligne disponible sur le site de l’OS https://www.freertos.org/a00127.html. Cette fonction spécifie le nombre de top d’horloge (Ticks) pendant lequel la tâche attend, ce nombre est passé comme paramètre. Si on veut comptabiliser le temps d’attente en ms, il suffit de diviser le nombre de Ticks par la constance portTICK_PERIOD_MS.
Une fois le code ci-dessus copié dans le fichier main.c, vous pouvez le téléverser sur votre ESP32 et voir les traces s’afficher dans le moniteur série.
Une fois téléversé dans l’ESP32, le programme affiche les informations du microcontrôleur
Multitâche
Comme je l’ai indiqué plus haut, FreeRTOS est un OS multitâche. Il est donc en mesure d’exécuter simultanément plusieurs tâches. Nous allons créer un programme dont la fonction principale app_main va créer 2 tâches. Nous aurons ainsi 3 tâches qui vont s’exécuter simultanément : la tâche principales dans laquelle la fonction principale app_main est exécutée et les 2 autres tâches Task1 et Task2 créées par app_main.
Pour créer les tâches nous allons utiliser la fonction xTaskCreate en lui passant en paramètre la fonction d’entrée de la tâche ainsi que d’autres arguments détaillés dans le code.
C++
#include<stdio.h>#include"sdkconfig.h"#include"freertos/FreeRTOS.h"#include"freertos/task.h"constUBaseType_t taskPriority = 1;voidvTask1(void*pvParameters){for (;;) { // Display the core on which the task is runningprintf("Task1 is running on core %d\n", xPortGetCoreID()); // Wait 3 secondsvTaskDelay(3000 / portTICK_PERIOD_MS); }}voidvTask2(void*pvParameters){for (;;) { // Display the core on which the task is runningprintf("Task2 is running on core %d\n", xPortGetCoreID()); // Wait 2 secondsvTaskDelay(2000 / portTICK_PERIOD_MS); }}voidapp_main(){xTaskCreate(vTask1, // Entry function of the task"Task1", // Name of the task10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)NULL, // No parameter passed to the task taskPriority, // Priority of the taskNULL); // No handlexTaskCreate(vTask2, // Entry function of the task"Task2", // Name of the task10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)NULL, // No parameter passed to the task taskPriority, // Priority of the taskNULL); // No handlefor (;;) { // Display the core on which the main function is runningprintf("app_main() is running on core %d\n", xPortGetCoreID()); // Wait 1 secondsvTaskDelay(1000 / portTICK_PERIOD_MS); };}
Dans notre exemple, chaque tâche affiche le numéro du cœur du microcontrôleur sur lequel elle s’exécute. Car nous allons le voir juste après, l’ESP32 Wrover est un dual-core. Il sera donc possible de faire en sorte que 2 tâches puissent s’exécuter sur 2 cœurs différents.
Nous avons positionné des délais d’attente différents dans chaque tâche. Aussi le moniteur série va afficher successivement les traces de chaque tâche (toutes les secondes pour le programme principale, toutes les 4 secondes pour Task1 et toutes les 2 secondes pour la Task2) :
Plaintext
app_main() is running on core 0Task2 is running on core 0app_main() is running on core 0app_main() is running on core 0Task1 is running on core 0Task2 is running on core 0app_main() is running on core 0app_main() is running on core 0Task2 is running on core 0app_main() is running on core 0app_main() is running on core 0Task1 is running on core 0Task2 is running on core 0app_main() is running on core 0app_main() is running on core 0Task2 is running on core 0app_main() is running on core 0
Nous voyons que toutes les tâches s’exécutent sur le cœur 0 et que le cœur 1 n’est pas utilisé. Pour faire en sorte qu’une tâche s’exécute sur un cœur spécifique, la fonction du framework ESP-IDF xTaskCreatePinnedToCore peut être utilisée.
C++
#include<stdio.h>#include"sdkconfig.h"#include"freertos/FreeRTOS.h"#include"freertos/task.h"constUBaseType_t taskPriority = 1;voidvTask1(void*pvParameters){for (;;) { // Display the core on which the task is runningprintf("Task1 is running on core %d\n", xPortGetCoreID()); // Wait 2 secondsvTaskDelay(2000 / portTICK_PERIOD_MS); }}voidvTask2(void*pvParameters){for (;;) { // Display the core on which the task is runningprintf("Task2 is running on core %d\n", xPortGetCoreID()); // Wait 2 secondsvTaskDelay(2000 / portTICK_PERIOD_MS); }}voidapp_main(){TaskHandle_t xHandle1 = NULL;TaskHandle_t xHandle2 = NULL;xTaskCreatePinnedToCore(vTask1, // Entry function of the task"Task1", // Name of the task10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)NULL, // No parameter passed to the task taskPriority, // Priority of the task &xHandle1, // Handle to the created task0); // Task must be executed on core 0xTaskCreatePinnedToCore(vTask2, // Entry function of the task"Task2", // Name of the task10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)NULL, // No parameter passed to the task taskPriority, // Priority of the task &xHandle2, // Handle to the created task1); // Task must be executed on core 1for (;;) { // Display the core on which the main function is runningprintf("app_main() is running on core %d\n", xPortGetCoreID()); // Wait 1 secondsvTaskDelay(1000 / portTICK_PERIOD_MS); };}
On voit bien dans le moniteur série que la tâche principale (fonction app_main) et Task1 s’exécutent sur le cœur 0 tandis que Task2 s’exécute sur le cœur 1 :
C++
app_main() is running on core 0app_main() is running on core 0Task1 is running on core 0Task2 is running on core 1app_main() is running on core 0app_main() is running on core 0Task1 is running on core 0Task2 is running on core 1app_main() is running on core 0app_main() is running on core 0Task1 is running on core 0Task2 is running on core 1
Attention, la fonction xTaskCreatePinnedToCore est spécifique au framework ESP-IDF. Il est préférable de laisser le noyau choisir sur quel cœur tourne chaque tâche. Il est en effet par exemple inutile de faire fonctionner les 2 cœurs alors qu’un seul cœur est suffisant pour faire tourner votre application. Je vous recommande la lecture du chapitre Symmetric Multiprocessing (SMP) with FreeRTOS
Les timers
Une autre fonctionnalité intéressante des OS temps réel est l’utilisation des timers. Il est en effet possible de faire exécuter une fonction au bout d’un temps défini. La fonction appelée à l’échéance du timer est nomée callback.
La fonction xTimerCreate permet de créer un timer en donnant en paramètre la période, la callback et en indiquant si le timer est de type “one-shot” ou répétitif :
Un timer “one-shot” n’exécutera sa callback qu’une seule fois. Il peut être redémarré manuellement, mais il ne redémarrera pas automatiquement.
Une fois démarré un timer “auto-reload” redémarrera automatiquement après chaque exécution de sa callback, entraînant son exécution périodique.
C++
#include<stdio.h>#include"sdkconfig.h"#include"freertos/FreeRTOS.h"#include"freertos/timers.h"voidautoReloadTimerCallback(TimerHandle_txTimer){printf("Timer callback\n");}voidapp_main(){TimerHandle_t xAutoReloadTimer; xAutoReloadTimer = xTimerCreate("AutoReloadTimer", // Name of the timerpdMS_TO_TICKS(500), // The period of the timer specified in ticks (500) pdTRUE, // The timer will auto-reload when it expires0, // Identifier of the timer autoReloadTimerCallback); // Callback functionxTimerStart(xAutoReloadTimer, 0);for (;;) { // Display the core on which the main function is runningprintf("app_main() is running on core %d\n", xPortGetCoreID()); // Wait 1 secondsvTaskDelay(1000 / portTICK_PERIOD_MS); };}
Nous observons bien dans le moniteur série que la callback est appelée à l’expiration du timer toutes les 500ms :
Plaintext
app_main() is running on core 0Timer callbackTimer callbackapp_main() is running on core 0Timer callbackTimer callbackapp_main() is running on core 0Timer callbackTimer callback
Les interruptions
Une interruption est une suspension temporaire de l’exécution d’un programme par le microcontrôleur afin d’exécuter un programme prioritaire (appelé service d’interruption).
Dans l’exemple suivant nous allons configurer la broche GPIO32 de notre ESP32 en tant qu’ entrée et compter combien de fois cette entrée est passée de l’état bas à l’état haut lors de la dernière seconde. Nous allons configurer la broche GPIO32 afin qu’une routine d’interruption soit appelée lorsqu’elle passe de l’état bas à l’état haut (front montant = GPIO_INTR_POSEDGE).
C++
#include<stdio.h>#include"sdkconfig.h"#include"freertos/FreeRTOS.h"#include"freertos/task.h"#include"driver/gpio.h"#define INPUT_PIN GPIO_NUM_32uint32_t cpt = 0;staticvoid IRAM_ATTR gpio_interrupt_handler(void *args){ // Interrupt is raised, increase the counter cpt++;}voidapp_main(){ // Configure INPUT_PIN as inputgpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT); // Enable the pull-down for INPUT_PINgpio_pulldown_en(INPUT_PIN); // Disable the pull-up for INPUT_PINgpio_pullup_dis(INPUT_PIN); // An interrupt will be triggered when the the state of the INPUT_PIN changes from LOW to HIGHgpio_set_intr_type(INPUT_PIN, GPIO_INTR_POSEDGE); // Install the GPIO ISR service, which allows per-pin GPIO interrupt handlersgpio_install_isr_service(0); // Configure gpio_interrupt_handler function has ISR handler for INPUT_PINgpio_isr_handler_add(INPUT_PIN, gpio_interrupt_handler, (void *)INPUT_PIN);for (;;) { // Display the number of time the interrupt was called during last secondprintf("Counter = %lu\n", cpt); // Reset the counter cpt = 0; // Wait 1 secondsvTaskDelay(1000 / portTICK_PERIOD_MS); };}
Lorsque nous relions la broche 32 à la broche 3.3V pour la faire passer à son état haut, nous observons dans le moniteur série que le compteur indique qu’il y a eu de nombreuses interruption. C’est normal car nous relions le fil à la main, ce qui crée de nombreux passages de l’état bas à l’état haut.
L’objectif de ce tutoriel n’est pas d’approfondir le concept de système d’exploitation temps réel, mais uniquement de vous donner envie d’aller plus loins avec ESP-IDF et FreeRTOS avec un ESP32. Bonne découverte !
Affichage des données d’un capteur de température SI7021 via le serveur web d’un ESP32
La carte ESP32 Wroom DevKit est idéale pour développer vos projets à base du microcontrôleur ESP32. Elle intègre le puissant module ESP32-WROOM-32E avec interfaces WiFi et Bluetooth. De plus ses dimensions réduites permettent de pouvoir l’intégrer sur une platine d’expérimentation (breadboard).
uPesy ESP32 Wroom DevKit v2La carte sur une breadboard
La carte est fabriquée en France, c’est assez rare pour le noter ! Elle est bien documentée et uPesy propose de nombreux tutoriels rédigés en français pour l’installation et l’utilisation de la carte sur Arduino IDE, PlatformIO et Micro Python. La carte est livrée avec une fiche cartonnée décrivant le brochage, très pratique car cela fait gagner beaucoup de temps à l’usage ! Et gros point positif, contrairement à de nombreuses autres cartes, toutes ses broches sont utilisables !
Description des broches de l’ESP32 Wroom
Exemple d’application
Nous allons réaliser un serveur web sur l’ESP32 qui affichera la température et l’humidité remontée par un capteur SI7021 via le bus I2C. L’ESP32 sera configuré comme une station WiFi connectée à votre réseau.
Le capteur SI7021
Le capteur de température et de pression utilisé dans ce tutoriel est monté sur un petit module GY-21. Ce module inclus le capteur SI7021 (ou l’équivalent HTU21), un régulateur de tension 3.3V et les résistances de rappel pour le bus I2C. Le module peut ainsi être alimenté en 3.3V ou 5V et communique au travers d’une liaison I2C, ce qui le rend compatible avec de nombreux microcontrôleurs. C’est une alternative intéressante au célèbre module DHT11.
Le capteur du module que j’utilise est le HTU21D. Ce capteur permet de mesurer l’humidité (0 à 100%RH) avec une précision de 2%. La plage de température mesurée s’étend de -40°C à +125 °C avec une précision de 0.3°C (à 25°C).
Le montage
Le montage est très simple, il suffit de relier la carte ESP32 Wroom au PC via le câble USB C. La carte va fournir l’alimentation de 3.3 V au capteur SI7021. Et il suffit ensuite de relier le bus I2C entre le capteur et la carte ESP32. La broche SCL du capteur est reliée à la broche 22 de la carte, et la broche SDA du capteur reliée à la broche 21 de la carte.
Le montage sur la platine d’essai
Ajoute de la carte uPesy et de la librairie Si7021 dans l’IDE Arduino
Il faut également installer la librairie SparkFun Si7021 Humidity and Temperature Sensor afin de pouvoir utiliser facilement le capteur SI7021.
Installation de la librairie permettant d’utiliser facilement le capteur SI7021
Le programme
// Serveur Web sur une carte ESP32 Wroom
// https://tutoduino.fr/
// Copyleft 2023
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include "SparkFun_Si7021_Breakout_Library.h"
#include <Wire.h>
const char* ssid = "mySSID";
const char* password = "MyPwd";
WebServer server(80);
Weather sensor;
void handleRoot() {
float humidity = 0;
float temp = 0;
getWeather(&humidity, &temp);
String textToDisplay = "Temperature = " + String(temp) + " C" +" ; Humidity = " + String(humidity) + " %";
server.send(200, "text/plain", textToDisplay);
}
void handleNotFound() {
String message = "File Not Found\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
for (uint8_t i = 0; i < server.args(); i++) {
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/plain", message);
}
void getWeather(float * humidity, float* temperature)
{
// Measure Relative Humidity from the HTU21D or Si7021
*humidity = sensor.getRH();
// Measure Temperature from the HTU21D or Si7021
*temperature = sensor.getTemp();
// Temperature is measured every time RH is requested.
// It is faster, therefore, to read it from previous RH
// measurement with getTemp() instead with readTemp()
}
void setup(void) {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
if (MDNS.begin("esp32")) {
Serial.println("MDNS responder started");
}
server.on("/", handleRoot);
server.on("/inline", []() {
server.send(200, "text/plain", "this works as well");
});
server.onNotFound(handleNotFound);
server.begin();
Serial.println("HTTP server started");
//Initialize the I2C sensor
sensor.begin();
}
void loop(void) {
server.handleClient();
delay(2);
}
Une fois le programme téléversé sur la carte ESP32, vous pouvez accéder à son serveur Web qui affiche la température et l’humidité mesurée par le capteur !
Affichage des informations du capteur par le serveur Web tournant sur la carte ESP32 Wroom
ESP32 Wrover
Il existe également la carte ESP32 Wrover DevKit qui est plus adaptée aux serveurs Web, car elle dispose d’une mémoire beaucoup plus importante. Elle possède une RAM externe de 4 Mo et une Flash de 16Mo (voir ce comparatif des différentes cartes ESP32 de uPesy). Sinon ses caractéristiques sont très similaires à la carte ESP32 Wroom, en dehors des 2 broches (entourées ci-dessous) qui correspondent aux GPIO16/TX2 et GPIO17/RX2 sur l’ESP32 Wroom et qui ne sont pas utilisables sur l’ESP32 Wrover.
Description des broches de l’ESP32 Grover
Utiliser Redis pour stocker les données d’un objet connecté et Grafana pour les visualiser
Redis est un Système de Gestion de Base de Données (SGBD) open-source très hautes performances, qui stocke les données en mémoire sous forme de clé-valeur.
Dans ce tutoriel nous allons développer un client Redis sur une carte ESP32 Wroom de uPesy (fabriquée en France !). Cette carte intègre un microcontrôleur ESP32, idéal pour un projet d’objet connecté (IoT) utilisant le WiFi ou le Bluetooth. Nous utiliserons le service Redis Cloud (version limitée gratuite) pour stocker les données et Grafana cloud (version limitée gratuite) pour les visualiser. Mais vous pouvez bien entendu installer très facilement le serveur Redis et Grafana sur un PC ou un Raspberry Pi si vous le préférez.
Pour programmer l’ESP32 nous utiliserons l’éditeur Visual Studio Code et la plateforme PlatformIO. Cet IDE est bien plus avancé que l’IDE Arduino et je vous recommande vivement son utilisation. J’ai également utilisé l’éditeur Visual Studio Code et la plateforme PlatformIO dans un autre tutoriel pour programmer le Raspberry Pi Pico.
Installer Visual Studio code et l’extension PlatformIO
Vous devez vous créer un compte sur le service Redis Cloud. Un compte gratuit est limité mais suffisant pour tester le service. Si vous le souhaitez vous pouvez bien entendu installer un serveur Redis sur votre ordinateur ou sur un Raspberry Pi, c’est un projet open-source.
Une fois votre compte créé, il faut ajouter une souscription et une base de données. Notez le Public endpoint et le Default user password, ils seront nécessaire dans votre projet sur l’ESP32 pour vous connecter au service Redis Cloud.
Le Public endpoint de votre base de données
Le mot de passe de l’utilisateur par défaut
Création du projet ESP32 sous Visual Studio Code
Il faut ensuite créer le projet pour l’ESP32 dans Visual Studio Code. Créez un nouveau projet en sélectionnant la carte uPesy ESP32 Wroom DevKit et le Framework Arduino :
Une fois le projet crée, il faut ajuster la vitesse du moniteur série afin de pouvoir afficher quelques traces sur le terminal de Visual Studio Code.
Pour cela, ajouter la ligne suivante dans le fichier platformio.ini :
monitor_speed = 115200
Vous trouvez ci-dessous le code d’un projet qui connecte l’ESP32 à une station Wifi et qui stocke la puissance du signal reçu (RSSI) dans votre service Redis Cloud sous forme d’un TimeSeries. Utiliser un TimeSeries permet de pouvoir visualiser sous Grafana la variation de la valeur du RSSI dans le temps sous forme de courbe.
// Client Redis sur une carte ESP32 Wroom
// https://tutoduino.fr/
// Copyleft 2023
#include <WiFi.h>
#define WIFI_SSID "MonSSID"
#define WIFI_PWD "MonMotDePasse"
// Redis client
#define REDIS_ADDR "MonAdresseRedisCloud"
#define REDIS_PORT MonPortRedisCloud
const char REDIS_PASSWORD[] = "MonMdpRedisCloud";
WiFiClient redisConn;
void wifi_setup()
{
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PWD);
Serial.println("\nConnecting");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(100);
}
Serial.println("\nConnected to the WiFi network");
Serial.print("Local ESP32 IP: ");
Serial.println(WiFi.localIP());
}
void redis_setup()
{
if (!redisConn.connect(REDIS_ADDR, REDIS_PORT))
{
Serial.println("Failed to connect to the Redis server!");
return;
}
// Envoie la commande AUTH au service Redis Cloud avec le mot de passe
redisConn.printf("*2\r\n$4\r\nAUTH\r\n$%d\r\n%s\r\n", strlen(REDIS_PASSWORD), REDIS_PASSWORD);
while (!redisConn.available())
{
delay(10);
}
String st = redisConn.readStringUntil('\n');
if (st == "+OK\r")
{
Serial.println("Connected to the Redis server!");
}
else
{
Serial.printf("Failed to authenticate to the Redis server! Errno: %s\n", st.c_str());
}
}
void setup()
{
Serial.begin(115200);
delay(1000);
wifi_setup();
redis_setup();
}
void loop()
{
// Recupere la puissance du signal Wifi reçu (RSSI)
Serial.printf("RSSI=%d\n", WiFi.RSSI());
String rssi(WiFi.RSSI());
// On souhaite stocker le RSSI dans un TimeSeries afin de pouvoir afficher sa variation dans le temps
// Envoie la commande TS.ADD avec la cle RSSI et sa valeur
redisConn.printf("*4\r\n$6\r\nTS.ADD\r\n$4\r\nRSSI\r\n$1\r\n*\r\n$%d\r\n%s\r\n", rssi.length(), rssi.c_str());
delay(1000);
}
Il faut remplacer dans ce code “MonAdresseRedisCloud” par le Public endpoint de votre service Redis Cloud dont on aura supprimé le “:” et le numéro de port (nombre à la fin). Il faut remplacer MonPortRedisCloud par ce numéro du port.
Exemple si le Public endpoint est : redis-14378.c923.us-central-6-4.ec6.cloud.redislabs.com:14378 votre code devra ressembler à ceci :
“MonMdpRedisCloud” doit être remplacé par le Default user password de votre service Redis Cloud.
Le protocole utilisé par Redis est RESP3. Les commandes doivent être envoyées sous forme de liste de blob string. Par exemple, pour s’authentifier auprès serveur Redis, il faut envoyer la commande AUTH suivie du mot de passe. Supposons que le mot de passe soit “MonMotDePasse” qui fait 13 caractères, il faut donc envoyer la chaîne de caractères : “*2\r\n$4\r\nAUTH\r\n$13\r\nMonMotDePasse\r\n” où *2 indique le nombre d’éléments dans la liste, $4 la longueur de la commande AUTH et $13 la longueur du mot de passe.
Téléverser le projet sur l’ESP32
Connectez votre carte ESP32 Wroom sur un port USB de votre PC et cliquez sur Upload and Monitor pour que le projet soit téléversé et exécuté sur l’ESP32.
Téléversement du projet sur l’ESP32
Dès que le projet est téléversé, il démarre et vous pouvez observer les valeurs du RSSI dans le terminal de Visual Studio Code.
Visualisation des données sous Grafana
Grafana permet de construire très simplement des tableaux de bord et de pouvoir les visualiser sur un navigateur.
Vous devez vous créer un compte limité mais gratuit sur le site Grafana. Une fois le service Grafana démarré, il faut installer le plugin Redis.
Une fois le plugin installé, il faut ensuite ajouter une nouvelle connection de type Data sources Redis :
Ajout d’une source Redis
Configurer la source de données Redis en indiquant l’adresse de votre service Redis Cloud qui est redis:// suivi de votre Public endpoint et le mot de passe de votre utilisateur par défaut.
Il faut ensuite créer un nouveau dashboard et une nouvelle Query sur la Data source Redis de type RedisTimeSeries. Sélectionnez la commande TS.RANGE avec la clé RSSI dont on souhaite visualiser la valeur sous forme de courbe. Cliquer sur RUN doit permettre de visualiser la courbe si la configuration est correcte.
Voilà, vous pouvez maintenant visualiser votre dashboard depuis un navigateur web.
Example de dashboard Grafana permettant de visualiser les données générées par un ESP32 et stockées sur un serveur Redis
Ce tuto est terminé, n’hésitez pas à donner votre avis et à laisser un commentaire, cela m’aidera à l’améliorer.
Merci à Gotronic et uPesy pour la fourniture de la carte ESP32 Wroom qui a été utilisée pour réaliser ce tutoriel !
Hébergez votre projet Arduino dans GitHub
GitHub est un service web d’hébergement de sources utilisant le logiciel de gestion de versions Git. GitHub permet de gérer l’évolution du projet et de travailler de manière collaborative.
Je vais décrire ici comment héberger son projet Arduino sous GitHub. Cela vous permettra d’avoir une sauvegarde en ligne de vos projets. Mais cela permettra surtout de pouvoir partager le fruit de son travail avec la communauté.
Création d’un compte GitHub
Vous devez vous créer un compte sur le site https://github.com/ en suivant la procédure “Sign up”. Un compte gratuit est suffisant pour héberger vos projets et les rendre accessibles à tous si vous le souhaitez.
Une fois que vous avez fourni votre email et un mot de passe, il faut ensuite vous choisir votre username. Ce username est important car tous vos projets seront accessibles via l’url https://github.com/username/projet.
Génération d’un token d’authentification
Vous devez créer un token qui vous permettra de vous authentifier dans les phases suivantes. Pour cela il faut aller dans le menu Settings/Developer setting/Tokens (classic) et cliquer sur Generate new token(classic).
Génération d’un token d’authentification
Il faut ensuite cocher la case repo qui vous permettra d’avoir le contrôle sur votre dépôt grâce à ce token. Attention votre token a une durée de vie limitée, il faut lui fixer une date d’expiration. Il faudra renouveler l’opération à l’issue de cette période.
Génération d’un token classic
Création d’un dépôt (repository)
Sous GitHub un dépôt (repository en anglais) est un espace dans lequel votre projet sera entreposé. Il permet de stocker tous les fichiers de votre projet. Le principe de Git est de gérer les différentes versions d’un projet, et bien entendu le dépôt permet d’avoir accès à toutes ces versions.
Afin de pouvoir stocker notre projet Arduino, il faut tout d’abord créer votre premier dépôt sous GitHub.
Il suffit d’indiquer le nom du dépôt et si il s’agit d’un dépôt public ou privé (visible par vous uniquement). Il faut créer ce dépôt totalement vide, n’ajoutez pas de fichier “README” et ne sélectionnez pas de licence à ce stade.
Création d’un dépôt sous GitHub
Une fois créé le dépôt est accessible depuis son url. Mon username étant tutoduino, mon dépôt myproject est accessible depuis cette url : https://github.com/tutoduino/myproject
Le concept de branche
La fonctionnalité de base de Git est de gérer différentes versions d’un code source. Il est ainsi possible de revenir sur une version précédente du code et d’historiser les modifications apportées.
Gestion de versions d’un fichier
Une autre fonctionnalité de Git est de pouvoir développer plusieurs versions en parallèle. Soit pour permettre de développer une fonctionnalité particulière sans pour autant “polluer” le flux principal, soit pour permettre à plusieurs développeurs de développer différentes fonctionnalités sans se perturber les uns les autres.
C’est là qu’intervient le concept de branche. Fondamentalement, une branche est la ligne isolée et indépendante, qui peut être créée par exemple pour le développement d’une nouvelle fonctionnalité.
Supposons que la ligne du milieu en bleu soit la branche principale (main) où le code est stable et mis à jour. Le développeur affecté au développement d’une nouvelle fonctionnalité va créé une branche à partir de la branche principale. Et c’est sur cette branche en orange qu’il va développer sa fonctionnalité. Une fois la fonctionnalité validée, le développeur pourra fusionner sa branche sur la branche principale.
Dans ce tuto, nous n’utiliserons que la branche principale (main), nous souhaitons en effet uniquement stocker les différentes versions de notre code source.
IDE Arduino Sous Linux
Si vous utilisez l’IDE Arduino sous Linux, voici comment stocker votre programme dans votre dépôt GitHub depuis votre répertoire local de stockage du projet.
Tout d’abord il faut installer Git sur Linux :
sudo apt install git
Créez un projet test sous l’IDE Arduino et positionnez vous dans le répertoire du projet contenant votre programme test.ino.
Entrez les commandes suivantes dans votre terminal sous Linux (après avoir remplacé tutoduino par votre username dans l’url github):
Lorsque vous tapez la dernière commande, git vous demande votre username for ‘https://github.com’ et votre Password for ‘https://tutoduino@github.com’ il faut indiquer votre username GitHub et le mot de passe est votre token généré plus haut.
Si tout c’est bien déroulé, le message suivant devrait être indiqué par Git :
tutoduino@my-pc:~/Arduino/test$ git push -u origin main
Username for 'https://github.com': tutoduino
Password for 'https://tutoduino@github.com':
Énumération des objets: 4, fait.
Décompte des objets: 100% (4/4), fait.
Compression par delta en utilisant jusqu'à 12 fils d'exécution
Compression des objets: 100% (3/3), fait.
Écriture des objets: 100% (4/4), 385 octets | 385.00 Kio/s, fait.
Total 4 (delta 0), réutilisés 0 (delta 0), réutilisés du pack 0
To https://github.com/tutoduino/myproject.git
* [new branch] main -> main
La branche 'main' est paramétrée pour suivre la branche distante 'main' depuis 'origin'.
Votre projet Arduino est maintenant bien stocké sur GitHub, vous pouvez le vérifier en allant sur l’url de votre projet sur GitHub https://github.com/tutoduino/myproject. Votre programme test.ino ainsi que le README doivent y être stockés maintenant.
Mise à jour sur GitHub d’une modification du programme
Si vous changez le code de votre programme sur votre ordinateur en local, il est très facile de pousser la mise à jour sur GitHub.
La commande git commit permet de capturer un instantané du programme qui sera ensuite pousser vers GitHub :
git commit -m "Mise à jour du code pour ajouter la fonctionnalité xxx"
La commande git push pousse le programme vers GitHub dans l’état qu’il était au moment du dernier git commit :
git push -u origin main
Téléchargement d’un programme depuis GitHub
Il est bien entendu possible de télécharger les programmes depuis GitHub vers un répertoire local de n’importe quel ordinateur. Il suffit pour cela de cloner le dépôt GitHub avec la commande suivante :
Notez qu’il est préférable de nommer le projet Arduino comme le dépôt GitHub, cela simplifie largement les opérations avec l’IDE Arduino.
Ensuite si vous modifiez un fichier dans votre répertoire de travail local, il suffit de faire un git add afin de placer la version modifiée dans une zone de préparation.
git add test.ino
Et une fois le code modifié il suffit de reprendre la procédure de commit et push :
git commit -m "Mise à jour du code pour ajouter la fonctionnalité xxx"
git push -u origin main
IDE Arduino sous Windows
Je vous recommande d’utiliser GitHub Desktop sur Windows, disponible sur le site https://desktop.github.com/.
Supposons que vous souhaitez stocker sous GitHub le projet Arduino myproject2
Vous devez tout d’abord créer le dépôt sur votre ordinateur. Pour cela ouvrez GitHub Desktop et cliquez sur Create a New Repository on your hard drive… :
Sélectionnez ensuite le nom du projet Arduino et indiquez dans quel répertoire il se situe (attention il faut indiquer le répertoire parent) puis créez le dépôt en cliquant sur Create repository :
Il suffit ensuite de publier le dépôt en cliquant sur Publish repository :
Vérifiez que le nom du dépôt qui va être publié sur GitHub correspond bien au dépôt que vous souhaitez créer et cliquez de nouveau sur Publish repository :
L’option Keep this code private permet de créer un dépôt privé, décochez cette option si vous souhaitez partager votre code avec la communauté.
Vous pouvez ensuite vérifier que le dépôt est bien créé sous GitHub :
Voilà, cette petite introduction est maintenant terminée. J’espère que cela vous a aidé à comprendre comment utiliser GitHub pour stocker vos projets Arduino. N’hésitez pas à noter ce tuto et à écrire des commentaires afin que je puisse l’améliorer.
Introduction au PIO (Programmable Input Output) du RP2040
Tout comme les autres microcontrôleur modernes, le RP2040 du Raspberry Pi Pico intègre plusieurs interfaces standards (UART, SPI, I2C…) lui permettant de communiquer facilement avec une grande variété de périphériques. Mais le RP2040 se distingue des autres microcontrôleur car il intègre des entrées/sorties programmables (Programmable Input/Output) permettant de créer vos propres interfaces ou de mettre en œuvre des interfaces spécifiques qui ne seraient pas gérées nativement par le RP2040. Un excellent exemple d’utilisation des PIO est l’interfaçage des bandes de LED NeoPixel comme nous allons le voir dans cet article.
Comment fonctionnent les PIO ?
Le RP2040 intègre 2 blocs PIO. Chaque bloc PIO est comparable à un petit processeur qui exécute le code indépendamment du CPU (Cortex-M0+). Les PIO permettent ainsi de gérer les entrées/sorties de manière déterministe, le timing étant très précis quelque soit la charge du CPU.
Chaque bloc PIO est composé de 4 machines à état (State Machine) qui peuvent exécuter indépendamment des petits programmes dont les instructions sont stockées dans une mémoire partagée (Instruction Memory). À chaque cycle d’horloge système, chaque machine à état récupère, décode et exécute une instruction. Chaque machine à état permet de manipuler les GPIO et transférer des données. Les programmes sont écrits avec un assembleur spécifique composé de 9 instructions : JMP , WAIT , IN , OUT , PUSH , PULL , MOV , IRQ , et SET.
Sur le RP2040, les 30 GPIO utilisateur (GP0-GP29) peuvent être utilisées en tant que PIO.
Schéma d’un bloc PIO (crédit : fiche technique RP2040)
Exemple : génération d’un signal carré
Par soucis de simplicité notre premier programme sera écrit en MicroPython.
Nous souhaitons que le PIO génère un signal carré sur la sortie GP28.
La fréquence d’horloge du système est de 125 MHz par défaut sur le RP2040. Les machines à état fonctionnent à la fréquence le l’horloge du système par défaut.
Nous allons programmer une machine à état pour qu’elle positionne la sortie à son état haut pendant 32 cycles soit 256 ns (1 cycle = 1/125000000 Hz = 8 ns) et qu’ensuite elle positionne la sortie à son état bas pendant 32 cycles également. La fréquence du signal carré sera donc de 1/0.000000512 s = 1.953 MHz.
Spécifie l’emplacement du début de la boucle du programme.
set(pins, 1) [31]
Positionne la sortie à l’état haut (instruction exécutée en 1 cycle) et reste dans cet état pendant 31 cycles. Cette étape dure donc bien 32 cycles.
set(pins, 0) [31]
Positionne la sortie à l’état bas (instruction exécutée en 1 cycle) et reste dans cet état pendant 31 cycles. Cette étape dure donc également 32 cycles.
wrap()
Spécifie l’endroit où se situe la fin de la boucle du programme.
Voici le code écrit en MicroPython que nous allons transférer sur le Raspberry Pi Pico et qui va programmer la machine à état du PIO comme nous venons de le définir :
import time
import rp2
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink():
wrap_target()
set(pins, 1) [31]
set(pins, 0) [31]
wrap()
sm = rp2.StateMachine(0, blink, set_base=Pin(28))
sm.active(1)
Nous observons bien un signal carré de fréquence 1.953 MHz en sortie de la broche 34 (GP28) du Pico.
Modification de la fréquence du signal carré
Il est possible de modifier la fréquence du signal carré en jouant sur le nombre de cycle pendant lesquels la machine a état laisse la sortie à l’état haut et bas (le paramètre 31 par exemple dans cette ligne de code set(pins, 1) [31]). Mais il est également possible de modifier la fréquence de fonctionnement de la machine à état dans l’appel de la fonction rp2.StateMachine().
import time
import rp2
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink():
wrap_target()
set(pins, 1)
set(pins, 0)
wrap()
sm = rp2.StateMachine(0, blink, freq=2500,set_base=Pin(28))
sm.active(1)
Dans l’exemple ci-dessus, l’horloge de la machine à état est configurée sur une fréquence de 2500 Hz. Le signal carré aura donc une fréquence de 1250 Hz, ce qui est bien confirmé par le relevé à l’oscilloscope.
Utilisation du PIO pour contrôler un bandeau de LED NeoPixel
Le contrôle de bandeau de LED NeoPixel est un excellent exemple de l’utilisation des PIO.
Le principe des LED NeoPixel (voir la fiche technique du composant WS2812B intégré à chaque LED NeoPixel) est d’envoyer un tableau de mots de 24 bits correspond aux couleurs de toutes les LEDs du bandeau. Chaque LED utilise le premier mot de 24 bits quelle reçoit sur sa broche DIN pour positionner sa propre couleur et transmet aux LED suivantes sur sa broche DO la suite des mots de 24 bits (son propre mot de 24 bits étant supprimé de cette liste). Une pause de 50ms (reset code) est nécessaire entre chaque envoi de tableau de mots de 24 bits.
Les contraintes temporelles doivent être respectées avec une précision de ±0.15us. La fréquence de l’horloge de la machine à état est réglée sur 5 MHz, ainsi la durée d’un cycle est de 0.2us. Nous allons donc approximer les durées des états hauts et bas à 0.4us et 0.8us car ils restent dans la tolérance de ±0.15us.
Programmation de la machine à état pour les LEDs NeoPixel
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
# 1 step is 0.2us (clock frequency must be set to 5MHz)
wrap_target()
# 1 step at 0 (to wait for data in low state, reset code)
out(x, 1)
# start of the cycle
# 2 step at 1
set(pins, 1) [1]
# 2 step at x
mov(pins, x) [1]
# 1 step at 0
set(pins, 0)
wrap()
La machine à état est démarrée en mode “auto-pull”, ce qui signifie qu’il n’est pas nécessaire d’exécuter la commande pull pour récupérer les données de la TX FIFO vers l’OSR.
Copie de la TX FIFO vers l’OSR
out(x, 1)
La commande out(x,1) copie 1 bit de l’OSR vers le registre x. Pendant ce cycle (0.2us), la sortie est à l’état bas car nous avons configuré set_init=rp2.PIO.OUT_LOW et out_init=rp2.PIO.OUT_LOW. Le fait de positionner la sortie à l’état bas au début de notre programme est important car cela permet d’avoir la sortie à l’état bas en attendant le premier bit de données (reset code).
set(pins, 1) [1]
La commande set(pins, 1) [1]positionne la sortie à l’état haut pendant 2 cycles (l’instruction prend s’exécute en 1 cycle et nous avons ajouter un délai de 1 cycle avec la commande [1]. A ce stade nous ne savons pas si le bit de donnée est 0 ou 1. Mais que ce soit 0 ou 1, il faut positionner la sortie à l’état haut pendant 2 cycles.
mov(pins, x) [1]
La commande mov(pins, x) [1] positionne la sortie à l’état correspondant à la valeur de x pendant 2 cycles.
set(pins, 0)
La commande set(pins, 0) positionne ensuite la sortie à l’état bas pendant 1 cycle.
Explication en image
Voici donc une explication en image du codage d’un 0 et d’un 1 avec le programme de la machine à état :
Représentation du positionnement de l’état de la sortie en fonction de la valeur du bit x (0 ou 1) avec la représentation du code de la machine à état positionnant cette sortie
Capture des signaux à l’oscilloscope montrant une séquence d’un bit à 0 suivi d’un bit à 1 :
Séquence d’un bit à 0 (état haut pendant 0.4us, état bas pendant 0.8us) suivi d’un bit à 1 (état haut pendant 0.8us, état bas pendant 0.4us)
Capture de la séquence de contrôle d’une LED la positionnant de couleur verte avec la luminosité à 10%. Les données envoyés sont 25 pour la composante green (luminosité de 10% -> 255*10/100 = 25). On observe bien la séquence binaire b00011001 correspondant à 25 sur le premier octet de données qui encode la composante green. Les deux octets suivants sont à 0 (b00000000), ils correspondent aux composantes red et blue.
Séquence complète pour une allumer une LED en vert avec une luminosité de 10%
Vu de cette séquence avec un analyseur logique (0x19 = 25 = b00011001)
Voici le programme complet en MicroPython qui fait clignoter un bandeau de 10 LED NeoPixel :
import array, time
import rp2
import machine
NUM_LEDS = 10
BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)
BRIGHTNESS = 0.1
def setColor(color):
r = int(color[0]*BRIGHTNESS)
g = int(color[1]*BRIGHTNESS)
b = int(color[2]*BRIGHTNESS)
return (g<<16) + (r<<8) + b
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
# 1 step = 0.2us (clock frequency must be set to 5MHz)
wrap_target()
# 1 step at 0 (to wait for data in low state, reset code)
out(x, 1)
# start of the cycle
# 2 step at 1
set(pins, 1) [1]
# 2 step at x
mov(pins, x) [1]
# 1 step at 0
set(pins, 0)
wrap()
sm = rp2.StateMachine(0, ws2812, freq=5_000_000, set_base=machine.Pin(12), out_base=machine.Pin(12))
sm.active(1)
ar = array.array("I", [0 for _ in range(NUM_LEDS)])
# Set all leds to green
for i in range(NUM_LEDS):
ar[i] = setColor(GREEN)
sm.put(ar, 8)
time.sleep_ms(50)
# Rotate one red led
cpt = 1
while True:
ar[cpt-1] = setColor(GREEN)
ar[cpt] = setColor(RED)
sm.put(ar, 8)
time.sleep_ms(50)
cpt = (cpt+1)%NUM_LEDS
Nous pouvons observer la sortie de la broche 12 avec un analyseur logique et confirmer que la programmation du PIO correspond bien au comportement souhaité.
Vu de la séquence dans laquelle la 6ème LED est de couleur rouge et les autres de couleur verte
Merci pour votre intérêt pour ce article. Si vous l’avez apprécié vous pouvez le noter en cliquant sur les étoiles ci-dessous ou en laissant un commentaire. Je vous signale également mon article sur le module XIAO RP2040 et le contrôle par PIO de sa LED RGB en utilisant le langage Rust.
Vérifiez la réputation des adresses IP publiques avec lesquelles vos équipements communiquent
Vous êtes nombreux à avoir apprécié mon tuto consacré à la détection d’activité anormale sur votre réseau local avec Suricata sur un Raspberry Pi. Je vous propose dans cet article un moyen complémentaire de détecter une activité anormale sur votre réseau local en vérifiant la réputation des adresses IP avec lesquelles vos équipements communiquent. Vous serez par exemple en mesure de détecter qu’un malware est installé sur un équipement de votre réseau car il communique avec un serveur de mauvaise réputation.
Architecture
L’architecture est identique à celle que j’ai utilisé pour mon tutoriel Suricata. Un Switch recopie toutes les trames échangées sur le réseau vers un Raspberry Pi grâce à la fonctionnalité de “Port Mirroring”. Pour capter le trafic Wifi, on utilise un petit routeur Wifi qui sera également relié sur un port du Switch. Il est ainsi possible pour le Raspberry Pi d’avoir la visibilité sur tout le trafic de votre réseau local.
Exemple d’architecture permettant d’analyser tout le trafic avec Wireshark sur un Raspberry Pi
Nous capturerons les trames reçus par le port Ethernet du Raspberry Pi grâce à l’outil TShark, qui est une version de Wireshark qui peut être appelée directement dans un terminal et sans interface graphique.
Mon programme IP Reputation Check utilisera ensuite le fichier PcapNg créé par TShark et l’analysera afin d’en extraire toutes les adresses IP externes avec lesquelles vos équipements communiquent.
Architecture simplifiée
Si vous souhaitez analyser uniquement des appareils communiquant en Wi-Fi, il est également possible de créer un hotspot Wi-Fi dans le Raspberry Pi et de sélectionner ce réseau Wi-Fi sur les appareil que vous souhaitez analyser. Cela simplifie l’architecture car vous n’avez pas besoin du petit routeur Wi-Fi ni du switch configuré en “Port Mirroring”.
Bien entendu le Raspberry ne devra pas utiliser son interface Wi-Fi pour communiquer avec votre Box internet, il devra y être connecté par un câble Ethernet.
Créer un hotspot est détaillé dans la documentation Raspberry, mais c’est maintenant faisable d’un simple click depuis l’interface graphique du Raspberry.
Création d’un hotspot Wi-Fi directement depuis l’interface graphique du Raspberry Pi
Il suffira ensuite de capturer le trafic sur l’interface de l’interface wlan0 du Raspberry.
Capture du trafic sur le Raspberry Pi
Installez Raspberry Pi OS Lite 64 bits sur votre Raspberry Pi avec l’outil Raspberry Pi Imager disponible sur le site https://www.raspberrypi.org/software/. J’utilise cette version de Raspberry Pi OS car ce tutoriel n’a pas besoin d’interface graphique (en dehors de l’architecture simplifiée expliquée ci-dessus) et il est recommandé d’utiliser la version 64 bits pour le Raspberry Pi 4. Mais vous pouvez bien entendu utiliser la version Raspberry Pi OS Desktop.
Installez TShark sur le Raspberry Pi :
Bash
sudoaptupdatesudoaptinstalltshark
Lors de l’installation choisissez le mode permettant a des utilisateurs qui ne sont pas super-utilisateurs de capturer les trames (cliquez sur Yes lorsque le choix vous est proposé).
Il est cependant nécessaire de modifier les droits du programme /usr/bin/dumpcap :
Lancez une capture durant 2 minutes afin de vérifier le bon fonctionnement du programme :
Bash
tshark-wmon-fichier.pcapng-aduration:120
Une fois que tout fonctionne, vous pouvez lancer une capture plus longue (1h) en évitant d’utiliser votre réseau de façon intensive afin de limiter le trafic légitime.
Programme de vérification de la réputation des adresses IP
Installez les librairies Python nécessaire à mon programme :
Ce programme écrit en Python3 utilise plusieurs services en ligne permettant de vérifier la réputation d’une adresse IP. Vous devez vous inscrire à ces services afin d’obtenir une clé API pour chacun d’eux. L’inscription est gratuite, mais elle permet un nombre limitée de requête par jour. C’est suffisant pour un tester un petit réseau local normalement. Vous pouvez bien entendu passer à un abonnement payant de ces services si vous souhaitez lever cette limite. Notez qu’une fois le nombre de requête atteinte pour un service, mon programme n’affichera plus d’information issues de ce service mais continuera à fonctionner avec les autres services.
Liste des services utilisés dont il faut récupérer une clé API:
Il faut copier les clé API des services auxquels vous êtes inscrits dans un fichier .env situé dans le répertoire IP_reputation_check. Si vous ne souhaitez pas utiliser un service, ne mettez tout simplement pas de clé pour ce service dans le fichier .env.
Voici un exemple de fichier .env dans lequel vous devez remplacer les “xxx” par vos clés API :
Plaintext
# API KEYSSHODAN_API_KEY=xxxVIRUS_TOTAL_KEY=xxxIPQS_KEY=xxxAPIVOID_KEY=xxxABUSEIPDB_KEY=xxx
Mon programme ip_reputation_check.py prend une liste d’adresse IP via le flux d’entrée standard stdin.
Vous pouvez ainsi l’utiliser de plusieurs manières :
Utilisation avec une liste d’adresses IP entrées au clavier
Vous pouvez entrer une ou plusieurs adresses IP directement dans la console. Entrez chaque adresse IP et appuyez sur la touche retour/entrée (symbole ↵). Appuyez une nouvelle fois sur la touche retour/entrée après la dernière adresse IP entrée.
Bash
$python3 ip_reputation_check.py↵8.8.8.8↵1.1.1.1↵↵
Le programme affichera les informations pour ces adresses :
Vous pouvez utiliser directement le fichier Pcap (ou PcapNg) contenant le trafic enregistré par TShark sur le Raspberry Pi. Le programme parse_pcap.py va extraire automatiquement la liste d’adresses IP externes de ce fichier, et la commande pipe “|” va permettre de transmettre cette liste au programme ip_reputation_check.py
Voici un exemple d’affichage de la réputation de cette adresse IP au 14 avril 2025.
AbuseIPDB indique que cette adresse a été indiquée comme suspecte 72 fois, ce qui lui permet de donner un indice de 100% sur la certitude que cette adresse soit malicieuse
ApiVoid indique un score de risque de 100
VirusTotal indique que cette adresse a été indiquée 4 fois comme malicieuse
IpQualityScore donne un score de fraude de 100 qui indique également qu’elle est suspicieuse et que de l’activité d’un réseau de Bot y a été détectée.
Voyant plusieurs services indiquer indiquer que cette adresse IP est malicieuse, on peut en conclure qu’elle est pour le moins suspecte. Il faudra donc analyser finement le comportement de votre équipement qui communique avec cette adresse IP.
Comment ça marche une clé Dallas (iButton) ?
Vous avez certainement déjà remarqué que les vendeurs ou serveurs utilisent ce type de clé sur leurs caisses enregistreuses. Il suffit de poser la clé Dallas sur son lecteur pour identifier automatiquement le porteur de la clé.
Caisse enregistreuse avec lecteur de clé Dallas
Les clés Dallas sont simplement constituées d’un iButton qui contient un numéro de série unique permettant d’identifier automatiquement son utilisateur. Le iButton communique avec le lecteur de clé Dallas via le bus 1-Wire.
Une clé Dallas avec son iButton
Le bus 1-Wire
1-Wire est un bus de communication conçu par Dallas Semiconductor qui permet de véhiculer des données et une alimentation sur un seul conducteur. Le second conducteur est tout simplement le fil de masse.
L’alimentation du iButton est fournie via le bus 1-Wire par le lecteur de clé Dallas, il n’a donc pas besoin d’intégrer de pile ou de batterie.
Le bus 1-Wire nécessite d’utiliser une résistance de rappel “pull-up” (voir mon article à ce sujet) de 4,7 kΩ.
Le schéma est très simple, il suffit de relier la sortie du lecteur de clé Dallas à la broche 2 de l’Arduino Uno en s’assurant que la résistance de rappel soit reliée au 3,3V ou 5V de l’Arduino.
Schéma électrique du lecteur de clé Dallas
iButton
Un iButton (e.g. DS1990A) contient un composant électronique qui stocke dans sa ROM un numéro de série unique et qui communique avec le lecteur de clé Dallas via le bus 1-Wire. Le numéro de série est également gravé sur le iButton.
iButton DS1990A ayant le numéro de série 0001393E0F8
Les iButton existent en deux dimensions (F3 et F5) et nous voyons sur l’image suivante l’emplacement des broches IO (alimentation et échange de données) et GND (masse).
Dimensions d’un iButton et emplacement des broches
La ROM du iButton a une taille de 64 bits (8 octets), elle contient :
Un “Family Code” de 1 octet (0x01 pour un iButton)
Le numéro de série unique sur 6 octets
Le CRC des 7 premiers octets (Family Code + Serial Number) codé sur 1 octet.
Programme Arduino pour lire la ROM d’un iButton
Voici le code Arduino qui permet de lire la ROM du iButton et afficher son numéro de série unique.
/*
iButton Reader
Created March 2022
by Tutoduino
*/
// For Dallas iButton reader
#include <OneWire.h>
#define PIN 2 // iButton is connected to PIN 2
OneWire iButton(PIN);
/* Print iButton ROM buffer */
void printBuffer(byte* buf) {
char serialNumber[16 + 1]; // Printable 8 hex bytes buffer (+1 for '\0')
sprintf(serialNumber,
"%02X%02X%02X%02X%02X%02X",
buf[6],
buf[5],
buf[4],
buf[3],
buf[2],
buf[1]);
Serial.print("Family code: ");
Serial.print(buf[0], HEX);
Serial.print(", Serial number: ");
Serial.print(serialNumber);
Serial.print(", CRC: ");
Serial.println(buf[7],HEX);
}
bool checkCrc8(byte* buf) {
// Compare the received CRC (8-BIT CRC CODE is byte 7 of the buffer)
// against the computed CRC on the first 7 bytes of the buffer
byte crc8 = iButton.crc8(buf, 7);
if (buf[7] != crc8) {
Serial.print("Invalid CRC: Received=");
Serial.print(buf[7],HEX);
Serial.print(" Expected=");
Serial.println(crc8, HEX);
printBuffer(buf);
return false;
}
return true;
}
void readButton(byte* buf) {
// Send Read ROM [0x33] Function Command to iButton
iButton.write(0x33);
// Read the 8 bytes of the ROM
iButton.read_bytes(buf,8);
printBuffer(buf);
// Check of CRC is ok
if (checkCrc8(buf) == true) {
// Check if the family code is iButton [0x01]
if (buf[0] != 0x01) {
Serial.println("Not iButton family code");
}
}
}
void setup() {
Serial.begin(115200);
Serial.println("Put iButton on reader");
iButton.reset();
}
void loop() {
byte oneWireBuffer[8]; // 64-Bit Unique ROM buffer
// Send a reset pulse and check presence pulse
if ( iButton.reset() != 0) {
readButton(oneWireBuffer);
}
delay(200);
}
Voici ci-dessous l’affichage dans le moniteur série de l’IDE Arduino des numéros de série de 2 iButton différents.
Affichage des informations contenues dans le iButton dans le moniteur série de l’Arduino
Changer le numéro de série d’une clé Dallas réinscriptible
Il est possible de modifier le numéro de série des clés Dallas réinscriptibles (RW1990). Le montage est identique au précédent mais le programme est un peu plus compliqué. En effet la séquence d’écriture des boutons RW1990 ne permet pas d’utiliser la librairie OneWire telle quelle. Je me suis inspiré du code de swiftgeek pour réaliser mon programme.
/*
iButton Writer
Created March 2022
by Tutoduino
Based on https://gist.github.com/swiftgeek/0ccfb7f87918b56b2259
*/
#include <OneWire.h>
#define PIN 2
OneWire iButton (PIN); // I button connected on PIN 2.
void sendLogical0() {
// Send logical 0
digitalWrite(PIN, LOW); pinMode(PIN, OUTPUT); delayMicroseconds(60);
pinMode(PIN, INPUT); digitalWrite(PIN, HIGH); delay(10);
}
void sendLogical1() {
digitalWrite(PIN, LOW); pinMode(PIN, OUTPUT); delayMicroseconds(10);
pinMode(PIN, INPUT); digitalWrite(PIN, HIGH); delay(10);
}
/* Write a byte to iButton ROM */
int writeByte(byte data){
for(byte data_bit=0; data_bit<8; data_bit++){
if (data & 1){
sendLogical0();
} else {
sendLogical1();
}
data = data >> 1;
}
return 0;
}
/* Print iButton ROM buffer */
void printBuffer(byte* buf) {
char serialNumber[16 + 1]; // Printable 8 hex bytes buffer (+1 for '\0')
sprintf(serialNumber,
"%02X%02X%02X%02X%02X%02X",
buf[6],
buf[5],
buf[4],
buf[3],
buf[2],
buf[1]);
Serial.print("Family code: ");
Serial.print(buf[0], HEX);
Serial.print(", Serial number: ");
Serial.print(serialNumber);
Serial.print(", CRC: ");
Serial.println(buf[7],HEX);
}
bool checkCrc8(byte* buf) {
// Compare the received CRC (8-BIT CRC CODE is byte 7 of the buffer)
// against the computed CRC on the first 7 bytes of the buffer
byte crc8 = iButton.crc8(buf, 7);
if (buf[7] != crc8) {
Serial.print("Invalid CRC: Received=");
Serial.print(buf[7],HEX);
Serial.print(" Expected=");
Serial.println(crc8, HEX);
printBuffer(buf);
return false;
} else {
return true;
}
}
void setNewBuffer(byte* newBuffer, byte* serialNumber) {
// Family Code = 0x01 (iButton)
newBuffer[0] = 0x01;
// SerialNumber
memcpy(newBuffer+1, serialNumber, 6);
// CRC
newBuffer[7] = iButton.crc8(newBuffer, 7);
}
void readButton(byte *buf) {
// Send Read ROM [0x33] Function Command to iButton
iButton.write(0x33);
// Read the 8 bytes of the ROM
iButton.read_bytes(buf,8);
printBuffer(buf);
// Check of CRC is ok
if (checkCrc8(buf) == true) {
// Check if the family code is iButton [0x01]
if (buf[0] != 0x01) {
Serial.println("Not iButton family code");
}
}
}
void setup(){
Serial.begin(115200);
Serial.println("Put iButton on reader");
}
void loop(){
byte oneWireBuffer[8]; //array to store the Ibutton ID.
byte newOneWireBuffer[8]; //array to store the new Ibutton ID.
byte newSerialNumber[6] = {0xBB, 0xAA, 0xCC, 0x01, 0x00, 0x00};
// Send a reset pulse and check presence pulse
if ( iButton.reset() != 0) {
// Read serial number
readButton(oneWireBuffer);
delay(500);
// Set new serial number in new buffer
setNewBuffer(newOneWireBuffer, newSerialNumber);
// Check if new serial number if already set
if (memcmp(oneWireBuffer, newOneWireBuffer, 8) == 0) {
return;
}
// Write new serial number
// Prepare to write
iButton.skip();
iButton.reset();
iButton.write(0xD1);
sendLogical0();
// Write
Serial.print(" Writing iButton ID:\n ");
iButton.skip();
iButton.reset();
iButton.write(0xD5);
for (byte x = 0; x<8; x++){
delay(10);
writeByte(newOneWireBuffer[x]);
Serial.print('*');
}
Serial.print('\n');
iButton.reset();
iButton.write(0xD1);
sendLogical1();
}
}
Voici ci-dessous l’affichage dans le moniteur série de l’IDE Arduino du changement de numéro de série d’un iButton.
Le numéro de série du iButton a été modifié
Il faut donc noter qu’il est extrêmement facile de dupliquer une clé Dallas, et que ce type de clé n’est pas du tout sécurisé contrairement à ce que certains peuvent affirmer dans leurs arguments de vente. Une clé Dallas permet simplement d’identifier facilement et rapidement un utilisateur…
Attention la clé Dallas ne sécurise pas les accès, contrairement aux arguments mis en avant par certains sites marchands comme ci-dessus !
Avertissement : ce tutoriel est uniquement à but pédagogique et son contenu ne doit pas être utilisé pour des activités illicites !
Le Power over Ethernet (PoE) permet d’alimenter votre Raspberry Pi (Raspberry Pi 4 Model B ou Raspberry Pi 3 Model B+) par son connecteur Ethernet. Le câble Ethernet va ainsi servir au transport des données ainsi qu’à l’alimentation électrique du Raspberry Pi.
L’alimentation d’un Raspberry Pi par PoE peut être très pratique lorsque le Raspberry Pi doit être installé dans un emplacement dépourvu de prise électrique. Sachant qu’un câble Ethernet peut avoir une longueur de 100 mètres cela peut être une solution intéressante en plus d’être élégante.
Raspberry Pi 4 alimenté via PoE par son câble Ethernet
Un commutateur (ou injecteur) PoE est nécessaire
Il est bien entendu nécessaire d’utiliser un commutateur (Switch) supportant la norme PoE. Ce type de commutateur permet de délivrer 15 W via le câble Ethernet et ainsi alimenter convenablement le Raspberry Pi.
Certains commutateurs supportent la nouvelle norme PoE+, qui permet de délivrer 30 W. Mais attention il faudra utiliser un PoE HAT compatible avec cette nouvelle norme également PoE+. Bien entendu cela fonctionne sans problème si vous avec un commutateurs PoE+ et un PoE HAT. Le commutateur détectera que le PoE HAT ne supporte pas la norme PoE+ et utilisera la norme PoE.
Raspberry alimenté par le Switch PoE via le câble Ethernet
Dans mon tutoriel j’utilise le Switch Netgear GS305EP. Ce commutateur 5 ports est compatible avec la norme PoE+. Il fournit des informations intéressantes sur le status du PoE, comme par exemple la puissance délivrée sur chaque port.
Status du PoE sur le Switch Netgear GS305EP
Si toutefois vous n’avez pas de Switch compatible PoE, vous pouvez utiliser un injecteur PoE.
Raspberry alimenté par l’injecteur PoE via le câble Ethernet
Les broches PoE du Raspberry Pi 4 Model B
Je vous invite à lire le chapitre concernant le schéma d’alimentation de mon article sur le PoE afin de comprendre cette partie.
Le Raspberry Pi 4 Model B utilise un connecteur RJ45 de référence Trxcom TRJG0926HENL, dont voici le schéma interne :
Schéma du connecteur RJ45 TRJG0926HENL utilisé par le Raspberry Pi 4 Model B
Les 4 prises centrales des transformateurs de couplages (VC1..VC4) du connecteur RJ45 sont reliées au connecteur 4 broches J14 (PoE) du Raspberry Pi.
Schéma de la partie Ethernet du Raspberry Pi 4 Model B
Le connecteur PoE du Raspberry Pi 4
Le connecteur J14 (PoE) est bien visible sur le Raspberry Pi 4, il se situe juste à l’arrière du connecteur RJ45 Trxcom.
Connecteur 4 broches J14 du Raspberry Pi 4 permettant de récupérer le courant PoE en sortie du connecteur RJ45
En fonction du mode utilisé par le PSE (équipement qui fournit l’alimentation PoE), le courant sera délivré sur des broches différentes du connecteur J14. On voit dans le tableau suivant la polarité des fils du câble Ethernet en fonction du mode PoE utilisé.
Polarité des fils du câble Ethernet en fonction du Mode PoE utilisé par le PSE
Par exemple le commutateur Netgear utilise l’alternative A (MDI-X). Il fournira donc le courant PoE sur les paires 1-2 et 3-6, avec la polarité négative sur la paire 1-2 et la polarité positive sur la paire 3-6. Comme la broche 1 (TR1_TAP) du connecteur PoE est reliée à la prise centrale des paires 3- 6, et que la broche 2 (TR0_TAP) du connecteur PoE du Raspberry Pi est reliée à la prise centrale des paires 1- 2, la tension PoE sera délivrée sur les broches 1 (+) et 2 (-) du connecteur J14.
Tension aux bornes des broches 1 et 2 du connecteur J14 (PoE)
Attention ! Comme vous pouvez le voir, la tension d’alimentation du PoE n’est pas de 5 V, elle est en générale comprise entre 48 V et 55 V. Il ne faut donc pas alimenter le Raspberry Pi directement via les broches de son connecteur J14 (PoE). Il est indispensable d’utiliser un module PoE HAT, qui va transformer cette tension en une tension de 5 V.
Le rôle principal des modules PoE HAT est de transformer la tension PoE (environ 50 V) en une tension de 5 V permettant d’alimenter le Raspberry Pi.
Tutoduino.fr
Le PoE-HAT de UCTRONICS
Le PoE-HAT de UCTRONICS n’embarque pas de ventilateur, mais son design permet de positionner un radiateur sur le CPU du Raspberry Pi 4.
Son avantage (et c’est un ÉNORME avantage !) est qu’il permet d’utiliser la plupart des GPIO du Raspberry Pi.
PoE-HAT de UCTRONICS
Le PoE-HAT FAN de UCTRONICS
Le PoE-HAT FAN de UCTRONICS embarque un petit ventilateur qui refroidit efficacement le Raspberry Pi.
Le ventilateur est assez bruyant et sa vitesse ne peut pas être contrôlée. Il tourne en permanence, même lorsque le Raspberry Pi est éteint. Le ventilateur fonctionne également lorsque le Raspberry Pi est alimenté par la prise USB.
L’avantage de ce HAT est que son design est compatible avec la majorité des boîtiers.
Son inconvénient majeur est que les GPIO du Raspberry Pi ne sont plus accessibles.
PoE-HAT FAN de UCTRONICS
Le Raspberry Pi PoE+ HAT
Le PoE+ HAT de la fondation Raspberry est compatible avec la norme PoE+. Cette norme permet de délivrer jusqu’à 30 W, le double du PoE. Mais sachant que le Raspberry Pi 4 va consommer au maximum 12,75 W(1) le PoE devrait être suffisant.
Ce PoE+ HAT est équipé d’un ventilateur régulé et assez discret. Lorsque le CPU est chargé le ventilateur devient un peu plus bruyant, mais il permet de maintenir la température du CPU sous les 50 °C. Lorsque le Raspberry Pi est éteint, le ventilateur s’arrête.
Notez que le ventilateur fonctionne bien entendu également lorsque le Raspberry Pi est alimenté par sa prise USB.
Le seul inconvénient de ce PoE+ HAT est qu’il bloque l’utilisation des GPIO du Raspberry Pi et que son ventilateur ne rentre pas dans tous les boîtiers.
PoE+ HAT de la fondation Raspberry
Efficacité du refroidissement
Ce tableau compare la température du CPU du Raspberry Pi 4 équipé des différents PoE HAT. Ce test est réalisé avec une charge CPU de 25% environ et aucun équipement branché sur les ports GPIO et USB.
UCTRONICS PoE-HAT
UCTRONICS PoE-HAT FAN
Raspberry PoE+ HAT
Température du CPU chargé à 25% pendant 5 minutes
62°C
39 °C
49 °C
Conclusion
Il n’y a vraiment pas un PoE HAT qui ressort en tête de ce comparatif. Chaque PoE HAT a ses propres avantages et inconvénients. Le choix se fera en fonction de vos besoins (compatibilité avec les boîtiers, silence du ventilateur, puissance de l’alimentation, accès aux GPIO…).
Voici un tableau récapitulatif pour vous aider dans votre choix :
UCTRONICS PoE-HAT
UCTRONICS PoE-HAT FAN
Raspberry PoE+ HAT
Compatibilité avec la majorité des boîtiers
Oui
Oui
Non
Ventilateur intégré
Non
Oui mais bruyant
Oui
Laisse accès aux GPIO
Oui
Non
Non
Support du PoE+ (30 W)
Non
Non
Oui
De façon tout à fait personnelle et pour mon utilisation quotidienne, j’ai opté pour l’utilisation du UCTRONICS PoE-HAT avec mon Raspberry Pi 4. Le silence de fonctionnement et l’accès aux GPIO étant mes critères de choix.
Merci à Kubii, revendeur officiel Raspberry Pi en France, pour la fourniture du matériel utilisé dans ce comparatif.
Téléchargez Visual Studio Code à partir de la page de téléchargement et installez le sur votre système :
Téléchargement de Visual Studio Code
Lancez Visual Studio Code et installez l’extension PlatformIO IDE :
Installation de l’extension PlatformIO IDE
Si vous rencontrez une erreur relative à l’installation de Python3 et qu’il est pourtant installé sur votre système, installez le package python3-venv.
sudo apt update
sudo apt install python3-venv
Création du projet PlatformIO
Créez votre projet en cliquant sur l’icône PlatformIO et puis sur New Project. Il faut sélectionner la carte (Board) Raspberry Pi Pico et le Framework Arduino Framework qui va vous permettre de programmer facilement votre Raspberry Pi Pico avec le langage C++.
Création du projet pour la carte Raspberry Pi Pico
Vous allez créer votre premier programme qui fait simplement clignoter le LED du Raspberry Pi Pico. Voici le code à éditer dans le fichier main.cpp :
// Clignotement de LED du Raspberry Pi Pico
// https://tutoduino.fr/
// Copyleft 2020
#include "Arduino.h"
void setup() {
// Declare la broche sur laquelle la LED est
// reliee comme une sortie
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
// Passer le sortie à l'état HAUT pour allumer la LED
digitalWrite(LED_BUILTIN, HIGH);
// Attendre 1 seconde, pendant ce temps la LED reste allumee
delay(1000);
// Passer le sortie à l'état BAS pour eteindre la LED
digitalWrite(LED_BUILTIN, LOW);
// Attendre 1 seconde, pendant ce temps la LED reste donc éteinte
delay(1000);
}
Vous devez ensuite compiler ce programme en cliquant sur Build dans le menu PlatformIO. Vérifier le résultat de la compilation avec l’affichage de SUCCESS dans le terminal.
Compilation du programme pour faire clignoter la LED du Raspberry Pi Pico
Téléversement du programme sur le Raspberry Pi Pico
Sur Linux il faut tout d’abord installer les règles de permission de PlatformIO pour les ports USB (udev), en tapant la commande suivante dans un shell :
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules
sudo service udev restart
Pour le premier téléversement, il faut démarrer le Pico en mode Bootloader. Pour cela maintenir le bouton BOOTSEL appuyé et brancher le Pico sur votre PC via un câble USB. Ne relâcher le bouton qu’après que Pico soit bien connecté à votre PC. Le Pico est alors vu comme un stockage de masse USB contenant les fichiers INDEX.HTML et INFO_U2F.TXT, vous pouvez à présent cliquer sur Upload dans le menu PlatformIO.
Vérifier que le téléversement du programme est réussi et que le mot SUCCESS est bien affiché dans le terminal.
Téléversement du programme sur le Raspberry Pi Pico
Une fois que le programme est téléversé sur le Raspberry Pi Pico, il s’exécute immédiatement. La LED du Raspberry Pi Pico doit clignoter toutes les secondes.
Vous pouvez maintenant téléverser vos prochains programmes sans avoir à mettre le Pico en mode Bootloader. Il suffira de cliquer sur Upload dans le menu PlatformIO.
Comme je l’explique dans mon article Power over Ethernet (PoE), il est possible d’alimenter un Raspberry Pi directement par son câble Ethernet. La manière la plus simple est d’utiliser un Switch compatible PoE et d’installer un PoE HAT sur le Raspberry Pi.
Le PoE HAT avec ventilateur UCTRONICS vendu par Kubii est particulièrement intéressant. Son ventilateur permet de refroidir le CPU du Raspberry Pi et sa taille compact lui permet de se loger dans la plupart des boîtiers du marché.
Installation
Le PoE HAT récupère le courant du connecteur Ethernet sur les 4 broches PoE du Raspberry Pi.
Broches PoE du Raspberry Pi
De petits écrous permettent de bien relier le PoE HAT au Raspberry Pi. Comme vous le voyez sur les photos ci-dessous il faut tout d’abord visser l’écrou directement sur le Raspberry Pi et ensuite visser le PoE HAT sur cet écrou.
Montage du PoE HAT sur le Raspberry Pi 4 (vissage de l’écrou support visible sur la photo de gauche)
Le PoE HAT utilise le connecteur PoE 5 broches mais il monopolise également le connecteur 40 broches. Les GPIO ne peuvent donc malheureusement plus être utilisées une fois le PoE HAT en place.
Une fois le PoE HAT installé, il ne reste plus qu’a relier le Raspberry Pi au Switch via un câble Ethernet. Et voilà le Raspberry Pi communique et est alimenté par un simple câble Ethernet !
Le Raspberry Pi est alimenté par le câble Ethernet grâce au PoE
Le Raspberry Pi 4 avec son boîtier équipé du PoE HAT et alimenté par son câble Ethernet
Ventilateur ou pas ?
Le ventilateur est efficace mais assez bruyant. Si vous êtes sensibles au bruit il est facile de le démonter.
Il est même possible de positionner un petit radiateur sur le CPU du Raspberry lorsque le PoE HAT est installé sans le ventilateur.
Module PoE HAT avec son ventilateur démonté
Mais si vous souhaitez ne pas utiliser de ventilateur, je vous conseille de commander le modèle de PoE HAT sans ventilateur. Il laisse plus de place pour mettre un radiateur plus important sur le CPU du Raspberry Pi. Je n’ai pas encore testé ce PoE HAT mais il semble bien laisser libre l’accès aux GPIO.
PoE HAT sans ventilateur
Sans ventilateur sur le PoE HAT, la température du CPU est assez importante (60 °C), même lorsque le Raspberry Pi ne fait pas grand chose.
Information sur le CPU
Température : 61.835 degrés
Charge: 2.3%
Avec le ventilateur sur le PoE HAT, la température du Raspberry Pi est bien régulée et reste de l’ordre de 40°C même lorsque le CPU est chargé.
Information sur le CPU
Température : 41.868 degrés
Charge : 26.0%
Puissance délivrée par le PoE HAT
Le PoE HAT est compatible avec le norme IEEE 802.3af, ce qui signifie que la puissance délivrée par le Power Sourcing Equipment (le Switch dans notre cas) est limitée à 15 W. Cette puissance est équivalente à une alimentation USB de 5 V et 3 A (5 V x 3 A = 15 W), parfait pour alimenter un Raspberry Pi.
L’interface du Switch indique bien qu’il délivre au Raspberry Pi 4 une puissance de 3.9 W (un courant de 72 mA d’intensité sous une tension de 54 V) .
Le Switch Netgear GS305EP délivre une puissance de 3.9 W au Raspberry Pi 4
Pour terminer cet article, je ne peux que féliciter Kubii pour sa démarche envers les personnes en situation de handicap. La commande a en effet été préparée par des travailleurs d’un ESAT.
Bravo Kubii !
Power over Ethernet (PoE)
Le Power over Ethernet (PoE) ou Alimentation électrique par câble Ethernet permet d’alimenter un équipement par le câble Ethernet. Cette technologie est très utilisée pour alimenter les caméras de surveillance et les téléphones sur IP. Il suffit de raccorder l’équipement par un simple câble Ethernet, il n’est plus nécessaire de le raccorder à une alimentation électrique externe.
L’équipement qui fournit l’alimentation est nommé Power Sourcing Equipment (PSE). L’équipement qui est de l’autre côté du câble et qui consomme le courant est nommé Powered Device (PD).
Cette technologie a été définie en 2003 par la norme IEEE 802.3af. Elle permettait à l’origine de fournir une puissance de 15.4 W (Type 1) en utilisant 2 paires du câble avec un courant de 350 mA maximum sous une tension de 44 V à 57 V.
En 2009 la norme IEEE 802.3af a été remplacée par la norme IEEE 802.3at. Cette norme appelée PoE+ permet de fournir une puissance de 30 W (Type 2) en utilisant 2 paires du câble avec une tension de 44 V à 57 V. Un câble de catégorie 5 est indispensable à partir de cette norme.
En 2018, le nouvelle norme 802.3bt appelée PoE++ permet d’atteindre une puissance de 60W (Type 3) ou 90 W (Type 4) en utilisant les 4 paires du câble Ethernet.
Exemple d’utilisation du PoE
Prenons l’exemple d’une caméra IP. La caméra nécessite une alimentation pour fonctionner et transmettre ses images au Switch via le câble Ethernet. L’installateur devra donc positionner la caméra prêt d’une prise de courant ou bien tirer une prise électrique jusqu’à la caméra en plus du câble Ethernet.
Caméra IP alimentée par un source externe
L’utilisation du PoE va simplifier l’installation de la caméra. Il n’est plus nécessaire d’avoir une source de courant à l’endroit de son installation. En effet le Switch PoE va générer le courant qui sera transmis par le câble Ethernet à la caméra. Le Switch et la camera doivent bien entendu tous les deux supporter la norme PoE.
Caméra IP alimentée par le Switch via le câble Ethernet
Injecteur PoE
Si votre Switch n’est pas compatible avec la norme PoE, il est possible d’utiliser un injecteur PoE. L’injecteur ajoute de l’énergie provenant de l’alimentation externe aux données provenant d’un Switch non PoE et transmet le courant et les données au PD (caméra par exemple).
Utilisation d’un injecteur PoE dans le cas ou le Switch ne supporte pas le PoE
Splitter PoE
Si votre équipement (caméra IP dans le schéma ci-dessous) n’est pas compatible avec la norme PoE, il est possible d’utiliser un Splitter PoE. Certains Splitter fournissent une tension de sortie configurable (e.g. 5 V, 9 V ou 12 V) sur un connecteur DC. D’autres Splitter fournissent du 5 V via une sortie USB.
Utilisation d’un Splitter PoE dans le cas ou la caméra IP ne supporte pas le PoE
Schémas d’alimentation
Le principe du Power over Ethernet (PoE) est d’utiliser des transformateurs à prise centrale. Dans le schéma ci-dessous, le signal des données qui est cadencé à un taux de symboles de 125 MHz est appliqué en entrée des deux transformateurs de gauche (paire 1/2 pour le transformateur du haut et paire 3/6 pour le transformateur du bas par exemple). Une tension continue de 48 V est appliquée entre les deux prises centrales des deux transformateurs de gauche. On récupère le signal des données en sortie des deux transformateurs de droite. La tension de 48 V est récupérée entre les deux prises centrales des transformateurs de droite. Le courant est ainsi transmit sur les mêmes paires que les données sans en perturber le fonctionnement.
Voici un lien vers cette simulation falstad d’un schéma d’alimentation sur 2 paires d’un câble Ethernet. On voit bien sur les deux premiers oscilloscopes que les données ne sont pas altérées par la tension continue de 48 V et que la tension continue de 48 V n’est pas non plus altérée par le signal des données.
Simulation d’un schéma d’alimentation sur 2 paires utilisant des transformateurs à prise centrale
Principe de fonctionnement d’un transformateur à prise centrale
Un transformateur à prise centrale permet d’ajouter une tension sur le signal de sortie du transformateur. Voici un exemple de ce principe simulé sur le schéma du bas de cette simulation falstad.
Tension appliquée sur la prise centrale d’un transformateur (circuit du haut)
Le circuit du haut montre l’utilisation d’un transformateur sans prise centrale de ratio 2. Le signal d’entrée est une tension sinusoïdale de 10 V à 40 Hz. L’oscilloscope en bas à gauche affiche en vert la tension d’entrée et en rouge la tension de sortie du transformateur. Nous voyons bien que le signal de sortie est à la même fréquence que le signal d’entrée mais que sa tension est de 20 V. Ce qui est le double de la tension d’entrée puisque le transformateur a un ratio de 2.
Le circuit du bas utilise un transformateur à prise centrale sur laquelle est appliquée une tension continue de 20 V. L’oscilloscope en bas à droite affiche en vert la tension d’entrée qui varie entre -10 V et +10 V à une fréquence de 40 Hz comme pour le circuit du haut. La tension de sortie en rouge sur l’oscilloscope a toujours la même fréquence de 40 Hz, mais par contre la tension de sortie varie entre +10 V et +30 V.
Les modes (Mode A, Mode B) d’injection de courant
La norme 802.3 définit deux alternatives pour l’injection du courant dans le câble par le PSE :
Mode A : l’alimentation est fournie via les prises centrales des transformateurs de couplage sur les paires 1/2 et 3/6.
Mode B : L’alimentation est fournie via les paires 4/5 et 7/8 dans le cas du 10BASE-T/100BASE-TX, et via les prises centrales des transformateurs de couplage sur les paires 4/5 et 7/8 dans le cas du 1000BASE-T.
Comme je l’explique dans mon article sur Ethernet, Ethernet 10BASE-T et 100BASE-TX n’utilisent que 2 paires (1/2 et 3/6) pour véhiculer les données. Dans le mode A le courant est transmis sur les mêmes paires que les données (1/2 et 3/6). Dans le mode B, les 2 paires non utilisées pour les données (4/5 et 7/8) sont utilisées pour véhiculer le courant.
Schéma pour Ethernet 10BASE-T/100BASE-TX (mode A en vert et mode B en rouge)
Ethernet 1000BASE-T utilise les 4 paires du câble pour transmettre les données à 1 Gbit/s. Le courant est donc forcément véhiculé sur les mêmes paires que les données. Avec PoE++, il est possible de faire passer le courant sur les 4 paires à la fois pour avoir plus de puissance.
Schéma pour Ethernet 1000BASE-T (mode A en vert et mode B en rouge)
Il est possible de simuler le schéma d’alimentation sous Falstad. Dans l’exemple ci-dessous nous simulons le schéma d’alimentation Ethernet 100BASE-TX avec un PSE en mode B.
Simulation sous falstad d’un schéma d’alimentation Ethernet 100BASE-TX en mode B
PSE End-span vs PSE Mid-span
Un PSE peut-être End-span ou Mid-span.
Un PSE End-span fournit directement l’alimentation PoE à un PD. Un Switch PoE est un exemple de End-span.
Le Switch est un PSE End-span
Un PSE Mid-span sert de périphériques intermédiaires entre un Switch non compatible PoE et un PD compatible PoE. Un injecteur PoE est un exemple de PSE Mid-span.
L’injecteur est un PSE Mid-span
Schéma d’alimentation pour un PSE Mid-span
Schéma d’alimentation d’un PSE Mid-span pour Ethernet 10BASE-T/100BASE-TX (mode A en vert et mode B en rouge)
Polarités utilisées par le PSE
On retrouve souvent des approximations sur les polarités du PoE. Pour être précis, la norme IEEE distingue 3 alternatives pour les polarités du PoE sur un PSE :
PSE MDI-X utilisant le Mode A
PSE MDI utilisant le Mode A
PSE utilisant le Mode B
Notez que les PSE qui utilisent des ports MDI/MDI-X à configuration automatique (« Auto MDI-X ») peuvent choisir l’un ou l’autre choix de polarité associé aux configurations de l’alternative A.
Polarités d’un PSE
PoE et Arduino
Il existait un Shield Ethernet incluant un module PoE pour Arduino Uno. Malheureusement ce module n’est plus disponible sur le site Arduino.
Arduino Ethernet Shield 2 incluant le PoE (indisponible)
Un Shield Ethernet PoE de la marque DFRobot (ref DFR0850) est disponible chez GoTronic.
Shield Ethernet PoE pour Arduino
PoE et Raspberry Pi
Le Raspberry Pi 4 Model B utilise un connecteur RJ45 de référence Trxcom TRJG0926HENL, dont voici le schéma interne :
Schéma du connecteur RJ45 TRJG0926HENL utilisé par le Raspberry Pi 4 Model B
Les 4 prises centrales des transformateurs de couplages (VC1..VC4) sont reliées au connecteur 4 broches “PoE” du Raspberry Pi. Il est donc possible de récupérer directement le courant PoE via ces broches.
Connecteur PoE 4 broches du Raspberry Pi 4
Attention la tension sur ces bornes n’est pas de 5 V mais d’environ 48 V. Il ne faut pas alimenter le Raspberry Pi directement via ces broches mais utiliser un module PoE. Par exemple ce module permet de transformer l’alimentation PoE en alimentation 5 V pour le Raspberry Pi .
Les modules PoE HAT ci-dessous permettent d’alimenter un Raspberry Pi via un simple câble Ethernet.
PoE HAT avec ou sans ventilateurModule HAT PoE+HAT PoE+ sur un Raspberry PiModule HAT PoE+ pour Raspberry Pi
Vous trouverez dans cet article un comparatif des différents PoE HAT pour le Raspberry Pi.
Il existe des petits dispositifs qui permettent de vérifier le mode utilisé par le PSE (Mode A ou Mode B).
Testeur Power over Ethernet (PoE) de la marque DIGITUS
Ce type de testeur fait le raccourci qu’un End-span utilise forcément le Mode A et qu’un Mid-span utilise forcément le Mode B. Si c’est souvent le cas, il aurait été plus juste d’indiquer Mode A / Mode B à la place de Endspan / Midspan sur le testeur. Car c’est bien le Mode que le testeur permet de vérifier.
Comme on le voit sur les photos suivantes, le testeur ne permet pas de tester si il s’agit d’un PSE de type Endspan ou Midspan, mais uniquement si celui-ci utilise le Mode A (LED Endspan) ou le Mode B (LED Midspan).
Le testeur connecté au port d’un Switch PoE+ indique qu’il s’agit d’un équipement de type Endspan (ce qui est correct)
Le testeur connecté au port d’un injecteur PoE+ indique qu’il s’agit d’un équipement de type Endspan alors qu’il s’agit d’un équipement de type Midspan utilisant le Mode A
N’hésitez pas à laisser un commentaire, merci pour votre lecture !
Qu’est ce que… l’impédance caractéristique d’un câble Ethernet ?
Vous avez certainement lu qu’un câble Ethernet doit avoir une impédance caractéristique égale à 100 Ω. Nous allons voir dans cet article à quoi correspond cette notion d’impédance caractéristique. L’ impédance est une forme de résistance (impédance vient du latin impedire qui veut dire « entraver ») au passage d’un courant électrique alternatif sinusoïdal. La définition de l’impédance est une généralisation de la loi d’Ohm au courant alternatif. L’impédance est notée Z et se mesure en Ohms (Ω) comme une résistance, mais avec U et I qui sont de forme sinusoïdale.
Z = U / I
L’impédance est un nombre complexe, elle possède une amplitude et une phase. Sa partie réelle est la résistance et sa partie imaginaire est la réactance.
Impédance caractéristique d’un câble
Quand une onde traverse la frontière entre deux milieux différents, une partie de son énergie est réfléchie et repart dans l’autre sens. Le principe est le même pour un câble, qui est une ligne de transmission d’un signal électrique.
Dans une ligne de transmission, l’impédance caractéristique correspond à l’impédance qu’on pourrait mesurer à ses bornes si elle avait une longueur infinie. C’est la raison pour laquelle des lignes de transmission ont besoin d’être “terminées” par une charge égale à son impédance caractéristique. Ainsi, le signal se perd dans une charge comme si la ligne continuait à l’infini et ne se réfléchit pas.
Autrement dit, l’impédance caractéristique est la valeur de l’impédance des dipôles qui, branchés aux extrémités d’un câble, permettent la transmission correcte du signal sans réflexion.
Ligne de transmission terminée par une charge d’impédance égale à son impédance caractéristique
Simulation d’une ligne de transmission dans Falstad
Il est intéressant de visualiser ce mécanisme de réflexion dans le simulateur falstad. Vous pouvez observer avec cet exemple la réflexion sur une ligne de transmission terminée par une impédance dont la valeur est différente de l’impédance caractéristique du câble. Ci-dessous une capture d’écran, on y voit très clairement la réflexion du signal indiquée par la flèche jaune.
Ligne de transmission d’impédance caractéristique de 100 Ohm terminée par une impédance de 330 Ohm
Si la ligne de transmission est terminée par une impédance égale à l’impédance caractéristique, il n’y a pas de réflexion. Comme nous pouvons le voir sur la figure suivante :
Ligne de transmission terminée avec une impédance égale à l’impédance caractéristique (100 Ohm)
Router une paire différentielle avec KiCad
Lorsque vous réalisez un PCB et qu’il est nécessaire de router chaque paire d’un câble Ethernet comme une paire différentielle et de paramétrer le PCB afin qu’il corresponde à l’impédance caractéristique du câble Ethernet (100 Ohms).
Routage d’une paire différentielle sous KiCad
A l’aide du “PCB Calculator” intégré à KiCad vous pouvez déterminer la largeur des pistes ainsi que l’espacement entre elles. Il faut renseigner les paramètres de votre PCB à partir des données récupérées sur le site du constructeur du PCB.
PCB Calculator KiCad
Chez JLCPCB ces paramètres sont disponibles dans le menu “capabilities” :
Paramètres du PCB fournis par le constructeur (JLCPCB par exemple)
Par exemple pour la réalisation de mon TAP Ethernet les pistes des paires différentielles doivent avoir une largeur de 0,3 mm et un espacement de 0,3 mm entre elles afin de garantir une impédance de 100 Ohms.
Réglage de la largeur des pistes et de l’espacement entre elles
Lorsque vous tracez une paire différentielle, il est important de respecter certaines contraintes comme :
Les pistes doivent avoir une longueur identique
La distance entre les pistes doit être constante, les pistes doivent rester parallèles au maximum
Il faut éviter les angles trop importants pour le routage des pistes
Afin d’avoir des pistes de longueur identique, KiCad propose une fonction pour égaliser la longueur des 2 pistes d’une paire différentielle, c’est la fonction “Tune Differential Pair Skew/Phase”.
Routage d’une paire différentielle sous KiCad afin que la longueur des deux pistes soit équivalente
Comment ça marche… Ethernet
Dans cet article je vais expliquer les bases d’Ethernet, qui est le standard le plus utilisé pour les réseaux locaux. Pourquoi cet article ? Car en réalisant mon tuto sur le TAP réseau Ethernet 100BASE-TX je me suis aperçu qu’il était difficile de trouver un article qui explique de manière concise et simple les bases d’Ethernet. Soit les articles sont très détaillés, mais on se perd dans les détails, soit il est trop basique et on comprend difficilement le fonctionnement précis de ce réseau. Dans ce article j’ai essayé de trouver un compromis afin de comprendre les bases en quelques minutes.
Le câble Ethernet à paires torsadées
Il existe plusieurs types de câbles regroupés par catégories, pour simplifier on peut dire qu’une catégorie va indiquer le débit maximum que le câble peut transmettre. Un câbles de catégorie 5e par exemple permet un débit allant jusqu’à 1 Gbit/s.
Les câbles sont constitués de 4 paires torsadées (8 fils). Ethernet utilise une signalisation différentielle pour la transmission des données, la tension différentielle varie de 0 à environ +1 V sur le câble positif et de 0 à environ -1 V sur le câble négatif. C’est pourquoi pour Ethernet 100BASE-T on nomme RX+ et RX- les 2 fils de la paire qui est utilisée pour recevoir les données sur une station, et TX+ et TX- les 2 fils de la paire qui est utilisée pour envoyées des données sur une station.
Câble Ethernet constitué de 4 paires torsadées
La prise RJ45
Les câbles Ethernet à paires torsadées sont équipés de connecteurs RJ45 composés de 8 broches.
Câblage du connecteur RJ45 :
La paire 1 correspondant aux fils bleu et blanc-bleu est reliée aux broches 4 et 5
La paire 2 correspondant aux fils blanc-orange et orange est reliée aux broches 1 et 2
La paire 3 correspondant aux fils blanc-vert et vert est reliée aux broches 3 et 6
La paire 4 correspondant aux fils blanc-marron et marron est reliée aux broches 7 et 8
Il existe différentes normes de câblage mais la plus répandue actuellement pour l’utilisation informatique est la norme T568B qui correspond au câblage décrit ci-dessus. L’autre norme T568A est très similaires puisque seules les paires 2 (orange, blanc-orange) et 3 (vert, blanc-vert) sont interchangées.
Connecteur RJ45 mâle câblé suivant la norme T568B
Lorsque l’on regarde de face un connecteur RJ45 mâle avec la languette de verrouillage sur le dessus, la paire 2 composée des fils de couleurs blanc-orange et orange se situe à gauche. La broche 1 du connecteur est reliée au fil blanc-orange et la broche 2 au fil orange. La broche 8 est reliée au fil marron qui appartient à la paire 4.
Le câble croisé
Dans le cas d’un réseau Ethernet 100BASE-TX (100 Mbit/s) seules 2 paires du câble sont utilisées. La paire sur laquelle une station reçoit les données est composée des fils RX+ (broche 3) et RX- (broche 6). La paire sur laquelle cette station émet ses données est composée des fils TX+ (broche 1) et TX- (broche 2).
Pour que 2 stations communiquent en étant reliées ensemble directement par un câble Ethernet, sans passer par un commutateur (switch), il faut utiliser un câble croisé entre ces deux stations. En effet on comprend aisément que les broches TX+/TX- du port RJ45 de la station qui émet vont devoir être connectées aux broches RX+/RX- du port RJ45 de la station qui reçoit les données. Il est donc nécessaire de croiser les 2 paires RX+/RX- et TX+/TX- à l’intérieur du câble Ethernet.
Schéma d’un câble croisé Ethernet 100BASE-T
Pour relier deux stations entre elles via à un concentrateur (hub) ou un commutateur (switch), il ne faut pas utiliser de câble croisé. En effet pour relier une station à cet type d’équipement réseau il faut toujours utiliser un câble droit.
Note : maintenant la plupart des équipements incorporent la fonction Auto MDI-X qui détermine automatiquement s’il faut croiser les signaux ou pas. Il devient donc inutile de se soucier d’utiliser un câble droit ou croisé dans votre installation.
La couche MAC
Le couche MAC (Media Access Control) est la couche liaison dans le modèle OSI. Elle se situe au dessus de la couche physique (le câble) et gère l’adressage physique des machines. Une adresse MAC est attribuée à chaque machine d’un réseau Ethernet. Une adresse MAC se compose de 6 octets et est représentée sous la forme d’octets en hexadécimal séparés par des double points.
Par exemple l’adresse MAC de mon PC est :
a0:c5:89:6d:d4:32
Les 3 premiers octets sont le OUI (Organisationally Unique Identifier) qui indique le fournisseur de la puce qui gère Ethernet sur votre machine. Il existe des sites qui permettent de retrouver ce fabricant en fonction d’une adresse MAC. Par exemple, le site https://macvendors.com/ m’indique qu’Intel est le fabricant de ma carte réseau.
Il existe des adresses MAC particulières. Par exemple FF:FF:FF:FF:FF:FF correspond à une l’adresse de “broadcast”, une trame envoyée vers cette adresse sera reçue par toutes les machines présentes sur le réseau.
La trame Ethernet
Une trame Ethernet est composée de 3 parties :
L’entête MAC (MAC Header) qui indique l’expéditeur et le destinataire du message ainsi que le type des données contenues dans le message
Les données (Data) contenues dans le message
Un contrôle de redondance cyclique (CRC) utilisé pour que le destinataire puisse vérifier que le message n’a pas été altéré pendant sa transmission sur le câble
Les données contenues dans une trame Ethernet est en général une encapsulation d’un protocole d’une couche supérieure du modèle OSI. Le champ “EtherType” de l’entête permet de savoir à quel type de protocole correspondent les données. Par exemple 0x0800 correspond au protocole internet (IP V4), et c’est donc une trame IP qui sera contenue dans le champ Data de la trame Ethernet.
Mode Half-duplex et Full-duplex
Il existe trois types de canaux de communication :
Simplex : le canal transporte l’information dans un seul sens
Half-duplex : le canal transporte l’information dans les deux sens mais alternativement
Full-duplex : le canal transporte l’information dans le deux sens simultanément
L’utilisation de concentrateur (hub) ne permet que le mode de fonctionnement half-duplex, alors que l’utilisation de commutateur (switch) permet le mode de fonctionnement full-duplex. La plupart des cartes réseaux supportent aujourd’hui le full-duplex.
Ethernet Gigabit 1000BASE-T
Comme nous l’avons vu Ethernet 100BASE-T est limité à un débit de 100Mbit/s, et il utilise 2 paires de fils sur les 4 disponibles.
Afin d’augmenter le débit et d’atteindre le Gigabit, Ethernet 1000BASE-T utilise les 4 paires de fils en mode full-duplex (l’information est transportée simultanément dans les deux sens).
A noter qu’il existe une norme 1000BASE-TX qui n’utilise que 2 paires, mais cela a été un échec commercial et aucun produit n’est disponible.
Ethernet sur un Arduino
En règle général, les Arduino ne possèdent pas de connecteur Ethernet, il faut utiliser un Shield Ethernet prévu pour offrir une connectivité Ethernet à un Arduino Uno.
Arduino Ethernet Shield
J’ai testé… la LilyGo T-Watch 2020 V3
Une montre connectée qui se programme avec l’IDE Arduino, j’en ai rêvé et c’est LilyGo qui l’a fait !
Installation de l’environnement
Tout d’abord il faut ajouter le gestionnaire de carte pour le microcontrôleur qui équipe la montre, il s’agit d’un microcontrôleur de la famille ESP32, bien connu des bidouilleurs qui aiment s’amuser avec le Wifi ou le Bluetooth 🙂
Ensuite il faut installer le gestionnaire de carte ESP32 by Espressif Systems en cliquant sur le menu “Gestionnaire de carte” :
Recherchez “esp32” et cliquez sur Installer :
Vous pouvez maintenant sélectionner la carte TTGO T-Watch dans le menu ESP32 Arduino :
Notre premier croquis (programme)
Nous allons ouvrir notre premier exemple de croquis qui tourne sur un ESP32, il s’agit de “WiFiScan” qui est un simple scanner de réseaux Wifi :
On compile le programme exemple et on le téléverse sur la montre que l’on a préalablement reliée au PC via son câble micro USB. Le programme s’exécute et vous voyez la liste des réseaux Wifi à votre portée s’afficher dans le moniteur série de l’IDE Arduino.
Dans cet exemple l’écran de la montre reste noir car nous n’avons pas programmé d’affichage.
Un exemple d’affichage sur la montre
De nombreux autres exemples sont fournis avec dans le GitHub de LilyGO, il suffit de télécharger la librairie “TTGO_TWatch_Library-master.zip” disponible à l’adresse suivante : https://github.com/Xinyuan-LilyGO/TTGO_TWatch_Library
Sur la page du GitHub cliquez sur le bouton vert “Code” puis sur “Download ZIP”
Le fichier ZIP se télécharge sur votre PC, et une fois qu’il est téléchargé il suffit de l’insérer dans l’IDE Arduino :
Et les nombreux exemples apparaissent dans le menu “Exemples” -> “TTGO TWatch Library”.
Nous allons tester le croquis exemple “LilyGoGui”, il affiche un écran sympa sur la montre. J’ai juste changé le croquis pour afficher “Tutoduino” 😉
Attention, il faut indiquer la version du matériel utilisé. Pour ce faire décommentez la ligne qui correspond à la version de votre montre (la mienne est une V3) dans le fichier “config.h” :
Compilez le croquis et téléversez le sur votre montre.
Et voilà notre montre avec un bel affichage 😉
A quoi ça sert… un module RTC ?
Un Arduino Uno et son micro-contrôleur ATmega328P ne possèdent pas d’horloge interne. Ils ne sont donc capables de retourner ni l’heure ni la date courante. C’est le rôle d’un composant appelé “RTC” qui signifie “Real Time Clock”, ou HTR en français qui signifie “horloge temps réel”.
Les composants DS1302 et DS1307
Un exemple de composant utilisé fréquemment comme RTC est le DS1302. Il nécessite un oscillateur externe de fréquence 32.768 kHz relié à ses broches 2 et 3. Afin de conserver l’heure et la date courante, une alimentation par pile est prévue sur ce composant.
Le micro-contrôleur communique avec ce composant par un lien série via les broches CE, I/O et SCLK. Il existe plusieurs librairies dans l’IDE Arduino utilisables pour ce composant, par exemple “Rtc by Makuna” qui est assez complète.
Le composant DS1307 reprend les mêmes principes que le DS1302 mais communique avec le micro-contrôleur par bus I2C (via ses broches SCL et SDA).
Remarque importante : Si vous n’utilisez pas de batterie il faut relier la broche VBAT à la masse, sinon le composant ne fonctionnera pas correctement.
Le DS1307 avec son oscillateur 32.768 kHz relié en I2C à l’Arduino Uno
Utilisation d’un module RTC
L’utilisation de modules basés sur ce composant est assez pratiques, car ils incluent généralement le composant, un oscillateur externe et le support pour la pile. Ils communiquent généralement avec l’Arduino via le bus I2C.
Exemple de module basé sur le composant DS1307
Le câblage de ce type de module est extrêmement simple. Il suffit de relier l’alimentation 5 V et les 2 fils du bus I2C.
Par contre j’ai observé des comportements étranges sur certains modules. Lorsque la pile n’était pas insérée dans le support l’Arduino avait des difficultés à reconnaître le module lors de la phase d’initialisation lors de l’appel de la fonction rtc.begin(). C’est très probablement lié au fait que sans pile le VBAT du DS1307 doit être relié à la masse, et certains modules ne doivent pas bien gérer ce cas.
Schéma du montage
Schéma de câblage du module RTC avec l’Arduino Uno
Programme de l’Arduino Uno
Je recommande d’utiliser librairie “RTClib” de Adafruit pour votre programme.
C++
// Utilisation d'un module RTC avec un Arduino Uno// https://tutoduino.fr/// Copyleft 2020#include"RTClib.h"RTC_DS1307 rtc;voidsetup () {Serial.begin(9600); // Attente de la connection serie avec l'Arduinowhile (!Serial); // Lance le communication I2C avec le module RTC et // attend que la connection soit operationellewhile (! rtc.begin()) {Serial.println("Attente du module RTC...");delay(1000); } // Mise à jour de l'horloge du module RTC avec la date et // l'heure courante au moment de la compilation de ce croquisrtc.adjust(DateTime(F(__DATE__), F(__TIME__)));Serial.println("Horloge du module RTC mise a jour");}voidloop () { DateTime now = rtc.now();charheure[20]; // Affiche l'heure courante retournee par le module RTC // Note : le %02d permet d'afficher les chiffres sur 2 digits (01, 02, ....)sprintf(heure, "Il est %02d:%02d:%02d", now.hour(), now.minute(), now.second());Serial.println(heure);delay(5000);}
Affichage sur le moniteur série
Affichage de l’heure retournée par le module RTC sur le moniteur série
Dérive de l’horloge
On remarque qu’au démarrage du programme, l’heure du moniteur série et l’heure retournée par le module RTC diffèrent de quelques secondes. C’est normal et vous le comprenez facilement à la lecture du programme. En effet nous mettons à jour l’heure du module RTC dans notre programme avec l’heure de compilation du programme et non l’heure à laquelle le programme démarre sur l’Arduino Uno. Dans la capture d’écran ci-dessus, vous voyez que le module RTC affiche 17:12:23 alors que le moniteur série affiche 17:12:45. L’horloge du module RTC est en retard de 22 secondes par rapport à l’heure du PC. Ce retard correspond au temps de compiler le programme, le téléverser et le démarrer sur l’Arduino. Mais il ne s’agit en aucun cas d’une dérive de l’horloge du module RTC.
La dérive est quand à elle liée à la précision de l’horloge du module RTC. Le constructeur de ce module annonce une dérive de 2 secondes par jour, ce qui veut dire que l’horloge peut avoir un retard ou une avance de 2 secondes par jour. L’horloge aura potentiellement un retard ou une avance d’environ 5 minutes par mois, ce qui est acceptable pour un petit montage électronique.
J’ai réalisé le tuto “Mesure de la dérive d’un module RTC” dans lequel j’explique comment mesurer cette dérive et je compare la dérive du module RTC DS1307 avec celui du DS3231.
Problème lors du reset de l’Arduino
Dans le programme ci-dessus, il y a un piège auquel il faut faire attention. L’horloge du module RTC est en effet mise à jour à chaque redémarrage de l’Arduino avec l’heure et la date de compilation du programme. Après un reset de l’Arduino le module RTC n’indique plus du tout l’heure courante… 😉
Pour éviter ce problème, il faut mettre à jour l’horloge du module RTC uniquement si le module indique que cette horloge n’est pas réglée. Afin de savoir si l’horloge a déjà été réglée nous utilisons la fonction isrunning().
C++
voidsetup () {Serial.begin(9600); // Attente de la connection serie avec l'Arduinowhile (!Serial); // Lance le communication I2C avec le module RTC et // attend que la connection soit operationellewhile (! rtc.begin()) {Serial.println("Attente du module RTC...");delay(1000); } // Mise a jour de l'horloge du module RTC si elle n'a pas // ete reglee au prealableif (! rtc.isrunning()) { // La date et l'heure de la compilation de ce croquis // est utilisee pour mettre a jour l'horlogertc.adjust(DateTime(F(__DATE__), F(__TIME__)));Serial.println("Horloge du module RTC mise a jour"); }}
Une pile c’est utile…
Mais lorsque le montage n’est plus alimenté, l’horloge du composant DS1307 est réinitialisée. Et au redémarrage du système la fonction isrunning() retournera FALSE. Le programme de l’Arduino réglera alors l’horloge avec l’heure de la compilation, qui ne sera plus du tout l’heure courante.
Aussi pour éviter ce problème, il faut que l’horloge du DS1307 ne soit pas réinitialisée en cas de perte d’alimentation. Pour cela une alimentation par pile est prévue sur ce composant et il continue à compter les tics du quartz et donc à conserver l’heure courante lorsqu’il est alimenté sur pile. Lorsque le circuit sera de nouveau alimenté, le programme n’aura pas besoin de mettre à jour l’horloge du module RTC.
Ajoutez un écran et vous avez une horloge
Pour finir cet article, ajoutons un petit écran OLED 0.96″ afin d’afficher l’heure, juste pour le fun 🙂
// Horloge sur afficheur OLED avec un module RTC et un Arduino Uno// https://tutoduino.fr/// Copyleft 2020#include"RTClib.h"#include"SSD1306Ascii.h"#include"SSD1306AsciiAvrI2c.h"#define I2C_ADDRESS 0x3CSSD1306AsciiAvrI2c oled;RTC_DS1307 rtc;voidsetup () {Serial.begin(9600); // Attente de la connection serie avec l'Arduinowhile (!Serial); // Lance le communication I2C avec le module RTC et // attend que la connection soit operationellewhile (! rtc.begin()) {Serial.println("Attente du module RTC...");delay(1000); } // Mise a jour de l'horloge du module RTC si elle n'a pas // deja ete regleeif (! rtc.isrunning()) { // La date et l'heure de la compilation de ce croquis // est utilisee pour mettre a jour l'horlogertc.adjust(DateTime(F(__DATE__), F(__TIME__)));Serial.println("Horloge du module RTC mise a jour"); }oled.begin(&Adafruit128x64, I2C_ADDRESS);oled.setFont(Adafruit5x7); oled.clear();oled.set2X();oled.println("Tutoduino");oled.set1X();oled.println("Apprendre");oled.println("l'electronique");oled.println("avec un Arduino");}voidloop () { DateTime now = rtc.now();charheure[20]; // Affiche l'heure courante retournee par le module RTC // Note : le %02d permet d'afficher les chiffres sur 2 digits (01, 02, ....)sprintf(heure, "Il est %02d:%02d:%02d", now.hour(), now.minute(), now.second());Serial.println(heure);oled.clear();oled.set2X();oled.println("Tutoduino");oled.set1X();oled.println();oled.println(heure);delay(1000);}
Influence de la température sur l’oscillateur
La température influence les oscillation du quartz et donc la dérive de l’horloge temps réel. En général un oscillateur résonne à une fréquence dont la précision est optimale pour une température de 25 °C. Lorsqu’il est utilisé avec une température différente ou qui varie, sa fréquence ne sera pas aussi précise.
Il existe des composants comme le DS3231 qui inclus un oscillateur compensé en température (TCXO) . La précision est bien meilleure que pour le DS1307, avec une dérive de quelques minutes au maximum par an.
Module à base de DS3231
A quoi ça sert… un réseau de résistances R-2R ?
Avez-vous remarqué que l’Arduino possède des entrées-sorties numériques et des entrées analogiques, mais qu’il n’a pas de sorties analogiques ? Une sortie analogique est pourtant très utile pour certaines applications, comme pour des montages audio et tout ce qui nécessite un contrôle analogique… Et bien nous allons voir qu’avec un réseau de résistances R-2R (resistor ladder en anglais) nous pouvons très simplement réaliser un convertisseur numérique-analogique qui convertit des sorties numériques de l’Arduino en sortie analogique.
Remarque : dans mon tutoriel C’est quoi… un signal PWM ? nous réalisons une pseudo sortie analogique pouvant prendre 256 valeurs. Mais il est important de noter que le signal reste un signal numérique (O V ou 5 V). La modulation des impulsions varie et cela réduit la puissance moyenne délivrée par la sortie numérique. Mais en aucun cas la sortie n’est analogique.
PWM avec un rapport cyclique de 75%, il ne s’agit pas d’une sortie analogique mais d’une sortie numérique (0 ou 5 V) dont les impulsions sont modulées
Réseau R-2R
Un réseau R-2R est un circuit composé de résistances ayant plusieurs étages identiques. Chaque étage est composé d’une résistance dont la valeur est le double (2R) de la valeur de la résistance qui sépare chaque étage (R). Chaque étage est relié à une sortie numérique de l’Arduino, qui peut prendre les valeurs 0 ou 1 (0 ou 5 V). Le nombre d’étage permet de doubler le nombre possibles de tension de sorties du montage. Le nombre de tensions de sorties différentes est donc 2n ou n est le nombre d’étages.
Dans l’exemple ci-dessous nous avons un réseau de résistances 2-2R composé de 2 étages et qui a donc 2²=4 tensions différentes en sortie.
Voici la table des valeurs de Vsortie en fonction des sorties numériques D0 et D1 de l’Arduino.
Il s’agit donc bien d’un convertisseur numérique-analogique 2 bits !
Plus le réseau de résistances R-2R a d’étages, plus le nombre de tensions de sortie possibles est grand. Un réseau R-2R de n étages est contrôlé par n bits et génère 2n tensions différentes. L’écart entre 2 tensions successives correspond à la résolution du convertisseur numérique -analogique. Cette résolution se calcule grâce à la formule suivante :
ΔV = V0 x 1 / 2n
où V0 est la tension des sorties numériques (5 V sur l’Arduino Uno) et n est le nombre d’étages du réseau R-2R.
On voit que la résolution du convertisseur 2 bits ci-dessus est : ΔV = V0 x 1 / 2n = 5 x 1 / 2² = 1.25 V
Réseau 2-2R de 4 bits
Dans l’exemple ci-dessous vous trouvez un réseau de résistances R-2R composé de 4 étages. Il est possible de générer 2⁴=16 tensions de sorties différentes. On parle d’un réseau de résistance R-2R de 4 bits, qui est un convertisseur numérique-analogique de 4 bits ayant une résolution de 0.3125 V (ΔV = V0 x 1 / 2n = 5 x 1 / 2⁴ = 0.3125 V).
Voici la table des valeurs de Vsortie en fonction des sorties numériques D0,D1, D2 et D3 de l’Arduino.
Voici la simulation de ce réseau de résistance R-2R de 4 bits réalisé avec falstad. Vous pouvez changer la valeur des sorties numériques de l’Arduino en cliquant sur les H et L (H=High=5V et L=Low=0V) et visualiser directement l’impact sur la tension de sortie du réseau.
J’ai réalisé un exemple de réseau de résistances R-2R 4 bits en utilisant des résistances 4,7 kΩ et 10 kΩ. Le rapport n’est pas 2 entre ces valeurs, cela rend le montage moins linéaire mais j’ai fait avec le matériel disponible. 😉
Réseau de résistances R-2R sur un Protoshield Arduino Uno
Dans cet exemple, le programme de l’Arduino Uno incrémente un nombre toutes les 2 secondes et positionne les sortie D0, D2, D4 et D6 avec la valeur du bit 0, 1, 2 et 3 de ce nombre. Nous pouvons donc observer sur un multimètre les valeurs de Vsortie très proches de celles de la table ci-dessus. Les valeurs ne sont pas exactement les même que la table car je n’ai pas utilisé des résistances dont le rapport est exactement 2.
Voici le code de l’Arduino Uno correspondant à ce test :
// Reseau de resistances R-2R 4 bits
// Convertisseur numerique-analogique 4 bits
// https://tutoduino.fr/
// Copyleft 2020
// La fonction r2rAnalogWrite() positionne les sorties
// numeriques D0, D2, D4 et D6 en fonction de la valeur
// du bit 0, 1, 2 ou 3 de la valeur passee en parametre
void r2rAnalogWrite(uint8_t valeur) {
digitalWrite(0, (valeur >> 0) & 0x01);
digitalWrite(2, (valeur >> 1) & 0x01);
digitalWrite(4, (valeur >> 2) & 0x01);
digitalWrite(6, (valeur >> 3) & 0x01);
// variante possible et plus rapide
// PORTD = ((valeur & 0b1000) << 3) | ((valeur & 0b100) << 2) | ((valeur & 0b10) << 1) | (valeur & 1);
}
void setup() {
// Les broches numeriques D0, D2, D4 et D6 sont
// configurees comme des sorties
pinMode(0, OUTPUT);
pinMode(2, OUTPUT);
pinMode(4, OUTPUT);
pinMode(6, OUTPUT);
// Sors une tension de 0 sur la sortie analogique R2R
r2rAnalogWrite(0);
}
void loop() {
uint8_t index;
// Boucle de 0 à 15 afin de tester toutes les valeurs
// des 4 bits du convertisseur numerique-analogique
for (index=0; index<16 ; index++) {
r2rAnalogWrite(index);
delay(2000);
}
r2rAnalogWrite(0);
delay(2000);
}
Si on observe à l’oscilloscope la sortie analogique, on voit bien que l’on obtient une rampe dont la tension varie de 0 à 4,68 V. J’ai modifié la temporisation pour l’affichage de la courbe sur l’oscilloscope avec un délai de 2 ms après chaque écriture analogique. On observe bien la résolution de 0,31 V (différence de tension entre 2 pas).
Autre exemple, notre sortie analogique peut bien entendu sortir une tension sinusoïdale. Dans cet exemple, le sinus est calculé en faisant appel à la fonction sin à chaque fois. Ce n’est pas une bonne pratique car cela consomme énormément de puissance processeur. La bonne pratique est indiquée en commentaire, il s’agit de pré-calculer les valeurs des sinus dans une table et d’aller lire dans cette table.
Notez que vous pouvez écrire les 8 bits du port D directement (portD = …) plutôt que d’écrire les sortie numériques l’une après l’autre avec la fonction digitalWrite. C’est nettement plus rapide et c’est ce que j’ai utilisé sur ce deuxième exemple.
// Reseau de resistances R-2R 4 bits
// Convertisseur numerique-analogique 4 bits
// https://tutoduino.fr/
// Copyleft 2020
#define tailleTableSinux 64
uint8_t tableSinus[tailleTableSinux] = {8, 8, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 11, 10, 9, 9, 8, 7, 6, 6, 5, 4, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 7, 7};
// La fonction r2rAnalogWrite() positionne les sorties
// numeriques D0, D2, D4 et D6 en fonction de la valeur
// du bit 0, 1, 2 ou 3 de la valeur passee en parametre
void r2rAnalogWrite(uint8_t valeur) {
/*
digitalWrite(0, (valeur >> 0) & 0x01);
digitalWrite(2, (valeur >> 1) & 0x01);
digitalWrite(4, (valeur >> 2) & 0x01);
digitalWrite(6, (valeur >> 3) & 0x01);
*/
// Alternative plus rapide en ecrivant sur le port D (conprend D0 à D7)
PORTD = ((valeur & 0b1000) << 3) | ((valeur & 0b100) << 2) | ((valeur & 0b10) << 1) | (valeur & 1);
}
void setup() {
// Les broches numeriques D0, D2, D4 et D6 sont
// configurees comme des sorties
pinMode(0, OUTPUT);
pinMode(2, OUTPUT);
pinMode(4, OUTPUT);
pinMode(6, OUTPUT);
}
void loop() {
uint8_t index;
// Sors une tension sinusoidale en utilisant la fonction sin
for (index=0; index<tailleTableSinux ; index++) {
r2rAnalogWrite(round((sin(index*2.0*3.14/(tailleTableSinux-1))+1)*15/2));
delay(1);
}
/*
// Sors une tension sinusoidale en utilisant la table precalculee
// Cette methode doit etre privilegiee car elle est beaucoup plus
// rapide en temps processeur
for (index=0; index<tailleTableSinux ; index++) {
r2rAnalogWrite(tableSinus[index]);
delay(1);
}
/**/
}
Voici le résultat visualisé sur l’oscilloscope :
Cet article est disponible sur ma chaîne YouTube, n’hésitez pas à vous y abonner. Merci pour votre attention et à bientôt pour un nouvel article sur mon blog.
Le principe du PWM est de réduire la puissance moyenne délivrée d’une sortie digitale (0 ou 1) en modulant les impulsions du signal. L’objectif est d’avoir une pseudo sortie analogique pouvant prendre 256 valeurs (0 à 255). Le PWM est utilisé par exemple pour contrôler la luminosité d’une LED, changer la couleur d’une LED RGB ou encore piloter la vitesse d’un moteur.
Le signal est modulé avec une fréquence fixe (490 Hz sur la broche digitale 9 de l’Arduino Uno par exemple) . Le PWM est caractérisé par son rapport cyclique, qui correspond au pourcentage du temps pendant lequel le signal est à 1 par rapport au temps pendant lequel le signal est à 0.
Dans les illustrations ci-dessous, on voit 3 exemples de rapports cycliques :
Rapport cycle 25% : le signal est à 5 V pendant 1/4 du cycle
Rapport cycle 50% : le signal est à 5 V pendant la moitié du cycle
Rapport cycle 75% : le signal est à 5 V pendant les 3/4 du cycle
Un rapport cyclique de 100% correspond à un signal qui reste à 5 V, alors qu’un rapport cycle de 0% correspond à un signal qui reste à 0 V.
Programmation d’un signal PWM
Voici un exemple de code utilisé pour faire varier la luminosité d’une LED en utilisant le PWM.
// Article de blog Tutoduino : C'est quoi... le PWM ?
// https://tutoduino.fr/
// Copyleft 2020
#define ledPin 9 // La LED est reliée à la broche digitale 9 qui supporte le PWM (f=490 Hz)
void setup() {
// La broche 9 est une sortie digitale
pinMode(ledPin, OUTPUT);
}
void loop() {
uint8_t fadeValue; // Valeur du rapport cyclique
// Rapport cyclique du PWN à 100%
fadeValue = 255;
analogWrite(ledPin, fadeValue);
delay(1000);
// Rapport cyclique du PWN à 75%
fadeValue = 191;
analogWrite(ledPin, fadeValue);
delay(1000);
// Rapport cyclique du PWN à 50%
fadeValue = 127;
analogWrite(ledPin, fadeValue);
delay(1000);
// Rapport cyclique du PWN à 25%
fadeValue = 63;
analogWrite(ledPin, fadeValue);
delay(1000);
// Rapport cyclique du PWN à 0%
fadeValue = 0;
analogWrite(ledPin, fadeValue);
delay(1000);
// Fait varier le rapport cyclique de 0 à 100% par pas de 5
for (fadeValue = 0 ; fadeValue < 255; fadeValue += 5) {
analogWrite(ledPin, fadeValue);
// Attendre 50 ms pour l'effet de fondu
delay(50);
}
}
Dans la première partie du programme j’ai configuré 5 rapports cycliques différents (100%, 75%, 50%, 25% et 0%). On voit que la luminosité de la LED est forte lorsque le rapport cyclique est de 100% et qu’elle diminue par palier avec le rapport cyclique. Avec le rapport cyclique 0%, la LED est éteinte.
Dans la deuxième partie, j’ai programmé un rapport cyclique qui varie de 0% à 100% avec une attente de 50 ms entre chaque incrément. Cela donne un effet d’allumage de LED en mode fondu.
Voici la vidéo du résultat obtenu avec un oscilloscope relié à la broche 9 de l’Arduino Uno.
A quoi ça sert… un pont diviseur de tension ?
Un pont de tension permet de diviser une tension d’entrée à l’aide de 2 résistances en série. Il permet ainsi d’obtenir une tension de sortie U2 plus faible que la tension d’entrée U. Il est utilisé couramment pour créer une tension de référence.
Schéma d’un pont diviseur de tension
Utilisons la loi d’Ohm
En appliquant la loi d’Ohm et la loi d’additivité des tension le calcul est assez simple :
U = (R1 + R2) x I
d’où I = U / (R1 + R2)
Comme U2 = I x R2 ,nous avons U2 = (U / (R1 + R2) ) x R2
Ainsi le calcul du pont diviseur est simple :
U2 = U x R2 / (R1 + R2)
Calculateur de pont diviseur de tension
Bien que le calcul du pont diviseur soit simple, voici un petit calculateur en ligne pour vous simplifier la vie. Vous n’avez qu’à entrer la tension d’entrée (U en volt) et la valeur des 2 resistances (R1 et R2 en Ohm) , la tension de sortie (U2) est calculée automatiquement :
Utilisation du simulateur en ligne falstad
Il est possible de simuler un point diviseur avec un logiciel comme falstad. En cliquant sur ce lien ou sur l’image ci-dessous vous ouvrirez ce simulateur. Vous pourrez modifier la tension d’entrée ainsi que les valeurs des résistances pour visualiser immédiatement la tension de sortie. Je vous recommande vivement d’utiliser falstad pour simuler tous vos schémas, c’est un outil très puissant !
Exemple de simulation de pont diviseur avec falstad
Exemple d’application dans mes tutos
Une application importante du pont diviseur de tension dans mes tutoriels est son utilisation pour limiter la tension aux bornes des broches de l’Arduino. En effet, l’Arduino ne supporte pas une tension supérieure à 5 V sur ses broches.
Une deuxième application du pont diviseur est pour limiter la tension en entrée du convertisseur numérique / analogique de l’Arduino. Lorsque l’on utilise une tension de référence externe, cette tension doit être la valeur maximale de la tension en entrée du convertisseur analogique / numérique.
Dans le tutoriel Shield Arduino testeur de pile j’utilise une tension de référence de 2,5 V. Et j’utilise donc un pont diviseur avec des résistances de 300 kΩ et 200 kΩ pour limiter la tension de sortie à 2,5 V alors que la tension de la pile peut aller jusqu’à 6,25 V. Dans ce circuit, j’ai utilisé de grosses résistances (centaines de kΩ) car je ne souhaitais pas que du courant circule dans cette partie du circuit. Mais bien sûr en utilisant des résistances de 300 Ω et 200 Ω le pont de tension aurait réduit la tension de la même façon (6,25 V -> 2,5 V) puisque c’est le rapport entre les 2 résistances qui importe dans le calcul.
Merci pour votre attention, n’hésitez pas noter cet article en cliquant sur les étoiles ci-dessous 🙂
Comment utiliser… un écran OLED I2C 128×64 ?
Ajouter un petit écran OLED à un Arduino permet de le rendre autonome, il n’y a plus besoin qu’il soit relié à un ordinateur et d’utiliser le moniteur série pour afficher des informations. Le coût de l’écran est très bas, on en trouve à moins de 8€ sur certains sites marchands. Ces écrans sont en général équipés d’une puce SSD1306 ou équivalent. Je conseille les versions I2C de ces afficheurs car cela réduit le câblage (2 fils + alimentation).
Par contre sa programmation est plus difficile. Il existe plusieurs librairies dont la célèbre Adafruit SSD1306, mais elles ne sont en général pas adaptées aux petits Arduino car elles occupent trop de mémoire.
Je recommande vivement la librairie SSD1306Ascii de Bill Greiman car elle utilise très peu de mémoire et est très simple d’utilisation.
Voici un exemple de code pour afficher du texte sur l’écran OLED relié en I2C :
// Utiliser un afficheur OLED 0.96" avec un Arduino Uno
// https://tutoduino.fr/
// Copyleft 2020
// Librairie pour l'afficheur OLED
// https://github.com/greiman/SSD1306Ascii
#include "SSD1306Ascii.h"
#include "SSD1306AsciiAvrI2c.h"
#define I2C_ADDRESS 0x3C
SSD1306AsciiAvrI2c oled;
void setup() {
oled.begin(&Adafruit128x64, I2C_ADDRESS);
oled.setFont(Adafruit5x7);
oled.clear();
oled.set2X();
oled.println("Tutoduino");
oled.set1X();
oled.println("Apprendre");
oled.println("l'electronique");
oled.println("avec un Arduino");
}
void loop() {
}
Le résultat sur mon écran OLED est sympa !
Vous remarquez que la première ligne de l’écran est jaune alors que le reste du texte est bleu. Il s’agit d’une caractéristique de mon écran, je n’ai pas géré de couleurs dans le programme, il est d’ailleurs impossible sur mon écran de modifier ces couleurs.
Au niveau du câblage avec l’Arduino Uno c’est très simple avec l’I2C. Il suffit de brancher l’alimentation (5 V et GND) et les deux fils SCL et SDA.
Merci à la société Go Tronic pour son soutien dans la réalisation de ce tutoriel
C’est quoi… une tension de référence ?
Cet article explique ce qu’est une tension de référence, et comment utiliser une tension de référence interne ou externe (LM4040, TL431…) avec un Arduino.
L’Arduino Uno possède 6 entrées analogiques, que l’on peut lire dans un programme (croquis) grâce à la fonction :
analogRead()
Le convertisseur analogique-numérique (CAN) de l’Arduino convertit la tension d’entrée de la broche analogique en une valeur numérique sur 10 bits (0 à 1023). La valeur 0 correspond à une tension d’entrée de 0 V. La valeur 1023 correspond à la tension de référence utilisée par le convertisseur analogique-numérique.
La tension de référence est donc la tension qui est utilisée comme valeur maximale de la tension de l’entrée par le convertisseur analogique-numérique.
Par défaut la tension de référence de l’Arduino Uno est sa tension d’alimentation. Si l’Arduino Uno est alimenté par la prise USB, cette tension de référence ne sera pas précise. Une alimentation USB est de 5 V ± 5%, soit de 4,75 V à 5,25 V. Pour avoir une bonne précision sur la lecture d’une entrée analogique, il faut utiliser une tension de référence précise également.
Pour avoir une tension de référence précise il y a plusieurs solutions avec l’Arduino Uno :
Alimenter l’Arduino par le jack ou la broche Vin car le régulateur interne de 5 V est plus précis que l’alimentation USB en général
Utiliser la tension de référence interne de 1,1 V
Utiliser une tension de référence externe
Tension de référence interne de 1,1 V
Cette tension de référence de 1,1 V est interne au micro-contrôleur ATmega328P. Elle est indépendante de la tension d’alimentation de l’Arduino Uno. Il suffit d’appeler la fonction :
analogReference(INTERNAL)
pour l’utiliser comme tension de référence pour le convertisseur analogique-numérique.
Attention : Cette tension interne n’est pas exactement de 1,1 V et elle est différente pour chaque micro-contrôleur. Afin d’avoir une mesure la plus précise possible des entrées analogiques, je vous conseille de mesurer la valeur de cette tension interne. Il suffit de mesurer la tension sur la broche AREF à l’aide d’un voltmètre. la tension de référence est envoyée sur la broche AREF lorsque l’Arduino n’est pas configuré pour utiliser une tension de référence externe.
Ensuite il suffit de calculer la valeur de la tension de l’entrée analogique en utilisant la formule :
#define REFERENCE_INTERNE 1.098 // mesurée avec un voltmètre sur la broche AREF de l'Arduino UNO
float tension = analogRead(INTERNAL) * (REFERENCE_INTERNE / 1023)
Tension de référence externe
Si vous avez la possibilité d’utiliser une tension de référence externe, c’est la meilleure solution pour assurer une grande précision dans la mesure des entrées analogiques.
Le LM4040 apporte une précision de 0,1% et offre une excellente tension de référence externe pour l’Arduino
Il suffit de relier la référence de tension externe sur la broche AREF de l’Arduino UNO et de configurer le convertisseur en appelant la fonction :
analogReference(EXTERNAL)
Voici une vidéo qui démontre l’intérêt du composant LM4040 comme référence de tension externe sur un Arduino Uno :
Le régulateur de tension TL431 est un composant souvent utilisé comme référence de tension. Son prix est attractif et il est possible de configurer la tension de référence dans une plage de 2,50 V à 36 V.
Le plus simple et le moins coûteux est d’utiliser le TL431 comme une référence de tension de 2,5 V. La résistance de 1 kΩ assure un courant de cathode IKA de 2,5 mA qui correspond bien à la plage de valeurs indiquée dans la fiche technique du composant (1 à 100 mA).
Attention : L’Arduino Uno possède un condensateur de 100 nF entre AREF et GND, ce qui rend le TL431 totalement instable (voir captures d’écran en bas de l’article). Ce comportement du TL431 est parfaitement décrit dans cette note de TI. Avec un Arduino Uno, il faut donc ajouter un condensateur de 10 μF entre la sortie VREF du composant et la masse.
Il suffit ensuite de relier la broche 1 du TL431 (REF) sur l’entrée AREF de l’Arduino pour avoir une référence externe de 2,5 V.
L’avantage de l’utilisation de cette référence externe de 2,5 V par rapport à la référence interne de 1,1 V de l’Arduino est que sa précision est bien meilleure (1%). De plus il n’est pas nécessaire de calibrer cette tension de référence. Ce type de référence de tension externe est utilisé dans le tutoriel sur le Testeur de pile.
Captures d’écran de l’oscilloscope pour montrer l’instabilité de AREF sur Arduino Uno avec un composant TL431
Instabilité de AREF sur Arduino Uno avec l’utilisation du TL431 sans condensateur ajouté entre VREF et GNDAvec un condensateur de 10 μF entre VREF et GND, AREF est stable sur Arduino Uno avec l’utilisation du TL431
A quoi ça sert… une résistance de rappel ?
Cet article explique l’utilité d’une résistance de rappel (pull-up ou pull-down) et détaille sa mise en œuvre avec un Arduino
Un Arduino possède plusieurs entrées numériques ayant deux états :
État HAUT qui correspond à une entrée à 5 V sur l’Arduino Uno
État BAS qui correspond à une entrée à 0 V sur l’Arduino Uno
Lorsque la broche d’une entrée numérique n’est pas reliée (interrupteur ouvert par exemple), la tension sur cette entrée est flottante et son état peut varier aléatoirement entre les deux états HAUT et BAS.
Une résistance de rappel permet de figer l’état d’une entrée numérique lorsque elle n’est pas reliée, soit à un état HAUT (dans ce cas la résistance est dite “pull-up”), soit à un état BAS (dans ce cas la résistance est dite “pull-down”).
L’exemple le plus simple à comprendre est la lecture de l’état d’un bouton poussoir ou d’un interrupteur. Si l’interrupteur est relié au 5V, lorqu’il est fermé l’entrée de l’Arduino est bien à l’état HAUT. Par contre lorsque cet interrupteur est ouvert, la broche de l’entrée numérique de l’arduino n’est reliée à rien et son état est flottant et passe donc aléatoirement de l’état HAUT à l’état BAS.
Le microcontrôleur qui équipe l’Arduino intègre des résistances de rappel internes qui évitent l’utilisation de résistances externes. Pour activer la résistance de rappel interne sur une broche donnée, il suffit de l’indiquer grâce à la fonction pinMode. Dans l’exemple ci-dessous, la résistance de rappel interne est activée pour la broche 2.
void setup(){
pinMode (2, INPUT_PULLUP);
}
Résistance “pull-down”
En rajoutant une résistance comme dans le schéma ci-dessous entre l’entrée numérique de l’Arduino et la masse du circuit, l’entrée numérique est figée à 0 V lorsque l’interrupteur est ouvert.
Lorsque l’interrupteur est fermé, l’entrée passe bien à l’état haut et un petit courant de fuite circule dans la résistance (c’est pourquoi il faut choisir une résistance assez forte pour limiter ce courant). En général une résistance de 10 kΩ est utilisée.
Résistance “pull-up”
Le bon exemple d’une résistance “pull-up” est son utilisation pour le RESET du micro-contrôleur ATmega328P contrôlé par sa broche 1.
Comme indiqué dans la documentation du micro-contrôleur, un reset est généré par la mise à l’état BAS (LOW = 0 V) de la broche RESET. Cette broche doit donc être à l’état HAUT (HIGH = 5 V) lorsque le micro-contrôleur fonctionne.
Si nous relions la broche sur le 0 V du circuit, il sera impossible de la faire passer à l’état HAUT. Et si nous relions le broche sur le 5 V du circuit, il sera impossible de la faire passer à l’état BAS…
C’est bien tout l’intérêt d’utiliser une résistance “pull-up” dans ce cas.
Lorsque l’interrupteur (ou une sortie de transistor) est ouvert, la broche sera à 5 V, le reset sera désactivé et donc le micro-contrôleur fonctionnera bien.
Lorsque l’interrupteur sera fermé, la broche sera à 0 V et le reset sera activé.
Voir dans le tutoriel Arduino à faible consommation un exemple d’utilisation de pull-up pour le reset de l’ATmega238P.