microduck/robotctl/Cargo.toml
Pierre Rouanet 04b885c60d monitor: joints, and a frame instead of a scroll
`robotctl monitor` printed one line per tick and no joint angles at all. The two
gaps are the same gap: fifteen measured angles beside fifteen commanded ones does
not fit on a line, and a line per tick at 10 Hz is not something a person reads —
it is something they scroll past looking for the moment it went wrong.

So on a terminal it is now one frame that repaints in place: the policy driving the
tick, the fall verdict beside the gravity vector it came from, requested against
applied with the limits that clamped it, and every joint measured against its
target with the signed error as a bar. The bar is the point — a servo not keeping
up, a leg holding a load, a policy asking for what the joint cannot do are all a
column of plausible numbers and an obviously off-centre bar.

Under it, the achieved loop rate as a trace. The instantaneous number cannot show
a dropout: a loop that fell to 20 Hz for half a second reads 50 Hz by the time
anyone looks, and a robot that stutters every few seconds was invisible.

Piped or redirected, it prints exactly what it printed before — `> run.log` and
`| grep` must keep working, and a screen-painting CLI that writes escape codes into
a log file is a CLI nobody can script. `--json` is untouched and is where the joint
vectors go for anything downstream.

Three things this deliberately does not do quietly:

  - **Nothing is hidden without saying so.** A terminal too short for fifteen
    joints reports which ones it is showing and scrolls (`↑↓`, `PgUp/PgDn`). A
    table that stops at the last row that happened to fit is half a leg presented
    as a whole robot.
  - **A frozen frame says it is frozen.** No state for five periods and the header
    counts up `STALLED 1.8s`. A live view whose numbers have quietly stopped
    changing is worse than no view.
  - **The trace has a fixed baseline** — the best rate seen this session, not the
    tallest bar on screen. An auto-scaled trace redraws its own zero as the window
    slides, so a loop running uniformly at half speed draws like a healthy one.

ratatui rather than hand-rolled escape codes, because the parts that go wrong are
the parts already solved: restoring the terminal when the process panics mid-frame,
a resize during a redraw, clipping a line to a width nobody checked. Terminal only
— no http, no crypto, no async runtime — so robotctl's rule about staying off the
update engine's dependency tree still holds, and it cross-checks clean for aarch64.

`JOINT_NAMES` moves to `duck-ipc-proto`, which `duck-control` now re-exports. The
joint order *is* protocol: `joints` and `targets` go over the wire as bare arrays,
so a client that cannot name index 3 cannot display them. One table, asserted
against `NUM_JOINTS`, rather than two that must be kept in step by hand. No binary
gains a dependency from the new edge — `robotd` already links both crates.

The stream is read on its own thread: the socket read blocks, terminal events do
not arrive on the socket, and a UI that notices a keystroke only when the robot
sends a frame stops responding at exactly the moment the robot does.

Assisted-by: Claude:claude-opus-5
2026-08-05 11:35:59 +02:00

39 lines
1.8 KiB
TOML

[package]
name = "robotctl"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Local CLI for the robot — currently the `update` namespace only"
# Its own crate rather than a binary of `updater`, because its role is broader:
# it will front `robotd` and other services too. Today it only implements
# `robotctl update …`.
#
# Depends on `duck-ipc-proto` alone, not on `updater`. That was the point of extracting the
# protocol: a support tool on the recovery path should not link the update engine's
# http/tar/zstd/crypto tree, and structurally cannot now reach into engine internals
# instead of going through the socket.
[dependencies]
duck-ipc-proto = { path = "../duck-ipc-proto" }
semver.workspace = true
clap = { version = "4.6.4", features = ["derive"] }
# Shell completions, generated from the same `Cli` the parser uses, so they cannot
# describe commands this binary does not have. Pure codegen over clap's own model —
# no runtime, and nothing on the recovery path depends on it.
clap_complete = "4.6.4"
serde_json.workspace = true
serde.workspace = true
libc = "0.2.189"
# `robotctl monitor`'s live view, and nothing else. Terminal only — no http, no crypto, no
# async runtime, so the rule this file opens with still holds: none of the update engine's
# tree comes in with it. Default features off because the widget set we do not draw (calendar,
# canvas) is not worth compiling, and `crossterm` is the one backend that matters.
#
# Not hand-rolled escape codes: the parts that go wrong are the parts a crate has already
# solved — restoring the terminal when the process panics mid-frame, a resize during a
# redraw, and clipping a line to a width nobody checked.
ratatui = { version = "0.30.2", default-features = false, features = [
"crossterm",
"layout-cache",
] }