Procédure de Reproduction
Projet M5Stack + ATECC608B : Publication / Souscription MQTT(S) avec clé TLS stockée dans le Secure Element.
Suivi de progression
0/11 complété
Générateur d'Environnement
Remplissez votre SSID, IP du broker et topic MQTT. Les extraits ci-dessous sont générés en direct pour coller des constantes cohérentes dans les codes Arduino et la configuration Mosquitto sans modification manuelle.
Constantes Arduino
Astuce : gardez ces valeurs identiques pour les builds Publisher et Subscriber.
Listener TLS Mosquitto
La configuration impose les certificats clients et mappe le CN du certificat au nom d'utilisateur (use_identity_as_username).
Architecture générale du projet
1.1 Matériel
M5Stack / M5Go
Cœur ESP32 avec écran intégré.
ENV II Unit
SHT31 (Temp/Hum) + BMP280 (Pression).
ATECC608B
Config TNGTLSU. Adresse I2C : 0x35.
Serveur
Mosquitto, OpenSSL, Python3.
1.2 Schéma de câblage (Wiring)
M5Stack
PORT A (Red)ENV II Unit
ATECC608B Unit
1.2 Bibliothèques nécessaires (Arduino)
1.3 Rôles des sketches
Outil de diagnostic. Vérifie la connexion I2C (0x35) et lit le statut.
Outil de provisionnement. Écrit la clé privée (DER) dans le Slot 8.
Publisher sécurisé. Lit la clé du Slot 8 -> RAM -> Connexion TLS.
Subscriber sécurisé. Affiche les données reçues sur l'écran LCD.
Préparation de l’environnement Arduino
2.1 Installation des bibliothèques
M5Unified, PubSubClient, WiFiClientSecure, Adafruit_SHT31, Adafruit_BMP280, et SparkFun_ATECCX08a_Arduino_Library.
2.2 Configuration des broches I2C
static const uint8_t I2C_SDA = 32; static const uint8_t I2C_SCL = 33; static const uint8_t ECC_ADDR = 0x35;
Génération de la PKI (CA, broker, clients)
Toutes les commandes suivantes sont à lancer dans un répertoire de travail
(ex: mqtt-atecc-demo/) sur la machine qui héberge le broker.
# 3.1 Créer une autorité de certification (CA) mkdir mqtt-atecc-demo cd mqtt-atecc-demo openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ca.key openssl req -x509 -new -nodes -key ca.key -days 3650 -out ca.crt \ -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Lab/OU=MQTT/CN=LocalMQTT-CA" # 3.2 Certificat serveur pour le broker MQTT openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out broker.key openssl req -new -key broker.key -out broker.csr \ -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Lab/OU=MQTT/CN=broker.local" openssl x509 -req -in broker.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out broker.crt -days 365 # 3.3 Certificat client pour le publisher (m5go-pub) openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out m5go-pub.key openssl req -new -key m5go-pub.key -out m5go-pub.csr \ -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Lab/OU=Devices/CN=m5go-pub" openssl x509 -req -in m5go-pub.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out m5go-pub.crt -days 365 # 3.4 Certificat client pour le subscriber (m5sub-display) openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out m5sub-display.key openssl req -new -key m5sub-display.key -out m5sub-display.csr \ -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Lab/OU=Devices/CN=m5sub-display" openssl x509 -req -in m5sub-display.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out m5sub-display.crt -days 365 # Astuce : # Si vous comptez vous connecter via IP (ex: 192.168.1.X), mettez cette IP dans le CN # (ex: CN=192.168.1.50) pour que les vérifications de validité passent.
Vérification du Nom d'Hôte TLS
Le client (M5Stack) vérifiera que le certificat du serveur correspond au nom d'hôte/IP auquel il se
connecte.
Si vous mettez YOUR_BROKER_IP dans le code, vous devez utiliser cette
adresse IP
comme CN (Common Name) dans la commande de certificat ci-dessus, ou l'ajouter comme
SAN.
Quel fichier va où ?
| Artefact | Description | Destination |
|---|---|---|
| ca.crt | Certificat Racine | Mosquitto + Tous Sketches |
| broker.crt / .key | Identité Serveur | Config Mosquitto seulement |
| m5go-pub.crt | Cert Public Publisher | Sketch Publisher (Étape 2 & 3) |
| m5go-pub.key | Clé Privée Publisher | Sketch Publisher (Étape 2 OU Slot 8 (Étape 3)) |
| m5sub-display.crt | Cert Public Subscriber | Sketch Subscriber |
Configuration du broker Mosquitto en TLS mutuel
4.1 Fichiers
Copier ca.crt, broker.crt, broker.key
dans /etc/mosquitto/certs/.
4.2 Configuration
(/etc/mosquitto/conf.d/secure.conf)
listener 8883 protocol mqtt allow_anonymous false cafile /etc/mosquitto/certs/ca.crt certfile /etc/mosquitto/certs/broker.crt keyfile /etc/mosquitto/certs/broker.key # TLS mutuel : exiger un certificat client signé par la CA require_certificate true use_identity_as_username true
Puis redémarrer : sudo systemctl restart mosquitto
Vérifier le secure element avec le sketch Scan_SE
Créer un sketch Scan_SE dans l'IDE Arduino. Ce code scanne l'I2C,
lit la config ATECC et vérifie les locks.
// Découverte et test du secure element ATECC608B-TNGTLSU sur M5
// - Scan du bus I2C pour trouver l'adresse 0x35
// - Initialisation de la librairie SparkFun
// - Lecture de la zone de configuration (revision, serial, locks)
// - Essai de génération de la clé publique si le device est "locké" comme attendu
#include <M5Unified.h>
#include <Wire.h>
// SparkFun ATECC library
#include <SparkFun_ATECCX08a_Arduino_Library.h>
// Brochage I2C utilisé pour le secure element
static const uint8_t I2C_SDA = 32;
static const uint8_t I2C_SCL = 33;
// Adresse I2C de l'ATECC608B-TNGTLSU (valeur usine)
static const uint8_t ECC_ADDR = 0x35; // ATECC608B-TNGTLSU default
// Instance de l'objet ATECC de la librairie SparkFun
ATECCX08A atecc;
// Affiche un octet en hexadécimal sur le port série, sur 2 digits
void printHexByte(uint8_t b) {
if (b < 0x10) Serial.print('0');
Serial.print(b, HEX);
}
// Affichage centré d'une ligne de texte sur l'écran M5
void drawCenteredLine(int16_t y, const String &text, int textSize = 2, uint16_t color = 0xFFFF) {
auto &lcd = M5.Display;
lcd.setTextSize(textSize);
lcd.setTextColor(color);
int16_t w = lcd.textWidth(text);
int16_t x = (lcd.width() - w) / 2;
lcd.setCursor(x, y);
lcd.print(text);
}
// Affiche un message d'erreur à la fois sur le port série et sur l'écran
void showError(const String &msg) {
Serial.println(msg);
auto &lcd = M5.Display;
lcd.clear();
// Titre rouge "SECURE ELEMENT" et "TEST FAILED"
drawCenteredLine(20, "SECURE ELEMENT", 2, 0xF800);
drawCenteredLine(50, "TEST FAILED", 2, 0xF800);
lcd.setTextSize(1);
lcd.setTextColor(0xFFFF);
lcd.setCursor(10, 80);
lcd.print(msg); // détail de l'erreur
}
// Scan I2C complet (1..126) et retour true si l'adresse "target" est trouvée
bool scanFor(uint8_t target) {
bool found = false;
for (uint8_t addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
uint8_t err = Wire.endTransmission();
if (err == 0) {
Serial.printf("I2C device found at 0x%02X\n", addr);
if (addr == target) found = true;
}
}
return found;
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
Serial.begin(115200);
delay(400);
auto &lcd = M5.Display;
lcd.setRotation(1);
lcd.clear();
// Écran d'introduction du test
drawCenteredLine(18, "Secure Element", 2);
drawCenteredLine(42, "Self Test", 2);
delay(600);
// Initialisation du bus I2C pour l'ATECC608B
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000);
Serial.println("\n=== Secure Element Test ===");
lcd.clear();
drawCenteredLine(20, "Scanning I2C...", 2);
// Scan I2C pour trouver l'ATECC608B à l'adresse 0x35
if (!scanFor(ECC_ADDR)) {
// Si l'adresse 0x35 n'est pas vue, on considère que l'ATECC est absent
showError("ATECC608B-TNGTLSU not found at 0x35");
return;
}
Serial.println("ATECC608 detected at 0x35.");
lcd.clear();
drawCenteredLine(20, "I2C OK", 2, 0x07E0);
drawCenteredLine(45, "0x35 detected", 2);
delay(600);
// Initialisation de la librairie SparkFun pour l'ATECC
lcd.clear();
drawCenteredLine(20, "Init ATECC608...", 2);
if (!atecc.begin(ECC_ADDR, Wire, Serial)) {
// Problème d'initialisation (commande Wake ou config)
showError("SparkFun begin() failed.");
return;
}
Serial.println("ATECC608 init OK.");
lcd.clear();
drawCenteredLine(20, "ATECC608", 2);
drawCenteredLine(45, "Init OK", 2, 0x07E0);
delay(500);
// Lecture de la zone de configuration (remplit les champs revisionNumber, serialNumber, lock statuses, etc.)
if (!atecc.readConfigZone(false)) {
showError("Cannot read config zone.");
return;
}
// Affichage de la révision
Serial.print("Revision: ");
for (int i = 0; i < 4; i++) {
printHexByte(atecc.revisionNumber[i]);
Serial.print(' ');
}
Serial.println();
// Affichage du numéro de série complet (9 octets)
Serial.print("Serial : ");
for (int i = 0; i < 9; i++) {
printHexByte(atecc.serialNumber[i]);
Serial.print(i < 8 ? ':' : '\n');
}
// État des locks (config, data/OTP, slot0)
Serial.print("Config Lock : ");
Serial.println(atecc.configLockStatus ? "Locked" : "Not locked");
Serial.print("Data/OTP : ");
Serial.println(atecc.dataOTPLockStatus ? "Locked" : "Not locked");
Serial.print("Slot0 lock : ");
Serial.println(atecc.slot0LockStatus ? "Unlocked (bit=1)" : "Locked (bit=0)");
// Professor’s fix:
// SparkFun example uses:
// if (atecc.configLockStatus && atecc.dataOTPLockStatus && atecc.slot0LockStatus)
// But in the TNG datasheet, SlotLocked bit = 0 when locked.
// So use '!' on slot0LockStatus:
// Ici, on considère que le device est "pleinement provisionné" si :
// - configLockStatus == true
// - dataOTPLockStatus == true
// - slot0LockStatus == false (bit 0 => slot verrouillé)
if (atecc.configLockStatus && atecc.dataOTPLockStatus && !atecc.slot0LockStatus) {
Serial.println("\nConfig and Data/OTP are locked and Slot0 is locked: generating public key...");
// On demande à l'ATECC de générer la clé publique correspondant à la clé privée slot 0
if (!atecc.generatePublicKey()) {
showError("Failure to generate device public key");
return;
}
} else {
Serial.println("\nDevice not fully personalized/locked, skipping public key generation.");
}
// Si tout est OK, on affiche un écran de test réussi avec quelques infos de config
lcd.clear();
drawCenteredLine(10, "SECURE ELEMENT", 2, 0x07E0);
drawCenteredLine(34, "TEST PASSED", 2, 0x07E0);
lcd.setTextSize(1);
lcd.setTextColor(0xFFFF);
lcd.setCursor(10, 60);
lcd.print("Serial: ");
for (int i = 0; i < 9; i++) {
if (atecc.serialNumber[i] < 0x10) lcd.print('0');
lcd.print(atecc.serialNumber[i], HEX);
if (i < 8) lcd.print(':');
}
// Affichage des locks sur l'écran (config et Data/OTP)
lcd.setCursor(10, 78);
lcd.print("Cfg: ");
lcd.print(atecc.configLockStatus ? "Locked" : "Not locked");
lcd.setCursor(10, 92);
lcd.print("Data/OTP: ");
lcd.print(atecc.dataOTPLockStatus ? "Locked" : "Not locked");
}
void loop() {
// Boucle vide, on laisse M5Unified gérer les events si nécessaire
M5.update();
}
Téléverser et vérifier dans le moniteur série à 115200 bauds.
Préparation du blob clé privée pour le slot 8
Conversion de la clé publisher en format DER précédé de sa longueur (2 octets Big Endian).
# 6.1 Convertir en DER
openssl pkcs8 -topk8 -nocrypt -in m5go-pub.key -outform DER -out m5go-pub.der
# 6.2 Préfixer la longueur avec Python
python3 - << 'EOF'
import struct
with open("m5go-pub.der", "rb") as f:
der = f.read()
derlen = len(der)
print("Longueur DER :", derlen)
with open("m5go-pub_withlen.bin", "wb") as f:
f.write(struct.pack(">H", derlen)) # longueur big endian
f.write(der)
EOF
# 6.3 Générer header C
xxd -i m5go-pub_withlen.bin > m5go_pub_key_der.h
Répéter exactement la même procédure pour la clé du subscriber
(m5sub-display.key).
Provisionnement du slot 8 avec Write_SE
Créer le sketch Write_SE. Important : Remplacez
le contenu du tableau m5go_pub_withlen_bin par celui de votre fichier .h généré
ci-dessus.
// Provisionnement du slot 8 du secure element ATECC608B
// Objectif : écrire un blob PKCS#8 (clé privée DER) précédé de sa longueur sur 2 octets
// dans la zone DATA du slot 8 (13 blocs de 32 octets, soit 416 octets).
#include <M5Unified.h>
#include <Wire.h>
#include <SparkFun_ATECCX08a_Arduino_Library.h>
// Brochage I2C du secure element
static const uint8_t I2C_SDA = 32;
static const uint8_t I2C_SCL = 33;
static const uint8_t ECC_ADDR = 0x35;
ATECCX08A atecc;
// Paste from m5go_pub_key_der.h (generated by xxd -i)
// Ce tableau contient : [2 octets de longueur DER] + [blob DER de la clé privée]
const uint8_t m5go_pub_withlen_bin[] = {
0x00, 0x8a, 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07,
0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48,
0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x6d, 0x30, 0x6b, 0x02, 0x01, 0x01,
0x04, 0x20, 0x58, 0x55, 0xb5, 0x34, 0xff, 0xb5, 0xd9, 0xcc, 0x58, 0x9d,
0xb0, 0x14, 0x17, 0xd1, 0x4f, 0xed, 0x16, 0xef, 0x24, 0xe2, 0x67, 0x37,
0x02, 0x19, 0x69, 0xc7, 0x41, 0x86, 0x9a, 0x88, 0x8a, 0xbb, 0xa1, 0x44,
0x03, 0x42, 0x00, 0x04, 0x14, 0x57, 0x96, 0x8c, 0x3f, 0xd7, 0xee, 0x11,
0x0e, 0x1e, 0x45, 0x4a, 0x5c, 0xdb, 0xbe, 0xf4, 0x1b, 0x7a, 0x8d, 0xf6,
0xba, 0x00, 0xc3, 0x5d, 0x8f, 0x91, 0x7d, 0xf5, 0xf5, 0x90, 0x0b, 0xfd,
0xe4, 0x3b, 0x01, 0xe9, 0xf2, 0xe1, 0x85, 0x27, 0x3c, 0x16, 0x0d, 0xab,
0xaf, 0x7b, 0x32, 0x49, 0xe9, 0x8f, 0x6d, 0x7e, 0xc1, 0xb7, 0x95, 0xc8,
0x8b, 0x23, 0xc6, 0xc3, 0xbf, 0x28, 0xc7, 0x0f
};
const size_t m5go_pub_withlen_bin_len = sizeof(m5go_pub_withlen_bin);
// Helpers for slot 8
// Calcule l'adresse ATECC (zone DATA) associée au slot 8, bloc "block"
static uint16_t slot8AddressForBlock(uint8_t block)
{
// Format d'adresse : bits [6:3] = slot (8), bits [8..] = block, bits [2:0] = word offset (0 pour 32 octets)
return ((uint16_t)8 << 3) | ((uint16_t)block << 8);
}
// Écrit 32 octets dans le slot 8, bloc "block"
bool writeDataSlot8Block(ATECCX08A &atecc, uint8_t block, const uint8_t *data32)
{
if (block >= 13) return false; // slot 8 = 13 blocs de 32 octets
uint16_t addr = slot8AddressForBlock(block);
if (!atecc.write(ZONE_DATA, addr, (uint8_t *)data32, 32))
{
Serial.print("write block "); Serial.print(block); Serial.println(" failed");
return false;
}
return true;
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
M5.Display.setRotation(1);
M5.Display.clear();
M5.Display.setCursor(10, 20);
M5.Display.println("Provision slot 8 (PUB)");
Serial.begin(115200);
delay(500);
Serial.println("\n--- ATECC Provision (Publisher, v2) ---");
// Initialisation I2C vers le secure element
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000);
// Initialisation de la librairie SparkFun
if (!atecc.begin(ECC_ADDR, Wire, Serial)) {
Serial.println("ERROR: atecc.begin() failed");
return;
}
// Lecture de la zone de configuration pour obtenir l'état des locks
if (!atecc.readConfigZone(false)) {
Serial.println("ERROR: readConfigZone() failed");
return;
}
// Affichage d'information sur les locks (pour debug)
Serial.print("ConfigLock="); Serial.println(atecc.configLockStatus);
Serial.print("DataOTPLock="); Serial.println(atecc.dataOTPLockStatus);
Serial.print("Slot0Lock="); Serial.println(atecc.slot0LockStatus);
// Taille du blob "2 octets de longueur + DER"
Serial.print("Key blob length (with prefix) = ");
Serial.println(m5go_pub_withlen_bin_len);
// Boucle d'écriture par blocs de 32 octets dans le slot 8
size_t offset = 0;
uint8_t block = 0;
while (offset < m5go_pub_withlen_bin_len && block < 13) {
uint8_t buf32[32];
memset(buf32, 0, sizeof(buf32)); // remplissage par 0 pour les zones partielles
size_t remaining = m5go_pub_withlen_bin_len - offset;
size_t toCopy = (remaining > 32) ? 32 : remaining;
memcpy(buf32, &m5go_pub_withlen_bin[offset], toCopy);
Serial.print("Writing slot8 block ");
Serial.print(block);
Serial.print(", bytes ");
Serial.println(toCopy);
if (!writeDataSlot8Block(atecc, block, buf32)) {
Serial.println("ERROR writing block, aborting");
return;
}
offset += toCopy;
block++;
}
// Fin du provisioning : tous les octets ont été écrits
Serial.println("Provisioning complete.");
M5.Display.setCursor(10, 50);
M5.Display.println("Done. Power-cycle.");
}
void loop() {
// Pas de logique en boucle, on laisse l'utilisateur re-booter après provisioning
M5.update();
}
Action: Téléverser sur le M5 publisher. Vérifier "Provisionnement complet". Redémarrer.
Faire de même pour le subscriber avec un sketch Write_SE_Sub utilisant la clé
subscriber.
Code final du publisher : MQTTS_SE_Pub
Mettre à jour les identifiants Wi-Fi, l'IP du Broker et les certificats (CA et Certificat client) dans le code ci-dessous.
// Publisher sécurisé avec ATECC608B :
// - La clé privée TLS n'est plus stockée en clair dans le firmware.
// - Elle est stockée en DER dans le slot 8 de l'ATECC (cf. Writing_slot8).
// - Au démarrage, on lit le slot 8, on reconstitue la clé en DER puis en PEM (Base64 + header/footer).
// - Cette clé PEM est ensuite donnée à WiFiClientSecure via setPrivateKey().
#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_SHT31.h>
#include <Adafruit_BMP280.h>
// ATECC608B (SparkFun)
#include <SparkFun_ATECCX08a_Arduino_Library.h>
// Brochage I2C partagé (capteurs + ATECC608B)
static const uint8_t I2C_SDA = 32;
static const uint8_t I2C_SCL = 33;
// Adresse par défaut du secure element
static const uint8_t ECC_ADDR = 0x35;
ATECCX08A atecc;
// -------- WIFI ----------
const char* WIFI_SSID = "**********";
const char* WIFI_PASS = "**********";
// -------- MQTT ----------
const char* MQTT_HOST = "**********"; // Broker IP
const uint16_t MQTT_PORT = 8883;
const char* TOPIC = "m5go/env";
// -------- CA CERT ----------
// Certificat de l'autorité qui a signé broker.crt
static const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIBwDCCAWYCCQDCLzlU+p80FTAKBggqhkjOPQQDAjBoMQswCQYDVQQGEwJGUjES
MBAGA1UECAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwD
TGFiMQ0wCwYDVQQLDARNUVRUMRUwEwYDVQQDDAxMb2NhbE1RVFQtQ0EwHhcNMjUx
MTE4MDc1ODQ0WhcNMzUxMTE2MDc1ODQ0WjBoMQswCQYDVQQGEwJGUjESMBAGA1UE
CAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwDTGFiMQ0w
CwYDVQQLDARNUVRUMRUwEwYDVQQDDAxMb2NhbE1RVFQtQ0EwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAATPDXo6/5pAbxyrHaQpEHR33AR1vWDcJk+H37T/oMOgMpRL
+ipsBETdbz46Dz2v8/da6NKXzeaGnVFyZIeeqJPjMAoGCCqGSM49BAMCA0gAMEUC
IGPrNzv/LGSCuUrBLCHPUTrDWnm1T7BtV8I2J4lruIuGAiEAknzvAWYCkJyYuq0S
yHP8+qRKegc2kbXrHI5Fw00bM40=
-----END CERTIFICATE-----
)EOF";
// -------- CLIENT CERT (m5go-pub) ----------
// Certificat X.509 du publisher (m5go-pub), signé par la CA ci-dessus
static const char client_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIBwDCCAWUCCQCU9fxQbpMYEDAKBggqhkjOPQQDAjBoMQswCQYDVQQGEwJGUjES
MBAGA1UECAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwD
TGFiMQ0wCwYDVQQLDARNUVRUMRUwEwYDVQQDDAxMb2NhbE1RVFQtQ0EwHhcNMjUx
MTE4MDc1OTE1WhcNMjYxMTE4MDc1OTE1WjBnMQswCQYDVQQGEwJGUjESMBAGA1UE
CAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwDTGFiMRAw
DgYDVQQLDAdEZXZpY2VzMREwDwYDVQQDDAhtNWdvLXB1YjBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABBRXlow/1+4RDh5FSlzbvvQbeo32ugDDXY+RffX1kAv95DsB
6fLhhSc8Fg2rr3sySemPbX7Bt5XIiyPGw78oxw8wCgYIKoZIzj0EAwIDSQAwRgIh
AJJGed0q5YxEyu6rxq7RYa8q11ETrm2EkD4GHxqrMUPkAiEAyrm2JuIKgBxvBywl
NlQDpBw01/c7AyxWYXQ0iARLybA=
-----END CERTIFICATE-----
)EOF";
// ------------ SENSORS ------------
// Objets capteurs pour ENV II
Adafruit_SHT31 sht31 = Adafruit_SHT31();
Adafruit_BMP280 bmp280;
// ------------ MQTT CLIENT ------------
// Client TLS (WiFiClientSecure) + client MQTT
WiFiClientSecure tlsClient;
PubSubClient mqtt(tlsClient);
unsigned long lastPub = 0;
// ------------ UI COLORS ------------
static const uint16_t COLOR_BG = 0x0000;
static const uint16_t COLOR_CARD = 0x0005;
static const uint16_t COLOR_ACCENT = 0x07FF;
static const uint16_t COLOR_TEXT = 0xFFFF;
static const uint16_t COLOR_OK = 0x07E0;
static const uint16_t COLOR_WARN = 0xF800;
// ========== Base64 & ATECC helpers (from section 7) ==========
// Table Base64 pour l'encodage DER -> PEM
static const char b64_table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Encodage Base64 classique sur des données binaires
String base64Encode(const uint8_t *data, size_t len) {
String out;
size_t i = 0;
while (i < len) {
size_t remain = len - i;
uint32_t octet_a = data[i++];
uint32_t octet_b = (remain > 1) ? data[i++] : 0;
uint32_t octet_c = (remain > 2) ? data[i++] : 0;
uint32_t triple = (octet_a << 16) | (octet_b << 8) | octet_c;
out += b64_table[(triple >> 18) & 0x3F];
out += b64_table[(triple >> 12) & 0x3F];
out += (remain > 1) ? b64_table[(triple >> 6) & 0x3F] : '=';
out += (remain > 2) ? b64_table[triple & 0x3F] : '=';
}
return out;
}
// ---------- ATECC608B Slot 8 helpers (SparkFun-only) ----------
// Compute ATECC data zone address for slot 8, 32-byte block read/write (block 0..12)
static uint16_t slot8AddressForBlock(uint8_t block)
{
// Addr[6:3] = slot number (8), Addr[8+] = block index, Addr[2:0] = word offset (0 for 32-byte access)
// See Microchip ATECC608x datasheet, "Data Zone Addressing"
return ((uint16_t)8 << 3) | ((uint16_t)block << 8);
}
// Write 32 bytes into Slot 8, given block index
bool writeDataSlot8Block(ATECCX08A &atecc, uint8_t block, const uint8_t *data32)
{
if (block >= 13) return false; // slot 8 = 13 blocks of 32 bytes (416 bytes total)
uint16_t addr = slot8AddressForBlock(block);
// SparkFun high-level wrapper:
// write(zone, address, data, length_of_data)
if (!atecc.write(ZONE_DATA, addr, (uint8_t *)data32, 32))
{
Serial.println("writeDataSlot8Block: write() failed");
return false;
}
return true;
}
// Read 32 bytes from Slot 8, given block index
bool readDataSlot8Block(ATECCX08A &atecc, uint8_t block, uint8_t *out32)
{
if (block >= 13) return false;
uint16_t addr = slot8AddressForBlock(block);
// SparkFun high-level read:
// read(zone, address, length, debug)
if (!atecc.read(ZONE_DATA, addr, 32, false))
{
Serial.println("readDataSlot8Block: read() failed");
return false;
}
// Response format in atecc.inputBuffer:
// [0] = COUNT = 32 (data) + 3 = 35
// [1..32] = data
// [33..34] = CRC
for (int i = 0; i < 32; i++)
{
out32[i] = atecc.inputBuffer[1 + i];
}
return true;
}
// Reconstruit la clé privée en PEM à partir du slot 8 (format [len][DER])
bool loadPrivateKeyPEMFromATECC(ATECCX08A &atecc, String &outPem) {
// Slot 8 size: 13 blocks * 32 bytes = 416 bytes
uint8_t slotData[416];
// Lecture de tous les blocs 0..12 du slot 8
for (uint8_t b = 0; b < 13; b++) {
if (!readDataSlot8Block(atecc, b, slotData + b * 32)) {
Serial.printf("Error reading slot 8 block %u\n", b);
return false;
}
}
// Longueur DER stockée sur 2 octets (big-endian) au début
uint16_t derLen = (slotData[0] << 8) | slotData[1];
// 416 bytes total, 2 bytes used for length => max DER size = 414
if (derLen == 0 || derLen > 414) {
Serial.printf("Invalid DER length in slot 8: %u\n", derLen);
return false;
}
const uint8_t *der = &slotData[2];
// Quick sanity check: DER PKCS#8 commence normalement par 0x30 (SEQUENCE)
if (der[0] != 0x30) {
Serial.println("Warning: DER does not start with 0x30 (SEQUENCE)");
}
// Encodage en Base64 puis ajout des headers PEM
String b64 = base64Encode(der, derLen);
outPem = "-----BEGIN PRIVATE KEY-----\n";
for (size_t i = 0; i < b64.length(); i += 64) {
outPem += b64.substring(i, i + 64) + "\n";
}
outPem += "-----END PRIVATE KEY-----\n";
return true;
}
// ------------ UI HELPERS (same as your original) ------------
void uiDrawHeader() {
auto& d = M5.Display;
d.fillScreen(COLOR_BG);
d.fillRoundRect(6, 4, d.width() - 12, 40, 8, COLOR_CARD);
d.setTextColor(COLOR_ACCENT, COLOR_CARD);
d.setTextSize(1.5);
d.setCursor(14, 20);
d.print("M5Go Secure ENV Publisher");
}
void uiDrawStatic() {
auto& d = M5.Display;
d.fillRoundRect(6, 50, (d.width() / 2) - 9, 50, 8, COLOR_CARD);
d.setTextColor(COLOR_TEXT, COLOR_CARD);
d.setTextSize(1.5);
d.setCursor(12, 60);
d.print("Wi-Fi");
int rx = (d.width() / 2) + 3;
d.fillRoundRect(rx, 50, (d.width() / 2) - 9, 50, 8, COLOR_CARD);
d.setCursor(rx + 6, 60);
d.print("MQTTS");
d.fillRoundRect(6, 106, d.width() - 12, 126, 8, COLOR_CARD);
d.setCursor(12, 120);
d.setTextColor(COLOR_ACCENT, COLOR_CARD);
d.print("Live data sent to topic m5go/env");
}
void uiSetWiFiStatus(const String& line, bool ok) {
auto& d = M5.Display;
int x = 12;
int y = 80;
int w = (d.width() / 2) - 24;
d.fillRect(x, y, w, 10, COLOR_CARD);
d.setCursor(x, y);
d.setTextColor(ok ? COLOR_OK : COLOR_WARN, COLOR_CARD);
d.print(line);
}
void uiSetMQTTStatus(const String& line, bool ok) {
auto& d = M5.Display;
int cardX = (d.width() / 2) + 3;
int x = cardX + 6;
int y = 80;
int w = (d.width() / 2) - 24;
d.fillRect(x, y, w, 10, COLOR_CARD);
d.setCursor(x, y);
d.setTextColor(ok ? COLOR_OK : COLOR_WARN, COLOR_CARD);
d.print(line);
}
void uiSetSensorValues(float t, float h, float p) {
auto& d = M5.Display;
d.setTextColor(COLOR_TEXT, COLOR_CARD);
d.setTextSize(2);
d.fillRect(12, 150, d.width() - 24, 20, COLOR_CARD);
d.setCursor(12, 150);
d.print("T:");
if (!isnan(t)) { d.print(t, 1); d.print("C"); }
else d.print("--.-C");
d.print(" H:");
if (!isnan(h)) { d.print(h, 1); d.print("%"); }
else d.print("--.-%");
d.fillRect(12, 180, d.width() - 24, 20, COLOR_CARD);
d.setCursor(12, 180);
d.print("P:");
if (!isnan(p)) { d.print(p, 1); d.print("hPa"); }
else d.print("---.-hPa");
}
void uiSetPublishInfo() {
auto& d = M5.Display;
d.setTextSize(1.5);
d.setTextColor(COLOR_ACCENT, COLOR_CARD);
d.fillRect(12, 210, d.width() - 24, 12, COLOR_CARD);
d.setCursor(12, 210);
d.print("Publishing every 5s");
}
// ------------ WIFI / MQTT / TLS ------------
// Connexion Wi-Fi
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uiSetWiFiStatus("Connecting...", false);
while (WiFi.status() != WL_CONNECTED) {
delay(300);
}
uiSetWiFiStatus(WiFi.localIP().toString(), true);
}
// Clé privée en PEM reconstituée depuis l'ATECC
String g_clientKeyPem; // dynamic key from ATECC
// Configuration TLS (CA, cert client, clé privée PEM venant de l'ATECC)
void setupTLS() {
tlsClient.setCACert(ca_cert);
tlsClient.setCertificate(client_cert);
tlsClient.setPrivateKey(g_clientKeyPem.c_str());
}
// Connexion au broker MQTT en TLS
void connectMQTT() {
mqtt.setServer(MQTT_HOST, MQTT_PORT);
while (!mqtt.connected()) {
String cid = "m5go-pub-" + String((uint32_t)ESP.getEfuseMac(), HEX);
uiSetMQTTStatus("Connecting...", false);
if (mqtt.connect(cid.c_str())) {
uiSetMQTTStatus("Connected", true);
} else {
uiSetMQTTStatus("Err:" + String(mqtt.state()), false);
delay(1500);
}
}
}
// ------------ SENSORS ------------
// Initialisation des capteurs sur le même bus I2C (Wire.begin déjà fait plus haut)
void setupSensors() {
Wire.begin(); // sensors on default I2C; ECC uses same pins but we already began earlier
sht31.begin(0x44);
bmp280.begin(0x76);
}
// ------------ SETUP / LOOP ------------
// Setup principal du M5Go publisher avec ATECC
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
M5.Display.setRotation(1);
uiDrawHeader();
uiDrawStatic();
Serial.begin(115200);
// I2C partagé entre l'ATECC et les capteurs
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000);
// Initialisation de l'ATECC608B et lecture de la config
Serial.println("Bringing up ATECC608B...");
if (!atecc.begin(ECC_ADDR, Wire, Serial)) {
Serial.println("ERROR: atecc.begin failed (TLS key will not load)");
} else {
if (!atecc.readConfigZone(false)) {
Serial.println("ERROR: readConfigZone failed");
} else {
Serial.printf("ConfigLock=%d, DataOTPLock=%d, Slot0Lock=%d\n",
atecc.configLockStatus,
atecc.dataOTPLockStatus,
atecc.slot0LockStatus);
}
// Lecture de la clé privée DER depuis le slot 8 et conversion en PEM
if (!loadPrivateKeyPEMFromATECC(atecc, g_clientKeyPem)) {
Serial.println("ERROR: could not load private key from ATECC");
} else {
Serial.println("Private key PEM built from ATECC slot 8.");
}
Serial.print("Key length from ATECC: ");
Serial.println(g_clientKeyPem.length());
}
// Initialisation capteurs + Wi-Fi
setupSensors();
connectWiFi();
// Si la clé n'a pas été correctement lue, on abandonne la connexion TLS/MQTT
if (g_clientKeyPem.length() == 0) {
Serial.println("No key from ATECC; skipping TLS/MQTT connection.");
return; // or while(1) to stop here
}
// Configuration TLS puis connexion MQTT
setupTLS();
connectMQTT();
}
// Boucle principale : maintien des connexions + publications périodiques
void loop() {
M5.update();
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
setupTLS();
}
if (!mqtt.connected()) {
connectMQTT();
}
mqtt.loop();
unsigned long now = millis();
if (now - lastPub >= 5000) {
lastPub = now;
// Lecture des capteurs ENV II
float t = sht31.readTemperature();
float h = sht31.readHumidity();
float p = NAN;
float pPa = bmp280.readPressure();
if (!isnan(pPa)) p = pPa / 100.0f;
// Mise à jour de l'affichage local
uiSetSensorValues(t, h, p);
uiSetPublishInfo();
// Construction du payload JSON
char payload[160];
snprintf(payload, sizeof(payload),
"{\"temp_c\":%.2f,\"hum_pct\":%.2f,\"press_hpa\":%.2f}",
t, h, p);
// Publication MQTT sur le topic sécurisé
mqtt.publish(TOPIC, payload, false);
}
}
Code final du subscriber : MQTTS_SE_Sub
Même principe : adapter Wi-Fi, IP Broker, CA, et Certificat Subscriber.
// Subscriber sécurisé avec ATECC608B : // - La clé privée TLS du subscriber est stockée dans le slot 8 (même format que le publisher). // - Au démarrage, lecture slot 8 -> DER -> Base64 -> PEM. // - Utilisation de cette clé PEM dans WiFiClientSecure pour l'authentification mutuelle TLS. #include <M5Unified.h> #include <WiFi.h> #include <WiFiClientSecure.h> #include <PubSubClient.h> #include <Wire.h> // ATECC608B (SparkFun) #include// Brochage I2C de l'ATECC côté subscriber static const uint8_t I2C_SDA = 32; static const uint8_t I2C_SCL = 33; static const uint8_t ECC_ADDR = 0x35; ATECCX08A atecc; // -------- WIFI ---------- const char* WIFI_SSID = "**********"; const char* WIFI_PASS = "**********"; // -------- MQTT ---------- const char* MQTT_HOST = "**********"; const uint16_t MQTT_PORT = 8883; const char* TOPIC = "m5go/env"; // -------- CA CERT (same CA as publisher) ---------- // CA commune au broker, au publisher et au subscriber static const char ca_cert[] PROGMEM = R"EOF( -----BEGIN CERTIFICATE----- MIIBwDCCAWYCCQDCLzlU+p80FTAKBggqhkjOPQQDAjBoMQswCQYDVQQGEwJGUjES MBAGA1UECAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwD TGFiMQ0wCwYDVQQLDARNUVRUMRUwEwYDVQQDDAxMb2NhbE1RVFQtQ0EwHhcNMjUx MTE4MDc1ODQ0WhcNMzUxMTE2MDc1ODQ0WjBoMQswCQYDVQQGEwJGUjESMBAGA1UE CAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwDTGFiMQ0w CwYDVQQLDARNUVRUMRUwEwYDVQQDDAxMb2NhbE1RVFQtQ0EwWTATBgcqhkjOPQIB BggqhkjOPQMBBwNCAATPDXo6/5pAbxyrHaQpEHR33AR1vWDcJk+H37T/oMOgMpRL +ipsBETdbz46Dz2v8/da6NKXzeaGnVFyZIeeqJPjMAoGCCqGSM49BAMCA0gAMEUC IGPrNzv/LGSCuUrBLCHPUTrDWnm1T7BtV8I2J4lruIuGAiEAknzvAWYCkJyYuq0S yHP8+qRKegc2kbXrHI5Fw00bM40= -----END CERTIFICATE----- )EOF"; // -------- CLIENT CERT (subscriber: m5sub-display) ---------- // Certificat X.509 du subscriber (m5sub-display) static const char client_cert[] PROGMEM = R"EOF( -----BEGIN CERTIFICATE----- MIIBxDCCAWoCCQCU9fxQbpMYETAKBggqhkjOPQQDAjBoMQswCQYDVQQGEwJGUjES MBAGA1UECAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwD TGFiMQ0wCwYDVQQLDARNUVRUMRUwEwYDVQQDDAxMb2NhbE1RVFQtQ0EwHhcNMjUx MTE4MDc1OTI2WhcNMjYxMTE4MDc1OTI2WjBsMQswCQYDVQQGEwJGUjESMBAGA1UE CAwJT2NjaXRhbmllMREwDwYDVQQHDAhUb3Vsb3VzZTEMMAoGA1UECgwDTGFiMRAw DgYDVQQLDAdEZXZpY2VzMRYwFAYDVQQDDA1tNXN1Yi1kaXNwbGF5MFkwEwYHKoZI zj0CAQYIKoZIzj0DAQcDQgAEIVXU/cVVe9o47aoqEAXK1lsUeJPzBN4d2LT15j1a EUpUB/7FhUJduZpGUYSTZgpx9eyR5HtSdn6GXKCtI4EzjTAKBggqhkjOPQQDAgNI ADBFAiAYnrRyQFHy2E7k7O+zt7YGvssxEH2g+c1dNhaiTe58awIhAOzbfpHlcbnH pH8/wnXlCS/Scki7fXG1vK6yimqflcky -----END CERTIFICATE----- )EOF"; // ------------ MQTT CLIENT ------------ // Client TLS + MQTT WiFiClientSecure tlsClient; PubSubClient mqtt(tlsClient); // ------------ UI COLORS ------------ static const uint16_t COLOR_BG = 0x0000; // black static const uint16_t COLOR_CARD = 0x0005; // deep blue static const uint16_t COLOR_ACCENT = 0x07FF; // cyan static const uint16_t COLOR_TEXT = 0xFFFF; // white static const uint16_t COLOR_OK = 0x07E0; // green static const uint16_t COLOR_WARN = 0xF800; // red // ------------ Base64 helper ------------ // Table Base64 utilisée pour l'encodage DER -> PEM static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // Encodage Base64 générique String base64Encode(const uint8_t *data, size_t len) { String out; size_t i = 0; while (i < len) { size_t remain = len - i; uint32_t octet_a = data[i++]; uint32_t octet_b = (remain > 1) ? data[i++] : 0; uint32_t octet_c = (remain > 2) ? data[i++] : 0; uint32_t triple = (octet_a << 16) | (octet_b << 8) | octet_c; out += b64_table[(triple >> 18) & 0x3F]; out += b64_table[(triple >> 12) & 0x3F]; out += (remain > 1) ? b64_table[(triple >> 6) & 0x3F] : '='; out += (remain > 2) ? b64_table[triple & 0x3F] : '='; } return out; } // ---------- ATECC608B Slot 8 helpers ---------- // Calcul de l'adresse ATECC pour slot 8 / bloc "block" static uint16_t slot8AddressForBlock(uint8_t block) { return ((uint16_t)8 << 3) | ((uint16_t)block << 8); } // Lecture d'un bloc de 32 octets du slot 8 bool readDataSlot8Block(ATECCX08A &atecc, uint8_t block, uint8_t *out32) { if (block >= 13) return false; uint16_t addr = slot8AddressForBlock(block); if (!atecc.read(ZONE_DATA, addr, 32, false)) { Serial.println("readDataSlot8Block: read() failed"); return false; } // atecc.inputBuffer: [0]=count, [1..32]=data, [33..34]=CRC for (int i = 0; i < 32; i++) { out32[i] = atecc.inputBuffer[1 + i]; } return true; } // Reconstruire la clé privée PEM à partir du blob [len][DER] en slot 8 bool loadPrivateKeyPEMFromATECC(ATECCX08A &atecc, String &outPem) { uint8_t slotData[416]; // Lecture des 13 blocs du slot 8 for (uint8_t b = 0; b < 13; b++) { if (!readDataSlot8Block(atecc, b, slotData + b * 32)) { Serial.printf("Error reading slot 8 block %u\n", b); return false; } } // Lecture de la longueur DER sur 2 octets uint16_t derLen = (slotData[0] << 8) | slotData[1]; if (derLen == 0 || derLen > 414) { Serial.printf("Invalid DER length in slot 8: %u\n", derLen); return false; } const uint8_t *der = &slotData[2]; if (der[0] != 0x30) { Serial.println("Warning: DER does not start with 0x30 (SEQUENCE)"); } // Encodage Base64 puis ajout headers PEM String b64 = base64Encode(der, derLen); outPem = "-----BEGIN PRIVATE KEY-----\n"; for (size_t i = 0; i < b64.length(); i += 64) { outPem += b64.substring(i, i + 64) + "\n"; } outPem += "-----END PRIVATE KEY-----\n"; return true; } // ------------ UI HELPERS ------------ // En-tête de l'écran du subscriber void uiDrawHeader() { auto& d = M5.Display; d.fillScreen(COLOR_BG); d.fillRoundRect(6, 4, d.width() - 12, 40, 8, COLOR_CARD); d.setTextColor(COLOR_ACCENT, COLOR_CARD); d.setTextSize(1.5); d.setCursor(14, 20); d.print("M5Stack Secure ENV Subscriber"); } // Parties statiques (Wi-Fi, MQTTS, texte topic) void uiDrawStatic() { auto& d = M5.Display; d.fillRoundRect(6, 50, (d.width() / 2) - 9, 50, 8, COLOR_CARD); d.setTextColor(COLOR_TEXT, COLOR_CARD); d.setTextSize(1.5); d.setCursor(12, 60); d.print("Wi-Fi"); int rx = (d.width() / 2) + 3; d.fillRoundRect(rx, 50, (d.width() / 2) - 9, 50, 8, COLOR_CARD); d.setCursor(rx + 6, 60); d.print("MQTTS"); d.fillRoundRect(6, 106, d.width() - 12, 126, 8, COLOR_CARD); d.setCursor(12, 120); d.setTextColor(COLOR_ACCENT, COLOR_CARD); d.print("Data received from topic m5go/env"); } // Affichage statut Wi-Fi void uiSetWiFiStatus(const String& line, bool ok) { auto& d = M5.Display; int x = 12; int y = 80; int w = (d.width() / 2) - 24; d.fillRect(x, y, w, 10, COLOR_CARD); d.setCursor(x, y); d.setTextColor(ok ? COLOR_OK : COLOR_WARN, COLOR_CARD); d.print(line); } // Affichage statut MQTT void uiSetMQTTStatus(const String& line, bool ok) { auto& d = M5.Display; int cardX = (d.width() / 2) + 3; int x = cardX + 6; int y = 80; int w = (d.width() / 2) - 24; d.fillRect(x, y, w, 10, COLOR_CARD); d.setCursor(x, y); d.setTextColor(ok ? COLOR_OK : COLOR_WARN, COLOR_CARD); d.print(line); } // Affichage des valeurs T/H/P void uiSetSensorValues(float t, float h, float p) { auto& d = M5.Display; d.setTextColor(COLOR_TEXT, COLOR_CARD); d.setTextSize(2); d.fillRect(12, 150, d.width() - 24, 20, COLOR_CARD); d.setCursor(12, 150); d.print("T:"); if (!isnan(t)) { d.print(t, 1); d.print("C"); } else d.print("--.-C"); d.print(" H:"); if (!isnan(h)) { d.print(h, 1); d.print("%"); } else d.print("--.-%"); d.fillRect(12, 180, d.width() - 24, 20, COLOR_CARD); d.setCursor(12, 180); d.print("P:"); if (!isnan(p)) { d.print(p, 1); d.print("hPa"); } else d.print("---.-hPa"); } // Indication d'une dernière mise à jour void uiSetLastUpdate() { auto& d = M5.Display; d.setTextSize(1.5); d.setTextColor(COLOR_ACCENT, COLOR_CARD); d.fillRect(12, 210, d.width() - 24, 12, COLOR_CARD); d.setCursor(12, 210); d.print("Last update received"); } // ------------ WIFI / TLS ------------ // Connexion Wi-Fi void connectWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); uiSetWiFiStatus("Connecting...", false); while (WiFi.status() != WL_CONNECTED) { delay(300); } uiSetWiFiStatus(WiFi.localIP().toString(), true); } // Clé privée PEM issue de l'ATECC String g_clientKeyPem; // Configuration TLS (CA, cert client, private key PEM) void setupTLS() { tlsClient.setCACert(ca_cert); tlsClient.setCertificate(client_cert); tlsClient.setPrivateKey(g_clientKeyPem.c_str()); } // ------------ MQTT CALLBACK ------------ // Callback appelée pour chaque message reçu sur les topics abonnés void handleMessage(char* topic, byte* payload, unsigned int length) { static char buf[256]; unsigned int len = (length < sizeof(buf)-1) ? length : sizeof(buf)-1; memcpy(buf, payload, len); buf[len] = '\0'; String msg = String(buf); float t = NAN, h = NAN, p = NAN; // Parsing manuel du JSON par recherche de sous-chaînes int ti = msg.indexOf("\"temp_c\":"); int hi = msg.indexOf("\"hum_pct\":"); int pi = msg.indexOf("\"press_hpa\":"); if (ti >= 0) t = msg.substring(ti + 9).toFloat(); if (hi >= 0) h = msg.substring(hi + 10).toFloat(); if (pi >= 0) p = msg.substring(pi + 12).toFloat(); // Mise à jour visuelle uiSetSensorValues(t, h, p); uiSetLastUpdate(); // Log série pour debug Serial.print("Msg ["); Serial.print(topic); Serial.print("]: "); Serial.println(msg); } // ------------ MQTT ------------ // Connexion sécurisé au broker et abonnement au topic m5go/env void connectMQTT() { mqtt.setServer(MQTT_HOST, MQTT_PORT); mqtt.setCallback(handleMessage); while (!mqtt.connected()) { String cid = "m5sub-" + String((uint32_t)ESP.getEfuseMac(), HEX); uiSetMQTTStatus("Connecting...", false); if (mqtt.connect(cid.c_str())) { uiSetMQTTStatus("Connected", true); mqtt.subscribe(TOPIC); } else { uiSetMQTTStatus("Err:" + String(mqtt.state()), false); delay(1500); } } } // ------------ SETUP / LOOP ------------ // Setup du subscriber avec ATECC void setup() { auto cfg = M5.config(); M5.begin(cfg); M5.Display.setRotation(1); uiDrawHeader(); uiDrawStatic(); Serial.begin(115200); // I2C vers l'ATECC Wire.begin(I2C_SDA, I2C_SCL); Wire.setClock(100000); Serial.println("Bringing up ATECC608B (subscriber)..."); if (!atecc.begin(ECC_ADDR, Wire, Serial)) { Serial.println("ERROR: atecc.begin failed (TLS key will not load)"); } else { if (!atecc.readConfigZone(false)) { Serial.println("ERROR: readConfigZone failed"); } else { Serial.printf("ConfigLock=%d, DataOTPLock=%d, Slot0Lock=%d\n", atecc.configLockStatus, atecc.dataOTPLockStatus, atecc.slot0LockStatus); } // Lecture de la clé privée spécifiquement provisionnée pour le subscriber if (!loadPrivateKeyPEMFromATECC(atecc, g_clientKeyPem)) { Serial.println("ERROR: could not load private key from ATECC"); } else { Serial.println("Private key PEM built from ATECC slot 8."); #ifdef DEBUG_PRINT_PEM // Affichage de la clé en PEM sur le port série (pour debug) Serial.println(g_clientKeyPem); #endif } Serial.print("Key length from ATECC: "); Serial.println(g_clientKeyPem.length()); } // Connexion Wi-Fi connectWiFi(); // Si aucune clé n'a été lue, on ne tente pas la connexion TLS/MQTT if (g_clientKeyPem.length() == 0) { Serial.println("No key from ATECC; skipping TLS/MQTT connection."); return; } // Configuration TLS avec la clé lue, puis connexion MQTT setupTLS(); connectMQTT(); } void loop() { M5.update(); if (WiFi.status() != WL_CONNECTED) { connectWiFi(); if (g_clientKeyPem.length() > 0) setupTLS(); } if (g_clientKeyPem.length() > 0 && !mqtt.connected()) { connectMQTT(); } mqtt.loop(); }
Séquence de test globale
- Tester le secure element sur chaque M5 avec
Scan_SE. - Provisonner le slot 8 de chaque ATECC avec
Write_SE(publisher / subscriber). - Configurer Mosquitto en TLS mutuel (port 8883, certs).
- Flasher
MQTTS_SE_Pubsur le publisher. - Flasher
MQTTS_SE_Subsur le subscriber. - Vérifier les logs Mosquitto (connexions TLS OK).
- Vérifier l'affichage des données sur le subscriber.
Télécharger le Code Source
Obtenez tous les sketches Arduino, fichiers de config et scripts auxiliaires.