Repository navigation
usecases
Eric Busboom edited this page Sep 8, 2026
·
1 revision
Actor-by-actor use cases drawn from the canonical design docs, each citing its source section.
Extracted from protocol, motion-api,
diffdrive, wifi-link, the
project README, and
clasi/issues/import-rogo-cli-adapt-robot-radio-to-v6-host.md. Each use
case cites the canonical section(s) it is drawn from rather than
restating their content; see specification for
the consolidated cross-reference index.
Actors
-
Student / classroom programmer — writes a program against the
motion API (
robot.move_x(...), etc.) to drive a physical or simulated robot. -
Host session — the
robot_v6client's reliability layer (Session,src/host/robot_v6/reliability.py) acting on the student's behalf at the wire level; called out separately from the student where the reliability layer's own behavior, not the student's intent, is what the use case is about. -
Developer running the sim server — runs
tools/simto develop or test host-side code with no robot attached. -
Firmware developer — implements or extends a concrete
Adapter(DiffDriveAdapter, a new port, or a new transport) against theProtocolHandlercontract. -
CLI / tooling user — drives a robot through the Rogo CLI (
rogo,src/host/rogo/) rather than writing a Python/JavaScript program directly.
- Actor: Student / classroom programmer
-
Preconditions: A
Session/robot connection is established (HELLOhas completed,STATUSshowsready=1); no motion is latched byestop. -
Main flow:
- Program calls
robot.wheels_v(left, right, duration)(or the equivalentWHEELS_Vwire call directly) —motion-api#3.2. - Client encodes
WHEELS_V <left> <right> <duration> #<id>and sends it —protocol#6,protocol#9.1. - Robot's
ProtocolHandlerdecodes the line, dispatches toDiffDriveAdapter::onWheelsV, which scales bycountsPerLengthand callsDifferentialDrive::drive(velocity, twist, lease)—protocol#5. - Robot replies
ack <id> <lastDone> <reason>. - Wheels hold the commanded ratio through the ramp for
durationms, then the lease expires and the kernel returns to neutral —diffdrive#3.1.
- Program calls
-
Postconditions: Wheels ran at the commanded ratio for the
requested time;
expectedNext_advanced pastid. -
Error flows:
- Speed out of range →
ack(arrived, sequence advances) pluserr 3 #<id>(ERR_RANGE) — a merits rejection, not a decode failure (protocol#8.2). - Line malformed (bad arity/unparseable field) →
nacknaming the same id, no sequence advance, pluserr <code> #<id>—protocol#8.9.
- Speed out of range →
- Actor: Student / classroom programmer
-
Preconditions: Session established; robot at rest or already in
motion (a
move_*supersedes awheels_*hold —motion-api#6). -
Main flow:
- Program calls
robot.move_x(distance, rotation, cruise=0, timeout)—motion-api#3.3. - Below the 50° pivot threshold the motion is one blended
curvature segment; at or above it, the robot pivots to the new
heading first, then travels straight —
motion-api#3.3. - Wire form
MOVE_X <distance> <rotation> <cruise> <timeout> #<id>is sent and acked —motion-api#9.1. -
timeoutacts as the required backstop if encoder travel never reaches the commanded distance —motion-api#3.1(shared withwheels_x).
- Program calls
-
Postconditions: Heading changed by exactly
rotation; position advanced along the resulting curve, or the timeout ended the move. -
Error flows:
-
Current implementation gap: on
DiffDriveAdapterspecifically (no planner),MOVE_Xdecodes and dispatches correctly butonMoveXreturnsResult::kUnknown— the command is acked (wire contract satisfied) but has no kinematic effect. This is documented, intended behavior for this one concrete adapter, not a bug —protocol#5,protocol#9.10item 1. - Timeout backstop fires before displacement is reached → motion ends
with
DoneReason::kTimeout, surfaced on the nextack/nackor telemetry line —protocol#8.8.1.
-
Current implementation gap: on
- Actor: Student / classroom programmer
-
Preconditions: Session established. For
go_to_w, a pose source is available (OTOS if fitted, else encoder odometry) —motion-api#3.6. -
Main flow:
- Program calls
robot.go_to_r(x, y, speed, arrive, timeout)(robot-frame) orgo_to_w(x, y, speed, arrive, timeout)(world-frame, which reads pose and transforms intogo_to_rfirst) —motion-api#3.5,motion-api#3.6. - The robot solves a constant-curvature arc tangent to current heading and drives it.
- The call is supervisory: the arc is re-solved as the robot
proceeds and re-issued when the solution has materially changed
(
|Δomega| > 0.05 rad/s,|Δ arc length| > 15 mm, or half the arc already covered) —motion-api#3.5. - Motion ends when the robot arrives within
arrivetolerance, ortimeoutfires.
- Program calls
-
Postconditions: Robot is within
arriveof(x, y); final heading is whatever the arc produced, not a chosen value —motion-api#3.5. -
Error flows:
- Same current-adapter gap as UC-002:
GO_TO_R/GO_TO_Wdecode and dispatch but answerkUnknownonDiffDriveAdapter—protocol#5. -
go_to_wwith no pose source fitted and no odometry seeded → application-level rejection (pose unavailable); a program needing a specific final heading must follow up with a pivot —motion-api#3.5.
- Same current-adapter gap as UC-002:
- Actor: Student / classroom programmer
-
Preconditions: A motion is active (any of UC-001 through UC-003),
or no motion is active (a
stop()with nothing running is harmless). -
Main flow:
- Program detects its own end condition (found the line, operator
pressed a button) and calls
robot.stop()—motion-api#3.7. - Wire form
STOP #<id>is sent;onStop(immediate=False, id)callsneutral(), zeroing duty this cycle with no ramp onDiffDriveAdapterspecifically —protocol#5.1. - Robot replies
ack(noerr—onStopalways returnskOkon this adapter) —protocol#5.1.
- Program detects its own end condition (found the line, operator
pressed a button) and calls
-
Postconditions: Motion ends at the current position/heading, not
queued behind whatever was in flight —
motion-api#6. -
Error flows:
-
stop(immediate=True)variant (STOP now #<id>) — same acceptance path; theimmediateflag is accepted but has no observable difference onDiffDriveAdapter, sinceneutral()was already immediate either way —protocol#5.1,motion-api#9.1.
-
- Actor: Student / classroom programmer
-
Preconditions: Program is in a motion loop (Mode B or A,
motion-api#5) and detects a fault condition — a bumper press, a caught exception, an operator Ctrl-C. -
Main flow:
- Program catches the fault (
except BaseExceptionin the reference code) and callsrobot.estop()before re-raising —motion-api#7. - Wire form
ESTOPis sent with no id, executes unconditionally regardless of trailing junk, and the latch is set before the handler's reply is written —protocol#8.3. - Robot replies the bare word
estop. - Motion is refused until
estopClear()is called —diffdrive#3.2.
- Program catches the fault (
-
Postconditions: Kernel latched at zero duty; every subsequent
motion command is refused (
kRefusedEstopped) until explicitly cleared. -
Error flows:
-
estop()sent while the sequence is stalled on a numeric gap or a decode-failure NAK → still executes;ESTOPis exempt from sequencing entirely for exactly this reason —protocol#8.3. -
One
estopis not proof of a stop: the motor brick can retain its last commanded speed across a single latched estop (measured 5 of 6 failures in one bench series, one producing 936 mm of continued travel). A caller must confirm the robot actually stopped (active flag clear, encoders holding) and re-issue if not —motion-api#6.
-
- Actor: Student / classroom programmer
-
Preconditions: A motion has been posted (e.g.
move_x) without a callback. -
Main flow:
- Program iterates the returned object in a
forloop; each iteration ticks the motion and yields a telemetry snapshot —motion-api#5.1. - Program inspects a sensor field on each snapshot (e.g.
t.color.blue,t.range) and callsrobot.stop()(expected condition) orrobot.stop(immediate=True)(must stop short) as appropriate —motion-api#7. - Over the wire, "tick" only drains telemetry the robot already
pushed and tests for completion — it never polls —
motion-api#5.3.
- Program iterates the returned object in a
-
Postconditions: Loop exits with
m.reasonset tostop,timeout,estop, oraborted—motion-api#5.1. -
Error flows:
- Program calls
tick/iterates while a fiber (Mode A) already owns the loop → raises, rather than double-ticking —motion-api#5.1. - An unticked, posted move in-process does nothing (safe no-op); the
same posted move over the wire runs regardless of whether the host
ever looks again, bounded only by its own timeout/lease —
motion-api#5.2.
- Program calls
-
Actor: Host session (
robot_v6.reliability.Session), on behalf of a student's program -
Preconditions: A program has queued several sequential motion
commands (the canonical example: eight legs of a square) and the
session is pipelining them without waiting for each ack —
protocol#8.1,reliability.pymodule docstring. -
Main flow:
- Session assigns strictly increasing ids and sends commands without blocking.
- One command (e.g. a turn) is lost in transit.
- The robot's
nack <n> <lastDone> <reason>names the next id it actually needs; the session resends everything buffered fromnforward, in order. - The robot's own Motion Layer executes the resent and subsequent
commands in order from its FIFO queue — ordered execution is
guaranteed structurally, not by the host holding commands back
(
reliability.pydocstring, correcting an earlier design note).
-
Postconditions: All eight motions complete exactly once, in order,
despite the one loss — proven as an executable scenario in
tests/host/robot_v6/test_reliability.py. -
Error flows:
- The lost
nackitself is lost too → self-heals, because every subsequent command re-triggers the samenackuntil the gap is filled —protocol#8.1. - Every command in the backlog is a genuine command (not a duplicate id) so none are silently dropped as stale.
- The lost
- Actor: Host session
- Preconditions: A sequenced command is in flight.
-
Main flow:
- A line is corrupted in transit such that it fails to decode (unknown verb, wrong arity, unparseable field) though it arrives with the expected next id.
- Robot classifies this as decode failure, not a merits
rejection: it replies
nack <expectedNext_> <lastDone> <reason>(naming the same id) pluserr <code> #<id>, and does not advance the sequence —protocol#8.9. - Session recognizes the nack and resends the same command (and anything buffered after it).
- Postconditions: The corrupted command is eventually delivered intact and the sequence advances normally.
-
Error flows:
-
The host itself constructs a genuinely malformed line (a real
bug, not transient corruption) → the robot NACKs the same id
forever and the stream wedges permanently; this library supplies no
give-up path. The session/application needs its own resend limit,
timeout, or operator-visible stall detector —
protocol#8.9, explicitly called out as a required application-level backstop.
-
The host itself constructs a genuinely malformed line (a real
bug, not transient corruption) → the robot NACKs the same id
forever and the stream wedges permanently; this library supplies no
give-up path. The session/application needs its own resend limit,
timeout, or operator-visible stall detector —
- Actor: Host session
-
Preconditions: A transport connection exists (socket, pipe, or
serial —
transport.py) but the protocol session is fresh or recovering from a suspected desync. -
Main flow:
- Host sends
HELLO(no id, no fields). - Robot resets
expectedNext_ = 1and emits its banner (DEVICE:NEZHA2:robot:<name>:<serial>) —protocol#8.3. The banner is colon-delimited because it belongs to the separately specified device-announcement protocol (microbit-radio-relay/docs/announce.md), not to the v6 line grammar —protocol#2.4. - Robot's
lastDone()/lastDoneReason()(Adapter-owned) are not reset by this — a reconnect does not erase what the robot already completed —protocol#8.8.
- Host sends
- Postconditions: Sequence numbering restarts at 1; session is ready for new sequenced commands.
-
Error flows:
-
HELLOsent with extra fields (wrong arity) → no reply of any kind,malformedCount()increments; there is noackto anchor anerragainst for an unsequenced verb —protocol#9.10item 7. - A host suspecting desync can instead read
STATUS'snext=field to resync tracking without a full reset —protocol#8.7. This only became possible on 2026-08-27, whenSTATUSwas unsequenced: while it was sequenced, a host that had lost its counter could not choose an id for it, and either got a stale re-ack with nostatusline or opened a gap and stalled the stream it was diagnosing. -
HELLOis not a liveness probe. It resetsexpectedNext_, so firing it at a live session desyncs it: the robot's counter goes to 1 while the host's stands at N, and since acked ids have already been retired from the host's pending buffer there may be nothing left to resend — the robot waits for#1indefinitely. UsePINGfor liveness andSTATUSfor diagnosis —protocol#8.3.
-
- Actor: Host session / developer diagnosing a stuck link
- Preconditions: The sequence is stalled on a numeric gap or a decode-failure NAK (UC-007/UC-008 in progress).
-
Main flow:
- Host sends any of
PING,HELP,ID,VER,STATUS— no id required, and a trailing#<id>from an old-style caller is tolerated —protocol#8.3,protocol#9.10item 2,protocol#9.11. - Robot answers each on its merits regardless of the stalled sequence
(
pong <now>, the verb list, the identity line, the version, the status line). A query gated behind a missing id cannot diagnose the very link that id went missing on. - Because a stall IS outstanding, each of those replies is followed by
a reminder line,
nack <expectedNext_> <lastDone> <reason>— "your last command didn't land, and here is the id I still need" (protocol#8.3, 2026-08-27). On a clean stream no such line is emitted.
- Host sends any of
-
Postconditions: Host confirms the robot is alive, learns what the
sequence is waiting for, and can read
STATUS'snext=/done=/reason=— all without consuming a sequence id or perturbing the stalled stream. -
Error flows: None. All five are maximally forgiving of trailing
content, matching
ESTOP's posture, specifically so they cannot be refused over a syntax nit —protocol#8.3. NoteESTOPandHELLOare deliberately excluded from the reminder:ESTOPreplies the bare wordestopand never queues behind anything, andHELLOresets the state a reminder would report on.
- Actor: Developer running the sim server
-
Preconditions:
tools/simis built (tools/sim/README.mdbuild command); no robot or serial port available. -
Main flow:
- Developer launches
/tmp/robot_sim --stdio(or--listen 127.0.0.1:7654for TCP) with an optional--period MSto control the simulated telemetry/step cadence. -
sim_main.cppcomposes the realProtocolHandlerwithProtocol::FakeMotionAdapter— a fixed-capacity FIFO motion queue, not a stub — so pipelined motion commands behave as they would against a real planner-bearing robot. - Developer's host code (or a test) connects via
robot_v6'sStdioTransport/SocketTransportand drives the exact same protocol traffic UC-001 through UC-010 describe.
- Developer launches
- Postconditions: Host-side behavior (codec, transport, reliability session) is validated end to end against a real wire grammar implementation with no physical robot.
-
Error flows:
- Ctrl-C, SIGTERM, or EOF on stdin all shut the simulator down
cleanly —
tools/sim/README.md.
- Ctrl-C, SIGTERM, or EOF on stdin all shut the simulator down
cleanly —
- Actor: Firmware developer
-
Preconditions: A target platform (e.g. MicroPython, JavaScript) or
a new concrete robot needs a
ProtocolHandler/Adapterimplementation;src/protocol/is treated as the reference archetype to read and port, not a library to link against directly on that platform —protocol#9.4. -
Main flow:
- Developer implements the
Adapterinterface (protocol#4) for the target — session identity, the six motion methods,onStop/onEstop,onGet/onSet,onTlm,lastDone/lastDoneReason, and (if the target supports remote invocation)onRunwith an explicit registration allowlist —protocol#6.3. - Developer validates against
golden_vectors.txtand the adversarial fixture (tests/protocol/test_protocol_adversarial.py) to confirm wire-level conformance —protocol#9.4. -
Language-specific hazards to check deliberately, not inherit:
leading-whitespace/underscore numeric leniency differs by host
language parser (
protocol#9.4); a dynamic language's naturalonRunimplementation (getattr/globals()) is more permissive than the C++ archetype's registration table and must have an allowlist added deliberately (protocol#9.7);onRun()must still return promptly even in an event-loop language (protocol#9.7). - If the target adapter is also a new transport (e.g. a WiFi
dual-plane link rather than plain serial), the developer follows
wifi-linkas the porting authority:CIPMUX=1command mode, the demux-by-link-id design, mandatory send-queue bounding/telemetry throttling, and the bring-up state machine —wifi-link#3,wifi-link#6,wifi-link#7.1,wifi-link#5. This is itself unimplemented in this repository as of this writing and is available for a firmware developer to pick up.
- Developer implements the
- Postconditions: A new adapter or transport speaks conformant protocol-v6 and passes the golden-vector and adversarial suites.
-
Error flows:
- A straight logic port reproduces a C++-only bug class (e.g. treats
an embedded NUL as verb-terminating) that a length-aware host
language would not naturally exhibit — flagged as a pinned
characterization test, not something to silently "fix away" from
the reference behavior without recording the divergence —
protocol#9.4.
- A straight logic port reproduces a C++-only bug class (e.g. treats
an embedded NUL as verb-terminating) that a length-aware host
language would not naturally exhibit — flagged as a pinned
characterization test, not something to silently "fix away" from
the reference behavior without recording the divergence —
- Actor: Firmware developer
-
Preconditions: A concrete adapter (e.g.
DiffDriveAdapter) exists; neitherProtocolHandlernor the kernel stores any configuration itself —protocol#7. -
Main flow:
- Developer adds a named field to the adapter's own config type
(e.g. a new
DifferentialDrive::Configmember) and maps a wire name to it in the adapter's field table. - Host sends
SET name value #<id>; the handler delegates toonSet, which is acked with no separateokreply — the ack itself is the acceptance signal —protocol#8.2. - Host sends
GET name #<id>(or bareGET #<id>for every field) to read it back —protocol#6.
- Developer adds a named field to the adapter's own config type
(e.g. a new
-
Postconditions: The new field is readable/writable over the wire
with no changes needed to
ProtocolHandleritself. -
Error flows:
- Unknown
SET/GETname →err 1 #<id>(ERR_UNKNOWN), layered on top of theackevery in-order command still gets —protocol#7. - A value that fails to parse at all is a decode failure (NAK), not a
rejection — distinct from an unknown name —
protocol#6.
- Unknown
- Actor: CLI / tooling user
-
Preconditions: The Rogo CLI is installed (the
rogoconsole script,[project.scripts]) and adapted ontorobot_v6/protocol v6, implemented insrc/host/rogo/cli.py(argument parsing and dispatch),src/host/rogo/connection.py(target resolution),robot_v6/motion.py(the six motion-API operations as wire calls), andsrc/host/rogo/repl.py(the interactive/piped/argument-list command loop) — seeclasi/sprints/001-import-rogo-cli-onto-the-v6-host/sprint.md. -
Main flow:
- User invokes
rogo drive <args>/rogo turn <args>/rogo goto <args>(orrogo repl) against a robot reachable throughrobot_v6'sTransport— a real robot, a relay server, ortools/sim(UC-011), all through the same client code (the stakeholder's own "either of those, the same way" requirement,transport.pydocstring). -
rogo.clitranslates the command into the correspondingrobot_v6.motioncall and reports the outcome/telemetry back to the terminal.rogo gotomaps onto the wire-levelGO_TO_Rverb only (robot-frame; world-framego_to_wis deferred until a pose source exists,specification.md#13) — not a camera-based closed loop (sprint.md's Design Rationale Decision 3).
- User invokes
- Postconditions: Robot executes the requested motion; CLI reflects the reliability-layer outcome (ack/nack, completion reason) to the user.
-
Error flows:
- Relay/robot unreachable → CLI surfaces a transport-level error
rather than hanging (per
TransportClosedvs. an ordinary read timeout distinction already intransport.py). - A requested motion (e.g.
goto, ordrive --mm) targets a verb with no kinematic effect on the connected adapter (UC-002/UC-003's current gap) → CLI prints the resultingerr/kUnknownoutcome as a soft warning and still exits 0 — a documented stakeholder decision, not a silent success or a crash (rogo.cli's_print_soft_warning()).
- Relay/robot unreachable → CLI surfaces a transport-level error
rather than hanging (per
- Actor: CLI / tooling user
-
Preconditions: Rogo's calibration flow is implemented in
src/host/rogo/calibrate.py— manual, tape-measure/protractor-verified trial sequencing only; the camera-based--automode elite had does not port (sprint.md's Design Rationale Decision 4) — andsrc/host/rogo/config.py, which loads/persists the per-robot config already staged inconfig/robots/(config/MANIFEST.md). -
Main flow:
- User invokes
rogo calibrate turns(measuresrotational_slip) orrogo calibrate distance(measures the newdistance_scalefield) against the active robot resolved viaconfig/robots/active_robot.json— there is no<robot>name argument. -
rogo.calibratedrives a batch of manual trials (each aWHEELS_Vspin/straight-line run viarobot_v6.motion, defaulting to a human-measurable 90° target rotation forturns,DEFAULT_TURN_TARGET_DEG), prompts for the operator's tape-measure/ protractor result each time, computes an updatedrotational_slip/distance_scalevalue (motion-api#2.1), and writes it back viarogo.config.save_robot_config()on confirmation.
- User invokes
-
Postconditions: Robot's config file reflects the newly measured
calibration value (
rotational_slipordistance_scale) for use by futurerogo turn/motion commands. -
Error flows:
- Measured value falls outside a sane range →
rogo.calibrate.compute_calibration()rejects the write and reports why rather than silently persisting a bad calibration (mirrors the specification's own caution against bendingtrackwidthto make turns land,motion-api#2.1).
- Measured value falls outside a sane range →
- Actor: CLI / tooling user (or an external tool/agent acting as the MCP client)
-
Preconditions:
src/host/rogo/mcp_server.pyis implemented, exposing 8 tools (hello,stop,drive,turn,goto,config_get,config_set,calibrate_turns) that delegate to the samerobot_v6.motion/rogo.config/rogo.calibratecallsrogo.cli's own subcommands use. -
Main flow:
- User invokes
rogo mcpto start an MCP server process that exposes those tools. It defaults tostdiotransport (no network surface at all);--listen HOST:PORTopts into TCP instead, restricted to loopback unless--allow-remoteis also given (sprint.md's Migration Concerns security note). - An external MCP client (an agent or another tool) calls a tool;
the server translates the call into
robot_v6/rogo.config/rogo.calibratetraffic against the connected robot/relay/sim, the same way UC-014's direct CLI commands do.
- User invokes
-
Postconditions: External tooling can drive and observe a robot
without embedding
robot_v6directly. -
Error flows:
- Same transport/adapter-gap error flows as UC-014 (a
kUnknownoutcome is reported as awarning/errorkey in the tool's own result, not raised), plus a genuine unreachable-target failure (TransportClosed, or a wait that never resolves) raised asrogo.mcp_server.UnreachableTargetErrorand surfaced through the MCP tool-call error channel rather than hanging.
- Same transport/adapter-gap error flows as UC-014 (a