The Rust follower stack,
taken by the hand

How one teleop setpoint, one robot command, and one telemetry frame travel through the ported system — and every Python idiosyncrasy that deliberately survived the trip. The port's contract was: the Python implementation is the specification. Where Python looked buggy, we ported the bug and filed it (PORTING.md §13.4) instead of silently fixing it. This document is the mental model you need before we start un-quirking it.

Rust crates
3
sdk · driver · node
Hot loops
6 threads
all off tokio, by design
Stream rate
100 Hz
cmdT = 1 / publish_rate_hz
Commands
16
+2 rejected (digital twin)
Guards
6
run on every frame, holds too

1 · Topology: three processes, two cities

The old world was one big Python process. The new world: the operator PC keeps the unchanged Python orchestrator, frontend and SpaceMouse leader; one new adapter (remote_follower_bridge.py) maps the in-process bus onto the network; and a self-contained Rust robot-node runs next to the arm. Everything that must be delivered rides gRPC; the two high-rate latest-wins streams (actions in, state out) ride UDP.

flowchart LR
  subgraph OP["Operator PC — Python, unchanged"]
    SM["SpaceMouse leader
100 Hz admittance"] -->|"state.leader"| ORCH["Orchestrator
mirrors in TELEOPERATION"] ORCH -->|"action.* topic"| BR["remote_follower_bridge.py"] ORCH -->|"cmd.* (corr, reply_to)"| BR BR -->|"state.* / event.* / bridge.*"| FE["frontend · profiler · TraceLogger"] end subgraph NODE["mini PC — robot-node (Rust)"] AL["action_listener thread
SeqGate"] --> CORE["NodeCore"] GRPC_S["tonic control plane (tokio)
Command / StreamActions / StreamEvents"] --> CORE CORE --> MB["driver mailbox
depth-1, latest-wins"] MB --> SJ["ServoJ sender thread
guard suite on every frame"] CN["CNDE reader thread"] --> TEL["publish_worker thread"] RP["UDP reply thread
rejection = e-stop"] end BR -->|"UDP S7A1 actions :50063"| AL BR <-->|"gRPC :50061"| GRPC_S TEL -->|"UDP S7N1 telemetry"| BR SJ -->|"ServoJ :20007"| CTRL["FR3 controller"] CTRL -->|"CNDE state :20005"| CN CTRL -->|"reject replies"| RP CORE -->|"XML-RPC :20003
postures · e-stop · IK"| CTRL

Who owns which thread

The one place the port is not line-for-line: Python leaned on threads hidden inside the vendor SDK; the Rust SDK is deliberately synchronous, so the driver owns them explicitly. The paced hot loops stay dedicated OS threads — tokio exists only for the gRPC control plane, so an e-stop RPC never contends with action parsing for an async worker.

ThreadLayerSpawnPacingJob
action_listener_{id}nodeactions.rs:257blocking recv, 200 ms timeoutUDP S7A1 in → SeqGate → ingest_action
publish_worker_{id}nodetelemetry.rs:223Instant-paced @ publish_rate_hzget_state → S7N1 frame out; drains event + trace queues
tonic runtime (tokio)nodemain.rs:242asyncCommand / StreamActions / StreamEvents; each command on spawn_blocking
CNDE readerdriverrobot.rs:1105blocking next_state()controller state → RwLock snapshot + first-frame latch
ServoJ senderdriverrobot.rs:1078Instant grid @ cmdTmailbox → guards → cap_delta → ServoJ; hold on starvation
UDP reply readerdriverrobot.rs:1152blocking recv, 100 ms timeoutcontroller rejection verdicts → e-stop (never an RPC!)
reconnect_{id}node, ad-hocnode.rs:379one-shotfire-and-forget disconnect → 0.5 s → connect; joined only at shutdown
_supervisor / _udp_listener / _health_loopbridge (py)remote_follower_bridge.py:642-652daemonslink + events relay · telemetry in · health/RTT probe

Locks that replaced the GIL (robot.rs:635-668, sync.rs)

sdk Mutex — the old _sdk_lock; held across multi-call RPC sequences. snapshot RwLock<Option<CndeState>> — single writer (CNDE thread). sdk_state Mutex — two-owner discipline: CNDE thread writes fairino_state, engage path writes probe_state, never the same field. Plus status, safety, postures, and the mailbox.

Sanctioned nesting only: safety → {status, sdk_state, stream, sdk, fk} (a guard trip fires e-stop while the safety mutex is held) and postures → sdk. The reply thread takes no RPC lock, ever.

Ports cheat-sheet

Operator ↔ node: gRPC :50061 · telemetry UDP :50062 · actions UDP :50063 (defaults; remote_node: block in the robot yaml).

Node ↔ controller: XML-RPC :20003 · CNDE realtime state :20005 · ServoJ setpoints UDP :20007.

Wire magics: actions "S7A1", telemetry "S7N1" — cross-flow datagrams can never parse.

2 · Walkthrough A — life of a teleop setpoint

The 100 Hz hot path. Follow one joint-position setpoint from the SpaceMouse to the arm's servo loop. The single most important object on this path is the depth-1 latest-wins mailbox — a fresher setpoint always supersedes a delayed one, which is why UDP loss and reordering are absorbed by design.

flowchart TD
  A["1-2 · leader state, mirrored by orchestrator
action.* on the bus (JSON)"] --> B["3-4 · bridge: dict to protobuf,
S7A1 datagram, seq++"] B --> C["5 · node listener: decode + SeqGate
stale dropped, e-stop never"] C --> D["6 · NodeCore.apply_action
hop stamps + command_latency_ms"] D --> E["7-8 · driver mailbox put
depth-1, latest-wins, read-without-take"] E --> F["9-10 · sender tick at cmdT
fresh? dequeue : hold"] F --> G["11 · guard suite — every frame
trip = e-stop"] G --> H["12 · cap_delta vs measured,
ServoJ UDP :20007"] H -.->|"controller replies ONLY on rejection"| I["13 · reply thread verdict
reject = e-stop"] F -.->|"producer late"| J["14 · hold last frame forever
(halt window optional, never de-energize)"]

Operator side (Python, unchanged except the bridge)

Step 1-2 · Leader → orchestrator mirror → action.{id}

The SpaceMouse leader's admittance thread builds a RobotState at 100 Hz and stamps leader_timestamp = now (spacemouse_leader.py:316-329). The orchestrator, only in TELEOPERATION mode, mirrors it to the follower: joints copied 1:1, velocities/efforts zeroed (orchestrator_node.py:342 → 383 → 414), published as RobotState.to_dict() JSON on action.{id}. Nothing here changed in the port — that is the zero-orchestrator-changes property.

Step 3 · Bridge ingress: _on_action_put_action

The bridge subscribes action.{id}, converts the bus dict into the wire protobuf Action (typed hot fields + everything else squeezed into rest_json), and routes it. Two things to notice: actions are only forwarded while the stream is engaged (PREPARE_STREAM engages, ENTER_IDLE disengages), and an e-stop action short-circuits into the dual-path sender before any of that (remote_follower_bridge.py:734-746).

servo7/nodes/remote_follower_bridge.py:750 — the engage gate; disengaged actions are counted + warned once, never sent

Step 4 · The S7A1 datagram

One datagram = one action. Big-endian 11-byte header, then the robot id, then the protobuf. The sender owns one process-wide wrap-aware u32 sequence counter that survives engage cycles (remote_follower_bridge.py:467-478; frame codec mirrored in rust frame.rs:30/43).

magic u32 "S7A1" version u16 = 1 seq u32 (wraps) id_len u8 robot_id bytes protobuf Action

Telemetry uses the same layout under magic "S7N1" with a JSON payload — the differing magic is what makes cross-flow parsing impossible.

Node ingress (Rust, dedicated OS thread)

Step 5 · Listener thread: decode, hostile-datagram policy, SeqGate

A dedicated OS thread (not tokio — a datagram burst must never starve the control plane) blocks on the socket with a 200 ms timeout. Garbage, wrong magic/version, or undecodable protobuf → counted as malformed and ignored, never a fault. A foreign robot_id is dropped silently and uncounted (it is another flow's traffic). Then the seq gate (actions.rs:341, 185):

rust/robot-node/src/actions.rs:185 — wrap-aware latest-wins with a 2 s epoch age-out (a restarted bridge's fresh counter is never blackholed)

Step 6 · NodeCore::apply_action — hop stamps and the latency number

Transport-neutral from here down — the gRPC StreamActions fallback feeds the exact same function. The action is rebuilt into a RobotState (rest_json first, typed fields overlaid), then stamped through the follower.{id}.* hop chain the profiler renders. command_latency_ms is wall-clock now − leader_timestamp — which is why it assumes chrony across hosts. Any failure in this function (reconstruction, panic, driver error) is answered with an e-stop, not a log line (node.rs:519, 552).

rust/robot-node/src/node.rs:530 — parsed → pre_hardware_write → set_state_traced → applied (terminal flush)

Driver: mailbox → paced sender → guards → hardware

Step 7 · set_state_by_ref — producer entry, never a sender

Ingress does not validate or send. It type-checks, records the commanded pose, short-circuits an e-stop straight into emergency_stop_impl (so it can never wait behind the send cadence), stamps servoj_enqueue, and overwrites the mailbox (robot.rs:1453-1468).

rust/fairino-robot/src/robot.rs:1453 — the by-ref signature keeps the hop list with the node so its terminal flush sees servoj_enqueue

Step 8 · The mailbox: depth-1, latest-wins, read-without-take

Python got this for free — "reference assignment is atomic under the GIL" — so Rust makes it a Mutex<Option<CommandSlot>> with very particular semantics (stream.rs:181, 200-209, 245-251):

  • The producer overwrites; the consumer clones and leaves the slot in place. It is cleared only by StreamHealth::reset() — never by the tick.
  • Staleness comes from t_mono: a slot re-read inside the stale window is a fresh re-send of the same setpoint, not a hold. A take-semantics port would silently flip starvation accounting and the flight-recorder record kind (pinned by test re_read_within_window_is_fresh_not_hold, stream.rs:483).
  • No queue may ever sit in front of this mailbox — buffering stale setpoints recreates the exact failure it exists to prevent. (The post-merge playout-queue project deliberately bounds + age-caps for this reason.)

Step 9 · The sender thread: an Instant grid at cmdT

cmdT = 1 / publish_rate_hz (10 ms at 100 Hz) and is never configured independently — the controller computes joint speed as delta/cmdT, so a mismatch inflates implied speed (stream.rs:44-54, robot.rs:727). The loop gates on ProbeState::Ready exactly (not != Off) so the readiness probe owns the channel alone during SETTLING. A tick error kills the thread — mirroring an uncaught exception in Python's daemon loop (robot.rs:1231-1244).

rust/fairino-robot/src/robot.rs:1206 — the grid advances by a fixed period; the optional phase lock may shift it (capped), never the spacing

Step 10 · One tick: tick_paced

The M6 core (send_frame) decides fresh-vs-hold and injects the guarded send as a closure. servoj_dequeue stamps and profiler chain exports fire only on fresh frames; holds export nothing (robot.rs:1259-1289).

rust/fairino-robot/src/robot.rs:1259 — note export_chain AFTER the guarded send returns, so exported chains reach the SDK handoff

Step 11 · The guard suite — every frame that reaches hardware

Sacred §8.8: fresh, hold, or synchronous — every frame passes validate_command. Hold frames are not exempt. Command-path order, first failure short-circuits (envelope.rs:239-270):

envelope trip-lock joint_limits workarea (URDF FK on commanded joints) max_joint_deltas ("command_guard") command_speed

joint_speed and eef_speed are state-side observers (observe_state, envelope.rs:284) — they watch the measured stream, not commands. Trip-lock: N consecutive violations latch tripped and fire the robot's e-stop callback once (guard.rs:213-228); only reset_after_estop_release clears the latch. Trip counts differ by config block: 3 for command_guard/joint_speed, 1 for command_speed/eef_speed/workarea (config.rs:479-484). On a trip inside guarded_send the driver e-stops and refuses the write (robot.rs:1474-1485).

Step 12 · set_state_impl: cap_delta, the sdk lock, and the wire

The last gates before hardware (robot.rs:1555-1597): e-stop bypass → latch gate → joint-count gate → streaming-ready gate → cap_delta → hop stamps around the sdk lock (command_pre_lock / command_lock_acquired) → record_hardware_ticksend_servoj (UDP passthrough, fire-and-forget). A transient RPC error drops the setpoint with a warning — never a crash.

cap_delta (sacred §8.10) (robot.rs:429-488): closes the loop against the measured pose, returns the target unchanged when within the per-joint step, and is a no-op when unconfigured or before the first observation. Its rate-limit warning is edge-triggered — once per ramp episode, via an AtomicBool.

The return leg and the failure paths

Step 13 · The controller only speaks when it rejects

ServoJ over UDP has no acks. A dedicated thread blocks on the reply socket (polling at cmdT would add up to one tick of e-stop latency). The verdict is a three-way match on (errcode, probe_state, status) — and this thread reads only the packet snapshot, never an XML-RPC (deadlock rule, sacred §8.12) (robot.rs:1737-1763).

rust/fairino-robot/src/robot.rs:1737 — code 0 ignore · SETTLING = probe rejection flag · latched = nothing · else e-stop

Step 14 · Starvation: hold forever, never de-energize

When the producer goes quiet the sender repeats the last frame (or freezes at the measured pose if nothing was ever sent), warning at a 2 s cadence (stream.rs:265-325, 373-394). Sacred §8.9: servoj_halt_after_ms = 0 (default) means hold forever; a positive window exits the stream phase — arm held — but never de-energizes. Link loss must not drop a payload.

stateDiagram-v2
    [*] --> FRESH
    FRESH --> FRESH : new slot, or re-read inside stale window (fresh re-send)
    FRESH --> HOLD : slot dwell exceeds stale window
    HOLD --> HOLD : repeat last_sent (guards still run) — warn every 2 s
    HOLD --> FRESH : producer resumes
    HOLD --> HALTED : halt_after_ms greater than 0 and gap reached
    HALTED --> [*] : exit stream phase — arm held, motors stay energized
    

3 · Walkthrough B — life of a robot command

Everything that must be delivered — mode changes, resets, IK solves — rides the reliable gRPC unary Command. The Python RobotNode._command_handlers table survives 1:1.

Step 1 · Bus request with corr

CommandRouter.request mints a corr from a counter and publishes {action, params, reply_to, corr} on cmd.{id}; replies demux by corr on one persistent subscription (command_router.py:155-199). Unchanged from the in-process days.

Step 2 · Bridge forwards — and never retries

_on_robot_command makes the unary RPC. On transport failure it publishes the F4 error reply {corr, value: null} so the requester fails fast instead of eating its timeout (remote_follower_bridge.py:1035-1049). PREPARE_STREAM / ENTER_IDLE also flip the bridge's action-stream engage flag (:1064-1066).

Step 3 · Node: one spawn_blocking per command

Handlers may block on the controller, so each command gets its own blocking task — an e-stop command never queues behind the action stream (they share nothing queue-like). The reply carries node_recv_mono/node_send_mono, sampled before dispatch and just before send (grpc.rs:233-248) — the raw material for clock-offset estimation.

Step 4 · Dispatch guards, panic = Python exception

dispatch: running check → param parse → twin-command rejection → catch_unwind. A handler panic (the ported ValueErrors) becomes Dispatch::Failed(panic_message) with the same log text — per-call exception isolation, exactly like Python (node.rs:272-298).

The 16-command table (node.rs:307-326)

Wire nameDriver callReturnsNotes
prepare_streamprepare_command_stream()Boolposture walk + probe; refuses while e-stop latched
enter_idleenter_idle()BoolManualIdle walk; no-op (true) while latched
reset_emergencyreset_from_emergency_stop()Boolthe only path that clears the latch
release_safetysafety_reset_after_estop_release()Nullclears every guard's trip latch
set_guard_enabledsafety_set_guard_enabled(g, on)Null+ DANGEROUS audit warning
set_safetysafety_update_runtime(block, payload)Nullports the apply-as-you-go coercion order
reconnectspawns reconnect_{id} threadNull (immediately)disconnect → 0.5 s → connect; fire-and-forget
teleop_enter / teleop_exiton_teleop_enter/exit_leader()Bool / Nullenter is a logged no-op (leader-only concept)
set_teleop_guidanceset_teleop_guidance(mode)Boollogs unsupported — drag was cut (§12)
level_tcplevel_tcp_target()joints | Null
orient_primitiveorient_primitive_target(p)joints | NullDLS solve in kin.rs/orient.rs since 2026-07-22
solve_iksolve_ik(pose, ref)joints | NullTCP-pose + IK held under one sdk lock
set_vacuumset_vacuum(on)Booltrait default false = "no capability", silent
set_compression_activeflips node atomicNullreplaces Python's 10 Hz flag poll; suppresses deadline warnings
set_trace_activetrace_set_enabled(on)Nullnode-control, not orchestrator vocabulary
enable_digital_twin · snap_twin_anchorUNSUPPORTEDtwin is operator-side visualization now
anything elseUNKNOWNbridge publishes no bus reply (same as Python)

Worked example: PREPARE_STREAM (how the arm becomes streamable)

  1. Refuse if latched (robot.rs:1814-1818) — sacred §8.7: recovery is only ever the explicit reset path.
  2. Posture walk to TELEOP_STREAM (posture.rs:234-266) — the same ladder every time, no controller-truth polling: return_to_manual (close servo window → Mode Manual → RobotEnable(1)) → enter_automatic (Mode Auto → RobotEnable(1)) → open the servo window. Each controller call runs the ported retry: retry only on codes in the set, ResetAllError re-issued before each attempt when clear_first (posture.rs:314-348). ServoMoveStart retries NOT_READY 6× / 0.3 s because RobotEnable returns before the servos energize (posture.rs:354-367).
  3. Close is belt-and-braces: close_servo_window always attempts and keeps no "is it open" flag — a cached guess once turned a failed close (err=14) into a runaway (posture.rs:371-392).
  4. The probe (robot.rs:1847-1908) — sacred §8.3: the controller replies to ServoJ only on rejection, so send one probe frame, wait the 0.2 s quiet window; silence is the accept. One probe in flight at a time; rejections during SETTLING mean "keep probing", never e-stop; 3 s of failures returns false instead of raising (Tenacity's retry_error_callback, hand-ported).
stateDiagram-v2
    OFF --> SETTLING : prepare_stream — posture walk done
    SETTLING --> SETTLING : ServoJ rejected — keep probing
    SETTLING --> READY : 0.2 s of silence = accept
    SETTLING --> OFF : 3 s exhausted — prepare returns false
    READY --> OFF : e-stop / halt window / enter_idle
    note right of READY : quirk — disconnect() never exits the stream phase, so READY survives teardown (harmless only because reconnect re-walks + re-probes)
    

Worked example: EMERGENCY_STOP (three triggers, one funnel)

Three independent triggers all funnel into emergency_stop_impl: (a) an operator e-stop action — dual-pathed by the bridge as a UDP datagram and a one-shot reliable StreamActions call with wait_for_ready, so a 100% UDP blackhole still delivers at gRPC speed (remote_follower_bridge.py:888-930); (b) a guard trip inside guarded_send; (c) a controller ServoJ rejection on the reply thread. Duplicates collapse into one latch. The orchestrator keeps re-firing e-stop states at 100 Hz while the leader is latched — by design, the latch makes that idempotent.

rust/fairino-robot/src/robot.rs:1640 — RobotEnable(0), never StopMotion (2026-06-18: StopMotion returned 0, arm kept driving). Latch logs once; the enable-off is re-issued every call until it takes.

Recovery is strictly ordered and the ordering is enforced by the latch gates, not by convention: prepare_stream and enter_idle both short-circuit while latched, so the only way forward is reset_emergency (refuses unless latched → clears every guard trip → status Idle → ResetAllError) → enter_idleprepare_stream (robot.rs:1679-1706). After any stream gap the leader must re-sync targets to the follower's measured pose first — resuming onto phase-advanced targets trips max_joint_deltas straight back into e-stop (N-D F3: the guard working as designed).

Measured cmd→RobotEnable(0) latencies (N-D): 48.5 ms median @ 50 ms RTT · 166.7 ms @ 150 ms · 150.9 ms @ 5% loss (p99 308 ms) · 47.3 ms around a hard link cut. Node-internal apply ≤ 0.7 ms — the wire is the entire latency. The hardware E-stop remains the real safety layer.

The reply leg doubles as a clock

sequenceDiagram
    participant B as Bridge (operator PC)
    participant N as Node (mini PC)
    B->>N: Command RPC — t0 = perf_counter()
    Note over N: t1 = node_recv_mono (before dispatch)
    Note over N: handler runs on spawn_blocking
    Note over N: t2 = node_send_mono (just before send)
    N-->>B: CommandReply {corr, value, t1, t2}
    Note over B: t3 = perf_counter()
    Note over B: offset = ((t1-t0)+(t2-t3))/2, uncertainty = RTT/2
    

Every command reply feeds one four-timestamp sample (remote_follower_bridge.py:1025-1033, clock_offset.py:44-51). The estimator keeps a window of 8, picks the min-RTT winner, and blends with a slow EWMA (α = 0.1). No clock is ever adjusted — estimation only, published with its uncertainty on bridge.{id}, reset on link loss (a restarted node restarts its monotonic epoch).

This is what lets the profiler translate node-side hop times onto the operator timeline (Walkthrough C, step 6). Wall-clock fields (leader_timestamp, command_latency_ms) still assume chrony — never trust sub-10 ms cross-host wall deltas.

4 · Walkthrough C — telemetry, events, and the trace backfeed

Three return channels with three delivery guarantees: lossy latest-wins UDP for the 100 Hz state stream, a reliable priority gRPC lane for robot events (an EMERGENCY_STOP notification must never be lost), and a capped bulk lane for diagnostics that must never delay the priority lane.

Step 1 · CNDE reader: the snapshot and the first-frame latch

The reader blocks in next_state(), writes each frame into the RwLock snapshot, and latches first_frame_seen (robot.rs:1127-1147). Sacred §8.11: every state read fails with BadResponse until that latch — a pre-sync seeded from an all-zero packet would jump the arm. (Python checked frame_cnt == 0 && joints all zero; the FR firmware rejects frame_cnt at CNDE config time, so the mechanism changed, the observable behavior did not.)

Step 2 · Reading a state: classify, demux, and two famous lies

read_state_from_pkg maps the packet to RobotState (joint arrays truncate with take — slice semantics, sim packets are really shorter) (state.rs:92-131). The classifier is most-severe-wins — and ports the quirk that only EmergencyStop == 1 counts as e-stop; any other non-zero value falls through (state.rs:151-169). Vacuum state is a bit demux over both DO bytes (state.rs:181-188). And yes: units ships as "radians" while the numbers are degrees — the dataclass default was never set in Python, so it is never set in Rust either.

Step 3 · The paced publisher: S7N1 frames + the deadline theater

publish_worker_{id} ticks at publish_rate_hz on a never-resetting grid; each tick is wrapped in catch_unwind so a ported-guard panic logs and the loop continues (Python's per-iteration except) (telemetry.rs:274-331). Payload = the exact RobotState::to_dict() JSON whose parity M1 proved — protobuf telemetry is a flagged later optimization, one variable at a time.

rust/robot-node/src/telemetry.rs:180 — the grid catches up rather than resetting; every 100th miss warns, unless video compression is running (the flag set by set_compression_active)

Step 4 · Bridge receive: wrap-aware latest-wins, epoch reset

The bridge drops out-of-order frames by seq (same half-window test as the node's action gate), ages the epoch out after 2 s of silence, resets it on link loss, and republishes the JSON payload unchanged on state.{id} — the frontend cannot tell the follower moved cities (remote_follower_bridge.py:1197-1225).

Step 5 · Events: two lanes, priority always drains first

Robot events (EMERGENCY_STOP, toggles) ride an unbounded priority channel; trace_batch diagnostics ride a capped (8) bulk channel that drops-newest on overflow. The stream implementation polls priority first, always — a trace batch can never delay or reorder an e-stop notification (grpc.rs:104-146, 202).

rust/robot-node/src/grpc.rs:202 — the ordering guarantee is structural, not scheduled

Step 6 · The trace backfeed: the profiler works cross-host

On every fresh guarded send the driver exports the action's hop chain into a bounded queue (try_send, drop-newest + counter — zero new locks on the sender tick) (trace.rs:375-387). The telemetry thread drains chains (≤160) and hardware-send wall stamps (≤200) every ~1 s into a trace_batch bulk event. Operator-side, TraceBackfeed shifts node-monotonic hop times by −offset (the estimator from Walkthrough B), drops and counts chains that go backwards in time (a wrong offset surfaces instead of rendering), and injects them into the operator TraceLogger — zero frontend changes (trace_backfeed.py:112-150).

One deliberate exception: hw_tick wall stamps are injected UNtranslated — the consumer-rhythm panel diffs consecutive sends per robot, so a constant cross-host offset cancels; routing them through the offset estimator would add error. Dedup is a strictly-increasing wall filter (trace_backfeed.py:86-102).

5 · The quirk catalog

Two very different lists. The first is load-bearing: each row is incident-derived and changing it is a correctness bug, however odd it looks. The second is the cleanup backlog: Python bugs we knowingly reproduced (fixture-pinned, so un-quirking one means updating its fixture too).

5.1 · Sacred — do not "fix" (§8)

#BehaviorWhy it existsRust home
1Vendor SDK returns a bare int instead of (err, data)BadResponse, treated transient, never a crashreal controller misbehaviorerr.rs:165
2code_of: empty tuple → −1callers key on −1err.rs:117-125
3Probe: silence for 0.2 s is the accept; one in flight; SETTLING rejections keep probing, never e-stopcontroller replies only on rejectionrobot.rs:99, 1878, 1855
4cmdT = 1/publish_rate_hz, never an independent knobcontroller computes speed = delta/cmdT; mismatch inflates implied speedstream.rs:44-54
5XML-RPC cmdType for SimMachine is a static construction-time decision — no runtime errcode-14 detectionerrcode-14 incident motivated it, oncerobot.rs:88-93, 722-726
6E-stop = RobotEnable(0), never StopMotion; latch logs once, enable-off re-issued every call2026-06-18: StopMotion returned 0, arm kept drivingrobot.rs:1640-1662
7prepare_stream / enter_idle refuse while latched — recovery only via explicit resetno accidental re-armingrobot.rs:1814, 1923
8Every frame that reaches hardware runs the guard suite — holds are not exempta hold is still a commandrobot.rs:1474
9Starvation holds forever by default; a positive halt window exits the stream phase but never de-energizesde-energizing on link loss would drop a held payloadstream.rs:373-394
10cap_delta closes the loop against the measured pose; no-op unconfigured / pre-observation; warning edge-triggered per episoderamp-in without oscillationrobot.rs:429-488
11State reads fail until the first real CNDE framean all-zero pre-sync would jump the armstate.rs:97-104
12The reply thread reads only the packet snapshot — never an XML-RPC calldeadlockrobot.rs:31-35, fault_diag.rs:38-44
13The (errcode, probe, status) verdict match: 0 → ignore · SETTLING → flag · latched → nothing · else → e-stopprobe vs stream vs latched are different worldsrobot.rs:1743-1760
14Realtime-stream config failure is a warning, not fatal (re-homed onto the CNDE handshake); soft-limit fetch failure skips limits with a warningdegraded stream beats no streamrobot.rs:960-971, 1011

5.2 · Ported bugs & oddities — the future cleanup backlog (§13.4)

Badges: ported bug = faithful reproduction of a Python defect, fixture-pinned · deviation = deliberate, documented divergence from Python · rust-side wart = introduced by the port itself, not by Python.

State layer

ported bug The units lie. Every Fairino state ships units: "radians" (the dataclass default) while the driver publishes degrees. Nothing downstream reads it — yet. types.rs:487

ported bug Only EmergencyStop == 1 classifies as e-stop — a 2 falls through to less-severe verdicts. state.rs:151-169

deviation Negative do_port reads the vacuum bit as off where Python raised ValueError on the read path. Unreachable from real configs. state.rs:176-188

Safety layer

ported bug Tripped guards bounce e-stop frames. Delta/speed/state guards check tripped before their e-stop bypass; the workarea guard checks e-stop first. So "e-stop frames bypass the trip-lock" holds only until one of those four trips. max_joint_deltas_guard.rs:46-54 vs workarea_guard.rs:75-80

ported bug A bogus end_effector_link builds an EMPTY FK chainavailable = true → the point is the base origin forever → the workarea guard silently checks (0,0,0) instead of failing closed. fk.rs:297-337, 251-266

ported bug Silent 65-hop chain truncation on very deep URDFs — chain rooted mid-tree, no error. fk.rs:305

ported bug set_enabled(false) clears the violation counter but not tripped — re-enabling a tripped guard keeps it tripped (probably deliberate; only reset_after_estop_release clears it). guard.rs:148-153

deviation Malformed config aborts startup where Python silently built no guard (truthy non-mapping guard block, string rates, explicit-null limits). Divergence is abort-vs-silence only — fail-closed. config.rs:559-573, policy.rs:147-158

Driver

ported bug disconnect never exits the stream phase — a READY probe state survives teardown. Harmless only because reconnect re-walks postures and re-probes. robot.rs:1054-1072

ported bug ERROR→CONNECTED is dead code on the success path — the next line overwrites with MOVING. (The recovery that matters lives on the read path.) robot.rs:1589-1595

ported bug E-stop with the interface down returns true even though nothing was de-energized — the latch still flips, so the passive gate holds. robot.rs:1654-1656

ported bug An error escaping a sender tick kills the ServoJ thread — in both implementations (Python: uncaught daemon exception; Rust: logged break). robot.rs:1231-1244

deviation Poisoned locks recover (PoisonError::into_inner) instead of cascading — Rust's default would have turned one guard panic into a permanently dead e-stop/reset path. Python has no poison concept; the panicking call still dies exactly like the raising call. sync.rs

rust-side wart Controller soft limits are fetched, stored, logged — and never fed into the safety envelope. robot.rs:1011-1014, 1049-1050

Node & wire

rust-side wart NotRunning is dressed as UNKNOWN on the wire so the bridge publishes no reply (matching Python's silence) — semantically it is neither unknown nor an error. grpc.rs:72-76

rust-side wart The robot YAML is parsed up to 4× during bringup (typed, action_port, build_robot, from_yaml_str). main.rs:100-153

rust-side wart missed_deadlines == 0 guard exists only because 0.is_multiple_of(100) is true. telemetry.rs:162

ported bug wrench/wrench_frame are always null in flight-ring state records (FT sensor unported). telemetry.rs:366

rust-side wart set_vacuum with no capability replies false silently (ports getattr(robot, "set_vacuum", None)). node.rs:462

rust-side wart Foreign-robot-id datagrams are dropped silently and uncounted; malformed ones are counted. A mis-configured id looks like a dead link with zero diagnostics. actions.rs:341

rust-side wart Bare --id defaults to "1"; bulk lane drops newest on overflow; action-thread shutdown latency up to 200 ms; reconnect threads joined only at shutdown. main.rs:64 · grpc.rs:166 · actions.rs:263 · node.rs:391

5.3 · Doc ↔ code mismatches found while writing this (worth resolving)

Phase lock: PORTING.md Part III says "the config flag is removed, the code stays dormant" — but servoj_phase_lock still exists and is fully wired (config.rs:429, default false; consumed at robot.rs:729/1296/1315). Dormant only because it defaults off. Either remove the flag or fix the doc.

Stale proto comment: proto/robot_node.proto:56-58 still says orient_primitive "replies null — no DLS solve yet", but the solve shipped 2026-07-22 (kin.rs / orient.rs / robot.rs:2070).

Drifting line citations: PORTING.md cites fairino.py:763-764 for the e-stop interface-None quirk; it now lives at fairino.py:794-795.

Working tree right now: uncommitted tunes in fairino_fr3_remote_node_local.yaml (joint_speed 660→60, command_speed 220/660→770/720-760) and spacemouse_leader.yaml (force_scale ×2.5, k_lin 250→150, max_tcp_v 0.25→0.55) — with the inline comments now stale relative to the values. Worth committing or reverting deliberately, not by accident.

6 · What the GIL gave Python for free — and Rust spells out

The port's real translation work was not syntax — it was making implicit Python semantics explicit. The flagship example, side by side:

Python: servo7/robot/hardware/fairino.py:184 + :617 — one atomic reference assignment IS the mailbox

The Rust translation had to preserve three invisible properties

1 · Latest-wins overwriteMutex<Option<CommandSlot>>, producer replaces (stream.rs:200).

2 · Read-without-take. Python's consumer "reads the reference once" — it never removes it. A natural Rust Option::take() port would compile, pass casual tests, and silently change starvation accounting + trace record kinds. The slot is cleared only by reset() (stream.rs:207, 245).

3 · Staleness from t_mono, so a re-read inside the window is a fresh re-send, not a hold — pinned by a dedicated test (stream.rs:483).

This pattern — enumerate what the GIL guaranteed, then reproduce each guarantee deliberately — repeats across the port. The table below is the rest of the dictionary.

Python idiomRust constructThe trap it avoids
time.perf_counter() (one process-wide clock)the one injected SharedClock (safety/clock.rs)every Instant::now() mints a new epoch — per-thread clocks would land in the same dump offset by arbitrary constants
if not x: (None or empty)hand-expanded is_none() || is_empty(), reviewed one by oneif let Some(p) alone silently changes behavior on empty (e.g. the probe's no-measured-pose gate, robot.rs:1896)
"k" in d gates on the KEYde_limit_fail_closed: explicit null rejected at deserialize (config.rs:559)a single Option<T> collapses absent and null — turning Python's startup abort into a silently skipped safety guard
d.get(k) crashes on a non-dict nodepolicy_document reproduces the AttributeError (policy.rs:147)serde's Value::get returns None — a corrupted safety_policy.yaml would load EMPTY and turn every strict robot non-strict
np.where(mask, …, -inf) + argmaxnp_argmax: first max wins, NaN is maximal (guard.rs:41-57)tie-breaking must match index-for-index, not just value-for-value — metrics name the worst joint
exceptions poison nothinglock_unpoisoned = PoisonError::into_inner (sync.rs)default poisoning turns one ported ValueError panic into a permanently dead e-stop path
Tenacity Retryingthree hand-rolled loops with exact stop/wait/exhaustion semantics (robot.rs:988, 1847 · posture.rs:314)the subtle one: _reach_stream_ready returns false on exhaustion instead of raising
list[:n] truncatesexplicit take/get, never [..n]sim packets and defensive paths make short arrays real; slicing panics
f-string log textchar-for-char identical messages + msgfmt.rs Python-repr renderingoperators, toasts, and grep depend on message text (e.g. near_limit_code)
try/finally teardownrun-on-both-arms shutdown; flight dump on every exit path incl. SIGTERM/SIGHUP (main.rs:268, 298)an early-return ? between connect and teardown would skip disconnect and lose the dump
Python: fairino.py:782 — emergency_stop, the specification

Same function, one structural difference

Compare with the Rust version in Walkthrough B: logic, ordering, log text, and the interface-down return True quirk are identical. The one Rust-only subtlety is the latch write: Python's adjacent if status != X: status = X is GIL-atomic; the Rust version re-checks under the status lock (newly_latched block) so a concurrently latched e-stop is never overwritten by a stale snapshot — the "snapshot-then-write status race" from the §9 reviewer watchlist, and the same discipline read_state_now uses for its ERROR→CONNECTED transition (robot.rs:1345-1368).

If you remember one review heuristic from the port: wherever Python did check-then-write on shared state, look for the Rust re-check under the lock.

7 · What was cut, what was gained

Deliberate cuts (decisions, not omissions)

Drag / hand-guiding — gone entirely. Leaders are SpaceMouse-only. Even the defensive drag-release calls at the head of every posture walk were cut. Accepted residual risk: an arm left in drag from the vendor pendant fails the walk's Mode() with err=123, surfacing immediately; recovery is manual at the pendant. posture.rs:15-22

Force-torque sensor — gone. The follower never reads it; admittance lives leader-side. The CNDE stream still carries both wrench fields; state.rs ignores them.

Cameras/observations — not in the node. Shared memory dies across hosts; separate workstream.

Legacy ZMQ remote-robot path — deleted post-N-E. The Gate C+ shim work discovered it never carried prepare_command_stream at all — the old remote path never supported a real streaming arm.

Improvements beyond the 1:1 port (each measured)

Rates matched at 100 Hz — the old 70→50 mismatch manufactured a latency lattice, velocity ripple, and silently discarded ~¼ of setpoints.

Actions moved to UDP (2026-07-20 decision record): latest-wins fits UDP; TCP head-of-line blocking eliminated for the WAN; e-stop dual-pathed onto reliable gRPC.

Cross-host profiler — trace backfeed + in-band clock-offset estimation; the latency panel works without trusting either clock.

Phase-locked send tick — built, validated, currently dormant (owner-disabled 2026-07-21; see mismatch ① in §5.3). Shifts the send grid, never the spacing (±0.3 ms/tick cap).

Parked next: a bounded playout queue (depth ~5, hard age cap, drain-to-newest, e-stops bypass) in front of the mailbox — deliberately post-merge, since it departs from Python parity and needs its own differential scenarios before metal.

8 · How it was proven, and how to read latency numbers

The validation ladder

Golden fixtures per module — every Rust module tested against JSONL vectors generated by executing the Python implementation (floats to 1e-9 relative; control flow exact).

Gate B — connect/disconnect/posture walks/e-stop/reset produce identical SDK call sequences via recorder mocks on both sides.

Gate C — Python driver vs Rust driver against fresh SimMachines on identical recorded input; exact event-sequence equality on the injected-fault scenarios (starve→hold→halt, oversized jump, e-stop mid-stream, ServoJ rejection). 6/6.

N-B/N-C/N-D — node-level command parity, telemetry parity + cadence, then the WAN suite (netem 5/50/150 ms, jitter, 1-5% loss, hard cut): hold/halt behavior, reconnect semantics, zero event loss, measured e-stop latency. WAN 5/5.

N-E — live SpaceMouse sessions, one and two machines, SimMachine then real FR3. The remaining gate before rustmain is the owner's live checklist (PORTING.md Part III).

⚠ Old-vs-new latency comparisons mislead

If you A/B the old in-process stack against the node and the old one "looks" faster: it measured a shorter path. The old trace chain ends when a setpoint is handed to the sender's mailbox — before the wait for the next send slot and the hardware send, which is the dominant term. The node's numbers include everything up to the hardware send.

Measured honestly (traces joined to the flight-recorder command ring): the node is ~3× faster at the median, far better in the worst case, and an order of magnitude steadier on the send grid — while the old stack also silently discarded a quarter of setpoints to its rate mismatch.

Latency here is dominated by waiting for the next fixed send slot (a controller contract), not by processing — regularity and tail behavior are the meaningful metrics, not medians on a lucky day.

Also: for ~10 min after a mini-PC cold boot, NTP slew makes displayed latency creep ~1 ms/min and trace chains drop as "backwards" — harmless, self-limiting, let the box warm up.

Where to go deeper: rust/PORTING.md is the full contract — §8 sacred quirks, §9 the reviewer's bug-class watchlist, §13.4 the ported-bug ledger, Part II the node spec, Part III the plain-language handoff + pre-merge checklist. This document was generated from the code on branch rust (2026-07-22); line numbers reference that tree.