Repository navigation
bench kit: reproduce the LifeTrac radio bench from GitHub (setup, deploy, legs, guards) - #142
Merged
Merged
Conversation
Board scripts, systemd/udev units, mosquitto/openocd configs, Dockerfiles, compose/YAML/TOML, Python, *.env(.example) and requirements*.txt now check out with LF on every OS, so a Windows clone (core.autocrlf=true) and `git archive` no longer hand the X8 boards CRLF files. *.ps1/*.bat check out CRLF (text=auto). Firmware images, archives, wheels, objects, images and PDFs are marked binary. Renormalize check (`git add --renormalize .`, not committed): - against the existing autocrlf working tree: 108 tracked files would change (60 *.pdf, 43 *.hex, 3 bench-evidence *.bin, 1 *.zip, bom.csv). These are NOT index CRLF: autocrlf had guessed those binaries were text and mangled them on checkout, and re-adding the mangled copies would have corrupted the index. Not committed. - after rewriting the working tree under the new rules: 1 file (DESIGN-STRUCTURAL/drawings/generated/bom.csv, which is stored with CRLF but is not covered by these rules; it only shows up because core.autocrlf=true). 0 covered files renormalize: the index already stores every covered file with LF, so this commit changes checkout behaviour only. The refresh recipe is in the file header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elled params run_live_radio_monitor.ps1 (bench kit, reproducibility audit): - A profile-2 (DTS) leg with -ForceFrfHz 0 is refused before anything touches a board; the profile default is 915.000 MHz, the RS-11.6 emitter channel every RS-13.1 DTS leg flew on by accident. New -AllowDefaultCarrier switch for a deliberate control leg (recorded in params.txt). Legs that pass -ForceFrfHz are unchanged. - [CmdletBinding()]: a misspelled parameter (e.g. -DurationSeconds) is now a ParameterBindingException instead of being silently ignored. Verified by binding the parsed param block in a synthetic scriptblock (no catch-all ValueFromRemainingArguments exists). - The PC MQTT broker and the -TxFeed host publisher start with -PythonExe (default `py`, run as `py -3`) instead of a hardcoded per-user interpreter path; the amqtt dependency is documented and an advisory re-probe warns when the broker did not come up. - Warn when -HostIp is not one of this PC's addresses (default unchanged). - Make /tmp/lifetrac_p0c writable before pushing 08_boot_user_app.cfg and warn if it did not land (the tractor SWD reset needs it). - Fix the "$HostIp:1883" messages, which printed empty under StrictMode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins the Python packages the bench PC's own tools import (amqtt for the harness's PC broker, paho-mqtt, numpy, opencv-python-headless, Pillow, cryptography) at the versions installed on the PC that flew RS-12/RS-13, with a per-package note of which scripts need it. amqtt was declared nowhere before. All six pins match the bench PC's installed versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seven repo-root WiFi helpers (connect_wpa.sh, do_wifi.sh,
setup_5star_wifi.sh, wifi_connect.sh, wifi_connect_fast.sh,
wifi_persist_and_reboot.sh, wifi_rescan_connect.sh) carried the bench
WiFi SSID and password as literals. They now require LIFETRAC_WIFI_SSID
and LIFETRAC_WIFI_PSK from the environment (POSIX ${VAR:?} guard: the
script stops before touching the radio if either is unset) and carry a
header saying credentials must never be committed. Behaviour is
otherwise unchanged. connect_wpa.sh writes /tmp/wpa.conf under umask 077
because the file holds the passphrase.
.vscode/tasks.json had the same password inline in "Rescan then
connect"; it and the five tasks that push and run these scripts now take
${env:LIFETRAC_WIFI_SSID} / ${env:LIFETRAC_WIFI_PSK} from the PC
environment, passed through `sudo -S env ...` (sudo drops the caller's
environment), using the quoting the SSID already used there. Not run:
no adb on the bench during this change.
The old values remain in git history. The operator is rotating that
password; after rotation the stored '5star' NetworkManager profile on the
tractor should be deleted (BENCH_BOARDS.md follow-ups). The SSID alone
still appears in a board-state capture
(bench-evidence/board_state_2026-10-10/tractor/20_network.txt), left
as evidence.
Checks: bash -n and sh -n on all seven; each exits 1 with the guard
message and no output when the variables are unset; the connect_wpa.sh
heredoc was exercised in a sandbox copy with fake values containing ',
$ and backticks; .vscode/tasks.json parses as strict JSON; git grep finds
the password in no tracked file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BENCH_QUICKSTART.md is the single map a newcomer follows to reproduce the RS-12/RS-13 radio bench: hardware, PC setup, boards, L072 firmware (expected md5 0c1bb0a9), board software, bench.env, the per-session power-up guard, staging and DTS carrier spot-check, the RS-13 harness line, analysis, park and power-down, board-state capture, a symptom table, and the GO rule. PC_SETUP.md pins the Windows 11 bench PC: Git for Windows with core.longpaths, py -3 + requirements-pc.txt / requirements-dev.txt, the exact Arm GNU Toolchain 12.2.MPACBTI-Rel1, WinLibs mingw32-make (PowerShell only), adb with MSYS_NO_PATHCONV, uuu 1.5.243, optional arduino-cli / J-Link, the Firefox scene rule (never --kiosk) and the local test command with CI's dummy fleet key. Links to the other kit files use the shared contract names (BENCH_SETUP.md, DEPLOY.md, bench.env.example, legs/README.md, requirements-pc.txt, ...). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BENCH_RUNBOOK.md: point newcomers at BENCH_QUICKSTART.md and the other kit docs near the top; link PC_SETUP.md from the toolchain fact and legs/stage_boards.sh from prep step 1; add the bench web UI on the base recipe (lifetrac-v25:latest from /tmp/lifetrac_strict, uvicorn on 8090, bench-only PIN, LIFETRAC_MQTT_HOST=127.0.0.1, MQTT tap because web_ui logs nothing, Auto arm/disarm trap, teardown), as recorded in the F10/F11 sessions and leg V; add the harness parameter-name trap (misspelled names used to fall into $args; now refused; read params.txt). README.md and DESIGN-CONTROLLER/README.md: one short pointer each to the radio bench quickstart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ash_l072.sh - full_flash_pipeline.sh and bench_tools/run_flash_bench.sh now default REVIVE_MODE to `reboot` (was `full`). Every flash since 2026-09-12 passed reboot by hand; the full path re-inserts x8h7 and OOPSes the base's 6.1.24 kernel. `REVIVE_MODE=full` stays selectable. revive_bridge.sh accepts `reboot` as an alias of reset_run_only (a standalone call with the wrapper's environment can no longer fall into the module reload) and warns before a module reload on a 6.x kernel. - run_flash_bench.sh: password, home dir from BENCH_SUDO_PW / BENCH_HOME (defaults fio, /home/fio); FLASH_VERIFY_ONLY passes through sudo; a preflight refuses, before any hardware is touched, when a pipeline file or the image is missing/empty or a .sh/.cfg has CRLF (tmpfiles ages /tmp at 5 d); FLASH_PREFLIGHT_ONLY=1 stops after that check. - bench_tools/flash_l072.sh (new, PC side): FLASH_RUNBOOK sections 1-2 as a script. Serials, base host/user/key, sudo password and REVIVE_MODE come from bench_tools/bench.env (or the environment) with the current bench values as defaults. Stages LF-clean and runs the board preflight; flashes only with --go (a flash is a radio-on event). Tested with stub adb/ssh/scp only; never run against a board. - bench_tools/wdog_regs.py: the board-only WDOG1 register reader recovered from the 2026-10-04/10 captures, code unchanged, with a header (what it reads, read-only, run inside the docker image with --privileged -v /dev/mem:/dev/mem). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…udit - setup_x8_mqtt_link.ps1 (comment only) and ADB_TIPS_AND_TRICKS.md 3.1: the 2026-05-24 `adb reverse` failure on LmP 4.0.11-934-91 was on the base 2D0A, not 2E2C. 934-91 has only ever run on 2D0A (2E2C runs the as-shipped 4.0.3-674-88), and the 05-24 Phase C plan issued `adb -s $base reverse` with $base = 2D0A (uname 6.1.24 = 934-91). - HC-02_linux_bridge_full.md: board1_healthcheck.sh was renamed to x8_max_carrier_healthcheck.sh in 97accdc (2026-05-15); the procedure and helper link now use the existing file. - FIRMWARE_UPDATES.md: the handheld is an Arduino MKR WAN 1310 (SAMD21, firmware/handheld_mkr, FQBN arduino:samd:mkrwan1310), not a Portenta H747. Heading, artifact name and bootloader entry (double-tap RESET) corrected. tools/flash_handheld.py does not exist yet, so it is now marked as planned, with arduino-cli as the stopgap. - BUILD-CONTROLLER/05_firmware_installation.md Step 2: a warning box. Uploading tractor_h7 as "Portenta H7 (M7 core)" overwrites the X8's stock x8h7 bridge firmware (as happened on 2026-05-04), and the current bench does not use tractor_h7. The step itself is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
x8_lora_bootloader_helper/mlm32l07x01.bin carries the stock Murata/Arduino file name, but it is the LifeTrac "hello v0.2" test build. It is byte-identical to murata_l072/build_hello/hello_world.bin; md5 and strings are in the README. The new README says so and also identifies: - hello.bin: identical to murata_l072_hello/hello.bin; - board2_l072_flash_dump.bin: a 16,440-byte readback of L072 flash taken with dump_l072_flash.py in May 2026. It matches no tracked build and is not the factory image; - LifeTrac-v25/tools/mlm32l07x01.bin: the real stock MKRWAN AT firmware (ARD-078 1.2.3, 83,032 B); - the actual bench firmware: `mingw32-make bench`, md5 0c1bb0a9. The files are not renamed, because old scripts default to that path (run_flash_l072.sh). The README also points to FLASH_RUNBOOK.md and the bench_tools docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…defaults - Section 0: the exact toolchain for byte-identical builds (Arm GNU Toolchain 12.2.MPACBTI-Rel1, gcc 12.2.1 20230214) and the expected md5s (bench 0c1bb0a9..., production 589c1203..., both 24 860 B at bb4a207); other compilers build working firmware with different bytes. - Sections 1-2: the scripted form bench_tools/flash_l072.sh (bench.env driven, stages + preflights, flashes only with --go), the wrapper's new staging preflight, and REVIVE_MODE now defaulting to reboot (full still selectable). - Section 3: wdog_regs.py now lives at bench_tools/wdog_regs.py, with the docker command and the expected WCR=0x773d. - New section 5, first flash from a factory board: what the stock MKRWAN board carries and how the X8 reaches the ROM (PA11/PF4 via the X8's openocd, AN3155 19200 8E1); a read-only --verify-only first contact against tools/mlm32l07x01.bin; option-byte / readout-protection rules (never 0x73/0x82/0x92 or writes to 0x1FF80000; the 2026-05-14 wunprot brick); the 2026-05 lessons (H7-side failures are not L072 bricks, the falsified PWM bypass, J-Link on the DNP CN2 header as the last resort, the 05-14/05-17 board disagreement). Cites the AI NOTES and T2/T4/HC-03. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the short "Build" section: the Arm GNU Toolchain 12.2.MPACBTI-Rel1 (gcc 12.2.1 20230214) needed for byte-identical builds, with the Linux tarball URL and Arm's published sha256; mingw32-make from PowerShell only; the all/bench/check/size/clean targets (and the bench-after-clean gotcha); the expected md5s at bb4a207 (bench 0c1bb0a9..., production 589c1203...); that Arduino's bundled gcc (build.ps1 picks it first) and Ubuntu's gcc-arm-none-eabi build working firmware with different bytes; and the two CI jobs (blocking size gate, non-blocking md5 check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…el1) New .github/workflows/l072-bench-md5.yml, on pushes/PRs touching firmware/murata_l072 (and workflow_dispatch). It downloads the Linux x86_64 Arm GNU Toolchain 12.2.MPACBTI-Rel1 (URL and sha256 pinned, verified against Arm's published .sha256asc; cached), runs `make bench` and `make`, and prints the md5s of firmware_bench_diag.bin and firmware.bin next to the murata_l072/README.md reference values and the committed firmware.bin, in the step summary and as ::warning annotations on mismatch. Job-level continue-on-error: true and the compare step always exits 0, so it never blocks. Existing workflows untouched. YAML checked with PyYAML; every run: block passes bash -n. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unit
provision_bench_board.sh <base|tractor> [--check] runs on the PC and
applies, idempotently, every board-side customization the original bench
pair carries (captures board_state_2026-10-04/base, 2026-10-10/tractor):
- both: sudoers drop-in via the existing install_lifetrac_nopasswd.sh,
docker running, probe image from the repo-root foundries_python.tar
- base: compose-apps-early-start{,-recovery}.service masked (the 934
recovery unit wipes /var/lib/docker every ~60 s), PC ssh key, optional
eth0 100BASE-TX full pin (--nic-100, autoneg kept on as measured),
eclipse-mosquitto:2
- tractor: units/disable-wifi.service, wpa_supplicant masked, nmcli radio
off, stored WiFi profiles deleted (names only printed), camera USB
provisioning by running provision_x8.sh unchanged (guarded against
restarting an installed camera compose app), eclipse-mosquitto:2 from
the base or a tarball, camera unit + container left stopped
It never opens /dev/ttymxc3, never starts lifetrac-base* or
lifetrac-tractor-compose, strips CR from everything it pushes, and does
not rely on adb propagating exit codes. --check changes nothing (exit 3
when work remains). Checked with bash -n and offline flow runs against a
fake adb; not run on hardware.
units/disable-wifi.service is the exact unit text from the tractor
capture (230 B, LF).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ordered procedure: radio-safety rules first (RXCONT at boot and park, the tractor camera unit stealing /dev/ttymxc3, the base rebooting after poweroff, never start the production units, GO per round), hardware list (ABX00049 + ABX00043, Taoglas TD.95.6H31, 12 V, USB-A to USB-C into the X8, C2 camera), DIP switches, image choice (934 pinned by URL + sha256 and recommended on both boards; the tractor role has only flown on 674, which is hosted but not byte-verified; image-latest is not the bench image), reflash via T3a, first boot (adb, password, sudoers via install_lifetrac_nopasswd.sh, base lease, ssh key), provision_bench_board.sh with a why-table, HC-01/HC-02 verification, L072 build/flash/health/park, then DEPLOY.md and BENCH_QUICKSTART.md. Links to the other kit docs by their contract names (DEPLOY.md, BENCH_QUICKSTART.md, PC_SETUP.md, bench.env.example, legs/README.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compare byte counts with and without CR (tr + wc, which the board scripts already use) instead of `tr | cmp`: nothing shows cmp exists on the LmP images, and a missing cmp would have flagged every script as CRLF and refused every flash. Re-tested the failure paths (missing staging, CRLF in one .sh while a CRLF harness cfg and stamp.py are ignored, bad REVIVE_MODE) and the FLASH_PREFLIGHT_ONLY=1 success path on a scratch copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t boot, fix the 05-13 row - Image table: 934.tar.gz (1,097,240,589 B, Last-Modified 2025-08-11) is the base image and the one to use; 674.tar.gz is hosted but never byte-verified; image-latest.tar.gz is not the bench image (it moved on; the bench bundle was fetched under that name on 2026-05-08). - Download/verify/extract commands, the full sha256 list of the 934 bundle used for both base reflashes (copied from the private archive's lmp_934_bundle_sha256.txt; outer tarball 2f2065c7...), and the extracted layout (uuu runs from mfgtool-files-portenta-x8/). - uuu 1.5.243 pinned (bench copy sha256 f6b76a62...); the bundle's own uuu.exe is 1.5.109 and was unstable on Windows 11. - DIP guidance: BOOT SEL ON; if SDP does not appear, add BOOT ON. Both bench reflashes needed BOOT SEL only; PID 0134 on both; 012B never seen here. - First-boot section and an erase warning. - Verdict row 2026-05-13 corrected against log/2026-05-13_board1_T3a_postreflash_v1.md: the board was 2D0A (then "Board 1"), BOOT SEL ON only, USB-C only, PID 0134, 12/12 stages, two power cycles to first boot. The old values are kept in the row's note; 2E2C has never been reflashed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rovisioner - Replace "No copy of the 674 image exists" with the hosted URL (674.tar.gz, 854,998,055 B, not downloaded or byte-verified, no sha256 on record); the base row now points at the 934 URL and T3a's sha256 list. - Pointer near the top to BENCH_SETUP.md and provision_bench_board.sh (applies the /etc + docker deltas listed here, never the production units). - disable-wifi.service is now in git (units/); the 5star open item notes that the provisioner deletes stored WiFi profiles (--keep-wifi-profiles defers it); the T3a row :66 doc error is marked fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rapper default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bench_tools/bench.env.example documents every per-bench setting (serials, base transport adb|ssh, base host/key, sudo password, PC host for the harness, scratch/evidence/archive dirs, the same-day DTS carrier and its date, the scene URL and Firefox profile, the board images, the base broker container). Copy it to bench.env, which the new bench_tools/.gitignore keeps out of git. bench_tools/lib/bench_env.sh is sourced by the bench scripts: it finds the repo root from its own location (git rev-parse --show-toplevel, with a fixed-depth fallback), loads bench.env or the example (CRLF-tolerant; a non-empty caller variable wins), fills computed defaults, exports MSYS_NO_PATHCONV=1, and provides win_path, stamp, board_sh / board_sudo / board_push / board_push_lf / board_pull / board_present (adb, or ssh for the base), strip_to_gzip, board_uart_holders, board_containers and bench_require_dts_carrier (refuses an empty, out-of-band, 915.000 MHz or stale carrier). Sourcing it touches no board. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copies of the RS-13.1 session scripts that run from any checkout: every path, serial, address, password and image comes from lib/bench_env.sh. The historical copies in bench-evidence/RS_13_vector_scene_2026-09-26/ scripts/ are untouched. - stage_boards.sh: stages /tmp/lifetrac_strict and now also the harness's /tmp/lifetrac_p0c/08_boot_user_app.cfg (LF-clean), md5-checks every pushed file, --probes-only. - push_fix_to_board.sh: pushes this checkout's image-path tree, md5 of every file, import smoke without any device. - leg_prep.sh: refuses profile-2 legs without a same-day DTS carrier, aborts on a radio-UART holder, defaults boot mode / strictness by leg tag, writes the harness command (-ForceFrfHz, -TxBatch 0, ...) as a PowerShell wrapper that also records the archive dir; --print-harness previews it without touching a board. - leg_post.sh: rs12_leg_report --capture (A14), archive found by itself, DTS-on-915 warning from params.txt, park retry on PARK_TRANSIENT. - link_sample.sh, leg2d_switch.sh, scene_check.sh: env-driven. - step1_pass.sh: step1_pass.sh + step1_pass_fix.sh merged (--from-work), always recreates bench_mqtt, scene check, refuses while a leg daemon or UART holder is present, pulls the pass files into the evidence dir. - leg_replay.py: --evidence-dir / --capture, runs from any directory (reproduces the recorded leg2a_r4 seq-gap line). - power_up_guard.sh: generic guard from guard_both.sh: waits, stops the tractor camera, stages probes, reads the radio, parks if listening, never transmits; adb or ssh for the base; --check-only for the end of a session; --stop-leg-daemons; --capture. - youtube_window.ps1: -Url / -ProfileDir / -FirefoxExe, "f" only sent to a YouTube window; never kiosk. A legs/.gitattributes keeps the scripts LF (Git Bash cannot run CRLF). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uild base_station/requirements.lock.txt: the 27 wheels in lifetrac-v25 4623980c2dac (d375128, flown since 2026-09-15). Its own build log shows the pip step as cached (layer 9e3efb0b5438); the list comes from the 2026-09-12 build that created that layer (docker_build.log, image 12fe97dece57). firmware/tractor_x8/requirements.lock.txt: the 20 wheels in lifetrac-tractor-x8 2727dfd36f9f (6586951, flown in RS-13), from its uncached build log rs13_build.log. Headers name image ids, python base digests and logs. Both Dockerfiles gain ARG LOCKED (default 0): LOCKED=1 applies the lock as a pip constraints file on top of requirements.txt. ARG PYTHON_IMAGE (default = the old FROM tag) can pin the base image by digest. Default builds unchanged; CI builds no image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bench_tools/legs/README.md: setup (bench.env), the radio/GO, park, screen and DTS-carrier rules, a table of what each script touches, the order for a session (power-up guard, staging, scene, spot-check) and for one leg (prep -> harness with -ForceFrfHz $DTS_CARRIER_HZ -TxBatch 0 -KfRequestDisable 1 -ProbeEcho 0 -LogFragArrivals 1 -> link_sample -> post -> park), the 2a-2d leg table, step 1, where files go, the changes from the RS-13.1 originals and the known limits. RS13_VECTOR_LEG.md: one pointer line to legs/ next to the note that the RS-13.1 scripts carry that session's paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deploy_base.sh [<commit>]: git archive (LF forced, CR count checked against the commit's blobs) of base_station/ firmware/ Dockerfile docker-compose*.yml deploy/ -> copy to the base over ssh (default) or adb -> board-side script as root: refuses while the docker-wiping recovery unit is unmasked, a lifetrac-base* unit is active or a container maps /dev/ttymxc3; backs up and replaces the tree in /var/rootdirs/opt/lifetrac/DESIGN-CONTROLLER; writes DEPLOYED_FROM.txt; creates .env and secrets/ only when missing (random key and PIN generated on the board, never printed); docker build LOCKED=1 with the log in /home/fio/docker_build_<sha>.log and the old image kept as :previous; starts ONLY the compose mosquitto service (--no-deps) and checks that 127.0.0.1:1883 accepts an MQTT CONNECT. Never starts lora_bridge. build_tractor_image.sh [<commit>]: builds lifetrac-tractor-x8:<sha> + :latest natively on the base from firmware/tractor_x8 (LOCKED=1), smoke-tests it with no network or devices, saves it to $BENCH_ARCHIVE_DIR/images on the PC (md5 checked), then adb-pushes it to the offline tractor and docker load -i's it, keeping the old :latest as :previous. --with-mosquitto, --no-tractor, --push-only FILE (rollback from the archive), --dry-run. Both read bench_tools/bench.env and contact no board in --dry-run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When and how to use deploy_base.sh and build_tractor_image.sh, their settings (bench.env), exactly what each changes on the base, the tractor and the PC, the lock files and the LOCKED=1 build, rollback (:previous tags, prev_tree backups, --push-only from the PC archive, never prune -a on the offline tractor), the traps (CRLF in git archive, sudo eating stdin, the docker-wiping recovery unit, compose up without a service name, /tmp tmpfs, adb from Git Bash), and the radio-safety rules: deploying never touches the radios, never start lora_bridge, and lifetrac-base.service / lifetrac-base-compose.service are enabled-but-failing on the bench base and should be disabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and leg scripts The six kit branches were written in parallel and each read bench.env on its own. Reconcile after the merge: lib/bench_env.sh accepts BENCH_ENV and BASE_USER/BENCH_ARCHIVE_DIR; the setup/deploy/flash scripts accept BENCH_ENV_FILE, BASE_SSH_USER and ARCHIVE_DIR; bench.env.example lists the optional overrides. pull_board_state.sh takes serials and the archive from bench.env. BENCH_SETUP/BENCH_RUNBOOK no longer contradict the new REVIVE_MODE default and staging preflight; the quickstart's spot-check scp uses the bench.env host/key/user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…leg docs BENCH_QUICKSTART: - bench.env is now step 1, right after PC setup and before provisioning, flash and deploy, which all read it; the board/firmware/software steps become 2-4 and steps 5-9 keep their numbers. - The shell-variable block takes the serials, base address, key, images and archive from bench.env (read in a subshell) instead of hardcoding the original bench's values. - The GO rule and step 5.1 say what the power-up guard really does: it reads each L072 with radio_state.py and parks a listening one with radio_park.py. The guard is armed with --wait-power-cycle. - Step 3 flashes with bench_tools/flash_l072.sh (stage + preflight, then --go) and sends new carriers to FLASH_RUNBOOK section 5 first. - foundries_python.tar and eclipse-mosquitto:2 come from provision_bench_board.sh, not build_tractor_image.sh. - The '11 files' flash-staging claims and the stale 'line 5: 1: image' symptom are replaced by the flash_l072.sh preflight (PREFLIGHT-OK/FAIL). - The spot-check step parks the tractor, uses a CAND placeholder, writes DTS_CARRIER_HZ / DTS_CARRIER_DATE into bench.env and explains the harness's profile-2 refusal and -AllowDefaultCarrier. - Step 6 makes the leg_prep.sh wrapper the primary path; the by-hand line uses -TxAdbSerial/-RxAdbSerial/-HostIp placeholders and <DTS_CARRIER_HZ from today's spot-check> instead of a literal carrier. - leg_replay.py usage, the pull_board_state ARCHIVE path form, the 934-on-both / USB-A-to-C wording from BENCH_SETUP, and new troubleshooting rows (execution policy, carrier refusals). PC_SETUP: a PowerShell execution policy section (CurrentUser RemoteSigned, or Bypass for one window or one script) plus Get-ExecutionPolicy in the check block; the uuu section now agrees with T3a (use 1.5.243, never the bundled 1.5.109). RS13_VECTOR_LEG / BENCH_RUNBOOK: the 2a block and the 'A leg' template carry -ForceFrfHz (required with profile 2) and the serial/HostIp placeholders, with a pointer to leg_prep.sh. legs/README: PC_HOST is required; the 'harness silently ignores misspelled parameters', 'OSE serials built in' and CRLF-cfg statements are corrected; leg_replay usage; how to park when a prepped leg is not flown. DEPLOY: ARCHIVE_DIR (old name BENCH_ARCHIVE_DIR) with the real $HOME/Documents default, the BASE_TRANSPORT default as bench.env.example sets it, the probe/mosquitto images from provisioning, and a rollback recipe that cannot rm in the wrong directory. BENCH_SETUP / BENCH_BOARDS / FLASH_RUNBOOK: the HC-02 L072 GET probe writes two bytes to /dev/ttymxc3; the old WiFi passphrase is in public history and must be rotated (the env-var half is done); flash_l072.sh and section 5 for the first flash; FLASH_RUNBOOK's manual lines are labelled by-hand and use bench.env placeholders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…YS paths lib/env_file.sh is now the only bench.env loader. lib/bench_env.sh, provision_bench_board.sh, flash_l072.sh, deploy_base.sh, build_tractor_image.sh and pull_board_state.sh all use it: CR stripped, a non-empty caller environment variable wins over the file (so TRACTOR_SERIAL=... bash flash_l072.sh flashes the board the operator named), and BASE_USER / BENCH_ARCHIVE_DIR are aliases of BASE_SSH_USER / ARCHIVE_DIR with the new name winning at the same level. deploy/build no longer export every bench.env value (the sudo password stayed in every child environment). Each setup script WARNs on stderr when bench.env is missing; bench_env.sh WARNs once per process tree when it falls back to bench.env.example. lib/bench_env.sh: board_push / board_pull give scp the /c/... form of the PC path (scp read C:/... as host "C", so every base push/pull over ssh failed, including the power-up guard's probe staging); BENCH_SSH_OPTS gets StrictHostKeyChecking=accept-new like deploy_base.sh; board_uart_holders answers unknown(...) instead of "free" when fuser is missing or sudo or the transport fails, so the UART gates fail closed; new bench_check_name. pull_board_state.sh: the archive path goes to adb and py in C:/... form (cygpath -m); with MSYS_NO_PATHCONV=1 the new $HOME/... default reached them as /c/... and the capture failed. Header names the real default. flash_l072.sh: also rejects a single quote in BENCH_SUDO_PW, honours ADB, prints the settings source, and refuses REVIVE_MODE=full on a 6.x kernel (the x8h7_drv re-insert OOPSes it). The base stays ssh-only (documented). build_tractor_image.sh: BASE_WORK / TRACTOR_WORK are honoured (they were documented but hardcoded), guarded because the board scripts chown -R them as root. provision: scp paths in /c/... form; stored WiFi profiles are backed up to a root-only tarball on the board before deletion. bench.env.example: states the shared rules, PC_HOST is required for radio legs, the override list shows the real defaults and drops the staging dirs (the harness hardcodes them). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2d override leg_prep.sh: never omits -HostIp. With PC_HOST empty it uses this PC's only IPv4 address on BASE_HOST's /24 (or its only IPv4 address), prints it, and refuses (exit 6) when that is ambiguous; the harness default is the original bench PC's lease, a dead control plane anywhere else. After the probes, the next-steps text says how to stop the capture and park both radios when the leg does not fly (power_up_guard.sh --stop-leg-daemons). Leg tags, scene names, --suffix and scene-check tags must match ^[A-Za-z0-9_.-]+$ (leg_prep, leg_post, leg2d_switch, link_sample, step1_pass, scene_check): they are interpolated into board shell commands. leg_post.sh: refuses the brackets and the park (exit 3, with the commands to fix it) while tx_smoke / rx_smoke / synth_pub / camera_svc runs or anything holds /dev/ttymxc3; UART probes run as lt_post_<board> and an EXIT trap removes only those on an abort; a re-run no longer overwrites the capture log when rs13_cap_base is already gone. step1_pass.sh: an EXIT trap removes the rs13_cap / camera_svc containers this pass started when it aborts or is interrupted. leg2d_switch.sh: clears the retained encode_mode_override at the end (and on an abort after the first publish) with a zero-length retained publish, which image_rx_daemon ignores, so nothing goes on the air and no later harness run replays it; kills only its own subscriber (client id lt2d_<leg>_<pid>), not every mosquitto_sub in the broker container. power_up_guard.sh: an unknown(...) UART-holder answer reads as "cannot tell, not probing". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… front
run_live_radio_monitor.ps1: when adb is not on PATH, throw a clear error
pointing at PC_SETUP.md instead of falling back to a per-user path that
exists only on the original bench PC. Nothing has touched a board at that
point. Everything else in the harness is unchanged.
youtube_window.ps1: SendKeys("f") goes out only when GetForegroundWindow()
is the bench window (Windows may refuse SetForegroundWindow, and the key
would land in the operator's terminal or editor); otherwise it says so. The
bench instance is matched by its exact -profile "<dir>" argument, not a
substring, and a drive-root, share-root or very short -ProfileDir is refused,
so -Close cannot hit the operator's own Firefox.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The six tasks.json WiFi tasks substituted ${env:LIFETRAC_WIFI_PSK} into the
command line, which VS Code echoes ("Executing task: ...") and which also
reached adb's argv, the board's sh -c / sudo argv and sudo's journal entry.
They are removed; one PSK-free task in their place points to running the
repo-root script by hand.
The seven repo-root WiFi script headers now show a by-hand recipe that keeps
the passphrase off command lines and out of sudo's log (read it in an
interactive board shell, then sudo --preserve-env=...), instead of
`sudo env LIFETRAC_WIFI_PSK=...`. connect_wpa.sh removes a stale
/tmp/wpa.conf before writing it, since umask 077 only applies to a new file.
The passphrase the kit removed is still in the public git history: it has to
be rotated (docs pass / operator).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iour change) `tr -d '<CR byte>'` becomes `tr -d '\r'`, which tr reads as the same byte. The file is a historical copy; this only lets git's content detection agree with the root .gitattributes rule (text eol=lf) for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, WiFi backup, new refusals) Minor drift the verifier found after the docs and scripts passes merged: DEPLOY.md states that a non-empty environment variable beats bench.env; QUICKSTART/BENCH_BOARDS no longer insist on C:/ archive paths (pull_board_state.sh converts them); WiFi profiles are backed up root-only before deletion and the PSK-carrying VS Code tasks are gone; legs/README lists leg_prep's PC_HOST auto-detect/exit 6, the tag check, leg_post's exit 3 and leg2d_switch's override cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Several failure paths can incorrectly report successful staging, evidence capture, broker startup, or radio parking.
8 open findings
Preserve staging verification exit status · New Reject negative force-frequency overrides · New Remove stale artifacts before refreshing pulls · New Refuse legs when health probes fail · New Handle mono-only captures without vector timestamps · New Preserve import smoke-test exit status · New Check capture process status after wait · New Abort when the control broker fails to start · New
What changed in this PR
Adds a reproducible, configurable two-board radio bench kit on top of #140, covering setup, deployment, flashing, leg execution, evidence capture, and shutdown.
Changes:
- Adds bench environment, provisioning, staging, flash, guard, and leg tooling.
- Documents the complete Windows/X8 workflow and pinned dependencies.
- Removes committed WiFi credentials and adds reproducibility safeguards.
| File | Description |
|---|---|
wifi_rescan_connect.sh |
Moves WiFi credentials to environment variables. |
wifi_persist_and_reboot.sh |
Removes embedded WiFi credentials. |
wifi_connect.sh |
Uses environment-provided WiFi credentials. |
wifi_connect_fast.sh |
Secures fast-connect credentials. |
setup_5star_wifi.sh |
Parameterizes NetworkManager profile setup. |
README.md |
Links the bench quickstart. |
X8_HEALTH_AND_RECOVERY/routines/HC-02_linux_bridge_full.md |
Updates the health-check helper name. |
X8_HEALTH_AND_RECOVERY/recovery/ADB_TIPS_AND_TRICKS.md |
Corrects board and image attribution. |
DESIGN-CONTROLLER/README.md |
Adds the radio bench entry point. |
x8_lora_bootloader_helper/run_live_radio_monitor.ps1 |
Adds carrier guards and portable tooling discovery. |
x8_lora_bootloader_helper/revive_bridge.sh |
Supports reboot-mode revival. |
x8_lora_bootloader_helper/README.md |
Documents helper binaries and firmware provenance. |
x8_lora_bootloader_helper/full_flash_pipeline.sh |
Makes reboot revival the default. |
bench_tools/wdog_regs.py |
Adds a read-only watchdog register probe. |
bench_tools/units/disable-wifi.service |
Adds persistent WiFi blocking. |
bench_tools/run_flash_bench.sh |
Adds flash staging preflight checks. |
bench_tools/RS13_VECTOR_LEG.md |
Documents parameterized leg execution. |
bench_tools/requirements-pc.txt |
Pins bench-PC Python dependencies. |
bench_tools/pull_board_state.sh |
Loads shared bench settings. |
bench_tools/PC_SETUP.md |
Documents Windows toolchain setup. |
bench_tools/lib/env_file.sh |
Implements shared environment-file precedence. |
bench_tools/lib/.gitattributes |
Enforces LF for library scripts. |
legs/youtube_window.ps1 |
Manages the camera scene window. |
legs/step1_pass.sh |
Automates camera-only validation passes. |
legs/stage_boards.sh |
Stages and verifies board tooling. |
legs/scene_check.sh |
Validates camera scene motion. |
legs/README.md |
Documents leg scripts and procedures. |
legs/push_fix_to_board.sh |
Pushes and verifies checkout code. |
legs/power_up_guard.sh |
Checks and parks radios safely. |
legs/link_sample.sh |
Captures live link statistics. |
legs/leg2d_switch.sh |
Automates the 2d codec switch. |
legs/leg_replay.py |
Replays and analyzes captures. |
legs/leg_prep.sh |
Prepares boards and capture infrastructure. |
legs/leg_post.sh |
Collects reports and parks radios. |
legs/.gitattributes |
Enforces LF for leg tooling. |
bench_tools/flash_l072.sh |
Adds portable staged L072 flashing. |
bench_tools/bench.env.example |
Defines shared bench configuration. |
bench_tools/BENCH_RUNBOOK.md |
Expands operational procedures. |
bench_tools/BENCH_BOARDS.md |
Documents board provisioning state. |
bench_tools/.gitignore |
Excludes local bench configuration. |
tractor_x8/requirements.lock.txt |
Records flown tractor dependencies. |
tractor_x8/Dockerfile |
Supports locked dependency builds. |
murata_l072/README.md |
Documents reproducible firmware builds. |
firmware/common/setup_x8_mqtt_link.ps1 |
Corrects board attribution. |
FIRMWARE_UPDATES.md |
Corrects handheld hardware instructions. |
DESIGN-CONTROLLER/Dockerfile |
Supports locked base-image dependencies. |
bench-evidence/.../step1_pass_fix.sh |
Repairs historical CR stripping. |
base_station/requirements.lock.txt |
Records flown base dependencies. |
BUILD-CONTROLLER/05_firmware_installation.md |
Warns about overwriting x8h7 firmware. |
do_wifi.sh |
Removes embedded WiFi credentials. |
connect_wpa.sh |
Generates a protected environment-driven profile. |
.vscode/tasks.json |
Removes credential-bearing WiFi tasks. |
.github/workflows/l072-bench-md5.yml |
Adds non-blocking firmware hash verification. |
.gitattributes |
Defines repository line-ending and binary rules. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| if [ $PROBES_ONLY = 0 ]; then | ||
| stamp "$who: code tree" | ||
| bash "$LEGS_DIR/push_fix_to_board.sh" "$who" 2>&1 | grep -E "== push|MISMATCH|verified|encoder from|has _tick|SKIP|FAILED" || true |
|
|
||
| # Bench kit (2026-10-10): refuse a DTS leg on the default carrier BEFORE | ||
| # anything touches a board. Legs that pass -ForceFrfHz are unaffected. | ||
| if ($RegProfile -eq 2 -and $ForceFrfHz -eq 0 -and -not $AllowDefaultCarrier) { |
Comment on lines
+101
to
+103
| for f in leg${LEG}_base.txt leg${LEG}_base.jsonl leg${LEG}_base.json; do | ||
| board_pull base "$BOARD_STAGE/legs/$f" "$E/$f" || echo "PULL FAILED $f" | ||
| done |
Comment on lines
+185
to
+188
| { hdr "health base"; board_sh base "$SUDO $PROBE_RUN $BASE_IMAGE -u /work/rs116_health_probe.py 2>&1"; } > "$E/leg${LEG}_health_base.txt" | ||
| grep -E "STATS-OK|STATS-FAILED|radio_state|RS12-URC" "$E/leg${LEG}_health_base.txt" | head -4 | ||
| { hdr "health tractor"; board_sh tractor "$SUDO $PROBE_RUN $TRACTOR_PROBE_IMAGE -u /work/rs116_health_probe.py 2>&1"; } > "$E/leg${LEG}_health_tractor.txt" | ||
| grep -E "STATS-OK|STATS-FAILED|radio_state|RS12-URC" "$E/leg${LEG}_health_tractor.txt" | head -4 |
| lost = sum(g for _, g in gaps) | ||
| print(f"leg {leg}: {len(seqs)} frames received; seq gaps within same-codec runs (row, lost): {gaps} -> {lost} lost of {len(seqs) + lost} = {100.0 * lost / (len(seqs) + lost):.1f} % after the first received frame ({switches} codec switch(es) not counted)") | ||
| print(f" resync episodes (start, end, ended by key): {episodes}") | ||
| print(f" all ended by an epoch start: {all(e[2] for e in episodes)}; final resync {dr.store.stats['resync']}, digest_ok {dr.store.snapshot(int(dr.last_ts * 1000) + 100)['digest_ok']}") |
Comment on lines
+72
to
+73
| board_sh "$WHO" "$SUDO $WORK_RUN $TRACTOR_APP_IMAGE -c \"import x8_image_pipeline.encode_vector as ev; print('encoder from', ev.__file__); print('codec', ev.vs.__file__, 'TTL_FRAMES', ev.vs.TTL_FRAMES); print('has _tick_ttl', hasattr(ev.VectorEncoder, '_tick_ttl'))\"" 2>&1 \ | ||
| | tee -a "$REC" |
| | tee -a "$E/step1_${TAG}_bootcheck.txt" | ||
|
|
||
| # 3. wait for the capture, then encoder timing and teardown | ||
| wait $CAP_PID |
Comment on lines
+302
to
+304
| if (-not $ar2.AsyncWaitHandle.WaitOne(1000, $false) -or -not $probe2.Connected) { | ||
| Write-Warning "PC broker not answering on $($HostIp):1883 after start. Check: $PythonExe $($pyPrefixArgs -join ' ') -c `"import amqtt`" (install with: py -3 -m pip install -r bench_tools\requirements-pc.txt)." | ||
| } |
dorkmo
added a commit
that referenced
this pull request
Oct 11, 2026
Conflicts in BENCH_RUNBOOK.md and RS13_VECTOR_LEG.md resolved by keeping both sides: the 2026-10-10 amendment notes and carrier-range detail from this branch, the bench.env / serial / HostIp placeholders and legs/ pointer from the bench kit. base_station on the merged tree: 1598 passed, 2 skipped. Co-Authored-By: Claude Opus 5.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.


Stacked on #140. Merge #140 first; GitHub then retargets this PR to
main.A reproducibility audit (2026-10-10) found that someone with a fresh clone of
maincould rebuild the radio firmware byte-identically and pass every test, but could not set up two boards and fly a leg the way RS-12/RS-13 did. This kit closes that gap. Docs and scripts only: no firmware or product-code change, and nothing in it was run against the boards (radios kept asleep).Entry point:
bench_tools/BENCH_QUICKSTART.md.What's in it
BENCH_QUICKSTART.mdtakes a newcomer from PC setup through bench.env, boards, firmware, deploy, a session, a leg, analysis, parking and board capture.PC_SETUP.mdlists the exact toolchain (Arm GNU 12.2.MPACBTI-Rel1 gives byte-identical L072 builds), Gitcore.longpaths, the PowerShell execution policy, adb, uuu 1.5.243 andrequirements-pc.txt.BENCH_SETUP.mdtakes two new boards to bench-ready.provision_bench_board.sh <base|tractor> [--check]is idempotent and does:/var/lib/dockeris wiped every 60 s);disable-wifi.serviceunit from the capture, the wpa mask, and a root-only backup of the WiFi profiles before deleting them);T3apins the OS image by URL and sha256 (934.tar.gz;674.tar.gzis hosted too) and fixes the mislabeled 05-13 row.deploy_base.shdoes the git archive, upload, extract andDEPLOYED_FROM.txt, creates secrets only when they are missing, buildslifetrac-v25, and starts only the broker. It never startslora_bridge.build_tractor_image.shbuilds on the base, saves to the archive, loads on the tractor, and keeps the old image as:previous.LOCKED=1build argument; default builds and CI are unchanged.DEPLOY.mddocuments both scripts.bench.env.example,lib/bench_env.shandlib/env_file.shdefine one set of names with one precedence order: a non-empty environment variable, thenbench.env, then the defaults.bench_tools/legs/holds parameterised copies of the RS-13 scripts. There are no hardcoded paths, serials or passwords.leg_preprefuses a DTS leg without today's carrier and needsPC_HOST. The scripts have cleanup traps, and there is a genericpower_up_guard.sh(reads and parks the radios, never transmits).run_live_radio_monitor.ps1now:-ForceFrfHzor-AllowDefaultCarrier;[CmdletBinding()]);py -3;REVIVE_MODE=rebootand checks its staging before it runs.flash_l072.shstages, checks and flashes on--go.FLASH_RUNBOOKgains a "first flash from a factory board" section.murata_l072/READMEgets a Building section with the expected md5s.l072-bench-md5.yml) checks the bench and production md5s with the pinned toolchain..gitattributesmakes board-side files check out with LF line endings and marks binaries; no committed file changes.Review
bench.envnames.bash -n(18 scripts plus the 7 WiFi scripts), the PowerShell parser (3 files), strict JSON fortasks.json, a markdown link and anchor check (0 broken), and base_station tests at 1537 passed, 2 skipped.Not verified on hardware (by rule — radios stayed asleep)
sudo --preserve-envon LmP;leg_prep'sPC_HOSTauto-detection;All of these pass syntax checks and code review only. The first real use should be a supervised dry run.
Open, outside this PR
foundries_python.tar). OSE needs to decide.EVIDENCE_DIRdefault;chmod 0777on the staging folders;🤖 Generated with Claude Code