pstop: consolidated working branch (restructure re-land + README + CI bumps) - #58
Draft
iliabaranov wants to merge 55 commits into
Draft
pstop: consolidated working branch (restructure re-land + README + CI bumps)#58iliabaranov wants to merge 55 commits into
iliabaranov wants to merge 55 commits into
Conversation
Squash of the reverted #53 restructure (65273ed) and the parked PR #57 README fix (mermaid diagram back, monorepo-correct layout table, latest status folded in). Re-landed on a working branch, NOT main: main stays on the pre-restructure tree until the downstream breakage that forced the revert (c35bd95) is identified and fixed here first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Squash of dependabot PRs #54/#55/#56: actions/checkout 4->7, actions/upload-artifact 4->7, actions/setup-python 6->7. The "pstop_c coverage" failures on those PRs were NOT the bumps: dependabot-triggered runs get no repo secrets, so BULLSEYE_LICENSE_KEY was empty and the installer died. Guard the job to skip for dependabot[bot] instead of failing red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
This was referenced Jul 23, 2026
…ents Mirror of the machine side's many-remote support: the remote now heartbeats up to DCS_PSTOP_MAX_MACHINES (4) machines as fully independent sessions. NO changes to the certification-track pstop_c library — each session simply owns its own pstop_c protocol_data_t. firmware/main/main.c (the auditable safety core): - Per-machine session array: own socket (local ports 8891..8894, so egress binding stays per peer type — Tailscale peers source-bind to the VPN IP, LAN peers don't; the 2026-07-20 fail-open lesson holds per machine), own bond/counter state, own reply-loss watchdog. - The blocking bond_with_retry() is gone: bonding is a non-blocking per-session state machine driven inside the 10 Hz tick (one BOND in flight, 5 s retry, drain-before-bond preserved), so one unreachable machine never stalls heartbeats to the others. - Lockstep now covers N encodings per tick: each core samples its E-stop loop ONCE, then encodes one message per bonded machine; the comparator memcmps every pair and a mismatch on ANY slot silences ALL sessions (lockstep integrity is a device property). - Replies demuxed per-session socket with source-address validation; select() bounds the reply wait regardless of machine count. - Transport priority-health: true on any reply; the repeated unhealthy kick (needed to force WG past a zombie keypair) fires on the old 5 s cadence only when EVERY configured machine is silent — a partially healthy device never churns the shared transport. - STOP and the arming press-and-release broadcast to all bonded machines; each machine still enforces its own min_stop_ms veto. dcs_support: - NVS ps_peers blob (byte-serialized, versioned) with migration from the legacy ps_ip/ps_port pair into slot 0; slot 0 mirrors back to the legacy keys so a firmware rollback keeps its primary machine. - POST /api/pstop_peers?slot=N&ip&port[&id] / ?slot=N&clear=1 — applies live within one tick; /api/pstop_peer still works (= slot 0) so existing fleet tooling is untouched. - /state.json: pstop_machines[4] with per-session cfg/endpoint/state/ counters; legacy pstop_* scalars stay as aggregates (worst-of last_msg, summed counters). JSON_CAP 2304 -> 3072. - LED ring: divides evenly among configured slots in slot order from LED 1 (one machine = whole ring, unchanged look); per-segment colours as before; device-level states (lockstep purple, OTA, locate) still override the whole ring. First cut — display design expected to evolve. tools/pstop_multi_machine_test.py: HIL harness — N unmodified machine_app_runner instances on this host, chip pointed at them via the API, asserts session isolation (kill one machine -> only that session re-bonds, others' replies keep climbing), recovery, slot clear, and zero lockstep mismatches throughout. Restores the bench unit's original peer afterwards. Builds clean under IDF 5.5; host runner untouched and building; MISRA delta on main.c limited to already-registered deviation classes. HIL validation on real hardware still pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
A steady green or red ring is indistinguishable from a frozen display. Overlay the boot spinner's comet shape as a subtle brightness wave on OK and STOP segments: head at 160% of base with a fading tail, far pixels at 85%, chasing the whole ring at the boot-spinner cadence so multiple machine segments share one coherent wave. Unreachable pulse, bond blue, and the device-level overrides are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Design intent: the MACHINE decides the update rate its safety case needs. pstop_c already supports this end to end — remote_details_t .heartbeat_ms (machine.toml default_heartbeat_ms / per-operator heartbeat_ms) is enforced by machine_check_heartbeats AND advertised to the operator in every reply's heartbeat_timeout field, bond ack included (machine.c). The gap was only on our remote, which ignored the field and always transmitted at the raw 10 Hz tick. Remote (main.c): each session adopts the machine's advertised window and transmits every heartbeat_timeout/2 (2x margin so jitter never costs the machine a whole window), clamped to [100 ms tick, 1000 ms]. Sends are decimated ticks; the per-session counter advances only on transmitting ticks so the machine sees contiguous counters (protocol.c tolerates gaps only up to max_lost_messages+1). The E-stop loop sampling stays at the fixed 10 Hz lockstep tick via a dedicated roll counter — the safety input cadence never slows down. The reply-loss watchdog scales with the adopted period (4 missed replies, floored at the validated 1500 ms). machine.toml: documents that heartbeat_ms is the machine-owned rate and retunes the remote with no remote-side configuration. state.json pstop_machines[] gains hb_ms (the window each session has adopted). No pstop_c changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Bench feedback: 85% base was too subtle. Dim the non-comet pixels to 55% of base and lengthen the tail (170/135/105/80/65 -> 55), so the wave reads clearly at a glance while the state colour stays dominant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
The adopted heartbeat window was applied correctly (HIL: 2 Hz sends against a 1000 ms machine window) but the atomic store for the pstop_machines[].hb_ms field was missing, so /state.json reported 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Every line now states its direction from the machine's perspective so two-node tests read unambiguously: - RX <- remote 0x... (what a pstop sent us; verbose adds cnt/rcnt/crc) - TX -> remote 0x... (our reply; verbose adds counter + advertised hb) - *** ROBOT STATUS / ARMED *** for the actuation-relevant transitions - STATE ... (trigger: RX <msg> from remote 0x...) for the arming cycle Startup banner names the role, machine_id, and the log grammar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Root cause of remotes showing offline/idle on the fleet console while fully healthy on the data plane (all 3 chips froze at one control-server stream drop 2026-07-24T03:52Z and stayed control-plane-offline ~17 h; diagnosed jointly with the fleet-side session): 1. When control ends the streaming map request but the TCP+Noise session stays up, GOAWAY / RST_STREAM(5) / END_STREAM / closing trailers were silently skipped — the chip never re-issued the MapRequest, and Tailscale marks a node online only while that stream is open. 2. The 120 s control watchdog was fed by ANY received frame, so the ACKs to our own H2 PINGs (stream 0) kept it fed forever. This is the surviving sibling of the 9cffb97 half-open fix: that one stopped SENT pings from feeding the watchdog; received PING ACKs still masked a dead map stream. Now: explicit stream termination returns connection-lost (immediate reconnect), and only stream-5 traffic feeds the watchdog. Control's keepalive MapResponses (~60 s, we request KeepAlive=true) feed a healthy stream with 2x margin against the 120 s watchdog; a dead one trips it and self-heals within 2 min instead of never. Fleet-console side (deriving "online" from check-in age + not evicting check-in-fresh devices from the live poll) is tracked in the pstop-fleet repo, not here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Documents the two external connections (WS2812 ring on GPIO17, DPST NC E-stop loops on GPIO39/40 and GPIO41/42) with the standard wire colors: red 5V, black GND, green LED data, white pole 1, yellow pole 2. Includes the fail-safe rationale (open loop = STOP) and the LED-1 rotation calibration pointer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Chained edges and mixed subgraph ranks made mermaid route wires through the middle of the boxes. One node per switch terminal, plain undirected links, TB rank inside each box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
…mption Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Search links rather than pinned listings (they rot); the load-bearing part is the specs, especially the E-stop needing 2NC contacts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Links open in a new window. The NKK FF0126BBCAEA01 matches the STEP model already in this folder. M1.6 x 5 x 2.5 mm inserts anchor the ESP32 board to the base. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
- LICENSES/ carries the canonical texts (CERN-OHL-P-2.0 from the CERN OHL repository, CC-BY-4.0 from creativecommons.org). - hardware/README.md gains a Licensing section: original design files under CERN-OHL-P-2.0, original docs/images under CC-BY-4.0, and a manifest naming each vendor reference file's owner and origin link (Waveshare schematic/photo, NKK FF01 E-stop, GlobTek USB-C jack). Two reference models have no identifiable origin (esp32.step, LED.step) — flagged in the to-do list for identify-or-redraw. - Root README states the license split; OSHWA checklist rows 1, 2, 4, 6, 9, 10 move to Met. - Removed the unused LED lens STEP (not part of this design, unclear origin). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
…sp32.step The board mock-up is xro's Printables model under CC BY-NC-SA 4.0 — attributed, with a note that the NC term conflicts with the OSHW definition and commercial use, so it is queued for a redraw. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
…ck-up New board fit model drawn in-house (CERN-OHL-P-2.0 with the rest of the design); casing.FCStd updated to reference it. The xro Printables mock-up and its NC-license caveat are gone — every file in hardware/ is now openly licensed or vendor-attributed. Last remaining to-do is assembly instructions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
The design has no custom electronics; hardware/README.md now lists the freely accessible datasheets for every active component (ESP32-S3, W5500, carrier schematic, WS2812B, NKK FF01), all links verified live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
The Waveshare silkscreen labels pins IOnn, not GPIOnn — relabel every doc pin reference so an assembler never has to translate. Firmware code identifiers are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
hardware/ASSEMBLY.md: six steps from base prep to commissioning, one photo each (renamed into hardware/assembly/). Drafted by reading the build photos against the wiring table and BOM — expect corrections from the person who actually built it. The photos surfaced two parts the BOM lacked: 1 oz adhesive steel ballast weights (x3, base floor pockets) and the PoE Module (B), which ships with the ESP32-S3-POE-ETH variant of the carrier board (folded into that row; no separate listing exists). OSHWA row 5 (assembly instructions) to Met. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
15 minutes per unit; diffuser is part of the lid print (no separate piece); solder board wires before seating; short M1.6 Phillips screws anchor the bare board (step 3), the longer screws shipped with the board fasten the PoE Module (B) in step 4 — every unit gets the module; ring wires thread through the routing hole; ring orientation matched to the photo for reproducibility; either PWR5V/GND pad pair works; fold (not "harness") the wires clear of the lid; first flash is automatic, BOOT+RST demoted to a fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Extracted from print.3mf (Bambu Studio project): X1C + 0.4 nozzle, textured PEI, 0.20 mm Strength preset with 6 walls / 25% grid / 5+3 shells, white PLA Tough+ + yellow PLA Matte via AMS for the two-color lid, tree supports + auto brim, two full units per plate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
From the exported sliced plate: 4 h 27 min and 144.3 g for two units (135.8 g white, 8.6 g yellow — the lid prints face-down so the yellow lives in the first three layers). print.gcode.3mf committed as the ready-to-print X1C plate. Both colors now listed as PLA Tough+. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Bench-verified while calibrating the first assembled unit; the old comment guessed clockwise. Comment-only, no functional change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
The centralized configuration management / OTA system Polymath runs is
proprietary and out of scope here; the firmware's check-in and OTA
tooling is intended to pair with such a backend, and the docs now say
exactly that (docs/API.md) without describing ours:
- docs/FLEET_SERVER.md deleted; README, API.md, TROUBLESHOOTING,
RECOVERY_PLAYBOOK, INTERNAL_RAM_REDUCTION, SAME_LAN_DIRECT_PATH_PLAN
reworded (server-sense "fleet" -> management backend; the generic
fleet-of-devices sense stays).
- The backend's VPN IP and URL are site configuration, not project
defaults: removed from sdkconfig.defaults and the Kconfig default
(now empty); they live in the gitignored sdkconfig.credentials, with
placeholders documented in sdkconfig.credentials.example. Builds
without a backend configured get no pinned peer and no OTA polling;
the direct /admin/api/ota upload is the update path.
- Code comments and user-visible strings reworded across microlink and
dcs_support ("fleet server" -> management server; pinned-peer label
"fleet-server" -> "mgmt-server"; "fleet unreachable" -> "backend
unreachable"). Identifiers (fleet_ota_*, ML_FLEET_SERVER_IP,
/admin/api/fleet-ota/*) are unchanged for compatibility.
- README credit reworded: microlink is by Malone Technologies; this
work was made possible in part by Polymath Robotics funding
microlink's open-source development.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
The backend previously had no authoritative way to know how often a device intends to check in, so idle/stale detection had to guess or poll the device. Each check-in now carries the device's current check_interval_s (including runtime changes made via /admin/api/fleet-ota/interval), letting the backend compute staleness from the payload alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
The library timeout is heartbeat_ms x max_missed_heartbeats (missed = diff/hb, stop at missed >= max) — not max+1 as the old comments claimed. Remotes send at hb/2 and deliberately withhold one send on a lockstep mismatch, so max_missed=1 tolerated zero withheld ticks and a single withhold raced the deadline exactly: 13 false heartbeat STOPs in 3.4 h on the bench, all on the unit with the higher mismatch rate. max_missed_heartbeats now defaults to 3 (both the runner default and machine.toml), and default_heartbeat_ms drops to 400 so remotes publish at 5 Hz — machine STOPs after ~1.2 s of true silence with six send slots of margin. Comments corrected to the real formula. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
…tive announce Four fixes from the 24h two-remote soak: - microlink: learn a candidate endpoint from an inbound DIRECT disco ping and race a ping back. A chip whose stored candidate list was empty stayed DERP-only forever even while the peer pinged it directly every few seconds (bench: tethered unit relaying via sfo DERP with the machine 30 cm away, ~0.5% send failures from relay backpressure). - microlink: preserve stored peer endpoints when a full peer re-sync carries an empty Endpoints array (same rationale as the derp_region preservation) — this is how the candidate list got wiped. - firmware: split pstop_send_fail by errno cause (nomem / route / other + last errno) in /state.json, so a climbing counter can be attributed without guesswork. - host runner: print the library's check_heartbeats verdict as a LIVENESS line instead of a fake 'RX <- ... UNKNOWN REJECTED' packet; document the real timeout formula (hb x max_missed, NOT max+1) and the max_missed=1 lockstep-withhold race in README; add optional bearer-authenticated console announce ([announce] in machine.toml, env-overridable, off by default, never on the safety path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Plugging a pstop into an unprepared Linux host looks broken in a
misleading way: the new wired connection sits in 'connecting...'
forever and the unit silently falls back to its provisioned WiFi. The
tether design requires the HOST to own the link (NetworkManager shared
mode: 10.42.0.1 + DHCP + NAT), which was only implied by 'the default
NetworkManager share' in host/README.
- ship host/70-esp-pstop.link (pins any app-mode unit to esp-pstop0 by
VID:PID, unit- and reboot-stable)
- host/README: 'USB tether — one-time host setup' section with the two
commands, the factory-default 10.42.0.1:8890 peer consequence, and
the multiple-units caveat
- root README quickstart: pointer to it
- TROUBLESHOOTING: symptom-first entry ('connecting... forever / unit
joins WiFi instead')
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Neither battery power nor a cellular uplink is a feature of this pstop. The vendored microlink still carried its full cellular subsystem (SIM7600/SIM7670 modem driver, AT-socket bridge, WiFi/cellular network switcher, admin-UI APN/SIM-PIN fields, Kconfig menus) — all disabled in our builds but presented to users and readers as capability. Removed: - delete ml_cellular, ml_at_socket, ml_net_switch (net_switch had zero consumers) + their Kconfig menus and CMake entries - microlink_internal.h: AT-socket routing wrappers -> plain BSD socket aliases; drop the IMEI device-name API (only net_switch used it) - ml_wg_mgr / ml_coord / ml_tcp / wireguard_lwip / config httpd+html: drop cellular branches, UI fields, settings JSON keys, and comments - NVS settings blob: cellular_apn/sim_pin and ppp_user/pass slots stay as reserved bytes — layout compatibility for deployed units — but are no longer readable or writable anywhere - README: 'battery-powerable' -> 'PoE- or USB-powered' Ring: MISMATCH purple is no longer held for 2 s — it now tracks the mismatch counter with a 300 ms bridging window (one ring frame), so purple shows exactly while the cores are disagreeing: a sustained fault is solid purple, a lone transient blip clears within ~one frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
pstop_c synced verbatim from main @ 3a77381 ('Adding support for a minimum delay between stop and OK', #59) — library untouched, as always. The library now refuses an OK arriving sooner than delay_between_stop_ms after the STOP that opened the arming cycle (replies STOP, stays STOPPED), re-evaluating as the OK stream continues, so the same episode completes once the delay elapses. Adopted library-native only (operator decision): the wrapper's own hold-duration veto is retired — machine.toml min_stop_ms now feeds app_config.delay_between_stop_ms directly, and the runner just logs 'ARMED' (with held time) or 'ANOMALY: arming DEFERRED' when the library refuses an early OK. Semantic change vs the old wrapper veto: an EMC blip now pauses the robot for min_stop_ms and then self-re-arms on the steady OK stream, instead of latching STOPPED until a fresh press. tools/pstop_test_remote.py updated to the new semantics and extended to assert BOTH the early refusal and the delayed acceptance; all 7 wire assertions pass against the rebuilt runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Run-3 soak: the tethered unit logged 134 send failures bucketed 'other' with errno -1 — which is exactly lwip's err_to_errno mapping for ERR_IF (low-level netif error, err.c table). wireguardif never returns ERR_IF; it comes from the esp-netif glue when the uplink DRIVER refuses the frame (e.g. USB-NCM transmit busy). Path up, one 200 ms send slot lost, retried next tick — a distinct cause worth its own counter instead of a mystery in 'other'. /state.json gains pstop_sf_txdrv. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
The library (#59) re-stamps stop_time_ms on every STOP message, so an early OK is measured against the LAST STOP received — while the wrapper's episode clock starts at the first. During a held press the old line printed episode ages over the minimum labeled as under it. Say what each number actually is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
STM32 virtual COM port (0483:5740, USB CDC, baud-agnostic). Binary A0-frame commands, ASCII CHn:ON/OFF ack lines; every set is verified against the ack and status() is a real hardware query (query-all reports 8 firmware channels; 1..4 populated). Auto-detects the port via /dev/serial/by-id. Verified live on the bench. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pytest suite driving a real remote through its actual E-stop loops and USB power feed via the 4-ch relay board, asserted at a dedicated machine_app_runner instance (chip peer slot 3, port 8894) so the bench machine in slot 0 is never touched. Covers: press/release arming with min_stop_ms timing, held-button, sub-min_stop blip deferral (#59), single-channel discordance (comparator silence -> MISSED_HEARTBEATS, no STOP on the wire, no re-arm on heal), power-loss recovery and cold-boot no-self-arm, boot-with-button-held. Unwired benches skip cleanly; test_00_rig validates the wiring itself. Driver: retry idempotent commands once — the board occasionally swallows a command arriving right after its previous reply. run.sh isolates the venv from ROS PYTHONPATH pytest plugins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
set_many() concatenates the command frames into a single write so the board actuates the channels back-to-back — measured 1.6-1.8 ms ack skew between loops A and B vs ~40-60 ms when commanded sequentially. HIL press()/release() now move both loops together, matching a real DPST button's pole skew and staying well inside one lockstep tick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bring-up fixes from the wired bench (PSTOP06 test unit on usb0): - hil.toml describes the as-built rig: NO contacts everywhere, so the de-energized rig is fail-safe (loops open, DUT unpowered); NCM iface is configurable (two pstops are attached to this bench) - /api/pstop_peers id MUST be 0x-prefixed: firmware uses strtoul base 0, so bare '01020390' parsed as octal (4227) -> INVALID_ID bond rejects; verify the echoed id - rig self-test no longer toggles the DUT power relay (it reboots the chip and poisons the following tests) - chip discovery retries while USB/NCM re-enumerates after power cycles; session fixture waits out a booting chip before skipping - MachineRunner fails fast if the runner dies at startup (EADDRINUSE) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HIL/CI prerequisite: nothing on the unauthenticated telemetry surface identified the running build, so a test harness could not verify the DUT is actually executing the build under test before trusting a result. fw_ver = PROJECT_VER (git short hash), fw_sha = truncated ELF SHA-256 from the app descriptor (unique per build even when two builds share a version tag). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
ruff: organize imports, drop ambiguous 'l' loop vars; add the SPDX header the copyright hook requires on run.sh. No behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b
Uses fw_ver/fw_sha from state.json (firmware 4c90711): the session hard-fails before any test when the DUT isn't running the build under test; matched against fw_ver (git short hash) or a fw_sha prefix. test_00 reports the identity, skipping on pre-4c90711 firmware. Note: fleet-console image sha != on-chip fw_sha (ELF) — CI pins the git hash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Working branch consolidating everything that was in flight when main was reverted (c35bd95):
pstop_c coveragenow skips for dependabot-triggered runs, whose missing repo secrets (emptyBULLSEYE_LICENSE_KEY) caused the red X on the dependabot PRsDraft on purpose: do not merge until the downstream breakage that forced the revert is identified and fixed here.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JRZvaANcTGNuMjJN2gVf8b