Projects / Robot Dog

Robot Dog

Teaching a chat assistant to sit, speak and follow you outside — on a robot that was never meant to let us in.

Active Started July 2026 · Last update 5 September 2026

Messaging your agents via WhatsApp is so passé. What if your agent could follow you around, listen and speak to you, and perform a variety of adorable tricks? Or alternatively, achieve sentience and burn down your flat?

This is the story of how we decided to turn Wheatley into a literal dogsbody.

What this is

Wheatley began as a text assistant and nothing more: an OpenClaw-hosted agent running on an ASUS NUC, wired to the home network by an Ethernet cable, reachable over WhatsApp, Discord and Telegram. Extremely good at your calendar. Completely incapable of following you into the next room. Getting from that to something that trots after you meant solving four separate problems — a body for it to inhabit, a brain that was mobile and wireless instead of bolted to a desk by a network cable, ears to pick a command out of a room, and a voice to answer you with.

The body is a Unitree Go2 Pro: a four-legged robot dog, roughly the size of a border collie, sold as a consumer product rather than as research equipment. The brain is a Raspberry Pi that carries the assistant and speaks to the robot over Wi-Fi. The ears are a USB microphone plugged into that Pi, and the voice comes out of a speaker beside it.

Put together, it works like this. Say “Wheatley, sit” out loud. The microphone hears it, a speech model running entirely on the Pi — no internet involved — matches the words against a short, fixed list of permitted commands, and a safety layer decides whether the robot is allowed to do it. Nothing moves that isn’t on that list, and the part that can move the robot never touches the cloud.

That is only half of it, and the quieter half. You can also just ask it things — what the weather is doing, what is on the calendar tomorrow, anything you would otherwise have typed into a chat window — and the question goes to the assistant itself, which answers you out loud, in a West Country accent. The robot is the body. The thing you are actually talking to is the same assistant that already runs the household.

The hard part was never the tricks. It was that the Go2 Pro is a sealed appliance — you cannot log into it, you cannot run your own code on it, and the manufacturer’s official toolkit only works on the pricier education model. Nearly every robot-dog integration published online takes for granted that you own that one. This log is the route around it, in order, including the wrong turns.

HardwareSoftware
Body Unitree Go2 Pro — consumer tier, stock firmware App-layer WebRTC through a reverse-engineered community driver — the only control channel this tier exposes
Brain Raspberry Pi 5, 8GB OpenClaw, running the Wheatley agent
Power & storage Geekworm X1208 — a 21700 cell as UPS with an M.2 socket on the same board; 1 TB NVMe Boots from a 64 GB microSD card; root filesystem on the 1 TB NVMe SSD
Ears USB microphone, with hardware gain control Offline speech recognition, grammar-locked to a fixed list of commands
Voice USB speaker, the same unit Hosted neural voice, with a local synthesiser as offline fallback
Safety — Stationary-trick allowlist, per-trick confirmation, a collapse-to-limp panic key, motion disarmed on every boot

Updates

27 July 2026

The journey begins, and nearly immediately ends

Problem one: the body. The robot was bought at the end of July, and before a line of code got written, a week went into establishing what could actually be done with one, and the answer was mostly bad news.

You cannot log into a Go2 Pro. You cannot run your own program on it. Unitree’s official development kit — the thing every tutorial assumes you have — is restricted to the education model, and every published community integration but one is built on that kit. So essentially none of the impressive robot-dog demos you can find online will run on this machine. Nothing on the box says so, and it is an easy fortnight to lose.

That leaves exactly one way in: the connection the Unitree phone app itself uses to drive the robot. It is a WebRTC link — the same technology as a video call — which the community has reverse-engineered into a driver. Undocumented, unsupported, and the foundation everything below is built on.

But we weren’t going to let that stop us. It was hacking time.

3 August 2026

First contact

The first script we wrote talked to the robot, and the robot answered.

That is what “first contact” means here: our own Python code, running on a desktop PC, opened that WebRTC connection to the dog, got through its encryption handshake, subscribed to its telemetry, took a photograph through its camera and hung up cleanly. Not the vendor’s app driving the robot — our code, for the first time. Until that ran, every plan on paper was theoretical. What the probe turned up:

  • the camera works — a real 1280×720 frame, well exposed indoors at night;
  • the front lamp is a controllable headlight, and it really does light the scene: mean image brightness went from 45.7 with the lamp off to 92.1 at full — worth having for evening photos;
  • multicast discovery fails from a wired host to a wireless robot, because access points frequently don’t bridge multicast. Always pass the address;
  • standing still costs about as much battery as you’d fear. Three short sessions took it from 85% to 81%; the quoted endurance is burned by standing, not just walking.

And the one everything else rests on: without explicitly awaiting the disconnect, the WebRTC library hangs the process forever. There is exactly one connection slot on this robot — fail to release it and the phone app can never connect again.

5 August 2026

First steps, and a surprisingly good speaker

Reading telemetry is one thing. Telling fifteen kilograms of legged machine to move is another, and this was the first time our code did it.

The safety design came before the fun. The list of permitted tricks holds only stationary ones — sit, stretch, wave, lie down; things that do not travel across the floor at anybody — each needs a typed confirmation before it fires, and one key drops the robot limp instantly with no confirmation at all. Thirteen commands across three sessions: zero failures, zero panic stops, every disconnect clean.

Worth knowing before you try this: the trick names come from a third-party driver table, not from the manufacturer’s documentation, so what any given one does is unknown until it has been read in primary documentation or watched on this unit. A wave, it turns out, lifts a paw. That rule now governs every new trick — real clearance, hand on the panic key, and anything doubtful gets previewed in the vendor’s own app first.

The same evening turned up something better than expected — the robot’s speaker is very good. It now greets people in the voice of a Portal 2 character.

9 August 2026

Ordering the parts

Problem two: the brain. The assistant was living on a NUC plugged into the wall by an Ethernet cable — perfectly good for something you message, useless for something that follows you down the road. What it needed was a computer small enough to eventually ride on the robot’s own back, and a battery to run it from.

So: a Raspberry Pi 5 8GB kit — board, active cooler, 27 W supply, case — plus a Geekworm X1208, which is a board that carries a 21700 cell as a UPS and an M.2 NVMe socket at the same time.

That combination matters more than the spec sheet makes it sound. The plan had been a separate battery board and a separate M.2 HAT, stacked on top of each other on top of the Pi; the X1208 collapses the two into one, which is one less thing in the tower and one less thing to work loose on the back of a walking robot. Because it takes full-length 2280 drives, it also opens up the cheap, common SSDs rather than the short ones. The catch is the input: it wants a genuine 5 V/5 A USB-C supply, which the kit’s own PSU provides and a drawer full of phone chargers does not.

Deferred at this point: the saddle to mount it all, and a cellular modem so the robot stops depending on a phone hotspot.

21 August 2026

If I only had a brain

The ASUS NUC that had been hosting the assistant died — the day after those parts were ordered. So the Pi bought to eventually ride on the robot’s back was drafted early, and took over both jobs at once: running Wheatley for the household, and driving the robot.

It was also the first time any of this code had run on ARM — a different processor architecture from the desktop it had all been written on, which is where you discover which of your dependencies were only ever tested on Intel. One predicted problem evaporated: the audio library everyone expects to fight compiled cleanly. An unpredicted one took its place, a sound-handling module dropped from the newer Python this machine ships with, which had to be restored from a backport package before anything would start at all.

23 August 2026

The loop closes — and the robot apocalypse begins

This was the day the pieces met. A command spoken out loud, heard by the microphone on the Pi, recognised, sent to the robot as a trick, with a sound played back to acknowledge it. Voice in, motion out, answer back — the whole chain running at once for the first time, with nobody touching a keyboard.

Closing that loop needs one instruction that sounds absurd written down: do not listen to yourself. The microphone and the speaker sit inches apart on the same USB device, so unless the service throws away everything it hears while it is talking, it transcribes its own replies and answers them. One line of code stands between this project and a robot holding a conversation with itself in an empty room.

And then, while we were testing this, something on the robot said “I’m here.” In a voice that is not ours, out of its own speaker, followed by a sharp stand-up.

Were we about to witness the opening shots of the Robot Wars? After evacuating my family to a secure location, we continued.

For a few minutes afterwards the robot appeared to be ignoring everything said to it. The logs rule that out: every phrase spoken in those minutes was malformed — not on the permitted list — so our own software had rejected them all before anything reached the robot. It never refused a command, because it was never sent one.

What the logs cannot settle is the stand-up. Our own code had issued one at almost exactly that moment, after the command had failed to register several times in a row — and a command that finally lands after a few apparent no-ops looks abrupt and unprovoked. So whether anything other than us moved the robot is genuinely unknown.

The voice is the part that could not be explained away. We ship no speech synthesis on the robot and no clip in the sound library says those words, so something onboard had spoken, and nothing we had written could have made it do so.

It was eventually solved, and the answer was mundane. The Go2 has a rudimentary voice-command system of its own, built into the firmware and entirely separate from anything we made — and it had somehow been activated by accident. It is now switched off, for good, and the ascendancy of our robot overlords is delayed until early next year.

25 August 2026

Your ears must be burning

Problem three: hearing. A USB microphone went into the Pi, and the test grew into the listening service the whole project now runs on — one speech model, loaded once, doing three separate jobs:

  • a command tier that is grammar-locked — it is incapable of recognising anything whatsoever except a short fixed list of phrases like “Wheatley, sit”. This is the only path in the entire system that can move the robot. Fed thirty-four seconds of ordinary conversation, it produces nothing at all;
  • a conversation tier — open dictation, passed to the assistant, answered out loud. It cannot reach the robot by construction: the code that moves anything exists only in the first tier;
  • and a socket for transcribing the voice notes people send in over chat.

The bug worth recording: unplugging the USB microphone produced no error whatsoever. The process kept its PID, stayed active, held a dead file handle and logged nothing — a quiet room and a missing microphone were indistinguishable. Restart=on-failure never fires, because there is no failure.

It also cooked the machine. The retry loop pinned a core, and the Pi — fanless, and living in the flat — heated up noticeably with it. That box had already hung once for seventy minutes with no cause ever established, so a process silently burning a whole CPU had my full attention. My wife was already equivocal about the purchase of a robot dog, and burning down the flat was unlikely to improve her opinion.

The fix: the stream is now built on demand, the device resolved by name rather than by the audio layer’s idea of “default”, and a forty-five second silence watchdog rebuilds it. Underneath sat the real gotcha: PortAudio snapshots its device list at initialisation, so a microphone plugged in after the process started is invisible forever, no matter how patiently you retry. Without forcing a re-enumeration, the retry loop is theatre.

25 August 2026

Act natural

A voice loop that works and a voice loop you want to talk to are different things. Two problems separate them: how long the answer takes to arrive, and what it sounds like when it does.

Latency first. Getting a question to the assistant and back was the slow part, and most of that delay turned out to be a program starting up. Asking through the ordinary command-line tool took 11.3 seconds, roughly 3.6 of which was a JavaScript runtime booting before it had even read the question. Talking to the assistant directly, over the protocol its own tools use, brings a turn under three seconds — and, better, streams the reply as it is written, so speech starts on the first finished sentence instead of after the last one.

The other half of the problem is that an assistant built for a chat window writes markdown, and markdown read aloud turns to gravel. Every reply now passes through a function whose only job is to make text sayable: it strips code fences and headers, drops emoji, turns table pipes into commas, and expands the symbols a voice cannot pronounce, so °C becomes “degrees celsius”. The order of those steps matters more than any one of them — strip bare URLs before markdown links, and you leave the wreckage of the link behind for the speaker to read out.

It also refuses to monologue. Past seven hundred characters it cuts the reply at the last full stop, on the grounds that nobody standing in a garden wants the long version.

26 August 2026

Never forget

Unglamorous maintenance on the new brain. The Pi had been running both the household assistant and this project from a 64 GB microSD card, with a SQLite database writing to it continuously. That is a wear-out clock with a countdown on it. So: a 1 TB NVMe SSD, cloned from that microSD inside the desktop rig — a block copy and a partition grow — then carried across to the Pi.

It does not boot from that SSD, and it turns out not to need to. The Pi 5’s bootloader cannot see this particular drive at all: with the microSD pulled it powers up, runs the fan flat out, never flickers the activity LED and never reaches the network. Three attempts, same result every time. It is a known incompatibility with this drive — a slower PCIe 3.0 drive works in the very same board, and no firmware option exists to force the link speed down, so there is nothing to fix and no point retrying.

So the machine now boots from a microSD card and runs everything else from the SSD. The boot partition is written only when the kernel updates; every byte of real churn lands on the drive. Which was always the actual goal — booting from an SSD was never the point, not wearing out a memory card was.

One trap worth passing on: a block clone copies the disk identifier along with everything else, so both drives ended up advertising identical partition IDs. That is not merely confusing, it is dangerous — it silently broke firmware updates, because the update tool was writing to a partition the bootloader never reads. Everything reported success and nothing changed.

27 August 2026

Cutting the cord

Everything so far had happened on the home network, where the robot’s address is fixed and known in advance. Outdoors there is no home network: the robot and the Pi have to find each other on a phone’s hotspot, which hands out new addresses every time. Hardcoding one — exactly what the code had been doing — stops working the moment you leave the house.

So the robot now finds itself. Instead of a hardcoded address, the host sweeps the local network for the robot’s hardware address and caches the last working one per network, so arriving on an unfamiliar hotspot costs a single scan and nothing after that. Proven three ways in one night: wired, then on a phone hotspot, then with the Ethernet cable physically pulled, so even the assistant’s spoken answers were coming over mobile data.

The same evening the robot got a face. There is a lamp on the front of the dog, and it is the only feedback you get while the machine is thinking: yellow the instant the wake word lands, another colour once the words resolve into something it understands, red at a refusal. From across a garden that tells you the difference between “it did not hear me” and “it heard me and said no”.

Two details only turn up with a robot in front of you. A colour set with a duration does not fade back to the colour you had before — when it expires the dog reverts to its own resting green — so the code holds every colour far longer than it needs and overwrites it, rather than letting one lapse. And the yellow carries a timeout of its own, because a wake word that never resolves into a command would otherwise leave the dog staring at you indefinitely.

28 August 2026

First full session outdoors

All four problems working at once, outside, with no laptop and no house involved: the Pi and the robot meeting on a phone hotspot, commands spoken aloud, answers spoken back. Connect, every trick, every routine.

A caveat worth keeping. The Pi’s system journal was on volatile storage, so when the machine hung later that afternoon and needed a power cycle, the log for the entire session went with it. The best evidence that will ever exist for this session is the recollection of the person who was standing next to the robot watching it move. The hang’s root cause was never established, and guessing at one would be worse than leaving it open.

30 August 2026

A better voice, and a bug that ate whole sentences

Problem four was the voice, and this is where it stopped sounding like a machine. Swapped the local speech synthesiser for a hosted neural voice — measured at 0.32 s against 0.46 s on short lines here — and kept the local one loaded at startup, so a dropped uplink degrades to a slightly slower neural voice instead of falling back to something robotic. Volume is now spoken too: “speak up” and “be quiet” move the hardware mixer in ten-percent steps, with a floor at twenty. At zero the confirming beep is inaudible, which leaves no audible way back.

The real win was a defect that had been silently eating replies. There is exactly one audio output, and a second sound played over a live one dies instantly with device or resource busy. It was invisible because that error went to /dev/null and the code logged the clip as played regardless. One lock now serialises every sound the service makes. What made it worth fixing wasn’t the collision itself — it was which side lost: a three-second acknowledgement was swallowing the actual answer that arrived a second behind it.

5 September 2026

Bought armour, got a second radio

Two purchases: a hard metal shell to protect the Pi, and a USB Wi-Fi adapter with an external aerial. We bought them as a pair, and the reason is a chain worth following.

The Pi has to ride on the dog — that is the entire point of it being a small computer with a battery rather than a box on a desk. A bare circuit board bolted to a walking robot gets knocked about, and eventually rained on, so it needs a shell. But a metal shell blocks Wi-Fi, a fact the case manufacturer prints in its own documentation. So we bought a USB Wi-Fi antenna to compensate for the signal degradation.

Then the accident. That adapter is not merely an aerial on a wire. It is a second radio — and a second radio retires the largest constraint this project has ever had.

Until today the Pi had exactly one Wi-Fi radio, and a radio can only be on one network at a time. The robot broadcasts a network of its own; the internet is on a different one. So the Pi could talk to the dog, or reach the assistant that answers questions, but never both at once. Every arrangement used so far has been a way of dodging that. Outdoors it meant putting the robot and the Pi on the same phone hotspot — which works, but makes the control link depend on a phone in somebody’s pocket. Indoors it meant an Ethernet cable, which frees the radio entirely and works perfectly, provided the dog never leaves the house.

With two radios the dodge is simply unnecessary. The built-in radio now belongs to the robot, and the USB one carries the internet.

One radio

Until 5 September

Pi 1 radio OR the dog its own network the internet and the assistant

A radio joins one network at a time. The dog, or the assistant — never both.

Two radios

Since 5 September

Pi built-in + USB aerial built-in USB aerial the dog centimetres the internet tens of metres

Two radios, two networks, at the same time. The long link gets the aerial that sits outside the metal.

The constraint that shaped every outdoor plan, and the accident that removed it.

That split is not arbitrary. The Pi rides on the dog’s back, so the robot is centimetres away and the shell’s interference costs nothing over that distance. The internet link is the long one — a phone in a pocket, tens of metres off, while the dog roams. So the aerial that sits outside the metal goes to the long link, and the built-in radio, shell and all, only has to shout across a few centimetres.

Measured on the same network minutes apart, the gap is not subtle: the built-in radio managed −48 dBm and 48 retries, the adapter −29 dBm and not one. That is roughly eighty times the signal power. The aerial does exactly what we bought it for.

An old bug died along the way. Every saved network now pins to a specific radio by its hardware address rather than by name, because USB numbering is not stable across replugs. That ends a defect which had been open for weeks — the roaming profiles can no longer wander off with the radio the robot needs.

None of which has met an actual dog yet. The robot’s own network has no saved profile, because its name and password only exist once you switch the machine into that mode, and we have never once run that mode here: every outdoor success so far was on a hotspot. There is also a new hazard that could not exist when there was only one radio — the robot’s network uses an address range some phone hotspots also use, and the Pi is now deliberately on both networks at the same time.

What’s next