isno.fr

OpenXtraflame

Reverse engineering complet du module Wi-Fi Extraflame Black Label T009_3 pour remplacer sa dépendance au cloud italien Omnyvore par un contrôle local via MQTT.

Code : github.com/Shad107/OpenXtraflame

OpenXtraflame
Module Wi-Fi Extraflame Black Label T009_3, vue extérieure avec cover fermé. Marquage "Compatible only with APP Total Control 2.0" visible en bas.
Sommaire
  1. Contexte
  2. Glossaire des commandes
  3. Extraction
  4. Analyse
  5. Firmware
  6. Flash
  7. La suite

Contexte

Le poêle à granulés Extraflame Teodora Evo se pilote à distance via l’app mobile officielle Total Control 2 (=publiée par Extraflame sur iOS et Android). Sous le capot, un module Wi-Fi Black Label T009_3 à base d’ESP32 qui parle à un backend cloud propriétaire hébergé par Omnyvore srl. Le module se vend 289 € TTC chez Extraflame et n’a jamais été reversé publiquement.

Deux problèmes justifient l’effort :

Les projets communautaires existants (philibertc/micronova_controller, Jorre05/micronova) contournent le problème en branchant un ESP externe sur le bus série du poêle. Élégant, mais on garde le module d’origine à côté qui continue de parler au cloud. Objectif : reflasher le module d’origine pour tout garder in-situ, zéro modification visible.

Glossaire des commandes

Un récap avant de rentrer dans le vif. Ce que fait chaque outil qu’on invoque plus bas dans l’article, avec les vrais arguments utilisés.

Identifier le chip :

python -m esptool --port COM3 --before no-reset --after no-reset chip-id

Retourne ESP32-D0WDQ6 rev v1.0 chez nous et confirme que le câblage UART fonctionne. --before no-reset / --after no-reset empêchent esptool de tenter un reset DTR/RTS qui sortirait le module de son mode download manuel.

Lire les protections du chip (=avant de dumper) :

python -m espefuse --port COM3 summary

Affiche les eFuses : FLASH_CRYPT_CNT (=chiffrement flash), ABS_DONE_0/1 (=Secure Boot), UART_DOWNLOAD_DIS, JTAG_DISABLE. Étape critique : si la flash était chiffrée, le dump serait du bruit binaire inutilisable.

Dump complet des 4 Mo :

python -m esptool --port COM3 --baud 460800 --before no-reset --after no-reset read-flash 0 0x400000 dump.bin

Lit les 4 Mo de flash en une passe à 460 800 bauds (=~100 s). C’est le firmware d’origine, à mettre au coffre avant tout.

Restauration à partir du dump :

python -m esptool --port COM3 --baud 460800 write-flash 0 dump.bin

Corde de rappel : à tout moment ça remet le module en état d’usine.

Erase complet avant premier flash custom :

python -m esptool --port COM3 --baud 460800 erase-flash

Efface toute la flash (=partition table, apps, NVS, tout). À faire une fois avant de flasher le firmware custom pour partir d’un état propre.

Configurer la cible ESP-IDF :

idf.py set-target esp32

Configure le projet pour compiler pour la puce esp32 classique (=vs esp32s3, esp32c3, etc.).

Build M5Stack ATOM Lite (=validation devkit) :

idf.py -DOPENXFLAME_TARGET=external build

Build module Extraflame Black Label (=production) :

idf.py -DOPENXFLAME_TARGET=blacklabel build

Flasher + attaquer le moniteur série :

idf.py -p COM3 -b 460800 flash monitor

Écrit le binaire sur le chip puis affiche les logs live. Sur Linux remplacer COM3 par /dev/ttyUSB0.

Moniteur série seul :

screen /dev/ttyUSB0 115200            # Linux
python -m serial.tools.miniterm COM3 115200   # Windows

Sous Linux, quitter avec Ctrl-A K. Sous Windows PuTTY en mode Serial fait aussi le job.

Extraction

Identification hardware

Démontage du module. PCB double-face vert avec sérigraphie COD.T009_3. Composants identifiés à la loupe.

ESP32-WROOM-32 vu de dessus, la puce principale du module

Au centre : le module ESP32-WROOM-32 (=Espressif dual-core Xtensa LX6). C’est le cerveau - c’est lui qu’on va reprogrammer. Autour :

Les 4 LEDs blanches CMS alignées et le bouton reset tactile chromé

Aucun connecteur JTAG ni pad de debug identifié. Les pads UART et IO0 (=nécessaire pour passer le chip en mode boot ROM UART) ne sont pas exposés en surface. Il va falloir souder directement sur les pattes de l’ESP32.

Branchement pas à pas (=débutants bienvenus)

Bonne nouvelle : pas de soudure nécessaire. Le module a un connecteur série 3-pin accessible (=celui qui le relie au poêle) et un pin exposé qui suffit pour extraire tout le firmware. Des fils dupont femelles maintenus contre les broches font le travail. Si tu débutes, teste d’abord sur un M5Stack ATOM Lite (=même famille ESP32, boîtier fini, ~12 €) : même protocole, moins de risques.

1. Identifier les DEUX connecteurs du module

Le module a deux connecteurs distincts qu’il ne faut pas confondre :

CN2, le connecteur poêle (=4-pin) :

Connecteur CN2 sur le PCB avec les 4 fils du câble poêle : vert, marron, blanc, jaune

Vue rapprochée du connecteur seul, sans câble branché, pour bien voir le détrompeur plastique et les 4 pins alignés :

Macro sur CN2 débranché : boîtier plastique noir vertical avec détrompeur en D, 4 pins dorés bien visibles côte à côte

L’autre bout du câble se branche sur la prise TA du poêle (=Termostato Ambiente) :

Prise TA côté poêle où les 4 fils rentrent

Zoom sur les 4 fils qui sortent du câble juste avant d’entrer dans la prise TA, code couleur bien identifiable même après quelques années d’usage :

Câble poêle en gros plan : gaine grise ouverte, 4 fils intérieurs vert (=GND), marron, blanc, jaune (=alim), avant l'entrée dans la prise TA du poêle

CN5, le header debug 3-pin (=celui qu’on utilise pour dumper) :

Header CN5 3-pin à côté de l'ESP32, celui utilisé pour brancher le CH340G

Vu de plus près, on distingue bien la sérigraphie CN5 juste à côté de l’ESP32-WROOM :

Macro sur le header CN5 3-pin - la sérigraphie CN5 est visible en bas, l'ESP32-WROOM juste à gauche

Et en super macro sur les 3 pins eux-mêmes (=triangle blanc à gauche = repère pin 1, la broche GND) :

Zoom ultra sur les 3 pins de CN5 - le triangle sérigraphié à gauche marque le pin 1 GND

Broches de CN5 :

BrocheRôle moduleNote
1GNDpin marqué par le triangle sérigraphié
2TXenvoie vers RXD du CH340G
3RXreçoit du TXD du CH340G

Bonne surprise du reverse : les trois signaux nécessaires au dump (GND + RX + TX) sont déjà sur CN5. Pas besoin de souder quoi que ce soit sur le PCB.

2. Alimenter le module (=avec son propre bloc, pas via le CH340G)

Le module a besoin de deux choses en même temps pour qu’on puisse le dumper :

  1. De l’alimentation - fournie par son bloc secteur d’origine (=celui qui l’alimente habituellement quand il est branché sur le poêle). Le module a un jack DC dédié à ça. On le laisse branché tout le long du dump.
  2. Une liaison série - fournie par le CH340G branché au PC en USB.

Pourquoi ne pas alimenter directement par le CH340G ? Parce que le CH340G ne peut sortir que quelques dizaines de mA sur son rail 3.3V, alors que le module Extraflame consomme plus (=régulation interne, LEDs, résistances). Résultat concret : si tu enlèves l’alim d’origine et que tu tentes d’alimenter le module via la broche 3.3V du CH340G, le module ne démarre pas ou démarre de façon instable. Garde le bloc d’alim d’origine branché.

Schéma mental (=à quoi ça ressemble sur le bureau) :

   secteur 230V


  ┌─────────┐         ┌──────────────┐
  │ bloc    │ jack DC │              │  connecteur série 3-pin
  │ d'alim  │────────►│    MODULE    │◄────────────── dupont ──────┐
  │ origine │         │  Extraflame  │                              │
  └─────────┘         │              │                              │
                      └──────────────┘                              │
                                                          ┌────────▼────────┐
                                                          │     CH340G      │
                                                          │  (mode 3.3V)    │
                                                          └────────┬────────┘
                                                                   │ USB

                                                                  PC

3. Câbler le CH340G en 3.3V (=impératif)

⚠️ Le CH340G doit être en mode 3.3V, sans quoi rien ne fonctionne correctement. Sur la plupart des modules, il y a un jumper à repositionner (=petit cavalier plastique entre deux picots), ou un cavalier à souder sur les modèles compacts. Certains modèles ont un mini-switch. C’est la première chose à vérifier avant tout branchement.

(À faire : ajouter ici une photo du jumper 3.3V sur mon CH340G, à prendre au prochain démontage.)

Ce qui se passe quand on se plante de niveau logique :

Trois fils dupont femelles pour l’UART (=+ un 4ᵉ pour IO0↔GND détaillé à l’étape 4) :

CH340GBroche CN5Fil sur ma photo (=à adapter selon ton kit)
GND1 (=GND module)rouge
RXD2 (=TX module)jaune
TXD3 (=RX module)bleu

⚠️ Croisement UART classique : le TXD du CH340G va sur le RX du module, le RXD du CH340G va sur le TX du module. C’est le piège numéro 1 en série TTL - retiens juste que TX parle à RX et vice-versa.

Enfoncer les dupont femelles directement dans le connecteur du module. Ça tient tout seul.

Trois dupont femelles maintenus sur CN5 pendant le dump : rouge sur pin 1 GND, jaune sur pin 2 TX module, bleu sur pin 3 RX module ; ESP32-WROOM-32 label lisible juste à côté

Vue d’ensemble du setup avec les deux sources branchées en parallèle : jack DC du bloc d’alim d’origine (=câble noir qui entre dans le module par le bas), et CH340G en USB-C (=à droite) qui apporte la liaison série via les 3 dupont rouge/jaune/bleu (=mêmes couleurs des deux côtés, pas de croisement visuel à mémoriser).

Setup complet en cours de dump : module Extraflame à gauche avec jack d'alim branché, CH340G à droite en USB-C, 3 dupont rouge (GND) / jaune (TX module = RXD CH340G) / bleu (RX module = TXD CH340G) qui relient les deux ; LED verte du module allumée = alim OK

Premier test avant de sortir esptool : ouvrir un moniteur série sur le CH340G (screen /dev/ttyUSB0 115200 sous Linux, PuTTY côté Windows en mode Serial COM3 à 115200 8N1), puis débrancher/rebrancher l’alim du module en gardant le fil IO0↔GND de l’étape 4 maintenu.

Ce que j’ai vraiment vu défiler dans le moniteur au premier essai concluant :

rst:0x1 (POWERON_RESET),boot:0x3 (DOWNLOAD_BOOT(UART0/UART1/SDIO_REI_REO_V2))
waiting for download

Deux lignes seulement, puis silence - c’est le ROM bootloader ESP32 qui dit « je suis en mode download, envoie tes commandes ». À un autre reset j’ai aussi vu le code équivalent boot:0xf (uart_boot(uart0)), l’ESP32 varie selon la config OTP et la révision du chip ; les deux signifient la même chose : mode download prêt.

Symptômes rencontrés quand le câblage n’était pas bon :

Une fois les deux lignes boot:0x3 + waiting for download visibles, quitter le moniteur série (=Ctrl-A K dans screen, fermer PuTTY côté Windows) pour libérer le port COM3 / /dev/ttyUSB0, puis lancer esptool (=étape suivante).

4. Passer le module en mode « download » (=fil IO0↔GND obligatoire)

Pour flasher ou dumper un ESP32, il faut qu’il démarre en mode download (=au lieu du mode normal qui lance le firmware). Sur la plupart des cartes ESP32 grand public (=NodeMCU, WROOM devkits, etc.), il y a un bouton BOOT à presser pendant le reset : ce bouton relie une broche spéciale de l’ESP32 (=GPIO0) à la masse GND, ce qui dit au chip « pas de firmware, entre en mode download ».

Sur le module Extraflame, aucun bouton BOOT n’est câblé et GPIO0 n’est pas exposé sur un header. Il faut donc poser un fil dupont supplémentaire directement sur le pad IO0 de l’ESP32-WROOM et le tenir en contact avec GND pendant le reset. Sans ça, le module rebooke en mode normal (=boot:0x13 SPI_FAST_FLASH_BOOT) et le firmware Extraflame se lance : esptool ne pourra pas dialoguer.

Setup complet du mode download : fil dupont jaune posé sur le pad IO0 de l'ESP32-WROOM en haut à droite, sa broche femelle libre à relier à GND pendant le reset ; les 3 autres fils UART (rouge/jaune/bleu) restent sur CN5 en bas, jack DC alim d'origine à gauche, CH340G en USB-C à droite

Zoom sur la manip précise : la broche dupont mâle du fil jaune se pose contre le pad IO0 de l’ESP32-WROOM32 (=au ras du côté gauche de la puce), et l’autre extrémité du même fil est enfoncée dans le pin 1 (=GND) du connecteur CN5/CN6 en bas à gauche. C’est un contact temporaire, on peut le tenir à la main pendant le hard reset.

Vue macro sur l'ESP32-WROOM32 : broche dupont mâle du fil jaune posée directement contre le pad IO0 (=côté gauche de la puce), fil qui redescend vers CN6 pin 1 GND en bas à gauche, câblage UART CN5 rouge/jaune/bleu visible en dessous

Concrètement, pour rentrer en mode download avec ce module :

  1. Le CH340G est branché en 3.3V à CN5 pour les 3 signaux UART (=étape 3 faite).
  2. Ajouter un 4ᵉ fil dupont : une extrémité posée / maintenue sur le pad IO0 de l’ESP32-WROOM (=au bord de la puce), l’autre reliée à GND (=le rail commun avec CN5 pin 1, ou n’importe quel autre point de masse du PCB).
  3. Le moniteur série est fermé (=port COM3 / /dev/ttyUSB0 libre).
  4. En gardant IO0 maintenu à GND, débrancher/rebrancher le bloc d’alim d’origine du module (=hard reset).
  5. Vérifier au moniteur série que la ligne boot:0x3 (DOWNLOAD_BOOT…) (=ou l’équivalent boot:0xf uart_boot) puis waiting for download s’affichent. Une fois cette trace vue, tu peux relâcher le fil IO0 - le chip reste en mode download jusqu’à la prochaine coupure d’alim.
  6. Le module est prêt à recevoir esptool.

Symptôme si tu oublies le fil IO0 : tu vois boot:0x13 SPI_FAST_FLASH_BOOT puis les logs Extraflame Black Label T009_3 firmware boot défilent. Le mode download n’a pas été armé. Recouper l’alim en maintenant IO0 à GND cette fois.

5. Le boot log complet du firmware d’origine

Si tu oublies le fil IO0 pendant le reset (=ou juste par curiosité), le module boote normalement et le firmware Extraflame défile dans le moniteur série. Ce log est précieux parce qu’il révèle la version ESP-IDF utilisée, la table de partitions et les noms de tâches applicatives, tout ça sans avoir à ouvrir Ghidra.

Boot mode normal (=boot:0x13 SPI_FAST_FLASH_BOOT, firmware Extraflame en train de démarrer) :

$ screen /dev/ttyUSB0 115200
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0018,len:4
load:0x3fff001c,len:5992
load:0x40078000,len:14352
load:0x40080400,len:3648
entry 0x400806a4
I (30) boot: ESP-IDF v4.3-dirty 2nd stage bootloader
I (30) boot: compile time 09:14:41
I (30) boot: Enabling RNG early entropy source...
I (35) boot: SPI Speed      : 40MHz
I (39) boot: SPI Mode       : DIO
I (43) boot: SPI Flash Size : 4MB
I (47) boot: Partition Table:
I (51) boot: ## Label            Usage          Type ST Offset   Length
I (58) boot:  0 nvs              WiFi data        01 02 00009000 00005000
I (66) boot:  1 otadata          OTA data         01 00 0000e000 00002000
I (73) boot:  2 phy_init         RF data          01 01 00010000 00001000
I (81) boot:  3 factory          factory app      00 00 00020000 00180000
I (88) boot:  4 ota_0            OTA app          00 10 001a0000 00180000
I (96) boot:  5 ota_1            OTA app          00 11 00320000 00080000
I (103) boot:  6 nvs_key          NVS keys         01 04 003a0000 00001000
I (111) boot: End of partition table
I (115) esp_image: segment 0: paddr=0001a020 vaddr=3f400020 size=1c1e8h
I (2100) cpu_start: Starting scheduler on APP CPU.
I (2110) MAIN: Extraflame Black Label T009_3 firmware boot
I (2110) MAIN: Loading credentials from NVS...
I (2120) MAIN: secure_code loaded, stove_model loaded
I (2130) WIFI: STA connecting to SSID '<configured>'...
I (2140) MICRONOVA: UART_INIT: SERIAL_UART, 38400 bps, 8N1

Ce sont des logs ESP-IDF natifs (=format « I (millis) TAG: message »), pas des logs ESPHome. L’application Extraflame utilise directement l’API de logging Espressif.

Cette sortie est précieuse :

  1. Elle confirme le boot mode (boot:0x13 = SPI flash normal, boot:0xf = download UART)
  2. Elle révèle la version ESP-IDF (v4.3-dirty = base v4.3 + patches propriétaires)
  3. Elle liste la table de partitions en clair (=évite d’avoir à la reconstruire depuis le dump)
  4. Elle affiche les logs applicatifs (=noms de tâches, événements) qui aident à mapper le firmware

Reset accidentel avant flash (=risque à connaître) : si tu es en boot mode et que tu presses reset, l’ESP32 rebascule côté boot ROM UART après quelques secondes (=waiting for download réapparaît). Tant que le CH340G reste branché correctement, tu peux relancer esptool.py autant de fois que tu veux.

Extraction du firmware

Câblage en place, mode boot UART confirmé (=trace boot:0x3 DOWNLOAD_BOOT puis waiting for download sur le moniteur série), on peut passer à esptool. Session réelle sous Windows PowerShell, l’utilisateur a été fait avec la version 5.3 de esptool installée via pip :

Zoom sur l'ESP32-WROOM avec les fils de contact - pas de soudure, tout se maintient par appui

Étape A - identifier le chip (=confirme que la connexion série marche) :

Ma vraie session, warnings compris (=je m’étais habitué à l’ancienne syntaxe chip_id avec no_reset, esptool v5 me le rappelle poliment) :

PS> python -m esptool --port COM3 --baud 115200 --before no_reset --after no_reset chip_id

Warning: Deprecated: Choice 'no_reset' for option '--before' is deprecated. Use 'no-reset' instead.
Warning: Deprecated: Choice 'no_reset' for option '--after' is deprecated. Use 'no-reset' instead.
Warning: Deprecated: Command 'chip_id' is deprecated. Use 'chip-id' instead.
esptool v5.3.0
Note: Pre-connection option "no-reset" was selected. Connection may fail
if the chip is not in bootloader or flasher stub mode.
Connected to ESP32 on COM3:
Chip type:          ESP32-D0WDQ6 (revision v1.0)
Features:           Wi-Fi, BT, Dual Core + LP Core, 240MHz,
                    Vref calibration in eFuse, Coding Scheme None
Crystal frequency:  40MHz
MAC:                4c:eb:d6:xx:xx:xx

Stub flasher running.

Warning: ESP32 has no chip ID. Reading MAC address instead.
MAC:                4c:eb:d6:xx:xx:xx

Staying in bootloader.
PS>

Trois choses à noter dans cette sortie :

Les options --before no-reset --after no-reset sont critiques dans notre setup : esptool par défaut essaie de resetter le chip via DTR/RTS, mais notre module Extraflame est déjà en mode download naturel - s’il tente un reset, on sort du mode et il faut recouper l’alim.

Chip identifié : ESP32-D0WDQ6 revision v1.0, MAC 4c:eb:d6:xx:xx:xx.

Vérifier que le firmware n’est pas chiffré (=étape critique). Avant de faire un dump, on veut savoir si le contenu de la flash est en clair ou chiffré via Flash Encryption (=une protection Espressif où chaque bloc de flash est chiffré avec une clé stockée dans les eFuses, illisible sans cette clé). Si c’est chiffré, un dump ne servira à rien - on obtiendrait du bruit binaire.

La commande espefuse summary lit les eFuses du chip et affiche l’état de toutes les protections :

PS> python -m espefuse --port COM3 summary

espefuse v5.3.0
Connected to ESP32 on COM3:
Chip type:          ESP32-D0WDQ6 (revision v1.0)

Flash fuses:
FLASH_CRYPT_CNT (BLOCK0)     Flash encryption is enabled if this
                             field has an odd number of bits set  = 0 R/W (0b0000000)
FLASH_CRYPT_CONFIG (BLOCK0)  Flash encryption config              = 0 R/W (0x0)

Security fuses:
UART_DOWNLOAD_DIS (BLOCK0)   Disable UART download mode           = False R/W (0b0)
ABS_DONE_0 (BLOCK0)          Secure boot V1 is enabled            = False R/W (0b0)
ABS_DONE_1 (BLOCK0)          Secure boot V2 is enabled            = False R/W (0b0)
JTAG_DISABLE (BLOCK0)        Disable JTAG                         = False R/W (0b0)

Tout ce qu’on veut voir dans ce rapport :

Extraflame n’a activé aucune protection sur ce module - probablement parce que le secure_code + stove_model dans la NVS suffisent à leur logique d’appairage cloud, et que sécuriser la flash aurait compliqué leurs OTA. C’est un cadeau pour nous : le dump va donner un binaire directement analysable.

Si un des flags ci-dessus avait été True, l’histoire aurait été très différente (=nécessité d’une attaque hardware type ChipWhisperer à ~2 000 €, ou abandon).

Étape B - dump complet des 4 Mo à 460 800 bauds :

PS> python -m esptool --port COM3 --baud 460800 --before no-reset --after no-reset read-flash 0 0x400000 ~\Desktop\extraflame_dump.bin

esptool v5.3.0
Connected to ESP32 on COM3:
Chip type:          ESP32-D0WDQ6 (revision v1.0)
MAC:                4c:eb:d6:xx:xx:xx

Stub flasher running.
Changing baud rate to 460800...
Changed.

Configuring flash size...
Read 4194304 bytes from 0x00000000 in 100.1 seconds (335.3 kbit/s)
to '~\Desktop\extraflame_dump.bin'.

Staying in bootloader.

4 194 304 octets (=4 Mo pile) extraits en 100,1 secondes à 335 kbit/s. Le Staying in bootloader à la fin confirme que le chip est resté en mode download (=grâce à --after no-reset), on pourrait relancer une autre commande sans avoir à recouper l’alim.

Backup immédiat avant toute manipulation : copie du fichier extraflame_dump.bin sur le Synology + une clé USB. Le principe est simple : jamais toucher au dump original, on travaille toujours sur des copies.

Note pour les utilisateurs Linux/macOS : les commandes sont identiques, sauf le nom du port (/dev/ttyUSB0 au lieu de COM3) et le chemin de destination (~/Desktop/extraflame_dump.bin).

Analyse

Analyse des partitions

Layout classique ESP-IDF avec deux partitions OTA :

# Name,   Type, SubType,  Offset,   Size
nvs,      data, nvs,      0x9000,   0x5000
otadata,  data, ota,      0xe000,   0x2000
app0,     app,  ota_0,    0x10000,  0x180000
app1,     app,  ota_1,    0x190000, 0x180000
spiffs,   data, spiffs,   0x310000, 0xE0000
nvs_key,  data, nvs_keys, 0x3F0000, 0x1000

Extraction de chaînes

Le vieux réflexe avant Ghidra : strings sur ota0. En 60 lignes clés :

Backend cloud Omnyvore

Les strings révèlent aussi le backend qui parle à l’app Total Control 2 : mqtt.extraflame.it:8883 (=broker MQTT TLS), opéré par la société italienne Omnyvore srl. Chaque module Black Label s’authentifie auprès de ce broker avec deux valeurs stockées dans la partition NVS secret1 :

Le firmware embarque aussi un certificat CA X.509 utilisé pour valider mqtt.extraflame.it en TLS. Analyse du cert :

Sujet    : /C=IT/ST=Vicenza/L=Vicenza/O=Omnyvore/OU=Tech Ops/CN=omnyvore.com
Emetteur : idem (=self-signed)
Valide   : 2017 -> 2027

C’est un CA self-signed dont la clé privée reste chez Omnyvore. Ce détail ferme la porte à la piste “fake cloud MQTT local sans toucher au module” :

Verdict : impossible de faire un fake cloud propre sans reflasher le module. Autrement dit, si Omnyvore ferme demain, aucune bidouille côté DNS ne sauvera un module resté en firmware d’origine. La seule voie durable, c’est le firmware custom (=celui de ce projet), qui remplace la couche MQTT et sort de la boucle Omnyvore une bonne fois pour toutes.

L’enjeu de survie est réel : si Omnyvore ferme demain (=petite structure, pas de garantie de service), tous les Extraflame vendus depuis 2018 perdent leur pilotage à distance. Un firmware local, c’est aussi une assurance long terme pour le parc installé.

Décompilation Ghidra

Ghidra 12.x supporte nativement Xtensa (=le processeur des ESP32 classiques), plus besoin du plugin communautaire d’autrefois. Astuce pour retrouver les symboles ESP-IDF : les fonctions logguent leur propre nom via ESP_LOGx(). Si on trouve la chaîne “uart_set_pin” référencée par une fonction, cette fonction EST uart_set_pin. C’est l’anchoring par log strings.

Fonctions clés localisées :

gpio_set_level      @ 0x400d98f4
uart_set_pin        @ 0x400d8740
uart_param_config   @ 0x400d88f4
uart_driver_install @ 0x400d90fc
uart_write_bytes    @ 0x400d8be8
uart_read_bytes     @ 0x400d8c64

Émulation QEMU + GDB

Plutôt que de risquer un brick sur le hardware réel, booter le firmware sous QEMU-Xtensa et capturer les appels bas niveau via GDB remote.

(gdb) break *0x400d8740        # uart_set_pin
(gdb) continue

Breakpoint hit at 0x400d8740
  a2 (uart_num) = 0x1
  a3 (tx_pin)   = 0x17    ; = GPIO 23
  a4 (rx_pin)   = 0x5     ; = GPIO 5

Résultat capturé sans ouvrir le module :

Mapping des 4 LEDs

En croisant les logs runtime avec la sémantique des LEDs Extraflame (=le manuel utilisateur cite POWER, BLE, WI-FI, SERVER), et en confirmant chaque bit dans les breakpoints sur gpio_set_level(), on obtient le mapping suivant :

Le module a 4 LEDs blanches CMS + 1 bouton reset tactile, rien d’autre côté façade. Le mapping :

LED sérigraphieGPIO ESP32Rôle d’origine
POWERGPIO 25allumée tant que le module est sous tension
BLEGPIO 26Bluetooth (=provisioning via app Total Control)
WI-FIGPIO 32STA connectée à la box
SERVERGPIO 33connexion cloud Omnyvore MQTT active

Actives à l’état haut (=gpio_set_level(pin, 1) allume la LED, confirmé par les patterns observés en QEMU au boot).

Le 5e GPIO qui s’agite en QEMU (=GPIO 22) correspond au bouton reset, pas à une LED cachée : sa direction est un input avec pull-up géré par le firmware, ce qui ressemble à une sortie du point de vue du breakpoint mais n’a rien à voir avec une source lumineuse.

Le firmware custom réutilise les mêmes 4 LEDs avec leur sémantique originale (=POWER toujours allumée, WIFI pendant STA up, SERVER quand MQTT vers Mosquitto local est up, BLE en mode SoftAP provisioning). Un utilisateur qui reflashe le module reste devant un jeu de LEDs qui a du sens, plutôt qu’un pattern maison illisible.

Un premier round de fumées : QEMU sans poêle en face

Avant d’avoir accès au vrai bus, j’ai breakpointé les 2 fonctions dans QEMU et laissé tourner 60 secondes :

(gdb) shell grep -c "READ hit" trace.log
706
(gdb) shell grep -c "WRITE hit" trace.log
0

706 lectures pour 0 écritures. Sur le coup j’ai cru que le module écoutait passivement (=slave, attend que le poêle lui parle). C’était une lecture partielle : sans poêle en face côté QEMU, les uart_read retournent tous vides, ce qui ressemble à de la « veille passive » alors qu’en vrai le module a simplement rien à décoder. Le reverse complet + le terrain contredisent cette interprétation : le module est bien maître RWMS, il polle activement, exactement comme les implémentations communautaires (philibertc/micronova_controller, Jorre05/micronova, agua-iot). Détail dans la section protocole ci-dessous.

Le protocole Micronova côté registres

Micronova est une carte-mère générique utilisée par plusieurs marques de poêles à granulés italiens (=Extraflame, EdilKamin, LAMINOX, Freepoint, Karmek One). Après reverse Ghidra du firmware d’origine navel + validation live sur mon Teodora Evo I_VENT, voici la vraie couche physique et logique du bus :

Couche physique (=à respecter au niveau du code sinon la carte reste muette) :

Rôle : le module Wi-Fi est maître RWMS (=Read/Write Micronova Serial). Il interroge le poêle en poll cyclique, le poêle répond. Une écoute passive renvoie 0 trame.

Format des frames (=protocole RWMS, avec checksum additif) :

Adresses des registres (=Micronova standard, documentées par la communauté et validées empiriquement sur Teodora Evo) :

AdresseRegistreSensEncodage
0x01ambient tempread°C × 2 (=raw / 2 pour °C)
0x03water tempread°C × 2 (=absent sur modèles ventilés)
0x21stove stateread/write0=OFF 1=Start 4=WORK 7=Standby 8=Alarm
0x34flame powerread%
0x3Cwater pressurereadbar × 10
0x3Efumes tempread°C raw
0x7Dtemp setpointread/write°C
0x7Fpower setpointread/writestep 1..5
0x9Dtemp activeread°C
0x9Fpower activereadstep 1..5

Commandes = writes au registre 0x21 :

Doc de référence maintenue à jour avec le firmware : docs/PROTOCOLE-MICRONOVA.md dans le repo GitHub couvre en plus le protocole SOTA2 complet (=framing SLIP, header 8 octets esptool, handshake SYNC, table des opcodes 0x02..0xDE), la machine à états, les timings de retry, les registres EEPROM chrono/date et les points de reverse restés ouverts.

Familles de poêles : le firmware d’origine gère plusieurs types via stove_type_t (=I_CALD chaudière, I_IDRO hydro, I_VENT ventilé, plus variantes 2/3/4/5). OpenXtraflame conserve le type au boot depuis la partition secret1 du dump original pour rester compatible avec les mêmes registres remplis par le poêle.

Firmware

Firmware custom

Le firmware OpenXtraflame implémente aujourd’hui :

Encore à écrire :

Validation QEMU

QEMU-Xtensa boote OpenXtraflame. UART0 = console, UART1 = socket TCP:4449 simulant le bus Micronova. Un simulateur Python joue le master.

[Master] Test 1 - Read RAM_TAMB (0x30)
  TX: 00 30
  RX: 00 ff                → value=0, checksum ok ✓
[Master] Test 2 - Write RAM_TAMB (0x30) = 42
  TX: b0 2a d5
  RX: 2a d5                → ACK ok, valeur écrite ✓
[Master] Test 3 - Re-read RAM_TAMB (0x30)
  TX: 00 30
  RX: 2a d5                → value=42, RAM shadow persist ✓

3 points de validation critiques confirmés : adressage des registres, format de frames, timing UART.

Flash

Test préalable sur M5Stack ATOM Lite (=Target External)

Avant de reflasher le module d’origine, on valide toute la chaîne firmware sur un M5Stack ATOM Lite (=ESP32-PICO en boîtier fini, ~12 €, Target External, TARGET=external). Câblage :

Le poêle étant physiquement à côté, on peut aussi le stubber avec un simple Python qui joue le master Micronova sur /dev/ttyUSB1 (=celui de la validation QEMU décrite plus haut, réutilisé sur hardware réel).

Build et flash :

$ idf.py set-target esp32
$ idf.py -DOPENXFLAME_TARGET=external build
$ idf.py -p /dev/ttyUSB0 -b 460800 flash monitor

Ce que je vérifie sur le devkit avant de toucher au Black Label :

Flash sur le module Black Label

Une fois la version External validée, même procédure sur le module d’origine avec OPENXFLAME_TARGET=blacklabel. Deux précautions :

  1. Le dump extraflame_dump.bin sauvegardé au tout début reste dans le tiroir. En cas de problème, on peut restaurer le firmware d’usine avec un simple esptool --port COM3 --baud 460800 write-flash 0 extraflame_dump.bin.
  2. Le premier boot du firmware custom écrit une nouvelle NVS vide. Le secure_code et le stove_model d’origine sont perdus côté cloud Omnyvore (=irrécupérable, mais on s’en fiche puisqu’on ne va plus parler à Omnyvore). Ils restent dans le dump binaire, donc en cas de rollback ils reviennent.
$ idf.py -DOPENXFLAME_TARGET=blacklabel build
$ python -m esptool --port COM3 --baud 460800 erase-flash
$ idf.py -p COM3 -b 460800 flash monitor

Le module rebranché au poêle affiche alors ses 4 LEDs selon la nouvelle sémantique (=POWER on, WI-FI en STA quand la box est jointe, SERVER quand Mosquitto local répond) et le poêle apparaît dans Home Assistant via MQTT Discovery.

Commandes utiles au quotidien

Une fois le module en STA sur le réseau, la plupart des opérations se font depuis le Web UI - on n’a plus besoin du CH340G. Voici les commandes les plus utilisées, à ranger dans un bookmark.

Suivre les logs du firmware en live (=depuis le PC, module encore branché en série) :

python -m serial.tools.miniterm COM3 115200

Le firmware log en ESP_LOGI/W/E classique. Toutes les trames Micronova reçues + les évènements Wi-Fi et MQTT défilent en temps réel.

Live log Micronova depuis le navigateur (=une fois le module en STA, sans le CH340G) :

OTA depuis un URL (=sans passer par le PC) :

OTA par curl (=script sans navigateur, utile pour CI) :

curl -X POST --data-binary @openextraflame.bin \
     -H "Content-Type: application/octet-stream" \
     http://192.168.20.11/ota/upload

Rollback vers la version précédente (=si un OTA a introduit une régression) :

Reset usine (=vider la config Wi-Fi/MQTT et repartir en SoftAP de provisioning) :

Restaurer le firmware Extraflame d’origine (=en dernier recours, avec le dump du début) :

python -m esptool --port COM3 --baud 460800 write-flash 0 extraflame_dump.bin

La suite

Déjà en place

En cours

Prochaines features

Matériel

Prix indicatifs relevés au moment du projet. Aucun lien affilié - je ne touche rien.