# Cheat sheet `robotctl`, which runs on the robot. Every command here was taken from `--help` on the branch that ships it, not from memory. Read-only commands need no privilege. Anything that **changes** the robot needs `sudo` (or a user in `--allow-user`/`--allow-group` for `configd`, `allow_uids`/`allow_gids` in `updater.toml` for `updaterd`). Branch builds, release candidates and the restart traps after an update are in [`cheatsheet-dev.md`](cheatsheet-dev.md) — they need a dev board. The same robot over Bluetooth from a laptop, with no network and no ssh, is [`duckctl.md`](duckctl.md). ## On the robot — `robotctl` ### The first thing to run ``` robotctl version ``` What every daemon is *running* against what is *installed*, plus warnings when they disagree. Run this before believing any other diagnosis — a daemon serving old code after an update looks exactly like a bug in the fix you just shipped. See "After an update" below. ``` robotctl health ``` Hardware and software in one report. Exits non-zero when the robot is unhealthy or unreachable, so it can gate a script — a hot motor or a pinned component is reported, not judged, and does not affect the exit code. `--json` for a support bundle. ### Watching the loop ``` robotctl monitor ``` What a client asked for beside what was actually applied, with the reason named 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. Projected gravity is the only IMU quantity on this stream — upright is about `[0, 0, -1]`, and it is what `fallen` is decided from. The stale-read counters and the ratios they mean anything against live in `robotctl health`. The last row of the header is the robot's condition rather than its behaviour: the pack's charge in volts and as a fraction, the hottest servo and the board's own temperature. It comes from `robot.health`, polled every two seconds, because none of it is on the state stream — and it is where anything wrong gets named, whether that is `unhealthy: control loop at 43.9 Hz`, `degraded: no robot on the motor bus after 3 attempts` or `orientation frozen — 25 stale reads`. That last one is on this row and nowhere else on the frame: a board that has stopped fusing keeps answering the bus, so nothing errors and the gravity vector above holds a plausible attitude indefinitely. 0% is `BATTERY_EMPTY_V`, which is where `robotd` sits the robot down and cuts power, so the figure is a countdown rather than a gauge — yellow at 30%, red at 15%. A reading that has not been taken says `batt not read yet` instead of `0.00 V`, which is what the first second of an uptime and a bus that cannot answer both look like. The row is drawn even when there is no state at all, and that is the case it matters most in: a board whose servo power is off never completes a control tick, so nothing arrives on the stream and the reason is only on the health answer. The bottom border names the policy that is loaded — the `.onnx` files, and whether a standing network is configured at all — because `walk` is a mode two releases with different gaits both report. A robot with no policy says so, and one whose policy would not load says that instead, which the stream's `held` cannot distinguish. Down the right-hand side, **the robot drawn as it is standing** — the same visual model the policies were trained against, posed by the measured joint angles and tilted by the IMU's gravity vector. A leg folded the wrong way, a head pitched into the floor and a duck lying on its side are all just numbers in the joints table; every one of them is obvious here. It is on by default and appears whenever the terminal is wide enough (about 110 columns — the tables come first, and the robot takes what is spare). **`d`** turns it off; **`[`** and **`]`**, or `←`/`→`, orbit it. Whenever the ToF is delivering frames, what it sees is drawn into the same scene — yellow for a hit, green for floor — depth-tested against the robot's own body, so a point behind the beak is hidden by it. That is what makes "is it seeing my hand, or is it seeing itself" answerable. It needs no key: the points appear when frames arrive and go when they stop. Under it, when the column is tall enough, **a map of where the robot has been**: odometry's track from the foot contacts and the IMU, in braille. The panel never grows — the world zooms out as the track does, so the whole path stays in frame. `+` is where it started, `●` is the robot with a short ray for its heading, screen-up is the heading it booted with. There is no magnetometer, so this is relative motion and it drifts; it answers "did it walk in a circle" and not "where is it". `q` quits; `↑`/`↓` scroll the joint list on a window too short for all of it; `u` switches the angles between degrees and radians; `t` opens the [ToF matrix](#the-tof-sensor-tofd); `d` toggles the robot view and `[` / `]` orbit it; `p` opens the pad's raw input stream — every evdev report from the gamepad, with the gaps between them, which is the only place a stalled radio is visible ([pair a gamepad](pair-a-gamepad.md#when-it-drops-while-you-are-driving)). 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`, which carries the whole state, one object per line: ``` robotctl monitor --json --hz 50 > run.jsonl ``` ### Configuring the robot ``` sudo robotctl configure ``` What has been changed on this robot, and nothing else: ``` robotctl configure --list ``` A robot nobody has touched prints nothing — that is the answer. Add `--json` for a support bundle. It needs no root and no terminal, which is the point: it is the first question to ask about a robot behaving oddly, and until now answering it meant a full-screen editor over ssh. An interactive editor over `/etc/robot/robotd.toml`: every key the daemons know, the feature switches first (policy on/off, walk/roller, limp-fall, audio, pet detection, battery shutdown, camera and video quality…), current value against default, one line of doc. SPACE toggles, ENTER types a value, `u` reverts a key to its default. Values in yellow (marked `•`) are the keys where this robot diverges from the defaults; everything else is the built-in default, and `unset` optionals show what they resolve to `(auto)`. Three properties worth trusting: - **It cannot disagree with the daemon.** The schema, the defaults and the validation come from the same crate `robotd` parses the file with, and the key list is pinned complete by a test — a new `[section]` in the daemon shows up here or the build fails. - **It cannot eat your file.** Comments, ordering and keys from other releases survive untouched; only the keys you change are written. Reverting a key removes it (and the comment attached to it) rather than pinning the default, so the file stays a list of *decisions*, not a copy of the defaults. - **It cannot write a file robotd refuses to start on.** Every save is validated through the daemon's own loader first, atomically (temp file + rename), and rejected with the reason. The daemons read the file once at startup, so saving offers a restart — of the ones that read what you changed: `[media]` is `mediad`, everything else is `robotd`. `sudo`, because the file is root-owned — without it the editor opens read-only and says so on the first write. `--file` points it elsewhere for a bench copy. The shipped `deploy/robotd.toml` stays the reference for *why* each knob exists; this is for flipping them. #### Video quality ``` sudo robotctl configure ``` Set `media.quality` — `1080p30`, `720p30`, `720p15` or `360p30` — and take the restart it offers. `media.camera` off streams a test pattern instead, which is what a board with no camera wants: the WebRTC *control* channel rides on the video track, so a pipeline that cannot start costs both. `media.bitrate` follows the quality unless you set it; the unit is bits per second. `media.congestion_control` is the other knob in that section, and it is the one that moves CPU: `disabled` drops the bandwidth estimator, which is the largest single consumer in `mediad` (7.6% of a core against capture's 0.3%), and makes `media.bitrate` the rate rather than a starting point. It costs adaptivity — on a link that degrades, the picture stalls instead of the rate falling. 720p30 is the rung the pipeline was measured at; a rung that does not hold runs slower rather than failing. `robotctl monitor` reports the achieved rate on the bottom border, in yellow with `of ` beside it when it is under 90% of what was asked for. What was applied: ``` journalctl -u mediad -b | grep streaming ``` ### Policies and skills A **slot** is what the robot runs by default — the walking gait, the standing network. A **skill** is what it runs when asked: a kick, the roulade, a bow. Both are files on the Hub, both change without a daemon release, and nothing below needs a restart. What this robot is running right now: ``` robotctl policy list ``` Two tables: the seven slots, then the skills — what runs by default, and what runs when asked. ```text SKILL RUNS FOR POLICY kick_left 0.5 s ball_kick_left.onnx roulade 1 s roulade.onnx * polite-bow 4 s fffiloni/microduck-polite-bow-b1d864/main/policy.onnx ground_pick — driven by the robot itself sit_toggle — driven by the robot itself ``` A `*` marks every row config has an opinion about, with a count at the bottom. A slot you switched off says `switched off` rather than looking like one the robot never had. The directory comes off the path because the `ORIGIN` column already says which one it was. `ground_pick` and `sit_toggle` have no length because the robot drives them itself — they answer to `robot do` like the rest, but they are not entries you can change. #### A newer official set The set the robot walks with lives on the Hub and versions on its own line: ``` robotctl policy check ``` ``` sudo robotctl policy update ``` `check` prints what is installed, what is newest, and what else the repo offers; it changes nothing and says so plainly when the Hub cannot be reached. `update` takes the newest unless you name one — `--version v1` is how to go back. The robot returns to its home pose, re-reads every slot and drives again, and **a slot you loaded yourself is left alone**, because it points somewhere else entirely. #### Trying your own file No release, no file to edit, no restart: ``` sudo robotctl policy load walk /home/radxa/my_walking.onnx ``` If the robot is walking, it goes to its home pose, loads it, and drives again. If it is doing something else — sitting, standing still — nothing moves: the network being replaced is not the one running, and the swap happens underneath. `sudo robotctl policy reset walk` puts that slot back; with no slot, it puts all seven back. #### Trying somebody else's Other people publish policies for this robot: ``` robotctl policy search microduck ``` ``` sudo robotctl policy load walk RemiFabre/microduck-flamingo-cycle ``` The repo name is enough — it is fetched and loaded in one step. Add `@v2` for a revision and `:policy.onnx` for a repo carrying more than one. `policy list` marks it `community`. **Nothing a stranger publishes is verified by anybody.** What makes it safe to try is the joint clamps, the fall reflex and the shape gate — not the description. Have the robot on its stand the first time. A policy whose manifest says it will not run here — wrong observation width, a newer daemon, a different robot — is refused before it downloads. One with no manifest is accepted and checked the usual way, at load. #### Adding a skill A policy in the walk slot replaces the gait. A policy added as a *skill* sits alongside it and runs when asked, which is what most published one-shots want: ``` sudo robotctl policy add polite-bow fffiloni/microduck-polite-bow-b1d864 ``` ``` robotctl robot do polite-bow ``` The length comes from the repo's manifest. A policy that holds until told otherwise — a one-footed stand, say — has no length of its own, so give it one, and the twist it reads: ``` sudo robotctl policy add flamingo RemiFabre/microduck-flamingo-cycle --hold 5 --command 1,1,0 ``` `--command` is what the network is fed while it runs. Most skills need none: they are trained on an all-zero command and being selected *is* the trigger. A policy that reads its twist as something else — flamingo's is `[flag, side, 0]` — needs it spelled out, and its README says what the slots mean. `sudo robotctl policy remove ` takes one out. A skill this robot's release ships comes back when you do, since removing the entry only removes the override. The robot must be driving for a skill to run — press **Start** on the pad first, or the request is refused saying so. #### Putting a skill on a button ``` robotctl pad bindings ``` ```text a ground_pick x roulade lb kick_left rb kick_right dpad_down sit_toggle `robotctl pad bind