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.
Affichage de tous les fichiers
mqtt_plain_publisher.ino
Étape 1 : Sketch Publisher (Sans TLS)
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
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
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
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
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)
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)
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
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.