Skip to content

bench kit: reproduce the LifeTrac radio bench from GitHub (setup, deploy, legs, guards) - #142

Merged
dorkmo merged 39 commits into
mainfrom
bench-kit-2026-10-10
Oct 11, 2026
Merged

dorkmo merged 39 commits into
mainfrom
bench-kit-2026-10-10

Conversation

@dorkmo

@dorkmo dorkmo commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

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 main could 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

  • Entry point and PC setup.
    • BENCH_QUICKSTART.md takes a newcomer from PC setup through bench.env, boards, firmware, deploy, a session, a leg, analysis, parking and board capture.
    • PC_SETUP.md lists the exact toolchain (Arm GNU 12.2.MPACBTI-Rel1 gives byte-identical L072 builds), Git core.longpaths, the PowerShell execution policy, adb, uuu 1.5.243 and requirements-pc.txt.
    • The repo and DESIGN-CONTROLLER READMEs link to the quickstart.
  • Board setup.
    • BENCH_SETUP.md takes two new boards to bench-ready.
    • provision_bench_board.sh <base|tractor> [--check] is idempotent and does:
      • install the sudoers drop-in;
      • mask the base's compose-apps units (otherwise /var/lib/docker is wiped every 60 s);
      • optionally pin the NIC;
      • turn the tractor's WiFi off (the disable-wifi.service unit from the capture, the wpa mask, and a root-only backup of the WiFi profiles before deleting them);
      • run camera provisioning;
      • preload the offline tractor's images.
    • T3a pins the OS image by URL and sha256 (934.tar.gz; 674.tar.gz is hosted too) and fixes the mislabeled 05-13 row.
  • Deploy and images.
    • deploy_base.sh does the git archive, upload, extract and DEPLOYED_FROM.txt, creates secrets only when they are missing, builds lifetrac-v25, and starts only the broker. It never starts lora_bridge.
    • build_tractor_image.sh builds on the base, saves to the archive, loads on the tractor, and keeps the old image as :previous.
    • The lock files hold the flown wheel versions, behind an opt-in LOCKED=1 build argument; default builds and CI are unchanged.
    • DEPLOY.md documents both scripts.
  • Settings and leg scripts.
    • bench.env.example, lib/bench_env.sh and lib/env_file.sh define one set of names with one precedence order: a non-empty environment variable, then bench.env, then the defaults.
    • bench_tools/legs/ holds parameterised copies of the RS-13 scripts. There are no hardcoded paths, serials or passwords. leg_prep refuses a DTS leg without today's carrier and needs PC_HOST. The scripts have cleanup traps, and there is a generic power_up_guard.sh (reads and parks the radios, never transmits).
    • The RS-13 record's copies stay unchanged as history.
  • Harness, flash and firmware.
    • run_live_radio_monitor.ps1 now:
      • refuses a profile-2 leg on the 915.000 MHz default unless given -ForceFrfHz or -AllowDefaultCarrier;
      • rejects misspelled parameters ([CmdletBinding()]);
      • starts the PC broker with py -3;
      • gives a clear "adb not on PATH" error.
    • The flash wrapper defaults to REVIVE_MODE=reboot and checks its staging before it runs. flash_l072.sh stages, checks and flashes on --go.
    • FLASH_RUNBOOK gains a "first flash from a factory board" section.
    • murata_l072/README gets a Building section with the expected md5s.
    • A non-blocking CI job (l072-bench-md5.yml) checks the bench and production md5s with the pinned toolchain.
  • Hygiene.
    • .gitattributes makes board-side files check out with LF line endings and marks binaries; no committed file changes.
    • The WiFi SSID and password are out of the seven repo-root scripts (now env vars), and the VS Code tasks that carried the password are removed.
    • The doc errors the audits found are fixed (board labels, a renamed script, the handheld hardware, an x8h7 overwrite warning, the misnamed L072 binaries).

Review

  • Six implementers in isolated worktrees, one owner per file. All six merged cleanly; afterwards I reconciled the bench.env names.
  • Three reviewers: a newcomer walkthrough, script safety, and mechanical checks. They found no blockers, 13 majors and about 30 minors.
  • A docs pass and a scripts pass fixed them. A verifier merged both and confirmed all 13 majors fixed.
  • Checks on the final tree: bash -n (18 scripts plus the 7 WiFi scripts), the PowerShell parser (3 files), strict JSON for tasks.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)

  • provision, deploy, build and flash against real boards;
  • the ssh transport;
  • sudo --preserve-env on LmP;
  • the new cleanup traps;
  • leg_prep's PC_HOST auto-detection;
  • the WiFi-profile backup.

All of these pass syntax checks and code review only. The first real use should be a supervised dry run.

Open, outside this PR

  • Change the WiFi passphrase on the access point. The old value is still in public git history; the docs now say so.
  • Licence: the repo has no LICENSE file and redistributes Arduino's OOTB image (foundries_python.tar). OSE needs to decide.
  • Remaining known minors:
    • the EVIDENCE_DIR default;
    • the harness's chmod 0777 on the staging folders;
    • the sudo password passed on command lines;
    • widening the carrier guard to profile 0.

🤖 Generated with Claude Code

dorkmo and others added 30 commits October 10, 2026 13:12
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>
dorkmo and others added 9 commits October 10, 2026 13:45
… 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>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 19:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several failure paths can incorrectly report successful staging, evidence capture, broker startup, or radio parking.

8 open findings
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
dorkmo changed the base branch from rs13-session-save-2026-10-04 to main October 11, 2026 00:54
@dorkmo
dorkmo merged commit 41d9e6d into main Oct 11, 2026
25 checks passed
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>
@dorkmo
dorkmo deleted the bench-kit-2026-10-10 branch October 11, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants