YouDuck.ai

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 sockettof.stream on tofd, raw input on paddnever 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”.