Skip to content

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

1

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)
HUB
ENV II Unit
0x44 (SHT30) 0x76 (BMP280)
ATECC608B Unit
0x35 (I2C)
GND (Noir)
VCC (Rouge - 5V)
SCL (Jaune - Pin 1)
SDA (Blanc - Pin 2)

1.2 Bibliothèques nécessaires (Arduino)

M5Unified PubSubClient WiFiClientSecure SparkFun_ATECCX08a Adafruit_SHT31 Adafruit_BMP280

1.3 Rôles des sketches

Scan_SE

Outil de diagnostic. Vérifie la connexion I2C (0x35) et lit le statut.

Write_SE

Outil de provisionnement. Écrit la clé privée (DER) dans le Slot 8.

MQTTS_SE_Pub

Publisher sécurisé. Lit la clé du Slot 8 -> RAM -> Connexion TLS.

MQTTS_SE_Sub

Subscriber sécurisé. Affiche les données reçues sur l'écran LCD.

2

Préparation de l’environnement Arduino

2.1 Installation des bibliothèques

1
Gestionnaire de Cartes Installez le support ESP32 by Espressif dans l'IDE Arduino.
2
Gestionnaire de Bibliothèques Installez M5Unified, PubSubClient, WiFiClientSecure, Adafruit_SHT31, Adafruit_BMP280, et SparkFun_ATECCX08a_Arduino_Library.
3
Sélectionner la Carte Assurez-vous que M5Stack-Core (ou M5Go) est sélectionné dans Outils > Type de carte.

2.2 Configuration des broches I2C

C++
static const uint8_t I2C_SDA  = 32;
static const uint8_t I2C_SCL  = 33;
static const uint8_t ECC_ADDR = 0x35;
3

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.

Terminal
# 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
4

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)

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

5

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.

Scan_SE.ino
// 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.

6

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

Bash & Python
# 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).

7

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.

Write_SE.ino
// 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.

8

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.

MQTTS_SE_Pub.ino
// 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);
  }
}
9

Code final du subscriber : MQTTS_SE_Sub

Même principe : adapter Wi-Fi, IP Broker, CA, et Certificat Subscriber.

MQTTS_SE_Sub.ino
// 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();
}
10

Séquence de test globale

  1. Tester le secure element sur chaque M5 avec Scan_SE.
  2. Provisonner le slot 8 de chaque ATECC avec Write_SE (publisher / subscriber).
  3. Configurer Mosquitto en TLS mutuel (port 8883, certs).
  4. Flasher MQTTS_SE_Pub sur le publisher.
  5. Flasher MQTTS_SE_Sub sur le subscriber.
  6. Vérifier les logs Mosquitto (connexions TLS OK).
  7. Vérifier l'affichage des données sur le subscriber.
Ressources

Télécharger le Code Source

Obtenez tous les sketches Arduino, fichiers de config et scripts auxiliaires.