Lilygo T-Display S3 sous IDE Arduino

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-DisplayLILYGO T-Display S3
MicrocontrôleurESP32 (Tensilica Xtensa LX6)ESP32S3 (Tensilica Xtensa LX7)
Écran135 x 240170 x 320
SRAM448 Ko512 Ko
FLASH4 Mo16 Mo
PSRAM8 Mo
TFT DriverST7789VST7789
TFT Interface SPISPI + 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 :

Plaintext
https://espressif.github.io/arduino-esp32/package_esp32_index.json

Voici une capture d’écran des étapes à suivre :

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.

Installation des librairies pour l’écran LCD

Installer la librairie TFT_eSPI by Bodmer.

É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'écran
TFT_eSPI tft = TFT_eSPI();

// -----------------------------------------------------------------------------
//  Mise en veille profonde de l'ESP32 et de l'ecran LCD
// -----------------------------------------------------------------------------
void enterDeepSleep() {
  // Configure la sortie de veille par appui sur le bouton
  esp_sleep_enable_ext0_wakeup((gpio_num_t)14, LOW);

  // Éteindre écran et désactiver son alimentation
  digitalWrite(38, LOW);
  digitalWrite(15, LOW);

  // Activer la veille profonde du microcontrolleur
  esp_deep_sleep_start();
}

void setup() {
  // Configuration de la broche 38 (rétroéclairage) en sortie
  pinMode(38, OUTPUT);
  // Allumer le rétroéclairage (HIGH = allumé)
  digitalWrite(38, HIGH);

  // Configuration de la broche 15 (activation de l'écran) en sortie
  pinMode(15, OUTPUT);
  // Activer l'écran (HIGH = activé)
  digitalWrite(15, HIGH);
  
  // Initialisation de l'écran TFT
  tft.init();

  // Rotation de l'écran (horizontal connecteur usb à gauche)
  tft.setRotation(3);

  // Effacer l'écran en le remplissant de noir
  tft.fillScreen(TFT_BLACK);

  // Affichage des différents messages sur l'écran
  tft.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 Flash
  uint32_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ésente
  if (psramFound()) {
    // Lire la taille de la PSRAM et l'afficher sur l'écran
    uint32_t psramSize = ESP.getPsramSize();
    tft.setCursor(10, 110);
    tft.setTextColor(TFT_CYAN);
    tft.print("PSRAM: ");
    tft.print(psramSize / (1024 * 1024));  // Conversion en Mo
    tft.println(" Mo");

  } else {
    // Si la PSRAM n'est pas détectée, afficher un message
    tft.setCursor(10, 110);
    tft.setTextColor(TFT_CYAN);
    tft.println("No PSRAM detected");
  }
}

void loop() {
  // Attendre 1 minute et entrer en veille profonde
  delay(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 TFT
TFT_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 tension
void initVoltageMeasurement() {
  // Résolution 12 bits et atténuation 11 dB (mesure jusqu'à 2600 mV)
  // Il s'agit des valeurs par défaut, objectif uniquement pédagogique
  analogSetPinAttenuation(LCD_BAT_VOLT, ADC_11db);
  analogReadResolution(12);
}

// Lecture de la tension batterie
float readBatteryVoltage() {
  int adcValue = analogRead(LCD_BAT_VOLT);
  return (adcValue / ADC_MAX) * ADC_REF * DIVIDER * ADC_CORR;
}

void setup() {
  // Configurer rétroéclairage et écran en sortie
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  // Allumer rétroéclairage
  pinMode(TFT_ON, OUTPUT);
  digitalWrite(TFT_ON, HIGH);  // Activer écran

  // Initialiser la mesure de tension
  initVoltageMeasurement();

  // Initialiser l'écran TFT
  tft.init();
  // Rotation de l'écran (connecteur USB à gauche)
  tft.setRotation(3);
}

void loop() {
  // Mesurer et afficher la tension batterie toutes les 10 secondes
  tft.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>

const char *ssid = "TP-Link_21B7";
const char *passphrase = "R3seauS3cur1t3IoT!";

void setup() {
  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());
}

void loop() {
  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 :

Bash
espsecure.py generate_flash_encryption_key my_key.bin

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.

Bash
espefuse --port /dev/ttyUSB0 burn_key flash_encryption my_key.bin

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/ttyUSB0 burn_key flash_encryption my_key.bin
espefuse v4.11.dev2
Connecting.....
Detecting chip type... ESP32

=== Run "burn_key" command ===
Sensitive data will be hidden (see --show-sensitive-info)
Burn keys to blocks:
 - BLOCK1 -> [?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??]
	Reversing the byte order
	Disabling read to key block
	Disabling write to key block

Burn keys in efuse blocks.
The key block will be read and write protected (no further changes or readback) 


Check all blocks for burn...
idx, BLOCK_NAME,          Conclusion
[00] BLOCK0               is not empty
	(written ): 0x0000000400000000000014380000b00000a5840d8e19b63400000000
	(to write): 0x00000000000000000000000000000000000000000000000000010080
	(coding scheme = NONE)
[01] BLOCK1               is empty, will burn the new value
. 
This is an irreversible operation!
Type 'BURN' (all capitals) to continue.
BURN
BURN BLOCK1  - OK (write block == read block)
BURN BLOCK0  - OK (all write block bits are set)
Reading updated efuses...
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/ttyUSB0 summary

Renvoie des points d’interrogation pour BLOCK1, la clé de chiffrement est correctement protégée en lecture.

Bash
BLOCK1 (BLOCK1)                                    Flash encryption key                              
   = ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? -/- 

#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 : Compteur qui 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).
Bash
espefuse --port /dev/ttyUSB0 burn_efuse FLASH_CRYPT_CNT 1
espefuse --port /dev/ttyUSB0 burn_efuse FLASH_CRYPT_CONFIG 0x0F

Vérifions les valeurs des fusibles électroniques à cette étape :

Bash
espefuse --port /dev/ttyUSB0 summary

FLASH_CRYPT_CNT est défini sur 1 et FLASH_CRYPT_CONFIG est défini sur 0xf, cela semble correct.

Bash
Flash fuses:
FLASH_CRYPT_CNT (BLOCK0)                           Flash encryption is enabled if this field has an o = 1 R/W (0b0000001)
                                                   dd number of bits set                             
FLASH_CRYPT_CONFIG (BLOCK0)                        Flash encryption config (key tweak bits)           = 15 R/W (0xf)

#4. Activer le chiffrement de la partition NVS

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.

Bash
$ espsecure encrypt_flash_data \
  --keyfile my_key.bin \
  --address 0x1000 \
  --output bootloader_enc.bin \
  bootloader.bin  

$ espsecure encrypt_flash_data \
  --keyfile my_key.bin \
  --address 0x8000 \
  --output partitions_enc.bin \
  partitions.bin  
  
$ espsecure encrypt_flash_data \
  --keyfile my_key.bin \
  --address 0xe000 \
  --output boot_app0_enc.bin \
  boot_app0.bin    
    
$ espsecure encrypt_flash_data \
  --keyfile my_key.bin \
  --address 0x10000 \
  --output firmware_enc.bin \
  firmware.bin  

#6. Flasher le firmware chiffré dans la mémoire Flash de l’ESP32

Flasher tous les binaires chiffrés à l’étape précédente dans la mémoire flash de l’ESP32 :

Bash
esptool --chip esp32 --port /dev/ttyUSB0 \
  write_flash \
  0x1000  bootloader_enc.bin \
  0x8000  partitions_enc.bin \
  0xe000  boot_app0_enc.bin \
  0x10000 firmware_enc.bin

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 pointeur

void setup() {
  Serial.begin(115200);
  pointeur = (char*)malloc(200 * sizeof(char));  // allocation dynamique de 200 octets en SRAM
  if (pointeur == NULL) {
    Serial.println("Erreur d'allocation mémoire!");
  } else {
    free(pointeur);
    pointeur = NULL;
  }
}

void loop() {
  delay(1000);
}

Une fois compilé, le programme occupe 2264 octets de Flash. 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
 * @param variableName Name of the variable for display
 * @param variable Pointer to the variable to check
 */
void printAddress(const char* variableName, void* variable) {
  Serial.print("Memory address of '");
  Serial.print(variableName);
  Serial.print("' is: 0x");
  Serial.println((uintptr_t)variable, HEX);
}

void setup() {
  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 connect
  Serial.println("\n=== SRAM vs PSRAM usage on ESP32C5 ===");

  // CHECK PSRAM AVAILABILITY AND STATUS
  size_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 byte

  if (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 leaks
    free(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");
  }
}

void loop() {
  // 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 SRAM
Memory 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 SRAM
Memory 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 PSRAM
Memory 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 à jour OTA, 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 :

Bash
~/.arduino15/packages/esp32/hardware/esp32/3.3.6/tools/partitions

Voici un exemple des schémas de partitions disponibles pour la version 3.3.6 du core ESP32.

Schémas de partitions stockés dans le répertoire d’installation du core ESP32 pour Arduino

Ces fichiers au format CSV contiennent la définition des partitions :

  • Name : Identifiant de la partition (ex : app0, spiffs).
  • Type : Type de partition (app, data).
  • SybType : Sous-type (ota_0, ota_1, spiffs, fatfs, etc.).
  • 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.).

Je vous recommande la lecture de la documentation dédiée au tables de partition sur le site Espressif : https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/partition-tables.html

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) :

Plaintext
# Name,   Type, SubType, Offset,   Size, Flags
nvs,      data, nvs,     0x9000,   0x5000,
otadata,  data, ota,     0xe000,   0x2000,
app0,     app,  ota_0,   0x10000,  0x140000,
spiffs,   data, spiffs,  0x150000, 0xA0000,
coredump, data, coredump,0x1F0000, 0x10000,

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 :

Plaintext
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x5000,
otadata,  data, ota,     0xe000,  0x2000,
app0,     app,  ota_0,   0x10000, 0x300000,
spiffs,   data, spiffs,  0x310000,0xE0000,
coredump, data, coredump,0x3F0000,0x10000,

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é

Île de France Mobilité met à disposition la Plateforme Régionale d’Information pour la Mobilité (PRIM). Elle rassemble de nombreuses données relatives à la mobilité en Île-de-France.

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 :

Exemple de recherche de l’identifiant de l’arrêt Pierre Semard dans la ville de Châtillon :

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 :

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) :

Bash
curl -X 'GET' 'https://prim.iledefrance-mobilites.fr/marketplace/stop-monitoring?MonitoringRef=STIF:StopPoint:Q:28607:' -H 'accept: application/json' -H 'apikey: YOUR-TOKEN'

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).

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-Display
Adafruit_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'heure
struct myTime_t {
  int heure;
  int minute;
};

// Structure pour stocker les informations sur les prochains passages
struct busSchedule_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"
const char* serviceUrl = "https://prim.iledefrance-mobilites.fr/marketplace/stop-monitoring?MonitoringRef=";

// Variable globale utilisée pour la mise en veille
uint8_t loopCounter;


// ---------------------------------------------------------------------------
//  Vérifie si time1 est superieur ou egal a time2 (seulement heure/minute)
// ---------------------------------------------------------------------------
bool isMyTimeGreaterOrEqual(myTime_t time1, tm time2) {
  // Compare heures
  if (time1.heure != time2.tm_hour) {
    return time1.heure > time2.tm_hour;
  }

  // Heures égales → compare minutes
  return time1.minute >= time2.tm_min;
}

// ---------------------------------------------------------------------------
//  Synchronisation de l'heure NTP + fuseau France (DST auto)
// ---------------------------------------------------------------------------
void setupTime() {
  // Fuseau France normalisé : CET-1CEST,M3.5.0/2,M10.5.0/3
  configTzTime("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_t convertUTCtoLocal(myTime_t utcTime) {
  struct tm localTime = { 0 };
  char buffer[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) {
    case 0:  // heure d'hiver = UTC + 1
      myLocalTime.heure += 1;
      break;
    case 1:  // heure d'ete = UTC + 2
      myLocalTime.heure += 2;
      break;
    default:  // indetermine ou erreur, retourne l'heure UTC
      break;
  }

  // Gestion dépassement 24h
  if (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_t getTimeHHMM(const char* isoString) {

  myTime_t myTime;

  // JJ et MM sont a des positions fixes dans ISO 8601
  myTime.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.
// -----------------------------------------------------------------------------
int getExpectedDepartureTime(busSchedule_t schedules[MAX_NB_BUS_SCHEDULE]) {

  // Nombre d'horaire retourné
  int nbScheduleInfo = 0;

  // Initialise les MAX_NB_BUS_SCHEDULE prochains bus
  for (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 connecte
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("ERROR: Wi-Fi not connected.");
    return 0;
  }

  // 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();
    return 0;
  }

  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 !");
    return 0;
  }

  // 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 elements
  for (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 bus
    const char* lineRef = visit["MonitoredVehicleJourney"]["LineRef"]["value"];
    // Horaire du prochain depart
    const char* expectedDepartureTime =
      visit["MonitoredVehicleJourney"]["MonitoredCall"]["ExpectedDepartureTime"];

    if (!lineRef || !expectedDepartureTime) {
      Serial.println("No data");
      continue;
    }

    // Vérifie que nous souhaitons les informations concernant cette ligne de bus
    bool 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 interesse
    if (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
// -----------------------------------------------------------------------------
void enterDeepSleep() {
  // Configure la sortie de veille par appui sur bouton
  esp_sleep_enable_ext0_wakeup((gpio_num_t)BUTTON_PIN, LOW);

  // Éteindre écran et le mettre en veille
  digitalWrite(TFT_BL, LOW);
  tft.writeCommand(0x10);

  // Activer la veille profonde du microcontrolleur
  esp_deep_sleep_start();
}

// ---------------------------------------------------------------------------
//  SETUP
// ---------------------------------------------------------------------------
void setup() {

  Serial.begin(115200);
  delay(1000);

  // Le bouton est une entrée qui permet de sortir de veille
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // Connexion Wi-Fi
  WiFi.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 horaire
  setupTime();  // NTP + TZ

  // Allume le rétroéclairage
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);

  // Initialisation écran
  tft.init(135, 240);
  tft.setRotation(1);
  tft.setFont(&FreeSansBold18pt7b);
  delay(1000);

  loopCounter = 0;
}


// ---------------------------------------------------------------------------
//  BOUCLE PRINCIPALE
// ---------------------------------------------------------------------------
void loop() {

  busSchedule_t nextBuses[MAX_NB_BUS_SCHEDULE];
  struct tm timeinfo;
  char timeString[6];
  char buf[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 locale
  if (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 bus
  int 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 affiches
  int 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 actuelle
    if (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 secondes
  delay(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'arret
const char* stopPointRef = "STIF:StopPoint:Q:28607:";

// Structure pour stocker le mapping
struct LineMapping {
  String ref;     // LineRef PRIM
  String number;  // numero de la ligne
};

// Tableau de correspondance entre les references PRIM des lignes et le numero des lignes
static const 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 tableau
static const int lineMappingsSize = sizeof(lineMappings) / sizeof(LineMapping);

// Fonction pour récupérer le numéro public de la ligne de bus à partir de son identifiant LineRef
inline String mapLineRefToNumber(const String& lineRef) {
  for (int i = 0; i < lineMappingsSize; i++) {
    if (lineMappings[i].ref == lineRef) {
      return lineMappings[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.

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 :

Plaintext
https://espressif.github.io/arduino-esp32/package_esp32_index.json

Voici une capture d’écran des étapes à suivre :

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 :

C++
// LilyGo T-Display
// https://tutoduino.fr/
// Copyleft 2025

#include <Adafruit_GFX.h>             // Core graphics library
#include <Adafruit_ST7789.h>          // Hardware-specific library for ST7789
#include <Fonts/FreeSansBold18pt7b.h> // GFX Font

// ST7789 pinout on 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

Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

void setup() {

  // Turn on backlight
  pinMode(TFT_BL, OUTPUT);      
  digitalWrite(TFT_BL, HIGH);

  // Init ST7789 135x240 resolution
  tft.init(135, 240); 

  // Display text
  tft.setRotation(1);
  tft.setFont(&FreeSansBold18pt7b);
  tft.setCursor(30, 70);
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_CYAN);
  tft.write("Tutoduino");
}

void loop() {
  delay(1000);
}

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-esp32c6
  friendly_name: capteur-temp-esp32c6   

esp32:
  board: esp32-c6-devkitc-1
  framework:
    type: esp-idf

# Enable logging only for warnings
logger:
  level: WARN

# Enable Home Assistant API
api:
  encryption:
    key: "your-key"

ota:
  - platform: esphome
    password: "your-pwd"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_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_1    

switch:
  - platform: gpio
    pin: GPIO3
    id: rf_switch
    name: "RF Switch"
    internal: true    
    restore_mode: ALWAYS_OFF

  - platform: gpio
    pin: GPIO14
    id: antenna_select
    name: "Antenna Select"
    internal: true    
    restore_mode: ALWAYS_ON # Uses external antenna

i2c:
  sda: GPIO22
  scl: GPIO23
  scan: False

sensor:
  - platform: bme280_i2c
    id: bme280_measure
    temperature:
      name: "Temperature BME280"
    pressure:
      name: "Pressure BME280"
    humidity:
      name: "Humidity BME280"
    address: 0x76
    update_interval: never    

  - platform: adc
    pin: GPIO0
    name: "Battery Voltage"
    id: battery_voltage
    update_interval: never
    attenuation: 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 power
deep_sleep:
  id: deep_sleep_1
  sleep_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 :

YAML
on_connect:
  # Update sensors
  - component.update: bme280_measure
  - component.update: battery_voltage

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 power
deep_sleep:
  id: deep_sleep_1
  sleep_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_signal
    name: "Signal WiFi"
    id: wifi_signal_strength
    update_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: !secret wifi_ssid
  password: !secret wifi_password
  fast_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 :

YAML
switch:
  - platform: gpio
    pin: GPIO3
    id: rf_switch
    name: "RF Switch"
    internal: true    
    restore_mode: ALWAYS_OFF

  - platform: gpio
    pin: GPIO14
    id: antenna_select
    name: "Antenna Select"
    internal: true    
    restore_mode: ALWAYS_ON # Uses external antenna

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.

Je vous invite à consulter mes autres tutoriels ESPHome : Intégrer un reTerminal E1002 dans Home Assistant avec ESPHome et Home Assistant : Afficher les données Météo-France sur un reTerminal E1001 avec ESPHome.

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çuInterprétation
-30 dBmSignal très fort, garanti une excellente connexion
-50 dBmSignal excellent, adapté à la plupart des usages exigeants (streaming…)
-65 dBmSignal acceptable pour une connexion stable et fiable
-90 dBmSignal 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.

La perte liée à la distance se calcule avec la formule Free Space Path Loss (FSPL) :

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.

Pour mieux comprendre la configuration du reTerminal et l’affichage d’informations issues de Home Assistant, je vous recommande la lecture de mon précédent tutoriel Intégrer un reTerminal E1002 dans Home Assistant avec ESPHome.

Intégration Météo-France de Home Assistant

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 :

YAML
action: weather.get_forecasts
entity_id: weather.nantes
data:
  type: daily
target:
  entity_id: weather.nantes

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_temperature
sensor:
  - platform: homeassistant
    id: temperature_nantes
    entity_id: sensor.nantes_temperature

Vous pouvez afficher la valeur de ce capteur sur l’écran du reTerminal E1001 via la configuration ESPHome suivante :

YAML
it.printf(20, 50, id(myFont), "Temperature: %.1f °C", id(temperature_nantes).state);

Affichage de la température intérieure

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 interface
i2c:
  sda: GPIO19
  scl: GPIO20
  scan: false
# temperature and humidity sensor
sensor:
  - platform: sht4x
    temperature:
      name: "Temperature"
      id: sht4x_temperature
    humidity:
      name: "Humidity"
      id: sht4x_humidity    
    address: 0x44
    update_interval: 60s

Vous pouvez afficher la valeur de ce capteur sur l’écran du reTerminal E1001 via la configuration ESPHome suivante :

YAML
it.printf(10, 10, id(myFont), BLUE, "Temperature: %.1f°C", id(sht4x_temperature).state);  
it.printf(10, 40, id(myFont), BLUE, "Humidity: %.1f%%", id(sht4x_humidity).state); 

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.

Puis de déclarer ce capteur dans la configuration ESPHome.

YAML
sensor:
  - platform: homeassistant
    name: "Temperature Serre"
    entity_id: sensor.lumi_lumi_weather_temperature_2
    id: temperature_serre

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) :

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 :

YAML
template:
  - trigger:
      - platform: time_pattern
        hours: /1
      - platform: homeassistant
        event: start
    action:
      - service: weather.get_forecasts
        data:
          type: hourly
        target:
          entity_id: weather.nantes
        response_variable: hourly

    sensor:
      - name: "Prévisions Météo"
        unique_id: previsions_meteo_nantes
        state: "{{ hourly['weather.nantes'].forecast[0] }}"
        attributes:
          forecast: "{{ hourly['weather.nantes'].forecast }}"
        availability: "{{ states('weather.nantes') not in ['unknown', 'unavailable', 'none'] }}"

  - sensor:
      - name: "Météo Jour Nantes"
        unique_id: meteo_jour_nantes
        state: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[0] }}"
        attributes:
          # Jour_0
          condition0: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[0].condition | default(0) }}"
          jour0: >
            {{ ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'][as_timestamp(state_attr('sensor.previsions_meteo_nantes','daily_forecast')[0].datetime) | timestamp_custom('%w', true) | int] }}
          temperature0: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[0].temperature | float(0) }}"
          templow0: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[0].templow | float(0) }}"
          precipitation0: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[0].precipitation | float(0) }}"          
          # Jour_1
          condition1: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[1].condition | default(0) }}"
          jour1: >
            {{ ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'][as_timestamp(state_attr('sensor.previsions_meteo_nantes','daily_forecast')[1].datetime) | timestamp_custom('%w', true) | int] }}
          temperature1: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[1].temperature | float(0) }}"
          templow1: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[1].templow | float(0) }}"
          precipitation1: "{{ state_attr('sensor.previsions_meteo_nantes','daily_forecast')[1].precipitation | float(0) }}"                    

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.

YAML
text_sensor:
  - platform: homeassistant
    name: "Jour0"
    entity_id: sensor.meteo_jour_nantes
    attribute: jour0
    id: jour0
  - platform: homeassistant
    name: "Jour1"
    entity_id: sensor.meteo_jour_nantes
    attribute: jour1
    id: jour1
    name: "ConditionJ0"
    entity_id: sensor.meteo_jour_nantes
    attribute: condition0
    id: conditionJ0
  - platform: homeassistant
    name: "ConditionJ1"
    entity_id: sensor.meteo_jour_nantes
    attribute: condition1
    id: conditionJ1
  - platform: homeassistant    

Les températures minimales et maximales étant des valeurs à virgule flottante, elle doivent être définies comme sensor.

YAML
sensor:
  - platform: homeassistant
    name: "TemperatureJ0"
    entity_id: sensor.meteo_jour_nantes
    attribute: temperature0
    id: temperatureJ0
    accuracy_decimals: 1  
  - platform: homeassistant
    name: "TemperatureJ1"
    entity_id: sensor.meteo_jour_nantes
    attribute: temperature1
    id: temperatureJ1
    accuracy_decimals: 1
    
  - platform: homeassistant
    name: "TemplowJ0"
    entity_id: sensor.meteo_jour_nantes
    attribute: templow0
    id: templowJ0
    accuracy_decimals: 1
  - platform: homeassistant
    name: "TemplowJ1"
    entity_id: sensor.meteo_jour_nantes
    attribute: templow1
    id: templowJ1
    accuracy_decimals: 1


  - platform: homeassistant
    name: "PrecipitationJ0"
    entity_id: sensor.meteo_jour_nantes
    attribute: precipitation0
    id: precipitationJ0
    accuracy_decimals: 1
  - platform: homeassistant
    name: "precipitationJ1"
    entity_id: sensor.meteo_jour_nantes
    attribute: precipitation1
    id: precipitationJ1
    accuracy_decimals: 1

Affichage des prévisions horaires

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.

YAML
template:
  - trigger:
      - platform: time_pattern
        hours: /1
      - platform: homeassistant
        event: start
    action:
      - service: weather.get_forecasts
        data:
          type: hourly
        target:
          entity_id: weather.nantes
        response_variable: hourly

    sensor:
      - name: "Prévisions Météo"
        unique_id: previsions_meteo_nantes
        state: "{{ hourly['weather.nantes'].forecast[0] }}"
        attributes:
          forecast: "{{ hourly['weather.nantes'].forecast }}"
        availability: "{{ states('weather.nantes') not in ['unknown', 'unavailable', 'none'] }}"

  - sensor:
      - name: "Météo Heure Nantes"
        unique_id: meteo_heure_nantes
        state: "{{ state_attr('sensor.previsions_meteo_nantes','forecast')[0] }}"
        attributes:
          # Heure_0
          condition0: "{{ state_attr('sensor.previsions_meteo_nantes','forecast')[0].condition | default(0) }}"
          heure0: "{{ as_timestamp(state_attr('sensor.previsions_meteo_nantes','forecast')[0].datetime) | int(0) | timestamp_custom('%H:%M', true) }}"
          temperature0: "{{ state_attr('sensor.previsions_meteo_nantes','forecast')[0].temperature | float(0) }}"
          precipitation0: "{{ state_attr('sensor.previsions_meteo_nantes','forecast')[0].precipitation | float(0) }}"
          # Heure_1
          condition1: "{{ state_attr('sensor.previsions_meteo_nantes','forecast')[1].condition | default(0) }}"
          heure1: "{{ as_timestamp(state_attr('sensor.previsions_meteo_nantes','forecast')[1].datetime) | int(0) | timestamp_custom('%H:%M', true) }}"
          temperature1: "{{ state_attr('sensor.previsions_meteo_nantes','forecast')[1].temperature | float(0) }}"
          precipitation1: "{{ state_attr('sensor.previsions_meteo_nantes','forecast')[1].precipitation | float(0) }}"

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.

YAML
text_sensor:
  - platform: homeassistant
    name: "Condition0"
    entity_id: sensor.meteo_heure_nantes
    attribute: condition0
    id: condition0
  - platform: homeassistant
    name: "Condition1"
    entity_id: sensor.meteo_heure_nantes
    attribute: condition1
    id: condition1
 
  - platform: homeassistant
    name: "Heure0"
    entity_id: sensor.meteo_heure_nantes
    attribute: heure0
    id: heure0
  - platform: homeassistant
    name: "Heure1"
    entity_id: sensor.meteo_heure_nantes
    attribute: heure1
    id: heure1    

Les précipitations et la température, étant des valeurs à virgule flottante, doivent être définies comme sensor.

Cet exemple illustre une configuration ESPHome pour des capteurs affichant la température et les précipitations pour les deux prochaines heures.

YAML
sensor:     
  - platform: homeassistant
    name: "Temperature0"
    entity_id: sensor.meteo_heure_nantes
    attribute: temperature0
    id: temperature0
    accuracy_decimals: 1
  - platform: homeassistant
    name: "Temperature1"
    entity_id: sensor.meteo_heure_nantes
    attribute: temperature1
    id: temperature1
    accuracy_decimals: 1
    
  - platform: homeassistant
    name: "Precipitation0"
    entity_id: sensor.meteo_heure_nantes
    attribute: precipitation0
    id: precipitation0
    accuracy_decimals: 1
  - platform: homeassistant
    name: "Precipitation1"
    entity_id: sensor.meteo_heure_nantes
    attribute: precipitation1
    id: precipitation1
    accuracy_decimals: 1

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.

YAML
esphome:
  name: reterminal
  friendly_name: reTerminal
  on_boot:
    priority: 600
    then:
      - output.turn_on: bsp_battery_enable
      - delay: 500ms   
      
captive_portal:

output:
  - platform: gpio
    pin: GPIO21
    id: bsp_battery_enable
    
sensor:
  # ADC for battery voltage measurement
  - platform: adc
    pin: GPIO1
    name: "Battery Voltage"
    id: battery_voltage
    update_interval: 600s      
    attenuation: 12db
    filters:
      - multiply: 2.0  # Voltage divider compensation

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Ω.

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_signal
    name: "Signal WiFi ReTerminal"
    id: wifi_signal_strength
    update_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: homeassistant
    id: temperature_d0
    entity_id: sensor.daily_weather_forecast_sensor
    attribute: temperature0
    on_value:
      then:
        # Update the Wifi signal measurement and refresh the e-paper display whenever this sensor updates
        - 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 glyph correspondant 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.

YAML
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: icon_font_20
    size: 20
    glyphs:
      - "\U000F12A3" # battery
      - "\U000F058C" # water
      - "\U000F050F" # thermometer
      - "\U000F1442" # clock

Ensuite vous pouvez afficher l’icône à partir de son code de cette façon :

YAML
      it.printf(0, 0+85, id(icon_font_20), "\U000F050F");  // icone thermometre

Veille profonde

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 power
deep_sleep:
  id: deep_sleep_1              # ID for deep sleep component
  run_duration: 30s             # Time to stay awake after wake-up
  sleep_duration: 30min         # Time to sleep between wake-ups
  wakeup_pin: GPIO3             # GPIO pin to wake up device
  wakeup_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: 600
    then:
      # 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 platform
    id: sht4x_component         # ID for the component
    temperature:                # Temperature sensor
      name: "Temperature"       # Sensor name
      id: sht4x_temperature     # ID for temperature sensor
    humidity:                   # Humidity sensor
      name: "Humidity"          # Sensor name
      id: sht4x_humidity        # ID for humidity sensor
    address: 0x44               # I2C address
    update_interval: never      # Disable automatic updates (manual read only)

Désactiver le rafraichissement automatique (update_interval: never) de l’écran :

YAML
display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.50inv2
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: never

Et enfin rafraîchir l’écran une fois qu’une donnée Home Assistant est reçue :

YAML
  - platform: homeassistant
    name: "TemperatureJ0"
    entity_id: sensor.meteo_jour_nantes
    attribute: temperature0
    id: temperatureJ0
    accuracy_decimals: 1
    on_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.

Je vous recommande la lecture du test des reTerminal E1001 et E1002 et je remercie son auteur WarC0zes pour son aide sur le forum HACF.

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.

Cet appareil est particulièrement bien adapté pour l’affichage des données issues du capteur Zigbee que j’ai développé dans mon tutoriel Créer un capteur de température Zigbee sur batterie avec un ESP32C6 et que j’ai intégré à Home Assistant.

Le reTerminal E1002 peut être configuré et personnalisé en adoptant trois approches distinctes :

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 :

Plaintext
https://espressif.github.io/arduino-esp32/package_esp32_index.json

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 pinouts
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));

// Create a dedicated SPI bus object for display communication
SPIClass hspi(HSPI);

/**
 * @brief Draws "Hello World!" in red at coordinates (100, 100) on the ePaper display.
 */
void helloWorld()
{
  display.setRotation(0);  // No rotation, natural orientation
  display.setFont(&FreeMonoBold12pt7b); // Use the bold 12pt monospace font
  display.setTextColor(GxEPD_RED);      // Set text color to red (one of supported display colors)
  display.setFullWindow();              // Use the full display area for drawing
  display.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 position
    display.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!".
 */
void setup()
{
  pinMode(EPD_RES_PIN, OUTPUT);      // Configure reset pin as output
  pinMode(EPD_DC_PIN, OUTPUT);       // Configure data/command pin as output
  pinMode(EPD_CS_PIN, OUTPUT);       // Configure chip-select pin as output

  // Initialize SPI bus for display with specified pins, speed, and settings
  hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);
  display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));

  // Initialize the ePaper display hardware and driver
  display.init(0);

  // Display "Hello World!" message
  helloWorld();

  // Put the display into low-power (hibernate) mode after rendering
  display.hibernate();
}

// No loop code needed. The display only updates once at startup in this demo.
void loop() {};

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)
SPIClass hspi(HSPI);
// Initialize the display object with the specified driver and buffer size
GxEPD2_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.
 * @param temperature A string representing the temperature to display.
 */
void displayTemperature(const char* temperature) {
  Serial1.print("Temperature to display: ");
  Serial1.println(temperature);

  // Configure display settings
  display.setRotation(0);  // Set display orientation to default (0 degrees)
  display.setFont(&FreeMonoBold12pt7b);  // Set font to FreeMonoBold12pt7b
  display.setTextColor(GxEPD_RED);  // Set text color to red
  display.setFullWindow();  // Use the full display window for rendering

  // Start the display update process
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);  // Clear the screen with white background
    display.setCursor(100, 100);  // Set cursor position to (100, 100)
    display.print("Temperature = ");
    display.print(temperature);  // Print the temperature value
    display.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 connected
  if (WiFi.status() != WL_CONNECTED) {
    Serial1.println("ERROR: Wi-Fi not connected.");
    return strdup("Error");  // Return an error message
  }

  // Initialize HTTP client and configure the request
  HTTPClient http;
  http.begin(ha_url);  // Set the target URL from secrets.h
  http.addHeader("Authorization", "Bearer " + String(ha_token));  // Add authorization header
  http.addHeader("Content-Type", "application/json");  // Set content type to JSON

  // Send a GET request to the Home Assistant API
  int httpCode = http.GET();
  if (httpCode != HTTP_CODE_OK) {
    Serial1.print("HTTP Error: ");
    Serial1.println(httpCode);
    Serial1.println(http.getString());  // Print the error response
    http.end();  // Close the connection
    return strdup("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 connection

  if (error) {
    Serial1.print("JSON parsing error: ");
    Serial1.println(error.c_str());
    return strdup("Error");  // Return an error message if JSON parsing fails
  }

  // Check if the "state" field exists in the JSON response
  if (!doc.containsKey("state")) {
    Serial1.println("ERROR: 'state' field missing in JSON response.");
    return strdup("Error");  // Return an error message if "state" is missing
  }

  const char* state = doc["state"];  // Extract the temperature value from the JSON
  Serial1.print("Temperature = ");
  Serial1.println(state);

  // Return a dynamically allocated copy of the temperature string
  return strdup(state);
}

/**
 * @brief Prints the reason for waking up from deep sleep.
 */
void printWakeupReason() {
  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.
 */
void setup() {
  // Initialize Serial1 for debugging
  Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
  delay(1000);  // Wait for serial port to initialize
  Serial1.println("System starting...");
  printWakeupReason();  // Print the reason for waking up

  // Configure the green button as an input with pull-up resistor
  pinMode(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 outputs
  pinMode(EPD_RES_PIN, OUTPUT);
  pinMode(EPD_DC_PIN, OUTPUT);
  pinMode(EPD_CS_PIN, OUTPUT);

  // Initialize SPI interface for the display
  hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);
  display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));

  // Connect to Wi-Fi
  WiFi.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 display
  display.init(0);
}

/**
 * @brief Main loop. Runs repeatedly after setup().
 */
void loop() {
  // Fetch the temperature from Home Assistant
  char* temperature = getTemperature();
  if (temperature != nullptr) {
    displayTemperature(temperature);  // Display the temperature
    free(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 power
  display.hibernate();
  delay(1000);  // Wait for 1 seconds before entering deep sleep

  // Configure deep sleep wakeup after 10 minutes
  esp_sleep_enable_timer_wakeup(10 * 60 * 1000000);  // 10 minutes in microseconds
  esp_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 file
const char* 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.
 */
int getBTC() {
  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 :

C++

/**
 * @brief Maps Open-Meteo weather code to icon index.
 * @param weatherCode Open-Meteo code
 * @return Icon bitmap index
 */
int weatherCodeToIcon(int weatherCode) {
  switch (weatherCode) {
    case 0: return 8;  // Sun
    case 1:
    case 2: return 3;  // Some clouds
    case 3: return 5;  // Cloudy
    case 45:
    case 48: return 2;  // Fog
    case 51:
    case 53:
    case 55:
    case 56:
    case 57: return 7;  // Drizzle
    case 61:
    case 63:
    case 65:
    case 66:
    case 67: return 1;  // Rain
    case 71:
    case 73:
    case 75:
    case 77: return 4;  // Snow
    case 80:
    case 81:
    case 82: return 6;  // Showers
    case 85:
    case 86: return 4;  // Snow
    case 95:
    case 96:
    case 99: return 0;  // Thunderstorm
    default: return 5; // Default to Cloudy
  }
}
C++

const unsigned char* epd_bitmap_allArray[] = {
	epd_bitmap_icon1_thunderstorm,
	epd_bitmap_icon2_rain,
	epd_bitmap_icon3_fog,
	epd_bitmap_icon4_partly_cloudy,
	epd_bitmap_icon5_snow,
	epd_bitmap_icon6_cloudy,
	epd_bitmap_icon7_showers,
	epd_bitmap_icon8_drizzle,
	epd_bitmap_icon9_sun
};
	

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 :

C++
display.setFont(&FreeSans12pt7b);
display.setCursor(x, y);
display.print("25.4");
display.write(0x60);

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 SHT4x via 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;

void setup() {
    // Initialize I2C with custom pins
    Wire.begin(I2C_SDA, I2C_SCL);
    
    uint16_t error;

    // Initialize the sensor
    sht4x.begin(Wire, 0x44);
}

void loop() {
    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.
 * @param batteryVoltage Measured battery voltage in volts.
 * @return Estimated battery charge percentage (0 to 100%).
 */
int getBatteryPercent(float batteryVoltage) {
  if (batteryVoltage > 4.20)
    return 100;
  else if (batteryVoltage > 3.96)
    return 90;
  else if (batteryVoltage > 3.91)
    return 80;
  else if (batteryVoltage > 3.85)
    return 70;
  else if (batteryVoltage > 3.80)
    return 60;
  else if (batteryVoltage > 3.75)
    return 50;
  else if (batteryVoltage > 3.60)
    return 40;
  else if (batteryVoltage > 3.40)
    return 20;
  else if (batteryVoltage > 3.20)
    return 10;
  else if (batteryVoltage > 3.00)
    return 5;
  else
    return 0;
}


/**
 * @brief Read actual battery voltage from ADC.
 * @return Battery voltage in Volts.
 */
float getBatteryVoltage() {
  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
}

void setup() {
  // Configure battery monitoring
  pinMode(BATTERY_ENABLE_PIN, OUTPUT);
  digitalWrite(BATTERY_ENABLE_PIN, HIGH);  // Enable battery monitoring

  // Configure ADC for battery
  analogReadResolution(12);  // 12-bit ADC
  analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
}

void loop() {
  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 communication
SPIClass hspi(HSPI);



// Global weather variables
int 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;
const float sht4xCalibration = -1;  // SHT4x calibration offset

/**
 * @brief Fetch temperature from Home Assistant REST API.
 * @param entityId - Home Assistant sensor entity (e.g., "sensor.lumi_lumi_weather_temperature")
 * @return Temperature value or 0.0 if error.
 */
float getHomeAssistantSensorState(String entityId) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial1.println("ERROR: Wi-Fi not connected.");
    return 0.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();
    return 0.0;
  }
  DynamicJsonDocument doc(1024);
  DeserializationError error = deserializeJson(doc, http.getString());
  http.end();
  if (error) {
    Serial1.print("JSON parsing error: ");
    Serial1.println(error.c_str());
    return 0.0;
  }
  if (!doc.containsKey("state")) {
    Serial1.println("ERROR: 'state' field missing in JSON response.");
    return 0.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.
 */
int fetchWeatherData() {
  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>();
  return 0;
}

/**
 * @brief Get Bitcoin price (USD) from CoinGecko API.
 * @return Bitcoin price as integer, 0 if not available.
 */
int getBTC() {
  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.
 */
int getETH() {
  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).
 * @param y Year
 * @param m Month
 * @param d Day
 * @return Weekday index
 */
int dayOfTheWeek(int y, int m, int d) {
  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.
 * @param dateStr "YYYY-MM-DD HH:MM"
 * @return "Saturday, October 4, 2025" style string.
 */
String formatDateEN(String dateStr) {
  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);
  return String(String(days[wday]) + ", " + String(months[m - 1]) + " " + String(d) + ", " + String(y));
}

/**
 * @brief Formats ISO date string as French date for display.
 * @param dateStr "YYYY-MM-DD HH:MM"
 * @return "Samedi 04 Octobre" style string.
 */
String formatDateFR(String dateStr) {
  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);
  return String(days[wday]) + " " + (d < 10 ? "0" : "") + String(d) + " " + months[m - 1];
}

/**
 * @brief Maps Open-Meteo weather code to icon index.
 * @param weatherCode Open-Meteo code
 * @return Icon bitmap index
 */
int weatherCodeToIcon(int weatherCode) {
  switch (weatherCode) {
    case 0: return 8;  // Sun
    case 1:
    case 2: return 3;  // Some clouds
    case 3: return 5;  // Cloudy
    case 45:
    case 48: return 2;  // Fog
    case 51:
    case 53:
    case 55:
    case 56:
    case 57: return 7;  // Drizzle
    case 61:
    case 63:
    case 65:
    case 66:
    case 67: return 1;  // Rain
    case 71:
    case 73:
    case 75:
    case 77: return 4;  // Snow
    case 80:
    case 81:
    case 82: return 6;  // Showers
    case 85:
    case 86: return 4;  // Snow
    case 95:
    case 96:
    case 99: return 0;  // Thunderstorm
    default: return 5; // Default to Cloudy
  }
}

/**
 * @brief Display forecast for one day at (x, y) with weather icon.
 */
void displayForecast(int x, int y, int day, int min, int max, int iconNb) {
  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.
 */
void displayCurrent(int x, int y, float current, int min, int max, int iconNb) {
  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.
 */
float getBatteryVoltage() {
  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.
 * @param batteryVoltage Measured battery voltage in volts.
 * @return Estimated battery charge percentage (0 to 100%).
 */
int getBatteryPercent(float batteryVoltage) {
  if (batteryVoltage > 4.20)
    return 100;
  else if (batteryVoltage > 3.96)
    return 90;
  else if (batteryVoltage > 3.91)
    return 80;
  else if (batteryVoltage > 3.85)
    return 70;
  else if (batteryVoltage > 3.80)
    return 60;
  else if (batteryVoltage > 3.75)
    return 50;
  else if (batteryVoltage > 3.60)
    return 40;
  else if (batteryVoltage > 3.40)
    return 20;
  else if (batteryVoltage > 3.20)
    return 10;
  else if (batteryVoltage > 3.00)
    return 5;
  else
    return 0;
}

/**
 * @brief Arduino setup: serial, display, WiFi, sensors.
 */
void setup() {
  Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
  delay(500);
  pinMode(GREEN_BUTTON, INPUT_PULLUP);

  // Prepare deep sleep wake-up on button press
  esp_sleep_enable_ext0_wakeup((gpio_num_t)GREEN_BUTTON, LOW);

  // SPI and display initialization
  hspi.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 WiFi
  WiFi.begin(wifi_ssid, wifi_password);
  Serial1.print("Connecting to WiFi...");
  unsigned long 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 pins
  Wire.begin(I2C_SDA, I2C_SCL);

  // Initialize onboard SHT4x sensor
  sht4x.begin(Wire, 0x44);

  // Configure battery monitoring
  pinMode(BATTERY_ENABLE_PIN, OUTPUT);
  digitalWrite(BATTERY_ENABLE_PIN, HIGH);  // Enable battery monitoring

  // Configure ADC for battery
  analogReadResolution(12);  // 12-bit ADC
  analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
}

/**
 * @brief Main loop: fetch sensors, display dashboard, go to deep sleep.
 */
void loop() {
  int x, y;
  int x_ha_box, y_ha_box, ha_box_w, ha_box_h;                          // home assistant box
  int x_current_box, y_current_box, current_box_w, current_box_h;      // current weather box
  int x_forecast_box, y_forecast_box, forecast_box_w, forecast_box_h;  // forecast box
  int x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, bitcoin_box_h;      // bitcoin box
  int x_battery_box, y_battery_box, battery_box_w, battery_box_h;      // battery box
  int16_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 sensor
  uint16_t error = sht4x.measureMediumPrecision(sht4xTemperature, sht4xHumidity);
  sht4xTemperature += sht4xCalibration;

  // Measure battery voltage
  vBat = getBatteryVoltage();
  vBatPercentage = getBatteryPercent(vBat);

  // Get Bitcoind value
  int btc = getBTC();
  int eth = getETH();

  // Fetch weather data for current and next 4 days
  if (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

#endif
  int 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 update
    display.setTextColor(GxEPD_BLACK);
    display.setFont(&FreeSans9pt7b);
    display.setCursor(600, 470);
    display.print(localTime);

    // Display date on top
    display.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 SHT4
    display.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);
    // Bitcoin
    display.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(" $");
    // Ethereum
    display.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.

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 :

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 object
Adafruit_INA3221 ina3221;
void setup() {
  Serial.begin(115200);
  while (!Serial)
    delay(10);  // Wait for serial port to connect on some boards
  Serial.println("Adafruit INA3221 simple test");
  // Initialize the INA3221
  if (!ina3221.begin(0x40, &Wire)) {  // can use other I2C addresses or buses
    Serial.println("Failed to find INA3221 chip");
      delay(10);
  }
  Serial.println("INA3221 Found!");
  ina3221.setAveragingMode(INA3221_AVG_4_SAMPLES);
  // Set shunt resistances for all channels
  for (uint8_t i = 0; i < 3; i++) {
    ina3221.setShuntResistance(i, SHUNT_RESISTANCE);
  }
}
void loop() {
  // Display voltage and current for channel 1
  float bus_voltage1 = ina3221.getBusVoltage(CH1);
  float current1 = ina3221.getCurrentAmps(CH1) * 1000;  // Convert to mA
  Serial.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 2
  float bus_voltage2 = ina3221.getBusVoltage(CH2);
  float current2 = ina3221.getCurrentAmps(CH2) * 1000;  // Convert to mA
  Serial.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 3
  float bus_voltage3 = ina3221.getBusVoltage(CH3);
  float current3 = ina3221.getCurrentAmps(CH3) * 1000;  // Convert to mA
  Serial.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 reading
  delay(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 mA
CH2 Solar panel : Bus voltage = 5.52 V, Current = 318.40 mA
CH3 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 mA
CH2 Solar panel : Bus voltage = 5.23 V, Current = 27.60 mA
CH3 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 mA
CH2 Solar panel : Bus voltage = 3.59 V, Current = -0.40 mA
CH3 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 module XIAO 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 :

Plaintext
https://espressif.github.io/arduino-esp32/package_esp32_index.json

Voici une capture d’écran des étapes à suivre :

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 39

CRGB rgb_led[1];

void setup() {
  // Init RGB LED in BGR ordering
  FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, BGR>(rgb_led, 1);

  // Init TFT screen
  tft.init();
  // Set screen in landscape mode for right side laptop usb port (turn 270°)
  tft.setRotation(3); 
  // Set screen background to BLACK
  tft.fillScreen(TFT_BLACK); 
  // Set "cursor" at top left corner of display (0,0) and select font 4
  tft.setCursor(0, 0, 4);
  // Set text color to white
  tft.setTextColor(TFT_WHITE);
  tft.println("Welcome to");
  tft.println("Tutoduino");    
}

void loop() {
  // Set LED color to blue, then pause
  rgb_led[0] = CRGB::Blue;
  FastLED.show();
  delay(500);
  // Set LED color to white, then pause
  rgb_led[0] = CRGB::White;
  FastLED.show();
  delay(500);
  // Set LED color to red, then pause
  rgb_led[0] = CRGB::Red;
  FastLED.show();
  delay(500);    
  // Set LED color to black, then pause
  rgb_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"
// Keyboard
USBHIDKeyboard Keyboard;
// LCD screen
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library
// RGB LED
#define RGB_DATA_PIN 40
#define RGB_CLOCK_PIN 39
CRGB 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"
static const uint32_t DISK_SECTOR_COUNT = 2 * 8; // 8KB is the smallest size that windows allow to mount
static const uint16_t DISK_SECTOR_SIZE = 512;    // Should be 512
static const uint16_t DISC_SECTORS_PER_TABLE = 1; //each table sector can fit 170KB (340 sectors)
static uint8_t msc_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_name
    FAT_U16(DISK_SECTOR_SIZE), //bytes_per_sector
    FAT_U8(1),    //sectors_per_cluster
    FAT_U16(1),   //reserved_sectors_count
    FAT_U8(1),    //file_alloc_tables_num
    FAT_U16(16),  //max_root_dir_entries
    FAT_U16(DISK_SECTOR_COUNT), //fat12_sector_num
    0xF8,         //media_descriptor
    FAT_U16(DISC_SECTORS_PER_TABLE),   //sectors_per_alloc_table;//FAT12 and FAT16
    FAT_U16(1),   //sectors_per_track;//A value of 0 may indicate LBA-only access
    FAT_U16(1),   //num_heads
    FAT_U32(0),   //hidden_sectors_count
    FAT_U32(0),   //total_sectors_32
    0x00,         //physical_drive_number;0x00 for (first) removable media, 0x80 for (first) fixed disk
    0x00,         //reserved
    0x29,         //extended_boot_signature;//should be 0x29
    FAT_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_LABEL
    0x00, 
    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_hms
    FAT_YMD2B(2018,11,5), //last_modified_ymd
    FAT_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_ARCHIVE
    0x00,                 //ignore
    FAT_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:d
    FAT_YMD2B(2020,11,5), //last_access_ymd
    FAT_U16(0),           //extended_attributes
    FAT_HMS2B(13,44,16),  //last_modified_hms
    FAT_YMD2B(2019,11,5), //last_modified_ymd
    FAT_U16(2),           //start of file in cluster
    FAT_U32(sizeof(README_CONTENTS) - 1) //file size
  },
  //------------- Block3: Readme Content -------------//
  README_CONTENTS
};
unsigned long start_time = 0;
static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize){
  memcpy(msc_disk[lba] + offset, buffer, bufsize);
  return bufsize;
}
static int32_t onRead(uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize){
  memcpy(buffer, msc_disk[lba] + offset, bufsize);
  return bufsize;
}
static bool onStartStop(uint8_t power_condition, bool start, bool load_eject){
  return true;
}
static void usbEventCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data){
}
void awarness_on() {
  // Eject USB key
  MSC.end();

  // Emulate keyboard and open a terminal
  Keyboard.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 screen
  tft.setRotation(3); 
  tft.setCursor(0, 0, 2);
  // Set text color to white
  tft.setTextColor(TFT_RED);
  tft.println("USB keys:");
  tft.println("Small but sneaky.");
  tft.println("Be worry!");  
  // Set LED color to red
  rgb_led[0] = CRGB::Red;
  FastLED.show();
  // Infinite loop
  while (1) {
    rgb_led[0] = CRGB::Red;
    FastLED.show();
    delay(1000);
    rgb_led[0] = CRGB::Black;
    FastLED.show();
    delay(1000);
  };
}
void setup() {
  // Init USB key
  USB.onEvent(usbEventCallback);
  MSC.vendorID("ESP32");//max 8 chars
  MSC.productID("USB_MSC");//max 16 chars
  MSC.productRevision("1.0");//max 4 chars
  MSC.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 black
  tft.init();
  tft.fillScreen(TFT_BLACK); 
  // Init RGB LED in BGR ordering and set it to black
  FastLED.addLeds<APA102, RGB_DATA_PIN, RGB_CLOCK_PIN, BGR>(rgb_led, 1);
  rgb_led[0] = CRGB::Black;
  FastLED.show();

  start_time = millis();
}
void loop() {
  // 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"

// Keyboard
USBHIDKeyboard Keyboard;

// LCD screen
TFT_eSPI tft = TFT_eSPI();  // Invoke custom library

// RGB LED
#define PIN_NEOPIXEL 38

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"

static const uint32_t DISK_SECTOR_COUNT = 2 * 8; // 8KB is the smallest size that windows allow to mount
static const uint16_t DISK_SECTOR_SIZE = 512;    // Should be 512
static const uint16_t DISC_SECTORS_PER_TABLE = 1; //each table sector can fit 170KB (340 sectors)

static uint8_t msc_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_name
    FAT_U16(DISK_SECTOR_SIZE), //bytes_per_sector
    FAT_U8(1),    //sectors_per_cluster
    FAT_U16(1),   //reserved_sectors_count
    FAT_U8(1),    //file_alloc_tables_num
    FAT_U16(16),  //max_root_dir_entries
    FAT_U16(DISK_SECTOR_COUNT), //fat12_sector_num
    0xF8,         //media_descriptor
    FAT_U16(DISC_SECTORS_PER_TABLE),   //sectors_per_alloc_table;//FAT12 and FAT16
    FAT_U16(1),   //sectors_per_track;//A value of 0 may indicate LBA-only access
    FAT_U16(1),   //num_heads
    FAT_U32(0),   //hidden_sectors_count
    FAT_U32(0),   //total_sectors_32
    0x00,         //physical_drive_number;0x00 for (first) removable media, 0x80 for (first) fixed disk
    0x00,         //reserved
    0x29,         //extended_boot_signature;//should be 0x29
    FAT_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_LABEL
    0x00, 
    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_hms
    FAT_YMD2B(2018,11,5), //last_modified_ymd
    FAT_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_ARCHIVE
    0x00,                 //ignore
    FAT_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:d
    FAT_YMD2B(2020,11,5), //last_access_ymd
    FAT_U16(0),           //extended_attributes
    FAT_HMS2B(13,44,16),  //last_modified_hms
    FAT_YMD2B(2019,11,5), //last_modified_ymd
    FAT_U16(2),           //start of file in cluster
    FAT_U32(sizeof(README_CONTENTS) - 1) //file size
  },

  //------------- Block3: Readme Content -------------//
  README_CONTENTS
};

static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize){
  memcpy(msc_disk[lba] + offset, buffer, bufsize);
  return bufsize;
}

static int32_t onRead(uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize){
  memcpy(buffer, msc_disk[lba] + offset, bufsize);
  return bufsize;
}

static bool onStartStop(uint8_t power_condition, bool start, bool load_eject){
  return true;
}

static void usbEventCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data){
}

void awarness_on() {

  // Eject USB key
  MSC.end();

  // Emulate keyboard and open a terminal
  Keyboard.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 screen
  digitalWrite(48, HIGH);  // turn on LCD backlight LCD_BL=48

  tft.setRotation(3); 
  // Set text color to white
  tft.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 red
  neopixelWrite(PIN_NEOPIXEL, 0, 255, 0);  
  delay(500);
  // Set RGB LED to black
  neopixelWrite(PIN_NEOPIXEL, 0, 0, 0);  
  delay(500);
  }
}

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string value = pCharacteristic->getValue();

      if (value.length() > 0) {
        if (value == "ON") {
          Serial.println("ON");
          awarness_on();
        } else if (value == "RESET") {
          Serial.println("RESET");
          ESP.restart();
        } else {
          Serial.println("else");
        }

      }
    }
};

void setup() {

  // Init USB key
  USB.onEvent(usbEventCallback);
  MSC.vendorID("ESP32");//max 8 chars
  MSC.productID("USB_MSC");//max 16 chars
  MSC.productRevision("1.0");//max 4 chars
  MSC.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 black
  tft.init();
  tft.fillScreen(TFT_BLACK); 
  digitalWrite(48, LOW); // turn off LCD backlight LCD_BL=48

  // Set RGB LED to black
  neopixelWrite(PIN_NEOPIXEL, 0, 0, 0);  

  // Init BLE server
  BLEDevice::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(new MyCallbacks());

  pCharacteristic->setValue("OFF");
  pService->start();

  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->start();
  
  }

void loop() {
}

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
sudo apt update
sudo apt install gqrx-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).

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.

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) .

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 mode Raw 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.

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
sudo apt update
sudo apt install rtl-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.

(1) https://github.com/merbanan/rtl_433/blob/master/src/devices/lacrosse.c

(2) https://www.f6fbb.org/domo/sensors/tx3_th.php

A la découverte de FIDO2 et des Passkeys

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.

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.

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.

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.

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.

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.

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.

Pour aller plus loin, je vous recommande vivement la lecture du guide de l’ANSSI (agence nationale de la sécurité des systèmes d’information) : Recommandations relatives à l’authentification multifacteur et aux mots de passe.

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)

Test de votre clé de sécurité

Si vous possédez une clé de sécurité FIDO2, vous pouvez dès à présent la tester grâce au site https://www.token2.com/tools/fido2-demo.

  • Insérer la clé de sécurité dans un port USB
  • Cliquer sur Register
  • 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
  • Auto-complétion permettant de coder plus rapidement
  • 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

Dans mon précédent article “Programmez un Raspberry Pi Pico avec Visual Studio Code et PlatformIO, j’explique comment programmer un Raspberry Pi Pico en C++ sous Visual Studio Code avec PlatformIO et le framework Arduino.

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.

Lecture et décodage des données du capteur Xiaomi

La première étape est d’identifier la caractéristique permettant d’avoir accès à la température et à l’humidité mesurée par le capteur. Je vous invite à lire mon précédent article Découvrir Bluetooth® Low Energy (BLE) avec un Arduino Nano ESP32.

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).

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“.

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();
}

Ce croquis est disponible sur mon Github.

Arduino Cloud

Les différentes étapes de la création du projet sous Arduino Cloud est détaillée dans mon tuto Créez votre premier objet connecté (IoT) avec la plateforme Arduino Cloud et un Arduino Nano ESP32.

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.

#!/usr/bin/python3
# -*- coding: utf8 -*-
# Copyleft https://tutoduino.fr/
import argparse
import asyncio
from bleak import BleakScanner
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
def device_found(
    device: BLEDevice, advertisement_data: AdvertisementData
):
    """Decode advertisement data"""
    try:
        """ Check Arduino manufacturer advertisement data (expect Arduino company ID 0x09A3) """
        manuf_data = advertisement_data.manufacturer_data[0x09A3]
        print('Temperature = {} ; RSSI = {}'.format(
            manuf_data[0], advertisement_data.rssi))
    except KeyError:
        pass
async def main():
    """Scan for devices"""
    scanner = BleakScanner(device_found)
    while True:
        await scanner.start()
        await asyncio.sleep(1.0)
        await scanner.stop()
asyncio.run(main())

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.

Remarque : lorsque l'Arduino est en veille il n'est plus possible de téléverser le code via l'IDE Arduino. Il faut suivre la procédure suivante afin de réinitialiser bootloader et téléverser le sketch via le programmeur Esptool: https://support.arduino.cc/hc/en-us/articles/9810414060188-Reset-the-Arduino-bootloader-on-the-Nano-ESP32

Exemple 3 : Wifi et BLE utilisés conjointement

Le principe de cet exemple est basé sur mon tutoriel Créez votre premier objet connecté (IoT) avec la plateforme Arduino Cloud et un Arduino Nano ESP32. L’Arduino Nano ESP32 est connecté à Arduino Cloud en Wifi. Il est ainsi possible de contrôler l’état de la LED interne de la carte Arduino Nano ESP32 depuis le Dashboard d’Arduino Cloud.

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.

Voici le croquis de l’Arduino Nano ESP32, il n’est pas optimisé mais montre comment imbriquer le maintien des connexions BLE et Wifi. Le fichier thingProperties.h est celui qui est auto-généré par Arduino Cloud dans mon tutoriel Créez votre premier objet connecté (IoT) avec la plateforme Arduino Cloud et un 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.

La documentation du framework ESP-IDF est disponible sur le site internet d’Espressif : https://docs.espressif.com/projects/esp-idf/en/stable/esp32/index.html

Création de notre premier projet

Pour programmer l’ESP32 nous utiliserons l’éditeur Visual Studio Code et la plateforme PlatformIO.

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.

C++
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
const char *model_info(esp_chip_model_t model)
{
    switch (model)
    {
    case CHIP_ESP32:
        return "ESP32";
    case CHIP_ESP32S2:
        return "ESP32S2";
    case CHIP_ESP32S3:
        return "ESP32S3";
    case CHIP_ESP32C3:
        return "ESP32C3";
    case CHIP_ESP32H2:
        return "ESP32H2";
    case CHIP_ESP32C2:
        return "ESP32C2";
    default:
        return "Unknown";
    }
}
void print_chip_info()
{
    esp_chip_info_t chip_info;
    esp_chip_info(&chip_info);
    printf("Chip model %s with %d CPU core(s), WiFi%s%s, ",
           model_info(chip_info.model),
           chip_info.cores,
           (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "",
           (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");
    unsigned major_rev = chip_info.revision / 100;
    unsigned minor_rev = chip_info.revision % 100;
    printf("silicon revision v%d.%d\n", major_rev, minor_rev);
}
void app_main()
{
    for (;;)
    {
        print_chip_info();
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    };
}

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"
const UBaseType_t taskPriority = 1;
void vTask1(void *pvParameters)
{
    for (;;)
    {
        // Display the core on which the task is running
        printf("Task1 is running on core %d\n", xPortGetCoreID());
        // Wait 3 seconds
        vTaskDelay(3000 / portTICK_PERIOD_MS);
    }
}
void vTask2(void *pvParameters)
{
    for (;;)
    {
        // Display the core on which the task is running
        printf("Task2 is running on core %d\n", xPortGetCoreID());
        // Wait 2 seconds
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}
void app_main()
{
    xTaskCreate(vTask1,       // Entry function of the task
                "Task1",      // Name of the task
                10000,        // 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
                NULL);        // No handle
    xTaskCreate(vTask2,       // Entry function of the task
                "Task2",      // Name of the task
                10000,        // 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
                NULL);        // No handle
    for (;;)
    {
        // Display the core on which the main function is running
        printf("app_main() is running on core %d\n", xPortGetCoreID());
        // Wait 1 seconds
        vTaskDelay(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 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task2 is running on core 0
app_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"
const UBaseType_t taskPriority = 1;
void vTask1(void *pvParameters)
{
    for (;;)
    {
        // Display the core on which the task is running
        printf("Task1 is running on core %d\n", xPortGetCoreID());
        // Wait 2 seconds
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}
void vTask2(void *pvParameters)
{
    for (;;)
    {
        // Display the core on which the task is running
        printf("Task2 is running on core %d\n", xPortGetCoreID());
        // Wait 2 seconds
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
}
void app_main()
{
    TaskHandle_t xHandle1 = NULL;
    TaskHandle_t xHandle2 = NULL;
    xTaskCreatePinnedToCore(vTask1,       // Entry function of the task
                            "Task1",      // Name of the task
                            10000,        // 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 task
                            0);           // Task must be executed on core 0
    xTaskCreatePinnedToCore(vTask2,       // Entry function of the task
                            "Task2",      // Name of the task
                            10000,        // 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 task
                            1);           // Task must be executed on core 1
    for (;;)
    {
        // Display the core on which the main function is running
        printf("app_main() is running on core %d\n", xPortGetCoreID());
        // Wait 1 seconds
        vTaskDelay(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 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 1
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 1
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 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"
void autoReloadTimerCallback(TimerHandle_t xTimer)
{
    printf("Timer callback\n");
}
void app_main()
{
    TimerHandle_t xAutoReloadTimer;
    xAutoReloadTimer = xTimerCreate("AutoReloadTimer",        // Name of the timer
                                    pdMS_TO_TICKS(500),       // The period of the timer specified in ticks (500)
                                    pdTRUE,                   // The timer will auto-reload when it expires
                                    0,                        // Identifier of the timer
                                    autoReloadTimerCallback); // Callback function
    xTimerStart(xAutoReloadTimer, 0);
    for (;;)
    {
        // Display the core on which the main function is running
        printf("app_main() is running on core %d\n", xPortGetCoreID());
        // Wait 1 seconds
        vTaskDelay(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 0
Timer callback
Timer callback
app_main() is running on core 0
Timer callback
Timer callback
app_main() is running on core 0
Timer callback
Timer 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_32
uint32_t cpt = 0;
static void IRAM_ATTR gpio_interrupt_handler(void *args)
{
    // Interrupt is raised, increase the counter
    cpt++;
}
void app_main()
{
    // Configure INPUT_PIN as input
    gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
    // Enable the pull-down for INPUT_PIN
    gpio_pulldown_en(INPUT_PIN);
    // Disable the pull-up for INPUT_PIN
    gpio_pullup_dis(INPUT_PIN);
    // An interrupt will be triggered when the the state of the INPUT_PIN changes from LOW to HIGH
    gpio_set_intr_type(INPUT_PIN, GPIO_INTR_POSEDGE);
    // Install the GPIO ISR service, which allows per-pin GPIO interrupt handlers
    gpio_install_isr_service(0);
    // Configure gpio_interrupt_handler function has ISR handler for INPUT_PIN
    gpio_isr_handler_add(INPUT_PIN, gpio_interrupt_handler, (void *)INPUT_PIN);
    for (;;)
    {
        // Display the number of time the interrupt was called during last second
        printf("Counter = %lu\n", cpt);
        // Reset the counter
        cpt = 0;
        // Wait 1 seconds
        vTaskDelay(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.

Plaintext
Counter = 0
Counter = 0
Counter = 0
Counter = 7
Counter = 40
Counter = 0

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).

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 est nécessaire d’installer la carte uPesy ESP32 Wroom dans l’IDE Arduino, vous devez suivre la procédure détaillé sur le site uPesy.

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

Installez Visual Studio Code et ajoutez l’extension PlatformIO IDE.

Installation de l’extension PlatformIO IDE

Configuration du service Redis

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 :

#define REDIS_ADDR "redis-14378.c923.us-central-6-4.ec6.cloud.redislabs.com"
#define REDIS_PORT 14378

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 GitHub
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):

echo "# myproject" >> README.md
git init
git add README.md
git add test.ino
git commit -m "first commit"
git remote add origin https://github.com/tutoduino/myproject.git
git branch -M main
git push -u origin main

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 :

git clone https://github.com/tutoduino/myproject.git

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.

Détaillons le programme de la machine à état (voir la documentation de la librairie rp2 si besoin) :

wrap_target()

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.

Le codage des données à envoyer est décrit dans est le diagramme de la documentation technique du WS2812B :

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

J’ai simplifié l’exemple WS2812 de la documentation technique du RP2040, car je le trouve inutilement compliqué.

Voici le programme de la machine à état PIO :

@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
sudo apt update
sudo apt install tshark

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 :

Bash
sudo chmod +x /usr/bin/dumpcap
sudo setcap cap_net_raw,cap_net_admin=ep /usr/bin/dumpcap

Lancez une capture durant 2 minutes afin de vérifier le bon fonctionnement du programme :

Bash
tshark -w mon-fichier.pcapng -a duration: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 :

Bash
sudo apt install python3-pip
pip install scapy==2.5.0rc1 
pip install shodan dotenv setuptools

Installez mon programme IP Reputation Check depuis GitHub :

Bash
sudo apt install git
git clone https://github.com/tutoduino/IP_reputation_check

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 KEYS
SHODAN_API_KEY=xxx
VIRUS_TOTAL_KEY=xxx
IPQS_KEY=xxx
APIVOID_KEY=xxx
ABUSEIPDB_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 :

Bash
-------------------------------
IP address 8.8.8.8
IPinfo           -> Organization: AS15169 Google LLC
IPinfo           -> Country: US
IPinfo           -> City: Mountain View
Shodan           -> Number of open ports: 3
Shodan           -> Hostnames: ['dns.google']
AbuseIpDb        -> Number of reports: 210
AbuseIpDb        -> Confidence of Abuse: 0
ApiVoid          -> Risk score: 0
ApiVoid          -> Detection rate: 0%
VirusTotal       -> Number of reports saying it is malicious: 0
VirusTotal       -> Number of reports saying it is suspicious: 0
VirusTotal       -> Reputation (<0 is suspicious): 549
VirusTotal       -> Harmless votes: 226
VirusTotal       -> Malicious votes: 41
IpQualityScore   -> Fraud score (>75 is suspicious): 0
IpQualityScore   -> Bot activity: True
IpQualityScore   -> VPN status: False
IpQualityScore   -> Proxy status: False
IpQualityScore   -> Tor status: False
-------------------------------
IP address 1.1.1.1
IPinfo           -> Organization: AS13335 Cloudflare, Inc.
IPinfo           -> Country: AU
IPinfo           -> City: Brisbane
Shodan           -> Number of open ports: 16
Shodan           -> Hostnames: ['one.one.one.one']
AbuseIpDb        -> Number of reports: 14
AbuseIpDb        -> Confidence of Abuse: 0
ApiVoid          -> Risk score: 0
ApiVoid          -> Detection rate: 0%
VirusTotal       -> Number of reports saying it is malicious: 0
VirusTotal       -> Number of reports saying it is suspicious: 0
VirusTotal       -> Reputation (<0 is suspicious): 115
VirusTotal       -> Harmless votes: 117
VirusTotal       -> Malicious votes: 30
IpQualityScore   -> Fraud score (>75 is suspicious): 0
IpQualityScore   -> Bot activity: True
IpQualityScore   -> VPN status: False
IpQualityScore   -> Proxy status: False
IpQualityScore   -> Tor status: False

Utilisation avec un fichier PcapNg

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

Bash
python3 parse_pcap.py mon-fichier.pcapng | python3 ip_reputation_check.py 

Utilisation avec un fichier CSV

Vous pouvez aussi fournir une liste d’adresses IP dans un fichier CSV.

Bash
python3 parse_ip_csv.py csv_file.csv | python3 ip_reputation_check.py

Exemple sur l’adresse 167.172.130.207

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.
Bash
-------------------------------
IP address 167.172.130.207
IPinfo           -> Organization: AS14061 DigitalOcean, LLC
IPinfo           -> Country: US
IPinfo           -> City: North Bergen
Shodan           -> Number of open ports: 2
Shodan           -> Hostnames: []
AbuseIpDb        -> Number of reports: 72
AbuseIpDb        -> Confidence of Abuse: 100
ApiVoid          -> Risk score: 100
ApiVoid          -> Detection rate: 6%
VirusTotal       -> Number of reports saying it is malicious: 4
VirusTotal       -> Number of reports saying it is suspicious: 2
VirusTotal       -> Reputation (<0 is suspicious): 0
VirusTotal       -> Harmless votes: 0
VirusTotal       -> Malicious votes: 0
IpQualityScore   -> Fraud score (>75 is suspicious): 100
IpQualityScore   -> Bot activity: True
IpQualityScore   -> VPN status: True
IpQualityScore   -> Proxy status: True
IpQualityScore   -> Tor status: False

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 !

Merci à GoTronic pour la fourniture du iButton Dallas DS1990A nécessaire à la rédaction de ce tutoriel.

Test comparatif des PoE HAT Raspberry Pi

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.

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.

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.

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-HATUCTRONICS PoE-HAT FANRaspberry PoE+ HAT
Température du CPU chargé à 25%
pendant 5 minutes
62°C39 °C49 °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-HATUCTRONICS PoE-HAT FANRaspberry PoE+ HAT
Compatibilité avec la majorité des boîtiersOuiOuiNon
Ventilateur intégréNonOui mais bruyantOui
Laisse accès aux GPIOOuiNonNon
Support du PoE+ (30 W)NonNonOui

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.

(1) Le Raspberry Pi 4 consomme 2,55 A au maximum, soit 12,75 W sous 5 V. La consommation est répartie de la façon suivante : 1,25 A pour le Raspberry Pi lui même, 1,2 A pour les périphériques USB, 50 mA pour les GPIO et 50 mA pour le HDMI) . Source https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#typical-power-requirements

Programmez un Raspberry Pi Pico avec Visual Studio Code et PlatformIO

PlatformIO est une plateforme largement utilisée pour les développements embarqués, et ce tutoriel va vous permettre de la découvrir.

Nous allons détailler ici la programmation d’un Raspberry Pi Pico en utilisant l’extension PlatformIO de l’éditeur Visual Studio Code.

Installation de l’environnement de développement

Téléchargez Visual Studio Code à partir de la page de téléchargement et installez le sur votre système :

Lancez Visual Studio Code et installez 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++.

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.

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.

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.

Si vous souhaitez programmer votre Pico en Python, je vous conseille la lecture de mon article “Programmez un Raspberry Pi Pico en Python avec Visual Studio Code“.

Test du PoE HAT UCTRONICS pour Raspberry Pi 4

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.

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.

Broches PoE du Raspberry Pi
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.

Vous trouverez dans cet article un comparatif des différents PoE HAT pour le Raspberry Pi.

Je vous invite à lire l’article de François Mocq sur Framboise314.fr à propos du nouveau module HAT PoE+ de la fondation Raspberry.

Testeur PoE

Il existe des petits dispositifs qui permettent de vérifier le mode utilisé par le PSE (Mode A ou Mode B).

Testeur PoE
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).

Testeur PoE utilisé sur un switch Netgear
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

Je vous conseille de regarder la vidéo d’Electro-bidouilleur qui traite du sujet.

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.

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
Trame Ethernet (source wikipedia)

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
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 🙂

Ouvrez les préférences :

Et insérez l’URL du gestionnaire de carte supplémentaire suivant : https://dl.espressif.com/dl/package_esp32_index.json

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.

DS1307 relié à un Arduino
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 câblage module RTC Arduino Uno
Schéma de câblage du module RTC avec l’Arduino Uno
Module RTC 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;

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

  // Attente de la connection serie avec l'Arduino
  while (!Serial);

  // Lance le communication I2C avec le module RTC et 
  // attend que la connection soit operationelle
  while (! 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 croquis
  rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  Serial.println("Horloge du module RTC mise a jour");
}

void loop () {
    DateTime now = rtc.now();
    char heure[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 module rtc
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… 😉

Reset de l'Arduino

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++
void setup () {
  Serial.begin(9600);

  // Attente de la connection serie avec l'Arduino
  while (!Serial);

  // Lance le communication I2C avec le module RTC et 
  // attend que la connection soit operationelle
  while (! 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 prealable
  if (! rtc.isrunning()) {
    // La date et l'heure de la compilation de ce croquis 
    // est utilisee pour mettre a jour l'horloge
    rtc.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 🙂

Voici le code correspondant qui utilise la librairie décrite dans mon autre article à propos des écrans OLED.

C++
// 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 0x3C

SSD1306AsciiAvrI2c oled;

RTC_DS1307 rtc;

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

  // Attente de la connection serie avec l'Arduino
  while (!Serial);

  // Lance le communication I2C avec le module RTC et 
  // attend que la connection soit operationelle
  while (! 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 reglee
  if (! rtc.isrunning()) {
    // La date et l'heure de la compilation de ce croquis 
    // est utilisee pour mettre a jour l'horloge
    rtc.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");
}

void loop () {
  DateTime now = rtc.now();
  char heure[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.

DS3231
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
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).

Réseau de résistances R-2R

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. 😉

Protoshield réseau résistances 2-2R
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).

sortie en rampe

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 :

sortie sinusoidale

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.

C’est quoi… un signal PWM ?

Le PWM est un acronyme anglais qui signifie Pulse Width Modulation, ou Modulation de Largeur d’Impulsion (MLI) en français.

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 !

ecran oled 2 couleurs

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.

câblage écran

Go Tronic

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).

TL431 régulateur de tension 2,5V

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.

TL431 avec condensateur entre VREF et GND

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.

Référence de tension externe 2,5 V avec un TL431 sur Arduino Uno

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

TL431 Arduino AREF sans capacité
Instabilité de AREF sur Arduino Uno avec l’utilisation du TL431 sans condensateur ajouté entre VREF et GND
TL431 AREF Arduino stable avec condensateur 10 uF
Avec 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 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.

ATmega328P

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.