← cyberloutre.fr 🦦 Loutre

🐰 Case study · Reverse engineering

Nabaztag Revival.

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
Un Nabaztag, lapin connecté de 2006

🪦 Le problème

Un objet qui n'a plus de maison.

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

Lui construire un remplaçant qui parle sa langue.

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 :

  • Il imite Violet. Quand le lapin appelle au démarrage, il accepte la conversation et lui envoie le « cerveau » dont il a besoin pour fonctionner, comme le faisait l'original.
  • Il pilote le lapin. Il bouge les oreilles à des positions précises, déclenche des jeux de lumière sur ses 5 LEDs, allume le nez, joue des sons et parle (synthèse vocale).
  • Il s'intègre à la maison connectée. Une petite interface permet à mon système domotique (Home Assistant) de demander au lapin de réagir — « annonce qu'il pleut », « clignote en bleu si Raphaël dort ».
  • Il garde le ton du lapin. Phrasé laconique d'origine — « Aujourd'hui… pluie ! » — c'est important.

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)

Là où ça devient intéressant.

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 boot réussit, mais toutes les commandes sont ignorées »

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.

🎙️ De la voix, sans aucun firmware custom

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

🔐 Trois formats binaires, dont un chiffré

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.

🧱 Cross-compiler whisper.cpp pour Alpine/musl

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 objet de 20 ans qui écoute et répond.

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

Un firmware maison pour écouter sans les mains.

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.

🧰 Remonter la toolchain « Metal » depuis les sources

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.

🔎 Reverse du dispatch, de la pile réseau et du chemin d'enregistrement

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.

🌐 Des problèmes systèmes, sur un appareil de 20 ans

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.

🎧 La boucle vocale, côté serveur

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

Un firmware signé, flashé pour de vrai — puis débriqué au JTAG.

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.

🧱 « J'ai brické le lapin »

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.

🩹 Réparé avec un Raspberry Pi en guise de sonde JTAG

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.

📡 La box lui coupait Internet — il s'en passe

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

🖥️ Une page de config enfin lisible en 2026

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

Ce qui arrive ensuite.

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 :

Phase 7 · en cours

🐍 Rendre le « cerveau » du lapin lisible par tout le monde

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

Phase 8 · en cours

🪓 Alléger le firmware en passant au langage natif

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

Ce que ce projet mobilise.

Voir le code sur GitHub →