Skip to content

L072 RS-12.21: FHSS scan recovers from FAILED (option b + L + 2 s dwell), firmware park command, 0x0D split — built, not flashed - #168

Merged
dorkmo merged 1 commit into
mainfrom
fhss-scan-recovery-2026-10-24
Oct 25, 2026
Merged

dorkmo merged 1 commit into
mainfrom
fhss-scan-recovery-2026-10-24

Conversation

@dorkmo

@dorkmo dorkmo commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

What this is

This implements the design owner's choice from #167's memo (FHSS_SCAN_FAILED_OPTIONS.md section 5, recorded as DECISIONS.md D-FHSS1). The package is option (b), the loss-timer fix L, a 2 s dwell, the RS-12.21 firmware park command, and the 0x0D fault-code split. The decay-window trade-off in memo section 5 item 3 is accepted with the choice.

Built, not flashed. Both boards still run e083ae46. The operator kept the radios off today; flashing and radio legs are planned for tomorrow, each on its own GO.

Firmware (L072)

  • (b) scan recovery. A node holds scan FAILED only while it holds originator authority.
    • A final FAIL on any other node keeps scanning, and it still reports 0x0D final every 30 s.
    • A node in FAILED that loses authority scans again on its next tick.
    • This fixes leg 6's stranded base and the tractor twin the SIL found.
    • The pure scan-policy contract is unchanged; sx1276_rx.c decides whether to hold FAILED.
  • L. A LOCK starts the 2 s loss timer at the locking frame.
  • d2. Each channel dwell is now 2 s, up from 500 ms. The walk continues across retries and across (b)'s restarts.
  • Park command (RS-12.21).
    • HOST_TYPE_RADIO_PARK_REQ 0x04 is answered with RADIO_PARK_URC 0x84 = {status 0 PARKED / 1 ALREADY / 2 REFUSED, RegOpMode readback}.
    • It puts the modem to SLEEP. While parked, no RX arm path runs: the scan is frozen, and the slot follower, the gamma-1 retune, the post-TX re-arm and the sx1276_rx_arm() backstop are all gated. TX is refused too.
    • A park request is refused while a TX is keyed or queued.
    • The park ends on a raw RXCONT write, a profile activation, or a reset.
    • Capability bit 7 (bitmap 0xFF) marks this image.
  • 0x0D split. TX_FIFO_RB_MISMATCH moves to 0x0F, and RX_SCAN_FAILED keeps 0x0D. A compile-time duplicate check now covers fault codes.
  • Wire versions are not bumped. Additive changes keep HOST_PROTOCOL_VER and HOST_WIRE_SCHEMA_VER, per the contract now written into host_types.h.
  • H7 mirror. mh_wire.h mirrors the new values, and check_mh_wire_sync.py passes with 65 constants.

SIL

  • check-rx-scan-failed-follower is now (b)'s acceptance check.
    • In the leg-6 scenario the base keeps scanning, LOCKs, and then follows the hop grid.
    • A silent follower reports every 30 s and walks all 50 channels.
    • The tractor twin regains authority.
    • Seeds 1-1000 pass. Reverting the whole fix, or any one part of it, makes the check fail.
  • New check-radio-park drives the real host_cmd.c: nothing re-arms for minutes, each way out of the park works, and TX is refused.
  • The shared rx_reverse_delivery sim now follows the firmware's recorded scan transitions. It adds four structural checks on every scenario.
  • The numbers match the memo exactly: leg-6 replay 100/100 (median 17.9 s, 99.8 % heard); cold start 1.3 % without a lock at 2 fps; the E3 decay window as documented.

Host

  • Fault decode. 0x0F decodes as the FIFO fault. 0x0D decodes as RX_SCAN_FAILED when the sub-byte is valid; otherwise it is named as an old-image FIFO fault.
  • radio_park.py uses the park command when the image advertises it. On e083ae46 it falls back to the register path and says so. The settle time is now 2.5 s.
  • Injector trigger. -InjectOn fault:0x0D:final now requires a valid attempt-3 scan sub, and fault:<other>:final is refused.
  • Leg report and scorer tell the restart image (cap 0xFF) from the absorbing one. A final 0x0D is no longer read as FAILED on the new image.

Builds (clean, reproducible)

image md5 size
production build/firmware.bin 2c5e055abff95126309a62409fafc654 25 324 B
bench build/firmware_bench_diag.bin (untracked, by policy) 0daae27978089eb97ab35c18718b62b0 25 324 B

These are recorded in README, l072-bench-md5.yml, FLASH_RUNBOOK, flash_l072.sh, BENCH_*, PC_SETUP, DECISIONS and TODO. check_size_budget.py passes (APP 25 324 of 188 416 B).

Verification

  • make check: full pass, run from PowerShell with WinLibs mingw32-make.
  • base_station suite: 1931 passed. The only failures are the 10 pre-existing Windows-only test_x8_watchdog_ownership_sil.py cases.
  • Harness offline tests: 197/197.
  • H7 wire tests build and pass.
  • git diff --check is clean, apart from the linker map's *fill* lines, which were already there.
  • TODO.md keeps 4841 lines; rows RS-12.21 and RS-12.15 are edited in place. The host_types.h line citations in SETTINGS_REFERENCE are re-derived.
  • Reviews: two adversarial passes, firmware and host, with 9 findings fixed. Among them, the leg report would otherwise have read a final 0x0D as FAILED on this image. A final integration review followed, and its minor findings are fixed here.

Tomorrow: flash and verify (memo section 6.1, BENCH_RUNBOOK "First flash and legs on the RS-12.21 image")

  1. Before the GO, no radio. Fly from the merged main with a new dated EVIDENCE_DIR. Run make check and a clean build: the bench md5 must be 0daae279…. The harness offline tests must show 0 failed.
  2. Flash GO. Run the power-up guard and staging. Preflight flash_l072.sh <board> $FW without --go, then flash the tractor, then the base, with --go. Each must show Verify OK and PIPELINE-EXIT=0.
  3. After the flash, nothing transmits.
    • Run rs116_health_probe.py: STATS-OK, tx_fifo_rb_bad 0.
    • Run radio_park.py on both boards. Want PARK_OK with cap_bitmap 0x000000FF, path firmware, persistent true, and 0x80 readbacks. path register or 0x7F means the old image is still on: stop.
    • A read-only radio_state.py 2 minutes or more later must read 0x80.
  4. First legs, each on its own GO. Leg R1 is an FHSS mono_g4 leg (profile 1, 2 fps, 300 s).
    • The base logs cap=0x000000FF.
    • It shows confirmed acquisition: ACQUIRED within 120 s.
    • Every 0x0D line is name=RX_SCAN_FAILED, there is no 0x0F line, and Δtx_fifo_rb_bad is 0.
    • The leg-end park is PARK_OK on the firmware path, and --check-only reads 0x80 two minutes later. This is the real test of the park gate, because profile 1 is still active then.
    • Stop on !! ACQUISITION FAILED or !! BASE NEVER LOCKED.
  5. Rollback, on its own GO: bench-evidence/RS_12_15_reverse_delivery_2026-10-18/firmware_flash/firmware_bench_diag_e083ae46.bin.

Open points for the design owner (non-blocking)

  • A TX refused while parked uses the existing ERR_PROTO refusal bytes. A distinct detail code would let a daemon tell a parked radio from an LBT, airtime or dwell refusal.
  • On the bench diag image, raw RegOpMode writes of any value are accepted. A raw TX or CAD write would bypass the park, as it already bypasses LBT, airtime and dwell. The production gate refuses those writes.
  • The 60 s RFCO_SUMMARY URC keeps flowing on the host link while parked. It is not radio activity.

Merge only on the operator's word.

🤖 Generated with Claude Code

…x0D split

The design owner's choice (DECISIONS.md D-FHSS1, 2026-10-24), from
FHSS_SCAN_FAILED_OPTIONS.md section 5:

- (b): a node holds scan FAILED only while it holds originator
  authority. A final FAIL on any other node keeps scanning (and still
  reports 0x0D final every 30 s); a node in FAILED that loses authority
  scans again on its next tick. Leg 6's stranded base and the tractor
  twin both recover. The pure scan policy is unchanged.
- L: a LOCK starts the 2 s loss timer at the locking frame.
- d2: 2 s dwell; the walk continues across retries and (b)'s restarts.
- RS-12.21 park: HOST_TYPE_RADIO_PARK_REQ 0x04 -> RADIO_PARK_URC 0x84
  {status, RegOpMode}. Sets host_parked: modem to SLEEP, every RX arm
  path gated (scan frozen, slot follower, gamma-1, post-TX re-arm,
  sx1276_rx_arm backstop), TX refused; ended by a raw RXCONT, a profile
  activation or reset. Capability bit 7 (bitmap 0xFF) marks the image.
- 0x0D split: TX_FIFO_RB_MISMATCH moves to 0x0F; compile-time duplicate
  check for fault codes. mh_wire.h mirrors the new values.
- SIL: check-rx-scan-failed-follower is now (b)'s acceptance check
  (fails if any part is reverted); new check-radio-park drives the real
  host_cmd.c; the shared sim follows the firmware's recorded scan
  transitions. SIL numbers match the memo (leg-6 replay 100/100, median
  17.9 s; 2 fps cold start 1.3 %).
- Host: 0x0F / 0x0D decode (old-image FIFO faults named as such),
  radio_park.py uses the park command when the image advertises it
  (register path on e083ae46), tighter fault:0x0D:final trigger, the
  leg report tells the restart image from the absorbing one.
- md5 records: production 2c5e055a, bench 0daae279 (25 324 B); built,
  NOT flashed: the boards still run e083ae46. Flash-and-verify plan in
  the memo section 6.1 and BENCH_RUNBOOK.

make check passes; base_station 1931 passed (10 Windows-only failures
as on main); harness offline 197/197; mh_wire sync 65 constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 25, 2026 13:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Cross-layer radio recovery and persistent parking need final human review and explicit operator authorization, with on-board validation still pending.

No open issues 🎉

What changed in this PR

Implements the FHSS recovery package selected from #167’s decision memo, allowing nodes without originator authority to resume scanning instead of remaining in FAILED. It pairs recovery with persistent firmware parking and updates host diagnostics and bench procedures. The images are built but not flashed.

Changes:

  • Adds scan recovery, a locking-frame loss timer, and continuous channel walking with a 2-second dwell.
  • Adds the radio park command, separates scan and FIFO fault codes, and updates host decoding, triggering, and scoring.
  • Extends simulation checks and records build hashes, rollout steps, and operator-controlled validation.

Paths below are relative to LifeTrac-v25/DESIGN-CONTROLLER/, except .github/.

File Description
tools/​check_mh_wire_sync.py Checks mirrored park and fault constants.
TODO.md Records implementation status and pending radio validation.
firmware/​x8_lora_bootloader_helper/​run_live_radio_monitor.ps1 Restricts final-fault triggers to valid scan encodings.
firmware/​x8_lora_bootloader_helper/​README.md Distinguishes new builds from flashed images.
firmware/​x8_lora_bootloader_helper/​method_g_stage1_probe.py Adds park requests and split fault decoding.
firmware/​x8_lora_bootloader_helper/​FLASH_RUNBOOK.md Documents image hashes and persistent-park verification.
firmware/​x8_lora_bootloader_helper/​bench_tools/​test_run_live_radio_monitor.ps1 Tests scan-final and FIFO fault triggers.
firmware/​x8_lora_bootloader_helper/​bench_tools/​rs13_probe_score.py Makes acquisition scoring scan-image aware.
firmware/​x8_lora_bootloader_helper/​bench_tools/​radio_park.py Selects firmware parking or legacy register fallback.
firmware/​x8_lora_bootloader_helper/​bench_tools/​provision_bench_board.sh Updates firmware provisioning guidance.
firmware/​x8_lora_bootloader_helper/​bench_tools/​PC_SETUP.md Updates reproducible-build references.
firmware/​x8_lora_bootloader_helper/​bench_tools/​legs/​README.md Documents firmware-path parking expectations.
firmware/​x8_lora_bootloader_helper/​bench_tools/​flash_l072.sh Recognizes the new firmware hashes.
firmware/​x8_lora_bootloader_helper/​bench_tools/​bracket_deltas.py Updates diagnostics for scanning without acquisition.
firmware/​x8_lora_bootloader_helper/​bench_tools/​BENCH_SETUP.md Updates build and parking instructions.
firmware/​x8_lora_bootloader_helper/​bench_tools/​BENCH_RUNBOOK.md Documents new diagnostics, parking, and first-leg checks.
firmware/​x8_lora_bootloader_helper/​bench_tools/​BENCH_QUICKSTART.md Updates build, acquisition, and shutdown guidance.
firmware/​x8_lora_bootloader_helper/​bench_tools/​BENCH_BOARDS.md Separates built images from board firmware inventory.
firmware/​tractor_h7/​murata_host/​mh_wire.h Mirrors park messages and split fault codes.
firmware/​murata_l072/​README.md Records new build hashes and unflashed status.
firmware/​murata_l072/​radio/​sx1276_tx.c Refuses transmission while parked.
firmware/​murata_l072/​radio/​sx1276_rx.c Implements scan recovery, loss timing, and park gates.
firmware/​murata_l072/​Makefile Integrates park and recovery acceptance checks.
firmware/​murata_l072/​include/​sx1276_rx.h Declares parking APIs and recovery contracts.
firmware/​murata_l072/​include/​sx1276_rx_scan_walker.h Documents channel walking across retries.
firmware/​murata_l072/​include/​sx1276_rx_scan_policy.h Sets the 2-second dwell and documents dispatcher recovery.
firmware/​murata_l072/​include/​sx1276_rx_scan_fail.h Clarifies final-failure reporting semantics.
firmware/​murata_l072/​include/​host_types.h Defines park protocol, capability, and unique fault codes.
firmware/​murata_l072/​host/​host_cmd.c Dispatches parking and refuses parked TX requests.
firmware/​murata_l072/​host/​host_cfg_profile.c Ends parking on profile activation.
firmware/​murata_l072/​FHSS_SCAN_FAILED_OPTIONS.md Records the adopted package and staged validation plan.
firmware/​murata_l072/​bench/​host_proto/​tx_len_guard.c Adds the parked-state test stub.
firmware/​murata_l072/​bench/​host_proto/​rx_reverse_delivery.c Extends simulation tracking and recovery invariants.
firmware/​murata_l072/​bench/​host_proto/​fhss_stub.c Adds the unpark test stub.
DESIGN-LORAFIRMWARE/​04_Hardware_Interface_and_Recovery.md Documents park messages and the fault-code split.
DECISIONS.md Records D-FHSS1 and its accepted trade-offs.
base_station/​tests/​test_rs13_scoring_sil.py Tests scoring for the restarting scan image.
base_station/​tests/​test_rs12_leg_report_sil.py Tests image-aware acquisition and fault reporting.
base_station/​tests/​test_fault_urc_log_sil.py Tests fault decoding, constants, and capability logging.
base_station/​image_rx_daemon.py Logs the firmware capability bitmap at startup.
.github/​workflows/​l072-bench-md5.yml Updates expected firmware build hashes.

Review effort: Balanced

@dorkmo
dorkmo merged commit db9552f into main Oct 25, 2026
27 checks passed
xchuck35 pushed a commit to xchuck35/LifeTrac that referenced this pull request Oct 26, 2026
… R2)

Operator: "proceed with any merges and you have a GO for any radio
testing". After OpenSourceEcology#168 and OpenSourceEcology#164 merged (main d597b09):

- Flash: both L072s now run bench 0daae279 (option (b) + L + 2 s
  dwell, firmware park 0x04/0x84, 0x0D split). Verify OK, flash_rc=0,
  DONE prep=0 flash=0 revive=0 on both. Post-flash: rs116 STATS-OK,
  tx_fifo_rb_bad 0; radio_park.py cap 0xFF, path firmware, PARKED,
  persistent, 0x80 held at the 2-minute re-read.
- R1 (FHSS mono_g4, 300 s, reactive PROBEs): first frame 11.0 s after
  activation, ACQUIRED (599/609); PROBE 204/204; leg-end firmware
  park held 2+ min with profile 1 active, the RS-12.21 park gate on
  hardware.
- R2 (480 s, -InjectOn fault:0x0D:final): tractor 0x0D attempts
  0x00/0x01/0x02/0x83 30 s apart, all name=RX_SCAN_FAILED; the
  injector fired on the final only; tractor heard 93/95 base commands
  in FAILED; RS-12.15 criteria PASS.
- End of session 15:02Z: both radios 0x80.

Records: the boards' build (0daae279; e083ae46 the rollback image)
in README / FLASH_RUNBOOK / BENCH_* / flash_l072.sh / DECISIONS /
SETTINGS_REFERENCE / TODO rows (in place) and the memo. Procedure:
youtube_window.ps1 -Front retries a refused focus change once with
the Alt-key workaround and exits 6 when the bench window still is not
in front; scene_check.sh then takes no frame (exit 5), so no other
window is photographed or put on the air. PIPELINE-EXIT is console
only: the success line is DONE prep=0 flash=0 revive=0.

Privacy: a diagnostic frame that showed a non-bench window was
deleted and never committed; every scene frame and a full-coverage
render of both base captures show YouTube content only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants