From 692dcf82063c03cae838863a8a07147dcfdc7855 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:12:37 -0500 Subject: [PATCH 01/32] repo: add .gitattributes (LF for board/container files, binary list) 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 --- .gitattributes | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..8e854d33 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,70 @@ +# Line-ending and binary rules for the LifeTrac repo. +# +# Why: this repo is mostly edited on Windows (core.autocrlf=true), but many +# files run on the Portenta X8 boards or inside Linux containers, where a CRLF +# breaks them ("/bin/bash^M: bad interpreter", systemd/udev/mosquitto parse +# errors, `source`d env files with a trailing \r in every value). The rules +# below make Git check those files out with LF on every OS and make +# `git archive` emit LF for them even when core.autocrlf=true. +# +# The index already stores these files with LF, so this file changes checkout +# behaviour only. An EXISTING working tree keeps its old copies until the files +# are checked out again. With a clean tree (commit or set aside your work +# first -- this rewrites every tracked file): +# git rm --cached -r -q . && git reset --hard +# On a Windows checkout made with core.autocrlf=true that refresh also repairs +# binaries that autocrlf had mangled on checkout (some *.pdf, *.hex, *.zip and +# bench-evidence *.bin files whose content Git guessed was text). +# Board-side deploys should still pass `-c core.autocrlf=false` to +# `git archive`, because files not listed here (.txt, .md, .json, .html, ...) +# follow core.autocrlf. +# +# Files not matched below keep the default behaviour (core.autocrlf). + +# --- Runs on the boards / in containers: always LF ------------------------- +*.sh text eol=lf +*.bash text eol=lf +*.py text eol=lf +*.service text eol=lf +*.timer text eol=lf +*.socket text eol=lf +*.rules text eol=lf +*.conf text eol=lf +*.cfg text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf +Dockerfile text eol=lf +Dockerfile.* text eol=lf +*.dockerfile text eol=lf +.env.example text eol=lf +*.env text eol=lf +*.env.example text eol=lf +requirements*.txt text eol=lf + +# --- Windows scripts: CRLF in the working tree ------------------------------ +# text=auto leaves anything Git detects as binary (e.g. a UTF-16 file) alone. +*.ps1 text=auto eol=crlf +*.bat text=auto eol=crlf + +# --- Binary: never convert, never text-diff/merge --------------------------- +*.bin binary +*.elf binary +*.hex binary +*.o binary +*.a binary +*.exe binary +*.dll binary +*.so binary +*.whl binary +*.tar binary +*.tgz binary +*.gz binary +*.xz binary +*.zip binary +*.jpg binary +*.jpeg binary +*.png binary +*.gif binary +*.pdf binary +*.rgb binary From 6f6bc443c615d2ada4308ac2323001a89f3ae109 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:15:36 -0500 Subject: [PATCH 02/32] harness: refuse DTS legs on the 915 MHz default carrier, reject misspelled 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 --- .../run_live_radio_monitor.ps1 | 91 ++++++++++++++++++- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/run_live_radio_monitor.ps1 b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/run_live_radio_monitor.ps1 index 3fbc9b95..03211f37 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/run_live_radio_monitor.ps1 +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/run_live_radio_monitor.ps1 @@ -6,7 +6,21 @@ Launches image_rx_daemon on 2D0A and image_tx_daemon on 2E2C, streams synthetic frames from host, and logs real-time throughput / goodput (B/s), fragment counts, and demodulation metrics (RSSI, SNR). + + PC requirements (bench kit, 2026-10-10): adb on PATH, the Python launcher + `py` (or -PythonExe), and the packages in bench_tools/requirements-pc.txt. + The PC MQTT broker this script starts when none answers on -HostIp:1883 + is start_mqtt_broker.py at the repo root, which needs `amqtt` + (`py -3 -m pip install -r bench_tools/requirements-pc.txt`). + + Parameter names are checked ([CmdletBinding()]): a misspelled name such as + -DurationSeconds is an error, not a silently ignored argument. + + Profile-2 (DTS) legs must pin a spot-checked carrier with -ForceFrfHz; the + profile default is 915.000 MHz, the channel of the RS-11.6 external + emitter. -AllowDefaultCarrier overrides the refusal. #> +[CmdletBinding()] param( [string]$TxAdbSerial = "2E2C1209DABC240B", [string]$RxAdbSerial = "2D0A1209DABC240B", @@ -72,6 +86,21 @@ param( # the same argument as FhssLinkId. The known bench interferer sits in the # 915.0 MHz channel; e.g. -ForceFrfHz 917000000 moves the link off it. [int]$ForceFrfHz = 0, + # Bench kit (2026-10-10): a profile-2 (DTS) leg with -ForceFrfHz 0 is + # REFUSED unless this switch is given. Profile 2's default carrier is + # 915.000 MHz, where the RS-11.6 external emitter bursts (~25 ms at -43 to + # -45 dBm every ~7.08 s); every RS-13.1 DTS leg flew there by accident and + # its losses fold on the emitter's period (RS-13.1 A19). Pin the carrier + # picked by a same-day receive-only spot-check instead (BENCH_RUNBOOK prep + # step 8). Use this switch only for a deliberate 915.000 MHz control leg. + [switch]$AllowDefaultCarrier, + # Bench kit (2026-10-10): Python used for the two PC-side processes this + # script may start -- the PC MQTT broker (start_mqtt_broker.py, needs the + # `amqtt` package) and the -TxFeed host synthetic publisher. Default `py` + # (the Windows Python launcher, run as `py -3`); pass a full python.exe + # path to use a specific interpreter or venv. Replaces a hardcoded + # per-user interpreter path. See bench_tools/requirements-pc.txt. + [string]$PythonExe = "py", # RS-12 (2026-08-17): 1 = RX daemon logs one line per data fragment with # the firmware us RX timestamp (frag_arrival:). Diagnostic instrument for # the silent URC-drop timing shape; adds ~2300 log lines per 300 s leg. @@ -178,6 +207,22 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Continue" +# 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) { + throw ("Refusing a profile-2 (DTS) leg with -ForceFrfHz 0: the profile default " + + "carrier is 915.000 MHz, the channel of the known RS-11.6 external emitter " + + "(~25 ms bursts at -43 to -45 dBm every ~7.08 s; RS-13.1 DTS losses folded on " + + "its period). Run a same-day receive-only channel spot-check with the tractor " + + "parked (channel_survey_sniff.py or hunt_sniff.ps1, then tools/survey_compare.py; " + + "BENCH_RUNBOOK prep step 8) and pass -ForceFrfHz , e.g. -ForceFrfHz " + + "927500000 if that channel is clean today. For a deliberate 915.000 MHz control " + + "leg add -AllowDefaultCarrier.") +} +if ($RegProfile -eq 2 -and $ForceFrfHz -eq 0) { + Write-Warning "-AllowDefaultCarrier: this DTS leg flies on 915.000 MHz, the RS-11.6 emitter channel." +} + # Profile env for both daemons; profile 1 needs the 50-ch wide mask. $profEnv = "-e LIFETRAC_REG_PROFILE=$RegProfile" if ($RegProfile -eq 1) { $profEnv = "$profEnv -e LIFETRAC_FHSS_WIDE_MASK=1" } @@ -216,22 +261,50 @@ $baseStation = Join-Path $repoRoot "LifeTrac-v25\DESIGN-CONTROLLER\base_station" $tractorX8 = Join-Path $repoRoot "LifeTrac-v25\DESIGN-CONTROLLER\firmware\tractor_x8" $helperDir = $PSScriptRoot +# PC-side Python (broker + -TxFeed host publisher). `py` is the Windows Python +# launcher and takes `-3` to pick the newest Python 3; any other value is used +# as the interpreter itself. The broker needs `amqtt` +# (bench_tools/requirements-pc.txt). +$pyPrefixArgs = @() +if ([System.IO.Path]::GetFileNameWithoutExtension($PythonExe) -ieq "py") { $pyPrefixArgs = @("-3") } +if (-not (Get-Command $PythonExe -ErrorAction SilentlyContinue)) { + Write-Warning "PythonExe '$PythonExe' not found; the PC broker / host publisher cannot start. Install Python 3 (py launcher) or pass -PythonExe ." +} + Write-Host "=== STARTING LIVE LORA RADIO MONITOR ($DurationS s) ===" -ForegroundColor Cyan Write-Host "TX Serial: $TxAdbSerial" Write-Host "RX Serial: $RxAdbSerial" Write-Host "MQTT Host: $HostIp" +# -HostIp must be THIS PC's LAN address (the boards dial it for the control +# broker). The default is the original bench PC's DHCP lease; warn, do not +# change it, when it is not one of this machine's addresses. +try { + $localIps = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop | ForEach-Object { $_.IPAddress }) + if ($localIps.Count -gt 0 -and $localIps -notcontains $HostIp) { + Write-Warning "HostIp $HostIp is not an address of this PC ($($localIps -join ', ')). Pass -HostIp ." + } +} catch { } # 1. Ensure Host MQTT Broker is running $socket = New-Object System.Net.Sockets.TcpClient $asyncResult = $socket.BeginConnect($HostIp, 1883, $null, $null) $waitResult = $asyncResult.AsyncWaitHandle.WaitOne(1000, $false) if (-not $waitResult -or -not $socket.Connected) { - Write-Host "[MQTT] Starting host broker on $HostIp:1883..." -ForegroundColor Yellow - Start-Process -FilePath "C:\Users\dorkm\AppData\Local\Python\pythoncore-3.14-64\python.exe" -ArgumentList "start_mqtt_broker.py" -WorkingDirectory $repoRoot -PassThru -NoNewWindow + Write-Host "[MQTT] Starting host broker on $($HostIp):1883 ($PythonExe $($pyPrefixArgs -join ' ') start_mqtt_broker.py; needs amqtt)..." -ForegroundColor Yellow + Start-Process -FilePath $PythonExe -ArgumentList (@($pyPrefixArgs) + @("start_mqtt_broker.py")) -WorkingDirectory $repoRoot -PassThru -NoNewWindow Start-Sleep -Seconds 2 + # Advisory re-probe only (the leg continues either way, as before): a + # broker that did not come up is almost always a missing `amqtt`. + $probe2 = New-Object System.Net.Sockets.TcpClient + try { + $ar2 = $probe2.BeginConnect($HostIp, 1883, $null, $null) + 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)." + } + } catch { } finally { $probe2.Close() } } else { $socket.Close() - Write-Host "[MQTT] Host broker active at $HostIp:1883" -ForegroundColor Green + Write-Host "[MQTT] Host broker active at $($HostIp):1883" -ForegroundColor Green } # 2. Clean up previous containers @@ -333,7 +406,16 @@ foreach ($pair in $warmPairs) { Write-Host "[RESET] RX L072 via gpio163 NRST..." $nrstCmd = "echo fio | sudo -S -p '' sh -c '[ -d /sys/class/gpio/gpio163 ] || echo 163 > /sys/class/gpio/export; echo out > /sys/class/gpio/gpio163/direction; echo 1 > /sys/class/gpio/gpio163/value; sleep 0.02; echo 0 > /sys/class/gpio/gpio163/value; sleep 0.10; echo 1 > /sys/class/gpio/gpio163/value'" cmd /c "`"$adbExe`" -s $RxAdbSerial shell `"$nrstCmd`"" | Out-Null +# Bench kit (2026-10-10): /tmp is tmpfs (wiped by every reboot/flash, aged at +# 5 d) and the flash pipeline may have left /tmp/lifetrac_p0c root-owned, so +# make the directory writable before the push and check the cfg landed -- the +# SWD reset below needs it and its push used to fail silently. +cmd /c "`"$adbExe`" -s $TxAdbSerial shell `"echo fio | sudo -S -p '' mkdir -p /tmp/lifetrac_p0c ; echo fio | sudo -S -p '' chmod 0777 /tmp/lifetrac_p0c`"" | Out-Null cmd /c "`"$adbExe`" -s $TxAdbSerial push `"$(Join-Path $helperDir '08_boot_user_app.cfg')`" /tmp/lifetrac_p0c/08_boot_user_app.cfg" | Out-Null +$cfgCheck = cmd /c "`"$adbExe`" -s $TxAdbSerial shell `"test -s /tmp/lifetrac_p0c/08_boot_user_app.cfg && echo CFG-OK || echo CFG-MISSING`"" 2>$null +if ("$cfgCheck" -notmatch "CFG-OK") { + Write-Warning "/tmp/lifetrac_p0c/08_boot_user_app.cfg is not on the tractor ($TxAdbSerial); the SWD reset of the TX L072 will fail." +} # 5. Fire the tractor SWD reset in the BACKGROUND, then launch the TX daemon # immediately. openocd needs ~7-10 s to reach its NRST pulse; the daemon needs @@ -436,7 +518,7 @@ if ($TxFeed -eq "camera") { $env:LIFETRAC_SYNTH_DURATION_S = "$($DurationS + 10)" $env:LIFETRAC_SYNTH_BYTE_BUDGET = "$SynthBudgetB" $pubScript = Join-Path $repoRoot "publish_synthetic_frames.py" - $pubProc = Start-Process -FilePath "C:\Users\dorkm\AppData\Local\Python\pythoncore-3.14-64\python.exe" -ArgumentList "`"$pubScript`"" -PassThru -NoNewWindow -WorkingDirectory $repoRoot + $pubProc = Start-Process -FilePath $PythonExe -ArgumentList (@($pyPrefixArgs) + @("`"$pubScript`"")) -PassThru -NoNewWindow -WorkingDirectory $repoRoot } Write-Host "=== MONITORING RADIO TRANSMISSIONS FOR $DurationS SECONDS ===" -ForegroundColor Green @@ -538,6 +620,7 @@ if ($Archive) { "tx_feed=$TxFeed", "reg_profile=$RegProfile", "force_frf_hz=$ForceFrfHz", + "allow_default_carrier=$([bool]$AllowDefaultCarrier)", "tx_serial=$TxAdbSerial", "rx_serial=$RxAdbSerial", "host_ip=$HostIp", From 562efdd1d96f579a1167054a22530da8acedec73 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:16:51 -0500 Subject: [PATCH 03/32] bench_tools: requirements-pc.txt for the PC-side bench tooling 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 --- .../bench_tools/requirements-pc.txt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/requirements-pc.txt diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/requirements-pc.txt b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/requirements-pc.txt new file mode 100644 index 00000000..2bd6a109 --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/requirements-pc.txt @@ -0,0 +1,47 @@ +# Bench PC (Windows) Python packages for the radio-bench tooling. +# +# py -3 -m pip install -r LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/requirements-pc.txt +# +# Scope: the tools that run ON THE PC during a bench session -- the harness's +# PC MQTT broker, leg reports, the VECTOR dry-run/replay, the channel-survey +# and periodicity analysers, and the -TxFeed host synthetic publisher. The +# board-side code runs inside the X8 docker images (lifetrac-v25, +# lifetrac-tractor-x8, arduino-ootb-python-devel) and needs nothing from this +# file; the boards' paho copy is the tracked repo-root _paho_pull/paho/. +# +# Versions are the ones installed on the bench PC that flew RS-12/RS-13 +# (Python 3.14.4, checked 2026-10-10). numpy/opencv match +# base_station/requirements-dev.txt. numpy 2.4 needs Python >= 3.11. +# To run the base-station SIL suite, install base_station/requirements-dev.txt +# instead (it is a superset for that purpose). +# +# Who needs what (import survey of the PC-side scripts): + +# start_mqtt_broker.py (repo root) -- the PC broker run_live_radio_monitor.ps1 +# starts on :1883 when none answers. Not declared anywhere else. +amqtt==0.11.3 + +# bench_tools/clear_retained_host.py, tools/vector_dry_run.py (capture/live), +# tools/lora_rtt.py, publish_synthetic_frames.py (-TxFeed host). +paho-mqtt==2.1.0 + +# tools/crc_dump_temporal.py, tools/rssi_sniff_periodicity.py (hunt_sniff.ps1), +# tools/bulk_loss_boundary.py; camera_service (imported by +# publish_synthetic_frames.py) and the RS-13 a16_* SIL scripts. +numpy==2.4.4 + +# RS-13 a16_* SIL scripts and the VECTOR encoder (x8_image_pipeline +# encode_vector.py / vector_extract.py) when run on the PC. +opencv-python-headless==4.14.0.94 + +# camera_service (publish_synthetic_frames.py -TxFeed host) and +# base_station/image_pipeline codec_decode / fallback_render. +Pillow==12.2.0 + +# base_station/lora_proto.py AES-GCM (imported lazily; needed by anything that +# builds or opens encrypted frames on the PC). +cryptography==48.0.0 + +# Not needed for the radio bench: pyserial (tools/provision.py, +# tools/pair_handheld.py -- handheld/USB provisioning) and pymodbus +# (tools/modbus_bench.py). Install them only for those tools. From 01c85393528f6c6e388de9688fcac36d9a7e6a99 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:18:09 -0500 Subject: [PATCH 04/32] security: take the WiFi SSID/password out of the repo-root scripts 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 --- .vscode/tasks.json | 18 ++++++++++++------ connect_wpa.sh | 21 ++++++++++++++++++--- do_wifi.sh | 14 ++++++++++++-- setup_5star_wifi.sh | 12 +++++++++++- wifi_connect.sh | 14 ++++++++++++-- wifi_connect_fast.sh | 12 +++++++++++- wifi_persist_and_reboot.sh | 14 ++++++++++++-- wifi_rescan_connect.sh | 12 +++++++++++- 8 files changed, 99 insertions(+), 18 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index be7ca0bb..1d5fed83 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -702,8 +702,9 @@ }, { "label": "Connect WiFi 5 star LAN", + "detail": "Needs LIFETRAC_WIFI_SSID and LIFETRAC_WIFI_PSK set in the PC environment (never commit them).", "type": "shell", - "command": "adb -s 2D0A1209DABC240B push wifi_connect.sh /tmp/wifi_connect.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wifi_connect.sh ; echo fio | sudo -S /tmp/wifi_connect.sh 2>&1\\\"", + "command": "adb -s 2D0A1209DABC240B push wifi_connect.sh /tmp/wifi_connect.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wifi_connect.sh ; echo fio | sudo -S env LIFETRAC_WIFI_SSID=\\\\\\\"${env:LIFETRAC_WIFI_SSID}\\\\\\\" LIFETRAC_WIFI_PSK=\\\\\\\"${env:LIFETRAC_WIFI_PSK}\\\\\\\" /tmp/wifi_connect.sh 2>&1\\\"", "isBackground": false, "problemMatcher": [], "group": "test" @@ -726,40 +727,45 @@ }, { "label": "Wait and connect WiFi", + "detail": "Needs LIFETRAC_WIFI_SSID and LIFETRAC_WIFI_PSK set in the PC environment (never commit them).", "type": "shell", - "command": "adb -s 2D0A1209DABC240B wait-for-device ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wifi_connect_fast.sh ; echo fio | sudo -S /tmp/wifi_connect_fast.sh 2>&1\\\"", + "command": "adb -s 2D0A1209DABC240B wait-for-device ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wifi_connect_fast.sh ; echo fio | sudo -S env LIFETRAC_WIFI_SSID=\\\\\\\"${env:LIFETRAC_WIFI_SSID}\\\\\\\" LIFETRAC_WIFI_PSK=\\\\\\\"${env:LIFETRAC_WIFI_PSK}\\\\\\\" /tmp/wifi_connect_fast.sh 2>&1\\\"", "isBackground": false, "problemMatcher": [], "group": "test" }, { "label": "Push and connect WiFi v2", + "detail": "Needs LIFETRAC_WIFI_SSID and LIFETRAC_WIFI_PSK set in the PC environment (never commit them).", "type": "shell", - "command": "adb -s 2D0A1209DABC240B wait-for-device ; adb -s 2D0A1209DABC240B push wifi_connect_fast.sh /tmp/wifi_connect_fast.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wifi_connect_fast.sh ; echo fio | sudo -S /tmp/wifi_connect_fast.sh 2>&1\\\"", + "command": "adb -s 2D0A1209DABC240B wait-for-device ; adb -s 2D0A1209DABC240B push wifi_connect_fast.sh /tmp/wifi_connect_fast.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wifi_connect_fast.sh ; echo fio | sudo -S env LIFETRAC_WIFI_SSID=\\\\\\\"${env:LIFETRAC_WIFI_SSID}\\\\\\\" LIFETRAC_WIFI_PSK=\\\\\\\"${env:LIFETRAC_WIFI_PSK}\\\\\\\" /tmp/wifi_connect_fast.sh 2>&1\\\"", "isBackground": false, "problemMatcher": [], "group": "test" }, { "label": "Rescan then connect", + "detail": "Needs LIFETRAC_WIFI_SSID and LIFETRAC_WIFI_PSK set in the PC environment (never commit them).", "type": "shell", - "command": "adb -s 2D0A1209DABC240B shell \\\"echo fio | sudo -S nmcli dev wifi rescan ifname wlan0 ; sleep 6 ; echo '--- visible nets ---' ; echo fio | sudo -S nmcli -t -f SSID,SIGNAL dev wifi list ifname wlan0 | head -n 15 ; echo '--- connect ---' ; echo fio | sudo -S nmcli dev wifi connect \\\\\\\"I'm a 5 star LAN!\\\\\\\" password 9182603083 ifname wlan0 ; sleep 3 ; echo '--- status ---' ; nmcli -t -f NAME,DEVICE,STATE con show --active ; ip -4 addr show wlan0\\\"", + "command": "adb -s 2D0A1209DABC240B shell \\\"echo fio | sudo -S nmcli dev wifi rescan ifname wlan0 ; sleep 6 ; echo '--- visible nets ---' ; echo fio | sudo -S nmcli -t -f SSID,SIGNAL dev wifi list ifname wlan0 | head -n 15 ; echo '--- connect ---' ; echo fio | sudo -S nmcli dev wifi connect \\\\\\\"${env:LIFETRAC_WIFI_SSID}\\\\\\\" password \\\\\\\"${env:LIFETRAC_WIFI_PSK}\\\\\\\" ifname wlan0 ; sleep 3 ; echo '--- status ---' ; nmcli -t -f NAME,DEVICE,STATE con show --active ; ip -4 addr show wlan0\\\"", "isBackground": false, "problemMatcher": [], "group": "test" }, { "label": "Rescan connect via script", + "detail": "Needs LIFETRAC_WIFI_SSID and LIFETRAC_WIFI_PSK set in the PC environment (never commit them).", "type": "shell", - "command": "adb -s 2D0A1209DABC240B push wifi_rescan_connect.sh /tmp/wrc.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wrc.sh ; echo fio | sudo -S /tmp/wrc.sh 2>&1\\\"", + "command": "adb -s 2D0A1209DABC240B push wifi_rescan_connect.sh /tmp/wrc.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wrc.sh ; echo fio | sudo -S env LIFETRAC_WIFI_SSID=\\\\\\\"${env:LIFETRAC_WIFI_SSID}\\\\\\\" LIFETRAC_WIFI_PSK=\\\\\\\"${env:LIFETRAC_WIFI_PSK}\\\\\\\" /tmp/wrc.sh 2>&1\\\"", "isBackground": false, "problemMatcher": [], "group": "test" }, { "label": "Persist profile and reboot", + "detail": "Needs LIFETRAC_WIFI_SSID and LIFETRAC_WIFI_PSK set in the PC environment (never commit them).", "type": "shell", - "command": "adb -s 2D0A1209DABC240B push wifi_persist_and_reboot.sh /tmp/wpr.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wpr.sh ; echo fio | sudo -S /tmp/wpr.sh 2>&1\\\"", + "command": "adb -s 2D0A1209DABC240B push wifi_persist_and_reboot.sh /tmp/wpr.sh ; adb -s 2D0A1209DABC240B shell \\\"chmod +x /tmp/wpr.sh ; echo fio | sudo -S env LIFETRAC_WIFI_SSID=\\\\\\\"${env:LIFETRAC_WIFI_SSID}\\\\\\\" LIFETRAC_WIFI_PSK=\\\\\\\"${env:LIFETRAC_WIFI_PSK}\\\\\\\" /tmp/wpr.sh 2>&1\\\"", "isBackground": false, "problemMatcher": [], "group": "test" diff --git a/connect_wpa.sh b/connect_wpa.sh index 0fe7042e..445599e4 100644 --- a/connect_wpa.sh +++ b/connect_wpa.sh @@ -1,10 +1,25 @@ #!/bin/sh -cat > /tmp/wpa.conf <<'EOF' +# WiFi credentials come ONLY from the environment -- never commit an SSID or +# password to this repo: +# LIFETRAC_WIFI_SSID network name LIFETRAC_WIFI_PSK WPA passphrase +# sudo drops the caller's environment, so pass them through, e.g. on the board: +# sudo env LIFETRAC_WIFI_SSID="" LIFETRAC_WIFI_PSK="" sh /tmp/connect_wpa.sh +# Bench note: the bench tractor runs with WiFi disabled on purpose +# (LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/ +# bench_tools/BENCH_BOARDS.md); do not re-enable it during radio legs. +: "${LIFETRAC_WIFI_SSID:?set LIFETRAC_WIFI_SSID (WiFi network name; never commit it)}" +: "${LIFETRAC_WIFI_PSK:?set LIFETRAC_WIFI_PSK (WiFi passphrase; never commit it)}" +# The config holds the passphrase: keep it root-only. (A '"' in the SSID or +# passphrase would need escaping for wpa_supplicant.conf.) +( +umask 077 +cat > /tmp/wpa.conf </dev/null || true wpa_supplicant -B -i wlan0 -c /tmp/wpa.conf sleep 3 diff --git a/do_wifi.sh b/do_wifi.sh index afa3169f..c2200d10 100644 --- a/do_wifi.sh +++ b/do_wifi.sh @@ -1,6 +1,16 @@ #!/bin/sh +# WiFi credentials come ONLY from the environment -- never commit an SSID or +# password to this repo: +# LIFETRAC_WIFI_SSID network name LIFETRAC_WIFI_PSK WPA passphrase +# sudo drops the caller's environment, so pass them through, e.g. on the board: +# sudo env LIFETRAC_WIFI_SSID="" LIFETRAC_WIFI_PSK="" sh /tmp/do_wifi.sh +# Bench note: the bench tractor runs with WiFi disabled on purpose +# (LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/ +# bench_tools/BENCH_BOARDS.md); do not re-enable it during radio legs. +: "${LIFETRAC_WIFI_SSID:?set LIFETRAC_WIFI_SSID (WiFi network name; never commit it)}" +: "${LIFETRAC_WIFI_PSK:?set LIFETRAC_WIFI_PSK (WiFi passphrase; never commit it)}" nmcli con delete 5star 2>/dev/null || true -nmcli con delete "I'm a 5 star LAN!" 2>/dev/null || true -nmcli con add type wifi con-name 5star ifname wlan0 ssid "I'm a 5 star LAN!" wifi-sec.key-mgmt wpa-psk wifi-sec.psk "9182603083" connection.autoconnect yes +nmcli con delete "$LIFETRAC_WIFI_SSID" 2>/dev/null || true +nmcli con add type wifi con-name 5star ifname wlan0 ssid "$LIFETRAC_WIFI_SSID" wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$LIFETRAC_WIFI_PSK" connection.autoconnect yes nmcli con up 5star ip -4 addr show wlan0 diff --git a/setup_5star_wifi.sh b/setup_5star_wifi.sh index b0f44834..db6fe801 100644 --- a/setup_5star_wifi.sh +++ b/setup_5star_wifi.sh @@ -1,5 +1,15 @@ #!/bin/sh +# WiFi credentials come ONLY from the environment -- never commit an SSID or +# password to this repo: +# LIFETRAC_WIFI_SSID network name LIFETRAC_WIFI_PSK WPA passphrase +# sudo drops the caller's environment, so pass them through, e.g. on the board: +# sudo env LIFETRAC_WIFI_SSID="" LIFETRAC_WIFI_PSK="" sh /tmp/setup_5star_wifi.sh +# Bench note: the bench tractor runs with WiFi disabled on purpose +# (LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/ +# bench_tools/BENCH_BOARDS.md); do not re-enable it during radio legs. +: "${LIFETRAC_WIFI_SSID:?set LIFETRAC_WIFI_SSID (WiFi network name; never commit it)}" +: "${LIFETRAC_WIFI_PSK:?set LIFETRAC_WIFI_PSK (WiFi passphrase; never commit it)}" nmcli con delete 5star 2>/dev/null || true -nmcli con add type wifi ifname wlan0 con-name 5star ssid "I'm a 5 star LAN!" -- wifi-sec.key-mgmt wpa-psk wifi-sec.psk 9182603083 connection.autoconnect yes +nmcli con add type wifi ifname wlan0 con-name 5star ssid "$LIFETRAC_WIFI_SSID" -- wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$LIFETRAC_WIFI_PSK" connection.autoconnect yes nmcli con up 5star ip -4 addr show wlan0 diff --git a/wifi_connect.sh b/wifi_connect.sh index 35071a8e..1b67a4c5 100644 --- a/wifi_connect.sh +++ b/wifi_connect.sh @@ -1,7 +1,17 @@ #!/bin/sh +# WiFi credentials come ONLY from the environment -- never commit an SSID or +# password to this repo: +# LIFETRAC_WIFI_SSID network name LIFETRAC_WIFI_PSK WPA passphrase +# sudo drops the caller's environment, so pass them through, e.g. on the board: +# sudo env LIFETRAC_WIFI_SSID="" LIFETRAC_WIFI_PSK="" sh /tmp/wifi_connect.sh +# Bench note: the bench tractor runs with WiFi disabled on purpose +# (LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/ +# bench_tools/BENCH_BOARDS.md); do not re-enable it during radio legs. set -e -SSID="I'm a 5 star LAN!" -PSK="9182603083" +: "${LIFETRAC_WIFI_SSID:?set LIFETRAC_WIFI_SSID (WiFi network name; never commit it)}" +: "${LIFETRAC_WIFI_PSK:?set LIFETRAC_WIFI_PSK (WiFi passphrase; never commit it)}" +SSID="$LIFETRAC_WIFI_SSID" +PSK="$LIFETRAC_WIFI_PSK" nmcli radio wifi on nmcli dev wifi rescan 2>/dev/null || true sleep 3 diff --git a/wifi_connect_fast.sh b/wifi_connect_fast.sh index 48d88c10..6094fe18 100644 --- a/wifi_connect_fast.sh +++ b/wifi_connect_fast.sh @@ -1,6 +1,16 @@ #!/bin/sh # Wait for brcmfmac firmware load to complete, then connect FAST +# WiFi credentials come ONLY from the environment -- never commit an SSID or +# password to this repo: +# LIFETRAC_WIFI_SSID network name LIFETRAC_WIFI_PSK WPA passphrase +# sudo drops the caller's environment, so pass them through, e.g. on the board: +# sudo env LIFETRAC_WIFI_SSID="" LIFETRAC_WIFI_PSK="" sh /tmp/wifi_connect_fast.sh +# Bench note: the bench tractor runs with WiFi disabled on purpose +# (LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/ +# bench_tools/BENCH_BOARDS.md); do not re-enable it during radio legs. set -e +: "${LIFETRAC_WIFI_SSID:?set LIFETRAC_WIFI_SSID (WiFi network name; never commit it)}" +: "${LIFETRAC_WIFI_PSK:?set LIFETRAC_WIFI_PSK (WiFi passphrase; never commit it)}" echo "=== waiting for wlan0 to appear ===" for i in 1 2 3 4 5 6 7 8 9 10 15 20; do if ip link show wlan0 >/dev/null 2>&1; then @@ -17,7 +27,7 @@ echo "=== rescan ===" nmcli dev wifi rescan ifname wlan0 2>/dev/null || true sleep 4 echo "=== connect ===" -nmcli dev wifi connect "I'm a 5 star LAN!" password "9182603083" ifname wlan0 +nmcli dev wifi connect "$LIFETRAC_WIFI_SSID" password "$LIFETRAC_WIFI_PSK" ifname wlan0 sleep 3 echo "=== status ===" nmcli -t -f NAME,DEVICE,STATE con show --active diff --git a/wifi_persist_and_reboot.sh b/wifi_persist_and_reboot.sh index 395ed016..1229de48 100644 --- a/wifi_persist_and_reboot.sh +++ b/wifi_persist_and_reboot.sh @@ -1,9 +1,19 @@ #!/bin/sh # Pre-create the NetworkManager profile so auto-connect fires during the # brief healthy window right after boot. +# WiFi credentials come ONLY from the environment -- never commit an SSID or +# password to this repo: +# LIFETRAC_WIFI_SSID network name LIFETRAC_WIFI_PSK WPA passphrase +# sudo drops the caller's environment, so pass them through, e.g. on the board: +# sudo env LIFETRAC_WIFI_SSID="" LIFETRAC_WIFI_PSK="" sh /tmp/wifi_persist_and_reboot.sh +# Bench note: the bench tractor runs with WiFi disabled on purpose +# (LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/ +# bench_tools/BENCH_BOARDS.md); do not re-enable it during radio legs. set -e -SSID="I'm a 5 star LAN!" -PSK="9182603083" +: "${LIFETRAC_WIFI_SSID:?set LIFETRAC_WIFI_SSID (WiFi network name; never commit it)}" +: "${LIFETRAC_WIFI_PSK:?set LIFETRAC_WIFI_PSK (WiFi passphrase; never commit it)}" +SSID="$LIFETRAC_WIFI_SSID" +PSK="$LIFETRAC_WIFI_PSK" NAME="5star" # Delete any prior profile with same name diff --git a/wifi_rescan_connect.sh b/wifi_rescan_connect.sh index a412d92d..1178204b 100644 --- a/wifi_rescan_connect.sh +++ b/wifi_rescan_connect.sh @@ -1,11 +1,21 @@ #!/bin/sh +# WiFi credentials come ONLY from the environment -- never commit an SSID or +# password to this repo: +# LIFETRAC_WIFI_SSID network name LIFETRAC_WIFI_PSK WPA passphrase +# sudo drops the caller's environment, so pass them through, e.g. on the board: +# sudo env LIFETRAC_WIFI_SSID="" LIFETRAC_WIFI_PSK="" sh /tmp/wifi_rescan_connect.sh +# Bench note: the bench tractor runs with WiFi disabled on purpose +# (LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/ +# bench_tools/BENCH_BOARDS.md); do not re-enable it during radio legs. +: "${LIFETRAC_WIFI_SSID:?set LIFETRAC_WIFI_SSID (WiFi network name; never commit it)}" +: "${LIFETRAC_WIFI_PSK:?set LIFETRAC_WIFI_PSK (WiFi passphrase; never commit it)}" echo "=== rescan (12s) ===" nmcli dev wifi rescan ifname wlan0 2>&1 || true sleep 12 echo "=== visible nets ===" nmcli -t -f SSID,SIGNAL dev wifi list ifname wlan0 | head -n 15 echo "=== connect ===" -nmcli dev wifi connect "I'm a 5 star LAN!" password "9182603083" ifname wlan0 +nmcli dev wifi connect "$LIFETRAC_WIFI_SSID" password "$LIFETRAC_WIFI_PSK" ifname wlan0 sleep 4 echo "=== active connections ===" nmcli -t -f NAME,DEVICE,STATE con show --active From dc5b9bb8d86b92fdbc73f2698b1234765999c55d Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:19:28 -0500 Subject: [PATCH 05/32] bench kit (K6): BENCH_QUICKSTART.md entry point + PC_SETUP.md 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 --- .../bench_tools/BENCH_QUICKSTART.md | 276 ++++++++++++++++++ .../bench_tools/PC_SETUP.md | 188 ++++++++++++ 2 files changed, 464 insertions(+) create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_QUICKSTART.md create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/PC_SETUP.md diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_QUICKSTART.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_QUICKSTART.md new file mode 100644 index 00000000..6e784e8a --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_QUICKSTART.md @@ -0,0 +1,276 @@ +# Radio bench quickstart + +Start here to reproduce the LifeTrac v25 LoRa bench from GitHub: two +Portenta X8 boards on Max Carriers talking over their Murata L072 radios, as +in the RS-12 and RS-13 campaigns. This page lists the steps in order with the +commands. The linked documents explain each step. + +**The GO rule.** Radios run only on an explicit operator GO, given for each +round. Without that GO, nothing that opens `/dev/ttymxc3` runs: no flash +(the L072 boots into RXCONT, so a flash turns the receiver on), no probe, no +channel spot-check, no leg. There is one exception: `radio_park.py` and +`radio_state.py`, which put a radio to sleep or check that it is asleep. +Power-up, staging, deploys and image builds do not touch the radios. + +## What you need + +- **Boards.** Two Arduino Portenta X8 (ABX00049), each on a Portenta Max + Carrier (ABX00043). The carrier holds the Murata CMWX1ZZABZ (L072 + SX1276). + You also need a 12 V supply per carrier and a USB-C data cable from each + board straight to the PC (no hub). +- **Antennas.** A 915 MHz antenna on each carrier's LoRa SMA (the bench uses + Taoglas TD.95.6H31). Never key a radio without one. +- **Network.** Ethernet for the **base** to a LAN with DHCP and internet; it + builds the Docker images. The **tractor** has WiFi off and is reached over + adb only. +- **Camera legs.** A USB UVC camera on the tractor (it enumerates as + `/dev/video1`), aimed at the PC screen. +- **PC.** Windows 11 on the same LAN as the base. Set it up with + [PC_SETUP.md](PC_SETUP.md). + +Board roles on this bench: the **base** is `2D0A1209DABC240B` and the +**tractor** is `2E2C1209DABC240B`. [BENCH_BOARDS.md](BENCH_BOARDS.md) lists +what is on every chip. + +## 0. PC setup + +Follow [PC_SETUP.md](PC_SETUP.md): Git for Windows (long paths), Python +through `py -3` plus [`requirements-pc.txt`](requirements-pc.txt), the Arm GNU +Toolchain 12.2.MPACBTI-Rel1, WinLibs `mingw32-make`, adb, and optionally `uuu` +and arduino-cli. + +The commands below run in **Git Bash from the repo root**, except those marked +PowerShell. They use these shell variables. The values shown are this bench's; +replace them with yours from `bench.env` (step 4). + +```bash +export MSYS_NO_PATHCONV=1 # adb from Git Bash: no MSYS path rewriting +DC=LifeTrac-v25/DESIGN-CONTROLLER +BT=$DC/firmware/x8_lora_bootloader_helper/bench_tools +BASE="ssh -i $HOME/.ssh/lifetrac_base_ed25519 fio@192.168.1.117" # base: ethernet + ssh +TRAC="adb -s 2E2C1209DABC240B shell" # tractor: adb only +R="docker run --rm --network=host --entrypoint python3 --device=/dev/ttymxc3 -v /tmp/lifetrac_strict:/work -w /work -e PYTHONPATH=/work:/work/paho" +BI=lifetrac-v25:latest # base daemon image +TI=hub.foundries.io/arduino/arduino-ootb-python-devel:738bc44 # tractor probe image +``` + +## 1. Boards (once per board) + +[BENCH_SETUP.md](BENCH_SETUP.md) covers this step: + +- the OS image, pinned by URL and sha256 (base LmP 4.0.11-934-91; the tractor + runs factory 4.0.3-674-88); +- reflashing through + [T3a](../../../X8_HEALTH_AND_RECOVERY/recovery/T3a_sdp_uuu_reflash.md) if + you need to; +- first boot; +- [`provision_bench_board.sh`](provision_bench_board.sh), which installs the + sudoers drop-in, masks the compose early-start units on the base **before + any docker use**, and on the tractor adds WiFi-off + ([`units/disable-wifi.service`](units/disable-wifi.service)) and the camera + unit handling. + +## 2. Radio firmware (L072) + +Both boards run the bench build **`0c1bb0a9`**. The production +`build/firmware.bin` (`589c1203`) refuses the bench carrier pin, so do not put +it on a bench board. + +```powershell +# PowerShell (mingw32-make does not work from Git Bash) +cd LifeTrac-v25\DESIGN-CONTROLLER\firmware\murata_l072 +mingw32-make check +mingw32-make bench +(Get-FileHash build\firmware_bench_diag.bin -Algorithm MD5).Hash # 0C1BB0A9573F813137F941DFA47177D0 +``` + +A different md5 means a different toolchain; see the toolchain section of +[murata_l072/README.md](../../murata_l072/README.md). If you have no +toolchain, the same bytes are committed as +[`firmware_bench_diag_0c1bb0a9.bin`](../../../bench-evidence/RS_13_vector_scene_2026-09-26/firmware/). + +To flash (**needs the GO**), push the tooling LF-clean and run +`REVIVE_MODE=reboot` exactly as [FLASH_RUNBOOK.md](../FLASH_RUNBOOK.md) §1–§4 +says. Success is `Verify OK` and `flash_rc=0` in +`/home/fio/pipeline_stamped.log`. The flash reboots the board, which wipes +`/tmp`, so stage again afterwards (step 5). + +## 3. Board software + +[DEPLOY.md](DEPLOY.md) explains both scripts and their arguments: + +- [`deploy_base.sh`](deploy_base.sh) puts a commit's `DESIGN-CONTROLLER` + subset on the base and builds `lifetrac-v25:latest` there. It never starts + `lora_bridge`. The base broker `design-controller-mosquitto-1` must be + running. +- [`build_tractor_image.sh`](build_tractor_image.sh) builds + `lifetrac-tractor-x8` natively on the base, then loads it onto the offline + tractor. It also loads the two images the tractor cannot pull: + `foundries_python.tar` (repo root) and `eclipse-mosquitto:2`. + +Wheel versions are pinned in the `requirements.lock.txt` files next to each +`requirements.txt`. + +## 4. Configure `bench.env` + +Copy [`bench.env.example`](bench.env.example) to `bench.env` as its header +says, then fill in your values: the board serials, the base address, the ssh +key path, the board password and the PC's LAN address. The `legs/` scripts +read it through [`lib/bench_env.sh`](lib/bench_env.sh). Do not commit +`bench.env`. + +## 5. Every session + +1. **Power-up radio safety.** Start the guard, then power the boards: + + ```bash + bash "$BT/legs/power_up_guard.sh" + ``` + + As each board comes up, the guard stops the tractor's + `lifetrac-camera.service` and the `tractor-camera` container (both grab + `/dev/ttymxc3` at every boot) and reports any UART holder. It sends nothing + to the radio. Each L072 boots into RXCONT, which only listens. If the + radios must stay quiet, park them (step 8). +2. **Staging.** + + ```bash + bash "$BT/legs/stage_boards.sh" + ``` + + This re-pushes `/tmp/lifetrac_strict` on both boards. Run it after any + reboot (and every flash reboots) or when the last push is 5 days old, + because `systemd-tmpfiles` ages `/tmp`. Afterwards, + `ls /tmp/lifetrac_strict | wc -l` must be at least 19. Flash staging in + `/tmp/lifetrac_p0c` must count 11 files (FLASH_RUNBOOK). +3. **Moving scene (camera legs).** Play the railroad video in a **normal** + Firefox window of a separate profile, with the player's own fullscreen + (Esc leaves it). **Never use `firefox --kiosk`**: a kiosk locked the + operator out of the PC. Before **every** camera run, check the scene: at + least 8 % of pixels must change over 10 s (`legs/scene_check.sh`; see + [legs/README.md](legs/README.md)). +4. **Channel spot-check (needs the GO; receive only) and the DTS carrier + pin.** Park the tractor, then sniff the candidate channel on the base, + where the known emitter is about 20 dB hotter: + + ```bash + scp -i $HOME/.ssh/lifetrac_base_ed25519 $DC/firmware/x8_lora_bootloader_helper/channel_survey_sniff.py fio@192.168.1.117:/tmp/lifetrac_strict/ + $BASE "sudo docker rm -f rx_smoke 2>/dev/null; sudo $R -e LIFETRAC_REG_PROFILE=2 $BI -u /work/channel_survey_sniff.py --start-hz 927500000 --stop-hz 927500000 --step-hz 500000 --dwell-s 45 --interval-s 0.05" | tee spot_$(date +%F).txt + py -3 $DC/tools/survey_compare.py spot_$(date +%F).txt + ``` + + A hot sample rules a channel out. A clean one does not prove the link; + only the leg does. To sweep instead of spot-checking, use + `--start-hz 902500000 --stop-hz 927500000 --dwell-s 30`, which takes about + 26 minutes. Pass `-ForceFrfHz ` on **every** profile-2 leg and + record it in RESULTS. The default, `-ForceFrfHz 0`, tunes 915.000 MHz, + which is the channel of an external emitter (RS-11.6). Picks hold for + hours, not days. + +## 6. Run a leg (needs the GO) + +[legs/README.md](legs/README.md) lists the per-leg prep and post scripts and +their arguments. They are parameterised copies of RS-13's `leg_prep.sh` and +`leg_post.sh`: + +- **prep:** scene check, health probes, `clear_retained.py`, pre-brackets, + base-side capture; +- **post:** post-brackets, reports, park. + +Then launch the harness, +[`run_live_radio_monitor.ps1`](../run_live_radio_monitor.ps1), from +PowerShell in the helper directory. This is RS-13 leg 2a, VECTOR at boot on +DTS: + +```powershell +cd LifeTrac-v25\DESIGN-CONTROLLER\firmware\x8_lora_bootloader_helper +.\run_live_radio_monitor.ps1 -TxFeed camera -RegProfile 2 -ForceFrfHz 927500000 -DurationS 300 -SynthFps 2 ` + -KfRequestDisable 1 -ProbeEcho 0 -LogFragArrivals 1 -TxBatch 0 ` + -CamExtraEnv "-e LIFETRAC_ENCODE_MODE=9 -e LIFETRAC_VECTOR_DETAIL=80" -HostIp -Archive +``` + +- 2b is the same leg on FHSS (`-RegProfile 1`, no `-ForceFrfHz`). +- 2c is the `mono_g4` control (`-CamExtraEnv "-e LIFETRAC_ENCODE_MODE=6"`). +- 2d is the over-the-air switch. + +[RS13_VECTOR_LEG.md](RS13_VECTOR_LEG.md) gives each leg and the pass criteria +P1–P8. Start the session's `RESULTS.md` from +[RS13_RESULTS_TEMPLATE.md](RS13_RESULTS_TEMPLATE.md). For RS-12-style tile +legs, see [BENCH_RUNBOOK.md](BENCH_RUNBOOK.md), "A leg". The archive goes to +`DESIGN-CONTROLLER/bench-evidence/radio_monitor__/`, and its +`params.txt` records what actually ran. + +## 7. Analyse + +The tools are +[`rs12_leg_report.py`](../../../tools/rs12_leg_report.py), +[`frag_gap_report.py`](frag_gap_report.py) and +[`vector_dry_run.py`](../../../tools/vector_dry_run.py). Run them from +`DESIGN-CONTROLLER`. From Git Bash, give them Windows-style `C:/...` paths, +not `/c/...`: + +```powershell +py -3 tools/rs12_leg_report.py --pre /leg_pre_base.txt --post /leg_post_base.txt --capture /leg_base.jsonl +py -3 firmware/x8_lora_bootloader_helper/bench_tools/frag_gap_report.py +py -3 tools/vector_dry_run.py replay /leg_base.jsonl --profile image_bw500 +py -3 tools/vector_dry_run.py tractor-log /camera_service.log +``` + +Then run `legs/leg_replay.py` for the post-lock sequence gaps and the P1 +resync episodes. Its arguments are in [legs/README.md](legs/README.md). + +The loss of record is the report's `capture seq gaps` line. The counter +`loss` line can be stale (RS13_VECTOR_LEG A14) or frozen by a dead stats +thread (RS-12.17). + +## 8. Park and power down + +About 2 minutes after the daemons stop, park both radios with +[`radio_park.py`](radio_park.py). The firmware's scan needs that long to fail +out. + +```bash +$BASE "sudo $R $BI -u /work/radio_park.py" # want PARK_OK, both readbacks 0x80 +$TRAC "sudo -n $R $TI -u /work/radio_park.py" +``` + +`PARK_TRANSIENT` (exit 5) means the scan was still walking: wait 60 s and +repeat. The park readback does not guarantee the radio stays off, so before +you call the bench off, re-check both boards with the read-only +[`radio_state.py`](radio_state.py) (same command line). + +To power down: + +- **tractor:** `$TRAC "sudo -n systemctl poweroff"`; it stays off. +- **base:** `$BASE "sudo systemctl poweroff"`, then **remove its power**. + Otherwise it boots again about 4.5 minutes later. + +## 9. Capture board state + +```bash +ARCHIVE= bash "$BT/pull_board_state.sh" tractor # add --images for docker saves +ARCHIVE= bash "$BT/pull_board_state.sh" base +``` + +The capture is read-only and withholds secrets. First stop the tractor's +camera unit (step 5.1). Review the reports before you copy text into +`bench-evidence/`, and never commit images or secrets. See +[BENCH_BOARDS.md](BENCH_BOARDS.md), "Capture at a power-up". + +## Troubleshooting + +[BENCH_RUNBOOK.md](BENCH_RUNBOOK.md) has the board facts and the full trap +list. These are the common symptoms: + +| symptom | cause | fix | +|---|---|---| +| probe or daemon gets no HostLink answer although the L072 transmits | tractor camera unit or container holds `/dev/ttymxc3` | `systemctl stop lifetrac-camera.service; docker stop tractor-camera`; `fuser /dev/ttymxc3` must be empty | +| flash fails with `run_flash_bench.sh: line 5: 1: image` (rc 127) | `/tmp/lifetrac_p0c` aged out, or a script was pushed with CRLF | re-push LF-clean, count 11 files (FLASH_RUNBOOK §1) | +| `ERR_PROTO FORBIDDEN detail=6` on `-ForceFrfHz` or a survey | production firmware on the board | flash the bench build (step 2) | +| DTS loss folds on a ~7.08 s period | the leg flew on 915.000 MHz | spot-check and pin `-ForceFrfHz` (step 5.4) | +| camera changes mode or a profile is commanded mid-leg | retained control topics | `clear_retained.py` on the base broker **and** `clear_retained_host.py` on the PC broker | +| report says ~99.8 % loss and `published=1` | stats thread died (RS-12.17) | use `capture seq gaps` / count `published frame_id` lines | +| docker images vanish on the base | `compose-apps-early-start-recovery.service` is unmasked: it runs `rm -rf /var/lib/docker` every 60 s | mask it ([BENCH_SETUP.md](BENCH_SETUP.md)) | +| board resets about 60 s into a long operation | the U-Boot-armed X8 watchdog | FLASH_RUNBOOK §3; close `/dev/watchdog0` with `V` | +| a harness parameter seems ignored | misspelled name | the harness now rejects unknown names; read the archive's `params.txt` | +| adb lost the board after `adb kill-server` | the board's adbd hung | run `adb -s wait-for-device` once; if that fails, replug USB-C or press RESET. Avoid `kill-server` while the boards are enumerating ([ADB_TIPS_AND_TRICKS.md](../../../X8_HEALTH_AND_RECOVERY/recovery/ADB_TIPS_AND_TRICKS.md)) | diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/PC_SETUP.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/PC_SETUP.md new file mode 100644 index 00000000..83ed2cb6 --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/PC_SETUP.md @@ -0,0 +1,188 @@ +# Bench PC setup (Windows 11) + +One Windows 11 PC drives the bench. Use **Git Bash** for the board scripts +(`*.sh`, adb, ssh) and **PowerShell** for the radio harness and +`mingw32-make`. The versions below are the ones that produced the RS-12 and +RS-13 evidence. Where a version is marked exact, a different one gives +different bytes. The PC needs no Docker, because images are built on the base +X8 ([DEPLOY.md](DEPLOY.md)). Next step: [BENCH_QUICKSTART.md](BENCH_QUICKSTART.md). + +| tool | bench PC version | needed for | +|---|---|---| +| Git for Windows (Git Bash) | 2.52 | checkout, board scripts | +| Python with the `py` launcher | 3.14 (CI: 3.11) | harness helpers, analysis tools, tests | +| Arm GNU Toolchain | **12.2.MPACBTI-Rel1, exact** | byte-identical L072 builds | +| WinLibs `mingw32-make` | GNU Make 4.4.1 | L072 build (PowerShell only) | +| Android platform-tools (`adb`) | current | tractor access, programming | +| `uuu` | 1.5.243 | OS reflash only | +| arduino-cli + `arduino:mbed_portenta` | 1.4.1 + **4.5.0** | optional: X8 H7/M4 sketches | +| SEGGER J-Link Software | current | optional: last-resort L072 recovery | +| Firefox | current | the moving scene for camera legs | + +## Git for Windows + +```bash +winget install Git.Git +git clone -c core.longpaths=true https://github.com/OpenSourceEcology/LifeTrac.git +``` + +- **Long paths.** Some `bench-evidence/` paths are longer than Windows' 260 + characters, and the checkout fails on them without `core.longpaths`. For an + existing clone, run `git config core.longpaths true`. +- **Line endings.** Git for Windows defaults to `core.autocrlf=true`. The + repo's [`.gitattributes`](../../../../../.gitattributes) keeps board-side + files (shell scripts, openocd `.cfg`, ...) LF in the working tree, because + a CRLF script breaks on the board: you get `1: image` from the flash + wrapper, or `/tmp/lifetrac_p0c\r/...`. Files checked out before + `.gitattributes` landed stay CRLF until git rewrites them, so still strip + `\r` when you push ([FLASH_RUNBOOK.md](../FLASH_RUNBOOK.md) §1). +- **ssh** ships with Git for Windows. Create the bench key once, then install + its public half on the base as [BENCH_SETUP.md](BENCH_SETUP.md) says. Keep + the private key in a password manager, never in git. + + ```bash + ssh-keygen -t ed25519 -f ~/.ssh/lifetrac_base_ed25519 -C lifetrac-bench-pc + ``` + +## Python + +Install Python 3.11 or newer from python.org; it comes with the `py` +launcher. Every command in the kit uses `py -3`. From the repo root: + +```bash +py -3 -m pip install -r LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/requirements-pc.txt +py -3 -m pip install -r LifeTrac-v25/DESIGN-CONTROLLER/base_station/requirements-dev.txt +``` + +- [`requirements-pc.txt`](requirements-pc.txt) holds what the harness and the + PC-side bench tools import, including the amqtt broker. +- `requirements-dev.txt` holds the test suite and the base-station modules + that the analysis tools load (`tools/vector_dry_run.py replay`, + `tools/rs12_leg_report.py`). + +The harness starts a PC MQTT broker (`start_mqtt_broker.py` at the repo root, +amqtt, listening on `0.0.0.0:1883`) when nothing answers on `-HostIp`:1883. + +- Check that the harness finds your Python. Older copies hardcoded the + original bench PC's `python.exe` path. +- The base's rx daemon uses this broker as its control broker, so allow + inbound TCP 1883 when Windows Defender Firewall asks, on **Private** + networks only. +- Retained control topics live on this broker as well as on the base's. + Clear both between campaigns with `clear_retained_host.py` + ([BENCH_RUNBOOK.md](BENCH_RUNBOOK.md)). + +### Run the test suite locally + +```bash +cd LifeTrac-v25/DESIGN-CONTROLLER/base_station +LIFETRAC_FLEET_KEY_HEX=0102030405060708090a0b0c0d0e0f10 py -3 -m pytest tests -q +``` + +In PowerShell: `$env:LIFETRAC_FLEET_KEY_HEX = "0102030405060708090a0b0c0d0e0f10"; py -3 -m pytest tests -q`. + +That key is the dummy key CI uses. Real fleet keys (the PC's gitignored +`key.h`, the base's `secrets/`) never go in git. + +## Arm GNU Toolchain 12.2.MPACBTI-Rel1 (exact) + +1. Download it from developer.arm.com: *Arm GNU Toolchain Downloads*, release + **12.2.MPACBTI-Rel1**. Take the Windows (mingw-w64-i686) host, + AArch32 bare-metal (`arm-none-eabi`) installer, + `arm-gnu-toolchain-12.2.mpacbti-rel1-mingw-w64-i686-arm-none-eabi.exe`. +2. Install it and tick *Add path to environment variable*. It installs to + `C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\12.2 mpacbti-rel1\bin`. +3. Check it: + + ``` + arm-none-eabi-gcc --version + arm-none-eabi-gcc.exe (Arm GNU Toolchain 12.2.MPACBTI-Rel1 (Build arm-12-mpacbti.34)) 12.2.1 20230214 + ``` + +Only this toolchain rebuilds the bench image `0c1bb0a9` and the production +image `589c1203` byte for byte. Any other GCC, including CI's Ubuntu +`gcc-arm-none-eabi`, builds working firmware with different bytes. Expected +md5s and the build steps are in +[murata_l072/README.md](../../murata_l072/README.md). + +## WinLibs `mingw32-make` (PowerShell only) + +```powershell +winget install BrechtSanders.WinLibs.POSIX.UCRT +``` + +This provides `mingw32-make.exe` (GNU Make 4.4.1 on the bench) and the host +`gcc` that `mingw32-make check` needs. If `mingw32-make` is not on PATH after +the install, it is in +`%LOCALAPPDATA%\Microsoft\WinGet\Packages\BrechtSanders.WinLibs.POSIX.UCRT_Microsoft.Winget.Source_8wekyb3d8bbwe\mingw64\bin`. + +Run it from **PowerShell**. It fails on this bench when run from Git Bash. + +## Android platform-tools (adb) + +```powershell +winget install Google.PlatformTools +``` + +The harness finds `adb` on PATH. From Git Bash, set `MSYS_NO_PATHCONV=1` and +give local files as Windows-style paths. Otherwise MSYS rewrites `/tmp/...` +arguments into Windows paths: + +```bash +export MSYS_NO_PATHCONV=1 +adb -s 2E2C1209DABC240B push C:/path/to/file.py /tmp/lifetrac_strict/ +``` + +Plug each board straight into a PC port, not a hub. Do not reach for +`adb kill-server` while boards are enumerating: it has dropped boards that +then did not come back ([ADB_TIPS_AND_TRICKS.md](../../../X8_HEALTH_AND_RECOVERY/recovery/ADB_TIPS_AND_TRICKS.md)). + +## uuu 1.5.243 (OS reflash only) + +You need `uuu` only to reflash an X8's eMMC over SDP +([T3a](../../../X8_HEALTH_AND_RECOVERY/recovery/T3a_sdp_uuu_reflash.md); +[BENCH_SETUP.md](BENCH_SETUP.md) pins the image). The LmP bundle ships its own +`uuu.exe` in `mfgtool-files-portenta-x8/`; NXP publishes the same version +under `github.com/nxp-imx/mfgtools/releases` (tag `uuu_1.5.243`). SDP needs +USB-C **and** 12 V on the carrier, plus a direct USB port. + +## Optional + +- **arduino-cli** (1.4.1 on the bench) with + `arduino-cli core install arduino:mbed_portenta@4.5.0`. You need it only + for the X8's H7/M4 sketches, not for radio legs. A sketch upload to the M7 + overwrites the stock x8h7 bridge ([BENCH_BOARDS.md](BENCH_BOARDS.md)). +- **SEGGER J-Link Software.** Use it only for last-resort L072 recovery over + SWD when the ROM-bootloader flash path fails. The carrier's L072 SWD header + CN2 is not populated by default + ([BRINGUP_MAX_CARRIER.md](../../murata_l072/BRINGUP_MAX_CARRIER.md) §2). + Do not accept J-Link OB firmware-update prompts. + +## Firefox (camera legs) + +Camera legs need moving content on the screen the camera watches. Set it up +like this: + +1. Open a **normal** window of a separate profile: + `firefox --no-remote -profile `. +2. Put a `user.js` in that profile with + `user_pref("media.autoplay.default", 0);` and + `user_pref("media.volume_scale", "0.0");`. +3. Open the railroad video by its watch URL. +4. Switch to the player's own fullscreen (`f`). Esc leaves it. + +**Never use `firefox --kiosk`**, or anything else fully fullscreen that the +operator cannot leave: a kiosk locked the operator out of the PC for a whole +round. The window helper and the scene check are in +[legs/README.md](legs/README.md); the procedure is +[RS13_VECTOR_LEG.md](RS13_VECTOR_LEG.md), "Scene". + +## Check + +```powershell +git config core.longpaths # true +py -3 --version # 3.11 or newer +arm-none-eabi-gcc --version # ... 12.2.MPACBTI-Rel1 ... 12.2.1 20230214 +mingw32-make --version # GNU Make 4.4.x +adb version +``` From b86399443ee63ae6949e5450d778f1762803ff94 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:19:36 -0500 Subject: [PATCH 06/32] bench kit (K6): runbook web UI recipe + quickstart pointers 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 --- LifeTrac-v25/DESIGN-CONTROLLER/README.md | 1 + .../bench_tools/BENCH_RUNBOOK.md | 79 ++++++++++++++++++- README.md | 4 + 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/README.md b/LifeTrac-v25/DESIGN-CONTROLLER/README.md index 3f5e0739..16a67ce7 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/README.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/README.md @@ -46,6 +46,7 @@ This folder contains the **primary controller design** for the LifeTrac v25 trac - **[VIDEO_OPTIONS.md](VIDEO_OPTIONS.md)** — wireless video streaming analysis (LoRa thumbnails on the air; WiFi/cellular alternatives are archived) - **[ARDUINO_CI.md](ARDUINO_CI.md)** — Arduino-CLI continuous integration setup - **[firmware/murata_l072/BRINGUP_MAX_CARRIER.md](firmware/murata_l072/BRINGUP_MAX_CARRIER.md)** — Murata L072 flash + bench bring-up runbook (SWD/OpenOCD/J-Link/DFU) +- **[Radio bench quickstart](firmware/x8_lora_bootloader_helper/bench_tools/BENCH_QUICKSTART.md)** — how to reproduce the two-board LoRa bench (the RS-12/RS-13 campaigns): PC and board setup, L072 firmware, deploy, legs, analysis, park - **[arduino_libraries.txt](arduino_libraries.txt)** — pinned library versions - **[RESEARCH-CONTROLLER/WIRELESS_OPTIONS.md](RESEARCH-CONTROLLER/WIRELESS_OPTIONS.md)** *(archived)* — historical comparison of wireless technologies considered before LoRa was selected diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_RUNBOOK.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_RUNBOOK.md index d3922990..8bce1871 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_RUNBOOK.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_RUNBOOK.md @@ -5,6 +5,17 @@ next session starts from a checklist instead of memory. Everything that keys a radio needs the operator's GO; radios are parked (LoRa SLEEP, `0x80` readback) whenever a session ends. +**New to the bench?** Start at [BENCH_QUICKSTART.md](BENCH_QUICKSTART.md). +It walks through PC and board setup, radio firmware, deploy, a first leg, +analysis and park, in order, with the commands. This runbook is the +per-session reference and the trap list. The rest of the bench kit: + +- [PC_SETUP.md](PC_SETUP.md) +- [BENCH_SETUP.md](BENCH_SETUP.md) +- [DEPLOY.md](DEPLOY.md) +- [`bench.env.example`](bench.env.example) +- [legs/README.md](legs/README.md) + ## What is here | file | runs on | purpose | @@ -37,7 +48,7 @@ the harness (`run_live_radio_monitor.ps1`) live one directory up. Leg reports: 12.2.MPACBTI-Rel1) gives byte-identical L072 builds. Also: WinLibs `mingw32-make` (PowerShell only), arduino-cli with `arduino:mbed_portenta` 4.5.0, `py -3`. The PC has no Docker; images are - built on the base X8. + built on the base X8. Installation steps: [PC_SETUP.md](PC_SETUP.md). * Base `192.168.1.117` via ssh key `~/.ssh/lifetrac_base_ed25519`; tractor ONLY via `adb -s 2E2C1209DABC240B` (WiFi stays off); base adb `2D0A1209DABC240B`. Password `fio` → `echo fio | sudo -S -p ''`. @@ -80,7 +91,8 @@ the harness (`run_live_radio_monitor.ps1`) live one directory up. Leg reports: 1. `git archive` the helper tooling to `/tmp/lifetrac_strict` on both boards (probes, `lora_proto.py`, and everything in this directory except the `/home/fio` files). `adb push` needs `MSYS_NO_PATHCONV=1` and a - Windows-style `C:/...` source path. + Windows-style `C:/...` source path. `legs/stage_boards.sh` does this + step from `bench.env` ([legs/README.md](legs/README.md)). 2. Flash staging: push the helper `.sh`/`.cfg`/`.py` pipeline files plus the bins to `/tmp/lifetrac_p0c` on both boards **LF-clean** (`tr -d '\r'`); a CRLF script fails silently as `1: image` from the wrapper. `scp` to the @@ -137,9 +149,72 @@ leg V), but that is because of the timing above, not because the write is authoritative. RXCONT is receive-only — the TX path only fires on a host request, and there is none with the daemons down. +## Bench web UI on the base (optional) + +The operator console (`base_station/web_ui.py`) is not part of the harness +loop. Run it on the base only when a leg needs it, for example the RS-1.4 +Auto selector (leg V) or a look at the tiles. While it runs it adds base +commands to the control plane (TILE_STALE scans, profile switches), so say in +RESULTS that it ran. This recipe comes from the F10/F11 sessions and leg V. + +1. **Push** `web_ui.py`, `lora_proto.py`, `image_pipeline/` and `web/` from + the checkout to `/tmp/lifetrac_strict` on the base. The harness pushes + `lora_proto.py` and `image_pipeline/` at every launch, but after a reboot + nothing is there until you push it. If `scp` cannot overwrite + `image_pipeline/`, remove its root-owned `__pycache__` first. + + ```bash + K="-i $HOME/.ssh/lifetrac_base_ed25519"; B=fio@192.168.1.117 + S=LifeTrac-v25/DESIGN-CONTROLLER/base_station + ssh $K $B "sudo rm -rf /tmp/lifetrac_strict/image_pipeline/__pycache__" + scp $K -r $S/web_ui.py $S/lora_proto.py $S/image_pipeline $S/web $B:/tmp/lifetrac_strict/ + ``` + +2. **Run** it in `lifetrac-v25:latest` with host networking: uvicorn on port + **8090**, away from the production compose's 8080, + MQTT to the base broker at `127.0.0.1`, and PIN **2525**. That PIN is for + the bench only and is never a field PIN; production reads its PIN from + `secrets/lifetrac_pin`. + + ```bash + ssh $K $B "sudo docker rm -f bench_webui 2>/dev/null; sudo docker run -d --name bench_webui --network=host \ + -v /tmp/lifetrac_strict:/work -w /work -e PYTHONPATH=/work:/work/paho \ + -e LIFETRAC_MQTT_HOST=127.0.0.1 -e LIFETRAC_PIN=2525 \ + --entrypoint python3 lifetrac-v25:latest -m uvicorn web_ui:app --host 0.0.0.0 --port 8090" + ``` + + Browse to `http://:8090`. Some modules, such as `audit_log`, + `build_config` and `settings_store`, are imported lazily, and web_ui only + degrades when they are missing. If a feature needs one, append + `:/app/base_station` to `PYTHONPATH` so the image's deployed copy fills + the gap. + +3. **Record what it did** with an MQTT tap. web_ui has no root logger, so its + info lines never reach `docker logs`: + + ```bash + ssh $K $B "sudo docker exec design-controller-mosquitto-1 mosquitto_sub -h 127.0.0.1 -F '%U %t %p' \ + -t lifetrac/v25/control/radio_profile -t 'lifetrac/v25/status/radio_profile/#' -t lifetrac/v25/video/link_stats" | tee webui_tap.txt + ``` + +4. **Auto selector.** Arm Auto only **after** the daemons are up. With no + daemon running, Auto pins FHSS within 60 s (the stale-link rule). At the + end of the session, set a concrete profile (e.g. 2) before you stop. A + retained `auto` otherwise re-commands the tractor at the next start. Then + run `clear_retained.py`. + +5. **Stop it** at the end of the session: + `ssh $K $B "sudo docker rm -f bench_webui"`. Port 8090 then closes. + ## Traps that cost time this campaign * Bash-tool heredocs mangle backslashes — write patch scripts to files. +* **Misspelled harness parameters were ignored without a warning.** + `run_live_radio_monitor.ps1` used to put an unknown name into `$args`, so + `-DurationSeconds 300` ran the 30 s default of `-DurationS`. The harness + now refuses unknown names at launch. Even so, read the archive's + `params.txt` for the values that actually ran (`duration_s`, + `force_frf_hz`, `reg_profile`, ...) before you quote a leg. * `git add ` sweeps the untracked bench binary (`build/firmware_bench_diag.bin`) into the commit; add firmware by path. * The committed `build/firmware.bin` is the PRODUCTION image (no diag flag); diff --git a/README.md b/README.md index e85b1b2a..53583dc2 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,7 @@ Each version folder contains: - **bill_of_materials.md** - Bill of materials information (where available) The documentation has been extracted and compiled from the [Open Source Ecology wiki](https://wiki.opensourceecology.org) to provide a comprehensive reference for each LifeTrac version. + +## LifeTrac v25 radio bench + +To reproduce the v25 LoRa radio bench (two Portenta X8 boards on Max Carriers, as in the RS-12/RS-13 test campaigns), start at the [radio bench quickstart](LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_QUICKSTART.md). From 0b211af4c46f7e7e427c78fece36a42a2e59390b Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:24:22 -0500 Subject: [PATCH 07/32] flash pipeline: REVIVE_MODE defaults to reboot; staging preflight; flash_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 --- .../bench_tools/flash_l072.sh | 159 ++++++++++++++++++ .../bench_tools/run_flash_bench.sh | 78 ++++++++- .../bench_tools/wdog_regs.py | 42 +++++ .../full_flash_pipeline.sh | 14 +- .../revive_bridge.sh | 10 +- 5 files changed, 291 insertions(+), 12 deletions(-) create mode 100755 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/flash_l072.sh create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/wdog_regs.py diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/flash_l072.sh b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/flash_l072.sh new file mode 100755 index 00000000..3ae2b2be --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/flash_l072.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# flash_l072.sh -- FLASH_RUNBOOK.md sections 1-2 as one PC-side script (Git +# Bash on the bench PC, or any Linux shell with adb/ssh). +# +# bash flash_l072.sh [--go] [--verify-only] [--revive reboot|full] +# +# Without --go: copies the flash pipeline LF-clean to /tmp/lifetrac_p0c and the +# wrapper to /home/fio on the board, then runs the wrapper's preflight +# (FLASH_PREFLIGHT_ONLY=1: file presence, CRLF, image md5). Nothing touches +# openocd, the radio UART or the watchdog. +# With --go: also stops the tractor's camera unit/container and runs the flash +# (run_flash_bench.sh -> full_flash_pipeline.sh). A FLASH IS A RADIO-ON EVENT: +# the L072 boots into RXCONT afterwards, and REVIVE_MODE=reboot reboots the X8 +# (12-24 s; /tmp is wiped). Use --go only on the operator's explicit GO. +# --verify-only: read back and compare against , write nothing +# (still enters the ROM bootloader and reboots; needs --go too). +# +# Configuration: sources $BENCH_ENV (default: bench.env next to this script; +# template bench.env.example) when it exists. Each variable can also come from +# the environment; the defaults are the 2026 bench: +# BASE_SERIAL 2D0A1209DABC240B base adb serial (the base is flashed over ssh) +# TRACTOR_SERIAL 2E2C1209DABC240B tractor adb serial (adb only, WiFi off) +# BASE_HOST 192.168.1.117 base ethernet address (DHCP lease) +# BASE_USER fio +# BASE_SSH_KEY ~/.ssh/lifetrac_base_ed25519 +# BENCH_SUDO_PW fio LmP default; both boards have NOPASSWD sudo +# BOARD_HOME /home/fio persistent home for the wrapper and logs +# REVIVE_MODE reboot or full (5.10 kernels only; see FLASH_RUNBOOK) +set -euo pipefail + +HERE=$(cd "$(dirname "$0")" && pwd) +HELPER=$(cd "$HERE/.." && pwd) +BENCH_ENV=${BENCH_ENV:-$HERE/bench.env} +if [ -f "$BENCH_ENV" ]; then + # shellcheck disable=SC1090 + . "$BENCH_ENV" +fi +BASE_SERIAL=${BASE_SERIAL:-2D0A1209DABC240B} +TRACTOR_SERIAL=${TRACTOR_SERIAL:-2E2C1209DABC240B} +BASE_HOST=${BASE_HOST:-192.168.1.117} +BASE_USER=${BASE_USER:-fio} +BASE_SSH_KEY=${BASE_SSH_KEY:-$HOME/.ssh/lifetrac_base_ed25519} +BASE_SSH_KEY=${BASE_SSH_KEY/#\~/$HOME} +BENCH_SUDO_PW=${BENCH_SUDO_PW:-fio} +BOARD_HOME=${BOARD_HOME:-/home/fio} +REVIVE_MODE=${REVIVE_MODE:-reboot} + +die() { echo "flash_l072: $*" >&2; exit 2; } +usage() { awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"; exit 2; } + +[ $# -ge 2 ] || usage +BOARD=$1; IMG=$2; shift 2 +GO=0; VERIFY_ONLY=0 +while [ $# -gt 0 ]; do + case "$1" in + --go) GO=1 ;; + --verify-only) VERIFY_ONLY=1 ;; + --revive) [ $# -ge 2 ] || die "--revive needs reboot|full"; REVIVE_MODE=$2; shift ;; + -h|--help) usage ;; + *) die "unknown option: $1" ;; + esac + shift +done +case "$BOARD" in base|tractor) ;; *) die "board must be base or tractor, got '$BOARD'" ;; esac +case "$REVIVE_MODE" in reboot|full) ;; *) die "REVIVE_MODE must be reboot or full, got '$REVIVE_MODE'" ;; esac +[ -s "$IMG" ] || die "image not found or empty: $IMG" + +# --- image sanity ----------------------------------------------------------- +IMGNAME=$(basename "$IMG") +SIZE=$(wc -c < "$IMG" | tr -d ' ') +MD5=$(md5sum "$IMG" | cut -d' ' -f1) +# 192 KB flash, of which the last 8 KB is the CFG region (include/memory_map.h). +[ "$SIZE" -le $((184 * 1024)) ] || die "$IMGNAME is $SIZE B, larger than the 184 KB APP region" +case "$MD5" in + 0c1bb0a9573f813137f941dfa47177d0) KIND="bench build on both boards since 2026-09-15 (RS-12.15 v2)" ;; + 589c120323c2d5e7ef9f459d7a4ba42d) KIND="PRODUCTION build (main bb4a2071): refuses diag register writes, so -ForceFrfHz and channel_survey_sniff.py fail on it -- bench boards normally run the bench build" ;; + e8ad842489d5acfc09f204c7807e4661) KIND="RS-12.10 bench build (pre-clock-authority A/B control)" ;; + 5a160e4a8c9296c7d2e49727bdfb8880) KIND="RS-12.15 v2 bench build as flown in legs R and T" ;; + 8c112e6f8b42109aab38a69f2b0aa147) KIND="first RS-12.10 build -- STATS-tail serializer bug, do not fly" ;; + *) KIND="not a recorded build (see bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md)" ;; +esac +echo "image: $IMGNAME $SIZE B md5 $MD5" +echo " $KIND" + +# --- LF-clean staging ------------------------------------------------------- +PIPE="full_flash_pipeline.sh run_flash_l072.sh prep_bridge.sh revive_bridge.sh wdt_pet.sh + stm32_an3155_flasher.py 07_assert_pa11_pf4_long.cfg 08_boot_user_app.cfg 99_release_and_reset.cfg" +WRAP="run_flash_bench.sh stamp.py kmsg_log.py" +STAGE=$(mktemp -d) +trap '[ -n "${STAGE:-}" ] && [ -d "$STAGE" ] && rm -rf -- "$STAGE"' EXIT +mkdir -p "$STAGE/p0c" "$STAGE/home" +for f in $PIPE; do tr -d '\r' < "$HELPER/$f" > "$STAGE/p0c/$f"; done +for f in $WRAP; do tr -d '\r' < "$HERE/$f" > "$STAGE/home/$f"; done +cp "$IMG" "$STAGE/p0c/$IMGNAME" # binary: never line-ending-converted + +PW=$BENCH_SUDO_PW +S="echo '$PW' | sudo -S -p ''" +ENVS="REVIVE_MODE=$REVIVE_MODE FLASH_VERIFY_ONLY=$VERIFY_ONLY BENCH_HOME=$BOARD_HOME" +[ "$PW" = "fio" ] || ENVS="$ENVS BENCH_SUDO_PW='$PW'" +RUN="bash $BOARD_HOME/run_flash_bench.sh /tmp/lifetrac_p0c/$IMGNAME" + +winpath() { if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else printf '%s\n' "$1"; fi; } + +if [ "$BOARD" = "base" ]; then + command -v ssh >/dev/null && command -v scp >/dev/null || die "ssh/scp not found" + SSHO=(-i "$BASE_SSH_KEY" -o BatchMode=yes -o ConnectTimeout=10) + TGT="$BASE_USER@$BASE_HOST" + echo "=== base $TGT (adb serial $BASE_SERIAL): staging" + ssh "${SSHO[@]}" "$TGT" "$S mkdir -p /tmp/lifetrac_p0c && $S chmod 0777 /tmp/lifetrac_p0c" + scp -q "${SSHO[@]}" "$STAGE"/p0c/* "$TGT:/tmp/lifetrac_p0c/" + scp -q "${SSHO[@]}" "$STAGE"/home/* "$TGT:$BOARD_HOME/" + echo "=== base: preflight" + ssh "${SSHO[@]}" "$TGT" "FLASH_PREFLIGHT_ONLY=1 $ENVS $RUN" + if [ "$GO" = "1" ]; then + echo "=== base: FLASHING (radio-on event; REVIVE_MODE=$REVIVE_MODE)" + set +e + ssh "${SSHO[@]}" "$TGT" "$ENVS $RUN" + RC=$? + set -e + fi +else + command -v adb >/dev/null || die "adb not found" + export MSYS_NO_PATHCONV=1 + ADB=(adb -s "$TRACTOR_SERIAL") + echo "=== tractor $TRACTOR_SERIAL: staging" + "${ADB[@]}" shell "$S mkdir -p /tmp/lifetrac_p0c; $S chmod 0777 /tmp/lifetrac_p0c" >/dev/null + for f in "$STAGE"/p0c/*; do "${ADB[@]}" push "$(winpath "$f")" /tmp/lifetrac_p0c/ >/dev/null; done + for f in "$STAGE"/home/*; do "${ADB[@]}" push "$(winpath "$f")" "$BOARD_HOME/" >/dev/null; done + echo "=== tractor: preflight" + "${ADB[@]}" shell "$S env FLASH_PREFLIGHT_ONLY=1 $ENVS $RUN" | tr -d '\r' + if [ "$GO" = "1" ]; then + echo "=== tractor: stopping lifetrac-camera.service + tractor-camera (they hold /dev/ttymxc3)" + "${ADB[@]}" shell "$S systemctl stop lifetrac-camera.service; $S docker stop -t 3 tractor-camera" >/dev/null 2>&1 || true + echo "=== tractor: FLASHING (radio-on event; REVIVE_MODE=$REVIVE_MODE)" + set +e + "${ADB[@]}" shell "$S env $ENVS $RUN" | tr -d '\r' + RC=${PIPESTATUS[0]} + set -e + fi +fi + +if [ "$GO" != "1" ]; then + echo + echo "Staged and preflighted only. To flash (operator GO required):" + echo " bash $0 $BOARD $IMG --go$([ "$VERIFY_ONLY" = 1 ] && echo ' --verify-only')" + exit 0 +fi + +echo +echo "flash exit status: ${RC:-?} (a dropped connection, e.g. 255 over ssh, is expected when REVIVE_MODE=reboot reboots the board)" +cat < PARK_OK 0x80) + unless a leg follows under the same GO. +EOF +exit "${RC:-0}" diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/run_flash_bench.sh b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/run_flash_bench.sh index 56f81b47..f0924648 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/run_flash_bench.sh +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/run_flash_bench.sh @@ -2,17 +2,77 @@ # run_flash_bench.sh — instrumented wrapper around full_flash_pipeline.sh: # kernel log tail + monotonic-stamped pipeline log + fsync'd pet log, # all under /home/fio so they survive a PMIC power-cycle. -IMG=${1:?image} -PW=fio +# +# Usage (on the board; see FLASH_RUNBOOK.md, or flash_l072.sh from the PC): +# bash /home/fio/run_flash_bench.sh /tmp/lifetrac_p0c/.bin +# Environment (all optional; defaults are the 2026 bench values): +# REVIVE_MODE reboot (default since 2026-10-10) | full +# reboot = openocd `reset run`, hand the watchdog back, +# `systemctl reboot`. full = re-insert the x8h7 modules, +# which OOPSes the base's 6.1.24 kernel; only for 5.10. +# FLASH_VERIFY_ONLY 1 = read back and compare, write nothing (first contact; +# still enters the ROM bootloader and reboots afterwards) +# FLASH_PREFLIGHT_ONLY 1 = check the staging below and exit; touches nothing +# BENCH_SUDO_PW sudo password (default: the LmP default `fio`; the bench +# boards have a NOPASSWD sudoers drop-in anyway) +# BENCH_HOME where the wrapper, stamp.py, kmsg_log.py and the logs +# live (default /home/fio, persistent across reboots) +IMG=${1:?usage: run_flash_bench.sh /tmp/lifetrac_p0c/.bin} +PW=${BENCH_SUDO_PW:-fio} +H=${BENCH_HOME:-/home/fio} +TOOLDIR=/tmp/lifetrac_p0c +REVIVE_MODE=${REVIVE_MODE:-reboot} +FLASH_VERIFY_ONLY=${FLASH_VERIFY_ONLY:-0} export PATH=/usr/sbin:/sbin:/usr/local/sbin:/usr/bin:/bin:$PATH -rm -f /home/fio/pipeline_stamped.log /home/fio/kmsg_flash.log /home/fio/wdt_pet.log -echo "$PW" | sudo -S -p '' bash -c 'nohup python3 -u /home/fio/kmsg_log.py /home/fio/kmsg_flash.log >/dev/null 2>&1 & echo $! > /home/fio/kmsg_log.pid' + +case "$REVIVE_MODE" in + reboot|full) ;; + *) echo "PREFLIGHT-FAIL: REVIVE_MODE=$REVIVE_MODE (want reboot or full)"; exit 3 ;; +esac + +# Preflight (2026-10-10): refuse before ANY hardware is touched when the +# staging is incomplete or CRLF-damaged. /tmp is tmpfs and systemd-tmpfiles +# ages it at 5 d, so the pipeline files vanish with the board up; a CRLF +# script fails in confusing ways (BENCH_RUNBOOK "Board facts"). +# Only the files this pipeline executes (08_boot_user_app.cfg belongs to the +# harness's tractor reset and is checked there). +need="$TOOLDIR/full_flash_pipeline.sh $TOOLDIR/run_flash_l072.sh $TOOLDIR/prep_bridge.sh + $TOOLDIR/revive_bridge.sh $TOOLDIR/wdt_pet.sh $TOOLDIR/stm32_an3155_flasher.py + $TOOLDIR/07_assert_pa11_pf4_long.cfg $H/stamp.py $H/kmsg_log.py" +missing="" +for f in $need "$IMG"; do + [ -s "$f" ] || missing="$missing $f" +done +if [ -n "$missing" ]; then + echo "PREFLIGHT-FAIL: missing or empty:$missing" + echo "Re-stage /tmp/lifetrac_p0c and $H (FLASH_RUNBOOK section 1). Nothing was touched." + exit 3 +fi +# CRLF breaks bash and the openocd cfg; python tolerates it, so only .sh/.cfg. +# (tr|cmp rather than grep $'\r': same answer on busybox, GNU and Git Bash.) +crlf="" +for f in $need; do + case "$f" in *.sh|*.cfg) tr -d '\r' < "$f" | cmp -s - "$f" || crlf="$crlf $f" ;; esac +done +if [ -n "$crlf" ]; then + echo "PREFLIGHT-FAIL: CRLF line endings in:" $crlf + echo "Re-push LF-clean copies (FLASH_RUNBOOK section 1). Nothing was touched." + exit 3 +fi +echo "PREFLIGHT-OK image=$IMG size=$(stat -c%s "$IMG") md5=$(md5sum "$IMG" | cut -d' ' -f1) REVIVE_MODE=$REVIVE_MODE FLASH_VERIFY_ONLY=$FLASH_VERIFY_ONLY" +if [ "${FLASH_PREFLIGHT_ONLY:-0}" = "1" ]; then + echo "FLASH_PREFLIGHT_ONLY=1: staging checked, stopping before openocd/UART/watchdog." + exit 0 +fi + +rm -f "$H/pipeline_stamped.log" "$H/kmsg_flash.log" "$H/wdt_pet.log" +echo "$PW" | sudo -S -p '' bash -c "nohup python3 -u $H/kmsg_log.py $H/kmsg_flash.log >/dev/null 2>&1 & echo \$! > $H/kmsg_log.pid" sleep 0.5 -echo "kmsg logger pid=$(cat /home/fio/kmsg_log.pid) uptime_s=$(cut -d' ' -f1 /proc/uptime)" -echo "$PW" | sudo -S -p '' env PATH=$PATH REVIVE_MODE=${REVIVE_MODE:-full} bash /tmp/lifetrac_p0c/full_flash_pipeline.sh "$IMG" 2>&1 | python3 -u /home/fio/stamp.py /home/fio/pipeline_stamped.log +echo "kmsg logger pid=$(cat "$H/kmsg_log.pid") uptime_s=$(cut -d' ' -f1 /proc/uptime)" +echo "$PW" | sudo -S -p '' env PATH=$PATH REVIVE_MODE=$REVIVE_MODE FLASH_VERIFY_ONLY=$FLASH_VERIFY_ONLY WDT_PET_LOG=$H/wdt_pet.log bash $TOOLDIR/full_flash_pipeline.sh "$IMG" 2>&1 | python3 -u "$H/stamp.py" "$H/pipeline_stamped.log" RC=${PIPESTATUS[1]} echo "PIPELINE-EXIT=$RC uptime_s=$(cut -d' ' -f1 /proc/uptime)" -echo "$PW" | sudo -S -p '' kill "$(cat /home/fio/kmsg_log.pid)" 2>/dev/null -echo "--- wdt_pet.log tail:"; tail -3 /home/fio/wdt_pet.log -echo "--- kmsg tail:"; tail -5 /home/fio/kmsg_flash.log +echo "$PW" | sudo -S -p '' kill "$(cat "$H/kmsg_log.pid")" 2>/dev/null +echo "--- wdt_pet.log tail:"; tail -3 "$H/wdt_pet.log" +echo "--- kmsg tail:"; tail -5 "$H/kmsg_flash.log" exit $RC diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/wdog_regs.py b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/wdog_regs.py new file mode 100644 index 00000000..15116be0 --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/wdog_regs.py @@ -0,0 +1,42 @@ +# wdog_regs.py -- READ-ONLY dump of the i.MX8M Mini WDOG1 registers. +# +# What it reads: WDOG1 at physical 0x30280000 through /dev/mem, opened +# O_RDONLY and mapped PROT_READ -- it writes nothing and cannot pet, stop or +# arm the watchdog. One line out: +# WCR (0x00) WDE bit2 = enabled, WT bits15:8 -> timeout (WT+1)/2 s, +# WDT bit3 = WDOG_B asserted on timeout (PMIC reset on the X8), +# WDZST bit0, WDBG bit1 +# WSR (0x02) service register +# WRSR (0x04) reset reason: SFTW bit0, TOUT bit1, POR bit4 +# WICR (0x06), WMCR (0x08) +# Expected on both bench boards (u-boot arms it, FLASH_RUNBOOK section 3): +# WCR=0x773d -> WDE=1 WT=119 (60.0 s), WDT=1. WRSR reads POR even after a +# watchdog reset, because WDOG_B power-cycles the board through the PMIC -- +# so WRSR cannot tell a watchdog reboot from a power-on. +# +# Where it runs: ON THE BOARD, inside a docker image, because the LmP host +# python has no `mmap` module and the image has no `devmem`/sysfs watchdog +# attributes. Needs root and the /dev/mem device: +# base (ssh): echo fio | sudo -S -p '' docker run --rm --privileged \ +# -v /dev/mem:/dev/mem -v /tmp/lifetrac_strict:/work \ +# --entrypoint python3 lifetrac-v25:latest /work/wdog_regs.py +# tractor (adb): same, with image +# hub.foundries.io/arduino/arduino-ootb-python-devel:738bc44 +# (push this file to /tmp/lifetrac_strict first; /tmp is tmpfs). +# +# Provenance: until 2026-10-10 this script existed only in /home/fio on the +# two boards (identical copies). Recovered from the read-only captures +# bench-evidence/board_state_2026-10-04/base/files/home_fio/wdog_regs.py and +# board_state_2026-10-10/tractor/; the code below is unchanged. +import mmap, os, struct, sys +BASE = 0x30280000 +fd = os.open("/dev/mem", os.O_RDONLY | os.O_SYNC) +m = mmap.mmap(fd, 0x1000, mmap.MAP_SHARED, mmap.PROT_READ, offset=BASE) +def r16(off): + return struct.unpack("> 8) & 0xFF +print("WDOG1 WCR=0x%04x WDE=%d WT=%d (%.1f s) WDT=%d WDZST=%d WDBG=%d | WSR=0x%04x | WRSR=0x%04x SFTW=%d TOUT=%d POR=%d | WICR=0x%04x WMCR=0x%04x" % ( + wcr, (wcr >> 2) & 1, wt, (wt + 1) / 2.0, (wcr >> 3) & 1, wcr & 1, (wcr >> 1) & 1, + wsr, wrsr, wrsr & 1, (wrsr >> 1) & 1, (wrsr >> 4) & 1, wicr, wmcr)) +m.close(); os.close(fd) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/full_flash_pipeline.sh b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/full_flash_pipeline.sh index f8568e2c..8702434a 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/full_flash_pipeline.sh +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/full_flash_pipeline.sh @@ -5,6 +5,16 @@ # # Default image is hello.bin in /tmp/lifetrac_p0c/. # Logs to /tmp/lifetrac_p0c/pipeline.log +# +# REVIVE_MODE (2026-10-10: default is now `reboot`; was `full`): +# reboot openocd `reset run`, hand the watchdog back ('V'), systemctl reboot. +# Required on the 6.1.24 base kernel; works on 5.10 too. Every flash +# since 2026-09-12 used it. +# full re-insert the x8h7 modules instead of rebooting. OOPSes the base's +# 6.1 kernel (spi_probe -> of_irq_get) and power-cycles it; kept only +# for a 5.10 board where a reboot is unwanted. +# Note: `sudo` drops the caller's environment, so pass it explicitly: +# sudo -S -p '' env REVIVE_MODE=full bash .../full_flash_pipeline.sh img.bin set -u TOOLDIR=/tmp/lifetrac_p0c @@ -38,7 +48,9 @@ echo "=== T2 $(date) revive_bridge ===" | tee -a "$LOG" # the base kernel (6.1.24-lmp) and power-cycles the board anyway. Bring the # H7 back with openocd reset run, hand the watchdog back, then reboot on # purpose. /tmp is lost either way; the caller re-pushes tooling. -if [ "${REVIVE_MODE:-full}" = "reboot" ]; then +REVIVE_MODE=${REVIVE_MODE:-reboot} +echo "REVIVE_MODE=$REVIVE_MODE" | tee -a "$LOG" +if [ "$REVIVE_MODE" = "reboot" ]; then REVIVE_MODE=reset_run_only bash $TOOLDIR/revive_bridge.sh 2>&1 | tee -a "$LOG" REVIVE_RC=${PIPESTATUS[0]} echo "revive_rc=$REVIVE_RC (reset_run_only)" | tee -a "$LOG" diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/revive_bridge.sh b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/revive_bridge.sh index 4a67ab16..988603b9 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/revive_bridge.sh +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/revive_bridge.sh @@ -52,10 +52,16 @@ sleep 1 # 2026-09-12/flash/). REVIVE_MODE=reset_run_only stops here, after the H7 # is back in its firmware, and lets the caller do a controlled reboot # instead of the module reload. -if [ "${REVIVE_MODE:-full}" = "reset_run_only" ]; then - echo "=== REVIVE_MODE=reset_run_only: H7 running, modules left unloaded; caller reboots ===" +# 2026-10-10: `reboot` (the flash pipeline's own name for this path, exported +# by run_flash_bench.sh) is accepted as an alias, so a standalone call with +# that environment cannot fall through to the module reload below. +if [ "${REVIVE_MODE:-full}" = "reset_run_only" ] || [ "${REVIVE_MODE:-full}" = "reboot" ]; then + echo "=== REVIVE_MODE=${REVIVE_MODE}: H7 running, modules left unloaded; caller reboots ===" exit 0 fi +case "$(uname -r)" in + 6.*) echo "=== WARNING: kernel $(uname -r): the x8h7 module reload below OOPSed the 6.1.24 base kernel twice (2026-09-12); expect a PMIC power-cycle. Prefer REVIVE_MODE=reset_run_only + reboot. ===" ;; +esac echo "=== [3/5] unexport x8h7 gpios ($X8H7_GPIO_BASE..$X8H7_GPIO_LAST) ===" for g in $(seq $X8H7_GPIO_BASE $X8H7_GPIO_LAST); do if [ -d /sys/class/gpio/gpio$g ]; then From 63e8692a9bef0bdf4d9840d98b9d9ba16dc695a2 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:26:55 -0500 Subject: [PATCH 08/32] docs: fix board, script-name and hardware errors found by the bench audit - 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 97accdcc (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 --- .../05_firmware_installation.md | 21 +++++++++++++++++++ .../DESIGN-CONTROLLER/FIRMWARE_UPDATES.md | 13 ++++++++---- .../recovery/ADB_TIPS_AND_TRICKS.md | 3 ++- .../routines/HC-02_linux_bridge_full.md | 10 +++++---- .../firmware/common/setup_x8_mqtt_link.ps1 | 5 +++-- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/LifeTrac-v25/BUILD-CONTROLLER/05_firmware_installation.md b/LifeTrac-v25/BUILD-CONTROLLER/05_firmware_installation.md index 6797bf32..7d8b2ddf 100644 --- a/LifeTrac-v25/BUILD-CONTROLLER/05_firmware_installation.md +++ b/LifeTrac-v25/BUILD-CONTROLLER/05_firmware_installation.md @@ -61,6 +61,27 @@ This prints something like `9f3a8c4e2b1d7506a8e3f4c5b9d2e1f0`. Copy it into a fi This is the radio + arbitration + Modbus master firmware that runs on the X8's onboard STM32H747. +> [!WARNING] +> **On a Portenta X8 this step overwrites the stock x8h7 firmware.** The +> H747's M7 core (flash bank 1) normally runs Arduino's stock **x8h7** +> firmware: the bridge through which X8 Linux reaches the H7-side peripherals +> (the `x8h7_*` kernel modules for GPIO, ADC, CAN, UART, PWM and RTC, and +> `m4-proxy` for M4 sketches). Arduino's own X8 workflow only uploads user +> sketches to the M4 core for this reason. Uploading `tractor_h7` as +> *Portenta H7 (M7 core)* replaces the bridge, and everything Linux does +> through it stops working until the stock image is back. +> `stm32h7-program.service` can restore it from +> `/usr/arduino/extra/STM32H747AII6_CM7.bin`, but whether it does so on its +> own depends on the LmP image. +> +> LifeTrac did this to both bench X8s on 2026-05-04; both have run stock +> x8h7 again since 2026-05-13. The current radio bench does **not** use +> `tractor_h7` at all: the LoRa radio is the Max Carrier's Murata L072, +> driven from X8 Linux over `/dev/ttymxc3`. See +> [`BENCH_BOARDS.md`](../DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_BOARDS.md) +> before flashing anything on an X8, and skip this step unless you mean to +> replace the bridge and know how to put the stock image back. + 1. Open `firmware/tractor_h7/tractor_h7.ino` in Arduino IDE 2. 2. Select **Tools → Board → Arduino Mbed OS Portenta Boards → Portenta H7 (M7 core)**. (The X8's onboard H747 enumerates the same way as a standalone H7 for the toolchain — same M7 binary.) 3. Select **Tools → Port** → the X8's USB-C serial port. diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/FIRMWARE_UPDATES.md b/LifeTrac-v25/DESIGN-CONTROLLER/FIRMWARE_UPDATES.md index eba23d54..b7579b01 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/FIRMWARE_UPDATES.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/FIRMWARE_UPDATES.md @@ -50,13 +50,18 @@ The release-key public half is baked into each node's boot loader at manufacture 3. Watch `journalctl -fu lifetrac-base` for ≥1 minute to confirm the new bridge handshakes with the tractor. - **Rollback:** `sudo /usr/local/bin/lifetrac-update vX.Y.` — same wrapper, previous tag. -### Handheld — Portenta H747 (M7 firmware) + secure element +### Handheld — Arduino MKR WAN 1310 (SAMD21 firmware, `firmware/handheld_mkr`) -- **Artifact:** `lifetrac-handheld-h747-vX.Y.Z.bin` for application code; secure-element provisioning is **separate** and only happens at manufacture or pair. +> The handheld is an MKR WAN 1310 (SAMD21 MCU + Murata LoRa module), not a +> Portenta H747 — see [HANDHELD_REMOTE.md](HANDHELD_REMOTE.md) and +> [firmware/handheld_mkr/](firmware/handheld_mkr/) (FQBN +> `arduino:samd:mkrwan1310`, [ARDUINO_CI.md](ARDUINO_CI.md)). + +- **Artifact:** `lifetrac-handheld-mkr-vX.Y.Z.bin` for application code; key provisioning (secure element or key store) is **separate** and only happens at manufacture or pair. - **Channel:** USB-C from the base-station laptop. **Never OTA** — handheld is the operator's safety-critical surface. - **Procedure:** - 1. Connect handheld via USB-C while it is **powered off**. The bootloader enumerates as `Arduino_Bootloader` on USB. - 2. `python tools/flash_handheld.py --image lifetrac-handheld-h747-vX.Y.Z.bin --port COM7`. + 1. Connect the handheld via USB-C and double-tap the MKR's RESET button: the SAMD21 bootloader starts (LED pulses) and enumerates as its own COM port. + 2. `python tools/flash_handheld.py --image lifetrac-handheld-mkr-vX.Y.Z.bin --port COM7` (planned tool; until it exists, compile the sketch as in [ARDUINO_CI.md](ARDUINO_CI.md) and flash it with `arduino-cli upload --fqbn arduino:samd:mkrwan1310 -p `). 3. Tool verifies signature, flashes, reboots. Power up the handheld and confirm the OLED displays the new firmware version on the splash screen. 4. **Re-run pairing** — the secure-element key is preserved across firmware updates by design, but verify pairing still works (cf. [OPERATIONS_MANUAL.md § 2](OPERATIONS_MANUAL.md)). - **Rollback:** keep the previous `.bin` on the maintenance laptop; reflash with the previous version via the same procedure. diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/recovery/ADB_TIPS_AND_TRICKS.md b/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/recovery/ADB_TIPS_AND_TRICKS.md index a13180b9..879eaf37 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/recovery/ADB_TIPS_AND_TRICKS.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/recovery/ADB_TIPS_AND_TRICKS.md @@ -137,7 +137,8 @@ Each of these has been documented as actively destabilizing the bench. & $adb -s reverse --list ``` -- Observed (2026-05-24, board 2E2C): `reverse tcp:1883 tcp:1883` returns +- Observed (2026-05-24, board 2D0A -- the base, the board that runs 934-91; + 2E2C runs the factory 674-88 image): `reverse tcp:1883 tcp:1883` returns exit 0 but the tunnel is never installed. From the X8, `exec 9<>/dev/tcp/127.0.0.1/1883` returns "Connection refused". - `reverse --list` returns `error: protocol fault (couldn't read status diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/routines/HC-02_linux_bridge_full.md b/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/routines/HC-02_linux_bridge_full.md index 2c65a4ab..6beb0d91 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/routines/HC-02_linux_bridge_full.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/X8_HEALTH_AND_RECOVERY/routines/HC-02_linux_bridge_full.md @@ -10,18 +10,20 @@ SWD work. ## Procedure ```powershell -$script = "LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/board1_healthcheck.sh" +$script = "LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/x8_max_carrier_healthcheck.sh" foreach ($s in '2D0A1209DABC240B','2E2C1209DABC240B') { $tag = if ($s -like '2D0A*') {'board1'} else {'board2'} $log = "LifeTrac-v25/AI NOTES/$(Get-Date -Format yyyy-MM-dd)_${tag}_healthcheck_v1_0.log" - adb -s $s push $script /tmp/board1_healthcheck.sh 2>&1 | Out-Null - adb -s $s exec-out "sh /tmp/board1_healthcheck.sh" 2>&1 | + adb -s $s push $script /tmp/x8_max_carrier_healthcheck.sh 2>&1 | Out-Null + adb -s $s exec-out "sh /tmp/x8_max_carrier_healthcheck.sh" 2>&1 | Out-File -FilePath $log -Encoding utf8 Get-Content $log } ``` -Helper: [board1_healthcheck.sh](../../firmware/x8_lora_bootloader_helper/board1_healthcheck.sh) +Helper: [x8_max_carrier_healthcheck.sh](../../firmware/x8_lora_bootloader_helper/x8_max_carrier_healthcheck.sh) +(renamed from `board1_healthcheck.sh` on 2026-05-15; older notes and the +2026-05-13 logs still use the old name). ## Pass criteria diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/common/setup_x8_mqtt_link.ps1 b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/common/setup_x8_mqtt_link.ps1 index 2b62d572..e79c91d7 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/common/setup_x8_mqtt_link.ps1 +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/common/setup_x8_mqtt_link.ps1 @@ -5,8 +5,9 @@ .DESCRIPTION The historical recipe used 'adb -s reverse tcp:1883 tcp:1883' to - expose the host broker as 127.0.0.1:1883 on the device. On LmP factory - image 934-91 (verified 2026-05-24 on board 2E2C1209DABC240B) that command + expose the host broker as 127.0.0.1:1883 on the device. On LmP image + 4.0.11-934-91 (verified 2026-05-24 on board 2D0A1209DABC240B, the base; + the tractor 2E2C runs the as-shipped 4.0.3-674-88 image) that command returns exit=0 but does NOT actually install the tunnel: adb reverse tcp:1883 tcp:1883 -> exit=0 (silent failure) From 0cfbe4ff8fb953e8ce8d8be1c4b0b1a307d58a60 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:27:08 -0500 Subject: [PATCH 09/32] docs(x8_lora_bootloader_helper): README flags the misnamed L072 binaries 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 --- .../x8_lora_bootloader_helper/README.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/README.md diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/README.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/README.md new file mode 100644 index 00000000..4907c214 --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/README.md @@ -0,0 +1,52 @@ +# x8_lora_bootloader_helper + +Tools that run on a Portenta X8 to reflash and talk to the Murata L072 +(the LoRa radio module on the Max Carrier, on `/dev/ttymxc3`), plus the +PC-side harness for radio bench legs. + +Where to start: + +- Flashing the L072: [FLASH_RUNBOOK.md](FLASH_RUNBOOK.md). +- Running the radio bench from scratch: + [bench_tools/BENCH_QUICKSTART.md](bench_tools/BENCH_QUICKSTART.md). +- What is on each bench board today: + [bench_tools/BENCH_BOARDS.md](bench_tools/BENCH_BOARDS.md). + +## Binaries in this folder: read before you flash + +The file names here are misleading. None of these files is the firmware +the bench boards run. + +| file | size | md5 | what it really is | +|---|---|---|---| +| `mlm32l07x01.bin` | 4,808 B | `1e3d4328dd74e371e4d583e6fe84f856` | **Misnamed.** A LifeTrac test build ("LIFETRAC L072 hello v0.2 (USART1+LPUART1)"): it prints a banner and a tick counter. It is byte-identical to `../murata_l072/build_hello/hello_world.bin`, built from [`../murata_l072/hello_world.c`](../murata_l072/hello_world.c). It is **not** the Murata/Arduino stock firmware, even though it has the stock file name. | +| `hello.bin` | 677 B | `972340b8486fb8589ff99fed3dbf4d29` | An earlier, smaller hello build with the same banner. It is byte-identical to `../murata_l072_hello/hello.bin`, built from [`../murata_l072_hello/main.c`](../murata_l072_hello/main.c). | +| `board2_l072_flash_dump.bin` | 16,440 B | `16e12477f904245f54a694eb7e31fc4c` | A **readback**, not a build. It holds the first 16,440 bytes of L072 flash from `0x08000000`, read in May 2026 (committed 2026-05-13) with [`dump_l072_flash.py`](dump_l072_flash.py) from the board the May notes call "Board 2" (2E2C under that naming). The length is just the size of the `murata_l072` build of that week. It is an early LifeTrac build. It matches no tracked build byte for byte and is not the factory image. Keep it as diagnostic evidence and never flash it. | + +### Why the hello build has the stock name + +The early flash scripts default to an image called `mlm32l07x01.bin`. For +example, [`run_flash_l072.sh`](run_flash_l072.sh) uses +`IMAGE=${1:-/tmp/lifetrac_p0c/mlm32l07x01.bin}`. The hello build was saved +under that name in May 2026, most likely so those defaults would pick it up +(the 2026-05-08 hello-world notes push it as +`/tmp/lifetrac_p0c/mlm32l07x01.bin`). The file is not renamed here because +old notes and scripts push it by that path. Pass an explicit image path +instead of relying on the default. + +### The real stock image and the real bench firmware + +- **Stock (factory) L072 firmware:** + [`LifeTrac-v25/tools/mlm32l07x01.bin`](../../../tools/mlm32l07x01.bin) + (83,032 B, md5 `33b8a001fac12396c6b7211a463a9cab`). This is Arduino's + MKRWAN AT-modem firmware: its strings include the `+VER`/`+DEV` AT + commands, `ARD-078` and version `1.2.3`. It was extracted from the MKRWAN + library's `MKRWANFWUpdate_standalone/fw.h`. Use it to put a module back to + factory state; the LifeTrac bench software does not work with it. +- **LifeTrac bench firmware (both bench boards since 2026-09-15):** build + it with `mingw32-make bench` in [`../murata_l072/`](../murata_l072/). That + gives `build/firmware_bench_diag.bin`, md5 + `0c1bb0a9573f813137f941dfa47177d0`; toolchain and expected hashes are in + [`../murata_l072/README.md`](../murata_l072/README.md). Saved copies of + every flashed bench build are in + [`bench-evidence/RS_13_vector_scene_2026-09-26/firmware/`](../../bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md). From 14a5532bf279bbb3af9983c769b3f6291316cdc7 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:28:27 -0500 Subject: [PATCH 10/32] FLASH_RUNBOOK: first flash from a factory board, toolchain/md5s, new 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 bb4a2071); 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 --- .../FLASH_RUNBOOK.md | 199 +++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/FLASH_RUNBOOK.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/FLASH_RUNBOOK.md index 76d3d3cd..5da7ab3e 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/FLASH_RUNBOOK.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/FLASH_RUNBOOK.md @@ -6,6 +6,16 @@ openocd holds BOOT0/NRST through the H7's GPIOs. Written 2026-09-12 after a session that met every trap below; the scripts live in this directory and are pushed to `/tmp/lifetrac_p0c` on the board. +- **Scripted form (2026-10-10):** [`bench_tools/flash_l072.sh`](bench_tools/flash_l072.sh) + does sections 1–2 from the PC. It reads the serials, base address, ssh key + and sudo password from `bench_tools/bench.env` (template + `bench_tools/bench.env.example`); the defaults are the current bench. + Without `--go` it only stages and preflights; `--go` flashes. +- **A board that has never been flashed** (stock MKRWAN AT firmware on the + L072): read [section 5](#5-first-flash-from-a-factory-board) first. +- **A flash is a radio-on event.** The L072 boots into RXCONT. Flash only on + the operator's explicit GO for that round. + ## 0. Which binary | build | command | binary | use | @@ -17,6 +27,31 @@ Run `mingw32-make check` before flashing anything; `check-stats-layout` pins the STATS wire against `host_types.h` (an additive-tail slip shifted four fields by one slot on 2026-09-12 — the test exists because of it). +### Byte-identical builds and the expected md5s + +The build is reproducible byte for byte, but only with the exact toolchain: +**Arm GNU Toolchain 12.2.MPACBTI-Rel1** (`arm-none-eabi-gcc --version` → +`12.2.1 20230214`). On the bench PC it is installed at +`C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\12.2 mpacbti-rel1\`, +with WinLibs `mingw32-make` run from PowerShell. Expected md5s at `main` +`bb4a2071` (`firmware/murata_l072` unchanged since): + +| build | md5 | size | +|---|---|---| +| bench `build/firmware_bench_diag.bin` (on both boards since 2026-09-15) | `0c1bb0a9573f813137f941dfa47177d0` | 24 860 B | +| production `build/firmware.bin` (committed) | `589c120323c2d5e7ef9f459d7a4ba42d` | 24 860 B | + +Other compilers (Arduino's bundled 7-2017q4, which `build.ps1` picks first; +Ubuntu's `gcc-arm-none-eabi`, which the main CI job uses) build working +firmware with **different bytes**, so a matching md5 also proves the +toolchain. Details and make targets: +[`../murata_l072/README.md`](../murata_l072/README.md#building). The +non-blocking CI job `.github/workflows/l072-bench-md5.yml` rebuilds both +images on Linux with the same Arm release and prints the md5s against these +values. `flash_l072.sh` names every recorded md5 before it stages. The older +builds are listed in +[`bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md`](../../bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md). + ## 1. Push the tooling (LF!) git autocrlf rewrites `*.sh`/`*.cfg` to CRLF in the working tree whenever @@ -38,6 +73,13 @@ The instrumented wrapper `run_flash_bench.sh` plus `stamp.py` and pipeline, pet and kernel logs under `/home/fio` so a reboot cannot eat the evidence. +`flash_l072.sh ` does all of this section (LF-clean +copies of the same nine pipeline files plus the bin, the three wrapper files +to `/home/fio`) and then runs the wrapper's preflight +(`FLASH_PREFLIGHT_ONLY=1`). The wrapper itself now refuses to start — before +touching openocd, the UART or the watchdog — when a pipeline file or the +image is missing or a `.sh`/`.cfg` has CRLF (`PREFLIGHT-FAIL`, exit 3). + ## 2. Flash ```bash @@ -45,8 +87,21 @@ evidence. adb -s 2E2C1209DABC240B shell "echo fio | sudo -S -p '' systemctl stop lifetrac-camera.service; echo fio | sudo -S -p '' docker stop tractor-camera; echo fio | sudo -S -p '' env REVIVE_MODE=reboot bash /home/fio/run_flash_bench.sh /tmp/lifetrac_p0c/firmware_bench_diag.bin" # base (ssh) — root's SSH shell has no sbin on PATH; the wrapper exports it ssh -i ~/.ssh/lifetrac_base_ed25519 fio@192.168.1.117 "REVIVE_MODE=reboot bash /home/fio/run_flash_bench.sh /tmp/lifetrac_p0c/firmware_bench_diag.bin" +# the same from the PC, one board at a time (stages, preflights, then flashes) +bash bench_tools/flash_l072.sh tractor ../murata_l072/build/firmware_bench_diag.bin --go +bash bench_tools/flash_l072.sh base ../murata_l072/build/firmware_bench_diag.bin --go ``` +**`REVIVE_MODE` now defaults to `reboot`** (2026-10-10) in both +`run_flash_bench.sh` and `full_flash_pipeline.sh`; it used to default to +`full`, so every flash had to pass `REVIVE_MODE=reboot` by hand. Passing it +explicitly, as above, still works. `REVIVE_MODE=full` remains selectable for +a 5.10 board where a reboot is unwanted; on the base's 6.1 kernel it oopses +(below). `revive_bridge.sh` treats `reboot` as `reset_run_only`, and it warns +before a module reload on any 6.x kernel. Other wrapper variables: +`FLASH_VERIFY_ONLY=1` (read back and compare, write nothing), +`BENCH_SUDO_PW` and `BENCH_HOME` (defaults `fio`, `/home/fio`). + What happens: `wdt_pet start` (pets `/dev/watchdog0` every 10 s, fsync'd log) → `prep_bridge` (unbind consumers, rmmod `x8h7_*`) → `run_flash_l072` (openocd holds BOOT0 high + NRST, AN3155 write + `Verify OK`, ~45 s) → @@ -59,7 +114,8 @@ Why the reboot is deliberate: the old revive re-inserted the `x8h7` modules, which OOPSes the base's 6.1.24 kernel in `spi_probe → of_irq_get` and drops the board anyway (twice on 2026-09-12, petter alive 2 s before). `REVIVE_MODE=full` keeps the old behaviour for the tractor's 5.10 -kernel if ever wanted; nothing needs it. +kernel if ever wanted; nothing needs it, which is why `reboot` is now the +default. ## 3. Watchdog facts (why the scripts look like this) @@ -71,7 +127,14 @@ kernel if ever wanted; nothing needs it. with no petter → reboot 60 s later (tractor, 5.10.93, 2026-09-12). - No `/sys/class/watchdog/*` attributes, no `devmem`, host python has no `mmap`: read registers with `wdog_regs.py` inside the docker image - (`--privileged -v /dev/mem:/dev/mem`). + (`--privileged -v /dev/mem:/dev/mem`). The script now lives in the repo at + [`bench_tools/wdog_regs.py`](bench_tools/wdog_regs.py). Until 2026-10-10 + it existed only in `/home/fio` on the boards; it was recovered from the + board captures. It is read-only (`O_RDONLY`, `PROT_READ`). Push it to + `/tmp/lifetrac_strict`, then: + `echo fio | sudo -S -p '' docker run --rm --privileged -v /dev/mem:/dev/mem -v /tmp/lifetrac_strict:/work --entrypoint python3 lifetrac-v25:latest /work/wdog_regs.py` + (on the tractor use the `arduino-ootb-python-devel:738bc44` image). + Expected: `WCR=0x773d WDE=1 WT=119 (60.0 s)`. ## 4. After the reboot @@ -94,3 +157,135 @@ bracket silently parses only the old fields. Park at the end of the session: `radio_park.py` → `PARK_OK {"opmode_readback": "0x80"}` on both boards. + +## 5. First flash from a factory board + +This section collects guidance from the 2026-05 bring-up notes that was not +in one place before. Both bench L072s have been flashed many times since +May. Nobody has run this first-flash procedure from scratch since then, so +treat it as consolidated guidance and record what you see. + +### What a factory board has + +- The L072 inside the Murata CMWX1ZZABZ runs Arduino's stock **MKRWAN AT + modem firmware**. The repo keeps 1.2.3 as the factory restore image, + [`../../../tools/mlm32l07x01.bin`](../../../tools/mlm32l07x01.bin) + (83 032 B). The file of the same name in this directory is misnamed: it is + LifeTrac "hello v0.2". +- **Nothing on the carrier needs to change for this path.** The X8 reaches + the L072's ROM bootloader with parts it already has. `/dev/ttymxc3` (i.MX + UART4, wired straight to the L072, not through the H7 bridge) carries + AN3155 at **19200 8E1**. The X8's own openocd + (`/usr/arduino/extra/openocd_script-imx_gpio.cfg`) halts the H747 and + drives the L072's **BOOT0 = H7 PA11** and **NRST = H7 PF4**. + [`07_assert_pa11_pf4_long.cfg`](07_assert_pa11_pf4_long.cfg) holds BOOT0 + high for a 600 s window. [`stm32_an3155_flasher.py`](stm32_an3155_flasher.py) + is stdlib-only, because LmP's python has no `termios`; `stty` sets up the + UART in the shell. Pin map: [Phase 0 crack note](../../../AI%20NOTES/2026-05-07_Portenta_X8_LoRa_Phase0_Crack_and_Phase1_Options_Copilot_v1_0.md). + First end-to-end flash, which wrote the MKRWAN image itself: + [Method G Phase 1 note](../../../AI%20NOTES/2026-05-08_Method_G_Phase1_End_to_End_Flash_Success_Copilot_v1_0.md). +- No J-Link or ST-Link is needed. The carrier's L072 SWD header `CN2` is + **not populated** by default + ([BRINGUP_MAX_CARRIER.md](../murata_l072/BRINGUP_MAX_CARRIER.md) §2); it + only matters for the last-resort recovery below. + +### Before the first flash + +1. Set up the X8 first ([`bench_tools/BENCH_SETUP.md`](bench_tools/BENCH_SETUP.md)): + the sudoers drop-in, docker usable, and on the tractor + `lifetrac-camera.service` / `tractor-camera` stopped. Both grab + `/dev/ttymxc3`, and `run_flash_l072.sh` refuses while anything holds it + (exit 90). +2. **Freshly reflashed X8 (uuu):** `gpio10` (H7 NRST) boots LOW, so openocd + reports `cannot read IDR` (README_P0C TT-1). `run_flash_l072.sh` already + exports gpio8/10/15, drives gpio10 high and unexports them before + openocd attaches. If attach still fails, compare against a good board + ([HC-03](../../X8_HEALTH_AND_RECOVERY/routines/HC-03_h7_swd_attach_diff.md)). + `SWD DPIDR 0xdeadbeef` on a newer openocd is the sysfs/mmap race, **not a + bricked L072** ([T4](../../X8_HEALTH_AND_RECOVERY/recovery/T4_lora_jlink_hardware_unbrick.md) + "When NOT to use"). +3. Build with the exact toolchain and check the md5 (section 0). + +### The first flash, step by step + +1. **Read-only first contact.** Run + `bash bench_tools/flash_l072.sh ../../../tools/mlm32l07x01.bin --go --verify-only`. + It enters the ROM bootloader, reads the flash back and compares it with + the stock image without writing anything, then reboots the X8. + - `READY: L072 in STM32 ROM bootloader` (openocd) followed by the + flasher's `Bootloader version: 31` means ROM entry and the UART path + work. + - `Verify-only OK` means the chip carries stock 1.2.3. + - A mismatch only means a different stock version. The real question is + whether ROM entry works. + - This is also how to capture what was on the chip before LifeTrac + touched it. +2. **Flash the bench build:** + `bash bench_tools/flash_l072.sh ../murata_l072/build/firmware_bench_diag.bin --go`. + - Erase: the flasher first tries Extended Erase mass erase (`0x44`, + `0xFFFF`). On the Murata factory part that NACKs, so it falls back to + **page-by-page erase** of only the pages the image needs. The rest of + the flash and the option bytes are untouched. + - Success means `Verify OK`, `flash_rc=0` and `PIPELINE-EXIT=0` in + `/home/fio/pipeline_stamped.log`. The X8 reboots + (`REVIVE_MODE=reboot`, section 2). +3. After the reboot, follow section 4: re-stage `/tmp`, stop the tractor + camera, run `rs116_health_probe.py`, and park the radio. + +### Option bytes and readout protection: do not touch them + +- **Never send these AN3155 commands from the X8:** Write Unprotect `0x73`, + Readout Protect `0x82`, Readout Unprotect `0x92`, or any Write Memory to + the option bytes at `0x1FF80000`. The pipeline's flasher sends none of + them. + - On 2026-05-14, `flash_l072_via_uart.py wunprot` (through + `recover_l072_opt.sh`) was ACKed, and then the chip went silent. Its + option-byte complement pairs no longer matched, and it would not run its + flash again without SWD + ([timeline](../../../AI%20NOTES/2026-05-14_W2-02_Board2_L072_wunprot_Brick_Timeline_Copilot_v1_0.md), + [root cause](../../../AI%20NOTES/2026-05-14_W2-02_Board2_L072_OPT_Bytes_Root_Cause_Copilot_v1_0.md)). + - That script, `flash_l072_via_uart.py`, `l072_unprotect.py`, + `run_l072_unprotect.sh` and `recover_l072_opt.sh` are still in this + directory **as history. Do not run them.** +- Readout Protect sets RDP level 1. The ROM then refuses reads, so the + pipeline can no longer verify. Going back to level 0 mass-erases the chip. + Level 2 is permanent. Neither has any use on the bench. +- An option-byte change needs a full power-on reset to load; NRST alone is + not enough (root-cause note §4.2). +- Factory-clean option bytes read `0x807800AA` at `0x1FF80000` (RDP `0xAA` = + level 0). Source: the + [J-Link note §3a](../../../AI%20NOTES/2026-05-17_Murata_L072_LoRa_Unbrick_JLink_Hardware.md). + The bench L072s' current option bytes were never recorded. Reading them + (AN3155 Read Memory, read-only) is an open item in + [`BENCH_BOARDS.md`](bench_tools/BENCH_BOARDS.md), to be done only on a GO. + +### When it goes wrong (2026-05 lessons) + +- **Openocd never prints `READY: L072 in STM32 ROM bootloader`.** This is + the H7 side (gpio10, the openocd race, or a wedged bridge), not the L072. + Power-cycle the X8 and carrier with the 12 V barrel and USB-C unplugged + for 10 s + ([T2](../../X8_HEALTH_AND_RECOVERY/recovery/T2_cold_power_cycle.md)) + before you suspect the chip. +- **Do not use the PWM/gpio163 "SWD bypass"** + (`swd_bypass_pa11_pf4_launcher.sh`, sysfs PWM4 + gpio163). It was shown + on 2026-05-17 not to enter the ROM. Only the openocd path does. +- **Last resort: an external J-Link on `CN2`** + ([T4](../../X8_HEALTH_AND_RECOVERY/recovery/T4_lora_jlink_hardware_unbrick.md), + [J-Link note](../../../AI%20NOTES/2026-05-17_Murata_L072_LoRa_Unbrick_JLink_Hardware.md)). + 1. Solder a 2×5 1.27 mm header. Use the standard Cortex 10-pin zig-zag + cable; the Segger needle adapter does not fit. + 2. VTref (~3.3 V) needs the 12 V barrel jack **and** the X8 seated and + powered. + 3. Run `JLink.exe -device STM32L072CZ -if SWD -speed 1000 -autoconnect 1`. + 4. `erase`: a mass erase that also restores the option-byte defaults. + 5. `mem32 0x1FF80000 4` must show `0x807800AA`. + 6. `loadbin , 0x08000000`, then `r` and `go`. + + Once the option bytes are clean, the X8 pipeline above works again. +- **Which board was bricked is uncertain.** The 05-14 brick ran on adb + serial `2E2C…`, but the 05-17 unbrick note says "Board 1 (`2D0A…`)". The + notes disagree, which [`BENCH_BOARDS.md`](bench_tools/BENCH_BOARDS.md) + lists as an open item. +- Do not accept J-Link OB firmware-update prompts. An X8 OS reflash (T3a) + never fixes an L072 problem. From c066e36682e8184aca718d5816c218694be20f5b Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:29:29 -0500 Subject: [PATCH 11/32] murata_l072/README: Building section with the exact toolchain and md5s 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 bb4a2071 (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 --- .../firmware/murata_l072/README.md | 73 +++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md index fef448b1..22002835 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md @@ -63,16 +63,79 @@ Future A/B layout (Phase 6, N-26) splits APP: Stack is reserved at the top (size set in [include/memory_map.h](include/memory_map.h) as `MM_STACK_SIZE`, default 2.5 KB per Claude review §2.3 recommendation). -## Build +## Building + +### Toolchain (byte-identical builds need exactly this) + +| tool | version | notes | +|---|---|---| +| cross compiler | **Arm GNU Toolchain 12.2.MPACBTI-Rel1**: `arm-none-eabi-gcc --version` prints `arm-none-eabi-gcc (Arm GNU Toolchain 12.2.MPACBTI-Rel1 (Build arm-12-mpacbti.34)) 12.2.1 20230214` | From developer.arm.com, *Arm GNU Toolchain Downloads*, release 12.2.MPACBTI-Rel1, "AArch32 bare-metal target (arm-none-eabi)". Bench PC install: `C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\12.2 mpacbti-rel1\bin` on `PATH`. Linux x86_64 tarball: `https://developer.arm.com/-/media/Files/downloads/gnu/12.2.mpacbti-rel1/binrel/arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-arm-none-eabi.tar.xz` (170 644 784 B, sha256 `17455a06c816031cc2c66243c117cba48463cd6a3a3fdfac7275b4e9c40eb314`, as published in Arm's `.sha256asc`). | +| make | Windows: WinLibs `mingw32-make` (winget `BrechtSanders.WinLibs.POSIX.UCRT`), **run from PowerShell only**. Linux: GNU make. | On Windows the Makefile uses `cmd` syntax for `mkdir`/`rmdir`, so it fails from Git Bash. | +| host compiler (`make check` only) | any gcc/cc (WinLibs `gcc` on Windows) | `HOST_CC` overrides. | + +Other compilers build **working firmware with different bytes**: +- Arduino's bundled `arm-none-eabi-gcc` 7-2017q4. [`build.ps1`](build.ps1) + picks it first when it is installed, so do not use `build.ps1` for a build + you will compare. +- Ubuntu's `gcc-arm-none-eabi`. The `L072 cross-compile` job in + `arduino-ci.yml` uses it for its link + size-budget gate. + +A matching md5 therefore proves both the source and the toolchain. + +### Targets ``` -make # build firmware.elf, firmware.bin, firmware.hex -make size # show region usage against the budget +mingw32-make # = all: build/firmware.elf/.bin/.hex + region sizes (Linux: make) +mingw32-make bench # build_bench/ with -DHOST_ALLOW_REG_WRITE_DIAG=1, copied to build/firmware_bench_diag.bin +mingw32-make check # host-compiler unit vectors + memory-map static asserts (no cross toolchain) +mingw32-make size # region usage against the budget +mingw32-make clean # removes build/ (including the committed production bin; restore it with `mingw32-make`) python3 tools/check_size_budget.py build/firmware.elf -make clean ``` -A real toolchain (`arm-none-eabi-gcc`) is required to link. CI now runs a full cross-compile gate on pinned `ubuntu-24.04` with Ubuntu's `gcc-arm-none-eabi` package, publishes the ELF/BIN/HEX/MAP artifacts, and enforces APP/RAM budgets via [tools/check_size_budget.py](tools/check_size_budget.py) using constants from [include/memory_map.h](include/memory_map.h). +- `all` makes the **production** image `build/firmware.bin`, which is + committed (`HOST_ALLOW_REG_WRITE_DIAG=0`). It refuses the bench's + diagnostic register writes, such as the DTS carrier pin `-ForceFrfHz`. + Rebuild and commit it with every firmware PR. +- `bench` makes `build/firmware_bench_diag.bin`, the image **both bench + boards** run. It is untracked: add firmware files to commits by path, never + with `git add `. +- `bench` writes into `build/` but does not create it. After a `clean`, run + `mingw32-make` (or create `build/`) before `mingw32-make bench`. +- Run `check` before flashing anything. Flashing is covered in + [`../x8_lora_bootloader_helper/FLASH_RUNBOOK.md`](../x8_lora_bootloader_helper/FLASH_RUNBOOK.md). + +### Expected md5s + +At `main` `bb4a2071` (re-checked 2026-10-04; `firmware/murata_l072` unchanged +since), built with the toolchain above and the default flags: + +| image | md5 | size | +|---|---|---| +| `build/firmware_bench_diag.bin` (bench, on both boards since 2026-09-15) | `0c1bb0a9573f813137f941dfa47177d0` | 24 860 B | +| `build/firmware.bin` (production, committed) | `589c120323c2d5e7ef9f459d7a4ba42d` | 24 860 B | + +Check with `certutil -hashfile build\firmware_bench_diag.bin MD5` +(PowerShell) or `md5sum build/firmware_bench_diag.bin`. +- Any source change moves both md5s. Record the new pair in the PR. +- Older flashed builds and their md5s are listed in + [`bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md`](../../bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md). + +### CI + +- `arduino-ci.yml` → `L072 cross-compile` (blocking): links with Ubuntu's + `gcc-arm-none-eabi`. It publishes the ELF/BIN/HEX/MAP artifacts and + enforces the APP/RAM budgets through + [tools/check_size_budget.py](tools/check_size_budget.py), using constants + from [include/memory_map.h](include/memory_map.h). Its bytes differ from + the bench build by design. +- `l072-bench-md5.yml` → `L072 bench md5 (Arm 12.2.MPACBTI-Rel1)` + (**non-blocking**): downloads the exact Arm release above for Linux, runs + `make bench` and `make`, and prints both md5s against this table and + against the committed `build/firmware.bin`, with a warning on mismatch. + The Windows bench PC builds are the reference. Whether a Linux build of + the same release is byte-identical is what this job tests; it has not been + checked by hand. ## Not yet present From 1e0342ac1e66e5a97015ec1edfa3ce572d1ba441 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:31:02 -0500 Subject: [PATCH 12/32] ci: non-blocking L072 bench md5 job (Arm GNU Toolchain 12.2.MPACBTI-Rel1) 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 --- .github/workflows/l072-bench-md5.yml | 123 +++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/l072-bench-md5.yml diff --git a/.github/workflows/l072-bench-md5.yml b/.github/workflows/l072-bench-md5.yml new file mode 100644 index 00000000..f6adbf42 --- /dev/null +++ b/.github/workflows/l072-bench-md5.yml @@ -0,0 +1,123 @@ +# L072 bench md5 -- NON-BLOCKING reproducibility check (bench kit, 2026-10-10). +# +# Rebuilds the Murata L072 firmware with the exact toolchain the bench PC uses +# (Arm GNU Toolchain 12.2.MPACBTI-Rel1, arm-none-eabi-gcc 12.2.1 20230214) and +# prints the md5s of the bench and production images next to the reference +# values in LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md +# ("Expected md5s") and the committed build/firmware.bin. Mismatches are +# warnings, never failures: the job is continue-on-error and the md5 step +# always exits 0. The blocking link/size gate stays in arduino-ci.yml. +name: L072 bench md5 (non-blocking) + +on: + push: + paths: + - 'LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/**' + - '.github/workflows/l072-bench-md5.yml' + pull_request: + paths: + - 'LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/**' + - '.github/workflows/l072-bench-md5.yml' + workflow_dispatch: + +permissions: + contents: read + +env: + # Linux x86_64 host build of the release the bench PC uses. URL and sha256 + # checked 2026-10-10 against Arm's published .sha256asc (170 644 784 B). + ARM_TOOLCHAIN_URL: https://developer.arm.com/-/media/Files/downloads/gnu/12.2.mpacbti-rel1/binrel/arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-arm-none-eabi.tar.xz + ARM_TOOLCHAIN_SHA256: 17455a06c816031cc2c66243c117cba48463cd6a3a3fdfac7275b4e9c40eb314 + # murata_l072/README.md "Expected md5s" (main bb4a2071). Update both when the + # firmware source changes and the new pair is recorded. + EXPECTED_BENCH_MD5: 0c1bb0a9573f813137f941dfa47177d0 + EXPECTED_PROD_MD5: 589c120323c2d5e7ef9f459d7a4ba42d + L072_DIR: LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072 + +jobs: + l072-bench-md5: + name: L072 bench md5 (Arm 12.2.MPACBTI-Rel1) + runs-on: ubuntu-24.04 + continue-on-error: true + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: Cache the Arm toolchain + uses: actions/cache@v4 + with: + path: ~/arm-gnu-toolchain-12.2.mpacbti-rel1 + key: arm-gnu-toolchain-12.2.mpacbti-rel1-x86_64-${{ env.ARM_TOOLCHAIN_SHA256 }} + + - name: Install Arm GNU Toolchain 12.2.MPACBTI-Rel1 + run: | + set -euo pipefail + TC="$HOME/arm-gnu-toolchain-12.2.mpacbti-rel1" + if [ ! -x "$TC/bin/arm-none-eabi-gcc" ]; then + curl -fsSL --retry 3 -o "$RUNNER_TEMP/tc.tar.xz" "$ARM_TOOLCHAIN_URL" + echo "$ARM_TOOLCHAIN_SHA256 $RUNNER_TEMP/tc.tar.xz" | sha256sum -c - + mkdir -p "$TC" + tar -xJf "$RUNNER_TEMP/tc.tar.xz" -C "$TC" --strip-components=1 + rm -f "$RUNNER_TEMP/tc.tar.xz" + fi + echo "$TC/bin" >> "$GITHUB_PATH" + "$TC/bin/arm-none-eabi-gcc" --version | head -1 + + - name: Record the committed production md5 + working-directory: ${{ env.L072_DIR }} + run: md5sum build/firmware.bin | cut -d' ' -f1 | tee "$RUNNER_TEMP/committed_prod.md5" + + - name: make bench + working-directory: ${{ env.L072_DIR }} + run: | + arm-none-eabi-gcc --version | head -1 + mkdir -p build # `bench` writes build/firmware_bench_diag.bin but does not create build/ + make bench + + - name: make (production) + working-directory: ${{ env.L072_DIR }} + run: make + + - name: Compare md5s with the README (warn only) + working-directory: ${{ env.L072_DIR }} + run: | + bench=$(md5sum build/firmware_bench_diag.bin | cut -d' ' -f1) + prod=$(md5sum build/firmware.bin | cut -d' ' -f1) + committed=$(cat "$RUNNER_TEMP/committed_prod.md5") + verdict() { [ "$1" = "$2" ] && echo "match" || echo "**differs**"; } + { + echo "## L072 md5 -- Arm GNU Toolchain 12.2.MPACBTI-Rel1 on Linux" + echo + echo "| image | built here | reference | |" + echo "|---|---|---|---|" + echo "| bench \`firmware_bench_diag.bin\` | \`$bench\` | README \`$EXPECTED_BENCH_MD5\` | $(verdict "$bench" "$EXPECTED_BENCH_MD5") |" + echo "| production \`firmware.bin\` | \`$prod\` | README \`$EXPECTED_PROD_MD5\` | $(verdict "$prod" "$EXPECTED_PROD_MD5") |" + echo "| production \`firmware.bin\` | \`$prod\` | committed \`$committed\` | $(verdict "$prod" "$committed") |" + echo + echo "A source change since bb4a2071 moves both README md5s; record the new" + echo "pair in murata_l072/README.md and in this workflow's env." + } >> "$GITHUB_STEP_SUMMARY" + echo "bench built=$bench expected=$EXPECTED_BENCH_MD5" + echo "production built=$prod expected=$EXPECTED_PROD_MD5 committed=$committed" + if [ "$bench" != "$EXPECTED_BENCH_MD5" ]; then + echo "::warning title=L072 bench md5 differs::firmware_bench_diag.bin md5 $bench != README $EXPECTED_BENCH_MD5 (source changed since bb4a2071, or this Linux build of 12.2.MPACBTI-Rel1 is not byte-identical to the bench PC build)" + fi + if [ "$prod" != "$EXPECTED_PROD_MD5" ]; then + echo "::warning title=L072 production md5 differs::firmware.bin md5 $prod != README $EXPECTED_PROD_MD5" + fi + if [ "$prod" != "$committed" ]; then + echo "::warning title=Committed firmware.bin not reproduced::the committed build/firmware.bin ($committed) differs from this build ($prod): rebuild and commit it with the firmware change, using the exact toolchain" + fi + exit 0 + + - name: Upload the built images + if: always() + uses: actions/upload-artifact@v4 + with: + name: l072-bench-md5-${{ github.sha }} + path: | + ${{ env.L072_DIR }}/build/firmware_bench_diag.bin + ${{ env.L072_DIR }}/build/firmware.bin + ${{ env.L072_DIR }}/build/firmware.map + if-no-files-found: warn + retention-days: 14 From 0ed259b6b4360c7a04c98507a00608e0cd702a86 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:31:47 -0500 Subject: [PATCH 13/32] bench kit(K2): provision_bench_board.sh + the tractor's disable-wifi unit provision_bench_board.sh [--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 --- .../bench_tools/provision_bench_board.sh | 481 ++++++++++++++++++ .../bench_tools/units/disable-wifi.service | 12 + 2 files changed, 493 insertions(+) create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/provision_bench_board.sh create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/units/disable-wifi.service diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/provision_bench_board.sh b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/provision_bench_board.sh new file mode 100644 index 00000000..41d9813d --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/provision_bench_board.sh @@ -0,0 +1,481 @@ +#!/usr/bin/env bash +# provision_bench_board.sh -- bring one freshly imaged Portenta X8 (on a Max +# Carrier) to the LifeTrac bench state, or report how far it is from it. +# +# bash provision_bench_board.sh base [--check] [--ssh] [--nic-100] +# bash provision_bench_board.sh tractor [--check] [--mosquitto-from-base | --mosquitto-tar FILE] +# bash provision_bench_board.sh --help +# +# Runs on the bench PC (Git Bash on Windows; any bash with adb works). Talks to +# one board over adb (the base can use ssh with --ssh). Every step checks first +# and changes only what is missing, so re-running it is safe. --check changes +# nothing and exits 3 if something is still to do. +# +# What it applies (BENCH_SETUP.md explains each step): +# both sudoers drop-in (../install_lifetrac_nopasswd.sh), docker running, +# the probe image hub.foundries.io/arduino/arduino-ootb-python-devel:738bc44 +# (repo-root foundries_python.tar; radio_park.py / radio_state.py / +# rs116_health_probe.py run in it) +# base compose-apps-early-start{,-recovery}.service masked, the PC's ssh +# key in fio's authorized_keys, eclipse-mosquitto:2 pulled, +# optional eth0 100BASE-TX full pin (--nic-100) +# tractor WiFi off for good (units/disable-wifi.service enabled, +# wpa_supplicant masked, nmcli radio wifi off, stored WiFi profiles +# deleted), camera USB provisioning (../provision_x8.sh), +# eclipse-mosquitto:2 loaded from the base or a tarball; the +# compose-apps mask too, unless the board still runs the factory +# 674 image (where that unit succeeds) +# +# Radio safety: this script never opens /dev/ttymxc3 (the L072 radio UART), +# never starts lifetrac-base.service, lifetrac-base-compose.service or +# lifetrac-tractor-compose.service, and on the tractor it leaves +# lifetrac-camera.service and the tractor-camera container stopped (both map +# /dev/ttymxc3; they come back at the next boot). +# +# Settings come from bench.env next to this script when it exists (copy +# bench.env.example), else these defaults (the original bench pair): +# BASE_SERIAL=2D0A1209DABC240B TRACTOR_SERIAL=2E2C1209DABC240B +# BASE_HOST=192.168.1.117 BENCH_SUDO_PW=fio (used once, to install sudoers) +# BASE_SSH_KEY=~/.ssh/lifetrac_base_ed25519 BASE_USER=fio ADB=adb +# Values already in bench.env win over the defaults. The password is never printed. + +set -u + +usage() { + sed -n '2,/^$/{s/^# \{0,1\}//;p}' "$0" + cat <<'EOF' +Options: + --check report only; change nothing (exit 3 if anything is missing) + --serial SERIAL adb serial of the board (default: BASE_SERIAL / TRACTOR_SERIAL) + --ssh base only: use ssh to BASE_HOST instead of adb + --nic-100 base only: pin 'Wired connection 1' to 100BASE-TX full + (workaround for a marginal cable; leave it off on a good cable) + --mosquitto-from-base tractor only: docker save eclipse-mosquitto:2 on the base, + copy it through the PC and docker load it on the tractor + --mosquitto-tar FILE tractor only: docker load eclipse-mosquitto:2 from FILE (a docker save tarball) + --mask-compose tractor only: mask compose-apps-early-start{,-recovery} even on the 674 image + --keep-wifi-profiles tractor only: do not delete stored NetworkManager WiFi profiles + --allow-camera-restart tractor only: allow provision_x8.sh to run although a camera compose + app is installed (it restarts lifetrac-camera.service, which maps the + radio UART; this script stops it again right after) + -h, --help this text +Exit codes: 0 done / all present, 1 a step failed, 2 usage or board unreachable, 3 --check found work to do. +EOF +} + +# ---------------------------------------------------------------- settings -- +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +HELPER_DIR=$(cd "$SCRIPT_DIR/.." && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/../../../../.." && pwd) + +BENCH_ENV=${BENCH_ENV:-$SCRIPT_DIR/bench.env} +if [ -f "$BENCH_ENV" ]; then + # strip CR so a bench.env saved by a Windows editor does not put \r into serials + # shellcheck disable=SC1090 + . <(sed 's/\r$//' "$BENCH_ENV") +fi +BASE_SERIAL=${BASE_SERIAL:-2D0A1209DABC240B} +TRACTOR_SERIAL=${TRACTOR_SERIAL:-2E2C1209DABC240B} +BASE_HOST=${BASE_HOST:-192.168.1.117} +BENCH_SUDO_PW=${BENCH_SUDO_PW:-fio} +BASE_SSH_KEY=${BASE_SSH_KEY:-$HOME/.ssh/lifetrac_base_ed25519} +BASE_USER=${BASE_USER:-fio} +ADB=${ADB:-adb} + +FOUNDRIES_IMG=hub.foundries.io/arduino/arduino-ootb-python-devel:738bc44 +FOUNDRIES_TAR=$REPO_ROOT/foundries_python.tar +MOSQ_IMG=eclipse-mosquitto:2 +NIC_CON='Wired connection 1' +RSTAGE=/tmp/lifetrac_provision # on the board (tmpfs; gone after a reboot) +UNIT_WIFI=$SCRIPT_DIR/units/disable-wifi.service + +# --------------------------------------------------------------- arguments -- +ROLE=""; CHECK_ONLY=0; SERIAL_ARG=""; USE_SSH=0; NIC_100=0 +MOSQ_FROM_BASE=0; MOSQ_TAR=""; MASK_COMPOSE=0; KEEP_WIFI=0; ALLOW_CAMERA_RESTART=0 +while [ $# -gt 0 ]; do + case "$1" in + base|tractor) ROLE=$1 ;; + --check) CHECK_ONLY=1 ;; + --serial|--mosquitto-tar) + case "${2:-}" in ''|-*) echo "$1 needs a value" >&2; exit 2 ;; esac + if [ "$1" = --serial ]; then SERIAL_ARG=$2; else MOSQ_TAR=$2; fi + shift ;; + --ssh) USE_SSH=1 ;; + --nic-100) NIC_100=1 ;; + --mosquitto-from-base) MOSQ_FROM_BASE=1 ;; + --mask-compose) MASK_COMPOSE=1 ;; + --keep-wifi-profiles) KEEP_WIFI=1 ;; + --allow-camera-restart) ALLOW_CAMERA_RESTART=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown argument: $1 (see --help)" >&2; exit 2 ;; + esac + shift +done +[ -n "$ROLE" ] || { usage; exit 2; } +if [ "$ROLE" = tractor ] && [ "$USE_SSH" = 1 ]; then + echo "--ssh is for the base only; the tractor is reached over adb (its WiFi stays off)" >&2; exit 2 +fi +if [ "$ROLE" = base ] && { [ "$MOSQ_FROM_BASE" = 1 ] || [ -n "$MOSQ_TAR" ]; }; then + echo "--mosquitto-* are for the tractor; the base pulls $MOSQ_IMG itself" >&2; exit 2 +fi + +if [ "$USE_SSH" = 1 ]; then + TRANSPORT=ssh; TARGET=$BASE_HOST +else + TRANSPORT=adb + if [ -n "$SERIAL_ARG" ]; then TARGET=$SERIAL_ARG + elif [ "$ROLE" = base ]; then TARGET=$BASE_SERIAL + else TARGET=$TRACTOR_SERIAL; fi +fi + +# ----------------------------------------------------------------- helpers -- +say() { printf '%s\n' "$*"; } +die() { printf 'ERROR: %s\n' "$2" >&2; exit "$1"; } +indent() { sed 's/^/ /'; } + +winpath() { # adb.exe wants C:/... paths; elsewhere the path is used as is + if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else printf '%s' "$1"; fi +} + +# on : run a shell command on a board, CR stripped +on() { + if [ "$1" = ssh ]; then + ssh -i "$BASE_SSH_KEY" -o BatchMode=yes -o ConnectTimeout=10 "$BASE_USER@$2" "$3" 2>&1 | tr -d '\r' + else + MSYS_NO_PATHCONV=1 "$ADB" -s "$2" shell "$3" 2>&1 | tr -d '\r' + fi +} +push_to() { # push_to + if [ "$1" = ssh ]; then + scp -q -i "$BASE_SSH_KEY" -o BatchMode=yes "$3" "$BASE_USER@$2:$4" + else + MSYS_NO_PATHCONV=1 "$ADB" -s "$2" push "$(winpath "$3")" "$4" >/dev/null + fi +} +pull_from() { # pull_from + if [ "$1" = ssh ]; then + scp -q -i "$BASE_SSH_KEY" -o BatchMode=yes "$BASE_USER@$2:$3" "$4" + else + MSYS_NO_PATHCONV=1 "$ADB" -s "$2" pull "$3" "$(winpath "$4")" >/dev/null + fi +} +rsh() { on "$TRANSPORT" "$TARGET" "$1"; } +# rtest '': true when it succeeds on the board (does not rely on +# adb propagating exit codes, which older adbd builds do not) +rtest() { [ "$(rsh "if { $1; } >/dev/null 2>&1; then echo __YES__; else echo __NO__; fi" | tail -n 1)" = __YES__ ]; } +rval() { rsh "$1" | tail -n 1; } + +LSTAGE=$(mktemp -d "${TMPDIR:-/tmp}/lifetrac_provision.XXXXXX") || die 2 "mktemp failed" +trap 'rm -rf "$LSTAGE"' EXIT + +stage_text() { # stage_text : strip CR, then push + local name; name=$(basename "$1") + sed 's/\r$//' "$1" > "$LSTAGE/$name" || return 1 + push_to "$TRANSPORT" "$TARGET" "$LSTAGE/$name" "$2/$name" +} +lsha() { sed 's/\r$//' "$1" | sha256sum | awk '{print $1}'; } +rsha() { rsh "sha256sum '$1' 2>/dev/null" | awk 'NF==2 {print $1}' | tail -n 1; } + +RESULTS=(); N_TODO=0; N_FAIL=0 +record() { # record