🐰 Case study · Reverse engineering
Un lapin connecté des années 2000, débranché de force en 2011 quand la société qui le faisait vivre a fermé. Je l'ai ramené à la vie chez moi, sans aucun service cloud, et je lui ai appris à dialoguer avec une IA — tout ça sans changer la moindre pièce dans l'objet.
Aujourd'hui : il se réveille, bouge, s'illumine, écoute, comprend le français, me répond à voix haute — et il lui arrive de prendre la parole tout seul. Live terrier.cyberloutre.fr
🪦 Le problème
Le Nabaztag a été conçu comme un objet connecté avant l'heure : toute son « intelligence » vit dans un serveur quelque part sur Internet, pas dans le lapin lui-même. À chaque démarrage, il appelle ce serveur, qui lui dit quoi faire. Sans lui, le lapin allume une LED et s'arrête.
La société qui faisait tourner ce service — Violet — a mis la clé sous la porte vers 2011. Depuis, des milliers de lapins servent de presse-papiers ou dorment dans des cartons.
Mon objectif : en réanimer un chez moi, sans dépendre d'aucun service extérieur, et l'intégrer à mon installation domotique pour qu'il réagisse à la vie de la maison (quelqu'un arrive, la météo change, un rappel). Puis, plus tard, lui apprendre à m'écouter et à me répondre.
Une règle que je me suis imposée : ne pas toucher au matériel. La méthode populaire pour faire revivre un Nabaztag consiste à remplacer sa carte interne par un Raspberry Pi — trop facile, et ce n'est plus le même objet. On garde l'original, tel quel.
🛠️ L'approche
Mon idée : écrire un petit serveur de remplacement, qui parle exactement la même langue que les serveurs disparus, et le faire tourner chez moi. Le lapin croit appeler Violet en 2008 ; en réalité, il discute avec mon logiciel sur le réseau local de la maison.
Concrètement, ce serveur fait quatre choses :
Phase 2 : la voix. Quand je maintiens le bouton sur la tête du lapin, il enregistre ce que je dis et envoie l'audio au serveur. Mon serveur le transcrit, le passe à une IA (Claude), et fait reparler le lapin avec la réponse. L'IA peut même animer le lapin pendant qu'elle parle — oreilles, LEDs, nez — en glissant des instructions dans son texte.
Pour les ingés : c'est un serveur Python sans aucune dépendance (bibliothèque standard uniquement), packagé en add-on Home Assistant OS (Docker). Le détail du protocole est dans la section suivante.
🧩 Les défis (et comment je les ai résolus)
Section pour les ingés et les curieux de mécanique. Si la technique te parle pas, scrolle direct jusqu'au résultat ↓, promis je ne le prends pas mal.
Le plus beau bug. Après reverse engineering de la machine à états du
handshake XMPP, le lapin démarrait correctement… mais ignorait
silencieusement chaque commande poussée. En instrumentant l'état interne
de l'appareil (il a une commande XMPP cachée getrunningstate),
j'ai découvert que le serveur DOIT répondre au <presence>
du lapin pour qu'il atteigne l'état interne « free » où il agit
sur les commandes. Sans cette réponse : boot OK, mais lapin sourd.
Bonus piège : le <unbind> du lapin partage un espace de
noms XML avec <bind> — un handler naïf l'avalait et
désynchronisait tout le suivi d'état.
Plutôt que de flasher quoi que ce soit, j'ai découvert que le firmware d'origine cachait déjà un push-to-talk : le bouton enregistre le micro 8 kHz et envoie un WAV en IMA-ADPCM par HTTP POST. J'ai écrit un décodeur IMA-ADPCM → PCM en Python pur, rééchantillonné de 8 à 16 kHz, et lancé un modèle whisper.cpp embarqué pour la transcription. Aucun bidouillage matériel, aucun firmware remplacé.
Le lapin bootait, respirait, et restait muet — jusqu'à ce que le serveur
réponde à son <presence>.En lisant le bytecode (écrit en « Metal », le langage de VM de Violet) et
la réimplémentation C++ de référence, j'ai récupéré trois formats de
paquets : l'AmbientPacket (icônes ventre / oreilles / nez /
LED du bas), le SleepPacket, et les « programmes »
MessagePacket — ces derniers obfusqués par un
chiffrement à substitution roulante (table d'inversion). Je l'ai
ré-implémenté et vérifié qu'il fait un aller-retour parfait contre la
propre routine de dé-obfuscation de l'appareil.
L'add-on tourne sur Alpine (musl). J'ai cross-compilé whisper.cpp dans un build Docker multi-stage (avec OpenMP), intégré espeak-ng pour une TTS hors-ligne, et branché l'add-on Piper de Home Assistant (Wyoming) pour une voix de meilleure qualité, récupérée via le proxy de l'API Supervisor. Au passage, un bug retors : sous s6-overlay, le service n'héritait pas du token de l'API Supervisor depuis l'environnement — il fallait le lire depuis le fichier container-environment de s6.
Le push-to-talk était déjà là, planqué dans le bytecode d'usine. Il suffisait de décoder ce que le lapin envoyait.
✨ Le résultat
Un appareil arrêté depuis deux décennies qui, désormais : se réveille, respire, bouge, s'illumine, écoute, comprend le français, demande à une IA et me répond d'une voix naturelle. Tout ça entièrement chez moi, sur le réseau local de la maison — le seul morceau de cloud, optionnel, c'est l'IA elle-même (et encore, on peut la remplacer par une IA locale).
La boucle complète — j'appuie sur le bouton, je parle, le lapin transcrit, l'IA répond, le lapin parle — tourne sur la vraie machine. L'IA peut même animer le lapin pendant qu'elle parle : oreilles, LEDs, nez. Tout a été vérifié en direct sur l'appareil physique, pas dans un simulateur.
Et il ne se contente plus d'attendre qu'on lui parle : il lui arrive de prendre la parole de lui-même — un bonjour le matin, un mot quand quelqu'un rentre, une petite remarque lancée au fil de la journée, toujours dans le phrasé laconique de Violet (« Aujourd'hui… pluie ! »), écrite à la volée par l'IA. Un mode nuit le fait taire de 22 h à 8 h : aucune arrivée tardive, aucun réveil à 3 h du matin ne te tirera du lit.
🔬 Plongée technique · Phase 3
Section très technique — pour les ingés et les makers. Si tu lis pour l'histoire, file directement à la feuille de route ↓.
Après avoir réanimé le lapin puis lui avoir donné la parole, il restait une dernière frontière : l'écoute mains-libres — dire « Nabi » et qu'il réponde, sans rien toucher. Le hic : le programme interne du lapin n'enregistre le micro que quand on appuie physiquement sur le bouton. Le serveur ne peut pas déclencher un enregistrement tout seul.
Jusqu'ici, tout avait été fait sans le moindre firmware custom. Pour les mains-libres, il en fallait un : j'ai écrit un firmware maison qui diffuse le micro en continu, en reverse-engineerant puis en recompilant le propre bytecode de l'appareil — et fait tourner par-dessus une chaîne vocale 100 % locale (reconnaissance → LLM → synthèse). Toujours aucune modification matérielle : le firmware est livré au démarrage, rien n'est flashé en dur.
Pour produire du bytecode, il faut d'abord le compilateur. J'ai remonté la toolchain Metal depuis ses sources : cross-compilation d'un compilateur C++ 32 bits dans un conteneur Linux (le build n'aboutit que sur x86, avec le multilib 32 bits). Preuve que la chaîne est correcte — j'ai recompilé le firmware d'origine depuis les sources, identique octet pour octet à celui d'usine.
En reverse-engineerant le dispatch de commandes, la pile réseau et le chemin d'enregistrement, j'ai isolé exactement pourquoi le serveur ne pouvait pas capter d'audio : le micro est verrouillé derrière le bouton. J'ai alors écrit un petit module firmware qui ajoute deux commandes pilotées par le serveur et diffuse l'audio 8 kHz en datagrammes UDP.
Routage inter-sous-réseaux : l'ARP du firmware résout déjà la passerelle pour les IP non locales, donc le flux traverse les VLAN sans bidouille. Et une contrainte half-duplex bien réelle — l'appareil ne peut pas enregistrer et jouer en même temps : le serveur coupe le micro avant de parler, puis le réarme ensuite.
Décoder le flux IMA-ADPCM du lapin en PCM, faire tourner un modèle de reconnaissance vocale local sur des fenêtres glissantes, détecter le mot d'éveil, envoyer la commande à un agent conversationnel (LLM), puis reparler la réponse d'une voix naturelle locale — l'agent pouvant même bouger les oreilles et les LED dans sa réponse.
« Nabi, raconte-moi une blague » — et le lapin a transcrit la phrase, interrogé le LLM, puis raconté la blague à voix haute. Mains-libres, sur la vraie machine.
Bilan : un gadget arrêté depuis 2006 fait désormais tourner un firmware maison qui diffuse son micro, écoute son nom, comprend le français, interroge un LLM et répond à voix haute — entièrement sur le réseau local, le LLM étant le seul composant cloud, et optionnel. (Sa toute première blague mains-libres : « pourquoi la tomate a-t-elle traversé la route ? Pour prouver qu'elle n'était pas une banane. »)
Honnêteté de rigueur : c'est une preuve de concept qui fonctionne, pas un produit fini. L'écoute permanente est optionnelle et gourmande en CPU — on l'active quand on en a envie.
Ce que cette phase a mobilisé
🔐 Plongée technique · Phase 4
Encore plus bas niveau — pour les makers. Si tu lis pour l'histoire, file à la feuille de route ↓.
Jusque-là, le firmware maison était livré au démarrage, jamais gravé. L'étape suivante : un vrai firmware flashé dans la puce — mais à une condition non négociable, qu'on ne puisse y pousser que du code signé par moi. J'ai donc bâti Naboot, un firmware maison qui ajoute une mise à jour à distance (OTA) verrouillée par signature Ed25519 : le lapin refuse tout binaire qui ne porte pas ma signature. Le tout testé de bout en bout en flashant par les ondes, sans ouvrir l'objet ni brancher le moindre câble.
Le moment qui fait peur : après un flash, le lapin ne bootait plus. Diagnostic après dissection : pas une faute de mon code, mais un bug de la chaîne de compilation d'origine — elle rangeait certaines variables globales dans des zones mémoire que la routine de démarrage oubliait de remettre à zéro. Résultat : des valeurs parasites, crash dès le boot. Le bug touchait même la version vierge — ce qui a innocenté mes modifications.
Pour ranimer une puce qui ne boote plus, il faut lui réécrire la mémoire par la « porte de service » (JTAG) — normalement avec une sonde dédiée. Je n'en avais pas : j'ai transformé un Raspberry Pi en sonde JTAG bricolée (huit fils sur le connecteur), réimplémenté la séquence de programmation de la puce OKI, et réécrit le firmware octet par octet. Lapin sauvé — et de nouveau capable d'accepter une mise à jour signée.
Le bug n'était pas dans mon firmware, mais dans la chaîne de compilation d'origine. Encore fallait-il un JTAG maison pour le prouver.
Une fois réparé, le lapin bootait mais restait tout orange : la box de la maison filtrait silencieusement ses requêtes DNS — et seulement les siennes (prouvé par un test témoin). Plutôt que de bricoler le pare-feu, je lui ai appris à retrouver son serveur tout seul par diffusion multicast (mDNS) sur le réseau local, sans rien reflasher. Boot, résolution multicast, et toute la boucle vocale repart sur le lapin réparé.
Au passage, j'ai réécrit les quatre pages de configuration Wi-Fi du lapin — celles qu'on voit en le branchant la première fois — en HTML moderne, responsive et thème sombre, tout en gardant exactement les champs que le firmware attend. Bonus : ~5 Ko de ROM économisés.
Bilan : un lapin de 2006 accepte aujourd'hui des mises à jour signées, poussées par les ondes, avec une page de configuration moderne, et sait retrouver son serveur même quand le réseau lui met des bâtons dans les roues. Tout a été vérifié sur le matériel — y compris la partie la moins glorieuse : le débrickage.
Ce que cette phase a mobilisé
🗺️ La feuille de route
L'essentiel est désormais en place et vérifié sur le matériel : le lapin se réveille, bouge, parle avec une IA, écoute sans les mains, accepte des mises à jour signées, retrouve son serveur même sur un réseau hostile, et prend la parole de lui-même. Un serveur public tourne sur terrier.cyberloutre.fr : n'importe quel Nabaztag du monde peut s'y connecter. Restent deux chantiers, bien avancés mais pas finis :
Première version livrée. Le cerveau du lapin est écrit en « Metal », un langage de 2006 que plus personne ne lit. J'ai déjà réécrit en Python toute la chaîne qui permet de le décoder, le vérifier et le recompiler — validée au bit près contre l'outil d'origine en C++. Reste à finir la couche qui laissera lire et écrire cette logique directement en Python, pour que d'autres puissent contribuer sans la vieille chaîne C++.
Prototype livré, mesuré. Une partie du firmware tient en environ 3000 lignes du langage Metal, qui tournent sur une petite machine virtuelle. Le langage natif de la puce (« C ») est plus compact : réécrire les morceaux les plus stables directement en C libère de la mémoire. Le prototype actuel produit déjà un firmware ~18 Ko plus léger que la version de secours et libère ~10 Ko de RAM à l'exécution. Le compromis assumé : on ne « durcit » ainsi que les parties bien comprises et stables — le reste garde la souplesse du bytecode, modifiable sans reflash.
🧱 Stack & compétences