Microduck system architecture
Last updated
The robot’s software is Rust, one workspace, no framework. systemd is the supervisor: lifecycle, restart-on-crash, ordering, watchdog.
The value of this page is diagnostic: knowing who owns a symptom tells you whose journal to read.
What each daemon owns
| Service | Owns | Notes |
|---|---|---|
robotd |
Motor control, kinematics, odometry, gait policies, sensor loop, safety | The near-real-time core, authoritative on anything that can hurt the robot |
mediad |
Camera and mic, encoding, perception, WebRTC and the remote gateway | Heaviest service, and the remote API front door |
updaterd |
The update engine | Installs signed releases, rolls back when a robot comes up unhealthy |
configd |
Wifi, robot identity, power, gamepad pairing | |
btd |
The BLE GATT server | A transport adapter only — it owns no state |
padd |
Reading the gamepad | Deliberately an unprivileged client |
tofd |
The head ToF sensor, an 8×8 depth matrix on the HAT’s I²C bus | Owns one sensor and publishes frames; reads nothing |
Why the splits fall where they do
None of these are arbitrary.
mediad is split from robotd so a media or perception crash cannot take out motor control.
tofd is the same rule on a smaller sensor, and the specifics make the case:
- Bringing up a VL53L5/8CX uploads about 90 KB of firmware over I²C, taking seconds
- That bus is shared with the audio codec
- Most ducks have no sensor fitted at all
A retry loop for that does not belong in the process that owns the motors. Nothing in the control loop reads depth, so nothing is lost by moving it out.
It is also deliberately not part of mediad: depth is a sensor on a bus, not a media pipeline, and it is useful long before there is a camera to annotate.
Gamepad pairing sits in configd rather than padd because bonding a device needs root and BlueZ, while padd is deliberately an unprivileged client.
Config is its own business because it must be reachable when robotd is dead, and btd must own nothing — so it belongs to neither.
How they talk
Control plane: JSON-RPC 2.0 over Unix sockets
One property matters to anyone using the robot:
Every client — the phone app, the console, the gamepad, your own script — sends exactly the same calls.
Whatever the official app can do, a script can do. There is no private interface.
The data plane is separate
Sensor streams do not travel over the control plane. Consumers subscribe on the owning daemon’s own socket — tof.stream on tofd, raw input on padd — never relayed through robotd.
tofd publishes the sensor’s own view and does not pretend to geometry it cannot compute. Reprojecting a frame into the robot’s frame means combining it with joint state from robot.state and the head forward kinematics in the kinematics crate.
Version and health are two questions
The design docs devote a section to this, and it is worth remembering:
“What is running” and “what is installed” are different questions. That is why robotctl version reports both and warns when they disagree — a daemon serving old code after an update looks exactly like the fix you just shipped not working.
Health is one question, so it is one command. robotctl health puts hardware and software in one report rather than making you poll seven processes.
Odometry is not a service
It is a struct in the control loop, for a direct reason: its inputs are exactly the sample the loop already read.
That also explains the track drawn in robotctl monitor: it comes from foot contacts and the IMU, with no magnetometer, so it is relative motion and it drifts. It answers “did it walk in a circle”, not “where is it”.