Build log · Embedded Linux & ownership

Taking a Roborock S7 off the cloud, permanently

A robot vacuum builds a floor plan of your home and uploads it. Rooting the S7 stops that: FEL mode boots a live image that patches the rootfs to open SSH, a Dustbuilder firmware makes it stick, and Valetudo replaces the vendor app with a local web interface. After that it is just an ARM Linux box that happens to vacuum — and the interesting engineering starts.

Roborock S7FEL modesunxi-tools DustbuilderValetudoSquashFS bind mountsDropbearMQTT

01 — Premise

Why root a vacuum

A modern robot vacuum has a LiDAR turret, a camera on some models, and a complete, continuously-updated floor plan of the inside of your house. All of that goes to a vendor cloud, and the app you control it with is a thin client over that cloud. If the vendor's servers go away, so does your vacuum's brain.

Rooting replaces that arrangement. Valetudo runs on the robot and exposes a local web interface plus MQTT, so Home Assistant talks to the machine directly on your LAN. No account, no outbound connection, no dependency on anyone's uptime.

The second reason is more honest: once it is rooted, it is an ARM Linux machine with a read-only rootfs, a writable data partition and a boot hook. Everything I learned making changes stick on that filesystem is the genuinely transferable part of this project, and it is most of what is below.

02 — Warning

Read this before you open it

This robot has no recovery partition

There is no safe mode to fall back to. A bad boot script can brick the device outright. The official documentation says so directly, and the stock boot hook file ships with that warning written into it.

A factory reset clears the data partition — which is precisely why every customisation belongs inside the guard condition described in making changes survive a reboot. Put it there and a factory reset disables your changes and recovers the robot. Put it outside and a factory reset recovers nothing.

Not a beginner project

Full disassembly, conformal coating to scrape, a test point to short while the robot boots, and a firmware flash. The Valetudo maintainers say plainly that this method is not suited to beginners and that you should get help from someone experienced if you are not comfortable with Linux and hardware. That is good advice and I am repeating it rather than softening it.

What this page is and is not

This is a build log explaining how the process works and what I learned living on the rooted device. It is not a replacement for the official documentation, which is maintained, versioned per model, and correct in ways a blog post drifts out of. Follow these, in this order:

Check your exact model against the supported-robots matrix first. The S7 is NAND-based and takes the FEL path; other Roborock models root differently, and following the wrong procedure is how people damage hardware.

03 — Mechanism

How FEL rooting works

FEL is a recovery mode built into Allwinner system-on-chips. When the SoC starts and is told not to boot normally, it waits on USB for a host to hand it code to execute. It is a factory provisioning feature — and it is below the level of anything the vendor's firmware could refuse.

Why it takes two passes

The live image does not itself make the robot rooted. It runs in RAM, reaches into the robot's real filesystem, and opens a door — SSH. That door is enough to get in and install a proper rooted firmware built for your specific model, which is what actually persists.

Understanding that split is what makes the process legible instead of magic: pass one gets you a shell, pass two gets you a system.

What you need

  • A Linux machine with sunxi-tools ≥ 1.3
  • A micro USB cable (data, not charge-only — this catches people out)
  • A jumper wire or a paperclip; anything grounded works for the GND side
  • A FEL image and a firmware package built from Dustbuilder for your exact model
  • Screwdrivers, and somewhere to lay out screws in disassembly order
# Debian / Ubuntu
sudo apt-get install sunxi-tools

# confirm the robot appears once it is in FEL mode
lsusb

04 — Procedure

The rooting procedure

Summarised so you know what you are in for. Do the actual run against the official FEL howto, which has the photographs.

  1. Charge the robot fully

    All of this happens on battery power — the robot is off the dock and open on your desk for the duration. Running out of charge partway through a flash is exactly the situation you do not want on a device with no recovery partition.

  2. Build your images first

    On Dustbuilder, select your model and tick Create FEL image. You want both the FEL image and the firmware package downloaded and verified before the robot is in pieces.

  3. Photograph every step of the disassembly

    Not optional advice. Which screw came from where, how the ribbon cables were routed, which connector faced which way. Reassembly is an hour of relief or an hour of guessing, and that is decided entirely by whether you took the photos.

  4. Find TPA17 — and do not confuse it with TP17

    The test point is on the underside of the mainboard. There is also a TP17 nearby. They are different pads and the official guide colour-codes them for exactly this reason — TPA17 is the one marked red.

    The board is conformal-coated against moisture. You have to gently scrape the coating off the pad to make electrical contact. Gently is doing real work in that sentence; you are removing a lacquer, not the pad.

  5. Short TPA17 to ground and power on

    Connect TPA17 to any ground point. Hold it. Press the power button for 3 seconds, then keep TPA17 grounded for about 5 more seconds. Then check lsusb — the SoC should now be enumerating in FEL mode.

  6. Run the live image

    With sunxi-tools, push the FEL image the builder produced. It boots in RAM, patches the robot's rootfs to enable SSH, and finishes.

  7. SSH in and install the firmware

    The robot should now be reachable over the network. Transfer the firmware package from Dustbuilder and run its install.sh. This is the step that makes root permanent.

  8. Reassemble, and set up Valetudo

    Reassemble using your photographs. On first boot, Valetudo provides its own Wi-Fi provisioning flow; from then on it is a web interface on your LAN, and the vendor app is out of the picture entirely.

The Roborock S7 disassembled on a workbench with the mainboard exposed.
Disassembled. Top cover off, mainboard exposed, screws laid out in order. media/roborock/s7-open.jpg
Close-up of the TPA17 test point on the underside of the Roborock mainboard with the conformal coating scraped away.
TPA17. Close macro of the pad with the coating scraped back. The most useful photo on this page. media/roborock/tpa17.jpg
Micro USB cable connected to the robot's mainboard with a jumper on the test point.
FEL connection. Micro USB attached, jumper on the pad, ready to power on. media/roborock/fel-usb.jpg
Terminal output showing sunxi-fel detecting the device and loading the image.
Host terminal. The device enumerating and the image loading. media/roborock/sunxi-fel.png

05 — The system

Anatomy of the rooted robot

Once you are in, the layout matters more than the fact that you got in. Everything about making changes stick follows from one fact: the root filesystem is read-only SquashFS.

The paths that matter
PathWhat it is
/mnt/dataThe writable partition. Everything you add lives here. Cleared by a factory reset
/mnt/reserve/_root.shThe boot hook. Runs on every start. This is your entry point into the running system
/mnt/data/valetudoThe Valetudo binary, launched by the hook
/mnt/data/valetudo_config.jsonValetudo's configuration, passed in by environment variable
/mnt/resources/Vendor resources — sounds and similar. Read-only, but mountable over
/mnt/data/miio/wifi.confProvisioning config the hook rewrites to drop the vendor cloud path
/mnt/data/authorized_keysYour SSH public key, bind-mounted into place at boot

The boot hook is the whole API

You cannot install a service the normal way, because there is nowhere to install it. What you get instead is one shell script that runs at startup, and everything the rooted system does for you happens inside it:

#!/bin/bash
if [[ -f /mnt/data/valetudo ]]; then
    mkdir -p /mnt/data/miio/

    # SSH key: the rootfs is read-only, so the key lives on the data
    # partition and is bind-mounted over both locations Dropbear
    # may consult. Each mount is guarded so rerunning is harmless.
    if [[ -f /mnt/data/authorized_keys ]]; then
        chmod 600 /mnt/data/authorized_keys
        grep -q " /root/.ssh/authorized_keys " /proc/mounts || \
            mount --bind /mnt/data/authorized_keys /root/.ssh/authorized_keys
        grep -q " /etc/dropbear/authorized_keys " /proc/mounts || \
            mount --bind /mnt/data/authorized_keys /etc/dropbear/authorized_keys
    fi

    # Steer provisioning away from the vendor cloud path.
    if grep -q -e "cfg_by=tuya" -e "cfg_by=rriot" /mnt/data/miio/wifi.conf; then
        sed -i "s/cfg_by=tuya/cfg_by=miot/g" /mnt/data/miio/wifi.conf
        sed -i "s/cfg_by=rriot/cfg_by=miot/g" /mnt/data/miio/wifi.conf
    fi

    # Clear cleanup logs so Valetudo can update itself.
    rm -r /mnt/data/rockrobo/rrlog/*REL

    VALETUDO_CONFIG_PATH=/mnt/data/valetudo_config.json /mnt/data/valetudo >> /dev/null 2>&1 &
fi

Three techniques in one short script, and all three generalise well beyond this device.

  • Bind mounts over read-only

    You cannot write to the rootfs, but you can mount something writable on top of a path. The file underneath is untouched; the kernel just resolves the path to yours instead.

  • Guarded, idempotent mounts

    Every mount checks /proc/mounts first. A hook that stacks mounts every run eventually fails in a way that is genuinely annoying to diagnose.

  • Everything inside the guard

    The whole body sits inside if [[ -f /mnt/data/valetudo ]]. Wipe the data partition and the hook becomes a no-op — which is what makes a factory reset a working escape hatch.

06 — Technique

Making changes survive a reboot

This is the part that took me the longest to get right, and it is the part that transfers to any locked-down embedded Linux device.

The two failures everyone hits first

  1. Writing to the rootfs

    You SSH in, edit a file under /mnt/resources, it appears to work, and it is gone after a reboot — or the write is refused outright. SquashFS is read-only by construction. Nothing you put there persists. Your files go under /mnt/data, always.

  2. Mounting over a directory with a partial copy

    A bind mount replaces the directory. If you copy in only the four files you changed and mount that over a folder of eighty, the robot now has four files and seventy-six missing ones. Symptoms are weird and specific — some things work, others silently do nothing.

    Seed from the stock layout first. Copy the entire original directory into your folder, then overlay your replacements on top of that complete set.

# seed the full stock layout, THEN overlay your own files
ssh root@robot "mkdir -p /mnt/data/mypack \
    && cp -a /mnt/resources/audio_default/. /mnt/data/mypack/"

# now copy your replacements over the complete copy
scp -O my_file.ogg root@robot:/mnt/data/mypack/sounds/

The race nobody warns you about

The boot hook runs early. The directory you want to shadow may not be populated yet — mount over it too soon and you have shadowed an empty mount point, which then gets populated underneath your mount where nothing can see it.

The fix is to wait for evidence that the real filesystem is up. Poll for a file you know belongs there:

if [[ -d /mnt/data/mypack ]]; then
    # wait for the stock resources to actually exist
    while [[ ! -f /mnt/resources/audio_default/power_on.ogg ]]; do sleep 1; done
    sleep 1

    # guard, then unmount whatever is there, then mount ours
    if ! grep -q "/mnt/data/mypack on /mnt/resources/audio_default " /proc/mounts; then
        umount /mnt/resources/audio_default 2>/dev/null || true
        mount -o bind /mnt/data/mypack /mnt/resources/audio_default
    fi
fi
Why poll instead of sleep

A fixed sleep 10 is a guess that is either too short on a cold boot or wasted time on a warm one. Waiting on a specific file is a condition rather than a hope, and it is why the extra sleep 1 after the loop is the only arbitrary delay in the script — a small settle margin after the condition is genuinely met.

Iterating without rebooting

Testing a boot hook by rebooting the robot every time is miserable. The same mount commands work live over SSH, so the development loop is: apply the mount by hand, test, adjust, and only reboot at the end to prove the hook itself is correct.

ssh root@robot '
  umount /mnt/resources/audio_default 2>/dev/null || true
  mount -o bind /mnt/data/mypack /mnt/resources/audio_default
  mount | grep audio_default
'

07 — Case study

Replacing every sound the robot makes

The concrete thing I built on top of all this: a complete custom voice pack. Every event sound the robot plays, replaced with clips cut from a track, built by a script and deployed with one command.

The build pipeline

  • A JSON map of timestamps to output filenames — the whole design lives in one editable file
  • A Python builder cutting each clip, normalising level, and encoding to OGG
  • Local MP3 previews written first, so I can listen before anything touches the robot
  • A deploy script that seeds the stock layout, overlays the pack, applies the bind mounts live, and reports the file count

The preview step is the one worth stealing. Cutting audio by timestamp is iterative — a clip is a quarter second late, or clips a word. Being able to hear the result locally turns a fifteen-minute round trip into a two-second one.

What I learned mapping events

Not every event you can see in the interface has a sound behind it. Changing fan speed in Valetudo produces no audio at all — the robot simply does not play anything for that transition. So the four fan-speed clips I had cut got remapped onto the clean-mode starts, which do fire.

That is the general shape of working on someone else's embedded system: the interface implies a set of events, and the firmware implements a different, overlapping set. You find the difference by testing each one and writing down which ones actually fired.

Verified event hooks after testing
EventFileFires?
Docked / chargingcharging.oggYes
Water tank insertedwater_box_carriage_in.oggYes
Water tank removedwater_box_carriage_out.oggYes
Clean startedstart.oggYes
Room / zone / spot cleanroom.ogg, zone.ogg, spot.oggYes
Returning to dockhome.oggYes
Fan speed change—No sound exists
Testing without waiting for a clean cycle

Valetudo has a Speaker Test under Robot → Misc. Docking the robot and attaching or removing the mop tank covers three more event paths in about ten seconds. Between those you can verify most of a pack without running a single cleaning job.

The Valetudo web interface showing the robot's live map on the local network.
Valetudo. Local web interface with the live map — no account, no cloud. media/roborock/valetudo-ui.png

08 — Status

Where it stands

Rooted, running Valetudo, no outbound cloud connections, integrated with Home Assistant over MQTT, and playing a full custom sound pack applied by a boot hook that survives reboots and updates.

What I would tell someone starting

  • Read the official docs completely before touching a screwdriver. The whole thing, for your exact model. Not the summary — the actual guide.
  • Photograph the disassembly. The single highest return-on-effort action in the entire project.
  • Keep everything inside the guard condition. It is what makes a factory reset an escape hatch instead of a shrug.
  • Test mounts live over SSH before trusting the hook. Rebooting to test a one-line change is how a Saturday disappears.
  • A device with no recovery partition deserves respect. I read every script twice before it went in the boot path, and I would do that again.
Terminal output from the deploy script showing the sound pack files uploaded and bind mounts applied.
Deploy output. Files uploaded, mounts applied, count verified. media/roborock/soundpack.png