{"id":638,"date":"2026-11-01T23:41:46","date_gmt":"2026-11-01T22:41:46","guid":{"rendered":"https:\/\/www.fabidouille.com\/?p=638"},"modified":"2026-11-01T23:56:33","modified_gmt":"2026-11-01T22:56:33","slug":"parle-a-ma-base","status":"publish","type":"post","link":"https:\/\/www.fabidouille.com\/?p=638","title":{"rendered":"Parle \u00e0 ma Base: my abandoned robot vacuum finds its dock again, and speaks French"},"content":{"rendered":"<p>One Sunday, one robot vacuum. At the start it spoke Mandarin and could not find its dock. At the end it speaks French, and it finds its dock.<\/p>\n<p>It is a Viomi \/ Xiaomi \u00ab\u00a0Mi Robot Vacuum-Mop P\u00a0\u00bb, in my antre. My home automation (Jeedom) controls it locally. But Xiaomi stopped supporting it years ago, the phone app crawls, and the voice talks in a language I do not read. So I asked my AI agents: find why it does not dock, remove the cloud, give it a French voice.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/11\/robot.jpg\" alt=\"A white round robot vacuum on a wooden floor, in front of its small black charging base, against a pine wall\"\/><figcaption>The patient, right in front of its dock.<\/figcaption><\/figure>\n<p>The first guess of the agent was \u00ab\u00a0dirty charge contacts\u00a0\u00bb. Wrong: when I put the robot on the dock by hand, it charges fine. Parle \u00e0 ma base. (It docks now. I cannot say which step fixed it: the robot was restarted many times that day.)<\/p>\n<h2 class=\"wp-block-heading\">Under the battery<\/h2>\n<p>To change anything, I needed a shell inside the robot. The public guide (rumpeltux\/viomi-rooting) uses a micro-USB port hidden under the battery. There it is:<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/11\/usb-port.jpg\" alt=\"A close-up of a black USB cable plugged into a small green circuit board inside a white plastic bay\"\/><figcaption>The micro-USB port, in the battery bay.<\/figcaption><\/figure>\n<p>At every boot, the robot appears on USB twice: first in a recovery mode (about 3 seconds), then as an Android debug bridge, <code>adb<\/code>, for less than half a second. Then its app starts and closes the door. The guide says: be fast, try about ten times.<\/p>\n<p>I was not fast. Some of my micro-USB cables carry no data, so I tested them. The first answer of the test screen: NO POWER.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/11\/cables.jpg\" alt=\"A screen with a USB data cable test and the message NO POWER\"\/><figcaption>The cable test screen. First answer: NO POWER.<\/figcaption><\/figure>\n<p>Then the first agent decided that this robot cannot be rooted without soldering. I did not believe it, and I said so, in capital letters. A second agent read the whole session and said the opposite: the 0.4 second window is normal, you just loop and replug.<\/p>\n<p>So the agent wrote a watcher, and I unplugged, replugged and pressed POWER for 3 seconds, again and again, reading the LED colours out loud.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/11\/watcher.png\" alt=\"Terminal output of the USB watcher: lines with PRESENT FEL and PRESENT FENETRE-ADB, each lasting a few seconds or less\"\/><figcaption>The watcher, real output. FEL is the recovery mode. FENETRE-ADB is the window: it closes in less than a second.<\/figcaption><\/figure>\n<p>It worked, by luck first: a file went through in a 0.47 second window. Then the agent made it reliable, in four steps:<\/p>\n<ol class=\"wp-block-list\">\n<li>A loop with <code>adb wait-for-device<\/code>, which reacts faster than polling, and a push that runs in the same instant. (<code>adb<\/code> also has to run as root on my PC. As a normal user, the answer is only \u00ab\u00a0Access denied\u00a0\u00bb.)<\/li>\n<li>In one window, the loop deletes one link: <code>\/etc\/rc.d\/S90robotManager<\/code>, the one that starts the robot app at boot. With no app to close the door, <code>adb<\/code> stays open at the next boot (8 seconds, then more). Without the app the robot does nothing else, so this is temporary.<\/li>\n<li>With <code>adb<\/code> open, the agent installs an SSH server, dropbear 2015.71-2 (the package and its checksum come from the guide), puts my server&rsquo;s public key in <code>authorized_keys<\/code>, and turns passwords off. Dropbear starts at every boot by itself: no more USB game.<\/li>\n<li>It puts the link back, so the robot app starts again, and from then on I use SSH only.<\/li>\n<\/ol>\n<pre class=\"wp-block-code\"><code>adb shell rm \/etc\/rc.d\/S90robotManager        # in the window, then reboot\nadb push dropbear ...                         # key only, passwords off\nadb shell ln -s ..\/init.d\/robotManager \/etc\/rc.d\/S90robotManager\nssh -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa root@robot<\/code><\/pre>\n<p>The old dropbear only speaks RSA, hence the two options. A <code>sed<\/code> command turned <code>option<\/code> into <code>optioff<\/code> in its config (the pattern \u00ab\u00a0on\u00a0\u00bb was too greedy), and an interrupted key generation left an empty host key, so the server refused to start. Both easy to fix, once found.<\/p>\n<p>While <code>adb<\/code> was open, we saved the whole flash memory (9 partitions, about 209 MB) to my server, with SHA-256 hashes. That is the way back if anything breaks.<\/p>\n<h2 class=\"wp-block-heading\">Un coup de balai<\/h2>\n<p>Then <a href=\"https:\/\/github.com\/Hypfer\/Valetudo\">Valetudo<\/a>: a free, open source app that runs next to the original firmware and takes the place of the cloud. Two changes make the robot ignore Xiaomi: ten lines in <code>\/etc\/hosts<\/code>, and a patch of the cloud address inside <code>miio_client<\/code>. Valetudo plays the cloud instead, and Jeedom still controls the robot locally. I turned on a password too: its log was readable by anyone on my network, token included.<\/p>\n<p>Not everything went well: with the USB cable plugged at boot the robot loops forever, and when I started Valetudo by hand the robot rebooted or crashed. At boot, it starts alone without any problem. I do not know why.<\/p>\n<h2 class=\"wp-block-heading\">Tout ou\u00efe<\/h2>\n<p>Now the voice. The robot says about 144 different sentences (\u00ab\u00a0battery low\u00a0\u00bb, \u00ab\u00a0cleaning started\u00a0\u00bb&#8230;). This model has no voice pack to download.<\/p>\n<p>So the agent took the robot&rsquo;s main application, a 5 MB ARM program called <code>RobotApp<\/code> that runs everything (cleaning, map, sounds), out of the flash backup. It opened it in Ghidra, a free tool that turns a binary back into readable code. The program contains a table of 144 file names (one per event, like \u00ab\u00a0start of a point cleaning\u00a0\u00bb), a second table with the sound folders, and a function <code>playAudio<\/code> that glues them together and starts a small player:<\/p>\n<pre class=\"wp-block-code\"><code>system(\"tinyplayer \/usr\/share\/audio\/mandarin\/10sound_point_clean_start.mp3\")<\/code><\/pre>\n<p>The plan: French MP3 files, same names, same folder. The system area is an overlay, so the originals stay underneath, and deleting my copies brings them back.<\/p>\n<p>The agent wrote the 144 sentences from the file names, in a small CSV: file name, French sentence.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/11\/voices.png\" alt=\"Terminal output: the first ten file names of the voice list, each followed by its French sentence, like Nettoyage de la zone, je d\u00e9marre\"\/><figcaption>The first lines of the French script.<\/figcaption><\/figure>\n<p>Then a script turns the CSV into audio, in two steps:<\/p>\n<ol class=\"wp-block-list\">\n<li><strong>Speak.<\/strong> Each sentence goes through a text-to-speech model, and gives one WAV file.<\/li>\n<li><strong>Convert.<\/strong> <code>ffmpeg<\/code> encodes each WAV like the originals: MP3, 22,050 Hz, mono, 160 kbps. The agent first measured the original messages, and set the loudness of mine to their median level, so that my voice is not louder or quieter than the robot&rsquo;s own beeps. Each file gets the exact name that the firmware looks for.<\/li>\n<\/ol>\n<p>Two voice models were tried. The first one is the one I use for my audiobook project: a Qwen speech model with a voice called \u00ab\u00a0Serena\u00a0\u00bb, driven by a written instruction. It runs on my desktop GPU, about 5 seconds per sentence, so 12 minutes for the 144. The result had a foreign accent. The second one is Piper, a small open source speech engine that runs on a plain CPU, with the voice <code>fr_FR-siwis-medium<\/code> (a 63 MB file, trained on a French speech database).<\/p>\n<p>Why not just pick one? The agent cannot hear, and I do not trust a description of an accent. So it made one short sentence (\u00ab\u00a0Nettoyage localis\u00e9, je d\u00e9marre.\u00a0\u00bb) in three versions: A, the Qwen voice as it was, B, Qwen with a stricter instruction (a native speaker from France, standard pronunciation, no foreign accent), and C, Piper. All three at the same loudness, because a louder file always sounds better. It was not a blind test: the file names said which engine was which. I listened, and the third one was much better. Piper also needed no loudness boost, while the Qwen files had to be amplified, which adds hiss on short sentences. So the 144 were made again with Piper.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/11\/voices.png\" alt=\"Terminal output: the first ten file names of the voice list, each followed by its French sentence, like Nettoyage de la zone, je d\u00e9marre\"\/><figcaption>The first lines of the French script.<\/figcaption><\/figure>\n<p>First a test with a single file, the factory test message: \u00ab\u00a0Pr\u00eat pour le test.\u00a0\u00bb I heard it from the robot. Then the 144 files, with their hashes checked. And it works: the robot now speaks French by itself. Ten messages had no file before: the robot was probably silent at those moments, and now it speaks.<\/p>\n<h2 class=\"wp-block-heading\">How AI was used<\/h2>\n<p>AI agents (Claude Code) did most of the work: read the guides, ran the USB watchers, installed SSH and Valetudo, read the voice code in Ghidra, wrote the French script, generated the audio, wrote the notes in my home repo. They were wrong about the dirty contacts, about soldering, and once pushed a file to my phone instead of the robot.<\/p>\n<p>I opened the robot, plugged and unplugged more times than I can count, chose the voice by ear, and said yes before each write on the robot.<\/p>\n<p>Note: I don&rsquo;t have much time for the blog these days, so this post was written with the help of an AI, from my own notes, then read and corrected by me. Better to share the content this way than not at all.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A robot vacuum that Xiaomi abandoned: rooted through a 0.4 second USB window, cloud removed with Valetudo, and 144 French voice messages made with AI agents. And it finds its dock again.<\/p>\n","protected":false},"author":0,"featured_media":633,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-638","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-non-classe"],"_links":{"self":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/638","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=638"}],"version-history":[{"count":10,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/638\/revisions"}],"predecessor-version":[{"id":649,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/638\/revisions\/649"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/media\/633"}],"wp:attachment":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=638"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=638"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=638"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}