Pierre Rouanet a26ca45bcb
Some checks failed
release / decide (push) Has been cancelled
release / staging (push) Has been cancelled
release / promote (push) Has been cancelled
release / stable (push) Has been cancelled
Merge pull request #118 from pollen-robotics/pause-btd-without-privacy-device
setup-board: pause btd without imposing Privacy = device
2026-08-20 11:40:12 +02:00
2026-08-20 10:34:40 +02:00
2026-08-20 10:34:40 +02:00
2026-08-20 10:52:28 +02:00
2026-08-20 10:52:28 +02:00

microduck daemon

The software that runs on the robot, and the machinery that ships it there.

robotctl is how you talk to a robot. It runs on the robot itself.

Every command describes itself, so you can explore rather than read:

robotctl --help
robotctl update --help

Tab completion comes with the install — press Tab for the commands a release actually has. For a shell it did not set up:

eval "$(robotctl completions zsh)"

Is it alive

robotctl health
robot     healthy
  loop      50.1 of 50.0 Hz · 2834 ticks · 0 missed · last 13 ms ago
  bus       ok
  imu       ready
  battery   7.62 V (64%)
  motors    41 °C max (left_knee) · 36 °C mean
  cpu       52 °C

software
  updaterd  0.5.0 (rev abc1234)
  robotd    0.5.0 (rev abc1234)
  configd   0.5.0 (rev abc1234)
  daemon    0.5.0 installed
            last update 0.4.1 → 0.5.0: applied

units
  updaterd  active · 0.5.0 (rev abc1234)
  robotd    active · 0.5.0 (rev abc1234)
  configd   active · 0.5.0 (rev abc1234)
  btd       active · 0.5.0 (rev abc1234)
  padd      active · 0.5.0 (rev abc1234)

The two software blocks answer different questions. software is what each daemon that serves a socket says about itself, and what is installed on the board; units is what systemd says about every unit a release manages — including btd and padd, which answer no version query and can only be reported from outside — with the release each process was actually launched from. That second one is the question after an update: a daemon still running the old binary shows a release older than the installed one, and the report names the restart that fixes it.

Drive it

Switch the pad on and drive — padd runs from boot, waits for a pad and drives whatever connects.

Pairing is once per pad and lives in docs/robot/pair-a-gamepad.md: sudo robotctl pad pair with the pad in pairing mode, plus what to do when it will not bond.

robotctl pad status
pad     Xbox Wireless Controller 78:86:2E:BB:13:28  connected
padd    active — driving whatever pad connects

padd reads the pad and sends intents over the socket. It has no privileged access — it is an ordinary client, sending exactly what the app and the SDK will send, which is why pairing is configd's job rather than its own.

Driving from your laptop works too, with the socket forwarded — pad in your hands, robot on the bench, nothing installed. Stop the one on the robot first, or two processes fight over the sticks:

sudo systemctl stop padd
ssh -L /tmp/robotd.sock:/run/robotd.sock radxa@192.168.1.42

Leave that open, and in another terminal from this clone:

cargo run -p padd -- --socket /tmp/robotd.sock

The controls:

Start enable / disable the policy — nothing moves until this is on. On a limp robot this is also what powers the joints: torque on, two seconds to the home pose, then it drives
Y / triangle switch between driving the body and posing the head
B / circle stop
left stick body: forward/back and strafe · head: neck pitch and roll
right stick body: turn · head: head pitch and yaw

Two things worth knowing before the robot surprises you. Sticks drive the body or the head, never both, so switching to head mode zeroes the body velocity rather than leaving it walking. And if the pad disconnects, padd sends nothing at all — robotd's deadman stops the robot on its own, which is the wanted behaviour and the reason padd does not invent a zero command.

The first Start after power-on moves the robot: the joints go from wherever they are resting to the home pose over two seconds. Hold it, or have it on its stand.

The same two steps by hand, for when there is no pad in the room:

sudo robotctl robot init
sudo robotctl robot relax --yes

init powers the joints and ramps to the home pose; relax cuts power, and the robot collapses if nothing is holding it. That is the only way back to limp short of pulling the plug — pressing Start again stops the policy but keeps the robot standing.

A robot the IMU already considers fallen refuses both Start and robot init: the fall gate holds a fallen robot limp on purpose. Stand it up by hand first.

Speeds are conservative by default. --max-linear (m/s), --max-angular (rad/s) and --max-head (radians) raise them; --deadzone is there because analogue sticks rarely rest at exactly zero and the robot creeps without it. The unit runs with the defaults, so to use those flags on the robot, stop it and run the binary yourself:

sudo systemctl stop padd
sudo -u padd /opt/robot/daemon/current/bin/padd --max-linear 0.25

systemctl start padd puts the default back.

Watch what it is doing

robotctl monitor

The one window into the control loop. It shows what a client asked for beside what was actually applied, and names the reason when they differ — safety clamps things constantly, and "the stick is forward and the robot is still" is unreadable without that. A limit is spelled out rather than named: deadman — no intent arrived recently, velocity zeroed.

Also on the frame: every joint measured against what it was commanded, the IMU's projected gravity and the fall verdict drawn from it, and the achieved loop rate as a trace so a stutter that has already recovered is still visible. The bottom border names the policy that is loaded, because walk is a mode two releases with different gaits both report — and "which network is this?" is the first question when comparing them.

p opens the gamepad's raw input: every evdev report from the pad padd is driving from, the sticks and buttons as the kernel delivered them, and the gaps between reports as a trace. That last part is the reason it exists — padd resends the last stick value at 50 Hz, so a radio that has stopped delivering still looks like a live driver in the asked column above it, and the robot walks on a command nobody is giving. Reading it is in pair a gamepad.

q quits, / scroll the joint list, u switches the angles between degrees and radians. Angles are degrees on screen — joints, head and the yaw rate. Redirected or piped it prints one line per tick instead, so > run.log and | grep FALLEN behave, and those numbers stay radians whatever the screen is set to. The joint vectors are in --json:

robotctl monitor --json --hz 50 > run.jsonl

Run your own policy

You do not need to cut a release to try a network. Point robotd at your own .onnx on the board, in /etc/robot/robotd.toml:

[policy]
walk = "/home/radxa/my_walking.onnx"
stand = "/home/radxa/my_stand.onnx"
sudo systemctl restart robotd

Your paths survive updates. Delete the lines to go back to the policy the release ships.

A policy that could not be loaded reports unhealthyrobotctl health and the bottom of monitor both name the reason. The shape a policy has to have, and what else is checked at load, are in docs/design/robotd-design.md §5.3.

Keep it up to date

What each daemon is running, and what is installed:

robotctl version

Install the latest release:

sudo robotctl update apply daemon

Go back if it misbehaves:

sudo robotctl update rollback daemon

daemon covers every binary. On a dev board this installs the latest stable release, which is usually a downgrade — use --ref below instead.

A release candidate — published, not yet promoted — is flagged as a prerelease, and a plain apply skips those so no robot drifts onto a build nobody has validated. Ask for one by name:

sudo robotctl update apply --staging daemon

To pick a candidate rather than the newest one:

sudo robotctl update apply --staging --version 0.5.1 daemon

The flag applies to that one command and leaves nothing switched on, so the next apply is back on stable. This is what a canary robot runs before a promotion.

Put your branch on the robot

Make sure the board has been through the dev install first — a board that has not will refuse branch builds.

Push your branch, then wait for CI to build it:

gh run list --branch my-branch

Once it is green, on the robot:

sudo robotctl update apply --ref my-branch daemon
robotctl version

Go back:

sudo robotctl update rollback daemon

Every push needs the apply again.

Or skip CI entirely

From a clone of this repo, with the board reachable over ssh:

scripts/dev-push.sh radxa@<board>

It builds here, signs with the dev key, copies the release to the board and applies it — same verification, same health gate, same auto-rollback, about a minute instead of a push and a CI run. Setup and the one-time first push are in dev-push.md.

Cut a release

Releases are built and signed in CI, never on a laptop. Two tags: the pre-release goes to a canary robot, and the release promotes exactly those bytes.

Bump version under [workspace.package] in Cargo.toml first — 0.5.0 to 0.5.1 here. xtask package refuses a tag that disagrees with it, which is what stops a robot reporting a version it is not running. Then refresh the lockfile:

cargo update --workspace

Merge that, then from main:

git tag daemon-staging-v0.5.1 && git push --tags

CI builds it, signs it, verifies it through the real update engine and publishes it as a prerelease. Watch it:

gh run list --workflow release

Once it is green, on the canary robot:

sudo robotctl update apply --staging daemon

Drive it. When it holds up, from main again:

git tag daemon-v0.5.1 && git push --tags

That promotes the staging build rather than rebuilding it — the same bytes, re-signed with the key customer robots trust. Creating the release in the GitHub UI instead of pushing the tag does the same thing.

A daemon-v tag with no staging build behind it is allowed and builds directly. Both are signed the same way, so the difference is validation rather than authenticity, and the release notes say which one happened. Key custody and the promote workflow — including min_supported, for forcing robots off a bad release — are in CONTRIBUTING.md and docs/project/ci-setup.md.

Where next

Going further with a robot

Cheat sheet Every robotctl command: wifi, updates, rollback, pinning, logs.
Dev board cheat sheet Branch builds, release candidates, and the restart traps after an update.
duck-btctl cheat sheet Every duck-btctl command: the robot over Bluetooth, from a laptop, with no network.
Setting up a dev board From nothing, and the fix when --ref is refused.
How it works The control loop, the update engine, the service split, the BLE path.

Contributing

CONTRIBUTING.md Building, testing, repo layout, conventions, releasing.
Roadmap What works today, what is next, and what we are deliberately not doing.
Open problems Records of what has gone wrong and what would close it.
Description
Mirror of pollen-robotics/microduck with full Git history
Readme 26 MiB
Languages
Rust 84.5%
Shell 11.9%
C 1.9%
HTML 1.2%
Python 0.4%