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
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 :
- Aucune intégration Home Assistant officielle.
- Si Omnyvore ferme demain, mon poêle perd son pilotage distant.
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.

Au centre : le module ESP32-WROOM-32 (=Espressif dual-core Xtensa LX6). C’est le cerveau - c’est lui qu’on va reprogrammer. Autour :
- Transformer T009-3-A - alimentation isolée
- IC3 R173720 - probablement régulateur 3.3V
- IC4 8L05A AK2SCB - LDO 5V→3.3V linéaire
- 4 LEDs blanches CMS + bouton reset tactile (photo ci-dessous)
- Connecteur SERIAL 4 broches (=alim + UART vers le poêle)
- Connecteur CN5 - le mystère du début, qu’on va comprendre plus tard

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- connecteur 4-pin sur le bord du PCB. C’est celui qui relie le module au poêle via un câble à 4 fils (=vert, marron, blanc, jaune). Il transporte l’alimentation et la communication avec le poêle. On ne s’y branche pas pour dumper.CN5- header 3-pin juste à côté de l’ESP32. C’est le connecteur debug prévu par Extraflame (=probablement pour la programmation d’usine). C’est LUI qu’on utilise pour le dump.
CN2, le connecteur poêle (=4-pin) :

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

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

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 :

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

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

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

Broches de CN5 :
| Broche | Rôle module | Note |
|---|---|---|
| 1 | GND | pin marqué par le triangle sérigraphié ▲ |
| 2 | TX | envoie vers RXD du CH340G |
| 3 | RX | reç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 :
- 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.
- 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
- Deux choses branchées au module en même temps : son alim d’origine (=fournit le jus) et le CH340G (=lit et écrit la série).
- La masse (
GND) doit être commune entre les deux, ce qui est fait automatiquement dès que tu relies le fil noir du CH340G à la broche 1 du connecteur série (=celle-ci est déjà reliée à la masse commune du module et donc du bloc d’alim). - Le CH340G reste branché en USB au PC pour toute la durée du dump.
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 :
- CH340G en 5V → les 3 LEDs à droite du module s’allument en permanence, aucune donnée exploitable ne remonte. C’est un tell-tale visible immédiatement : si tu vois ces 3 LEDs allumées après reset, coupe tout et vérifie ton jumper.
- CH340G en 3.3V → aucune LED allumée en mode download, la trace série est propre.
Trois fils dupont femelles pour l’UART (=+ un 4ᵉ pour IO0↔GND détaillé à l’étape 4) :
| CH340G | Broche CN5 | Fil sur ma photo (=à adapter selon ton kit) |
|---|---|---|
GND | 1 (=GND module) | rouge |
RXD | 2 (=TX module) | jaune |
TXD | 3 (=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.

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

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 :
- 3 LEDs à droite du module allumées en permanence après power-on → jumper CH340G en 5V au lieu de 3.3V. C’est ce que j’avais au premier essai. À corriger avant tout, sinon le chip peut être endommagé.
boot:0x13 SPI_FAST_FLASH_BOOTpuis les logs Extraflame défilent →IO0n’était pas àGNDpendant le reset. Le firmware d’origine se lance normalement. Recouper l’alim en maintenant bienIO0↔GNDcette fois.- Écran totalement vide dans le moniteur série → RX du CH340G pas relié, masse pas commune, ou module pas alimenté. Vérifier chaque fil.
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.

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.

Concrètement, pour rentrer en mode download avec ce module :
- Le CH340G est branché en 3.3V à CN5 pour les 3 signaux UART (=étape 3 faite).
- Ajouter un 4ᵉ fil dupont : une extrémité posée / maintenue sur le pad
IO0de 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). - Le moniteur série est fermé (=port
COM3//dev/ttyUSB0libre). - En gardant
IO0maintenu àGND, débrancher/rebrancher le bloc d’alim d’origine du module (=hard reset). - Vérifier au moniteur série que la ligne
boot:0x3 (DOWNLOAD_BOOT…)(=ou l’équivalentboot:0xf uart_boot) puiswaiting for downloads’affichent. Une fois cette trace vue, tu peux relâcher le filIO0- le chip reste en mode download jusqu’à la prochaine coupure d’alim. - 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 :
- Elle confirme le boot mode (
boot:0x13= SPI flash normal,boot:0xf= download UART) - Elle révèle la version ESP-IDF (
v4.3-dirty= base v4.3 + patches propriétaires) - Elle liste la table de partitions en clair (=évite d’avoir à la reconstruire depuis le dump)
- 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 :

É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 trois
Warning: Deprecated:sont des simples avertissements de compatibilité -esptoolv5 accepte encoreno_reset(avec underscore) etchip_id(avec underscore) mais recommande les nouvelles formesno-resetetchip-id(=avec tirets). Dans le reste de l’article j’utilise directement les nouvelles formes, mais tu peux garder les anciennes, ça marche. - Le warning
ESP32 has no chip ID. Reading MAC address instead.est normal sur ESP32 : la commandechip-idest un vestige de générations plus anciennes de chips. Sur ESP32,esptoolfait un fallback sur la MAC address, ce qui suffit largement à confirmer que le chip est bien identifié et que la connexion marche. Staying in bootloader.confirme qu’après la commande, le chip est resté en mode download (=grâce à--after no-reset). On peut donc enchaîner directement avec la commande suivante (=espefuse, puisread-flash) sans avoir à recouper l’alim.
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 :
FLASH_CRYPT_CNT = 0(=nombre pair de bits à 1) → la flash est en clair, on peut la dumper telle quelleABS_DONE_0 = FalseetABS_DONE_1 = False→ Secure Boot désactivé, aucune signature à validerUART_DOWNLOAD_DIS = False→ le mode download UART est autorisé, on peut flasher/dumper viaesptoolJTAG_DISABLE = False→ JTAG également accessible si on en a besoin plus tard
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 :
- Framework : ESP-IDF v4.3-dirty
- Backend cloud :
mqtt.extraflame.it - Wi-Fi manager :
tonyp7/esp32-wifi-manager - UART Micronova : 38400 bps 8N1
- Registres RAM_ :
ACCENDI, SPEGNI, TAMB, TFUMI, TH2O, STATO, ALLARME - Log révélateur : “Waiting for Micronova master idle…” - indice fort que le module écoute
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 :
secure_code(=8 chiffres, code d’appairage utilisé comme username/password MQTT)stove_model(=10 chiffres, identifiant du modèle Extraflame chez Omnyvore)
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” :
- Signer un cert serveur pour
mqtt.extraflame.itavec ce CA demande la clé privée du CA, qu’on n’a pas et qu’on n’aura pas (=absente du firmware, absente du serveur MQTT public, elle est offline chez Omnyvore). - Rediriger
mqtt.extraflame.itpar DNS vers un Mosquitto local sans toucher au module fait échouer le TLS handshake côté module : le certificat local n’est pas signé par le CA embarqué. - Autoriser un cert local dans le module reviendrait à modifier la NVS ou le firmware, donc autant remplacer directement toute la couche MQTT.
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 :
- UART Micronova :
UART_NUM_1, TX=GPIO_NUM_23, RX=GPIO_NUM_5, 38400 8N1 - 4 GPIOs actifs sur les 4 LEDs du panneau :
25, 26, 32, 33(=un 5e GPIO 22 s’agite aussi au boot, associé au bouton reset, pas à une LED supplémentaire visible sur la face du 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érigraphie | GPIO ESP32 | Rôle d’origine |
|---|---|---|
POWER | GPIO 25 | allumée tant que le module est sous tension |
BLE | GPIO 26 | Bluetooth (=provisioning via app Total Control) |
WI-FI | GPIO 32 | STA connectée à la box |
SERVER | GPIO 33 | connexion 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) :
- UART1, 1200 baud, 8N2, pas de contrôle de flux.
- Inversion de ligne obligatoire :
uart_set_line_inverse(UART1, UART_SIGNAL_RXD_INV | UART_SIGNAL_TXD_INV)(=masque0x24). Le bus utilise une polarité RS-232 aux niveaux TTL, sans convertisseur physique. Sans inversion, RX est indécodable. - Le
38400 8N1visible au boot log correspond au canal SOTA2 (=protocole de reflash du firmware du poêle, séparé), pas à la télémétrie. - Bus half-duplex sur un seul fil : chaque octet émis est ré-entendu en écho, à sauter dans le décodage.
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) :
- Lecture :
[loc][addr](2 octets) →[checksum][value](2 octets) - Écriture :
[loc][addr][value][checksum](4 octets) →[checksum][value](2 octets) locencode le mode :0x00= read RAM,0x20= read EEPROM,0x80= write RAM,0xA0= write EEPROM (bit 5 pour EEPROM, bit 7 pour write).checksum = (loc + addr + value) & 0xFF(=somme, pas complément).
Adresses des registres (=Micronova standard, documentées par la communauté et validées empiriquement sur Teodora Evo) :
| Adresse | Registre | Sens | Encodage |
|---|---|---|---|
0x01 | ambient temp | read | °C × 2 (=raw / 2 pour °C) |
0x03 | water temp | read | °C × 2 (=absent sur modèles ventilés) |
0x21 | stove state | read/write | 0=OFF 1=Start 4=WORK 7=Standby 8=Alarm |
0x34 | flame power | read | % |
0x3C | water pressure | read | bar × 10 |
0x3E | fumes temp | read | °C raw |
0x7D | temp setpoint | read/write | °C |
0x7F | power setpoint | read/write | step 1..5 |
0x9D | temp active | read | °C |
0x9F | power active | read | step 1..5 |
Commandes = writes au registre 0x21 :
[0x80, 0x21, 0x01, 0xA2]= allumer[0x80, 0x21, 0x06, 0xA7]= éteindre[0x80, 0x21, 0x00, 0xA1]= reset alarme
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 :
- Un master polling loop sur UART1 à 1200 8N2 inversé, qui interroge le poêle en cyclique et parse les réponses
[checksum, value]avec vérification additive. - Une RAM shadow qui reflète l’état des registres Micronova standard (=
STOVE_STATE 0x21,TAMB 0x01,FUMES_TEMP 0x3E,POWER_GET 0x9F,TEMP_GET 0x9D, etc.). - Un bridge MQTT minimaliste : publish périodique d’un JSON sur
<prefix>/<stove>/state(=state, power, alarm, températures) + souscription aux topics de commandecmd/on,cmd/off,cmd/setpoint,cmd/power,cmd/reset_alarm. Chaque commande écrit dans la RAM shadow, le poêle la lit à son prochain poll. - Une web UI embarquée pour la config Wi-Fi + Mosquitto au premier boot en mode SoftAP
openextraflame_XXXX, puis accessible en STA.
Encore à écrire :
- Publication automatique des Home Assistant MQTT Discovery topics (=le hook
mqtt_bridge_publish_discovery()existe et sera appelé auMQTT_EVENT_CONNECTED, mais son payload est encore unTODO). - Mode Guardian optionnel qui archive les OTA officielles Extraflame côté LAN (=filet si Omnyvore disparaît en cours de route).
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 :
GPIO 17(=STOVE_UART_TX_PIN) versRXdu poêleGPIO 16(=STOVE_UART_RX_PIN) versTXdu poêleGNDcommun
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 :
- SoftAP
openextraflame_XXXXbien annoncé au boot. - Web UI accessible sur
http://192.168.4.1/pour saisir SSID + password Wi-Fi + adresse Mosquitto. - STA reconnecte à la box après reboot, MQTT publie sur
extraflame/poele/state. - Frames Micronova lues sans erreur de checksum.
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 :
- Le dump
extraflame_dump.binsauvegardé au tout début reste dans le tiroir. En cas de problème, on peut restaurer le firmware d’usine avec un simpleesptool --port COM3 --baud 460800 write-flash 0 extraflame_dump.bin. - Le premier boot du firmware custom écrit une nouvelle NVS vide. Le
secure_codeet lestove_modeld’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) :
- Web UI → onglet 🔍 Debug : ring buffer des 64 dernières trames RX/TX avec timestamps, refresh 1 s. Utile pour vérifier que le poêle poll bien le module.
OTA depuis un URL (=sans passer par le PC) :
- Web UI → onglet ⬆️ OTA → coller
https://www.isno.fr/firmware/openextraflame-latest.bin(=ou une URL GitHub Releases une fois le repo public) → bouton ⬇️ Pull. - Le module télécharge tout seul, vérifie la signature SHA256, écrit dans le slot OTA alternatif et reboote.
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) :
- Web UI → onglet OTA → bouton ↩️ Rollback. Le module réactive le slot d’où on venait et reboote.
Reset usine (=vider la config Wi-Fi/MQTT et repartir en SoftAP de provisioning) :
- Web UI → onglet ⚙️ Avancé → bouton 💥 Reset usine. Efface la partition NVS mais garde le firmware installé.
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
- ✅ Firmware validé sur M5Stack ATOM Lite (=
OPENXFLAME_TARGET=external) et sur le vrai module Black Label en production - ✅ Provisioning Wi-Fi via SoftAP
openxtraflame_XXXXpuis passage automatique en STA sur le réseau IoT - ✅ Web UI embarquée à 7 onglets : Dashboard, Wi-Fi, MQTT, Poêle, OTA, Debug, Avancé
- ✅ OTA fonctionnel de bout en bout : upload direct du
.bindepuis le navigateur, ou pull depuis une URL HTTPS (=validation CA via le bundle Mozilla d’ESP-IDF), avec barre de progression en temps réel (=polling/ota/statuschunk-par-chunk) - ✅ Auto-wipe de la partition
phy_initau changement de version firmware (=évite qu’un OTA hérite d’une calibration RF invalide et se bloque enreason=210sur la STA suivante) - ✅ Rollback safety :
esp_ota_mark_app_valid_cancel_rollback()sur le premier boot réussi, un firmware buggé fait rollback automatique vers le slot précédent - ✅ Protection contre le clobber accidentel des credentials Wi-Fi/MQTT (=un save depuis un onglet non lié n’écrase plus le password avec une chaîne vide), placeholders explicites
•••••••• (défini, laisser vide pour ne pas changer)sur les champs password déjà provisionnés - ✅ UI polie : toasts non bloquants (=remplacent les
alert()natifs), modal dialogs custom (=au lieu deconfirm()bloqué sur mobile Chrome), retours visuels de progression sur toutes les actions longues - ✅ Live log des trames Micronova dans l’onglet Debug (=ring buffer 64 entrées, refresh 1 s)
- ✅ Home Assistant MQTT Discovery : au premier
MQTT_EVENT_CONNECTED, le module publie 11 topicshomeassistant/*retenus qui auto-provisionnent dans HA un device uniqueExtraflame - OpenXtraflame Black Labelavec sensors (=T° ambiante / fumées / eau / puissance / état / alarme), binary sensoronline, switch on/off, button reset alarme, number setpoint et select puissance - ✅ Last Will Testament MQTT :
<prefix>/availabilityretenu àofflinesi le module tombe, HA grise instantanément les entités - ✅ Auto-discovery du broker MQTT côté firmware via mDNS : bouton 🔍 Détecter HA dans l’onglet MQTT qui tente successivement
_mqtt._tcp,_home-assistant._tcpethomeassistant.local, remplit auto les champs host / port - ✅ Miroir OTA sur
www.isno.fr/firmware/openextraflame-latest.bin(=résolvable en LAN et WAN, HTTPS Let’s Encrypt) - ✅ mDNS repeater activé sur OPNsense entre VLAN 20 IoT et VLAN 50 SERVERS pour que le multicast Zeroconf traverse
En cours
- Publier le repo
Shad107/OpenXtraflameen public sur GitHub (=historique nettoyé, README refait) - Bascule du miroir
isno.frvers un simple redirectLocation: https://github.com/.../releases/latest/download/...une fois le repo public
Prochaines features
- Guardian mode optionnel : archive silencieuse des OTA officielles Extraflame sur un serveur local (=filet si Omnyvore ferme un jour, on garde les binaires factory)
- Support autres marques Micronova : profils dispatcher par
stove_typepour EdilKamin, LAMINOX, Freepoint, Karmek One (=le protocole est le même, seul le mapping RAM diffère parfois)
Matériel
- ◨Module Wi-Fi Extraflame Black Label 289 € TTC
Le module d'origine à reverser. Fourni avec le poêle ou en pièce détachée.
- ◨
Interface série 3.3V/5V pour flasher et dumper l'ESP32. Attention à bien sélectionner le mode 3.3V.
- ◨
Pour raccorder rapidement le CH340G aux pattes de l'ESP32 sans souder. Kit 40 fils.
- ◨
Petit ESP32-PICO en boîtier fini utilisé pour tester le firmware sans toucher au module d'origine. Cible TARGET_EXTERNAL.
- ◨Scotch d'électronicien (optionnel) ~3 €
Pour maintenir un fil dupont en contact sur le pin 2 pendant la lecture - pas obligatoire, tenir à la main marche aussi.
Amazon / boutique bricolage
Prix indicatifs relevés au moment du projet. Aucun lien affilié - je ne touche rien.