RobotDx › Reports › IO diagnostics v1: the first end-to-end build. Published copy, redacted. Taken out: 12 home paths, 12 hostnames, 82 serials, 2 videos replaced by a still, 33 screenshots with serials and machine names covered. Evidence for D03, D04, D07, D08, D09, D13. Written for Zach at the time, so it says “you”.
BAM Robotics · IO diagnostics · v1 handoff · 2026-10-22

IO diagnostics v1

v1 runs end to end on this PC, passively: the real host builds one tree from the OS (25 devices across USB, PCI, network, serial and video), and the real moteus_servo and intel_rgbd plugins hang their rows under it without opening an adapter or a camera.

The dashboard, the chip, the table, the activity log, the gated action dialog and the GNOME tray all render from the real host, and stock viewers (Lichtblick, ROS 2 Humble) show the same records. Not proved yet: the drivers' active checks (CAN queries, the RealSense camera_info read). Those are your run to make, with the rack powered and realsense-viewer closed (decision 4).

io-diagnostics: main, uncommittedbam_ws: branch claude/traj-tracking-sim-to-real-a3da60, 4 untracked plugin files, not mergedno git writes anywhere
Safety incident, read first. At about 08:46 PDT a mutation test removed --passive from the runner. The live-passive test then ran moteus_servo's ACTIVE diagnose once, for about 3 s, against fdcanusb XXXXXXXX.

Live on this PC (passive)

  • OS skeleton: 25 devices; chip IO ✓ with no plugin
  • PCIe link health on every function: all clean (no false positives)
  • moteus_servo: fdcanusb XXXXXXXX → CAN bus → CAN 1 / 14 / 15 from the rack inventory
  • intel_rgbd: both D405s with identity, USB link and their colour/depth stream rows
  • chip IO 3 ⚠ with the drivers passive (decision 2)

Replayed from recordings

  • F1 18:11 (plug): XXXXXXXX's bus error-passive, CAN 15 fault 33, Port 1-7 flapping, D405 moved 28 px
  • F2 18:25 (bus busy): tview held XXXXXXXX, so every check and button waits
  • F3 19:02 (after a restart); recordings older than 3× their period show as STALE
  • A fake rack PC: PCIe frame grabber, capture card x1 of x4, two EtherCAT NICs

Blocked: waits on you + the rack

  • The ACTIVE driver checks (hard rule: your run)
  • realsense-viewer (pid 2300632) holds both D405s, so intel_rgbd would say BusyHeldBy
  • On the rack: bounded replies on a real fdcanusb (lane D), and the SDK path: physical_port, asic_serial vs USB serial (lane E)
  • Lane D stays "unresolved" until that run

The dashboard, driven

One frame from the hero video. The video itself is left out of the public copy.

A still from the middle of the video. The video is not in the public copy: its frames show the machine's name and device serials.

Recorded by docs/record_v1_hero.js from the real host serving recorded frame F2 (18:25, tview holds XXXXXXXX), in headless Chrome. It shows: the top-bar chip and its causes; a cause opening the adapter's row (its own self-report is unknown: port held by tview, with a hint that CAN 15's driver below is in error); the driver-registered Run comms check button, blocked by the port holder with its run button disabled; the table; the activity log; and a search. 8/8 assertions passed during the recording. .

Five tree drawings, side by side, same frame

Same host, same records, and the same row selected in all five (at 18:11 the XXXXXXXX CAN bus, at 18:25 the CAN 15 driver). Only the tree renderer changes; the bottom-right switch in the app flips between them live and remembers the choice. All five follow your rules: chevrons only on hover, the status shape on the right, nothing green, greyed when not connected, and a row draws its own level. Pick one below; v1 is the default if you say nothing.

click any image for its shipped pixels
default
v1 tree at 1811
on hoverv1 hover
v1 tree at 1825
on hoverv1 hover

VS Code explorer: hover chevrons in a left gutter, type icon + name, muted ids, a status shape at the far right only on problem rows; rollups are a quiet dot.

v2 tree at 1811
on hoverv2 hover
v2 tree at 1825
on hoverv2 hover

Hover a row to swap its icon for the chevron; thin indent guides; a folded parent with problems shows a count pill ("2 ▲").

v3 tree at 1811
on hoverv3 hover
v3 tree at 1825
on hoverv3 hover

Only the paths that hold a problem open by themselves; each level's healthy rows fold into one muted "N OK" line; a black box shows its own level with a quiet "child in error" hint.

v4 tree at 1811
on hoverv4 hover
v4 tree at 1825
on hoverv4 hover

Type icon + name + id on the left; a right-edge rail that is invisible when OK and takes the problem shape and colour otherwise.

v5 tree at 1811
on hoverv5 hover
v5 tree at 1825
on hoverv5 hover

The tree is also the table: one nested name column, then quiet aligned PORT · ID · RATE · LAST SEEN · FIRMWARE columns, the status shape + message last, only when the row is not OK.

The 100 measured checks behind the five (all green)
check (hover a cell for the measured detail)v1v2v3v4v5
switch✓✓✓✓✓
no-green✓✓✓✓✓
shapes✓✓✓✓✓
right✓✓✓✓✓
chevrons✓✓✓✓✓
greyed✓✓✓✓✓
select✓✓✓✓✓
search✓✓✓✓✓
tabs✓✓✓✓✓
errors✓✓✓✓✓

100 passed · 0 failed · run 2026-10-22T16:00:24.383Z · viewport 1600×1000 · /usr/bin/node ~/io-diagnostics/tests/test_tree_variants.js

The whole dashboard

Whole window
The real host serving recorded frame F1 (18:11), proposal v1, CAN 15 driver selected from the chip. Click to see the shipped pixels.

Live on this PC, with the real drivers

This PC, right now, through the real host with the real driver plugins in --passive mode (sysfs, by-id links, /proc holders, the rack inventory and firmware cache files; no adapter or camera opened):

live tree, real drivers passive
The live dashboard, 1:1 (shot by tools/shoot_dashboard.js): the D405 carries intel_rgbd's identity, USB link and stream rows, and its two registered actions say who blocks them (realsense-viewer, pid 2300632); the fdcanusb carries moteus_servo's CAN bus.
The same tree as text, captured for this report — chip IO 3 ⚠, 25 devices
? rack-pc  25 devices  — RealSense D405 (USB XXXXXXXXXXXX): passive run: the SDK was not asked (sysfs only)
  ? USB bus 1 · 480 Mb/s  2 devices  — CAN bus: passive run: the bus was not queried
      Port 1-3  12 Mb/s  — 1 device · 12 Mb/s  [below: unknown]
        fdcanusb XXXXXXXX  XXXXXXXX  — on port 1-3 · USB full-speed · 12 Mb/s  [below: unknown]
          /dev/ttyACM0    — present
        ? CAN bus    — passive run: the bus was not queried
          ? CAN 1    — passive run: the board was not queried  (knock-on)
          ? CAN 14    — passive run: the board was not queried  (knock-on)
          ? CAN 15    — passive run: the board was not queried  (knock-on)
      Port 1-6  12 Mb/s  — 1 device · 12 Mb/s
        ITE Device    — on port 1-6 · USB full-speed · 12 Mb/s · no driver diagnostics
  ? USB bus 6 · 10 Gb/s  2 devices  — RealSense D405 (USB XXXXXXXXXXXX): passive run: the SDK was not asked (sysfs only)
      Port 6-1  5 Gb/s  — 1 device · 5 Gb/s  [below: unknown]
      ? RealSense D405 (USB XXXXXXXXXXXX)  XXXXXXXXXXXX  — passive run: the SDK was not asked (sysfs only)
          /dev/video0 · capture    — present
          /dev/video1    — present
          /dev/video2 · capture    — present
          /dev/video3    — present
          /dev/video4 · capture    — present
          /dev/video5    — present
          colour stream    — configured 640x480 rgb8 at 30 Hz (intel_rgbd RgbdConfig default); diagnostics never opens a stream
          depth stream    — configured 848x480 z16 at 30 Hz (intel_rgbd RgbdConfig default); diagnostics never opens a stream
      Port 6-2  5 Gb/s  — 1 device · 5 Gb/s  [below: unknown]
      ? RealSense D405 (USB XXXXXXXXXXXX)  XXXXXXXXXXXX  — passive run: the SDK was not asked (sysfs only)
          /dev/video6 · capture    — present
          /dev/video7    — present
          /dev/video8 · capture    — present
          /dev/video9    — present
          /dev/video10 · capture    — present
          /dev/video11    — present
          colour stream    — configured 640x480 rgb8 at 30 Hz (intel_rgbd RgbdConfig default); diagnostics never opens a stream
          depth stream    — configured 848x480 z16 at 30 Hz (intel_rgbd RgbdConfig default); diagnostics never opens a stream
  … (USB bus 7, PCI, network, tty: see the next section)

Why "?": in passive mode the drivers say unknown with code PassiveOnly ("the bus was not queried"), which the contract ranks above warn, so the chip reads IO 3 ⚠ with nothing wrong. That is decision 2 below.

Discovery beyond USB

The OS skeleton is passive and always there: 25 devices on this PC at capture time, chip IO ✓, with no driver plugin running. Every source is a file read (sysfs, by-id links, the udev database, the kernel log); nothing is opened.

sourcerowson this PC right nowwhat it reads
USB
io_facts.usb
9fdcanusb, ITE Device, Intel(R) RealSense(TM) Depth Camera 405, USB2.0 HUB, USB2.1 Hub, Logi Webcam C920e …sysfs /sys/bus/usb + usb.ids, by-id links, kernel log (plug / re-enumerate history)
PCI / PCIe
io_facts.pci
16IOMMU, FCH SMBus Controller, AD103 [GeForce RTX 4080 SUPER], Audio device, NVMe SSD Controller PM9A1/PM9A3/980PRO, MT7922 802.11ax PCI Express Wireless Network Adapter …sysfs /sys/bus/pci: class, driver, link width/speed vs max, AER error counters
Network
io_facts.netdev
2wlp5s0 (link down), enp6s0 (1 Gb/s · link up)/sys/class/net: link, speed, driver; an EtherCAT master is detected, the segment is not scanned
Serial ttys
io_facts.tty
1/dev/ttyACM0/sys/class/tty + /dev/serial/by-id, hung under the USB or platform device
video4linux
io_facts.v4l
14/dev/video0 · capture, /dev/video1, /dev/video2 · capture, /dev/video3, /dev/video4 · capture, /dev/video5 …/sys/class/video4linux + /dev/v4l/by-id, hung under the camera

PCIe link health, this PC

Width below the card's maximum → WARN PcieLinkNarrow (speed is ignored: a GPU drops speed at idle). AER correctable → WARN, nonfatal/fatal → ERROR. A likely frame grabber with no driver bound → WARN NoKernelDriver. On this PC every link is clean, so the chip stays quiet: no false positives.

functionlinkAER corr / nonfatal / fatallevel
AD103 [GeForce RTX 4080 SUPER]x16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)0 / 0 / 0OK
Audio devicex16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)0 / 0 / 0OK
NVMe SSD Controller PM9A1/PM9A3/980PROx4 of x4 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)0 / 0 / 0OK
MT7922 802.11ax PCI Express Wireless Network Adapterx1 of x1 · 5.0 GT/s PCIe (max 5.0 GT/s PCIe)0 / 0 / 0OK
RTL8111/8168/8211/8411 PCI Express Gigabit Ethernet Controllerx1 of x1 · 2.5 GT/s PCIe (max 2.5 GT/s PCIe)0 / 0 / 0OK
USB controllerx1 of x1 · 2.5 GT/s PCIe (max 2.5 GT/s PCIe)0 / 0 / 0OK
600 Series Chipset SATA Controllerx1 of x1 · 2.5 GT/s PCIe (max 2.5 GT/s PCIe)0 / 0 / 0OK
Raphaelx16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)— / — / —OK
Rembrandt Radeon High Definition Audio Controllerx16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)— / — / —OK
Family 19h PSP/CCPx16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)— / — / —OK
USB controllerx16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)— / — / —OK
USB controllerx16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)— / — / —OK
Family 17h/19h HD Audio Controllerx16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)— / — / —OK
USB controllerx16 of x16 · 16.0 GT/s PCIe (max 16.0 GT/s PCIe)— / — / —OK

Hardware this PC does not have: the fake rack machine

Lane B's sysfs spec (tests/data/sysfs-rack-fixture.json) served through the same io_facts sources and the real dashboard: a PCIe frame grabber with no kernel driver, a capture card that trained x1 of x4, two I210 NICs carrying EtherCAT segments (detected, not scanned), and a USB-serial adapter.

fake machine: frame grabber detail
1:1: the grabber row says it has no kernel driver bound and 3 correctable AER errors since boot; the capture card's own row warns it trained x1 of x4.
search EtherCAT
Search "EtherCAT": both segments, under their NICs.
The fake machine's PCI subtree, as the host printed it
▲ PCI root bus 0000:00  8 devices  — Likely frame grabber · Coreco Inc: likely frame grabber / capture card with no kernel driver bound: it cannot capture until its vendor driver loads
  ▲ Likely frame grabber · Coreco Inc    — likely frame grabber / capture card with no kernel driver bound: it cannot capture until its vendor driver loads
    I210 Gigabit Network Connection    — Network controller · kernel driver igb · no driver diagnostics
      enp6s0 · EtherCAT segment (not scanned)    — 100 Mb/s · link up
    I210 Gigabit Network Connection    — Network controller · kernel driver igb · no driver diagnostics
      enp7s0 · EtherCAT segment (not scanned)    — 100 Mb/s · link up
  ▲ Pro Capture Endpoint    — trained narrower than the card can: x1 of x4 · 2.5 GT/s PCIe (max 2.5 GT/s PCIe) (reseat or another slot)
      /dev/video20 · capture    — present
    USB controller    — Serial bus controller · kernel driver xhci_hcd · no driver diagnostics
    USB controller    — Serial bus controller · kernel driver xhci_hcd · no driver diagnostics
    ACP/ACP3X/ACP6x Audio Coprocessor    — Multimedia controller · kernel driver snd_pci_acp6x · no driver diagnostics
    USB controller    — Serial bus controller · kernel driver xhci_hcd · no driver diagnostics

The black-box rule, on a real tree

Zach's rule (2026-10-22): a black box reports for itself. A row draws its own entity's self-report and the host never overrides it. What is worse further down is an advisory hint. A row with nothing to report itself (the host, a USB bus) shows the worst thing below it. And when a parent has failed, the failures under it collapse into that one cause. Below is the real host's output for recorded frame F1, row by row along the path to CAN 15's fault, from io-diagnostics once --json --fixture F1:

row (host → … → CAN 15 driver)its own self-reportworst below (rollup)the row showswhy
rack-pc host · rootnone (grouping row)ERROR XXXXXXXX CAN busERRORa pure grouping row has no self-report, so it shows the worst below
USB bus 1 · 480 Mb/s bus · vianone (grouping row)ERROR XXXXXXXX CAN busERRORa pure grouping row has no self-report, so it shows the worst below
Port 1-3 port · viaOK 1 device · 12 Mb/sERROR CAN 15 driverOK ▾ error belowits own report wins; the worse level below is only a quiet hint
fdcanusb XXXXXXXX device · viaOK on port 1-3 · USB full-speed · 12 Mb/sERROR CAN 15 driverOK ▾ error belowits own report wins; the worse level below is only a quiet hint
XXXXXXXX CAN bus bus · viaOK error-active · err 0 · warn 0 · 3 of 3 answerERROR CAN 15 driverOK ▾ error belowits own report wins; the worse level below is only a quiet hint
CAN 15 node · viaOK answers · 36.5 V · stoppedERROR CAN 15 driverOK ▾ error belowits own report wins; the worse level below is only a quiet hint
CAN 15 driver part · part_ofERROR fault 33 (motor driver) on bridge enable · last trip 13:34:07—ERRORit reports this itself
The same subtree as the CLI prints it (✖ is the row's own error; [below: …] is the hint)
  Port 1-3  12 Mb/s  — 1 device · 12 Mb/s  [below: error]
    fdcanusb XXXXXXXX  XXXXXXXX  — on port 1-3 · USB full-speed · 12 Mb/s  [below: error]
      /dev/ttyACM0    — present
      XXXXXXXX CAN bus    — error-active · err 0 · warn 0 · 3 of 3 answer  [below: error]
        CAN 1  XXXXXXXXXXXXXXXX  — answers · 36.5 V · stopped
          CAN 1 driver    — 36.5 V · fault 0
          CAN 1 motor    — 10:1 (GIM4305-10; camera-measured 0.104 output/rotor) · output sign 1 · tag 0
        CAN 14  XXXXXXXXXXXXXXXX  — answers · 36.5 V · stopped
          CAN 14 driver    — 36.5 V · fault 0
          CAN 14 motor    — 10:1 (GIM4305-10) · output sign -1 · tag 1, 21
        CAN 15  XXXXXXXXXXXXXXXX  — answers · 36.5 V · stopped  [below: error]
        ✖ CAN 15 driver    — fault 33 (motor driver) on bridge enable · last trip 13:34:07
          CAN 15 motor    — 10:1 (GIM4305-10) · output sign 1 · tag 2
Knock-ons collapse into one cause: this PC right now, with the real moteus_servo plugin in --passive mode
rowown reportcounted as
CAN busUNKNOWN passive run: the bus was not queriedthe one cause (counted)
CAN 1UNKNOWN passive run: the board was not queriedknock-on of CAN bus (not counted)
CAN 14UNKNOWN passive run: the board was not queriedknock-on of CAN bus (not counted)
CAN 15UNKNOWN passive run: the board was not queriedknock-on of CAN bus (not counted)

Chip on that run: IO 3 ⚠, counting only RealSense D405 (USB XXXXXXXXXXXX), RealSense D405 (USB XXXXXXXXXXXX), CAN bus. The three boards under the unqueried bus are knock-ons, so they collapse into the bus instead of tripling the count.

The plugin contract: what a manufacturer writes

A manufacturer writes one executable and one TOML file. The executable answers three verbs with JSON lines on stdout; the host never imports it, so its dependencies, language and crashes stay its own (it runs in the driver's own uv environment as a subprocess). The smallest real example, examples/acme_gripper/, run through the real host by tests/test_integration.py (which also fails if the README drifts from these files):

acme_gripper.py — stdlib only
"""The smallest IO diagnostics plugin: a made-up USB gripper (1234:5678) with one part, as JSON lines.

    <python> acme_gripper.py describe
    <python> acme_gripper.py diagnose --device usb:1-4

A real plugin reads its device here (inside its own driver's environment); this one only shows the shapes.
"""

import json
import os
import sys
import time

SCHEMA = "diagnostics-record/0"


def emit(**record):
    print(json.dumps({"schema": SCHEMA, **record}))


if sys.argv[1] == "describe":  # what it claims, and the buttons it registers (none here)
    print(json.dumps({"name": "acme_gripper", "version": "1.0", "schema": SCHEMA,
                      "matches": ["usb:1234:5678"], "actions": []}))
elif sys.argv[1] == "diagnose":  # one run for one device; paths are relative to the device's own row ("")
    device = json.loads(os.environ.get("IO_DIAGNOSTICS_DEVICE", "{}"))  # the host's skeleton dict for it
    now = time.time()
    emit(record="entity", path="", name=f"Acme gripper {device.get('serial') or ''}".strip(), kind="device",
         identity={"manufacturer": "Acme", "model": "Gripper G1", "sw_version": "2.3.0"})
    emit(record="entity", path="jaw", name="jaw", kind="part", part_of="")
    emit(record="status", path="", name="gripper: comms", level="ok", message="answers in 2 ms",
         stamp=now, expected_period_s=30)
    emit(record="status", path="jaw", name="jaw: temperature", level="warn", code="JawHot",
         message="jaw motor 71 °C (warn above 70 °C)", stamp=now, expected_period_s=30,
         measurements=[{"key": "temperature", "value": 71.0, "unit": "Cel", "warn_high": 70.0}])
acme_gripper.toml — dropped into plugins.d/
name = "acme_gripper"
command = ["{python}", "{here}/acme_gripper.py"]
timeout_s = 10
period_s = 30
matches = ["usb:1234:5678"]
what diagnose --device usb:1-4 printed (schema key dropped)
{"record": "entity", "path": "", "name": "Acme gripper G1-0042", "kind": "device", "identity": {"manufacturer": "Acme", "model": "Gripper G1", "sw_version": "2.3.0"}}
{"record": "entity", "path": "jaw", "name": "jaw", "kind": "part", "part_of": ""}
{"record": "status", "path": "", "name": "gripper: comms", "level": "ok", "message": "answers in 2 ms", "stamp": 1792685757.684107, "expected_period_s": 30}
{"record": "status", "path": "jaw", "name": "jaw: temperature", "level": "warn", "code": "JawHot", "message": "jaw motor 71 °C (warn above 70 °C)", "stamp": 1792685757.684107, "expected_period_s": 30, "measurements": [{"key": "temperature", "value": 71.0, "unit": "Cel", "warn_high": 70.0}]}
verbprintswho calls it, v1
describeone object: name, version, schema diagnostics-record/0, matches, registered actionsthe host, once (must be passive)
diagnose --device <id>JSON lines tagged entity / status / event (never fault: the host's FaultManager keeps faults)the host, every period_s per matched device, killed past timeout_s
act --device <id> --action <n> --confirm <token>a result recordnobody in v1: buttons are shown with their gates, never executed

Matches: usb:VID:PID · pci:VENDOR:DEVICE · pci-class:… · by-id:<glob> · tty:<glob> · v4l:<glob> · netdev:<glob>, in the TOML or returned by describe. TOML keys: name, command, cwd, timeout_s, period_s, matches, enabled, env, diagnose_args, description. What the host does for you: the device row (port, VID:PID, serial, link speed, plug history) already exists; your rows hang under it; a check not re-sent within max(3 × expected_period_s, 5 s) turns STALE on the host's clock; a crash, a hang or a malformed line becomes a check on the device row, never a dead dashboard.

The two real driver plugins, as they describe themselves

Captured for this report through passive_guard.py (which lets only describe and diagnose --passive through). These are the bam_ws files on branch claude/traj-tracking-sim-to-real-a3da60.

moteus_servo — fdcanusb adapters + moteus boards

moteus_servo 0.1.0 · schema diagnostics-record/0 · matches usb:0483:5740, by-id:*fdcanusb*

actiondangerscopepreconditionswhat it does
Run comms check
run_comms_check
querydeviceport_free; bus_lock_freeAddressed scan of every id, reply loss over a few queries, read-only config and identity, and the adapter's can status. Query frames and console reads only: no board changes mode.
Clear faults
clear_faults
changesnodeport_free; bus_lock_free; board_answersSend stop (mode 0) to one board; moteus clears a latched fault on stop. The motor stays unpowered.
Reboot board
reboot_board
interruptsnodeport_free; bus_lock_free; board_answersSend `d reset`: the board restarts and is back on the bus in about a second. Any session using it fails.
raw describe output
{
 "name": "moteus_servo",
 "version": "0.1.0",
 "schema": "diagnostics-record/0",
 "description": "fdcanusb CAN-FD adapters and the moteus boards on their bus: ids, identity, firmware vs the latest release, read-only config, can status, reply loss",
 "matches": [
  "usb:0483:5740",
  "by-id:*fdcanusb*"
 ],
 "commands": [
  "describe",
  "diagnose",
  "act"
 ],
 "path_root": "--path, else $IO_DIAGNOSTICS_DEVICE_PATH, else fdcanusb-<serial>; the bus is <root>/can (part_of the adapter), each board <root>/can/<id> (via the bus)",
 "global_options": [
  "--inventory DIR",
  "--firmware-cache DIR",
  "--bus-lock FILE",
  "--expect-ids LIST",
  "--expected-period S",
  "--timeout S"
 ],
 "actions": [
  {
   "schema": "diagnostics-record/0",
   "record": "action",
   "name": "run_comms_check",
   "title": "Run comms check",
   "description": "Addressed scan of every id, reply loss over a few queries, read-only config and identity, and the adapter's can status. Query frames and console reads only: no board changes mode.",
   "danger": "query",
   "scope": "device",
   "confirm": "Query every id on {name} and read bus health? No board changes mode.",
   "preconditions": [
    [
     "port_free",
     "No other process has the adapter open (tview, moteus_tool, a peer)"
    ],
    [
     "bus_lock_free",
     "The rack bus lock is free (no experiment running)"
    ]
   ],
   "kind": "touches"
  },
  {
   "schema": "diagnostics-record/0",
   "record": "action",
   "name": "clear_faults",
   "title": "Clear faults",
   "description": "Send stop (mode 0) to one board; moteus clears a latched fault on stop. The motor stays unpowered.",
   "danger": "changes",
   "scope": "node",
   "confirm": "Send stop to CAN {id} on {name}? A latched fault clears; the motor stays unpowered.",
   "preconditions": [
    [
     "port_free",
     "No other process has the adapter open (tview, moteus_tool, a peer)"
    ],
    [
     "bus_lock_free",
     "The rack bus lock is free (no experiment running)"
    ],
    [
     "board_answers",
     "CAN {id} answers a query on this bus"
    ]
   ]
  },
  {
   "schema": "diagnostics-record/0",
   "record": "action",
   "name": "reboot_board",
   "title": "Reboot board",
   "description": "Send `d reset`: the board restarts and is back on the bus in about a second. Any session using it fails.",
   "danger": "interrupts",
   "scope": "node",
   "confirm": "Reboot CAN {id} on {name}? It drops off the bus for about a second; any session using it fails.",
   "preconditions": [
    [
     "port_free",
     "No other process has the adapter open (tview, moteus_tool, a peer)"
    ],
    [
     "bus_lock_free",
     "The rack bus lock is free (no experiment running)"
    ],
    [
     "board_answers",
     "CAN {id} answers a query on this bus"
    ]
   ]
  }
 ]
}

intel_rgbd — Intel RealSense cameras

intel_rgbd 0.1.0 · schema diagnostics-record/0 · matches usb:8086:0b* (dict form)

actiondangerscopepreconditionswhat it does
Check firmware
check_firmware
querydevicecamera_free; bus_lock_freeRead the installed and the SDK's recommended firmware through camera_info (no stream, no option read), while no other process holds a RealSense and the bus lock is free.
Hardware reset…
hardware_reset
interruptsdevicecamera_free; bus_lock_freeRealSenseCamera.hardware_reset(): the camera drops off USB for about 3 s, re-enumerates, and every option returns to its firmware default (measured on the D435: exposure 250 -> 166, auto-exposure back on). Anything streaming from it must start again.
raw describe output
{
 "name": "intel_rgbd",
 "title": "RealSense camera",
 "version": "0.1.0",
 "schema": "diagnostics-record/0",
 "matches": [
  {
   "bus": "usb",
   "vid": "8086",
   "pid": "0b*"
  }
 ],
 "actions": [
  {
   "schema": "diagnostics-record/0",
   "record": "action",
   "name": "check_firmware",
   "title": "Check firmware",
   "description": "Read the installed and the SDK's recommended firmware through camera_info (no stream, no option read), while no other process holds a RealSense and the bus lock is free.",
   "danger": "query",
   "scope": "device",
   "confirm": "",
   "preconditions": [
    [
     "camera_free",
     "No other process holds a RealSense and realsense-viewer is closed"
    ],
    [
     "bus_lock_free",
     "No rack experiment holds the bus lock"
    ]
   ],
   "kind": "touches"
  },
  {
   "schema": "diagnostics-record/0",
   "record": "action",
   "name": "hardware_reset",
   "title": "Hardware reset…",
   "description": "RealSenseCamera.hardware_reset(): the camera drops off USB for about 3 s, re-enumerates, and every option returns to its firmware default (measured on the D435: exposure 250 -> 166, auto-exposure back on). Anything streaming from it must start again.",
   "danger": "interrupts",
   "scope": "device",
   "confirm": "Hardware-reset {name} ({serial})? It drops off USB for about 3 s and every option returns to its firmware default.",
   "preconditions": [
    [
     "camera_free",
     "No other process holds a RealSense and realsense-viewer is closed"
    ],
    [
     "bus_lock_free",
     "No rack experiment holds the bus lock"
    ]
   ]
  }
 ],
 "icon": "camera",
 "sdk": "pyrealsense2 2.58.3.10794",
 "period_s": 60.0,
 "device_ids": "RealSense serial, USB serial, USB port (6-2), usb:8086:<pid>:<USB serial>, /dev/videoN, a /dev/v4l/by-id link, a sysfs path or a host tree path (host/usb-6-2/...)",
 "confirm_token": "hardware_reset takes --confirm <the camera's RealSense serial>: identity.serial_number, the {serial} in its confirm text"
}

Both are declared in plugins.d/ with enabled = false (their default diagnose talks to the hardware, behind a port-holder check and the rack bus lock); --enable NAME turns one on for a single run.

Interop: our records in stock viewers

Decision 1 of the build plan: own the dashboard, and stay backwards compatible through one pure adapter (contract.record.to_diagnostic_status) that strips our added fields. A stock viewer then shows our records with zero code of ours inside it.

One frame from the hero video. The video itself is left out of the public copy.

A still from the middle of the video. The video is not in the public copy: its frames show the machine's name and device serials.

Lichtblick (open source, run locally in docker): its stock Diagnostics Summary + Detail panels on our /diagnostics topic, served by our foxglove-sdk bridge. 10 stock rows, levels coloured, stale rows greyed (CAN 14).
Lichtblick summary
Lichtblick's own Summary panel, 1:1: error red (CAN 15 fault 33), warn yellow (firmware update), OK green; the CAN 14 row is greyed by Lichtblick from the stock STALE level our adapter sends.
Lichtblick stale
Its Detail panel on that row: our adapter's stale message ("no update for 52 s") and our values (serial, firmware, station) as plain stock key/values.

ROS 2 Humble (docker, no network): 5/5 checks

✓ decoderclpy read 2337 bytes -> 10 statuses, equal to the bridge's JSON
✓ encode2337 bytes, identical except 75 of 159 alignment padding bytes (rclpy/Fast-CDR leaves padding unwritten; ours is zero)
✓ schema5 sections vs 5 installed .msg files; every field and constant matches
✓ transportreceived 10 statuses over DDS from the publisher process
✓ contractfrom_diagnostic_status read 10 rclpy statuses (level as bytes); to_diagnostic_status gave the same stock statuses back
ros2 topic echo /diagnostics (first 40 lines)
header:
  stamp:
    sec: 1792677600
    nanosec: 0
  frame_id: ''
status:
- level: "\0"
  name: 'fdcanusb XXXXXXXX: USB'
  message: on port 1-3 as /dev/ttyACM0
  hardware_id: usb:0483:5740:XXXXXXXX
  values:
  - key: port
    value: 1-3
  - key: tty
    value: /dev/ttyACM0
  - key: by-id
    value: /dev/serial/by-id/usb-mjbots_fdcanusb_272813EF-if00
  - key: link_speed
    value: 12 Mbit/s
- level: "\0"
  name: 'CAN bus: state'
  message: error-active, no bus errors
  hardware_id: usb:0483:5740:XXXXXXXX
  values:
  - key: state
    value: error-active
  - key: tx_error_count
    value: '0'
  - key: rx_error_count
    value: '0'
- level: "\0"
  name: 'CAN 1: comms'
  message: answers · 36.5 V · stopped
  hardware_id: moteus:32bb145a
  values:
  - key: serial
    value: XXXXXXXXXXXXXXXX
  - key: firmware
    value: 1.1.2 (657b5c5)
  - key: station

Plus tests/test_integration.py: the real host serves F1 → lane C's --from-host source → stock array with our fields stripped → CDR and back. Not proven: diagnostic_aggregator (not in the image).

Tests and evidence

suiteresultnotes
io-diagnostics pytest141 passedre-run 2026-10-22 09:10 while building this report (10.5 s); was 119 + 1 xfail before integration. 22 new in tests/test_integration.py, two of them live on rack-pc
moteus_servo plugin tests35 passedre-run 2026-10-22 09:12 for this report; fakes only
intel_rgbd package suite125 passed · 18 skippedre-run 2026-10-22 09:12 for this report, through the device-less pyrealsense2 shadow (tools/safe_pytest_intel_rgbd.py)
tree proposals (node)100 / 100tests/test_tree_variants.js: 10 checks × 5 variants × 2 frames, measured in headless Chrome
dashboard proof shots (node)24 / 24tools/shoot_dashboard.js: F1, F2, live with real drivers passive, fake machine
hero recording8 / 8docs/record_v1_hero.js: the flow in the video, asserted while it was recorded
ROS 2 Humble5 / 5decode, encode, schema, transport, contract + ros2 topic echo
Lichtblick10 stock rowscolours checked per row; its own containers and ports cleaned up
mutation checksred when brokendropping diagnose_args, removing the intel alias, removing pci_statuses, changing the README example each turned a guarded test red

The pytest counts above were re-run while this report was built, rather than copied from a lane's message. The node counts come from their own result files (tree-variants-report.json, shots.json, hero.json, verdict.json).

Decisions: what needs you

Each one has a recommendation and the reversible default that happens if you say nothing. Your choices are kept in this browser only. Copy puts them on the clipboard as text you can paste into the chat.

1
Tree drawing

Pick in the strip above. Recommendation: v1 Quiet IDE tree. Its main job is plug-and-see (which port, what it is), so every healthy row stays visible but quiet, and the chip already answers "what needs me". v3's healthy-row folding could become a toggle later.

default if silent: v1
2
Chip noise in passive mode

With the drivers in --passive mode, every driver row says unknown/PassiveOnly and the chip reads 'IO 3 ⚠' with nothing wrong. The contract ranks unknown above warn, so it is honest but noisy.

3
USB buses under their xHCI controllers?

Today 'USB bus 1…8' are siblings of 'PCI root bus 0000:00'; the true wire is host → PCIe → xHCI function → USB bus. Lane B already records the controller in attrs.

4
Your live ACTIVE run

The one run that talks to the hardware is yours (hard rule). realsense-viewer (pid 2300632) holds both D405s, so intel_rgbd would report BusyHeldBy. Still to prove on the rack: bounded replies on a real fdcanusb (lane D) and the SDK path (physical_port, asic_serial vs USB serial, lane E).

5
The 08:46 safety incident

A mutation test ran moteus_servo's ACTIVE diagnose once (~3 s, query-only, behind its gate). passive_guard.py now refuses any diagnose without --passive and any act, independent of the host; re-verified with the same mutation (refused in 0.011 s).

6
Landing

The orchestrator commits after review. io-diagnostics goes on its main: the code, tests/fixtures/plugins/v6_frames.json (271 KB), and the new examples/ and tools/ files. reports/media/ is gitignored and stays local. bam_ws gets two commits on claude/traj-tracking-sim-to-real-a3da60, one per driver, so they can be cherry-picked. Merging that branch is your call.

default if silent: committed on the branch, not merged

Left, and deliberately not done

Done and proved: everything in the green column above, plus the integration fixes. intel_rgbd now accepts the host's own skeleton id usb:6-1; before the fix, every host run ended in DeviceNotFound. once now judges live records at the end of the runs, not at the start. And lane B's io_facts/usb.py fixes are in: a 1.5 Mb/s device no longer breaks the USB source, and hubs no longer list the camera's video nodes. The gaps closed during the completeness check: PCIe link health (tree.pci_statuses), --machine SPEC.json for hardware this PC lacks, --enable NAME and diagnose_args, one command through a hatchling build backend, an end-to-end host → bridge → CDR test, and a README whose manufacturer example is tested against the files.

Not done, all non-blocking:

Deliberately not done: no git writes (the orchestrator commits after review). No active hardware run (hard rule: yours). Actions are never executed in v1, by design. The tray was never launched on your desktop. EtherCAT segments are detected but not scanned (as the build plan says). The record contract was not edited, because it is frozen at c9784d0.

Run it

Fixture demo: the 18:25 bus-busy frame on http://127.0.0.1:8812/ (the CAN boards turn stale about 11 s in)

uv run --project ~/io-diagnostics io-diagnostics serve --fixture F2 --offset 330

Live, passive: this PC's real skeleton, no driver plugin runs

uv run --project ~/io-diagnostics io-diagnostics serve

Live with the real drivers in --passive mode (behind passive_guard.py)

uv run --project ~/io-diagnostics io-diagnostics serve --plugins ~/io-diagnostics/tests/fixtures/plugins-live-passive

Hardware this PC lacks (frame grabber, x1-of-x4 capture card, EtherCAT NICs)

uv run --project ~/io-diagnostics io-diagnostics serve --machine ~/io-diagnostics/tests/data/sysfs-rack-fixture.json

The top-bar chip (tray) + dashboard

uv run --project ~/io-diagnostics io-diagnostics tray

Your ACTIVE run: close realsense-viewer and tview first, with the rack powered

uv run --project ~/io-diagnostics io-diagnostics once --json --enable moteus_servo --enable intel_rgbd

Rebuild this report (add --recapture for fresh passive captures, --record-hero for a new video)

uv run --project ~/io-diagnostics --with pillow python ~/io-diagnostics/docs/build_v1_report.py

Built by docs/build_v1_report.py on 2026-10-22T09:21:22-07:00. Host captures were taken 2026-10-22T09:15:56-07:00 on rack-pc through io-diagnostics once --json, and F1 was judged at its own frame clock (2026-10-21T18:11:50-07:00). The images are WebP crops, cut from the proof shots named in each caption.