Skip to content

Codes Sources Complets

Accédez au code source brut pour les trois étapes, y compris les sketches Arduino (C++), les fichiers de configuration et les scripts auxiliaires.

mqtt_plain_publisher.ino
Étape 1 : Sketch Publisher (Sans TLS)
// Mesures ENV II (SHT31 + BMP280) sur M5Go et publication en clair sur un broker MQTT (sans TLS)

#include <M5Unified.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>

#include <Adafruit_SHT31.h>
#include <Adafruit_BMP280.h>

// Identifiants Wi-Fi (réseau non chiffré au niveau application, seulement WPA2 au niveau Wi-Fi)
const char* WIFI_SSID = "**********";
const char* WIFI_PASS = "**********";

// Paramètres du broker MQTT (sans TLS)
const char* MQTT_HOST = "**********";
const uint16_t MQTT_PORT = 1883;
const char* TOPIC = "m5go/env";

// Objets pour les capteurs ENV II
Adafruit_SHT31  sht31 = Adafruit_SHT31();
Adafruit_BMP280 bmp280;

// Client TCP sans TLS -> tout le trafic MQTT passe en clair
WiFiClient wifiClient;       // pas de TLS -> trafic en clair
PubSubClient mqtt(wifiClient);

unsigned long lastPub = 0;   // timestamp de la dernière publication (ms depuis boot)

// Fonction utilitaire : affiche une ligne de texte sur l'écran à la ligne "line"
void drawLine(const String& s, int line) {
  M5.Display.setCursor(0, 24 * line);
  M5.Display.fillRect(0, 24 * line, M5.Display.width(), 24, BLACK);
  M5.Display.print(s);
}

// Connexion au Wi-Fi (bloquante tant que non connectée)
void connectWiFi() {
  WiFi.mode(WIFI_STA);                     // mode station (client)
  WiFi.begin(WIFI_SSID, WIFI_PASS);        // démarrage de la connexion
  drawLine("WiFi: connecting...", 0);
  while (WiFi.status() != WL_CONNECTED) {  // boucle jusqu'à connexion
    delay(300);
  }
  // Une fois connecté, on affiche l'adresse IP locale
  drawLine("WiFi: " + WiFi.localIP().toString(), 0);
}

// Connexion au broker MQTT (sans TLS)
void connectMQTT() {
  mqtt.setServer(MQTT_HOST, MQTT_PORT);    // configuration de l'adresse du broker
  while (!mqtt.connected()) {              // boucle jusqu'à connexion MQTT
    // clientId unique basé sur l'adresse MAC de l'ESP32
    String cid = "m5go-" + String((uint32_t)ESP.getEfuseMac(), HEX);
    drawLine("MQTT: connecting...", 1);
    if (mqtt.connect(cid.c_str())) {
      drawLine("MQTT: connected", 1);
    } else {
      // Affiche le code d'erreur MQTT (state)
      drawLine("MQTT rc=" + String(mqtt.state()), 1);
      delay(1500);
    }
  }
}

// Initialisation des capteurs I2C ENV II
void setupSensors() {
  Wire.begin();                           // SDA=21, SCL=22 (bus I2C par défaut du M5)
  if (!sht31.begin(0x44)) {              // initialisation SHT31 à l'adresse 0x44
    drawLine("SHT31 not found", 2);
  }
  if (!bmp280.begin(0x76)) {             // initialisation BMP280 à l'adresse 0x76
    drawLine("BMP280 not found", 3);
  }
}

// Fonction setup : initialisation générale (écran, série, capteurs, Wi-Fi, MQTT)
void setup() {
  auto cfg = M5.config();                 // configuration par défaut M5Unified
  M5.begin(cfg);
  M5.Display.setRotation(1);              // orientation paysage
  M5.Display.fillScreen(BLACK);
  M5.Display.setTextSize(2);
  M5.Display.setTextColor(WHITE, BLACK);
  drawLine("M5Go ENV -> MQTT", 0);        // en-tête sur l'écran

  Serial.begin(115200);                   // debug série
  setupSensors();                         // init capteurs ENV II
  connectWiFi();                          // connexion Wi-Fi
  connectMQTT();                          // connexion au broker MQTT
}

// Boucle principale : assure la connexion et publie toutes les 5 secondes
void loop() {
  M5.update();                            // gestion des boutons / événements M5

  // Si on perd le Wi-Fi ou MQTT, on tente de se reconnecter
  if (WiFi.status() != WL_CONNECTED) connectWiFi();
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();                            // gestion interne du client MQTT (keep-alive, etc.)

  unsigned long now = millis();
  if (now - lastPub >= 5000) {  // toutes les 5 s
    lastPub = now;

    // Lecture capteur SHT30/SHT31
    float t = sht31.readTemperature();     // température en °C (NAN si échec)
    float h = sht31.readHumidity();        // humidité relative en %

    // Lecture capteur BMP280
    float p = NAN;
    float pPa = bmp280.readPressure();     // pression en Pascal
    if (!isnan(pPa)) p = pPa / 100.0f;     // conversion Pa -> hPa

    // Construction du payload JSON (format texte)
    char payload[160];
    snprintf(payload, sizeof(payload),
             "{\"temp_c\":%.2f,\"hum_pct\":%.2f,\"press_hpa\":%.2f}",
             t, h, p);

    // Affichage local sur l'écran
    drawLine("T: " + String(t,1) + " C  H: " + String(h,1) + " %", 2);
    drawLine("P: " + String(p,1) + " hPa", 3);
    drawLine(String("PUB ")+TOPIC, 4);

    // Publication MQTT sur le topic (QoS 0, message non retenu)
    mqtt.publish(TOPIC, payload, false);
  }
}
mqtts_no_se_publisher.ino
Étape 2 : MQTTS avec Clés Firmware
// Publisher sécurisé : M5Go envoie les mesures ENV II via MQTT sur un broker TLS
// La clé privée et le certificat client sont stockés en clair dans le firmware (PROGMEM).

#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <Wire.h>

#include <Adafruit_SHT31.h>
#include <Adafruit_BMP280.h>

// -------- WIFI ----------
// Réseau Wi-Fi local auquel M5Go va se connecter
const char* WIFI_SSID = "**********";
const char* WIFI_PASS = "**********";

// -------- MQTT ----------
// Broker Mosquitto configuré avec TLS sur le port 8883
const char* MQTT_HOST = "**********";  // Broker IP
const uint16_t MQTT_PORT = 8883;
const char* TOPIC = "m5go/env";

// -------- CA CERT ----------
// Certificat de l'autorité de certification (CA) qui a signé le certificat du broker.
// Permet au client de vérifier qu'il parle bien au bon broker TLS.
static const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFsTCCA5mgAwIBAgIUCQ3b6jzGLXOCrozP4jU46+/w71wwDQYJKoZIhvcNAQEL
BQAwaDELMAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwI
VG91bG91c2UxDDAKBgNVBAoMA0xhYjENMAsGA1UECwwETVFUVDEVMBMGA1UEAwwM
TG9jYWxNUVRULUNBMB4XDTI1MTExMTE3MzM1NVoXDTM1MTEwOTE3MzM1NVowaDEL
MAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwIVG91bG91
c2UxDDAKBgNVBAoMA0xhYjENMAsGA1UECwwETVFUVDEVMBMGA1UEAwwMTG9jYWxN
UVRULUNBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmrAkrWR8biEZ
e+wY+wev8QW6f487FE/rWEqXnpFVB4IuLnv0Djvgjlwbrk1pQ6Wfbp0jVExSaIoF
7NvxuvmNLLJAGW2mzwYM6ZXl716/kTDyA7ucmgYcVgBY4YIk5QVq2aikb4srI6dr
oUkgxkfstbIQQo3OE9BrI96ubEnrRNPUeFQEkp0ZO47zEY4nD6U6OPwfDf9eOFC4
TLV1dUM8i5V4ReB1dhrkyh2ljdf/WruFD4BFfGcIbMpTlQRiXAjJXLoI9PejEC/k
r+I1lml0GJQaQB/fW4ZlTjag1FVMAeSCK3uVAaS4lXAB7Rz1Juxc4vVjxbkWtqEX
YvubUn75W4hcQWbE1NUqlzlbSRAfJnigns6XH1U9jMNqhVRkCLbAKHqoO1nhuQWo
zXXwQJQohfspbAqIzuNoEzT0QzWwB+BNb1QDfwYMZ79A4lyIvOq2NpDecL3u8J4N
CSD6d/dafkWdiM0Le+FMoMPeVMzs6vKttZsNu9+eoTyq2rjlA8hVN50hYEgskZU+
6yUdfN25lPA8ZIMlP/XltXUtJO6GkG4oGb8m7nBV5OHrt09TOJAhMAyxx3DUrwgJ
UEK65eeIgNjfFOi2Ul6LLzUs0PY99F736YuDfWSxQ+8RMlY58OaPwSd4eHcudlSb
vx0K+2N7GtSy2IYsJvtWrwNU9bIAoI0CAwEAAaNTMFEwHQYDVR0OBBYEFG72k3vn
Sxr5a0eGnVObzaAhOJdZMB8GA1UdIwQYMBaAFG72k3vnSxr5a0eGnVObzaAhOJdZ
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABuWRAzwRX0TMlnc
e/YY3OxgAzphDhWEeWujaL5ybqk3ttOkYomG+YGbMJxbY4z8CMJiKOXH6/J7ZEpJ
sDWZXd48Fd6WEgpkluB3HUb3XfCu5CPIbL0m4hg0kr2pCeffqP005iC8ONMFMtGA
xanV3R2teA7we9NCqSzk5MyG3uxeeOobmZ3Moeg9RG8gEQ1tEhB8OWNPqkq4OERn
3Gs0NN2gPP0Ep8a4El3yr+lP9sy2uUTBLZm0jDDlTX+bXubEzg2W3W1UOmVI1xtu
78VE3PoqWj8GO1PVl2kOw45yov//XA0tTEvUyguiCWhO4RaXJUGCC2roiYGZ6voP
9XNLFQkXi6y61KBsr/h7wmRYFYRkwBSKTQ7mKxdMV6R4dwFiIuHaXxyzLzwJKegZ
uY3SMGXTuSorndmXDg6U9zQDiOFOGWfQnH6UaP5VjpblANu4uzn41gjFiJN9Kt3q
KrOnC58qsI7g5PbfaKypsc15wOTlZhZl8U/UKnmuBT7T6X8gzdQecSR2fc1T35fe
+a5EtKyHnhpfc893SGmd+o3FD/U/IQUBZK/Ailo/pRO+muBRpbQcIkvLX5PC8B/j
r1uTV+/tXOHybbkYrT0p1i5Coc5+nRF1/utwPTm6jzAHlLJXe9Q/8lurQ1qjPvaG
KGuyhMbNWN2zYFWrpRiyuWjElDuF
-----END CERTIFICATE-----
)EOF";

// -------- CLIENT CERT (m5go-pub) ----------
// Certificat X.509 du client (M5Go publisher), signé par la CA ci-dessus.
static const char client_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIEnzCCAoegAwIBAgIUZ0trSk8RXBw+4/8NrTPscJPOiGEwDQYJKoZIhvcNAQEL
BQAwaDELMAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwI
VG91bG91c2UxDDAKBgNVBAoMA0xhYjENMAsGA1UECwwETVFUVDEVMBMGA1UEAwwM
TG9jYWxNUVRULUNBMB4XDTI1MTExMTE3NDUxNVoXDTI2MTExMTE3NDUxNVowZzEL
MAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwIVG91bG91
c2UxDDAKBgNVBAoMA0xhYjEQMA4GA1UECwwHRGV2aWNlczERMA8GA1UEAwwIbTVn
by1wdWIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDwesIQB5i/AEO9
pC8wWEH40RZT53wXkn81w9DYtZrSU/Y1Aih2MlgoG97AdlAhnQXMrCkpZ48J46OA
h0B+hZlAsQwZIl9Rleirt1gr4M19XXANTQDs/DwCuQ6tUI0k8ySVW+b0MAPbtxbO
sheYUho/XU7tNCZVOjg+OX1PxM4N9uKp6K77cpJHQkhE2hqVIhYgLOejj1D+V5OT
BkNZ2qVM+7M0GtS8EWQyDYVuKgBdrlaAXVaBUVMs0L8GicI/cEB0qzebhZhv0ANP
U0mILQQ0i7H3HdZIxhJye0yeff82akjfY7Cc/TRRVyi4IPjNXtmLHUWnQghrFywh
7vCnPyObAgMBAAGjQjBAMB0GA1UdDgQWBBRylU3ZhLbvd8w4veGLeb8giqTm8jAf
BgNVHSMEGDAWgBRu9pN750sa+WtHhp1Tm82gITiXWTANBgkqhkiG9w0BAQsFAAOC
AgEAU93GH3175NH4D5Ibzfq6kc8/ZWGzTOXAOMhXN3Sefg1oEuJR5f53u0+35ab0
Lg3qM2JroIAFulAKQBbSvQ70+sk46KtljI3VBZSQrmzTqh88R2hXifpPDGn58K9N
c/mYd8/BMJcZx9SIlcll+OlBGq3JzajWWl8bg8LVgDDovfr4CGcKuSizm4WC8soG
zCjXjQv8Vyikhw6UzUhgBU9iXV9MMs2k1iieXQDLoYZq+t/mtMjnRmb0SrNV0/XC
8ZjwRiQmkJv1ADefwJjPzagBy/YvoMg/e1IV9KNrS9zUgw1/z8f466FeqlKuUczW
43ho2X1oXPB4YS1LKEyI5ipOaKew8i6lle/r6GFrEI85S/ic+foPZhNlLAU9rsN2
84RqEyl1V87YkxO6/L/C2ebenpizJLPH00GwKtlkSBIhfHyfCVOT1koNYHG5sG/0
CzMaY5elcFJQREMUtT03LnFI1v/nb9ulvNY16eODi9YklLWmuiCrxMCCcHVQPuVY
hRAbsWhYD10LFPVCMQHTnwpITAkfheMThMY+HOkObfj5H7O5q2e3WFRP6b6EIOwJ
AypR3wVRJ0f31ntqpYjpmAAmqlCmkhxTMz1IdteGOeTVMK59GSMbqYObVqQGCEuw
4YXW3Gup0KiWk7ZsOijN/SHjm/nP8SojOqzK/sB27EAafm4=
-----END CERTIFICATE-----
)EOF";

// -------- CLIENT KEY (m5go-pub) ----------
// Clé privée correspondant au certificat client ci-dessus (stockée en clair dans le firmware).
static const char client_key[] PROGMEM = R"EOF(
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwesIQB5i/AEO9
pC8wWEH40RZT53wXkn81w9DYtZrSU/Y1Aih2MlgoG97AdlAhnQXMrCkpZ48J46OA
h0B+hZlAsQwZIl9Rleirt1gr4M19XXANTQDs/DwCuQ6tUI0k8ySVW+b0MAPbtxbO
sheYUho/XU7tNCZVOjg+OX1PxM4N9uKp6K77cpJHQkhE2hqVIhYgLOejj1D+V5OT
BkNZ2qVM+7M0GtS8EWQyDYVuKgBdrlaAXVaBUVMs0L8GicI/cEB0qzebhZhv0ANP
U0mILQQ0i7H3HdZIxhJye0yeff82akjfY7Cc/TRRVyi4IPjNXtmLHUWnQghrFywh
7vCnPyObAgMBAAECggEAa6BVSD1OJHWm27ImmHl3lTdmych5ZkdffZ2U09h7YRTI
xTKhDz067UCD8hlBhbm0BcUraud5QhDKdVSTDc0XKLfUVU9n36i7CFc3M/QZo7j0
1E8ZUfcVmJZgNjst4FKdlecat0DiCSypHXrhSn+8VY2aLFlBqrUyxM6QAepv2hk9
b2QtAUV24t55nj6d7QpgIO+gsiJ2Xzx54S2YY5Pf/mxTslIE0t+ocFumvegPY7SG
FJkOqlV1LtuF47geBN9K5hWUcPMg1h5gijFQ39btrPHP4Pg7V4qFCjkrOpBWR/S5
8dpf+LZiOYJa49cvC5wajAM02RDyg7BsxaHKnhmEuQKBgQD+qCP0BJhbZ7eevU3y
v/0KDJNyQIdzMuUfngACNojiKzRfixPl0X5M8jL2ws4c8VrkLiL7Y+AiII1cV2FV
OEsnRrHQ7mL8baXWeCI4jW8wTX7ces2mnxlYLpx1H9BxxLUHiRK04DlL1kmeNwfv
ap7qULMvt4eBVcGLEPv59EF1lwKBgQDxv3lnjlA9w/DPYpdaIoY0lszlSkkagBdU
/A4WU2ZCXZslEJHGyTrC1Rerbo/IQ4YCce959MuBC7LPFVzC1Sg5l4slEoFsVYqI
7uFwxH19SYy582WKccF+vhzQrAGNRF29osVCRG3/0fg7imju983+bmi3dC1kMS1f
K2hIZxLqnQKBgCPXdnf5zZfP2UA2VKo961dmvbnu6yGDoEv66PVmx41Nl2l7IanO
+n/J9vJUKL5aGfjTpYjMXddvzXWZttFPwwQcJxrI8pWkuRqeffKHtYaO4bQWdKtm
6SJILS0u9R+OGAyfdkqO5IGP/3yNMki4MPW5tf6ZTjEd6Mex9EUR48SxAoGBAK2s
KyHUQZ096QB0Cdu9NcEOHUEUbxRUtW5ebhhn7ez7pnuoPbIb2tUhlZGZKj5rFBkp
lSt+S7z3lIvlAvENhYpqbpJBiy0y/wWE5/zFjIm3jxv/2hDtzF6rYbQf/jVoyhd0
mlYTJxtD0xujQeN7r0d8Nkqlcf6qvbfDTXXGZvaZAoGBAMxmQoUUY0ibwviE8X7H
5PArmuSDoKY1JPBLzdmi7JyUJqCKrPDScqFsK8sNyLiO+8/O/XQf0XCzyFOGPhgy
H/zKp9PQRcXvSWIYAXTAdBR87EQcm2QxyJe0IDYE+hLK0NYIVM4YhVEDyFxJiq72
bsw9XMOYaaccbZWZg8rhxZGm
-----END PRIVATE KEY-----
)EOF";

// ------------ SENSORS ------------
// Objets pour les capteurs ENV II
Adafruit_SHT31  sht31 = Adafruit_SHT31();
Adafruit_BMP280 bmp280;

// ------------ MQTT CLIENT ------------
// Client TLS (WiFiClientSecure) encapsulé dans PubSubClient pour MQTT
WiFiClientSecure tlsClient;
PubSubClient mqtt(tlsClient);

unsigned long lastPub = 0;   // horodatage de la dernière publication

// ------------ UI COLORS (RGB565) ------------
// Palette de couleurs pour l'affichage (fond, cartes, texte, etc.)
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

// ------------ UI HELPERS ------------
// Dessine l'en-tête de l'écran
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");
}

// Dessine les cartes statiques (Wi-Fi, MQTTS, info topic)
void uiDrawStatic() {
  auto& d = M5.Display;

  // WiFi card
  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");

  // MQTT card
  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");

  // Data card
  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");
}

// Met à jour l'état Wi-Fi (texte + couleur OK/WARN)
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);
}

// Met à jour l'état MQTT (texte + couleur OK/WARN)
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);
}

// Affiche les valeurs de température, humidité et pression sur l'écran
void uiSetSensorValues(float t, float h, float p) {
  auto& d = M5.Display;
  d.setTextColor(COLOR_TEXT, COLOR_CARD);
  d.setTextSize(2);

  // Ligne 1: T / H
  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("--.-%");

  // Ligne 2: P
  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");
}

// Affiche une info sur la fréquence de publication
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 au Wi-Fi (bloquante tant que non connectée)
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);
}

// Configuration du contexte TLS avec la CA, le certificat client et la clé privée
void setupTLS() {
  tlsClient.setCACert(ca_cert);          // CA du broker
  tlsClient.setCertificate(client_cert); // certificat client m5go-pub
  tlsClient.setPrivateKey(client_key);   // clé privée du client
}

// Connexion au broker MQTT sur TLS (port 8883)
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 ENV II sur I2C
void setupSensors() {
  Wire.begin();
  sht31.begin(0x44);
  bmp280.begin(0x76);
}

// ------------ SETUP / LOOP ------------
// Initialisation du M5, des capteurs, du Wi-Fi et de la pile TLS/MQTT
void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Display.setRotation(1);
  uiDrawHeader();
  uiDrawStatic();

  Serial.begin(115200);

  setupSensors();
  connectWiFi();
  setupTLS();
  connectMQTT();
}

// Boucle principale : maintien des connexions et publication toutes les 5s
void loop() {
  M5.update();

  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
    setupTLS();  // on re-applique la config TLS si reconnection Wi-Fi
  }

  if (!mqtt.connected()) {
    connectMQTT();
  }

  mqtt.loop();   // gestion interne du client MQTT

  unsigned long now = millis();
  if (now - lastPub >= 5000) {
    lastPub = now;

    // Lecture capteurs
    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();

    // Création du payload JSON
    char payload[160];
    snprintf(payload, sizeof(payload),
             "{\"temp_c\":%.2f,\"hum_pct\":%.2f,\"press_hpa\":%.2f}",
             t, h, p);

    // Publication MQTT (QoS 0, non-retained)
    mqtt.publish(TOPIC, payload, false);
  }
}
mqtts_no_se_subscriber.ino
Étape 2 : MQTTS avec Clés Firmware
// Subscriber sécurisé : M5Stack Core s'abonne à m5go/env via MQTT/TLS
// et affiche les valeurs reçues en temps réel.

#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

// -------- 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 ----------
// CA qui a signé le certificat du broker
static const char ca_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFsTCCA5mgAwIBAgIUCQ3b6jzGLXOCrozP4jU46+/w71wwDQYJKoZIhvcNAQEL
BQAwaDELMAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwI
VG91bG91c2UxDDAKBgNVBAoMA0xhYjENMAsGA1UECwwETVFUVDEVMBMGA1UEAwwM
TG9jYWxNUVRULUNBMB4XDTI1MTExMTE3MzM1NVoXDTM1MTEwOTE3MzM1NVowaDEL
MAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwIVG91bG91
c2UxDDAKBgNVBAoMA0xhYjENMAsGA1UECwwETVFUVDEVMBMGA1UEAwwMTG9jYWxN
UVRULUNBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmrAkrWR8biEZ
e+wY+wev8QW6f487FE/rWEqXnpFVB4IuLnv0Djvgjlwbrk1pQ6Wfbp0jVExSaIoF
7NvxuvmNLLJAGW2mzwYM6ZXl716/kTDyA7ucmgYcVgBY4YIk5QVq2aikb4srI6dr
oUkgxkfstbIQQo3OE9BrI96ubEnrRNPUeFQEkp0ZO47zEY4nD6U6OPwfDf9eOFC4
TLV1dUM8i5V4ReB1dhrkyh2ljdf/WruFD4BFfGcIbMpTlQRiXAjJXLoI9PejEC/k
r+I1lml0GJQaQB/fW4ZlTjag1FVMAeSCK3uVAaS4lXAB7Rz1Juxc4vVjxbkWtqEX
YvubUn75W4hcQWbE1NUqlzlbSRAfJnigns6XH1U9jMNqhVRkCLbAKHqoO1nhuQWo
zXXwQJQohfspbAqIzuNoEzT0QzWwB+BNb1QDfwYMZ79A4lyIvOq2NpDecL3u8J4N
CSD6d/dafkWdiM0Le+FMoMPeVMzs6vKttZsNu9+eoTyq2rjlA8hVN50hYEgskZU+
6yUdfN25lPA8ZIMlP/XltXUtJO6GkG4oGb8m7nBV5OHrt09TOJAhMAyxx3DUrwgJ
UEK65eeIgNjfFOi2Ul6LLzUs0PY99F736YuDfWSxQ+8RMlY58OaPwSd4eHcudlSb
vx0K+2N7GtSy2IYsJvtWrwNU9bIAoI0CAwEAAaNTMFEwHQYDVR0OBBYEFG72k3vn
Sxr5a0eGnVObzaAhOJdZMB8GA1UdIwQYMBaAFG72k3vnSxr5a0eGnVObzaAhOJdZ
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABuWRAzwRX0TMlnc
e/YY3OxgAzphDhWEeWujaL5ybqk3ttOkYomG+YGbMJxbY4z8CMJiKOXH6/J7ZEpJ
sDWZXd48Fd6WEgpkluB3HUb3XfCu5CPIbL0m4hg0kr2pCeffqP005iC8ONMFMtGA
xanV3R2teA7we9NCqSzk5MyG3uxeeOobmZ3Moeg9RG8gEQ1tEhB8OWNPqkq4OERn
3Gs0NN2gPP0Ep8a4El3yr+lP9sy2uUTBLZm0jDDlTX+bXubEzg2W3W1UOmVI1xtu
78VE3PoqWj8GO1PVl2kOw45yov//XA0tTEvUyguiCWhO4RaXJUGCC2roiYGZ6voP
9XNLFQkXi6y61KBsr/h7wmRYFYRkwBSKTQ7mKxdMV6R4dwFiIuHaXxyzLzwJKegZ
uY3SMGXTuSorndmXDg6U9zQDiOFOGWfQnH6UaP5VjpblANu4uzn41gjFiJN9Kt3q
KrOnC58qsI7g5PbfaKypsc15wOTlZhZl8U/UKnmuBT7T6X8gzdQecSR2fc1T35fe
+a5EtKyHnhpfc893SGmd+o3FD/U/IQUBZK/Ailo/pRO+muBRpbQcIkvLX5PC8B/j
r1uTV+/tXOHybbkYrT0p1i5Coc5+nRF1/utwPTm6jzAHlLJXe9Q/8lurQ1qjPvaG
KGuyhMbNWN2zYFWrpRiyuWjElDuF
-----END CERTIFICATE-----
)EOF";

// -------- CLIENT CERT (m5sub-display) ----------
// Certificat client du subscriber (identité m5sub-display)
static const char client_cert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIEpDCCAoygAwIBAgIUZ0trSk8RXBw+4/8NrTPscJPOiGIwDQYJKoZIhvcNAQEL
BQAwaDELMAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwI
VG91bG91c2UxDDAKBgNVBAoMA0xhYjENMAsGA1UECwwETVFUVDEVMBMGA1UEAwwM
TG9jYWxNUVRULUNBMB4XDTI1MTExMTE3NDUyOFoXDTI2MTExMTE3NDUyOFowbDEL
MAkGA1UEBhMCRlIxEjAQBgNVBAgMCU9jY2l0YW5pZTERMA8GA1UEBwwIVG91bG91
c2UxDDAKBgNVBAoMA0xhYjEQMA4GA1UECwwHRGV2aWNlczEWMBQGA1UEAwwNbTVz
dWItZGlzcGxheTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKRjJ/EO
LNc4bQ1e2G2OgoPxFw320BFVYGkGLzxR+rRxXPJl2AvUhFN2QYq9s/JgPJeBDBtV
zDjGSND6t5oPVd6F0/EYdp5ncvbu/onYoYphFHwKMBka6eTn2g3IDRG5X/ldbRGO
fsIMpW1QN+6TTB3vx39+2JStpxWlR/df6oJZmxuprrXlMJWEnXNg/A8DF0FqOmxU
COBCudIsMuNR4v+1idnSdVGOv9ud3UUJBOe9uGzeGU7eCTz6RuC/qSJzr0FEdFM4
f1FXZ/+MV44MArGZibUJO0J1IIu1lOY3L+IZS1gjOBlvj6SYJaUpoxpvzBxcZgm4
snqRjBH/4DNZrykCAwEAAaNCMEAwHQYDVR0OBBYEFIDubbgpQD3vIWkiVG/em5i2
H0YoMB8GA1UdIwQYMBaAFG72k3vnSxr5a0eGnVObzaAhOJdZMA0GCSqGSIb3DQEB
CwUAA4ICAQAql9iyTrkVrtjQTyY3A2VMowMoNR9bd+YBNrR98StlddHMdCBmHF+t
gznt5vU0cG+K0avqEu8W/VAfYIP8jEvZtYMAeNRDHZfHu8ZAR8NTqeb4WFCl8xbG
3ThnBBpXf6ZYDpg3ksszHKNg+lh/zmmtrdPA8oxytqYf4/uLCSSrcXFlFIFZHhUx
Zqd4h/UMcklcmg9myH+fLssjTRGHunF85+GPjZrrLLL9bXnrtSVu7SPJe5mwhM+q
wvzD+MS24I/yo2HARO7my8Xi5XKukhLwwaAA4othkiQ5JD4bLz6VHt2ZXzAqPioV
PGrjVkAg5ydDuOQNlnz5S9l+iqEN5dmAS5oj5M1mCPzvws0Fa+pTZzVmeJWDIOEq
aUbCfygdBBt3nrL53pi/nBZWEzcFOwA63U2cRjHM6IGkr4ardNMpuY0nEsON6jmo
xUML1YzDGJVuwT4PXsN/JH7eFm8Lqkt4btb9hLi4ZpstCFPfCq7EAqWRnPDwsLJM
h/nJr5nIYVK7HJo53nFIpyXS5U9Xui5fL4G10Fd4GcYW64qwfD5VNdyd0qbBGoZH
hIwQytkRWVXvf0r+fevkZYymKicra33i0Cj3VJ6ecy8HeRXm+ueAc7X8NatfFJAL
4RwV97MjLKSnrMLa7wdA+kcAzcyu+7E5AVlB1LGowbk+xVkww9gAxg==
-----END CERTIFICATE-----
)EOF";

// -------- CLIENT KEY (m5sub-display) ----------
// Clé privée correspondant au certificat client du subscriber
static const char client_key[] PROGMEM = R"EOF(
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCkYyfxDizXOG0N
XthtjoKD8RcN9tARVWBpBi88Ufq0cVzyZdgL1IRTdkGKvbPyYDyXgQwbVcw4xkjQ
+reaD1XehdPxGHaeZ3L27v6J2KGKYRR8CjAZGunk59oNyA0RuV/5XW0Rjn7CDKVt
UDfuk0wd78d/ftiUracVpUf3X+qCWZsbqa615TCVhJ1zYPwPAxdBajpsVAjgQrnS
LDLjUeL/tYnZ0nVRjr/bnd1FCQTnvbhs3hlO3gk8+kbgv6kic69BRHRTOH9RV2f/
jFeODAKxmYm1CTtCdSCLtZTmNy/iGUtYIzgZb4+kmCWlKaMab8wcXGYJuLJ6kYwR
/+AzWa8pAgMBAAECggEAThvfSqbVn53sE0JRzBy+3zqbHcN8sf/DQ6PwpdS+Lyb+
Zx8FPPnJtm6mTeo9qpkXsVVcD1EBLKjqyIGY49cNkiI8xn49Pp8g/TwqLg2FFF90
gEvG9ubW5DFWZt37d8SBWdeOj50JleK0Z/CJ4w6SSFc6j9YRyvOIRLy4KU6l+zEd
hBArFDCO4TtZjw758EWUpUmInhBFSDimGPt0JlivjvO/0gHUaBsQvwYI7s3zJt3N
zrfyGLYZ8PvEAq+7+h6pt8ugTMHX64z1K5h9NAIs0QFMSGHD87tzTavYvOSgVNsY
6kODA7lantkLSY5QkE9dwibozTRG91gLkl+HQmPRkQKBgQDSQJpRdF/OhTrokQL2
MkeUoRMFVhZ9Gat5z4SaVHvARey+YxVuOMwR6R5hJuRMMfPkGP/w6FA5dHjzWeXD
xr+MnjKfQ2tXLoLsRtLVDKYTXXaaN9tQkY/+lo9ou7PpzXPDgmMpuKISUtt/lg4w
Dt1wUT6pG34CDoFlLBFFS96GQwKBgQDIJ8v+FmFqd5cf94HTOjmUOntUuW9nNHjx
7lHb0sR2HzQaw9ZUD8mcZy33ZFwEEq1Ytppxh1ZhdzxDIWgpemU3M7bXlzL1lQ+a
M0TMfWQ32BTiMKzHjxAFX1Z1faG2y0E4eawV0O7pvEAI2nJ5qohHdXLRHsoo2HTA
k7J67OocIwKBgQCOGtnyaeyDQisxmylcS1l0DHY1qhzjCuGKIAco5CcMKna7q26F
o9/RzzUYRtgQ48ZLCdaa9fmbC6zgFYelDgTTPu1KFaBMOYSFu8yt8LGi7w3FaDFU
QD4JvatKB2uvf4xZvRvWzBLGvbfbgQkv1Cw4yMDIPWuqajFstx8pLgFFzwKBgCXA
Y9hrzjnvjoCIBWOawstzcFmdlCaKHhm7kpL8oPOKlSBLObynMaafS2sy8awO/cUS
w/SPyzoc7C/ZODVCkZ6k0WK+cO0jDUtPSjWrnOBvkBjNh3koQaRRxBPq+zpoAcgu
IsgGnVlWmVlSIm9SO9wGif5paUXk9bhw4yQOVWWzAoGBAJ7+lEtDDcV1lEFW98Oi
Z91xG4JfaXj7uRCEq7cSAgjF1KqKoqzi3rfNGyqLD0C19mpb9nb4HmIsDzf4V4tH
G4h3jb+0pIgSW1gcPov5SiJw5cMqqRhf1f/6lH34S9t+BgjFTSXyAVz17e3zzMOP
AjIBJJ7vNCi5FWU+a4w0gtm5
-----END PRIVATE KEY-----
)EOF";

// ------------ MQTT CLIENT ------------
// Client TLS pour MQTT
WiFiClientSecure tlsClient;
PubSubClient mqtt(tlsClient);

// ------------ UI COLORS ------------
// Palette couleur identique au publisher, adaptée à l'UI du subscriber
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

// ------------ UI HELPERS ------------
// En-tête de l'écran
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 : cartes Wi-Fi, MQTTS, texte explicatif
void uiDrawStatic() {
  auto& d = M5.Display;

  // WiFi card
  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");

  // MQTT card
  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");

  // Data card
  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 sur l'écran
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");
}

// Indique qu'un nouveau message a été reçu
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);
}

// Configuration TLS (CA, cert client, clé privée)
void setupTLS() {
  tlsClient.setCACert(ca_cert);
  tlsClient.setCertificate(client_cert);
  tlsClient.setPrivateKey(client_key);
}

// ------------ MQTT CALLBACK ------------
// Callback appelée à chaque message MQTT 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 très simple 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 de l'affichage
  uiSetSensorValues(t, h, p);
  uiSetLastUpdate();

  // Debug série
  Serial.print("Msg [");
  Serial.print(topic);
  Serial.print("]: ");
  Serial.println(msg);
}

// ------------ MQTT ------------
// Connexion au broker, abonnement, et boucle de reconnexion
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);   // abonnement au topic m5go/env
    } else {
      uiSetMQTTStatus("Err:" + String(mqtt.state()), false);
      delay(1500);
    }
  }
}

// ------------ SETUP / LOOP ------------
// Initialisation générale
void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Display.setRotation(1);
  uiDrawHeader();
  uiDrawStatic();

  Serial.begin(115200);

  connectWiFi();
  setupTLS();
  connectMQTT();
}

// Boucle principale : maintien du Wi-Fi, de MQTT et traitement des messages
void loop() {
  M5.update();

  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
    setupTLS();
  }

  if (!mqtt.connected()) {
    connectMQTT();
  }

  mqtt.loop();
}
Scan_SE.ino
Auxiliaire : Scan I2C & Vérif Verrou
// 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-TNGTLS 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();
}
Write_SE.ino
Auxiliaire : Provisionne Slot 8 avec Clé Privée
// 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();
}
mqtts_with_se_publisher.ino
Étape 3 : Publisher Sécurisé (Clé Matérielle)
// 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);
  }
}
mqtts_with_se_subscriber.ino
Étape 3 : Subscriber Sécurisé (Clé Matérielle)
// 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 <SparkFun_ATECCX08a_Arduino_Library.h>

// 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();
}
mosquitto_mqtts.conf
Configuration Broker
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
Page Finale

Mentions Légales & Licence

Examinez les licences des bibliothèques tierces et les conditions d'utilisation.