From 963e24360bc8dd043bb86146979ff0966184fa25 Mon Sep 17 00:00:00 2001 From: dorkmo <1923070+dorkmo@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:19:55 -0500 Subject: [PATCH] L072 RS-12.21: scan recovers from FAILED (option b), firmware park, 0x0D 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 --- .github/workflows/l072-bench-md5.yml | 16 +- LifeTrac-v25/DESIGN-CONTROLLER/DECISIONS.md | 22 + .../04_Hardware_Interface_and_Recovery.md | 4 +- .../DESIGN-CONTROLLER/SETTINGS_REFERENCE.md | 74 +- LifeTrac-v25/DESIGN-CONTROLLER/TODO.md | 4 +- .../base_station/image_rx_daemon.py | 13 +- .../tests/test_fault_urc_log_sil.py | 207 +- .../base_station/tests/test_radio_park_sil.py | 542 ++++ .../tests/test_rs12_leg_report_sil.py | 209 +- .../tests/test_rs13_scoring_sil.py | 31 + .../murata_l072/FHSS_SCAN_FAILED_OPTIONS.md | 208 +- .../firmware/murata_l072/Makefile | 108 +- .../firmware/murata_l072/README.md | 22 +- .../murata_l072/bench/host_proto/fhss_stub.c | 6 + .../murata_l072/bench/host_proto/radio_park.c | 1055 +++++++ .../bench/host_proto/rx_reverse_delivery.c | 260 +- .../host_proto/rx_scan_failed_follower.c | 823 +++-- .../bench/host_proto/tx_len_guard.c | 3 + .../firmware/murata_l072/build/firmware.bin | Bin 24972 -> 25324 bytes .../firmware/murata_l072/build/firmware.elf | Bin 63616 -> 64020 bytes .../firmware/murata_l072/build/firmware.hex | 2740 +++++++++-------- .../firmware/murata_l072/build/firmware.map | 1064 +++---- .../murata_l072/host/host_cfg_profile.c | 9 +- .../firmware/murata_l072/host/host_cmd.c | 49 + .../firmware/murata_l072/include/host_types.h | 146 +- .../firmware/murata_l072/include/sx1276_rx.h | 55 +- .../murata_l072/include/sx1276_rx_scan_fail.h | 7 + .../include/sx1276_rx_scan_policy.h | 75 +- .../include/sx1276_rx_scan_walker.h | 6 +- .../firmware/murata_l072/radio/sx1276_rx.c | 251 +- .../firmware/murata_l072/radio/sx1276_tx.c | 13 + .../firmware/tractor_h7/murata_host/mh_wire.h | 12 + .../FLASH_RUNBOOK.md | 37 +- .../x8_lora_bootloader_helper/README.md | 4 +- .../bench_tools/BENCH_BOARDS.md | 12 +- .../bench_tools/BENCH_QUICKSTART.md | 67 +- .../bench_tools/BENCH_RUNBOOK.md | 140 +- .../bench_tools/BENCH_SETUP.md | 11 +- .../bench_tools/PC_SETUP.md | 6 +- .../bench_tools/bracket_deltas.py | 5 +- .../bench_tools/flash_l072.sh | 2 + .../bench_tools/legs/README.md | 5 + .../bench_tools/provision_bench_board.sh | 2 +- .../bench_tools/radio_park.py | 243 +- .../bench_tools/rs13_probe_score.py | 40 +- .../test_run_live_radio_monitor.ps1 | 41 + .../method_g_stage1_probe.py | 240 +- .../run_live_radio_monitor.ps1 | 34 +- .../tools/check_mh_wire_sync.py | 10 + .../tools/rs12_leg_report.py | 414 ++- 50 files changed, 6571 insertions(+), 2776 deletions(-) create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_radio_park_sil.py create mode 100644 LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/radio_park.c diff --git a/.github/workflows/l072-bench-md5.yml b/.github/workflows/l072-bench-md5.yml index 650b1702..04456d46 100644 --- a/.github/workflows/l072-bench-md5.yml +++ b/.github/workflows/l072-bench-md5.yml @@ -28,10 +28,12 @@ env: # 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" (13f20aa1, the RS-12.15 reverse-delivery - # fix). Update both when the firmware source changes and the new pair is recorded. - EXPECTED_BENCH_MD5: e083ae46a64686031b39866eca42e094 - EXPECTED_PROD_MD5: 148de24e1f9acce417c46c2c69a5be6b + # murata_l072/README.md "Expected md5s" (the RS-12.21 image: scan option (b) + + # L + d2, the firmware park command, the 0x0D split; built 2026-10-25, not yet + # flashed). Update both when the firmware source changes and the new pair is + # recorded. + EXPECTED_BENCH_MD5: 0daae27978089eb97ab35c18718b62b0 + EXPECTED_PROD_MD5: 2c5e055abff95126309a62409fafc654 L072_DIR: LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072 jobs: @@ -94,13 +96,13 @@ jobs: 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." + echo "A source change after the recorded pair moves both README md5s; record" + echo "the new 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 13f20aa1, or this Linux build of 12.2.MPACBTI-Rel1 is not byte-identical to the bench PC build)" + echo "::warning title=L072 bench md5 differs::firmware_bench_diag.bin md5 $bench != README $EXPECTED_BENCH_MD5 (source changed after the recorded pair, 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" diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/DECISIONS.md b/LifeTrac-v25/DESIGN-CONTROLLER/DECISIONS.md index 793f2c0a..c242013f 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/DECISIONS.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/DECISIONS.md @@ -344,3 +344,25 @@ None of these decisions is implemented. Each one lists its options and a **RECOM * **H747 and the X8 link service, PWR-13:** report idle (engine stopped, every valve output off) and hold motion off until the H747 resets or the request is withdrawn. Idle is checked when the reboot is requested; the hold covers motion, not engine start. "Engine stopped" needs an engine-running signal other than the ignition sense I1, and until PWR-12 the H747 reports busy while the key is off. Until this exists the gate blocks every tractor reboot. * **No systemd-owned hardware watchdog (L2) on the tractor:** a watchdog reset cannot wait for idle. The tractor keeps today's u-boot-armed, kernel-petted WDOG1, so a kernel hang still resets it. * **LmP's `docker.service` `FailureAction=reboot` goes off on the tractor** (PWR-13): it reboots without waiting for idle. LmP ties it to OTA rollback, so with it off a Docker-breaking update rolls back only through the armed, idle-gated supervisor. + +--- + +## D-FHSS1 — FHSS scan FAILED: no absorbing FAILED for a node without its own clock + +**Context:** [firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md](firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md) (the decision memo), TODO RS-12.21 and RS-12.15. On FHSS (profile 1) the base is a follower. Its cold-start scan had four 30 s attempts at a 500 ms dwell; after the fourth it entered scan FAILED, which was absorbing (only a reset or a profile re-activation left it), and a follower in FAILED never LOCKs and never hops. That is the leading hypothesis for leg 6 (2b_f3) of 2026-10-24, where the base never locked and heard 5 of the tractor's 606 frames. The SIL reproduces it against the real firmware and found a tractor twin: in FAILED, one camera stall plus one heard command ends the tractor's self-follow for the session. Walker model: about 7-8 % of 2 fps one-fragment legs end in FAILED, 21-31 % at 1 fps. + +| Option | Pros | Cons | +|---|---|---| +| A. A valid frame recovers from FAILED (+ L) | Fastest recovery alone (leg-6 replay median 17.2 s) | Needs the receiver kept armed in FAILED; changes the pure scan policy's contract and the 2026-10-18 leg-X expectations; a foreign transmitter can LOCK a parked base at any time | +| **B. No absorbing FAILED without originator authority, with L (a LOCK re-anchors the 2 s loss timer) and d2 (2 s dwell, the walk continuing across retries and restarts); the firmware park command and the 0x0D fault-code split in the same image** | The normal acquisition path; the pure policy and its vectors unchanged; the tractor twin fixed; no host re-arm needed; leg-6 replay 100/100 (median 17.9 s, 99.8 % heard); 2 fps cold starts not locked by 120 s 6.7 % → 1.3 % | Both boards scan forever once the daemons stop, so the bench park needs the firmware command; continuous RX instead of STANDBY; one 0x0D every 30 s while unacquired; moves the tractor's decay window (below) | +| C. Host watchdog re-sends `REG_PROFILE=1` on a final 0x0D | No flash | Never usable on the tractor (it restarts its grid at epoch 0); FAILED stays absorbing between firings; the tractor twin stays | +| D. A longer dwell alone (d2 or d10) | Fewer cold starts end in FAILED | Does not recover from FAILED (leg-6 replay 0/100) | + +**PICK: B** (the memo's section 5 item 2). Picked by Dorkmo on 2026-10-24, with the decay-window trade-off of the memo's section 5 item 3 accepted: in the SIL (SIL-E3, 200 runs; the tractor with 2.5 s camera stalls every 9 s, before any FAILED) the image moves from main's authority on 22.1 % of passes, 53.2 % of commands received and 6.9 own-grid renumberings per run to 14.4 %, 59.1 % and 8.6 per run (each still up to 17 slots). What a renumbering costs the base's camera reception is not modelled. Closing the decay window itself stays the open RS-12.15 grid-policy change. What it means: + +* **Scan (`radio/sx1276_rx.c`):** a final FAIL holds FAILED only while the node has originator authority; any other node re-enters SCANNING at once, and a node in FAILED whose authority lapses restarts its scan on its next tick. Every later attempt still sends `RX_SCAN_FAILED` (0x0D) with the final bit, one every 30 s until a LOCK: the 30 s FAIL means "report and keep scanning (receive only)". The pure policy (`radio/sx1276_rx_scan_policy.c`) is unchanged. L: a LOCK starts the 2 s loss timer at the locking frame. d2: `SX1276_RX_SCAN_DWELL_MS` 500 → 2000 ms, a fixed constant (a 50-channel sweep takes 100 s instead of 25 s). +* **Park command (RS-12.21):** `HOST_TYPE_RADIO_PARK_REQ` 0x04, answered by `RADIO_PARK_URC` 0x84 {status 0 PARKED, 1 already parked, 2 refused; RegOpMode readback}. A `host_parked` flag gates every RX arm path and the modem sleeps until a host raw RXCONT write, a profile activation or a reset; a TX_FRAME_REQ while parked gets the ERR_PROTO refusal. VER/READY capability bit 7 (`HOST_CAP_RADIO_PARK`) marks the image. +* **Fault codes:** `RX_SCAN_FAILED` keeps 0x0D and `TX_FIFO_RB_MISMATCH` moves to 0x0F, with a compile-time duplicate check of the `HOST_FAULT_CODE_*` values. +* **Wire versions:** `HOST_PROTOCOL_VER` and `HOST_WIRE_SCHEMA_VER` unchanged: an additive request/URC pair and a fault-code value move, per the 2026-05-19 §13.1 #7 contract; hosts tell the images apart by capability bit 7. +* **Power:** continuous RX instead of STANDBY while unacquired. Fine for the base and the tractor; a battery node would want the back-off that [the 2026-05-19 analysis](../AI%20NOTES/2026-05-19_RX_Scan_FAILED_State_Analysis_Copilot_v1_0.md) §13.2 deferred. This pick replaces that analysis's absorbing-FAILED contract. +* **Status:** implemented 2026-10-25 on branch `fhss-scan-recovery-2026-10-24` and pinned in the SIL (`check-rx-scan-failed-follower`, `check-radio-park`, `check-rx-reverse-delivery`); built (bench `0daae27978089eb97ab35c18718b62b0`, production `2c5e055abff95126309a62409fafc654`), **not flashed**. The flash and the first radio checks (memo section 6.1) each need the operator's GO. diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/DESIGN-LORAFIRMWARE/04_Hardware_Interface_and_Recovery.md b/LifeTrac-v25/DESIGN-CONTROLLER/DESIGN-LORAFIRMWARE/04_Hardware_Interface_and_Recovery.md index 5c2c3964..4c91ebd3 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/DESIGN-LORAFIRMWARE/04_Hardware_Interface_and_Recovery.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/DESIGN-LORAFIRMWARE/04_Hardware_Interface_and_Recovery.md @@ -75,6 +75,8 @@ Properties: | `0x02` | H→L | `UID_REQ` | request L072 96-bit unique device ID | | `0x82` | L→H | `UID_URC` | reply with UID | | `0x03` | H→L | `RESET` | soft reset request (firmware does an orderly NVIC_SystemReset) | +| `0x04` | H→L | `RADIO_PARK_REQ` | firmware radio park (RS-12.21, 2026-10-24): modem to LoRa SLEEP, no RX arm path runs and TX is refused until a raw RXCONT write, a profile activation or a reset; refused while a TX is on air or queued (contract in `host_types.h`) | +| `0x84` | L→H | `RADIO_PARK_URC` | reply: `{status 0 PARKED / 1 ALREADY / 2 REFUSED, RegOpMode readback}` | | `0x10` | H→L | `TX_FRAME` | transmit a v25 frame; payload is the on-air bytes (already encrypted if H7 owns crypto) plus a small header (channel hint, SF hint, profile hint, priority) | | `0x90` | L→H | `TX_DONE_URC` | TX completed; payload includes timing metadata | | `0x91` | L→H | `RX_FRAME_URC` | a frame was received; payload includes radio metadata + bytes | @@ -87,7 +89,7 @@ Properties: | `0x41` | H→L | `STATS_DUMP` | request counter dump | | `0xC1` | L→H | `STATS_URC` | counter dump payload | | `0xF0` | L→H | `BOOT_URC` | sent once when the L072 finishes boot; carries safe-mode-was-considered flag, golden-jump-was-considered flag | -| `0xF1` | L→H | `FAULT_URC` | unsolicited fault notification | +| `0xF1` | L→H | `FAULT_URC` | unsolicited fault notification; codes `HOST_FAULT_CODE_*` in `host_types.h` (0x0D RX_SCAN_FAILED; TX_FIFO_RB_MISMATCH moved 0x0D → 0x0F on 2026-10-24, the 0x0D split) | | `0xFE` | L→H | `ERR_PROTO` | host sent something we don't understand; carries the offending type and ver | | `0xFF` | both | `RESERVED` | never sent | diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/SETTINGS_REFERENCE.md b/LifeTrac-v25/DESIGN-CONTROLLER/SETTINGS_REFERENCE.md index 3040238c..0473638a 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/SETTINGS_REFERENCE.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/SETTINGS_REFERENCE.md @@ -90,10 +90,10 @@ SX1276 registers, modem geometry, pin maps, LBT, RX scan/retune, and the Python- | LORA_FW_DEEP_SLEEP_BUILD | `1` | 0/1 | `firmware/murata_l072/config.h:37` | live | Seeds CFG key 0x08 only; no deep-sleep scheduler exists in the tree. | | LORA_FW_BEACON_ENABLE | `1` | 0/1 | `firmware/murata_l072/config.h:40` | live | Seeds CFG key 0x09 only; grep finds no beacon implementation. | | DEFAULT_BEACON_CHANNEL_IDX | `0U` | validator 0..7 | `firmware/murata_l072/host/host_cfg.c:30` | live | Stored and reportable; no beacon code consumes it. | -| HOST_TYPE_REG_READ_REQ | `0x30` | payload_len must be 1 | `firmware/murata_l072/include/host_types.h:36` | live | Raw SX1276 register read of any address — no allow-list on reads. | -| HOST_TYPE_REG_WRITE_REQ | `0x31` | allow-list {01,06,07,08,09,1D,1E,26,31,33,37,3B,40} | `firmware/murata_l072/include/host_types.h:37` | live | The backdoor the daemons use to pin RegFrf and re-arm RXCONT; 0x01 writes fold into the mode SM. | -| HOST_TYPE_REG_DATA_URC | `0xB0` | n/a | `firmware/murata_l072/include/host_types.h:54` | live | 2-byte {addr, value} answer to REG_READ_REQ. | -| HOST_TYPE_REG_WRITE_ACK_URC | `0xB1` | n/a | `firmware/murata_l072/include/host_types.h:55` | live | 2-byte echo of an accepted REG_WRITE_REQ. | +| HOST_TYPE_REG_READ_REQ | `0x30` | payload_len must be 1 | `firmware/murata_l072/include/host_types.h:83` | live | Raw SX1276 register read of any address — no allow-list on reads. | +| HOST_TYPE_REG_WRITE_REQ | `0x31` | allow-list {01,06,07,08,09,1D,1E,26,31,33,37,3B,40} | `firmware/murata_l072/include/host_types.h:84` | live | The backdoor the daemons use to pin RegFrf and re-arm RXCONT; 0x01 writes fold into the mode SM. | +| HOST_TYPE_REG_DATA_URC | `0xB0` | n/a | `firmware/murata_l072/include/host_types.h:115` | live | 2-byte {addr, value} answer to REG_READ_REQ. | +| HOST_TYPE_REG_WRITE_ACK_URC | `0xB1` | n/a | `firmware/murata_l072/include/host_types.h:116` | live | 2-byte echo of an accepted REG_WRITE_REQ. | | LIFETRAC_FORCE_FRF_HZ | unset; auto-pins 915000000 for profile 0 or 2 | integer Hz; non-integer warned and ignored | `firmware/x8_lora_bootloader_helper/method_h_stage2_tx_probe_v2.py:522` | live | Force-writes RegFrf after standby; works around the firmware not retuning on a same-id profile set. | | RXCONT arm / re-arm (tractor) | `RegOpMode <- 0x85`, read/write timeout 0.5 s | n/a | `firmware/tractor_x8/image_tx_daemon.py:512` | live | Raw opmode write so the tractor can hear 0xFB frames; bypasses firmware, so every TX parks STANDBY and it must re-arm (`:636`). | | read_reg/write_reg RegOpMode timeout (base) | 0.5 s | unconstrained | `base_station/image_rx_daemon.py:874` | live | Timeout on the raw RegOpMode read/write used to re-arm RXCONT. | @@ -258,9 +258,9 @@ without first widening the mask fails with `MASK_POPCOUNT` (wire status 8). Both | HOST_RFCO_SUMMARY_CHANNEL_COUNT | `50U` | pinned == SX1276_FHSS_CHANNEL_COUNT | `firmware/murata_l072/include/host_rfco_summary.h:92` | live | Channel-table width for hop counts and dwell peaks; the 64-entry dwell array is truncated to 50. | | HOST_RFCO_SUMMARY_REASON_SLOTS | `8U` | `_Static_assert >= 7U` | `firmware/murata_l072/include/host_rfco_summary.h:93` | live | Blocked-attempt reason buckets in the SUMMARY payload. | | HOST_RFCO_SUMMARY_LAST_CLAMP_REASON_NONE | `0xFF` | compile-time | `firmware/murata_l072/include/host_rfco_summary.h:117` | live | "No non-OK TX this window" sentinel; duplicated as `HOST_RFCO_LAST_CLAMP_NONE` at `host_rfco.c:125`. | -| HOST_TYPE_AIRTIME_INVARIANT_REJECT_URC | `0xC2`, 15-byte payload | HOST_AIRTIME_REJECT_PAYLOAD_LEN = 15 | `firmware/murata_l072/include/host_types.h:59` | live | {SF, CR, BW, payload_len, computed ToA, cap} emitted from `sx1276.c:442`. | -| HOST_TYPE_RFCO_PERTX_URC | `0xC3`, 21-byte payload | schema_ver 1 | `firmware/murata_l072/include/host_types.h:93` | live | Per-TX compliance snapshot emitted from `sx1276_tx.c:100`. | -| HOST_TYPE_RFCO_SUMMARY_URC | `0xC4`, 191-byte payload | schema_ver 1 | `firmware/murata_l072/include/host_types.h:117` | live | Per-minute summary: 50-bucket hop histogram, 50 dwell peaks, 8 reason counters, trailing CRC-16. | +| HOST_TYPE_AIRTIME_INVARIANT_REJECT_URC | `0xC2`, 15-byte payload | HOST_AIRTIME_REJECT_PAYLOAD_LEN = 15 | `firmware/murata_l072/include/host_types.h:120` | live | {SF, CR, BW, payload_len, computed ToA, cap} emitted from `sx1276.c:442`. | +| HOST_TYPE_RFCO_PERTX_URC | `0xC3`, 21-byte payload | schema_ver 1 | `firmware/murata_l072/include/host_types.h:154` | live | Per-TX compliance snapshot emitted from `sx1276_tx.c:100`. | +| HOST_TYPE_RFCO_SUMMARY_URC | `0xC4`, 191-byte payload | schema_ver 1 | `firmware/murata_l072/include/host_types.h:178` | live | Per-minute summary: 50-bucket hop histogram, 50 dwell peaks, 8 reason counters, trailing CRC-16. | | per-region conditional | none | n/a | `firmware/murata_l072/include/host_cfg_keys.h:43` | **dead** | There is **no** region/locale abstraction in the L072 tree. Grep for region/ETSI/EU868/868/ARIB returns only flash-region hits. | | comm.lora_region | `"us915"` | enum us915 / eu868 / au915 | `base_station/config/build.default.toml:60` | live | The fleet-wide regulatory region selector; codegens `LIFETRAC_COMM_LORA_REGION_*` into `firmware/common/lifetrac_build_config.h:69`. Does not reach the L072 profile machinery. | | CMD_OP_RADIO_PROFILE | `0x65` | profile 0..2 | `base_station/lora_proto.py:958` | live | Strict-path 0xFB opcode; base commands the tractor to switch profile (phase A). | @@ -420,7 +420,7 @@ RFCO compliance-URC constants live in [§2](#2-regulatory--fhss). | CFG_KEY_WIRE_SCHEMA_VERSION (0x81) | `2` | read-only | `firmware/murata_l072/host/host_cfg.c:138` | live | Payload-schema version. **`tools/mh_wire_constants.py:54` still mirrors 1 — the two peers disagree.** | | CFG_KEY_CFG_DIRTY (0x82) | `0`; cfg_get special-cases it to the live flag (`host_cfg.c:425`) | read-only | `firmware/murata_l072/host/host_cfg.c:140` | diagnostic | Latches 1 the first time any key is set to a non-default value; cleared only by `cfg_init()`. | | cfg profile initial active state | profile 0, mask 0xFF, bw 125000, gain 2, ceiling 17 | n/a | `firmware/murata_l072/host/host_cfg.c:349` | live | Seed for `host_cfg_profile_reset()` at cfg_init. **Never run through the validator.** | -| cfg_status_t | 0 OK … 14 PROFILE_REJECT_NULL_ARG | 0..14 | `firmware/murata_l072/include/host_cfg.h:7` | live | Status byte in CFG_OK_URC byte 1. `host_types.h:201` only defines HOST_CFG_STATUS_* for 0..6; 7..14 exist solely here. | +| cfg_status_t | 0 OK … 14 PROFILE_REJECT_NULL_ARG | 0..14 | `firmware/murata_l072/include/host_cfg.h:7` | live | Status byte in CFG_OK_URC byte 1. `host_types.h:254` only defines HOST_CFG_STATUS_* for 0..6; 7..14 exist solely here. | | CFG_STATUS_PROFILE_* wire codes | UNROUTED 7 … NULL_ARG 14 | 0..14 | `firmware/murata_l072/include/host_cfg.h:46` | live | Additive extension returned by `cfg_set(0x14)`. | | HOST_CFG_OK_PAYLOAD_LEN | `4U` | fixed wire contract | `firmware/murata_l072/include/host_cfg_wire.h:9` | live | CFG_OK_URC payload length. | | HOST_CFG_DATA_HEADER_LEN | `2U` | fixed wire contract | `firmware/murata_l072/include/host_cfg_wire.h:10` | live | CFG_DATA_URC header preceding value bytes. | @@ -431,32 +431,34 @@ RFCO compliance-URC constants live in [§2](#2-regulatory--fhss). | Setting | Default | Range | Where | Status | Purpose | | --- | --- | --- | --- | --- | --- | -| HOST_PROTOCOL_VER | `1U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:6` | live | Stamped into every outbound frame; enforced inbound at `host_cmd.c:771`. | -| HOST_WIRE_SCHEMA_VER | `2U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:7` | live | Payload-schema version reported in BOOT/VER/READY URCs and CFG key 0x81. | -| HOST_CAPABILITY_BITMAP | `0x7F` (PING, VERSION, UID, REG_IO, STATS, RESET, TX_FRAME) | bits 0..6 | `firmware/murata_l072/include/host_types.h:17` | live | Advertised in VER/BOOT/READY. Static — does **not** track HOST_ALLOW_REG_WRITE_DIAG, so REG_IO is advertised even when writes are refused. | -| HOST_TYPE_PING_REQ | `0x00` | n/a | `firmware/murata_l072/include/host_types.h:26` | live | Echoes payload/seq/flags back under the same type code. | -| HOST_TYPE_VER_REQ | `0x01` | n/a | `firmware/murata_l072/include/host_types.h:27` | live | Triggers VER_URC 0x81. | -| HOST_TYPE_UID_REQ | `0x02` | n/a | `firmware/murata_l072/include/host_types.h:28` | live | Returns the 96-bit STM32 UID as UID_URC 0x82. | -| HOST_TYPE_RESET_REQ | `0x03` | n/a | `firmware/murata_l072/include/host_types.h:29` | live | Immediate `platform_system_reset()` with no ack; resets all CFG state via `cfg_init()`. Both daemons send it at link open unless LIFETRAC_SKIP_RESET_REQ is set. | -| HOST_TYPE_TX_FRAME_REQ | `0x10` | payload_len must equal 2 + length | `firmware/murata_l072/include/host_types.h:31` | live | {tx_id, length, body}; parks into the depth-4 ring when busy or the hop slot cannot fit, ERR_PROTO QUEUE_FULL when full. | -| HOST_TYPE_CFG_SET_REQ | `0x20` | payload_len must equal 2 + len | `firmware/murata_l072/include/host_types.h:33` | live | The only opcode that changes configuration; answered by CFG_OK_URC 0xA0. | -| HOST_TYPE_CFG_GET_REQ | `0x21` | payload_len must be 1 | `firmware/murata_l072/include/host_types.h:34` | live | Answers CFG_DATA_URC 0xA1 on success, CFG_OK_URC carrying the error otherwise. | -| HOST_TYPE_STATS_RESET_REQ | `0x40` | n/a | `firmware/murata_l072/include/host_types.h:39` | live | Zeroes every radio and UART counter; sends no reply. | -| HOST_TYPE_STATS_DUMP_REQ | `0x41` | n/a | `firmware/murata_l072/include/host_types.h:40` | live | Emits STATS_URC 0xC1 with the counter block: 132 B originally, additive tails since — RS-11.5 → 144 (tx_fifo_rb_ok/bad, tx_done_early), RS-12 → 152 (rx_urc_lost, rx_pretx_drained), RS-12.10 → 168 (rx_fifo_skip, tx_deaf_max_us, tx_deaf_sum_us, tx_done_to_rearm_max_us), RS-12.15 → 208 (the six-entry `fhss_dec_*` consider_remote decision histogram, clk_demotion_reset, clk_demotion_kept, tx_first_anchor, tx_stream_streak_max). Parsers are offset-guarded; `check-stats-layout` pins the producer. | -| HOST_TYPE_VER_URC | `0x81` | name truncated at 40 B, git at 16 B | `firmware/murata_l072/include/host_types.h:43` | live | {proto, schema, maj, min, patch, name_len, git_len, 0, cap_u32le, name, git}. | -| HOST_TYPE_UID_URC | `0x82` | n/a | `firmware/murata_l072/include/host_types.h:44` | live | 12-byte STM32 unique id, three u32 LE. | -| HOST_TYPE_TX_DONE_URC | `0x90` | status 0 OK, 1 TIMEOUT, 2 LBT_ABORT, 3 BUSY | `firmware/murata_l072/include/host_types.h:46` | live | 7-byte {tx_id, status, time_on_air_us, tx_power_dbm}; seq always 0. | -| HOST_TYPE_RX_FRAME_URC | `0x91` | 8-byte header | `firmware/murata_l072/include/host_types.h:49` | live | {len, snr_db, rssi_dbm, timestamp_us, payload}. | -| HOST_TYPE_CFG_OK_URC | `0xA0` | status byte 0..14 | `firmware/murata_l072/include/host_types.h:51` | live | Fixed 4-byte {key, status, actual_len, 0}. **A rejection arrives on this same type — the host must inspect byte 1, not trust the type match.** | -| HOST_TYPE_CFG_DATA_URC | `0xA1` | value_len <= 8 | `firmware/murata_l072/include/host_types.h:52` | live | {key, value_len, value}. | -| HOST_TYPE_RADIO_IRQ_URC | `0xC0` | n/a | `firmware/murata_l072/include/host_types.h:57` | diagnostic | 4-byte radio-event bitmask; compile-gated behind HOST_EMIT_RADIO_IRQ_DEBUG_URC (0), so production never emits it. | -| HOST_TYPE_STATS_URC | `0xC1` | HOST_STATS_PAYLOAD_LEN = 208 (RS-12.15) | `firmware/murata_l072/include/host_types.h:58` | live | 52 u32 LE fields, additive-only (132 B / 33 fields originally — see the STATS_DUMP_REQ row for the tail history). | -| HOST_TYPE_BOOT_URC | `0xF0` | n/a | `firmware/murata_l072/include/host_types.h:157` | live | 6-byte {reset_cause, radio_ok, radio_version, proto_ver, schema_ver, clock_source_id}, sent first in `host_cmd_init`. | -| HOST_TYPE_READY_URC | `0xF2` | n/a | `firmware/murata_l072/include/host_types.h:160` | live | 8-byte final boot handshake {0x01, proto, schema, at_shell_enabled, capability_bitmap}. | -| HOST_TYPE_ERR_PROTO_URC | `0xFE` | err 1 BAD_VERSION … 8 FORBIDDEN | `firmware/murata_l072/include/host_types.h:161` | live | 5-byte {offending_type, offending_ver, err_code, detail}. | -| HOST_STATS_PAYLOAD_LEN | `208U` (RS-12.15; was 132U) | `_Static_assert` pins `sizeof(host_stats_wire_t)`; `tools/check_mh_wire_sync.py` pins the H7 mirror | `firmware/murata_l072/include/host_types.h:220` | live | Serialized STATS_URC payload size. | -| HOST_FAULT_URC_PAYLOAD_LEN | `24U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:163` | live | FAULT_URC payload size. | -| HOST_AIRTIME_REJECT_PAYLOAD_LEN | `15U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:77` | live | AIRTIME_INVARIANT_REJECT_URC payload size. | +| HOST_PROTOCOL_VER | `1U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:19` | live | Stamped into every outbound frame; enforced inbound at `host_cmd.c:771`. | +| HOST_WIRE_SCHEMA_VER | `2U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:20` | live | Payload-schema version reported in BOOT/VER/READY URCs and CFG key 0x81. Additive changes keep it and HOST_PROTOCOL_VER (the version contract above them in host_types.h): new types, appended tails, fault codes — so RS-12.21's RADIO_PARK_REQ/URC and the 0x0D split (2026-10-24) bumped neither. | +| HOST_CAPABILITY_BITMAP | `0xFF` (PING, VERSION, UID, REG_IO, STATS, RESET, TX_FRAME, RADIO_PARK); `0x7F` before 2026-10-24 | bits 0..7 | `firmware/murata_l072/include/host_types.h:34` | live | Advertised in VER/READY (not BOOT). Static — does **not** track HOST_ALLOW_REG_WRITE_DIAG, so REG_IO is advertised even when writes are refused. Bit 7 (HOST_CAP_RADIO_PARK, RS-12.21) also marks an image with the 0x0D split (see HOST_TYPE_FAULT_URC). | +| HOST_TYPE_PING_REQ | `0x00` | n/a | `firmware/murata_l072/include/host_types.h:44` | live | Echoes payload/seq/flags back under the same type code. | +| HOST_TYPE_VER_REQ | `0x01` | n/a | `firmware/murata_l072/include/host_types.h:45` | live | Triggers VER_URC 0x81. | +| HOST_TYPE_UID_REQ | `0x02` | n/a | `firmware/murata_l072/include/host_types.h:46` | live | Returns the 96-bit STM32 UID as UID_URC 0x82. | +| HOST_TYPE_RESET_REQ | `0x03` | n/a | `firmware/murata_l072/include/host_types.h:47` | live | Immediate `platform_system_reset()` with no ack; resets all CFG state via `cfg_init()`. Both daemons send it at link open unless LIFETRAC_SKIP_RESET_REQ is set. | +| HOST_TYPE_RADIO_PARK_REQ | `0x04` | payload none (ignored) | `firmware/murata_l072/include/host_types.h:76` | live (RS-12.21, 2026-10-24; not yet flashed) | Firmware radio park, the bench's persistent radio-off state: sets the host_parked flag, puts the modem to LoRa SLEEP; while set no path arms RX (scan machine frozen, slot follower, gamma-1, post-TX re-arm, `sx1276_rx_arm()`) and TX_FRAME_REQ is refused (ERR_PROTO FORBIDDEN, detail 0). Refused while a TX is on air or queued. Ended by a raw REG_WRITE 0x01=0x85 (RXCONT), a profile activation (CFG_SET REG_PROFILE) or any reset; raw STANDBY/SLEEP writes do not end it. Answered by RADIO_PARK_URC 0x84. Pinned by `check-radio-park`. | +| HOST_TYPE_TX_FRAME_REQ | `0x10` | payload_len must equal 2 + length | `firmware/murata_l072/include/host_types.h:78` | live | {tx_id, length, body}; parks into the depth-4 ring when busy or the hop slot cannot fit, ERR_PROTO QUEUE_FULL when full. | +| HOST_TYPE_CFG_SET_REQ | `0x20` | payload_len must equal 2 + len | `firmware/murata_l072/include/host_types.h:80` | live | The only opcode that changes configuration; answered by CFG_OK_URC 0xA0. | +| HOST_TYPE_CFG_GET_REQ | `0x21` | payload_len must be 1 | `firmware/murata_l072/include/host_types.h:81` | live | Answers CFG_DATA_URC 0xA1 on success, CFG_OK_URC carrying the error otherwise. | +| HOST_TYPE_STATS_RESET_REQ | `0x40` | n/a | `firmware/murata_l072/include/host_types.h:86` | live | Zeroes every radio and UART counter; sends no reply. | +| HOST_TYPE_STATS_DUMP_REQ | `0x41` | n/a | `firmware/murata_l072/include/host_types.h:87` | live | Emits STATS_URC 0xC1 with the counter block: 132 B originally, additive tails since — RS-11.5 → 144 (tx_fifo_rb_ok/bad, tx_done_early), RS-12 → 152 (rx_urc_lost, rx_pretx_drained), RS-12.10 → 168 (rx_fifo_skip, tx_deaf_max_us, tx_deaf_sum_us, tx_done_to_rearm_max_us), RS-12.15 → 208 (the six-entry `fhss_dec_*` consider_remote decision histogram, clk_demotion_reset, clk_demotion_kept, tx_first_anchor, tx_stream_streak_max). Parsers are offset-guarded; `check-stats-layout` pins the producer. | +| HOST_TYPE_VER_URC | `0x81` | name truncated at 40 B, git at 16 B | `firmware/murata_l072/include/host_types.h:90` | live | {proto, schema, maj, min, patch, name_len, git_len, 0, cap_u32le, name, git}. | +| HOST_TYPE_UID_URC | `0x82` | n/a | `firmware/murata_l072/include/host_types.h:91` | live | 12-byte STM32 unique id, three u32 LE. | +| HOST_TYPE_RADIO_PARK_URC | `0x84` | status 0 PARKED, 1 ALREADY_PARKED, 2 REFUSED | `firmware/murata_l072/include/host_types.h:93` | live (RS-12.21, 2026-10-24; not yet flashed) | 2-byte {status, RegOpMode read back after the request (0x80 = LoRa SLEEP)}, same seq as the request. ALREADY re-applies SLEEP; REFUSED (a TX on air or queued) changes nothing. | +| HOST_TYPE_TX_DONE_URC | `0x90` | status 0 OK, 1 TIMEOUT, 2 LBT_ABORT, 3 BUSY | `firmware/murata_l072/include/host_types.h:99` | live | 7-byte {tx_id, status, time_on_air_us, tx_power_dbm}; seq always 0. | +| HOST_TYPE_RX_FRAME_URC | `0x91` | 8-byte header | `firmware/murata_l072/include/host_types.h:108` | live | {len, snr_db, rssi_dbm, timestamp_us, payload}. | +| HOST_TYPE_CFG_OK_URC | `0xA0` | status byte 0..14 | `firmware/murata_l072/include/host_types.h:112` | live | Fixed 4-byte {key, status, actual_len, 0}. **A rejection arrives on this same type — the host must inspect byte 1, not trust the type match.** | +| HOST_TYPE_CFG_DATA_URC | `0xA1` | value_len <= 8 | `firmware/murata_l072/include/host_types.h:113` | live | {key, value_len, value}. | +| HOST_TYPE_RADIO_IRQ_URC | `0xC0` | n/a | `firmware/murata_l072/include/host_types.h:118` | diagnostic | 4-byte radio-event bitmask; compile-gated behind HOST_EMIT_RADIO_IRQ_DEBUG_URC (0), so production never emits it. | +| HOST_TYPE_STATS_URC | `0xC1` | HOST_STATS_PAYLOAD_LEN = 208 (RS-12.15) | `firmware/murata_l072/include/host_types.h:119` | live | 52 u32 LE fields, additive-only (132 B / 33 fields originally — see the STATS_DUMP_REQ row for the tail history). | +| HOST_TYPE_BOOT_URC | `0xF0` | n/a | `firmware/murata_l072/include/host_types.h:284` | live | 6-byte {reset_cause, radio_ok, radio_version, proto_ver, schema_ver, clock_source_id}, sent first in `host_cmd_init`. | +| HOST_TYPE_READY_URC | `0xF2` | n/a | `firmware/murata_l072/include/host_types.h:287` | live | 8-byte final boot handshake {0x01, proto, schema, at_shell_enabled, capability_bitmap}. | +| HOST_TYPE_ERR_PROTO_URC | `0xFE` | err 1 BAD_VERSION … 8 FORBIDDEN | `firmware/murata_l072/include/host_types.h:288` | live | 5-byte {offending_type, offending_ver, err_code, detail}. | +| HOST_STATS_PAYLOAD_LEN | `208U` (RS-12.15; was 132U) | `_Static_assert` pins `sizeof(host_stats_wire_t)`; `tools/check_mh_wire_sync.py` pins the H7 mirror | `firmware/murata_l072/include/host_types.h:282` | live | Serialized STATS_URC payload size. | +| HOST_FAULT_URC_PAYLOAD_LEN | `24U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:290` | live | FAULT_URC payload size. | +| HOST_AIRTIME_REJECT_PAYLOAD_LEN | `15U` | fixed wire contract | `firmware/murata_l072/include/host_types.h:138` | live | AIRTIME_INVARIANT_REJECT_URC payload size. | | AT shell command set | `AT, ATI, AT+VER?, AT+RADIO?, AT+STAT?, AT+HELP, AT+HELP?, AT+BIN` | exact-match strings | `firmware/murata_l072/host/host_cmd.c:839` | diagnostic | Case-insensitive read-only shell; none of these change configuration. Unrecognised lines answer `ERROR\r\n`. | | STM32_UID_BASE | `0x1FF80050UL` | fixed silicon address | `firmware/murata_l072/host/host_cmd.c:18` | live | Base of the 96-bit unique device ID read for UID_URC. | | TOPIC_* telemetry ids | `TOPIC_GPS 0x01` … `TOPIC_SEMANTIC_MAP 0x2A` | uint8; 0x2A reserved for v26 | `firmware/common/lora_proto/lora_proto.h:57` | live | Full over-air telemetry topic space. 0x2A must not be emitted in v25. | @@ -926,7 +928,7 @@ Deadmen, watchdogs, settle delays, profile-switch windows, and the build-config | PLATFORM_HSE_READY_TIMEOUT | `800000UL` | unitless busy-wait loop count, **not milliseconds** | `firmware/murata_l072/hal/platform.c:7` | live | HSERDY wait during clock bring-up; exhaustion drops to HSI16 and later raises `CLOCK_HSE_FAILED` from `main.c:89`. | | IWDG_BOOT_WINDOW_MS | `500U` | unconstrained | `firmware/murata_l072/config.h:108` | **dead** | Intended boot watchdog window (N-20). Zero references anywhere; there is **no IWDG driver at all** — no IWDG_KR/PR/RLR writes exist outside the reset-cause read at `hal/platform.c:28`. | | IWDG_RUN_WINDOW_MS | `100U` | CFG validator 50..5000 | `firmware/murata_l072/config.h:109` | live | Its only consumer is the CFG_KEY_IWDG_WINDOW_MS default bytes at `host_cfg.c:121`. Reported over CFG_GET, never programmed into hardware. | -| HOST_TYPE_FAULT_URC | `0xF1`, 24-byte payload | fault codes 0x01..0x0D (`host_types.h:165`); 0x09 and 0x0C are benign diagnostics | `firmware/murata_l072/include/host_types.h:159` | live | {code, sub, rsvd, pc, lr, psr, bfar, uptime_ms}. | +| HOST_TYPE_FAULT_URC | `0xF1`, 24-byte payload | fault codes 0x01..0x0F (`host_types.h:292`); 0x09 and 0x0C are benign diagnostics | `firmware/murata_l072/include/host_types.h:286` | live | {code, sub, rsvd, pc, lr, psr, bfar, uptime_ms}. 0x0D RX_SCAN_FAILED (sub = final/warm/hw_suspect/crc_seen/attempt bits; since RS-12.21 (b) a final one repeats every 30 s until a LOCK), 0x0E TX_DONE_EARLY and 0x0F TX_FIFO_RB_MISMATCH (sub = tx_id). **The 0x0D split (2026-10-24):** TX_FIFO_RB_MISMATCH was 0x0D too, colliding with RX_SCAN_FAILED from 2026-08-02; it moved to 0x0F, and `host_fault_code_compile_time_uniqueness_check()` now refuses a duplicate code at build time. On an older image a 0x0D whose sub is not a valid scan encoding is its FIFO fault; an image advertising HOST_CAP_RADIO_PARK has the split. | | _PROFILE_TO_BUDGET_US | `{0: 380000, 1: 860000, 2: 930000}` µs per 1 s | per-profile constant | `firmware/tractor_x8/image_tx_daemon.py:195` | live | Host mirror of the firmware QoS gate, deliberately under the firmware caps (400000/400000/950000) so a paced TX can never draw ABORT_QOS. | | AirtimeBudget.admit poll clamp | `min(max(wait, 0.005), 0.25)` | n/a | `firmware/tractor_x8/image_tx_daemon.py:272` | live | Bounds how long `admit()` sleeps: at least 5 ms, at most 250 ms, so shutdown stays responsive. | | PER_FRAGMENT_TX_TIMEOUT_S | `3.0` s | n/a | `firmware/tractor_x8/image_tx_daemon.py:179` | live | Per-fragment TX_DONE deadline on the serial path (`:1002`) and the in-flight watchdog on v3 (`:1132`); a miss is charged to the airtime budget as RF-spent. | @@ -1125,7 +1127,7 @@ what its name suggests. "Dead" means changing it has *no runtime effect at all*. | bench mirror tables (SF/BW/CR, ToA vectors, chantab vectors, slot-clock pins) | `firmware/murata_l072/bench/host_proto/airtime_invariant.c:56`, `:168`, `fhss_chantab_vectors.c:37`, `fhss_clock_test.c:100` | Golden pins that fail the host check build on drift; they do not run on target. | | lora_ping.c standalone PHY | `firmware/murata_l072/lora_ping.c:315` | Separate bring-up binary, not linked into `firmware.bin`. Its BW is 125 kHz, unlike production 250. | | RADIO_MINIMIZER_DEFAULT / RADIO_ACTIVE_TEST_DEFAULT / PING_ROLE | `firmware/murata_l072/include/dev_build_policy.h:14`, `:26`, `lora_ping.c:25` | Affect the ping images only; `dev_build_policy.h` is included solely by lora_ping.c. | -| HOST_EMIT_RADIO_IRQ_DEBUG_URC / HOST_TYPE_RADIO_IRQ_URC | `firmware/murata_l072/config.h:66`, `include/host_types.h:57` | Off by default since RS-3.7; production builds never emit the URC. | +| HOST_EMIT_RADIO_IRQ_DEBUG_URC / HOST_TYPE_RADIO_IRQ_URC | `firmware/murata_l072/config.h:66`, `include/host_types.h:118` | Off by default since RS-3.7; production builds never emit the URC. | | HOST_UART_TX_MIRROR_USART1 | `firmware/murata_l072/config.h:74` | Doubles blocking busy-wait per URC; off in production. | | HOST_UART_RX_ECHO_DIAG | `firmware/murata_l072/host/host_uart.c:19` | Stage-1 ingress echo; off in production. | | LIFETRAC_BENCH_BOOT_HEARTBEAT_ENABLE | `firmware/murata_l072/config.h:86` | ASCII boot markers; off in production. | diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/TODO.md b/LifeTrac-v25/DESIGN-CONTROLLER/TODO.md index 18c3b7dc..eb0bca8f 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/TODO.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/TODO.md @@ -2697,7 +2697,7 @@ conflicts. Every leg needs GO; radios stay parked between legs. setting a `host_parked` flag that gates every arm path, puts the modem to SLEEP, cleared by a host RXCONT write (`sx1276_rx_note_external_arm`) or reset; host-check pinned; flashed and verified at a GO. Until then - PARK_OK means "asleep now and nothing re-armed it", not "cannot wake". **Open, 2026-10-24 (FHSS leg 2b_f3, [RESULTS](bench-evidence/bench_session_2026-10-24/RESULTS.md), Anomalies): the base never LOCKED during a leg (counters); leading hypothesis (H1, unverified): its cold-start scan ran out into the same absorbing FAILED and the follower then never hopped.** Observed: its first frame came 138.0 s after its profile activation, past the 4 x 30 s scan budget (1 of 27 archived profile-1 legs past 120 s). The base heard 5 of the tractor's 606 frames, on channels consistent with a receiver parked on the walker's last channel and then on each PROBE's TX channel (44, 6, 6, 18, 18); by the firmware a follower in FAILED never hops (the slot follower needs LOCKED + an adopted grid, or originator authority). Base Δ`clk_demotion_reset` 0 and Δ`fhss_dec_aligned` 0 (never LOCKED); post `radio_state` 4 on both boards (2 on the other five legs); the tractor sent all 606 normally. The base's 0x0D faults are not logged (`image_rx_daemon.py` drops FAULT URCs), so the FAILED entry is inferred. SIL, 2026-10-24 (no radio): `check-rx-scan-failed-follower` (`firmware/murata_l072/bench/host_proto/rx_scan_failed_follower.c`, the real `sx1276_rx.c` / `sx1276_tx.c`, in `make check`) reproduces the mechanism under H1's premise (no frame for the 120 s budget) and pins it as a known defect: four 0x0D (sub 20 21 22 A3), FAILED, STANDBY on ch 9; then frames are adopted but never LOCK or hop, each later catch is on the previous PROBE's channel, never ALIGNED, ends in RX_CONT. That is a model, not a hardware observation: the base's faults are still unlogged. It also pins a tractor twin (RS-12.15): an originator in FAILED that adopts a command heard in a camera stall loses authority and self-follow for good. Walker model over the 27 legs: about 7-8 % per 2 fps one-fragment leg (4-10 % by leg), 21-31 % at 1 fps, under 1 % at 2.4+ TX/s; the base tick at 610-1400 ppm slow moves it by under 2 points; a six-leg 2 fps round loses a leg 34-40 % of the time (model output; the archive has 1 of 8 such legs). Options (a)-(d) with their SIL results, the 0x0D code collision and the HSI16 clock item: [FHSS_SCAN_FAILED_OPTIONS.md](firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md). Recommendation for the design owner: (b) + the loss-timer fix + a 2 s dwell, flashed with this row's park command (which (b) needs) and a new code for TX_FIFO_RB_MISMATCH; the fix and the dwell also change the tractor's RS-12.15 decay window (memo section 5). Next, no radio: log the base's FAULT URCs (0x0D decoded), the acquisition check in the leg report, a pass-rule clause for a leg whose base never locks. Radio only on a new GO (first: a forced-FAILED leg on main). + PARK_OK means "asleep now and nothing re-armed it", not "cannot wake". **Open, 2026-10-24 (FHSS leg 2b_f3, [RESULTS](bench-evidence/bench_session_2026-10-24/RESULTS.md), Anomalies): the base never LOCKED during a leg (counters); leading hypothesis (H1, unverified): its cold-start scan ran out into the same absorbing FAILED and the follower then never hopped.** Observed: its first frame came 138.0 s after its profile activation, past the 4 x 30 s scan budget (1 of 27 archived profile-1 legs past 120 s). The base heard 5 of the tractor's 606 frames, on channels consistent with a receiver parked on the walker's last channel and then on each PROBE's TX channel (44, 6, 6, 18, 18); by the firmware a follower in FAILED never hops (the slot follower needs LOCKED + an adopted grid, or originator authority). Base Δ`clk_demotion_reset` 0 and Δ`fhss_dec_aligned` 0 (never LOCKED); post `radio_state` 4 on both boards (2 on the other five legs); the tractor sent all 606 normally. The base's 0x0D faults are not logged (`image_rx_daemon.py` drops FAULT URCs), so the FAILED entry is inferred. SIL, 2026-10-24 (no radio): `check-rx-scan-failed-follower` (`firmware/murata_l072/bench/host_proto/rx_scan_failed_follower.c`, the real `sx1276_rx.c` / `sx1276_tx.c`, in `make check`) reproduces the mechanism under H1's premise (no frame for the 120 s budget) and pins it as a known defect: four 0x0D (sub 20 21 22 A3), FAILED, STANDBY on ch 9; then frames are adopted but never LOCK or hop, each later catch is on the previous PROBE's channel, never ALIGNED, ends in RX_CONT. That is a model, not a hardware observation: the base's faults are still unlogged. It also pins a tractor twin (RS-12.15): an originator in FAILED that adopts a command heard in a camera stall loses authority and self-follow for good. Walker model over the 27 legs: about 7-8 % per 2 fps one-fragment leg (4-10 % by leg), 21-31 % at 1 fps, under 1 % at 2.4+ TX/s; the base tick at 610-1400 ppm slow moves it by under 2 points; a six-leg 2 fps round loses a leg 34-40 % of the time (model output; the archive has 1 of 8 such legs). Options (a)-(d) with their SIL results, the 0x0D code collision and the HSI16 clock item: [FHSS_SCAN_FAILED_OPTIONS.md](firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md). Recommendation for the design owner: (b) + the loss-timer fix + a 2 s dwell, flashed with this row's park command (which (b) needs) and a new code for TX_FIFO_RB_MISMATCH; the fix and the dwell also change the tractor's RS-12.15 decay window (memo section 5). Next, no radio: log the base's FAULT URCs (0x0D decoded), the acquisition check in the leg report, a pass-rule clause for a leg whose base never locks. Radio only on a new GO (first: a forced-FAILED leg on main). **Built 2026-10-25, NOT flashed** (the design owner's decision of 2026-10-24, [DECISIONS.md](DECISIONS.md) D-FHSS1: memo section 5 item 2, with the decay-window trade-off of item 3 accepted; branch `fhss-scan-recovery-2026-10-24`): scan option (b) + L + d2; this row's firmware park command, `HOST_TYPE_RADIO_PARK_REQ` 0x04 answered by `RADIO_PARK_URC` 0x84 {status 0 PARKED / 1 already / 2 refused, RegOpMode} (a `host_parked` flag gates every RX arm path and SLEEPs the modem; a host raw RXCONT, a profile activation or a reset ends it; a TX_FRAME_REQ while parked gets ERR_PROTO; VER/READY capability bit 7); and the 0x0D split (TX_FIFO_RB_MISMATCH on 0x0F, a compile-time duplicate check of the fault codes). `HOST_PROTOCOL_VER` / `HOST_WIRE_SCHEMA_VER` unchanged: additive changes (host_types.h). Pinned by `check-radio-park`, `check-rx-scan-failed-follower` and `check-rx-reverse-delivery`; host side `radio_park.py` (the firmware path by the VER bit, a 2.5 s settle) and `rs12_leg_report.py` / `rs13_probe_score.py` (`--scan-image`: on this image a final 0x0D is a 30 s report). Bench `0daae27978089eb97ab35c18718b62b0`, production `2c5e055abff95126309a62409fafc654`, 25 324 B each (the same from a clean copy of the tree and after `mingw32-make clean`). Both boards stay on `e083ae46` until the flash GO. Flash and verify plan for the next radio day, each step on its GO: memo section 6.1 (flash the bench build tractor then base with `flash_l072.sh`; `radio_park.py` must report `cap_bitmap` 0x000000FF, `path` firmware, PARKED, and `radio_state.py` must still read 0x80 at least 2 min later; then R1, a 2c cold-start leg: base acquisition ACQUIRED, every 0x0D `name=RX_SCAN_FAILED` with no `fw=old`, no 0x0F and Δ`tx_fifo_rb_bad` 0, and its leg-end park on the firmware path still 0x80 at least 2 min later with profile 1 active, the real test of the park gate; then R2, the leg-X shape with `-InjectOn fault:0x0D:final` firing on the tractor's attempt-3 fault only). Fly from a checkout that holds this work: the harness pushes its daemons (the base logs the VER capability bitmap) and probe helpers. T1 needs a harness option to delay the tractor's stream (not built). - [x] **RS-12.11 — base command scheduler vs fragment arrivals: DONE 2026-09-12 (PR #118, gate passed, see above).** @@ -2839,7 +2839,7 @@ conflicts. Every leg needs GO; radios stay parked between legs. tractor adopting (the SIL margin is ~0.5 ms); whether self-follow should also run in scan FAILED, as it does now (if not, the final FAIL's park comes back with it); and the half-duplex collisions that remain (D-VS9's reserved - control slot). 2026-10-24 (FHSS leg 2b_f3): the base never LOCKED and heard 5 of 606 frames; leading hypothesis: a follower in scan FAILED has no such escape (open, RS-12.21). SIL 2026-10-24 (`check-rx-scan-failed-follower`, no radio) widens the self-follow question: in FAILED an originator still adopts any accepted header, only a LOCKED -> SCANNING demotion clears adoption, FAILED never has one, and an adopted node holds no authority; so one camera stall plus one command heard in it ends self-follow for good (SIL-E3, 50 runs with stalls: authority on 3.8 % of passes, 36.6 % of commands received; main before FAILED under the same stalls, i.e. the decay window: 22.1 %, 53.2 %; in FAILED without stalls: 100 %, 80.7 %). Options (a) and (b) bring it back to main's pre-FAILED behaviour (decay-window phase restarts included); the recommended image (b + loss-timer fix + 2 s dwell) also moves that pre-FAILED decay window itself (SIL-E3, 200 runs: commands received 53.2 → 59.1 %, authority 22.1 → 14.4 % of passes, own-grid renumberings +24 %; the base's cost of a renumbering is not modelled; memo section 5); a host re-activation watchdog must not run on the tractor (it restarts its grid at epoch 0). The L072s tick on HSI16, not the ±2 ppm TCXOs `sx1276_fhss_clock.h` assumes: base vs tractor L072 -2366 to -3210 ppm on 2026-10-24 (-4122 to +871 ppm over 27 legs), so size the lead tolerance and the 12 ms head start against that. See [FHSS_SCAN_FAILED_OPTIONS.md](firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md). + control slot). 2026-10-24 (FHSS leg 2b_f3): the base never LOCKED and heard 5 of 606 frames; leading hypothesis: a follower in scan FAILED has no such escape (open, RS-12.21). SIL 2026-10-24 (`check-rx-scan-failed-follower`, no radio) widens the self-follow question: in FAILED an originator still adopts any accepted header, only a LOCKED -> SCANNING demotion clears adoption, FAILED never has one, and an adopted node holds no authority; so one camera stall plus one command heard in it ends self-follow for good (SIL-E3, 50 runs with stalls: authority on 3.8 % of passes, 36.6 % of commands received; main before FAILED under the same stalls, i.e. the decay window: 22.1 %, 53.2 %; in FAILED without stalls: 100 %, 80.7 %). Options (a) and (b) bring it back to main's pre-FAILED behaviour (decay-window phase restarts included); the recommended image (b + loss-timer fix + 2 s dwell) also moves that pre-FAILED decay window itself (SIL-E3, 200 runs: commands received 53.2 → 59.1 %, authority 22.1 → 14.4 % of passes, own-grid renumberings +24 %; the base's cost of a renumbering is not modelled; memo section 5); a host re-activation watchdog must not run on the tractor (it restarts its grid at epoch 0). The L072s tick on HSI16, not the ±2 ppm TCXOs `sx1276_fhss_clock.h` assumes: base vs tractor L072 -2366 to -3210 ppm on 2026-10-24 (-4122 to +871 ppm over 27 legs), so size the lead tolerance and the 12 ms head start against that. See [FHSS_SCAN_FAILED_OPTIONS.md](firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md). 2026-10-25: the recommended image (b + L + d2), adopted by the design owner 2026-10-24 ([DECISIONS.md](DECISIONS.md) D-FHSS1), is **built, not flashed** (RS-12.21 row; boards stay on `e083ae46` until the flash GO): FAILED no longer exists for a node without originator authority, which answers "should self-follow also run in FAILED" (FAILED is held only under authority); the decay-window question stays open (the trade-off accepted, memo section 5 item 3). On air after the flash (memo section 6.1): R2, the leg-X shape, re-checks this row's 2026-10-18 criteria on the new image; T4 with camera stalls comes later. **SHARPENED 2026-09-12 (RS-12.14 gate leg J): the dominant mechanism is FHSS CLOCK AUTHORITY, not reverse delivery. Every follower lock loss in the three profile-1 legs starts 1–2 s after a base command the TRACTOR RECEIVED (H: 1 diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_rx_daemon.py b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_rx_daemon.py index d008d05f..2e9e361c 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_rx_daemon.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_rx_daemon.py @@ -124,6 +124,7 @@ def train_seq_of(raw: bytes) -> int: configure_regulatory_profile_if_needed, verify_modem_matches_profile, install_fault_urc_logger, + parse_version, read_reg, write_reg, SX1276_REG_OP_MODE, @@ -472,8 +473,16 @@ def _open_link(self) -> HostLink: last_exc = None for attempt in range(1, 6): try: - link.request(HOST_TYPE_VER_REQ, HOST_TYPE_VER_URC, timeout=2.0) - LOG.info("L072 VER warm-up ok (attempt %d)", attempt) + ver = link.request(HOST_TYPE_VER_REQ, HOST_TYPE_VER_URC, timeout=2.0) + # 2026-10-24 review: the capability bitmap names the image + # (bit 7 HOST_CAP_RADIO_PARK: the RS-12.21 image, whose scan + # restarts after a final RX_SCAN_FAILED); rs12_leg_report.py + # reads it off this line for the acquisition rule. + try: + cap = f"0x{parse_version(ver['payload'])['cap_bitmap']:08X}" + except Exception: # noqa: BLE001 + cap = "?" + LOG.info("L072 VER warm-up ok (attempt %d) cap=%s", attempt, cap) last_exc = None break except Exception as exc: diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_fault_urc_log_sil.py b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_fault_urc_log_sil.py index ba73242e..db356333 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_fault_urc_log_sil.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_fault_urc_log_sil.py @@ -12,10 +12,17 @@ otherwise, wherever the reader takes it off the wire. The legacy lines stay, so tools count `FAULT_URC: code=0x.. sub=0x.. name=` lines only. +2026-10-24, later (the 0x0D split, FHSS_SCAN_FAILED_OPTIONS.md section 7): +RX_SCAN_FAILED keeps 0x0D and TX_FIFO_RB_MISMATCH moves to 0x0F. The decoder +names a 0x0D RX_SCAN_FAILED whenever its sub is a scan encoding (the +in-flight check survives as `basis=`, the only trace of the pre-split +ambiguity) and the pre-split FIFO fault (`fw=old`) otherwise; it no longer +writes AMBIGUOUS. + Contract pinned here: * the formatter on REAL payloads from the committed archives, incl. the - honest 0x0D decode (RX_SCAN_FAILED / TX_FIFO_RB_MISMATCH / AMBIGUOUS) - and the L072 uptime (payload bytes 20-23); + 0x0D decode (RX_SCAN_FAILED with its basis / the pre-split + TX_FIFO_RB_MISMATCH), 0x0F, and the L072 uptime (payload bytes 20-23); * the line is seen by the case-sensitive FAULT greps the tools use, and is told apart from its legacy twins by `name=`; * the scan sub-byte model matches the firmware (sx1276_rx_scan_fail.h, @@ -127,31 +134,64 @@ def test_final_attempt(self): self.assertIn("name=RX_SCAN_FAILED attempt=3 final=1", line) self.assertIn("tx_inflight=- uptime_ms=123670 ", line) - def test_sub_equal_to_an_in_flight_tx_id_is_ambiguous(self): - """Leg X's sub 0x00 against fragment 0 in flight: both firmware - emitters can produce it, so the line says so and carries both reads.""" + def test_sub_equal_to_an_in_flight_tx_id_keeps_its_basis(self): + """Leg X's sub 0x00 against fragment 0 in flight. A scan encoding is + RX_SCAN_FAILED after the 0x0D split (PR #166 named it AMBIGUOUS); + basis=tx_id_in_flight keeps what still matters on a pre-split image: + this URC could also be fragment 0's FIFO readback mismatch.""" self.assertEqual( mg.format_fault_urc_line(LEGX_ATTEMPT0, frozenset({0x00})), - "FAULT_URC: code=0x0D sub=0x00 name=AMBIGUOUS attempt=0 final=0 " - "warm=0 hw_suspect=0 crc_seen=0 tx_id=0x00 basis=tx_id_in_flight " + "FAULT_URC: code=0x0D sub=0x00 name=RX_SCAN_FAILED attempt=0 final=0 " + "warm=0 hw_suspect=0 crc_seen=0 basis=tx_id_in_flight " "tx_inflight=0x00 uptime_ms=33668 " "raw=0d0000000000000000000000000000000000000084830000") - def test_no_tx_context_is_ambiguous(self): + def test_no_tx_context_is_still_a_scan_failure(self): line = mg.format_fault_urc_line(LEG6_ATTEMPT0) - self.assertIn("name=AMBIGUOUS", line) - self.assertIn("basis=no_tx_context tx_inflight=?", line) + self.assertIn("name=RX_SCAN_FAILED attempt=0 final=0 warm=0 " + "hw_suspect=1 crc_seen=0 basis=no_tx_context tx_inflight=? ", + line) - def test_non_scan_sub_is_a_fifo_mismatch_whatever_the_context(self): + def test_the_decoder_never_writes_ambiguous(self): + for sub in range(256): + for ctx in (None, frozenset(), frozenset({sub})): + d = mg.decode_fault_0x0d(sub, ctx) + self.assertIn(d["name"], ("RX_SCAN_FAILED", "TX_FIFO_RB_MISMATCH")) + self.assertEqual(d["name"] == "RX_SCAN_FAILED", + mg.rx_scan_fail_sub_valid(sub), hex(sub)) + self.assertEqual(d["old_fw"], d["name"] == "TX_FIFO_RB_MISMATCH") + self.assertNotIn("AMBIGUOUS", + mg.format_fault_urc_line(fault(0x0D, sub), ctx)) + + def test_non_scan_sub_is_a_pre_split_fifo_mismatch_whatever_the_context(self): # 0xFE/0xFD: the command-frame tx_ids (hw_suspect+crc_seen both set); - # 0x03: attempt 3 without `final`; 0x84: final with attempt 4. + # 0x03: attempt 3 without `final`; 0x84: final with attempt 4. No + # image after the 0x0D split sends these as 0x0D: fw=old says so. for sub in (0xFE, 0xFD, 0x03, 0x84, 0x30, 0x05): for ctx in (None, frozenset(), frozenset({sub})): line = mg.format_fault_urc_line(fault(0x0D, sub, 7), ctx) self.assertIn(f"name=TX_FIFO_RB_MISMATCH tx_id=0x{sub:02X} " - "basis=not_scan_encoding", line, (sub, ctx)) + "fw=old basis=not_scan_encoding", line, (sub, ctx)) self.assertNotIn("attempt=", line) + def test_0x0f_is_the_fifo_mismatch(self): + """After the split TX_FIFO_RB_MISMATCH is 0x0F, sub = the host's + tx_id; the code says it all (no basis, no fw= token), whatever the + sub would read as under 0x0D.""" + for sub in (0x00, 0x02, 0x83, 0xFD, 0xFE): + for ctx in (None, frozenset(), frozenset({sub})): + line = mg.format_fault_urc_line(fault(0x0F, sub, 180000), ctx) + self.assertEqual( + line, f"FAULT_URC: code=0x0F sub=0x{sub:02X} " + f"name=TX_FIFO_RB_MISMATCH tx_id=0x{sub:02X} uptime_ms=180000 " + f"raw={fault(0x0F, sub, 180000).hex()}", (sub, ctx)) + self.assertEqual(mg.HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH, 0x0F) + self.assertEqual(mg.HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH_PRE_SPLIT, 0x0D) + self.assertEqual(mg.HOST_FAULT_CODE_RX_SCAN_FAILED, 0x0D) + # the legacy text needs no decode: byte-identical shape + self.assertEqual(mg.format_fault_payload(fault(0x0F, 0x02, 9)), + f"code=0x0F sub=0x02 raw={fault(0x0F, 0x02, 9).hex()}") + def test_other_codes(self): self.assertEqual( mg.format_fault_urc_line(SETTLE_RX_INACTIVE), @@ -225,7 +265,7 @@ def test_told_apart_from_its_legacy_twins(self): def test_log_levels(self): self.assertEqual(mh.fault_urc_log_level(fault(0x09, 1)), logging.INFO) self.assertEqual(mh.fault_urc_log_level(fault(0x0C, 1)), logging.INFO) - for code in (0x01, 0x0A, 0x0B, 0x0D, 0x0E): + for code in (0x01, 0x0A, 0x0B, 0x0D, 0x0E, 0x0F): self.assertEqual(mh.fault_urc_log_level(fault(code, 0)), logging.WARNING, code) self.assertEqual(mh.fault_urc_log_level(b""), logging.WARNING) @@ -320,18 +360,35 @@ def test_host_types_codes_and_payload(self): d = self._defines("host_types.h") codes = {k[len("HOST_FAULT_CODE_"):]: v for k, v in d.items() if k.startswith("HOST_FAULT_CODE_")} - # the double booking this whole decode exists for - self.assertEqual(codes["TX_FIFO_RB_MISMATCH"], 0x0D) + # the 0x0D split (2026-10-24): the double booking is gone, every + # code is unique and the host names each one as the firmware does self.assertEqual(codes["RX_SCAN_FAILED"], 0x0D) + self.assertEqual(codes["TX_FIFO_RB_MISMATCH"], 0x0F) + self.assertEqual(len(set(codes.values())), len(codes), codes) for name, value in codes.items(): - if value == 0x0D: - continue self.assertEqual(mg.HOST_FAULT_CODE_NAMES.get(value), name, name) - self.assertNotIn(0x0D, mg.HOST_FAULT_CODE_NAMES) + self.assertEqual(set(mg.HOST_FAULT_CODE_NAMES), set(codes.values())) self.assertEqual(d["HOST_FAULT_URC_PAYLOAD_LEN"], 24) self.assertEqual(d["HOST_TYPE_FAULT_URC"], mg.HOST_TYPE_FAULT_URC) self.assertEqual(d["HOST_TYPE_TX_FRAME_REQ"], mg.HOST_TYPE_TX_FRAME_REQ) self.assertEqual(d["HOST_TYPE_TX_DONE_URC"], mg.HOST_TYPE_TX_DONE_URC) + # RS-12.21 park command (HostLink.radio_park, bench_tools/radio_park.py) + self.assertEqual(d["HOST_TYPE_RADIO_PARK_REQ"], mg.HOST_TYPE_RADIO_PARK_REQ) + self.assertEqual(d["HOST_TYPE_RADIO_PARK_URC"], mg.HOST_TYPE_RADIO_PARK_URC) + self.assertEqual((mg.HOST_TYPE_RADIO_PARK_REQ, mg.HOST_TYPE_RADIO_PARK_URC), + (0x04, 0x84)) + # 2026-10-24 review: the capability bit radio_park.py and + # rs12_leg_report.py read the image by, and that the image sets it + with open(os.path.join(_FW_INC, "host_types.h"), encoding="utf-8") as fh: + text = fh.read() + bit = re.search(r"#define\s+HOST_CAP_RADIO_PARK\s+\(1UL << (\d+)\)", text) + self.assertEqual(1 << int(bit.group(1)), mg.HOST_CAP_RADIO_PARK) + self.assertRegex(text, r"HOST_CAP_TX_FRAME \| \\\s*\n\s*HOST_CAP_RADIO_PARK\)") + tools = os.path.join(_DC_DIR, "tools") + if tools not in sys.path: + sys.path.insert(0, tools) + import rs12_leg_report as rep + self.assertEqual(rep.HOST_CAP_RADIO_PARK, mg.HOST_CAP_RADIO_PARK) def test_every_emittable_sub_is_a_scan_encoding(self): emitted = set() @@ -343,6 +400,12 @@ def test_every_emittable_sub_is_a_scan_encoding(self): emitted.add(sub) d = mg.decode_fault_0x0d(sub, frozenset()) self.assertEqual(d["name"], "RX_SCAN_FAILED", hex(sub)) + self.assertEqual(d["basis"], "tx_id_not_in_flight") + # a TX with tx_id == sub in flight moves the basis only + busy = mg.decode_fault_0x0d(sub, frozenset({sub})) + self.assertEqual((busy["name"], busy["basis"], busy["scan"]), + ("RX_SCAN_FAILED", "tx_id_in_flight", + d["scan"])) self.assertEqual(d["scan"]["attempt"], retries) self.assertEqual(d["scan"]["warm"], int(warm)) self.assertEqual(d["scan"]["crc_seen"], int(crc)) @@ -384,6 +447,7 @@ def __init__(self): self.on_tx = {} # tx_id -> [(type, payload)] before TX_DONE self.park_tx = False # True: TX_FRAME_REQ draws no reply self.refuse_tx = 0 # next N TX_FRAME_REQs: ERR_PROTO FORBIDDEN + self.ver_payload = bytes(12) # VER_URC: proto..reserved, cap_bitmap (LE) # -- os surface used by HostLink -- def open(self, *_a, **_k): @@ -425,7 +489,7 @@ def _handle(self, fr): for ftype, payload in self.before_reply.pop(t, []): self.push(ftype, payload) if t == 0x01: # VER_REQ - self.push(0x81, bytes(12), seq) + self.push(0x81, self.ver_payload, seq) elif t == 0x20: # CFG_SET_REQ self.push(0xA0, bytes([p[0], 0]), seq) elif t == 0x30: # REG_READ_REQ @@ -536,9 +600,9 @@ def test_staleness_ignores_a_wall_clock_step(self): def test_resent_tx_id_stays_in_flight_until_both_tx_dones(self): """Review 2026-10-24 (dup tx_id): the pipelined watchdog / serial retry resend a tx_id while the L072 may still hold the first copy. - The first TX_DONE must not retire both: a FIFO fault on the resend - (sub = tx_id 0x00, also a scan encoding) is AMBIGUOUS, not a - determinate RX_SCAN_FAILED.""" + The first TX_DONE must not retire both: a pre-split FIFO fault on + the resend (code 0x0D, sub = tx_id 0x00, also a scan encoding) + keeps basis=tx_id_in_flight, not tx_id_not_in_flight.""" link = self.link(observe=False) lines = [] link.on_fault_urc = lambda fr, ids: lines.append( @@ -553,7 +617,7 @@ def test_resent_tx_id_stays_in_flight_until_both_tx_dones(self): self.fake.push(0xF1, fault(0x0D, 0x00, 70_000)) # nothing in flight while len(lines) < 2: self.assertTrue(link.read_frames(0.2)) - self.assertIn("sub=0x00 name=AMBIGUOUS attempt=0", lines[0]) + self.assertIn("sub=0x00 name=RX_SCAN_FAILED attempt=0", lines[0]) self.assertIn("basis=tx_id_in_flight tx_inflight=0x00 " "uptime_ms=40000", lines[0]) self.assertIn("sub=0x00 name=RX_SCAN_FAILED attempt=0", lines[1]) @@ -691,25 +755,60 @@ def test_open_link_logs_boot_and_request_faults(self): "tx_inflight=- uptime_ms=64012", msg) self.assertEqual(len(fault_urc_lines(records)), 2) + def test_open_link_logs_the_capability_bitmap(self): + """2026-10-24 review: the VER warm-up line names the image by its + capability bitmap (bit 7 HOST_CAP_RADIO_PARK: the RS-12.21 image, whose + scan restarts after a final RX_SCAN_FAILED), in the shape + rs12_leg_report.py's acquisition rule reads; a short VER payload logs + cap=?.""" + tools = os.path.join(_DC_DIR, "tools") + if tools not in sys.path: + sys.path.insert(0, tools) + import rs12_leg_report as rep + for payload, cap in ((bytes(8) + (0xFF).to_bytes(4, "little"), "0x000000FF"), + (bytes(8) + (0x7F).to_bytes(4, "little"), "0x0000007F"), + (bytes(11), "?")): + with self.subTest(cap=cap): + self.fake.ver_payload = payload + _link, records = self.open_link(self.daemon()) + msgs = [r.getMessage() for r in records + if r.getMessage().startswith("L072 VER warm-up ok")] + self.assertEqual(msgs, [f"L072 VER warm-up ok (attempt 1) cap={cap}"]) + m = rep.VER_CAP_RE.search(msgs[0]) + if cap == "?": + self.assertIsNone(m) + else: + self.assertEqual(int(m.group(1), 16) & rep.HOST_CAP_RADIO_PARK, + int(cap, 16) & 0x80) + def test_command_tx_fifo_mismatch(self): - d = self.daemon() - link, _ = self.open_link(d) - self.fake.on_tx[0xFD] = [(0xF1, fault(0x0D, 0xFD, 5000))] - body = pack_command_frame(CMD_OP_REQ_KEYFRAME) - with self.assertLogs(self.logger, level="DEBUG") as cap: - d._send_command_frame(link, body, copies=1) - self.assert_one_fault_line( - cap.records, "code=0x0D sub=0xFD name=TX_FIFO_RB_MISMATCH " - "tx_id=0xFD basis=not_scan_encoding tx_inflight=0xFD") - # the base's command TX wait also print()s a stdout twin (review - # 2026-10-24): one docker log, one count on the `name=` key - self.assertIn("FAULT_URC during TX wait: " + mg.format_fault_payload( - fault(0x0D, 0xFD, 5000)), self.stdout.getvalue()) - self.assert_one_count(cap.records, 1) - # non-FAULT handling around it: TX_DONE still completes the send - self.assertTrue(any("command TX opcode=0x60 copy=1/1 OK (on air)" - in r.getMessage() for r in cap.records)) - self.assertEqual(getattr(d, "_cmd_tx_ok", 0), 1) + """A pre-split image's FIFO fault on a base command (code 0x0D, sub + = tx_id 0xFD), and the same fault from a split image (code 0x0F).""" + for code, expect in ( + (0x0D, "code=0x0D sub=0xFD name=TX_FIFO_RB_MISMATCH tx_id=0xFD " + "fw=old basis=not_scan_encoding tx_inflight=0xFD " + "uptime_ms=5000"), + (0x0F, "code=0x0F sub=0xFD name=TX_FIFO_RB_MISMATCH tx_id=0xFD " + "uptime_ms=5000")): + with self.subTest(code=hex(code)): + self.stdout.seek(0) + self.stdout.truncate() + d = self.daemon() + link, _ = self.open_link(d) + self.fake.on_tx[0xFD] = [(0xF1, fault(code, 0xFD, 5000))] + body = pack_command_frame(CMD_OP_REQ_KEYFRAME) + with self.assertLogs(self.logger, level="DEBUG") as cap: + d._send_command_frame(link, body, copies=1) + self.assert_one_fault_line(cap.records, expect) + # the base's command TX wait also print()s a stdout twin + # (review 2026-10-24): one docker log, one count on `name=` + self.assertIn("FAULT_URC during TX wait: " + mg.format_fault_payload( + fault(code, 0xFD, 5000)), self.stdout.getvalue()) + self.assert_one_count(cap.records, 1) + # non-FAULT handling around it: TX_DONE still completes the send + self.assertTrue(any("command TX opcode=0x60 copy=1/1 OK (on air)" + in r.getMessage() for r in cap.records)) + self.assertEqual(getattr(d, "_cmd_tx_ok", 0), 1) def test_rx_loop_logs_fault_and_still_handles_frames(self): """The RX loop used to drop FAULTs at DEBUG; now the observer logs @@ -763,15 +862,19 @@ def test_open_link_logs_boot_fault(self): def test_pipelined_tx_scan_and_fifo_faults(self): """Fragments 0 and 1 in flight (depth 2): a scan fault (sub 0x20) - reads as RX_SCAN_FAILED; a 0x0D whose sub is fragment 1's tx_id is - AMBIGUOUS. The historical pipelined line is still written verbatim - and the TX_DONE accounting is unchanged.""" + reads as RX_SCAN_FAILED; a 0x0D whose sub is fragment 1's tx_id + reads as RX_SCAN_FAILED too, with basis=tx_id_in_flight (a pre-split + image could have meant fragment 1's FIFO fault; PR #166 named it + AMBIGUOUS); a split image's 0x0F on fragment 1 is the FIFO fault. + The historical pipelined line is still written verbatim and the + TX_DONE accounting is unchanged.""" d = self.daemon() link, _ = self.open_link(d) scan = fault(0x0D, 0x20, 34012) - fifo = fault(0x0D, 0x01, 34200) + busy = fault(0x0D, 0x01, 34200) + fifo = fault(0x0F, 0x01, 34201) self.fake.on_tx[0x00] = [(0xF1, scan)] - self.fake.on_tx[0x01] = [(0xF1, fifo)] + self.fake.on_tx[0x01] = [(0xF1, busy), (0xF1, fifo)] frame = image_tx_daemon._PendingFrame(seq=7, payload=b"x", enqueued_ms=0) with self.assertLogs(self.logger, level="DEBUG") as cap: @@ -782,15 +885,19 @@ def test_pipelined_tx_scan_and_fifo_faults(self): "final=0 warm=0 hw_suspect=1 crc_seen=0 basis=tx_id_not_in_flight " "tx_inflight=0x00,0x01 uptime_ms=34012") self.assert_one_fault_line( - cap.records, "code=0x0D sub=0x01 name=AMBIGUOUS attempt=1 final=0 " - "warm=0 hw_suspect=0 crc_seen=0 tx_id=0x01 basis=tx_id_in_flight " + cap.records, "code=0x0D sub=0x01 name=RX_SCAN_FAILED attempt=1 final=0 " + "warm=0 hw_suspect=0 crc_seen=0 basis=tx_id_in_flight " "tx_inflight=0x01 uptime_ms=34200") + self.assert_one_fault_line( + cap.records, "code=0x0F sub=0x01 name=TX_FIFO_RB_MISMATCH tx_id=0x01 " + "uptime_ms=34201") legacy = [r.getMessage() for r in cap.records if r.getMessage().startswith("FAULT during pipelined TX: ")] self.assertEqual(legacy, [ "FAULT during pipelined TX: " + mg.format_fault_payload(scan), + "FAULT during pipelined TX: " + mg.format_fault_payload(busy), "FAULT during pipelined TX: " + mg.format_fault_payload(fifo)]) - self.assert_one_count(cap.records, 2) + self.assert_one_count(cap.records, 3) self.assertEqual((d.frames_tx_ok, d.fragments_tx_ok, d.fragments_tx_fail), (1, 2, 0)) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_radio_park_sil.py b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_radio_park_sil.py new file mode 100644 index 00000000..b9a32a72 --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_radio_park_sil.py @@ -0,0 +1,542 @@ +"""2026-10-24 (RS-12.21) — the firmware park command, host side. + +`HostLink.radio_park()` (firmware/x8_lora_bootloader_helper/ +method_g_stage1_probe.py) sends HOST_TYPE_RADIO_PARK_REQ (0x04) and sorts +the answer: RADIO_PARK_URC (0x84: a status byte, then the RegOpMode +readback), ERR_PROTO UNKNOWN_TYPE (an image from before the command), any +other ERR_PROTO, or nothing within the timeout. + +`bench_tools/radio_park.py` parks through it when the firmware answers +(persistent: the host_parked flag gates every RX arm path until a host RXCONT +write, a profile activation or a reset) and otherwise falls back to the +STANDBY -> SLEEP register writes it has always made (transient: the scan +walker re-arms RXCONT until the scan fails out into FAILED). Its last line +says which path parked the radio and whether the park is persistent; its +PARK_OK / PARK_TRANSIENT / PARK_FAIL lines and exit codes 0/2/3/4/5 keep +their meaning. 2026-10-24 review: the VER reply's capability bit 7 +(HOST_CAP_RADIO_PARK) picks the path -- an image that advertises the command +is asked again on silence (a late answer counts) and never gets the register +writes, which cannot hold on it -- and the settle outlasts the firmware's +slowest re-arm, the 2 s scan dwell. + +Everything runs against a scripted L072 (FakeParkL072) that stands in for +`os` under the real HostLink and models the firmware's RX owners re-arming +RXCONT one scan dwell (SX1276_RX_SCAN_DWELL_MS, read from the firmware +header) after the radio left RX -- no hardware, no radio. The firmware's own +unpark path (a host raw RXCONT write) is pinned by check-radio-park +(firmware/murata_l072/bench/host_proto/radio_park.c), not here. +""" +import contextlib +import io +import json +import os +import re +import sys +import threading +import time +import unittest +from unittest import mock + +_BS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_DC_DIR = os.path.dirname(_BS_DIR) +_HELPER_DIR = os.path.join(_DC_DIR, "firmware", "x8_lora_bootloader_helper") +_BT_DIR = os.path.join(_HELPER_DIR, "bench_tools") +for _p in (_HELPER_DIR, _BT_DIR): + if _p not in sys.path: + sys.path.insert(0, _p) + +import method_g_stage1_probe as mg # noqa: E402 +import radio_park # noqa: E402 + +REG_OP_MODE = 0x01 +RXCONT, STANDBY, SLEEP = 0x85, 0x81, 0x80 +CAP_SPLIT, CAP_OLD = 0xFF, 0x7F # VER cap_bitmap with / without bit 7 + + +def _fw_define(header: str, name: str) -> int: + """A `#define NAME U` of the L072 firmware headers.""" + path = os.path.join(_DC_DIR, "firmware", "murata_l072", "include", header) + with open(path, encoding="utf-8") as fh: + m = re.search(rf"#define\s+{name}\s+(\d+)U", fh.read()) + return int(m.group(1)) + + +# The firmware's slowest RX re-arm: the scan walker's next step, at most one +# channel dwell after the radio left RX (2 s since RS-12.21 d2), and a slot. +DWELL_S = _fw_define("sx1276_rx_scan_policy.h", "SX1276_RX_SCAN_DWELL_MS") / 1000.0 +SLOT_S = _fw_define("sx1276_fhss_clock.h", "SX1276_FHSS_SLOT_MS") / 1000.0 + + +def err_proto(offending_type: int, err_code: int, detail: int = 0) -> bytes: + """The 5-byte ERR_PROTO payload host_uart_send_err_proto() builds.""" + return bytes([offending_type, 1, err_code, detail & 0xFF, detail >> 8]) + + +class FakeParkL072: + """Stands in for `os` inside method_g_stage1_probe. Answers VER, register + read/write and RADIO_PARK_REQ the way host_cmd.c does for the image `fw`: + + split answers the park command: sets host_parked, SLEEP, URC + {PARKED or ALREADY_PARKED, RegOpMode}; + leaky the same, but the gate misses an arm path (a defect); + refuse URC {REFUSED, RegOpMode}, radio unchanged; + old ERR_PROTO UNKNOWN_TYPE (host_cmd.c dispatch default); + bad_length ERR_PROTO BAD_LENGTH (knows the command, not the frame); + silent no answer at all. + + `walking`: an RX owner (scan walker, retune, slot follower) still has + work, so advance() -- the firmware's clock, moved by radio_park's sleeps + -- re-arms RXCONT DWELL_S after the radio left RX (the worst phase of the + walker's dwell) unless the park gate holds. A host REG_WRITE RegOpMode + RXCONT clears the park, as sx1276_rx_note_external_arm() does. `cap`: + the VER reply's capability bitmap (default: bit 7 for an image that + answers the command); `late`: answer the park command only when the next + host frame arrives (after the caller's timeout).""" + O_RDWR = O_NOCTTY = O_NONBLOCK = 0 + + def __init__(self, fw="split", opmode=RXCONT, walking=True, cap=None): + self._lock = threading.Lock() + self._out = bytearray() # L072 -> host + self._in = bytearray() # host -> L072 (partial frame) + self.host_frames = [] + self.fw = fw + self.opmode = opmode + self.walking = walking + self.cap = cap + self.parked = False + self.before_park = [] # [(type, payload, seq)] ahead of the reply + self.answer_ver = True + self.answer_reg_read = True + self.ver_payload = None # a VER_URC payload as is (None: by cap) + self.late = False + self._late_seq = None + self.now = 0.0 # firmware clock, s (radio_park's sleeps) + self.left_rx_at = None # when the radio last left RX + + # -- os surface used by HostLink -- + def open(self, *_a, **_k): + return 99 + + def close(self, _fd): + pass + + def read(self, _fd, n): + with self._lock: + if not self._out: + raise BlockingIOError + chunk = bytes(self._out[:n]) + del self._out[:n] + return chunk + + def write(self, _fd, data): + self._in.extend(data) + while True: + try: + start = self._in.index(0) + end = self._in.index(0, start + 1) + except ValueError: + break + body = bytes(self._in[start + 1:end]) + del self._in[:end + 1] + if body: + self._handle(mg.parse_frame(body)) + return len(data) + + # -- scripting -- + def push(self, ftype, payload, seq=0): + with self._lock: + self._out.extend(mg.build_frame(ftype, seq, payload)) + + def _set_opmode(self, value): + if (self.opmode & 0x07) == 0x05 and (value & 0x07) != 0x05: + self.left_rx_at = self.now + self.opmode = value + + def advance(self, s): + self.now += s + if (self.walking and (not self.parked or self.fw == "leaky") + and (self.opmode & 0x07) != 0x05 and self.left_rx_at is not None + and self.now - self.left_rx_at >= DWELL_S): + self.opmode = RXCONT + + def ver_cap(self): + if self.cap is not None: + return self.cap + return CAP_SPLIT if self.fw in ("split", "leaky", "refuse") else CAP_OLD + + def sent(self, ftype): + return [f for f in self.host_frames if f["type"] == ftype] + + def _handle(self, fr): + self.host_frames.append(fr) + t, seq, p = fr["type"], fr["seq"], fr["payload"] + if self._late_seq is not None: # the delayed park answer first + late, self._late_seq = self._late_seq, None + self._park_reply(late) + if t == mg.HOST_TYPE_VER_REQ: + if self.answer_ver: + self.push(mg.HOST_TYPE_VER_URC, + self.ver_payload if self.ver_payload is not None else + bytes(8) + self.ver_cap().to_bytes(4, "little"), seq) + elif t == mg.HOST_TYPE_REG_READ_REQ: + if self.answer_reg_read: + self.push(mg.HOST_TYPE_REG_DATA_URC, bytes( + [p[0], self.opmode if p[0] == REG_OP_MODE else 0]), seq) + elif t == 0x31: # REG_WRITE_REQ + if p[0] == REG_OP_MODE: + self._set_opmode(p[1]) + if (p[1] & 0x07) == 0x05: + self.parked = False + self.push(0xB1, bytes(p[:2]), seq) + elif t == mg.HOST_TYPE_RADIO_PARK_REQ: + for ftype, payload, s in self.before_park: + self.push(ftype, payload, s) + if self.late: + self._late_seq = seq + else: + self._park_reply(seq) + else: + self.push(mg.HOST_TYPE_ERR_PROTO_URC, err_proto( + t, mg.HOST_ERR_PROTO_UNKNOWN_TYPE, t), seq) + + def _park_reply(self, seq): + t = mg.HOST_TYPE_RADIO_PARK_REQ + if self.fw == "old": + self.push(mg.HOST_TYPE_ERR_PROTO_URC, err_proto( + t, mg.HOST_ERR_PROTO_UNKNOWN_TYPE, t), seq) + elif self.fw == "bad_length": + self.push(mg.HOST_TYPE_ERR_PROTO_URC, err_proto( + t, mg.HOST_ERR_PROTO_BAD_LENGTH, 0), seq) + elif self.fw == "refuse": + self.push(mg.HOST_TYPE_RADIO_PARK_URC, bytes( + [mg.RADIO_PARK_STATUS_REFUSED, self.opmode]), seq) + elif self.fw in ("split", "leaky"): + status = (mg.RADIO_PARK_STATUS_ALREADY_PARKED if self.parked + else mg.RADIO_PARK_STATUS_PARKED) + self.parked = True + self._set_opmode(SLEEP) + self.push(mg.HOST_TYPE_RADIO_PARK_URC, + bytes([status, self.opmode]), seq) + # "silent": nothing + + +class _FakeTime: + """radio_park's `time`: its only use is sleep(), which moves the fake + L072's clock (FakeParkL072.advance) by the time slept.""" + + def __init__(self, fake): + self.fake = fake + self.slept = [] + + def sleep(self, s): + self.slept.append(s) + self.fake.advance(s) + + +class _Case(unittest.TestCase): + def setUp(self): + self.fake = FakeParkL072() + for p in (mock.patch.object(mg, "os", self.fake), + mock.patch.object(mg, "configure_uart", lambda *_a: None)): + p.start() + self.addCleanup(p.stop) + self.stdout = io.StringIO() + quiet = contextlib.redirect_stdout(self.stdout) + quiet.__enter__() + self.addCleanup(quiet.__exit__, None, None, None) + + def link(self): + return mg.HostLink("/dev/fake", "921600") + + +# --------------------------------------------------------------------------- +# 1. HostLink.radio_park +# --------------------------------------------------------------------------- +class HostLinkRadioPark(_Case): + def test_split_image_parks(self): + link = self.link() + seq = link.seq # the next send's seq + got = link.radio_park() + self.assertEqual(got, {"answer": "urc", "status": 0, + "status_name": "PARKED", "opmode": SLEEP, + "detail": "", "seq": seq, "answer_seq": seq}) + req = self.fake.sent(mg.HOST_TYPE_RADIO_PARK_REQ) + self.assertEqual([(f["payload"], f["ver"]) for f in req], + [(b"", mg.HOST_PROTOCOL_VER)]) # empty payload + self.assertEqual(link.radio_park()["status_name"], "ALREADY_PARKED") + + def test_old_image_says_unknown_type(self): + self.fake.fw = "old" + got = self.link().radio_park() + self.assertEqual(got["answer"], "unknown_type") + self.assertIn("offending_type=0x04", got["detail"]) + self.assertIn("err_code=0x02(UNKNOWN_TYPE)", got["detail"]) + self.assertEqual((got["status"], got["opmode"]), (None, None)) + self.assertEqual(self.fake.opmode, RXCONT) # untouched + + def test_other_err_proto_is_not_unknown_type(self): + self.fake.fw = "bad_length" + got = self.link().radio_park() + self.assertEqual(got["answer"], "err_proto") + self.assertIn("err_code=0x03(BAD_LENGTH)", got["detail"]) + + def test_no_reply_within_the_timeout(self): + self.fake.fw = "silent" + t0 = time.monotonic() + link = self.link() + seq = link.seq + got = link.radio_park(timeout=0.3) + self.assertLess(time.monotonic() - t0, 2.0) + self.assertEqual(got, {"answer": "no_reply", "status": None, + "status_name": None, "opmode": None, + "detail": "", "seq": seq, "answer_seq": None}) + + def test_a_late_answer_to_an_earlier_try(self): + """2026-10-24 review: the answer to a try that timed out lands during + the next one. Named in earlier_seqs it is the answer; otherwise it is + dropped as stale, as before.""" + self.fake.late = True # answers when the next frame lands + link = self.link() + first = link.radio_park(timeout=0.3) + self.assertEqual(first["answer"], "no_reply") + got = link.radio_park(timeout=0.5, earlier_seqs=[first["seq"]]) + self.assertEqual((got["answer"], got["status_name"], got["answer_seq"]), + ("urc", "PARKED", first["seq"])) + self.assertNotEqual(got["seq"], first["seq"]) + + def test_a_late_answer_not_asked_for_is_dropped(self): + self.fake.late = True + link = self.link() + first = link.radio_park(timeout=0.3) + got = link.radio_park(timeout=0.3) # the first's answer lands: stale + self.assertEqual((first["answer"], got["answer"]), ("no_reply", "no_reply")) + self.assertEqual(list(link.urc_queue), []) + + def test_refused_and_short_payloads(self): + self.fake.fw = "refuse" + got = self.link().radio_park() + self.assertEqual((got["answer"], got["status"], got["status_name"], + got["opmode"]), ("urc", 2, "REFUSED", RXCONT)) + link = self.link() + seq = link.seq # the next send's seq + self.fake.fw = "silent" + self.fake.before_park = [(mg.HOST_TYPE_RADIO_PARK_URC, b"\x00", seq)] + got = link.radio_park(timeout=0.5) + self.assertEqual((got["status"], got["status_name"], got["opmode"]), + (0, "PARKED", None)) + seq = link.seq + self.fake.before_park = [(mg.HOST_TYPE_RADIO_PARK_URC, b"", seq)] + got = link.radio_park(timeout=0.5) + self.assertEqual((got["answer"], got["status"], got["status_name"]), + ("urc", None, None)) + seq = link.seq + self.fake.before_park = [(mg.HOST_TYPE_RADIO_PARK_URC, b"\x07\x80", seq)] + self.assertEqual(link.radio_park(timeout=0.5)["status_name"], "UNKNOWN_0x07") + + def test_other_frames_are_requeued_in_order_and_seen_once(self): + """URCs that land before the answer go back on urc_queue as + request() leaves them; a FAULT among them reaches on_fault_urc once; + a stale RADIO_PARK_URC (an earlier seq) is dropped, an ERR_PROTO for + another seq is kept.""" + link = self.link() + faults = [] + link.on_fault_urc = lambda fr, ids: faults.append(fr["payload"][:2]) + fault = bytes([0x0D, 0xA3]) + bytes(22) + stats = bytes(8) + other_err = err_proto(0x30, mg.HOST_ERR_PROTO_BAD_LENGTH, 1) + self.fake.before_park = [ + (mg.HOST_TYPE_FAULT_URC, fault, 0), + (mg.HOST_TYPE_RADIO_PARK_URC, bytes([0, SLEEP]), 0x7777), # stale + (mg.HOST_TYPE_STATS_URC, stats, 0), + (mg.HOST_TYPE_ERR_PROTO_URC, other_err, 0x6666), + ] + got = link.radio_park() + self.assertEqual(got["answer"], "urc") + self.assertEqual([(f["type"], bytes(f["payload"])) for f in link.urc_queue], [ + (mg.HOST_TYPE_FAULT_URC, fault), (mg.HOST_TYPE_STATS_URC, stats), + (mg.HOST_TYPE_ERR_PROTO_URC, other_err)]) + self.assertEqual(faults, [b"\x0d\xa3"]) + again = link.read_frames(0.2) # the queued copies + self.assertEqual(len(again), 3) + self.assertEqual(faults, [b"\x0d\xa3"]) # not seen twice + + def test_queued_frames_before_the_call_survive(self): + link = self.link() + self.fake.push(mg.HOST_TYPE_STATS_URC, bytes(8)) + link.urc_queue.extend(link.read_frames(0.2)) # parked by a request() + self.assertEqual(len(link.urc_queue), 1) + self.assertEqual(link.radio_park()["answer"], "urc") + self.assertEqual([f["type"] for f in link.urc_queue], [mg.HOST_TYPE_STATS_URC]) + + +# --------------------------------------------------------------------------- +# 2. bench_tools/radio_park.py +# --------------------------------------------------------------------------- +class RadioParkTool(_Case): + def run_tool(self, **fake): + for k, v in fake.items(): + setattr(self.fake, k, v) + self.time = _FakeTime(self.fake) + with mock.patch.object(radio_park, "time", self.time), \ + mock.patch.object(radio_park, "PARK_CMD_TIMEOUT_S", 0.3): + rc = radio_park.main(["/dev/fake"]) + last = self.stdout.getvalue().strip().splitlines()[-1] + verdict, _sp, blob = last.partition(" ") + return rc, verdict, json.loads(blob) + + def reg_writes(self): + return [bytes(f["payload"]) for f in self.fake.sent(0x31)] + + def test_split_image_parks_persistently(self): + rc, verdict, info = self.run_tool(fw="split", walking=True) + self.assertEqual((rc, verdict), (0, "PARK_OK")) + self.assertEqual(info["path"], "firmware") + self.assertIs(info["persistent"], True) + self.assertEqual(info["park_status"], "PARKED") + self.assertEqual((info["opmode_readback"], info["opmode_after_settle"]), + ("0x80", "0x80")) + self.assertEqual(info["settle_s"], radio_park.SETTLE_S) + self.assertIn("RXCONT write", info["holds"]) + self.assertNotIn("park_cmd", info) + self.assertEqual(self.reg_writes(), []) # no register writes + self.assertIn(radio_park.SETTLE_S, self.time.slept) + # the walker had work through the settle and the gate held + self.assertEqual(self.fake.opmode, SLEEP) + self.assertEqual((info["cap_bitmap"], info["park_tries"]), ("0x000000FF", 1)) + self.assertNotIn("late", info) + + def test_split_image_already_parked(self): + self.fake.parked, self.fake.opmode = True, SLEEP + rc, verdict, info = self.run_tool(fw="split") + self.assertEqual((rc, verdict, info["park_status"], info["persistent"]), + (0, "PARK_OK", "ALREADY_PARKED", True)) + + def test_old_image_scan_failed_out_parks_by_register(self): + rc, verdict, info = self.run_tool(fw="old", walking=False) + self.assertEqual((rc, verdict), (0, "PARK_OK")) + self.assertEqual(info["path"], "register") + self.assertIs(info["persistent"], False) + self.assertEqual(info["park_cmd"], "unknown_type") + self.assertIn("UNKNOWN_TYPE", info["park_cmd_err"]) + self.assertIn("until the scan fails out", info["holds"]) + self.assertNotIn("park_status", info) + self.assertEqual(self.reg_writes(), [bytes([REG_OP_MODE, STANDBY]), + bytes([REG_OP_MODE, SLEEP])]) + self.assertEqual((info["opmode_readback"], info["opmode_after_settle"]), + ("0x80", "0x80")) + self.assertEqual((info["cap_bitmap"], info["park_tries"]), ("0x0000007F", 1)) + + def test_old_image_scan_walking_is_transient(self): + rc, verdict, info = self.run_tool(fw="old", walking=True) + self.assertEqual((rc, verdict), (5, "PARK_TRANSIENT")) + self.assertEqual((info["path"], info["persistent"], info["park_cmd"]), + ("register", False, "unknown_type")) + self.assertEqual((info["opmode_readback"], info["opmode_after_settle"]), + ("0x80", "0x85")) + + def test_settle_outlasts_the_slowest_rearm(self): + """2026-10-24 review: SETTLE_S must outlast a scan channel dwell (2 s + since RS-12.21 d2, read from the firmware header) and a slot, or a + walker that re-arms late in the settle reads PARK_OK -- as the 1.5 s + settle did against the 2 s dwell (shown with it patched back).""" + self.assertGreaterEqual(radio_park.SETTLE_S, DWELL_S + SLOT_S) + with mock.patch.object(radio_park, "SETTLE_S", 1.5): + rc, verdict, _info = self.run_tool(fw="old", walking=True) + self.assertEqual((rc, verdict), (0, "PARK_OK")) # the walker not yet back + + def test_split_image_silent_is_a_fail_with_no_register_write(self): + """2026-10-24 review: an image that advertises the command (VER cap bit + 7) is asked PARK_CMD_TRIES times; unanswered it is a PARK_FAIL that made + no register write -- one cannot hold on it (its scan restarts after a + final RX_SCAN_FAILED and re-arms RX every dwell).""" + rc, verdict, info = self.run_tool(fw="silent", cap=CAP_SPLIT, walking=True) + self.assertEqual((rc, verdict), (4, "PARK_FAIL")) + self.assertEqual((info["path"], info["persistent"], info["park_cmd"], + info["park_tries"], info["cap_bitmap"]), + ("firmware", False, "no_reply", radio_park.PARK_CMD_TRIES, + "0x000000FF")) + self.assertIn("did not answer it in 3 tries; a register SLEEP write cannot " + "hold on this image", info["reason"]) + self.assertEqual(len(self.fake.sent(mg.HOST_TYPE_RADIO_PARK_REQ)), + radio_park.PARK_CMD_TRIES) + self.assertEqual(self.reg_writes(), []) + self.assertEqual(self.fake.opmode, RXCONT) # untouched + # one that says UNKNOWN_TYPE although it advertises the command: no retry + self.fake.host_frames.clear() + self.stdout.seek(0) + self.stdout.truncate() + rc, verdict, info = self.run_tool(fw="old", cap=CAP_SPLIT) + self.assertEqual((rc, verdict, info["park_cmd"], info["park_tries"]), + (4, "PARK_FAIL", "unknown_type", 1)) + self.assertIn("refused it in 1 try", info["reason"]) + self.assertIn("UNKNOWN_TYPE", info["park_cmd_err"]) + self.assertEqual(self.reg_writes(), []) + + def test_split_image_late_answer_parks(self): + """2026-10-24 review: the PARKED answer to a try that timed out lands + during the next try and is the answer (the split image is parked); the + old tool drained it and fell back to the register path.""" + rc, verdict, info = self.run_tool(fw="split", late=True, walking=True) + self.assertEqual((rc, verdict), (0, "PARK_OK")) + self.assertEqual((info["path"], info["persistent"], info["park_status"], + info["park_tries"], info["late"]), + ("firmware", True, "PARKED", 2, True)) + self.assertEqual(self.reg_writes(), []) + self.assertEqual(self.fake.opmode, SLEEP) + + def test_short_ver_reply_is_read_as_an_image_without_the_command(self): + self.fake.ver_payload = bytes(11) + rc, verdict, info = self.run_tool(fw="old", walking=False) + self.assertEqual((rc, verdict, info["path"], info["cap_bitmap"]), + (0, "PARK_OK", "register", None)) + + def test_no_reply_falls_back_to_the_register_path(self): + # an image whose VER bitmap lacks bit 7 (the fake's default for "silent") + rc, verdict, info = self.run_tool(fw="silent", walking=False) + self.assertEqual((rc, verdict, info["path"], info["park_cmd"]), + (0, "PARK_OK", "register", "no_reply")) + self.assertNotIn("park_cmd_err", info) + self.assertEqual(len(self.reg_writes()), 2) + + def test_other_err_proto_falls_back_with_its_text(self): + rc, verdict, info = self.run_tool(fw="bad_length", walking=False) + self.assertEqual((rc, verdict, info["path"], info["park_cmd"]), + (0, "PARK_OK", "register", "err_proto")) + self.assertIn("BAD_LENGTH", info["park_cmd_err"]) + + def test_refused_is_a_fail_and_touches_nothing(self): + rc, verdict, info = self.run_tool(fw="refuse") + self.assertEqual((rc, verdict), (4, "PARK_FAIL")) + self.assertEqual((info["path"], info["park_status"], info["persistent"], + info["opmode_readback"]), + ("firmware", "REFUSED", False, "0x85")) + self.assertEqual(self.reg_writes(), []) + self.assertNotIn(radio_park.SETTLE_S, self.time.slept) + + def test_a_leaky_gate_is_transient_on_the_firmware_path(self): + rc, verdict, info = self.run_tool(fw="leaky", walking=True) + self.assertEqual((rc, verdict), (5, "PARK_TRANSIENT")) + self.assertEqual((info["path"], info["persistent"]), ("firmware", False)) + self.assertEqual((info["opmode_readback"], info["opmode_after_settle"]), + ("0x80", "0x85")) + + def test_ver_failure_keeps_exit_2(self): + rc, verdict, info = self.run_tool(answer_ver=False) + self.assertEqual((rc, verdict, info["stage"]), (2, "PARK_FAIL", "ver")) + self.assertEqual(self.fake.sent(mg.HOST_TYPE_RADIO_PARK_REQ), []) + + def test_readback_failure_is_exit_3(self): + rc, verdict, info = self.run_tool(fw="split", answer_reg_read=False) + self.assertEqual((rc, verdict, info["stage"], info["path"]), + (3, "PARK_FAIL", "opmode", "firmware")) + + def test_park_line_is_last(self): + """leg_post.sh / power_up_guard.sh read `tail -1` and match PARK_OK*.""" + self.run_tool(fw="split") + lines = self.stdout.getvalue().strip().splitlines() + self.assertTrue(lines[-1].startswith("PARK_OK {"), lines[-1]) + self.assertEqual(sum(ln.startswith("PARK_") for ln in lines), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs12_leg_report_sil.py b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs12_leg_report_sil.py index a97edef4..6418e2f3 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs12_leg_report_sil.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs12_leg_report_sil.py @@ -1908,9 +1908,13 @@ def named(pc, sub, in_flight=frozenset()): self.assertEqual(lines[3], " !! the base's final RX_SCAN_FAILED at +30.0 s (rx_daemon.log " "line 9, sub 0x80): its scan entered FAILED there") self.assertEqual(len(lines), 4) # nothing read as TX_FIFO_RB_MISMATCH + # a final named AMBIGUOUS, as PR #166's daemons wrote it (before the 0x0D + # split) when its sub was an in-flight tx_id + amb = (121.4, urc(0xA3, "AMBIGUOUS", " attempt=3 final=1 warm=0 hw_suspect=1 " + "crc_seen=0 tx_id=0xA3")) with tempfile.TemporaryDirectory() as d: lines = rep.format_acquisition(self._acq(Path(d), base_extra=[ - named(31.4, 0x20), named(121.4, 0xA3, frozenset({0xA3}))])) + named(31.4, 0x20), amb])) self.assertIn("attempt 3 at +120.0 s (line 11, sub 0xA3: final, hw_suspect, AMBIGUOUS " "with TX_FIFO_RB_MISMATCH)", lines[2]) self.assertEqual(lines[3], " !! the base's final RX_SCAN_FAILED at +120.0 s " @@ -1918,6 +1922,27 @@ def named(pc, sub, in_flight=frozenset()): "TX_FIFO_RB_MISMATCH): its scan entered FAILED there if this " "URC was the scan's (the daemon could not rule out a TX FIFO " "readback mismatch on a TX with that tx_id)") + # 2026-10-24, the 0x0D split: the decoder now names that URC RX_SCAN_FAILED + # (the in-flight tx_id stays in basis=), so the report reads a plain final + self.assertIn("name=RX_SCAN_FAILED attempt=3 final=1 warm=0 hw_suspect=1 " + "crc_seen=0 basis=tx_id_in_flight tx_inflight=0xA3 ", + named(121.4, 0xA3, frozenset({0xA3}))[1]) + with tempfile.TemporaryDirectory() as d: + lines = rep.format_acquisition(self._acq(Path(d), base_extra=[ + named(31.4, 0x20), named(121.4, 0xA3, frozenset({0xA3}))])) + self.assertIn("attempt 3 at +120.0 s (line 11, sub 0xA3: final, hw_suspect)", lines[2]) + self.assertEqual(lines[3], " !! the base's final RX_SCAN_FAILED at +120.0 s " + "(rx_daemon.log line 11, sub 0xA3): its scan entered " + "FAILED there") + # a split image's FIFO fault is code 0x0F: never one of the base's scan faults + with tempfile.TemporaryDirectory() as d: + payload = bytes([0x0F, 0xFD, 0, 0]) + bytes(16) + (61400).to_bytes(4, "little") + acq = self._acq(Path(d), base_extra=[ + named(31.4, 0x20), + (61.4, "WARNING image_rx_daemon: " + mg.format_fault_urc_line( + payload, frozenset({0xFD})))]) + self.assertEqual([(f["code"], f["sub"]) for f in acq["faults"]], [(0x0D, 0x20)]) + self.assertNotIn("TX_FIFO_RB_MISMATCH", "\n".join(rep.format_acquisition(acq))) # a sub outside today's layout that the daemon still names RX_SCAN_FAILED (a # firmware that raised its retry budget) reads as a scan fault, not a FIFO one with tempfile.TemporaryDirectory() as d: @@ -1950,6 +1975,188 @@ def named(pc, sub, in_flight=frozenset()): acq["basis"]) +class AcquisitionOnTheRestartImage(unittest.TestCase): + """2026-10-24 review: on the RS-12.21 image (scan option (b), VER capability + bit 7) a final RX_SCAN_FAILED is a 30 s report and the scan goes on, so the + absorbing-FAILED rule of every older image must not decide the leg.""" + + VER_FIX = (1.0, "INFO image_rx_daemon: L072 VER warm-up ok (attempt 1) cap=0x000000FF") + VER_OLD = (1.0, "INFO image_rx_daemon: L072 VER warm-up ok (attempt 1) cap=0x0000007F") + + @staticmethod + def urc(pc, sub): + return (pc, f"WARNING image_rx_daemon: FAULT_URC: code=0x0D sub=0x{sub:02X} " + f"name=RX_SCAN_FAILED basis=x tx_inflight=- uptime_ms=1 raw=0d") + + def _acq(self, image="auto", **kw) -> dict: + with tempfile.TemporaryDirectory() as d: + arch, _ = _two_board_leg(Path(d), capture=False, mask=True, **kw) + return rep.acquisition(_read(arch / "rx_daemon.log"), + _read(arch / "tx_daemon.log"), image=image) + + def test_a_cold_final_then_a_lock_is_a_late_acquisition(self) -> None: + # memo section 6 T3: the base silent for 125 s sends a cold final at + # +120 s (sub 0xA3) and then LOCKs on the stream (here from +138 s on, + # 343 of the tractor's 606 fragments): ACQUIRED, late + late = dict(heard=range(264, 607), base_extra=[self.urc(121.4, 0xA3)]) + acq = self._acq(**dict(late, base_extra=[self.VER_FIX] + late["base_extra"])) + self.assertIs(acq["restarts"], True) + self.assertIn("logs the L072's VER capability bitmap 0x000000FF, bit 7 " + "(HOST_CAP_RADIO_PARK, the RS-12.21 image) set", acq["image"]) + self.assertEqual((acq["status"], acq["failed"]), (rep.ACQUIRED, False)) + self.assertTrue(acq["basis"].startswith( + "the base decoded 343 of the tractor's 606 fragments on air (56.6 %)"), + acq["basis"]) + self.assertIn("; a late acquisition: the base's final RX_SCAN_FAILED at +120.0 s " + "(rx_daemon.log line ", acq["basis"]) + self.assertIn("is cold (warm=0): the base's L072 had not locked since it booted, " + "but on this image a final is a 30 s report, not a stop (RS-12.21 " + "scan option (b): the scan restarts at once and reports again every " + "30 s until a LOCK; ", acq["basis"]) + self.assertTrue(acq["basis"].endswith( + "; the last final is cold (warm=0: no lock since the L072 booted), so the lock " + "shown came after +120.0 s"), acq["basis"]) + lines = rep.format_acquisition(acq) + self.assertTrue(lines[1].startswith(" confirmed acquisition: ACQUIRED: "), lines[1]) + self.assertIn("its scan reported FAILED there and scanned on (this image restarts " + "it at once and reports every 30 s until a LOCK, RS-12.21 (b))", + lines[-1]) + self.assertNotIn("!! ACQUISITION FAILED", "\n".join(lines)) + # the same log read by the absorbing rule: FAILED, as before -- with the + # image not identified (no VER line) the report says so and how to change it + acq = self._acq(**late) + self.assertIsNone(acq["restarts"]) + self.assertEqual(acq["status"], rep.FAILED) + lines = rep.format_acquisition(acq) + self.assertIn("The base's image was not identified (rx_daemon.log logs no VER " + "capability bitmap", lines[1]) + self.assertIn("run again with --scan-image restarts.", lines[1]) + self.assertIn("its scan entered FAILED there", lines[-1]) + # --scan-image restarts on that log, and an older image's bitmap (bit 7 clear) + self.assertEqual(self._acq(image="restarts", **late)["status"], rep.ACQUIRED) + acq = self._acq(**dict(late, base_extra=[self.VER_OLD] + late["base_extra"])) + self.assertIs(acq["restarts"], False) + self.assertEqual(acq["status"], rep.FAILED) + self.assertNotIn("not identified", "\n".join(rep.format_acquisition(acq))) + self.assertEqual(self._acq(image="absorbing", **dict( + late, base_extra=[self.VER_FIX] + late["base_extra"]))["status"], rep.FAILED) + with self.assertRaises(ValueError): + self._acq(image="b", **late) + + def test_finals_to_the_end_of_the_log_are_a_failed_acquisition(self) -> None: + # a final every 30 s up to 16.6 s before the log ends, and no lock shown + finals = [self.urc(121.4 + 30.0 * k, 0xA3) for k in range(7)] + acq = self._acq(heard=(40, 300, 560), base_extra=[self.VER_FIX] + finals) + self.assertEqual(acq["status"], rep.FAILED) + self.assertIn("; 7 final reports in a row, the last at +300.0 s (rx_daemon.log line ", + acq["basis"]) + self.assertTrue(acq["basis"].endswith( + "; nothing shows a lock after the last final (the log ends 16.6 s after the last " + "final, within the 35 s a next report may take)"), acq["basis"]) + lines = rep.format_acquisition(acq) + self.assertIn(". This image restarts the scan after each final RX_SCAN_FAILED " + "(RS-12.21 scan option (b)), so the base was not stranded on one " + "channel, but it never locked onto the tractor's hop grid in the " + "window: this leg's loss and command figures measure an unacquired " + "base, not the link. The tractor logged 606 frame(s) on air.", lines[1]) + self.assertNotIn("absorbing", lines[1]) + # warm finals from an early one on, with a lock shown that nothing places + # after the last: not determined + warm = [self.urc(122.0 + 30.0 * k, 0xC3) for k in range(7)] + acq = self._acq(heard=range(139, 607), base_extra=[self.VER_FIX] + warm) + self.assertEqual(acq["status"], rep.UNDETERMINED) + self.assertIn("came less than 122 s after the activation", acq["basis"]) + self.assertIn("(a lock resets the attempt count but on this image anchors the loss " + "timer at the locking frame (RS-12.21 L), so a locked scan drops back", + acq["basis"]) + self.assertIn("; nothing places the lock shown after the last final (the log ends " + "16.0 s after the last final, within the 35 s a next report may take), " + "and its evidence spans the leg: the base decoded 468 of", + acq["basis"]) + + def test_a_sign_of_a_lock_after_the_finals(self) -> None: + # a non-final attempt after the final: the attempt count restarts only at a + # LOCK, so the scan locked in between -- itself a lock shown + acq = self._acq(heard=(40, 300, 560), base_extra=[ + self.VER_FIX, self.urc(121.4, 0xA3), self.urc(251.4, 0x60)]) + self.assertEqual(acq["status"], rep.ACQUIRED) + self.assertTrue(acq["basis"].startswith( + "the base's RX_SCAN_FAILED at +250.0 s (rx_daemon.log line "), acq["basis"]) + self.assertIn(", sub 0x60) is attempt 0, not final: the attempt count restarts only " + "at a LOCK; a late acquisition: ", acq["basis"]) + self.assertTrue(acq["basis"].endswith( + "; the scan locked between +120.0 s and +250.0 s"), acq["basis"]) + # a warm early final, a lock shown, and no fault in the 196 s after it: + # this image would have reported again within 30 s without a LOCK + acq = self._acq(heard=range(264, 607), base_extra=[self.VER_FIX, + self.urc(122.0, 0xC3)]) + self.assertEqual(acq["status"], rep.ACQUIRED) + self.assertTrue(acq["basis"].endswith( + "; no RX_SCAN_FAILED followed the last final in the 196.0 s the log runs past " + "it, so the lock shown came within 30 s of +120.6 s"), acq["basis"]) + # two finals and no VER line: the second shows the image restarts its scan. + # The log runs 166.6 s past it with no report and no lock shown: a LOCK is + # likely (or the URCs were lost), so not determined, never FAILED + acq = self._acq(heard=(40, 300, 560), base_extra=[self.urc(121.4, 0xA3), + self.urc(151.4, 0xA3)]) + self.assertIs(acq["restarts"], True) + self.assertIn("after its final one at line ", acq["image"]) + self.assertIn("which only an image that restarts its scan does", acq["image"]) + self.assertEqual(acq["status"], rep.UNDETERMINED) + self.assertIn("; no RX_SCAN_FAILED followed the last final in the 166.6 s the log " + "runs past it, which on this image means a LOCK within 30 s of it " + "unless its FAULT URCs were lost, but nothing else shows one", + acq["basis"]) + # the same two at the log's end: FAILED + acq = self._acq(heard=(40, 300, 560), base_extra=[self.urc(271.4, 0xA3), + self.urc(301.4, 0xA3)]) + self.assertEqual((acq["restarts"], acq["status"]), (True, rep.FAILED)) + self.assertIn("This image restarts the scan", rep.format_acquisition(acq)[1]) + # two legacy lines of one URC (no FAULT_URC line, so both count) are not a + # second report: the image stays unidentified, and on the restart image + # they are one report + twin = [(121.4, "WARNING image_rx_daemon: FAULT during pipelined TX: code=0x0D " + "sub=0xA3 raw=0da3"), + (121.41, "WARNING image_rx_daemon: FAULT: code=0x0D sub=0xA3 raw=0da3")] + acq = self._acq(heard=(40, 300, 560), base_extra=twin) + self.assertEqual((len(acq["faults"]), acq["restarts"]), (2, None)) + acq = self._acq(image="restarts", heard=(40, 300, 560), + base_extra=[(271.4, twin[0][1]), (271.41, twin[1][1])]) + self.assertEqual(acq["status"], rep.FAILED) + self.assertNotIn("final reports in a row", acq["basis"]) + + def test_silence_counts_over_the_whole_window(self) -> None: + # leg 6's first frame at +138.1 s is past the 120 s budget: FAILED on an + # absorbing image, but on this image the scan does not strand + acq = self._acq(heard=LEG6, base_extra=[self.VER_FIX]) + self.assertEqual(acq["status"], rep.UNDETERMINED) + self.assertTrue(acq["basis"].startswith("a frame received is not a lock"), + acq["basis"]) + self.assertEqual(self._acq(heard=LEG6)["status"], rep.FAILED) + # nothing in the whole window: FAILED + acq = self._acq(heard=(), base_extra=[self.VER_FIX]) + self.assertEqual(acq["status"], rep.FAILED) + self.assertEqual(acq["basis"], "the base's stats counter still read rx_frames=0 at " + "+315.6 s (rx_daemon.log line 13), its last stats line " + "in the window (always on, it counts every image " + "fragment, logged or not): no image fragment reached " + "the base in the 316.6 s window, and its scan locks " + "only on a frame it decodes") + + def test_the_report_takes_scan_image(self) -> None: + with tempfile.TemporaryDirectory() as d: + arch, _ = _two_board_leg(Path(d), capture=False, mask=True, heard=range(264, 607), + base_extra=[self.urc(121.4, 0xA3)]) + auto = _run([arch]) + fix = _run([arch, "--scan-image", "restarts"]) + self.assertIn("!! ACQUISITION FAILED (see the acquisition line below)", auto) + self.assertIn("The base's image was not identified", auto) + self.assertNotIn("ACQUISITION FAILED", fix) + self.assertIn(" confirmed acquisition: ACQUIRED: the base decoded 343 of", fix) + self.assertIn("; a late acquisition: ", fix) + self.assertIn("--scan-image restarts given", fix) + + class LegReportEndToEnd(unittest.TestCase): """2026-10-24: the archive report on a leg-6 shaped leg.""" diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs13_scoring_sil.py b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs13_scoring_sil.py index 99f5a819..4772366a 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs13_scoring_sil.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_rs13_scoring_sil.py @@ -788,6 +788,37 @@ def test_void_unlocked_leaves_the_failed_leg_out_and_lists_it(self) -> None: self.assertEqual(plain.split("pooled FHSS")[0], void.split("voided by")[0]) self.assertLess(void.index("voided by"), void.index("pooled FHSS")) + def test_scan_image_reads_the_restart_image(self) -> None: + # 2026-10-24 review: on the RS-12.21 image (--scan-image restarts) a first + # frame past the budget is no failure -- its scan does not strand -- so the + # leg is not voided; a base that caught nothing in the whole window still + # fails, for that image's reason + with tempfile.TemporaryDirectory() as root: + legs = self._round(root, _fhss_log(range(1, 6), 138.1), + _streamed(_tractor_log([1, 2, 4, 5]))) + rc0, out0 = _run(ps.main, ["--void-unlocked", "--scan-image", "restarts"] + legs) + none = _archive(root, "Fv7", self.FHSS, _fhss_log([], None), _tractor_log([])) + rc1, out1 = _run(ps.main, ["--void-unlocked", "--scan-image", "restarts", + none, legs[0]]) + self.assertEqual(rc0, 0, out0) + self.assertNotIn("voided", out0) + self.assertIn(" base acquisition: NOT DETERMINED; first image reception 138.1 s after " + "the base's profile-1 activation, past the 120 s FHSS scan budget; " + "evidence: a frame received is not a lock", out0) + self.assertIn("pooled FHSS VECTOR 13/15 = 86.7 %", out0) + self.assertEqual(rc1, 0, out1) + self.assertIn(" !! base ACQUISITION FAILED; no image reception in the 299.5 s the base " + "log covers after its profile-1 activation, past the 120 s FHSS scan " + "budget; evidence: the base's stats counter still read rx_frames=0 at " + "+289.5 s (rx_daemon.log line ", out1) + self.assertIn("its last stats line in the window (always on, it counts every image " + "fragment, logged or not): no image fragment reached the base in the " + "299.5 s window, and its scan locks only on a frame it decodes. Its scan " + "scanned on (the RS-12.21 image restarts it after each final " + "RX_SCAN_FAILED) but never locked onto the hop grid", out1) + self.assertNotIn("parked on", out1) + self.assertIn("left out of the pool: Fv7.", out1) + def test_void_unlocked_keeps_legs_whose_acquisition_is_not_a_failure(self) -> None: # review 2026-10-24: only a failed acquisition voids. A log shorter than the # budget with no frame (not determined) and a log without the activation line diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md index 163072f3..af968dcd 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md @@ -1,6 +1,13 @@ # FHSS scan FAILED: options for a node that cannot recover -**Status:** decision memo. No option adopted. Nothing flashed, no radio used. +**Status:** decision memo, **adopted by the design owner (Dorkmo) on +2026-10-24**: the option (b) package of section 5 item 2 (b + L + d2, the +RS-12.21 park command in the same image, the 0x0D split of section 7), with +the decay-window trade-off of section 5 item 3 accepted +([`DECISIONS.md`](../../DECISIONS.md) D-FHSS1). Implemented on branch +`fhss-scan-recovery-2026-10-24` and built 2026-10-25 (bench `0daae279`, +production `2c5e055a`, section 9). **Not flashed; no radio used.** The +flash and the first radio checks are planned in section 6.1. **Date:** 2026-10-24 (main `5d00dac2`). **Decision owner:** the design owner (Dorkmo). **Rows:** RS-12.21 and RS-12.15 in [`../../TODO.md`](../../TODO.md). @@ -44,7 +51,8 @@ including the variants without L, `noL`, that section 4 quotes). - Four fix options were prototyped in the SIL only. (a), (b) and a host watchdog (c) all recover leg 6 in 100/100 replays. (d), a longer dwell, makes FAILED rarer but does not recover from it. -- **Recommendation for the design owner (section 5):** option (b) with the +- **Recommendation for the design owner (section 5; adopted 2026-10-24, + built 2026-10-25, not flashed):** option (b) with the loss-timer fix L and a 2 s dwell, flashed together with the RS-12.21 park command it needs and a new code for one of the two 0x0D faults. L and the 2 s dwell also change the tractor's normal RS-12.15 decay window @@ -378,7 +386,33 @@ the change in section 5, item 3. ## 5. Recommendation (for the design owner, Dorkmo) -This is a recommendation, not a decision. +This was a recommendation, not a decision. **Decision, 2026-10-24:** the +design owner adopted item 2 as written (b + L + d2, the park command and +the 0x0D split in one image) and accepted the decay-window trade-off of +item 3 (more own-grid renumbering for more commands received; closing the +decay window stays the open RS-12.15 grid-policy change). Recorded as +[`DECISIONS.md`](../../DECISIONS.md) D-FHSS1. Implemented 2026-10-25 on +branch `fhss-scan-recovery-2026-10-24`, built, **not flashed**: + +- scan: (b) in `radio/sx1276_rx.c` (the pure policy is unchanged, vector 16 + still pins "FAILED absorbs" there), L in `scan_drive`, d2 as a fixed + 2000 ms `SX1276_RX_SCAN_DWELL_MS`; the acceptance check is + `check-rx-scan-failed-follower` (name kept), and the shared simulation + now checks (b)'s rule, the walk order and L in every reverse-delivery + scenario. Its `-e` / `-e3` output on the real sources is byte-identical + to the b+L+d2 prototype's, so the figures in this section and in + section 4 stand for the built image; +- park: `HOST_TYPE_RADIO_PARK_REQ` 0x04, answered by `RADIO_PARK_URC` 0x84 + {status 0 PARKED / 1 already parked / 2 refused, RegOpMode readback}; + VER/READY capability bit 7 (`HOST_CAP_RADIO_PARK`, bitmap 0xFF; 0x7F + before); pinned by `check-radio-park`; +- fault codes: section 7's proposal, items 1-3 (TX_FIFO_RB_MISMATCH on + 0x0F; `HOST_PROTOCOL_VER` and `HOST_WIRE_SCHEMA_VER` unchanged, the + contract of item 5); +- hosts: `radio_park.py` (the firmware path on a bit-7 image), + `rs12_leg_report.py` / `rs13_probe_score.py` (`--scan-image`: on this + image a final 0x0D is a 30 s report, not FAILED), the 0x0D decode and + the `-InjectOn fault:0x0D:final` rule of item 4 (section 7). 1. **Now, no radio, no flash.** - Make the base's scan visible: log FAULT URCs in `image_rx_daemon.py` @@ -471,6 +505,149 @@ deploy to the X8s, which is also the operator's call. | T4 tractor twin | main, then fix | a 2026-10-18 leg-X shape (scan FAILED first) with 2.5 s camera stalls and commands; on the fix, the same stalls before FAILED too (the decay window, section 5, item 3) | main: commands received drop after the first stall with a command heard in it; fix: tractor `clk_demotion_*` counters move and delivery returns to the fix image's own pre-FAILED level (SIL-E3, b+L+d2: 59.7 % out of FAILED, 59.1 % before it, main 53.2 % before it) | | T5 fault codes | fix | the 0x0D split | scan faults arrive as 0x0D only; `tx_fifo_rb_bad` 0 and no 0x0F | +### 6.1 Flash and verify plan (the next radio day) + +Written 2026-10-25 with both radios off, for the round after it. Both +boards run `e083ae46` until step 6. Each step that flashes or opens +`/dev/ttymxc3` needs the operator's GO: the flash GO covers steps 6-10 +(steps 4-5 touch no radio beyond the power-up guard's read and park, the +GO rule's exception), and the rollback and each leg need their own. Commands and variables (`$BT`, `$BASE`, +`$TRAC`, `$R`, `$BI`, `$TI`) as in +[`BENCH_QUICKSTART.md`](../x8_lora_bootloader_helper/bench_tools/BENCH_QUICKSTART.md) +§3, §5 and §6; flashing in +[`FLASH_RUNBOOK.md`](../x8_lora_bootloader_helper/FLASH_RUNBOOK.md). + +**Before the GO (no radio).** + +1. Fly from a checkout that holds this work (the merged `main`, or this + branch), with its `bench.env` and a new dated `EVIDENCE_DIR`. The + harness pushes the daemons and probe helpers from the checkout it runs + in, so the FAULT decode, the base's VER capability line and + `radio_park.py`'s firmware path ride with it. +2. PowerShell, in `firmware/murata_l072`: `mingw32-make check` (every + target `[PASS]`, `check-radio-park`, `check-rx-scan-failed-follower` and + `check-rx-reverse-delivery` among them), then `mingw32-make clean`, + `mingw32-make`, `mingw32-make bench`. Want bench + `build/firmware_bench_diag.bin` md5 `0daae27978089eb97ab35c18718b62b0` + and production `build/firmware.bin` `2c5e055abff95126309a62409fafc654`, + 25 324 B each (section 9). Another md5 means another toolchain or + source: stop. +3. `powershell -NoProfile -File bench_tools\test_run_live_radio_monitor.ps1` + (in `x8_lora_bootloader_helper`): 0 failed. + +**Flash (the flash GO; a flash is a radio-on event).** + +4. Power-up guard and staging as every session (`legs/power_up_guard.sh`, + `legs/stage_boards.sh`). On `e083ae46` the new `radio_park.py` reports + `"cap_bitmap": "0x0000007F"`, `"park_cmd": "unknown_type"` and + `"path": "register"`: the fallback, as expected. +5. Stage and preflight each board without `--go`: + `bash "$BT/flash_l072.sh" tractor "$FW"`, with + `FW=$DC/firmware/murata_l072/build/firmware_bench_diag.bin`. Want the + `image:` line `25324 B md5 0daae279...` followed by `RS-12.21 bench + build (...)`, then `PREFLIGHT-OK`. The same for `base` (over ssh). +6. Flash the tractor, then the base, one at a time: + `bash "$BT/flash_l072.sh" tractor "$FW" --go`, then `base`. Success: + `Verify OK`, `flash_rc=0` and `PIPELINE-EXIT=0` in + `/home/fio/pipeline_stamped.log`; the board reboots in 12-24 s and + `/tmp` is wiped. Keep the consoles and a copy of the image in a + `firmware_flash/` folder of the session, as on 2026-10-18 + (`bench-evidence/RS_12_15_reverse_delivery_2026-10-18/firmware_flash/`). + Never put the production bin on a bench board. + +**After the flash (same GO; nothing transmits).** + +7. Stop the tractor's camera unit and container, `fuser /dev/ttymxc3` + empty, and re-stage `/tmp/lifetrac_strict` (`legs/stage_boards.sh`). + Do not use `legs/power_up_guard.sh` here: it parks the radio, and + steps 8 and 9 expect an unparked one. (If it was used anyway, step 8 + reads `radio_state=1` (SLEEP) and step 9 reports + `"park_status": "ALREADY"` with both readbacks `0x80`; that is also a + pass.) +8. `rs116_health_probe.py` on both: `STATS-OK`, + `RS115-INSTRUMENTED-FIRMWARE=YES`, `RS12-URC-COUNTERS=YES`, + `RS12-10-COUNTERS=YES` (the STATS layout did not change), + `tx_fifo_rb_bad` 0, `radio_state=4`. +9. Image and park in one step: `radio_park.py` on both. VER_URC still says + `0.0.0` / `dev`; the image shows in its capability bitmap. Want the last + line `PARK_OK` with `"cap_bitmap": "0x000000FF"`, `"path": "firmware"`, + `"park_status": "PARKED"`, `"persistent": true` and both readbacks + `0x80`. `"path": "register"` or `0x0000007F` means the old image is + still on the chip: stop. +10. The park holds: at least 2 min after step 9, read both with the + read-only `radio_state.py` (or `legs/power_up_guard.sh --check-only`): + `0x80`. Right after a flash the L072 is on its boot profile (fixed + 915 MHz, FHSS not initialised, no scan), so this proves the command + and the image. The test of the gate against the scan (T3's park + clause) is R1's leg-end park below, with profile 1 still active: on + `e083ae46` such a park held only after the scan had failed out, and on + this image the scan of a node without authority never stops. Anything + but `0x80` after a firmware `PARK_OK` is an arm path the gate missed: + stop radio work and power both boards down (tractor `systemctl + poweroff`; base `systemctl poweroff`, then remove its power). +11. Rollback, only if needed and on its own GO: the flown `e083ae46` copy, + `bench-evidence/RS_12_15_reverse_delivery_2026-10-18/firmware_flash/firmware_bench_diag_e083ae46.bin`, + with `flash_l072.sh`. + +**First legs (each on its own GO).** Each leg's daemon start activates +profile 1, which ends the park; `leg_post.sh` parks again at the end. + +- **R1: cold-start acquisition and the fault codes (T3's cold start, T5).** + The 2026-10-24 command-delivery shape: `legs/leg_prep.sh 2c_ + image_bw250 --reactive-fire 1` (`mono_g4`, profile 1, 2 fps, 300 s, + `-ProbeEcho 0 -TxBatch 0 -LogFragArrivals 1 -StopDrainS 4`), then + `legs/leg_post.sh`. Confirm: + - the base's `rx_daemon.log` has `L072 VER warm-up ok (attempt N) + cap=0x000000FF`; + - `leg_report.txt`: the `acquisition:` line within 120 s (2026-10-24 + legs 1-5: 15-75 s; SIL-E2 at 2 fps: 1.3 % of cold starts not locked + by 120 s) and `confirmed acquisition: ACQUIRED`, with the image read + from the capability line (no "image not identified" note); + - every `code=0x0D` line on either board is `name=RX_SCAN_FAILED` with + its attempt fields, none carries `fw=old` (a pre-split FIFO fault); + no `code=0x0F` line and Δ`tx_fifo_rb_bad` 0 on both (the FIFO fault + has never fired; a first 0x0F must match a `tx_fifo_rb_bad` step); + - a base 0x0D, if any, comes before its first lock, about 30 s apart; a + final one (attempt 3) is followed by more finals 30 s apart until the + lock (scan option (b)), and the report reads it as a late + acquisition, not FAILED; + - `leg_deltas.txt`: no `!! BASE NEVER LOCKED`, base + Δ`clk_demotion_reset` ≥ 1, the RS-12.15 criteria as on 2026-10-24, + probe delivery in line with that round (`mono_g4` 542/543); + - `leg_park.txt`: `PARK_OK` with `"path": "firmware"` on both at the + first attempt (no 60 s retries), and `power_up_guard.sh + --check-only` at least 2 min later reads `0x80` on both (T3's park + clause: profile 1 is still active, so without the gate the scan would + re-arm RXCONT every 2 s dwell); + - STOP: `!! ACQUISITION FAILED` or `!! BASE NEVER LOCKED` on this image. + Park, do not re-fly, and read the base's 0x0D lines first. +- **R2: the injector trigger and the tractor in FAILED (T4's first + half).** The 2026-10-18 leg-X shape: profile 1, 480 s, KF requests off: + `legs/leg_prep.sh 2c_ image_bw250 --duration 480`, with + `--reactive-fire` left at 0 (the base's PROBEs would lock the tractor's + scan before it fails out; `leg_prep.sh` already writes + `-KfRequestDisable 1`), and `-InjectCmd "dual_inject.py 120" -InjectOn + fault:0x0D:final` added to the harness line it prints. Confirm: the tractor's 0x0D `name=RX_SCAN_FAILED` + lines for attempts 0, 1 and 2, then attempt 3 final, about 30 s apart + (leg X: 0x00 0x01 0x02 0x83); `INJECT ... trigger: fault:0x0D:final` on + that attempt-3 line and on no earlier one; the base's commands heard with + the tractor in FAILED (leg X: 36/36); the RS-12.15 bracket criteria as + on 2026-10-18. A tractor that keeps streaming holds FAILED (it has + authority). If its stream stalls for 1 s or more it restarts its scan + under (b), so on this image a later final 0x0D and moving + `clk_demotion_*` counters are normal. +- **Later, not on the first day:** T1 (the base silent for ≥ 125 s, then + the stream) needs a harness option that delays the tractor's stream, + which is not built (build and test it offline first); T2 (a set of about + 10 cold starts); T4 with camera stalls. + +After the round: record it in the session's RESULTS.md, then update the +"on the boards" records (`murata_l072/README.md` "Expected md5s", +`FLASH_RUNBOOK.md`, `BENCH_BOARDS.md`, `BENCH_RUNBOOK.md` "Board facts", +`flash_l072.sh`'s build list) and the TODO.md RS-12.21 and RS-12.15 rows. +End the session with both radios parked by the firmware path and the +`--check-only` reading. + ## 7. Side item: FAULT code 0x0D means two faults `HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH` and `HOST_FAULT_CODE_RX_SCAN_FAILED` @@ -539,6 +716,16 @@ non-final scan attempt. the 2026-05-19 §13.1 #7 contract. Both md5s change: flash GO, md5 tables (`.github/workflows/l072-bench-md5.yml`, `README.md`). +**Adopted 2026-10-24, implemented 2026-10-25 (not flashed).** Items 1-3 as +proposed (`include/host_types.h`; the duplicate check covers every +HOST_FAULT_CODE_*). Item 4: the hosts name a 0x0D with a valid scan sub +RX_SCAN_FAILED on any image and keep the in-flight test only as a `basis=` +token; any other 0x0D sub is an old image's FIFO fault and gets `fw=old`; +0x0F is TX_FIFO_RB_MISMATCH. The version gate is the VER capability bit 7 +(`HOST_CAP_RADIO_PARK`), which the same image added with the park command; +`-InjectOn fault:0x0D:final` fires on a valid scan sub at attempt 3 only. +Item 5: no `HOST_PROTOCOL_VER` / `HOST_WIRE_SCHEMA_VER` bump. + Moving RX_SCAN_FAILED instead would touch more host code and leave every archived 0x0D needing version-gated decoding. @@ -681,6 +868,15 @@ one CRC error (R:490-492). `noL` to leave L out). With b, d2 and d10 also continue the walk across b's two restarts (b+d10r and plain b restart at channel 0, as their retries do). -- Unchanged by this work: the bench image (md5 `e083ae46a64686031b39866eca42e094`) - and the production image (md5 `148de24e1f9acce417c46c2c69a5be6b`), both - rebuilt and checked on 2026-10-24. +- Unchanged by the analysis of 2026-10-24: the bench image (md5 + `e083ae46a64686031b39866eca42e094`) and the production image (md5 + `148de24e1f9acce417c46c2c69a5be6b`), both rebuilt and checked that day; + `e083ae46` stays on both boards until the flash of section 6.1. +- The adopted image (section 5, 2026-10-25; branch + `fhss-scan-recovery-2026-10-24`, built, not flashed): bench + `build/firmware_bench_diag.bin` md5 `0daae27978089eb97ab35c18718b62b0`, + production `build/firmware.bin` md5 `2c5e055abff95126309a62409fafc654`, + 25 324 B each (APP 25 324 / 188 416 B, RAM 6 748 / 17 920 B, + `tools/check_size_budget.py` PASS), with arm-none-eabi-gcc 12.2.1 (Arm + GNU Toolchain 12.2.MPACBTI-Rel1). The same md5s from a clean copy of the + tree and again after `mingw32-make clean` in the worktree. diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/Makefile b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/Makefile index 601b2845..6804f136 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/Makefile +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/Makefile @@ -149,7 +149,7 @@ LDFLAGS := \ # -------------------------------------------------------------------------- # Targets # -------------------------------------------------------------------------- -.PHONY: all hello lora_ping_tx lora_ping_rx lora_ping_tx_active lora_ping_rx_active clean size check check-cpp check-asserts check-opmode-owner check-cfg-contract check-cfg-contract-unit check-cfg-contract-wire check-cfg-wire-owner check-rx-counter-owner check-tx-counter-owner check-lbt-owner check-airtime-counter-owner check-airtime-invariant check-fhss-scheduler check-fhss-clock check-fhss-authority check-rx-grid-policy check-rx-reverse-delivery check-rx-scan-failed-follower check-legal-dwell check-rfco-pertx check-rfco-summary-counters check-rfco-summary check-rfco-summary-emit check-rfco-summary-integration check-cfg-profile check-cfg-profile-wire check-cfg-clamp-fuzz check-lora-pkt-hdr check-rx-counters check-rx-retune-policy check-rx-retune-counters check-rx-scan-policy check-rx-scan-counters check-rx-scan-fail check-rx-scan-walker check-rx-frame-urc check-reg-write-gate check-rx-urc-lost help check-rx-fifo-track bench check-stats-layout +.PHONY: all hello lora_ping_tx lora_ping_rx lora_ping_tx_active lora_ping_rx_active clean size check check-cpp check-asserts check-opmode-owner check-cfg-contract check-cfg-contract-unit check-cfg-contract-wire check-cfg-wire-owner check-rx-counter-owner check-tx-counter-owner check-lbt-owner check-airtime-counter-owner check-airtime-invariant check-fhss-scheduler check-fhss-clock check-fhss-authority check-rx-grid-policy check-rx-reverse-delivery check-rx-scan-failed-follower check-radio-park check-legal-dwell check-rfco-pertx check-rfco-summary-counters check-rfco-summary check-rfco-summary-emit check-rfco-summary-integration check-cfg-profile check-cfg-profile-wire check-cfg-clamp-fuzz check-lora-pkt-hdr check-rx-counters check-rx-retune-policy check-rx-retune-counters check-rx-scan-policy check-rx-scan-counters check-rx-scan-fail check-rx-scan-walker check-rx-frame-urc check-reg-write-gate check-rx-urc-lost help check-rx-fifo-track bench check-stats-layout all: $(BUILD)/$(TARGET).bin $(BUILD)/$(TARGET).hex size @@ -267,8 +267,9 @@ AIRTIME_INVARIANT_BIN := $(BUILD)/airtime_invariant TX_LEN_GUARD_BIN := $(BUILD)/tx_len_guard RX_REVERSE_DELIVERY_BIN := $(BUILD)/rx_reverse_delivery RX_SCAN_FAILED_FOLLOWER_BIN := $(BUILD)/rx_scan_failed_follower +RADIO_PARK_BIN := $(BUILD)/radio_park -check: check-cpp check-asserts check-fhss-chantab check-fhss-scheduler check-fhss-clock check-fhss-authority check-rx-grid-policy check-rx-reverse-delivery check-rx-scan-failed-follower check-airtime-invariant check-tx-len-guard check-legal-dwell check-frames-per-dwell check-rfco-pertx check-rfco-summary-counters check-rfco-summary check-rfco-summary-emit check-rfco-summary-integration check-cfg-profile check-cfg-profile-wire check-cfg-clamp-fuzz check-lora-pkt-hdr check-rx-counters check-rx-retune-policy check-rx-retune-counters check-rx-scan-policy check-rx-scan-counters check-rx-scan-fail check-rx-scan-walker check-rx-frame-urc check-reg-write-gate check-rx-urc-lost check-rx-fifo-track check-stats-layout +check: check-cpp check-asserts check-fhss-chantab check-fhss-scheduler check-fhss-clock check-fhss-authority check-rx-grid-policy check-rx-reverse-delivery check-rx-scan-failed-follower check-radio-park check-airtime-invariant check-tx-len-guard check-legal-dwell check-frames-per-dwell check-rfco-pertx check-rfco-summary-counters check-rfco-summary check-rfco-summary-emit check-rfco-summary-integration check-cfg-profile check-cfg-profile-wire check-cfg-clamp-fuzz check-lora-pkt-hdr check-rx-counters check-rx-retune-policy check-rx-retune-counters check-rx-scan-policy check-rx-scan-counters check-rx-scan-fail check-rx-scan-walker check-rx-frame-urc check-reg-write-gate check-rx-urc-lost check-rx-fifo-track check-stats-layout @echo "[OK] memory map invariants hold" check-cpp: $(BUILD) @@ -926,7 +927,11 @@ check-fhss-clock: $(BUILD) # stalls (no self-follow in the decay window). Fails 21 checks against the # pre-fix sx1276_rx.c. sx1276_rx_scan_counters.c is NOT linked: the # harness provides sx1276_rx_scan_counter_record() to mirror the scan -# state machine. +# state machine. RS-12.21 (2026-10-24) adds per-pass structural checks to +# every scenario: scan FAILED only under originator authority, each FAIL +# held or left by that rule with every exit recorded, the scan walker in +# channel order across attempts, no LOCK demoted sooner than 2 s after its +# last anchor. check-rx-reverse-delivery: $(BUILD) @echo "[RXREV] Building rx-reverse-delivery SIL test (real sx1276_rx.c/sx1276_tx.c) ..." @$(HOST_CC) -std=gnu11 -Wall -Wextra -Werror -Iinclude -I. -Ibench/host_proto \ @@ -954,29 +959,31 @@ check-rx-reverse-delivery: $(BUILD) @echo "[RXREV] Running rx-reverse-delivery SIL test ..." @$(RX_REVERSE_DELIVERY_BIN) -# RS-12.21 / FHSS leg 6 (2b_f3, 2026-10-24): CHARACTERISATION OF KNOWN -# DEFECTS in the absorbing scan FAILED state. Reuses +# RS-12.21 / FHSS leg 6 (2b_f3, 2026-10-24): ACCEPTANCE CHECK of the scan +# FAILED fix -- option (b) + L + d2 of FHSS_SCAN_FAILED_OPTIONS.md, adopted +# 2026-10-24: FAILED is held only under originator authority, a LOCK starts +# the LOCKED loss timer, a 2 s scan dwell whose walk continues across +# attempts. (It replaces PR #167's characterisation of the defects.) Reuses # check-rx-reverse-delivery's simulated SX1276, clock, air and main loop # (rx_reverse_delivery.c is #included with RX_REVERSE_DELIVERY_SIM_ONLY) # against the same REAL sx1276_rx.c / sx1276_tx.c / scan / grid / clock -# TUs. (1) The base, a follower on profile 1: no frame for the 4 x 30 s -# scan budget (assumed, as hypothesis H1 has it), then a 2 fps originator -# stream and a reactive PROBE after each frame heard. Pinned: FAILED -# absorbs and a valid frame never LOCKs; the grid is adopted but never -# followed; the receiver stays on the walker's last channel, then re-arms -# on each PROBE's channel; consider_remote SNAPs or rejects against a -# scheduler epoch frozen at the last own TX or SNAP. (2) The tractor: -# scenario 8 (its scan runs out into FAILED) with camera stalls and the -# idle drain. Pinned: a command heard in a stall's decay window is adopted, -# nothing in FAILED undoes the adoption, and the tractor never holds -# originator authority or self-follows again. A failure means that -# behaviour changed: a firmware fix for RS-12.21 (its PR replaces these -# checks with the fix's acceptance checks; `rx_scan_failed_follower -e` -# prints the recovery measures) or an unrelated change to the scan -# constants, the walker, the TX path or the shared sim. Firmware path -# only: a host-side watchdog never flips it. +# TUs, with its RS-12.21 structural checks. (1) The base, a follower on +# profile 1: no frame for the 4 x 30 s scan budget (as hypothesis H1 has +# it), then a 2 fps originator stream and a reactive PROBE after each frame +# heard. Pinned: at the final FAIL it re-enters SCANNING on the walker's +# next channel, never parked; it LOCKs on the stream and from then on +# follows the hop grid (clock, channel, ALIGNED frames, >= 95 % heard, +# PROBEs on the grid) without a host re-arm. (1b) A silent follower: a +# fault every 30 s, a restart each time, all 50 channels walked. (2) The +# tractor: scenario 8 (its scan runs out into FAILED, held while it +# streams) with camera stalls and the idle drain. Pinned: a decay of its +# authority restarts the scan, a command heard in a decay window LOCKs and +# its demotion clears the adoption, and the tractor regains authority and +# self-follow. Each pinned behaviour fails on the pre-fix firmware. +# `rx_scan_failed_follower -e` / `-e3` print the memo's evaluation. +# Firmware path only: no host-side recovery runs in it. check-rx-scan-failed-follower: $(BUILD) - @echo "[RXSFF] Building rx-scan-failed-follower SIL characterisation (real sx1276_rx.c/sx1276_tx.c) ..." + @echo "[RXSFF] Building rx-scan-failed-follower SIL acceptance (real sx1276_rx.c/sx1276_tx.c) ..." @$(HOST_CC) -std=gnu11 -Wall -Wextra -Werror -Iinclude -I. -Ibench/host_proto \ -DLIFETRAC_FHSS_TX_ROUTED=1 \ bench/host_proto/rx_scan_failed_follower.c \ @@ -999,9 +1006,64 @@ check-rx-scan-failed-follower: $(BUILD) radio/sx1276_fhss.c \ radio/sx1276_fhss_chantab.c \ -o $(RX_SCAN_FAILED_FOLLOWER_BIN) - @echo "[RXSFF] Running rx-scan-failed-follower SIL characterisation (known defects) ..." + @echo "[RXSFF] Running rx-scan-failed-follower SIL acceptance (RS-12.21 (b) + L + d2) ..." @$(RX_SCAN_FAILED_FOLLOWER_BIN) +# RS-12.21 (2026-10-24): the FIRMWARE RADIO PARK (HOST_TYPE_RADIO_PARK_REQ +# 0x04 / RADIO_PARK_URC 0x84, include/host_types.h), the bench's persistent +# radio-off state now that scan option (b) keeps a node without authority +# scanning, plus the 0x0D split (TX_FIFO_RB_MISMATCH on 0x0F). Reuses +# check-rx-reverse-delivery's simulated SX1276, clock, air and main loop +# (rx_reverse_delivery.c #included with RX_REVERSE_DELIVERY_SIM_ONLY and +# RX_SIM_REAL_HOST) against the REAL host_cmd.c (dispatcher, TX ring, +# REG_WRITE gate), host_cfg.c / host_cfg_profile.c (CFG_SET REG_PROFILE) and +# the same real radio TUs; every request through host_cmd_dispatch(), every +# reply captured. Pinned: parked after a final FAIL (base), while LOCKED +# (base, slot-follower and gamma-1 cases) and in a held FAILED (tractor, +# (b)'s restart case), nothing touches the modem or runs the scan machine +# for minutes; TX refused while parked (ERR_PROTO FORBIDDEN); the park +# refused while a TX is keyed or queued; ended by a raw RXCONT and by a +# profile activation, not by raw STANDBY/SLEEP (nor RXSINGLE, which the +# production register gate this SIL links refuses; on a diag build a raw +# 0x86 is accepted and ends the park); a SCANNING node +# resumes its dwell and attempt with the parked time not counted, a LOCKED +# node parked past the 2 s loss timer demotes at its first tick; FAULT 0x0F +# at the FIFO-readback emitter. +# -Wno-unused-function as in the firmware build: host_cmd.c keeps helpers +# (put_u16_le) that nothing calls. +check-radio-park: $(BUILD) + @echo "[PARK ] Building radio-park SIL test (real host_cmd.c, sx1276_rx.c, sx1276_tx.c) ..." + @$(HOST_CC) -std=gnu11 -Wall -Wextra -Werror -Wno-unused-function -Iinclude -I. -Ibench/host_proto \ + -DLIFETRAC_FHSS_TX_ROUTED=1 \ + bench/host_proto/radio_park.c \ + host/host_cmd.c \ + host/host_cfg.c \ + host/host_cfg_profile.c \ + host/host_cfg_wire.c \ + host/host_rx_wire.c \ + host/host_reg_gate.c \ + host/host_rfco.c \ + radio/sx1276_rx.c \ + radio/sx1276_tx.c \ + radio/sx1276_airtime.c \ + radio/lora_pkt_hdr.c \ + radio/sx1276_legal_dwell.c \ + radio/sx1276_rx_counters.c \ + radio/sx1276_rx_retune_policy.c \ + radio/sx1276_rx_retune_counters.c \ + radio/sx1276_rx_scan_policy.c \ + radio/sx1276_rx_scan_fail.c \ + radio/sx1276_rx_scan_walker.c \ + radio/sx1276_rx_fifo_track.c \ + radio/sx1276_rx_grid_policy.c \ + radio/sx1276_fhss_authority.c \ + radio/sx1276_fhss_clock.c \ + radio/sx1276_fhss.c \ + radio/sx1276_fhss_chantab.c \ + -o $(RADIO_PARK_BIN) + @echo "[PARK ] Running radio-park SIL test (RS-12.21 firmware park + 0x0D split) ..." + @$(RADIO_PARK_BIN) + $(BUILD): @$(call MKDIR_P,$(BUILD)) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md index 60acf2eb..7595c4a9 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/README.md @@ -107,14 +107,28 @@ python3 tools/check_size_budget.py build/firmware.elf ### Expected md5s -At `13f20aa1` (the RS-12.15 reverse-delivery fix, built on the bench PC -2026-10-14 and repeated after `mingw32-make clean`), with the toolchain above -and the default flags: +The RS-12.21 image (scan option (b) + L + d2, the firmware park command and +the 0x0D split, [FHSS_SCAN_FAILED_OPTIONS.md](FHSS_SCAN_FAILED_OPTIONS.md) +section 5 item 2, adopted 2026-10-24 as [DECISIONS.md](../../DECISIONS.md) +D-FHSS1; branch `fhss-scan-recovery-2026-10-24`), built on the bench PC +2026-10-25, repeated from a clean copy of the tree and again after +`mingw32-make clean`, with the toolchain above and the default flags. +**Built, not flashed** (the flash and its checks: the memo's section 6.1, +on the operator's GO): + +| image | md5 | size | +|---|---|---| +| `build/firmware_bench_diag.bin` (bench; not flashed yet) | `0daae27978089eb97ab35c18718b62b0` | 25 324 B | +| `build/firmware.bin` (production, committed) | `2c5e055abff95126309a62409fafc654` | 25 324 B | + +The image flashed on the boards until then, at `13f20aa1` (the RS-12.15 +reverse-delivery fix, built 2026-10-14 and repeated after +`mingw32-make clean`): | image | md5 | size | |---|---|---| | `build/firmware_bench_diag.bin` (bench; on both boards since 2026-10-18) | `e083ae46a64686031b39866eca42e094` | 24 972 B | -| `build/firmware.bin` (production, committed) | `148de24e1f9acce417c46c2c69a5be6b` | 24 972 B | +| `build/firmware.bin` (production) | `148de24e1f9acce417c46c2c69a5be6b` | 24 972 B | Both bench boards run `e083ae46` since 2026-10-18, flashed for the RS-12.15 reverse-delivery round (Verify OK, `flash_rc=0` on both; diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/fhss_stub.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/fhss_stub.c index 10e56ab2..5992c3df 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/fhss_stub.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/fhss_stub.c @@ -94,3 +94,9 @@ uint32_t sx1276_stub_last_budget_us(void) { * radio/sx1276_rx.c must not see a second definition. */ void sx1276_rx_scan_reset(void) { } + +/* RS-12.21 (2026-10-24): host_cfg_profile_activate() also ends a firmware + * radio park. Same placement rationale; check-radio-park pins the real one + * through the real activation. */ +void sx1276_rx_unpark(void) { +} diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/radio_park.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/radio_park.c new file mode 100644 index 00000000..bf7f0d9f --- /dev/null +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/radio_park.c @@ -0,0 +1,1055 @@ +/* + * RS-12.21 (2026-10-24): the FIRMWARE RADIO PARK (HOST_TYPE_RADIO_PARK_REQ, + * include/host_types.h), system-in-the-loop against the real firmware. + * + * WHY. The bench parks both L072s between legs. radio_park.py did it with + * raw STANDBY and SLEEP writes and a 0x80 readback, which held only because + * a node without traffic ran its scan out into FAILED and parked itself + * there (TODO RS-12.21). Scan option (b) (FHSS_SCAN_FAILED_OPTIONS.md + * section 5 item 2, adopted 2026-10-24) removes that self-park: a node + * without originator authority scans on after its budget, re-arming RXCONT + * every 2 s dwell and reporting a final RX_SCAN_FAILED every 30 s, so a host + * SLEEP write holds only until the next walker step. The park command is the + * firmware's own persistent radio-off state, flashed in the same image. + * + * WHAT. The simulated SX1276, clock, air and main loop of + * check-rx-reverse-delivery (rx_reverse_delivery.c, #included with + * RX_REVERSE_DELIVERY_SIM_ONLY and RX_SIM_REAL_HOST) against the REAL + * host_cmd.c (dispatcher, TX ring, REG_WRITE gate), host_cfg.c and + * host_cfg_profile.c (CFG_SET REG_PROFILE activation), host_reg_gate.c, + * and the same real sx1276_rx.c / sx1276_tx.c / scan / grid / clock TUs. + * Every host request goes through host_cmd_dispatch() and every reply is + * captured at host_uart_send_urc(), FAULT_URCs included (fed back to the + * shared simulation's bookkeeping). The shared structural checks + * (check_structure) run in every scenario. The production build's REG_WRITE + * gate is used (HOST_ALLOW_REG_WRITE_DIAG = 0). + * + * ASSERTED (sequence numbers match the output): + * B boot: the flag is clear at start -- a reset zeroes it, and this + * process start is that reset; READY_URC and VER_URC advertise + * HOST_CAP_RADIO_PARK; the profile-1 activation goes through CFG_SET. + * P1 park after the final FAIL (base, nothing on air): the scan runs out + * (four RX_SCAN_FAILED, the last final) and, by (b), scans on. PARK_REQ + * -> RADIO_PARK_URC {0 PARKED, 0x80}, same seq, IRQ flags cleared. + * Then 10 min with a stream on air: the modem ends every pass in SLEEP, + * no firmware call touches it (no retune, standby, RX arm or TX), the + * scan machine records nothing (unparked: a walker step every 2 s, a + * FAIL and a (b) restart every 30 s), no FAULT goes out, no frame is + * heard. REG_READ 0x01 = 0x80; a repeat PARK_REQ -> {1 ALREADY, 0x80}; + * TX_FRAME_REQ -> ERR_PROTO {0x10, 1, FORBIDDEN, 0, 0} with its seq, + * nothing queued, keyed or reported (no RFCO); sx1276_tx_begin() and + * sx1276_rx_arm() refuse. A host raw RXCONT (REG_WRITE 0x01 = 0x85) + * ends it: RX_CONT at once; the interrupted attempt FAILs exactly 30 s + * plus the time parked after it began (the parked time not counted, + * no FAIL at the first tick); the base then LOCKs onto a stream. + * P2 park while LOCKED (the base following a stream), with a frame's DIO0 + * edge latched: that frame is not delivered and no header considered. + * 2 min LOCKED on an adopted grid with a valid clock (the slot + * follower's case), then 2 min with the clock reset (the gamma-1 + * fallback's case): nothing touches the modem, the scan machine stays + * LOCKED and records nothing, gamma-1 records no decision. CFG_SET + * REG_PROFILE=1 ends it: the scan starts over and LOCKs again. + * P3 park against TX: PARK_REQ while a TX is on air -> {2 REFUSED, 0x83}, + * nothing changed, and the post-TX re-arm runs at its TX_DONE; while a + * TX_FRAME_REQ waits in host_cmd.c's TX ring -> REFUSED, and the frame + * goes out; then PARKED. The 0x0D split at its emitter: that first + * TX's FIFO, altered before TX_DONE, raises FAULT 0x0F + * (TX_FIFO_RB_MISMATCH), sub = its tx_id, and no 0x0D. Raw writes that + * do not end the park: RXSINGLE (refused in a production build), + * STANDBY (the modem leaves SLEEP, and for 60 s nothing arms it; a + * repeat PARK_REQ puts it back: ALREADY, 0x80) and SLEEP. RESET_REQ + * reaches platform_system_reset(). A raw RXCONT ends it, and a + * TX_FRAME_REQ goes on air again. + * P4 the tractor: its scan ran out into FAILED, held while it streams; + * the camera daemon stops and the tractor is parked while it still + * holds authority. 5 min: the authority decays, yet no (b) restart, + * nothing touches the modem, FAILED kept. The daemon's raw RXCONT ends + * the park, the first scan tick restarts the scan (b), and the camera + * streams again. + * P5 a raw RXCONT unpark resumes the scan machine as sx1276_rx_unpark() + * promises (2026-10-24 review): parked 10 s while SCANNING, 700 ms + * into a dwell, the next walker step comes the dwell's remaining + * 1300 ms after the unpark, not at the first tick; parked 10 s while + * LOCKED on an adopted grid with the stream still on air, the clock + * is stale at the unpark (F6) and the scan demotes on the first tick + * after it (its loss timer counts the park, as the clock's freshness + * does), then re-acquires. + * Each scenario fails with the park gates in sx1276_rx.c removed (shown + * 2026-10-24 in a scratch copy); P5 with the SCANNING anchor shift removed + * or the shift applied to LOCKED as well. + */ + +#define RX_REVERSE_DELIVERY_SIM_ONLY 1 +#define RX_SIM_REAL_HOST 1 +/* Some of its helpers are not used here. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#include "rx_reverse_delivery.c" +#pragma GCC diagnostic pop + +#include "host_cfg.h" +#include "reset_cause.h" + +/* The wire values the hosts were built against (RS-12.21 decision record): + * a change here is a wire change, not a refactor. */ +_Static_assert(HOST_TYPE_RADIO_PARK_REQ == 0x04U, "RS-12.21: PARK_REQ is 0x04"); +_Static_assert(HOST_TYPE_RADIO_PARK_URC == 0x84U, "RS-12.21: PARK_URC is 0x84"); +_Static_assert(HOST_RADIO_PARK_URC_PAYLOAD_LEN == 2U && + HOST_RADIO_PARK_STATUS_PARKED == 0U && + HOST_RADIO_PARK_STATUS_ALREADY == 1U && + HOST_RADIO_PARK_STATUS_REFUSED == 2U, + "RS-12.21: PARK_URC is {status 0/1/2, opmode}"); +_Static_assert(HOST_FAULT_CODE_RX_SCAN_FAILED == 0x0DU && + HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH == 0x0FU, + "the 0x0D split: RX_SCAN_FAILED keeps 0x0D, the FIFO fault is 0x0F"); +_Static_assert((HOST_CAPABILITY_BITMAP & HOST_CAP_RADIO_PARK) != 0UL, + "RS-12.21: the park is advertised"); +_Static_assert(HOST_PROTOCOL_VER == 1U && HOST_WIRE_SCHEMA_VER == 2U, + "RS-12.21: additive changes, no version bump (host_types.h)"); + +#define FINAL_BIT ((uint8_t)(1U << SX1276_RX_SCAN_FAIL_SUB_BIT_FINAL)) +#define OPMODE_SLEEP 0x80U +#define OPMODE_TX 0x83U + +/* ------------------------------------------------------------------ */ +/* Host link: replies captured, host-side collaborators stubbed */ +/* ------------------------------------------------------------------ */ + +typedef struct { + uint32_t count; + uint16_t seq; + uint16_t len; + uint8_t payload[32]; +} urc_slot_t; +static urc_slot_t g_urc[256]; /* last URC of each type, and a count */ +static uint32_t g_fault_n[256]; /* FAULT_URCs by code */ +static uint8_t g_fault_sub[256]; /* ... and the last one's sub */ + +void host_uart_send_urc(uint8_t type, uint16_t seq, uint8_t flags, + const uint8_t *payload, uint16_t payload_len) { + urc_slot_t *u = &g_urc[type]; + (void)flags; + ++u->count; + u->seq = seq; + u->len = payload_len; + memset(u->payload, 0, sizeof(u->payload)); + if (payload != NULL) { + memcpy(u->payload, payload, + (payload_len < sizeof(u->payload)) ? payload_len : sizeof(u->payload)); + } + if (type == HOST_TYPE_FAULT_URC && payload != NULL && payload_len >= 2U) { + ++g_fault_n[payload[0]]; + g_fault_sub[payload[0]] = payload[1]; + sim_note_fault(payload[0], payload[1]); + } +} + +/* host_uart.c is not linked: its ERR_PROTO packer, as there. */ +void host_uart_send_err_proto(uint16_t seq, uint8_t offending_type, + uint8_t offending_ver, uint8_t err_code, + uint16_t detail) { + uint8_t payload[5]; + payload[0] = offending_type; + payload[1] = offending_ver; + payload[2] = err_code; + payload[3] = (uint8_t)(detail & 0xFFU); + payload[4] = (uint8_t)((detail >> 8) & 0xFFU); + host_uart_send_urc(HOST_TYPE_ERR_PROTO_URC, seq, 0U, payload, + (uint16_t)sizeof(payload)); +} +void host_uart_send_ascii(const char *text) { (void)text; } +void host_uart_note_diag_mark(uint8_t mark) { (void)mark; } + +/* host_stats.c is not linked (the radio-side counters are the sim's). */ +void host_stats_reset(void) {} +void host_stats_note_radio_events(uint32_t events) { (void)events; } +uint16_t host_stats_serialize(uint8_t *out, uint16_t out_cap) { + (void)out; (void)out_cap; + return 0U; +} + +/* platform (the clock is the sim's). */ +reset_cause_t platform_reset_cause_take(void) { return RESET_CAUSE_POR_BOR; } +uint8_t platform_clock_source_id(void) { return (uint8_t)PLATFORM_CLOCK_SOURCE_HSE_OK; } +bool platform_fault_take_replay(platform_fault_record_t *out_record) { + (void)out_record; + return false; +} +void platform_diag_trace(const char *text) { (void)text; } +static uint32_t g_system_resets; +void platform_system_reset(void) { ++g_system_resets; } + +/* The sx1276.c setters host_cfg.c / host_cfg_profile.c call; the sim's + * register file already holds SF7 / BW250 / CR4/5. */ +void sx1276_set_tx_power_dbm(uint8_t dbm) { (void)dbm; } +void sx1276_set_sf_bw_cr(uint8_t sf, uint16_t bw_khz, uint8_t cr_den) { + (void)sf; (void)bw_khz; (void)cr_den; +} + +/* sx1276_modes.c's mirror of a host raw RegOpMode write (handle_reg_write). + * In the sim the tracked state IS the chip, so the write lands here. */ +void sx1276_modes_sync_external(uint8_t raw_opmode) { + switch (raw_opmode & 0x07U) { + case 0x00U: + g_radio.mode = SX1276_STATE_SLEEP; + break; + case 0x05U: + g_radio.mode = SX1276_STATE_RX_CONT; + g_radio.rx_since_us = g_now_us; + break; + default: + g_radio.mode = SX1276_STATE_STANDBY; + break; + } + trace_mix(5U, raw_opmode); +} + +/* ------------------------------------------------------------------ */ +/* Host requests through the real dispatcher */ +/* ------------------------------------------------------------------ */ + +static uint16_t g_host_seq = 0x0100U; + +static uint16_t host_req(uint8_t type, const uint8_t *payload, uint16_t len) { + static host_frame_t f; + const ctx_t saved = g_ctx; + memset(&f, 0, sizeof(f)); + f.ver = HOST_PROTOCOL_VER; + f.type = type; + f.seq = ++g_host_seq; + f.payload_len = len; + if (len > 0U) { + memcpy(f.payload, payload, len); + } + g_ctx = CTX_HOST; + host_cmd_dispatch(&f); + g_ctx = saved; + return f.seq; +} + +/* PARK_REQ: the reply's status byte (0xFF: no well-formed reply), its + * opmode byte in *opmode. */ +static uint8_t park_req(uint8_t *opmode) { + const urc_slot_t *u = &g_urc[HOST_TYPE_RADIO_PARK_URC]; + const uint32_t n0 = u->count; + const uint16_t seq = host_req(HOST_TYPE_RADIO_PARK_REQ, NULL, 0U); + CHECK(u->count == n0 + 1U && u->seq == seq && + u->len == HOST_RADIO_PARK_URC_PAYLOAD_LEN, + "[%s] PARK_REQ seq %u: %u replies, seq %u, len %u (expect 1, %u, %u)", + g_sc.name, seq, u->count - n0, u->seq, u->len, seq, + HOST_RADIO_PARK_URC_PAYLOAD_LEN); + if (u->count != n0 + 1U) { + return 0xFFU; + } + *opmode = u->payload[1]; + return u->payload[0]; +} + +static void reg_write_ok(uint8_t reg, uint8_t value) { + const uint8_t p[2] = { reg, value }; + const uint32_t n0 = g_urc[HOST_TYPE_REG_WRITE_ACK_URC].count; + const uint16_t seq = host_req(HOST_TYPE_REG_WRITE_REQ, p, 2U); + CHECK(g_urc[HOST_TYPE_REG_WRITE_ACK_URC].count == n0 + 1U && + g_urc[HOST_TYPE_REG_WRITE_ACK_URC].seq == seq, + "[%s] REG_WRITE %02x=%02x not acknowledged", g_sc.name, reg, value); +} + +static void reg_write_refused(uint8_t reg, uint8_t value) { + const uint8_t p[2] = { reg, value }; + const urc_slot_t *e = &g_urc[HOST_TYPE_ERR_PROTO_URC]; + const uint32_t n0 = e->count; + const uint32_t ack0 = g_urc[HOST_TYPE_REG_WRITE_ACK_URC].count; + const uint16_t seq = host_req(HOST_TYPE_REG_WRITE_REQ, p, 2U); + CHECK(e->count == n0 + 1U && e->seq == seq && + e->payload[2] == HOST_ERR_PROTO_FORBIDDEN && + g_urc[HOST_TYPE_REG_WRITE_ACK_URC].count == ack0, + "[%s] REG_WRITE %02x=%02x was not refused", g_sc.name, reg, value); +} + +static uint8_t reg_read(uint8_t reg) { + const urc_slot_t *u = &g_urc[HOST_TYPE_REG_DATA_URC]; + const uint32_t n0 = u->count; + const uint16_t seq = host_req(HOST_TYPE_REG_READ_REQ, ®, 1U); + CHECK(u->count == n0 + 1U && u->seq == seq && u->payload[0] == reg, + "[%s] REG_READ %02x: no reply", g_sc.name, reg); + return u->payload[1]; +} + +/* TX_FRAME_REQ with a 16-byte body. */ +static uint16_t tx_req(uint8_t tx_id) { + uint8_t p[2U + CMD_PAYLOAD_LEN]; + memset(p, 0, sizeof(p)); + p[0] = tx_id; + p[1] = (uint8_t)CMD_PAYLOAD_LEN; + p[2] = MARK_BASE_CMD; + p[3] = tx_id; + return host_req(HOST_TYPE_TX_FRAME_REQ, p, (uint16_t)sizeof(p)); +} + +static uint8_t cfg_set_req(uint8_t key, const uint8_t *value, uint8_t len) { + uint8_t p[2U + CFG_KEY_MAX_VALUE_LEN]; + const urc_slot_t *u = &g_urc[HOST_TYPE_CFG_OK_URC]; + const uint32_t n0 = u->count; + uint16_t seq; + p[0] = key; + p[1] = len; + memcpy(&p[2], value, len); + seq = host_req(HOST_TYPE_CFG_SET_REQ, p, (uint16_t)(2U + len)); + CHECK(u->count == n0 + 1U && u->seq == seq && u->payload[0] == key, + "[%s] CFG_SET %02x: no CFG_OK reply", g_sc.name, key); + return u->payload[1]; +} + +/* CFG_SET REG_PROFILE=1 -- host_cfg.c -> host_cfg_profile_activate(). */ +static void activate_fhss(void) { + const uint8_t profile = (uint8_t)REG_PROFILE_FCC_15_247_FHSS_50CH_BW250; + CHECK(cfg_set_req(CFG_KEY_REG_PROFILE, &profile, 1U) == (uint8_t)CFG_STATUS_OK && + host_cfg_profile_active()->profile_id == profile, + "[%s] the profile-1 activation failed", g_sc.name); + /* The activation reset the scan machine to BOOT. */ + g_scan_mirror = SX1276_RX_SCAN_STATE_BOOT; +} + +/* A TX_FRAME_REQ while parked: ERR_PROTO {0x10, ver, FORBIDDEN, 0, 0} with + * its seq, the existing refusal; nothing queued, keyed or reported. */ +static void expect_tx_refused(uint8_t tx_id) { + const urc_slot_t *e = &g_urc[HOST_TYPE_ERR_PROTO_URC]; + const uint32_t n0 = e->count; + const uint32_t tx0 = g_calls.tx; + const uint32_t rfco0 = g_urc[HOST_TYPE_RFCO_PERTX_URC].count; + const uint16_t seq = tx_req(tx_id); + CHECK(e->count == n0 + 1U && e->seq == seq && e->len == 5U && + e->payload[0] == HOST_TYPE_TX_FRAME_REQ && + e->payload[1] == HOST_PROTOCOL_VER && + e->payload[2] == HOST_ERR_PROTO_FORBIDDEN && + e->payload[3] == 0U && e->payload[4] == 0U, + "[%s] TX_FRAME_REQ while parked: %u ERR_PROTO replies, last" + " {%02x %02x %02x %02x %02x} seq %u (expect {10 01 08 00 00} seq %u)", + g_sc.name, e->count - n0, e->payload[0], e->payload[1], + e->payload[2], e->payload[3], e->payload[4], e->seq, seq); + CHECK(!sx1276_tx_busy() && !host_cmd_tx_mailbox_pending() && + g_calls.tx == tx0 && + g_urc[HOST_TYPE_RFCO_PERTX_URC].count == rfco0 && + g_radio.mode == SX1276_STATE_SLEEP, + "[%s] TX_FRAME_REQ while parked: queued %u, keyed %u, RFCO %u," + " modem state %u", g_sc.name, (unsigned)host_cmd_tx_mailbox_pending(), + g_calls.tx - tx0, g_urc[HOST_TYPE_RFCO_PERTX_URC].count - rfco0, + (unsigned)g_radio.mode); +} + +/* ------------------------------------------------------------------ */ +/* Observation */ +/* ------------------------------------------------------------------ */ + +typedef struct { + uint32_t freq, standby, rx_cont, tx, sleep, scan_rec; + uint32_t faults, frames_recv, retune_dec, consider_dec; +} snap_t; + +static uint32_t total_faults(void) { + uint32_t n = 0U; + for (unsigned i = 0U; i < 256U; ++i) { + n += g_fault_n[i]; + } + return n; +} + +static snap_t snap_now(void) { + snap_t s; + uint32_t rt[SX1276_RX_RETUNE_DECISION_DIM]; + uint32_t cr[SX1276_RX_CONSIDER_REMOTE_COUNT_DIM]; + memset(&s, 0, sizeof(s)); + s.freq = g_calls.freq; + s.standby = g_calls.standby; + s.rx_cont = g_calls.rx_cont; + s.tx = g_calls.tx; + s.sleep = g_calls.sleep; + s.scan_rec = g_calls.scan_rec; + s.faults = total_faults(); + s.frames_recv = g_sc.frames_recv; + sx1276_rx_retune_counts(rt); + for (unsigned i = 0U; i < SX1276_RX_RETUNE_DECISION_DIM; ++i) { + s.retune_dec += rt[i]; + } + sx1276_rx_consider_remote_counts(cr); + for (unsigned i = 0U; i < SX1276_RX_CONSIDER_REMOTE_COUNT_DIM; ++i) { + s.consider_dec += cr[i]; + } + return s; +} + +/* Nothing touched the parked modem, the scan machine stood still. */ +static void expect_untouched(const char *what, const snap_t *a, const snap_t *b) { + CHECK(b->freq == a->freq && b->standby == a->standby && + b->rx_cont == a->rx_cont && b->tx == a->tx, + "[%s] %s: the firmware touched the parked modem (retunes %u," + " standby %u, RX arms %u, TX %u)", g_sc.name, what, + b->freq - a->freq, b->standby - a->standby, + b->rx_cont - a->rx_cont, b->tx - a->tx); + CHECK(b->scan_rec == a->scan_rec, + "[%s] %s: the scan machine ran while parked (%u records)", + g_sc.name, what, b->scan_rec - a->scan_rec); + CHECK(b->faults == a->faults, + "[%s] %s: %u FAULT URCs while parked", g_sc.name, what, + b->faults - a->faults); + CHECK(b->retune_dec == a->retune_dec, + "[%s] %s: gamma-1 decided %u times while parked", g_sc.name, what, + b->retune_dec - a->retune_dec); + CHECK(b->frames_recv == a->frames_recv && b->consider_dec == a->consider_dec, + "[%s] %s: %u frames delivered, %u headers considered while parked", + g_sc.name, what, b->frames_recv - a->frames_recv, + b->consider_dec - a->consider_dec); +} + +/* Base role: the shared simulation's ideal originator, one frame per slot + * on its own grid (rx_reverse_delivery.c's orig_start, which its + * SIM_ONLY build leaves out). */ +static void orig_on(uint64_t first_boundary_us, uint32_t first_abs) { + g_orig.on = 1U; + g_orig.next_slot_us = first_boundary_us; + g_orig.next_abs = first_abs; +} + +/* Run the firmware loop; return the passes that ended out of SLEEP. */ +static uint32_t run_parked_ms(uint32_t ms) { + uint32_t not_asleep = 0U; + const uint64_t until = g_now_us + (uint64_t)ms * 1000ULL; + while (g_now_us < until) { + fw_pass(); + if (g_radio.mode != SX1276_STATE_SLEEP) { + ++not_asleep; + } + } + return not_asleep; +} + +/* Run until the scan machine has LOCKed `locks` times in all (or the cap). */ +static void run_until_locks(uint32_t locks, uint32_t cap_ms) { + const uint64_t t0 = g_now_us; + while (g_sc.locks < locks && g_now_us - t0 < (uint64_t)cap_ms * 1000ULL) { + fw_pass(); + } +} + +/* Run until no TX is keyed or waiting in host_cmd.c's ring (or the cap). */ +static void run_until_tx_idle(uint32_t cap_ms) { + const uint64_t t0 = g_now_us; + while ((sx1276_tx_busy() || host_cmd_tx_mailbox_pending()) && + g_now_us - t0 < (uint64_t)cap_ms * 1000ULL) { + fw_pass(); + } +} + +static void print_park(void) { + if (g_verbose == 0) { + return; + } + print_scenario(); + printf(" modem calls: retunes=%u standby=%u rx_arms=%u tx=%u sleep=%u |" + " scan records=%u | FAULT 0x0D=%u 0x0F=%u | PARK_URC=%u ERR_PROTO=%u" + " | resets=%u | parked=%u\n", + g_calls.freq, g_calls.standby, g_calls.rx_cont, g_calls.tx, + g_calls.sleep, g_calls.scan_rec, g_fault_n[0x0DU], g_fault_n[0x0FU], + g_urc[HOST_TYPE_RADIO_PARK_URC].count, + g_urc[HOST_TYPE_ERR_PROTO_URC].count, g_system_resets, + (unsigned)sx1276_rx_parked()); +} + +/* ------------------------------------------------------------------ */ +/* Scenarios */ +/* ------------------------------------------------------------------ */ + +static void test_boot(void) { + static const uint8_t mask50[8] = { + 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0x03U, 0x00U + }; + const uint8_t profile = (uint8_t)REG_PROFILE_FCC_15_247_FHSS_50CH_BW250; + const urc_slot_t *ready = &g_urc[HOST_TYPE_READY_URC]; + const urc_slot_t *ver = &g_urc[HOST_TYPE_VER_URC]; + uint32_t cap; + uint16_t seq; + + g_sc.name = "B boot"; + /* Power-on: a reset. The flag is zero-initialised storage (.bss), which + * startup.c clears on every reset, RESET_REQ's NVIC reset included. */ + CHECK(!sx1276_rx_parked(), "[%s] parked out of reset", g_sc.name); + + g_ctx = CTX_BOOT; + host_cmd_init(true, 0x12U); + g_ctx = CTX_NONE; + cap = (uint32_t)ready->payload[4] | ((uint32_t)ready->payload[5] << 8) | + ((uint32_t)ready->payload[6] << 16) | ((uint32_t)ready->payload[7] << 24); + CHECK(g_urc[HOST_TYPE_BOOT_URC].count == 1U && ready->count == 1U && + (cap & HOST_CAP_RADIO_PARK) != 0UL && cap == HOST_CAPABILITY_BITMAP, + "[%s] READY_URC capability 0x%08x lacks HOST_CAP_RADIO_PARK", g_sc.name, + cap); + seq = host_req(HOST_TYPE_VER_REQ, NULL, 0U); + cap = (uint32_t)ver->payload[8] | ((uint32_t)ver->payload[9] << 8) | + ((uint32_t)ver->payload[10] << 16) | ((uint32_t)ver->payload[11] << 24); + CHECK(ver->count == 1U && ver->seq == seq && + ver->payload[0] == HOST_PROTOCOL_VER && + ver->payload[1] == HOST_WIRE_SCHEMA_VER && + (cap & HOST_CAP_RADIO_PARK) != 0UL, + "[%s] VER_URC: proto %u schema %u capability 0x%08x", g_sc.name, + ver->payload[0], ver->payload[1], cap); + + /* The daemons' activation: 50-channel mask, then REG_PROFILE=1. */ + CHECK(cfg_set_req(CFG_KEY_FHSS_CHANNEL_MASK, mask50, 8U) == (uint8_t)CFG_STATUS_OK, + "[%s] CFG_SET FHSS_CHANNEL_MASK failed", g_sc.name); + CHECK(cfg_set_req(CFG_KEY_REG_PROFILE, &profile, 1U) == (uint8_t)CFG_STATUS_OK && + host_cfg_profile_active()->profile_id == profile, + "[%s] CFG_SET REG_PROFILE=1 failed", g_sc.name); + CHECK(!sx1276_rx_parked() && total_faults() == 0U, + "[%s] parked %u, %u faults after boot", g_sc.name, + (unsigned)sx1276_rx_parked(), total_faults()); +} + +static void test_park_after_final_fail(void) { + uint8_t op = 0U; + uint32_t park_ms; + uint32_t not_asleep; + snap_t s0; + snap_t s1; + + scenario_begin("P1 park after the final FAIL (base)", ROLE_BASE, 0x5EED0001U); + CHECK(!sx1276_rx_parked(), "[%s] parked at the start", g_sc.name); + { + const uint64_t t0 = g_now_us; + while (g_sc.scan_fail_urc < 4U && g_now_us - t0 < 130000000ULL) { + fw_pass(); + } + } + CHECK(g_sc.scan_fail_urc == 4U && (g_fail_sub[3] & FINAL_BIT) != 0U && + g_scan_mirror == SX1276_RX_SCAN_STATE_SCANNING && + g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] setup: %u FAILs (last sub %02x), scan state %u, modem state %u" + " (expect 4, final, SCANNING by (b), RX_CONT)", g_sc.name, + g_sc.scan_fail_urc, g_fail_sub[3], (unsigned)g_scan_mirror, + (unsigned)g_radio.mode); + + park_ms = platform_now_ms(); + s0 = snap_now(); + g_radio.irq = (uint8_t)(IRQ_RX_DONE | IRQ_VALID_HEADER); /* stale flags */ + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_PARKED && op == OPMODE_SLEEP && + sx1276_rx_parked() && g_radio.mode == SX1276_STATE_SLEEP && + g_radio.irq == 0U, + "[%s] PARK_REQ: status/opmode %02x, parked %u, modem state %u, IRQ" + " flags %02x (expect PARKED, 0x80, 1, SLEEP, 00)", g_sc.name, op, + (unsigned)sx1276_rx_parked(), (unsigned)g_radio.mode, g_radio.irq); + + /* Ten minutes, with a stream on air the whole time. */ + orig_on(g_now_us + 37000ULL, 777U); + not_asleep = run_parked_ms(600000U); + s1 = snap_now(); + CHECK(not_asleep == 0U, "[%s] %u parked passes ended out of SLEEP", + g_sc.name, not_asleep); + expect_untouched("10 min parked, a stream on air", &s0, &s1); + CHECK(s1.sleep == s0.sleep + 1U && g_sc.frames_sent > 1000U, + "[%s] %u SLEEP transitions (expect the park's 1), %u frames on air", + g_sc.name, s1.sleep - s0.sleep, g_sc.frames_sent); + CHECK(reg_read(0x01U) == OPMODE_SLEEP, + "[%s] RegOpMode read back out of SLEEP after 10 min parked", g_sc.name); + + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_ALREADY && op == OPMODE_SLEEP && + sx1276_rx_parked(), + "[%s] repeat PARK_REQ: %02x (expect ALREADY, 0x80)", g_sc.name, op); + expect_tx_refused(0x42U); + { + /* The radio-layer gates themselves. */ + sx1276_tx_request_t req; + const snap_t a = snap_now(); + const uint32_t rfco0 = g_urc[HOST_TYPE_RFCO_PERTX_URC].count; + memset(&req, 0, sizeof(req)); + req.tx_id = 0x43U; + req.length = (uint8_t)CMD_PAYLOAD_LEN; + CHECK(!sx1276_tx_begin(&req) && !sx1276_tx_busy(), + "[%s] sx1276_tx_begin() keyed a parked radio", g_sc.name); + CHECK(!sx1276_rx_arm(), "[%s] sx1276_rx_arm() armed a parked radio", + g_sc.name); + { + const snap_t b = snap_now(); + CHECK(b.tx == a.tx && b.rx_cont == a.rx_cont && b.standby == a.standby && + b.freq == a.freq && + g_urc[HOST_TYPE_RFCO_PERTX_URC].count == rfco0 && + g_radio.mode == SX1276_STATE_SLEEP, + "[%s] a refused TX/arm touched the modem", g_sc.name); + } + } + + /* The park ends at the daemons' raw RXCONT. The stream stops first, so + * the interrupted attempt runs out and its FAIL times the resume. */ + g_orig.on = 0U; + (void)run_parked_ms(1000U); + { + const uint32_t unpark_ms = platform_now_ms(); + const uint32_t due = g_fail_ms[3] + SX1276_RX_SCAN_REDESIGN_MS + + (unpark_ms - park_ms); + reg_write_ok(0x01U, 0x85U); + CHECK(!sx1276_rx_parked() && g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] raw RXCONT: parked %u, modem state %u", g_sc.name, + (unsigned)sx1276_rx_parked(), (unsigned)g_radio.mode); + { + const uint64_t t0 = g_now_us; + while (g_sc.scan_fail_urc < 5U && g_now_us - t0 < 40000000ULL) { + fw_pass(); + } + } + CHECK(g_sc.scan_fail_urc == 5U && g_fail_ms[4] >= due && + g_fail_ms[4] <= due + 2U && (g_fail_sub[4] & FINAL_BIT) != 0U, + "[%s] the attempt the park interrupted FAILed at %u ms (sub %02x)," + " expect %u ms: its start + 30 s + %u ms parked", g_sc.name, + g_fail_ms[4], g_fail_sub[4], due, unpark_ms - park_ms); + } + { + const uint32_t locks0 = g_sc.locks; + const uint32_t frames0 = g_sc.frames_recv; + orig_on(g_now_us + 41000ULL, 4242U); + run_until_locks(locks0 + 1U, 200000U); + run_for_ms(20000U); + CHECK(g_sc.locks > locks0 && g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED && + g_sc.frames_recv >= frames0 + 40U, + "[%s] no re-acquisition after the park: locks %u -> %u, %u frames", + g_sc.name, locks0, g_sc.locks, g_sc.frames_recv - frames0); + } + print_park(); + check_structure(); +} + +static void test_park_while_locked(void) { + uint8_t op = 0U; + uint32_t not_asleep; + uint32_t frames0; + uint32_t consider0; + snap_t s0; + snap_t s1; + + scenario_begin("P2 park while LOCKED (base following)", ROLE_BASE, 0x5EED0002U); + CHECK(!sx1276_rx_parked(), "[%s] parked at the start", g_sc.name); + orig_on(g_now_us + 37000ULL, 1234U); + run_until_locks(1U, 150000U); + run_for_ms(10000U); + CHECK(g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED && + sx1276_rx_grid_adopted() != 0U && sx1276_fhss_clock_valid() != 0U && + g_sc.follow_locked > 0U, + "[%s] setup: scan state %u, adopted %u, clock %u, follow retunes %u", + g_sc.name, (unsigned)g_scan_mirror, sx1276_rx_grid_adopted(), + sx1276_fhss_clock_valid(), g_sc.follow_locked); + + /* Park with a frame's DIO0 edge latched for the next pass. */ + { + const uint64_t t0 = g_now_us; + while (g_radio.dio0 == 0U && g_now_us - t0 < 5000000ULL) { + fw_pass(); + } + } + CHECK(g_radio.dio0 != 0U, "[%s] setup: no frame edge to park on", g_sc.name); + frames0 = g_sc.frames_recv; + consider0 = snap_now().consider_dec; + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_PARKED && op == OPMODE_SLEEP, + "[%s] PARK_REQ: %02x (expect PARKED, 0x80)", g_sc.name, op); + s0 = snap_now(); + /* The park cleared the chip's IRQ flags; raise RxDone again, as on a + * chip that did not take the write, so the edge alone is what the + * rx_service gate must refuse (the FIFO still holds the frame here). */ + g_radio.irq = (uint8_t)(IRQ_RX_DONE | IRQ_VALID_HEADER); + fw_pass(); + CHECK(g_sc.frames_recv == frames0 && snap_now().consider_dec == consider0, + "[%s] the frame latched before the park was delivered (%u) or its" + " header considered (%u)", g_sc.name, g_sc.frames_recv - frames0, + snap_now().consider_dec - consider0); + + /* 2 min LOCKED on the adopted grid: the slot follower's case. */ + not_asleep = run_parked_ms(120000U); + /* 2 min LOCKED without a clock: the gamma-1 fallback's case. */ + sx1276_fhss_clock_reset(); + not_asleep += run_parked_ms(120000U); + s1 = snap_now(); + CHECK(not_asleep == 0U, "[%s] %u parked passes ended out of SLEEP", + g_sc.name, not_asleep); + expect_untouched("4 min parked LOCKED, a stream on air", &s0, &s1); + CHECK(g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED && g_sc.frames_sent > 500U, + "[%s] scan state %u after the park (expect LOCKED, frozen); %u frames" + " on air", g_sc.name, (unsigned)g_scan_mirror, g_sc.frames_sent); + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_ALREADY && op == OPMODE_SLEEP, + "[%s] repeat PARK_REQ: %02x (expect ALREADY, 0x80)", g_sc.name, op); + + /* The park ends at a profile activation (CFG_SET REG_PROFILE=1). */ + { + const uint32_t locks0 = g_sc.locks; + const uint32_t frames1 = g_sc.frames_recv; + activate_fhss(); + CHECK(!sx1276_rx_parked(), "[%s] still parked after the activation", + g_sc.name); + run_until_locks(locks0 + 1U, 150000U); + run_for_ms(10000U); + CHECK(g_sc.locks > locks0 && g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED && + g_sc.frames_recv >= frames1 + 15U, + "[%s] no re-acquisition after the activation: locks %u -> %u," + " %u frames", g_sc.name, locks0, g_sc.locks, + g_sc.frames_recv - frames1); + } + print_park(); + check_structure(); +} + +static void test_park_vs_tx(void) { + uint8_t op = 0U; + + scenario_begin("P3 park against TX; raw writes; reset", ROLE_BASE, 0x5EED0003U); + CHECK(!sx1276_rx_parked(), "[%s] parked at the start", g_sc.name); + run_for_ms(3000U); + CHECK(g_radio.mode == SX1276_STATE_RX_CONT, "[%s] setup: not in RX", g_sc.name); + + /* (a) A TX on air. Its FIFO is altered before TX_DONE: the 0x0D split. */ + { + const uint32_t err0 = g_urc[HOST_TYPE_ERR_PROTO_URC].count; + const uint32_t f0d = g_fault_n[0x0DU]; + const uint32_t f0f = g_fault_n[0x0FU]; + (void)tx_req(0x51U); + CHECK(g_urc[HOST_TYPE_ERR_PROTO_URC].count == err0 && sx1276_tx_busy() && + g_radio.mode == SX1276_STATE_TX, + "[%s] setup: the TX did not key", g_sc.name); + g_radio.fifo[LORA_PKT_HDR_LEN + 3U] ^= 0xFFU; + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_REFUSED && op == OPMODE_TX && + !sx1276_rx_parked() && sx1276_tx_busy(), + "[%s] PARK_REQ during a TX: %02x, parked %u (expect REFUSED, 0x83, 0)", + g_sc.name, op, (unsigned)sx1276_rx_parked()); + run_until_tx_idle(1000U); + CHECK(!sx1276_tx_busy() && g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] the post-TX re-arm did not run (modem state %u)", g_sc.name, + (unsigned)g_radio.mode); + CHECK(g_fault_n[0x0FU] == f0f + 1U && g_fault_sub[0x0FU] == 0x51U && + g_fault_n[0x0DU] == f0d, + "[%s] FIFO readback mismatch: FAULT 0x0F x%u (sub %02x), 0x0D x%u" + " (expect 0x0F once, sub 51, no 0x0D)", g_sc.name, + g_fault_n[0x0FU] - f0f, g_fault_sub[0x0FU], g_fault_n[0x0DU] - f0d); + } + /* (b) A TX waiting in host_cmd.c's TX ring (its first TX anchored the + * slot clock, so a frame that does not fit the slot waits). */ + { + const uint64_t t0 = g_now_us; + uint32_t ok0; + uint32_t err0; + while ((sx1276_tx_busy() || sx1276_tx_slot_wait_us(CMD_PAYLOAD_LEN) == 0U) && + g_now_us - t0 < 2000000ULL) { + fw_pass(); + } + ok0 = g_sc.fw_tx_ok; + err0 = g_urc[HOST_TYPE_ERR_PROTO_URC].count; + (void)tx_req(0x52U); + CHECK(host_cmd_tx_mailbox_pending() && !sx1276_tx_busy(), + "[%s] setup: the frame did not wait in the TX ring", g_sc.name); + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_REFUSED && !sx1276_rx_parked(), + "[%s] PARK_REQ with a TX queued: %02x (expect REFUSED)", g_sc.name, op); + run_until_tx_idle(2000U); + CHECK(g_sc.fw_tx_ok == ok0 + 1U && + g_urc[HOST_TYPE_ERR_PROTO_URC].count == err0 && + g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] the queued frame did not go out (TX_DONE %u, ERR_PROTO %u)", + g_sc.name, g_sc.fw_tx_ok - ok0, + g_urc[HOST_TYPE_ERR_PROTO_URC].count - err0); + } + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_PARKED && op == OPMODE_SLEEP, + "[%s] PARK_REQ once idle: %02x (expect PARKED, 0x80)", g_sc.name, op); + /* Refused at once, even when the frame would otherwise wait in the TX + * ring for the slot (the clock is anchored and keeps running parked). */ + { + const uint64_t t0 = g_now_us; + while (sx1276_tx_slot_wait_us(CMD_PAYLOAD_LEN) == 0U && + g_now_us - t0 < 2000000ULL) { + fw_pass(); + } + CHECK(sx1276_tx_slot_wait_us(CMD_PAYLOAD_LEN) > 0U, + "[%s] setup: no slot wait to test the ring against", g_sc.name); + } + expect_tx_refused(0x53U); + + /* Raw RegOpMode writes that do not end the park. */ + reg_write_refused(0x01U, 0x86U); /* RXSINGLE: diag builds only */ + CHECK(sx1276_rx_parked() && g_radio.mode == SX1276_STATE_SLEEP, + "[%s] a refused raw RXSINGLE ended the park", g_sc.name); + { + snap_t a; + snap_t b; + uint32_t not_standby = 0U; + const uint64_t until = g_now_us + 60000000ULL; + reg_write_ok(0x01U, 0x81U); /* STANDBY */ + CHECK(sx1276_rx_parked() && g_radio.mode == SX1276_STATE_STANDBY, + "[%s] raw STANDBY: parked %u, modem state %u", g_sc.name, + (unsigned)sx1276_rx_parked(), (unsigned)g_radio.mode); + a = snap_now(); + while (g_now_us < until) { + fw_pass(); + if (g_radio.mode != SX1276_STATE_STANDBY) { + ++not_standby; + } + } + b = snap_now(); + CHECK(not_standby == 0U && b.rx_cont == a.rx_cont && b.freq == a.freq && + b.tx == a.tx && b.scan_rec == a.scan_rec, + "[%s] parked in STANDBY for 60 s: %u passes out of STANDBY, RX arms" + " %u, retunes %u, scan records %u", g_sc.name, not_standby, + b.rx_cont - a.rx_cont, b.freq - a.freq, b.scan_rec - a.scan_rec); + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_ALREADY && op == OPMODE_SLEEP && + g_radio.mode == SX1276_STATE_SLEEP, + "[%s] PARK_REQ from STANDBY: %02x (expect ALREADY, 0x80)", g_sc.name, op); + } + reg_write_ok(0x01U, 0x80U); /* SLEEP: radio_park.py's write */ + CHECK(sx1276_rx_parked() && g_radio.mode == SX1276_STATE_SLEEP, + "[%s] raw SLEEP ended the park", g_sc.name); + { + const uint32_t r0 = g_system_resets; + (void)host_req(HOST_TYPE_RESET_REQ, NULL, 0U); + CHECK(g_system_resets == r0 + 1U, + "[%s] RESET_REQ did not reach platform_system_reset()", g_sc.name); + } + + /* The park ends at the raw RXCONT, and TX works again. */ + reg_write_ok(0x01U, 0x85U); + CHECK(!sx1276_rx_parked() && g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] raw RXCONT: parked %u, modem state %u", g_sc.name, + (unsigned)sx1276_rx_parked(), (unsigned)g_radio.mode); + { + const uint32_t ok0 = g_sc.fw_tx_ok; + const uint32_t err0 = g_urc[HOST_TYPE_ERR_PROTO_URC].count; + (void)tx_req(0x54U); + run_until_tx_idle(2000U); + CHECK(g_sc.fw_tx_ok == ok0 + 1U && + g_urc[HOST_TYPE_ERR_PROTO_URC].count == err0 && + g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] TX after the park: TX_DONE %u, ERR_PROTO %u, modem state %u", + g_sc.name, g_sc.fw_tx_ok - ok0, + g_urc[HOST_TYPE_ERR_PROTO_URC].count - err0, (unsigned)g_radio.mode); + } + print_park(); + check_structure(); +} + +static void test_tractor_park_in_failed(void) { + static const uint8_t trains[4] = { 23U, 45U, 23U, 9U }; /* leg H mix */ + uint8_t op = 0U; + uint32_t restarts0; + uint32_t not_asleep; + snap_t s0; + snap_t s1; + + scenario_begin("P4 tractor: FAILED held, daemon stops, park", ROLE_TRACTOR, + 0x789ABCDU); + CHECK(!sx1276_rx_parked(), "[%s] parked at the start", g_sc.name); + g_th.period_ms = 500U; + g_th.jitter_ms = 40U; + memcpy(g_th.train_w, trains, 4U); + g_th.next_frame_us = g_now_us + 50000ULL; + g_th.next_rxcont_us = g_now_us; + base_model_init(); + g_base.mode = CMD_PUMP_AT_TRAIN_END; + g_base.min_gap_ms = 1000U; + g_base.next_allowed_us = UINT64_MAX; /* the base never commands */ + run_for_ms(150000U); + CHECK(g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED && g_sc.fail_final == 1U && + auth_now_ms(platform_now_ms()) != 0U, + "[%s] setup: scan state %u, final FAILs %u, authority %u (expect FAILED" + " held while streaming)", g_sc.name, (unsigned)g_scan_mirror, + g_sc.fail_final, auth_now_ms(platform_now_ms())); + + /* The camera daemon stops right after a TX_DONE; its train drains. */ + { + const uint32_t ok0 = g_sc.fw_tx_ok; + const uint64_t t0 = g_now_us; + while (g_sc.fw_tx_ok == ok0 && g_now_us - t0 < 2000000ULL) { + fw_pass(); + } + g_th.next_frame_us = UINT64_MAX; + g_th.frames_waiting = 0U; + while ((g_th.train_active != 0U || g_th.outstanding != 0U || + sx1276_tx_busy() || g_txq_count != 0U) && + g_now_us - t0 < 5000000ULL) { + fw_pass(); + } + g_th.next_rxcont_us = UINT64_MAX; /* and with it its raw RXCONT */ + } + CHECK(g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED && + auth_now_ms(platform_now_ms()) != 0U && !sx1276_tx_busy(), + "[%s] setup: authority lost before the park (scan state %u)", g_sc.name, + (unsigned)g_scan_mirror); + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_PARKED && op == OPMODE_SLEEP, + "[%s] PARK_REQ: %02x (expect PARKED, 0x80)", g_sc.name, op); + restarts0 = g_sc.tick_restarts; + s0 = snap_now(); + not_asleep = run_parked_ms(300000U); + s1 = snap_now(); + CHECK(not_asleep == 0U, "[%s] %u parked passes ended out of SLEEP", + g_sc.name, not_asleep); + expect_untouched("5 min parked in FAILED, authority decayed", &s0, &s1); + CHECK(g_sc.tick_restarts == restarts0 && + g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED && + auth_now_ms(platform_now_ms()) == 0U, + "[%s] parked: (b) restarts %u, scan state %u, authority %u (expect 0," + " FAILED, 0)", g_sc.name, g_sc.tick_restarts - restarts0, + (unsigned)g_scan_mirror, auth_now_ms(platform_now_ms())); + + /* The daemon restarts: its raw RXCONT ends the park, and the first scan + * tick restarts the scan (b). */ + reg_write_ok(0x01U, 0x85U); + CHECK(!sx1276_rx_parked() && g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] raw RXCONT: parked %u, modem state %u", g_sc.name, + (unsigned)sx1276_rx_parked(), (unsigned)g_radio.mode); + { + const snap_t a = snap_now(); + fw_pass(); + { + const snap_t b = snap_now(); + CHECK(g_sc.tick_restarts == restarts0 + 1U && + g_scan_mirror == SX1276_RX_SCAN_STATE_SCANNING && + b.freq == a.freq + 1U && b.faults == a.faults && + g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] first tick after the park: (b) restarts %u, scan state %u," + " walker retunes %u, faults %u (expect 1, SCANNING, 1, 0)", + g_sc.name, g_sc.tick_restarts - restarts0, + (unsigned)g_scan_mirror, b.freq - a.freq, b.faults - a.faults); + } + } + /* And the camera streams again. */ + { + const uint32_t ok0 = g_sc.fw_tx_ok; + g_th.next_frame_us = g_now_us + 50000ULL; + g_th.next_rxcont_us = g_now_us; + run_for_ms(20000U); + CHECK(g_sc.fw_tx_ok >= ok0 + 20U && auth_now_ms(platform_now_ms()) != 0U, + "[%s] after the park: %u TX_DONE, authority %u", g_sc.name, + g_sc.fw_tx_ok - ok0, auth_now_ms(platform_now_ms())); + } + print_park(); + check_structure(); +} + +/* Run until the scan walker retunes (a BEGIN_SCAN / ADVANCE_CHANNEL step); + * return its pass's ms, or UINT32_MAX at the cap. */ +static uint32_t run_until_walker_step(uint32_t cap_ms) { + const uint32_t steps0 = g_sc.walker_retunes; + const uint64_t t0 = g_now_us; + while (g_sc.walker_retunes == steps0 && + g_now_us - t0 < (uint64_t)cap_ms * 1000ULL) { + fw_pass(); + } + return (g_sc.walker_retunes == steps0) ? UINT32_MAX : g_pass_now_ms; +} + +static void test_unpark_resume(void) { + uint8_t op = 0U; + + scenario_begin("P5 raw-RXCONT unpark: SCANNING resumes, LOCKED demotes", + ROLE_BASE, 0x5EED0005U); + CHECK(!sx1276_rx_parked(), "[%s] parked at the start", g_sc.name); + + /* (a) SCANNING, nothing on air: park 700 ms into a dwell, for 10 s. */ + { + const uint32_t step_ms = run_until_walker_step(5000U); + uint32_t park_ms; + uint32_t unpark_ms; + uint32_t due; + uint32_t next_ms; + uint32_t steps0; + CHECK(step_ms != UINT32_MAX && g_scan_mirror == SX1276_RX_SCAN_STATE_SCANNING, + "[%s] setup: no walker step (scan state %u)", g_sc.name, + (unsigned)g_scan_mirror); + run_for_ms(700U); + park_ms = platform_now_ms(); + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_PARKED && op == OPMODE_SLEEP, + "[%s] PARK_REQ while SCANNING: %02x (expect PARKED, 0x80)", g_sc.name, + op); + steps0 = g_sc.walker_retunes; + CHECK(run_parked_ms(10000U) == 0U && g_sc.walker_retunes == steps0, + "[%s] parked SCANNING: out of SLEEP, or %u walker steps", g_sc.name, + g_sc.walker_retunes - steps0); + unpark_ms = platform_now_ms(); + due = step_ms + SX1276_RX_SCAN_DWELL_MS + (unpark_ms - park_ms); + reg_write_ok(0x01U, 0x85U); + CHECK(!sx1276_rx_parked() && g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] raw RXCONT: parked %u, modem state %u", g_sc.name, + (unsigned)sx1276_rx_parked(), (unsigned)g_radio.mode); + next_ms = run_until_walker_step(5000U); + CHECK(next_ms != UINT32_MAX && next_ms >= due && next_ms <= due + 2U, + "[%s] the dwell the park interrupted ended at %u ms, %u ms after" + " the unpark; expect %u ms: its start + %u ms dwell + %u ms parked" + " (the remaining %u ms after the unpark)", g_sc.name, next_ms, + next_ms - unpark_ms, due, SX1276_RX_SCAN_DWELL_MS, + unpark_ms - park_ms, due - unpark_ms); + } + + /* (b) LOCKED on an adopted grid, the stream on air: park for 10 s. */ + { + uint32_t locks0; + uint32_t dem0; + uint32_t not_asleep; + orig_on(g_now_us + 37000ULL, 2468U); + run_until_locks(g_sc.locks + 1U, 150000U); + run_for_ms(10000U); + CHECK(g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED && + sx1276_rx_grid_adopted() != 0U && sx1276_fhss_clock_valid() != 0U, + "[%s] setup: scan state %u, adopted %u, clock %u", g_sc.name, + (unsigned)g_scan_mirror, sx1276_rx_grid_adopted(), + sx1276_fhss_clock_valid()); + CHECK(park_req(&op) == HOST_RADIO_PARK_STATUS_PARKED && op == OPMODE_SLEEP, + "[%s] PARK_REQ while LOCKED: %02x (expect PARKED, 0x80)", g_sc.name, + op); + dem0 = g_sc.demotions; + not_asleep = run_parked_ms(10000U); + CHECK(not_asleep == 0U && g_sc.demotions == dem0 && + g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED, + "[%s] parked LOCKED: %u passes out of SLEEP, %u demotions, scan" + " state %u (expect 0, 0, LOCKED: frozen)", g_sc.name, not_asleep, + g_sc.demotions - dem0, (unsigned)g_scan_mirror); + /* F6: the clock is stale -- and so must the lock be. */ + CHECK(sx1276_fhss_clock_valid() != 0U && + sx1276_fhss_clock_age_ms(platform_now_ms()) > SX1276_FHSS_CLOCK_FRESH_MS, + "[%s] the adopted clock is fresh after 10 s parked", g_sc.name); + reg_write_ok(0x01U, 0x85U); + CHECK(!sx1276_rx_parked() && g_radio.mode == SX1276_STATE_RX_CONT, + "[%s] raw RXCONT: parked %u, modem state %u", g_sc.name, + (unsigned)sx1276_rx_parked(), (unsigned)g_radio.mode); + fw_pass(); + CHECK(g_sc.demotions == dem0 + 1U && + g_scan_mirror == SX1276_RX_SCAN_STATE_SCANNING && + sx1276_rx_grid_adopted() == 0U, + "[%s] first tick after a 10 s LOCKED park: %u demotions, scan state" + " %u, adopted %u (expect 1, SCANNING, 0: the loss timer counts the" + " park, as the clock's freshness does)", g_sc.name, + g_sc.demotions - dem0, (unsigned)g_scan_mirror, + sx1276_rx_grid_adopted()); + locks0 = g_sc.locks; + run_until_locks(locks0 + 1U, 150000U); + run_for_ms(5000U); + CHECK(g_sc.locks > locks0 && g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED, + "[%s] no re-acquisition after the LOCKED park: locks %u -> %u", + g_sc.name, locks0, g_sc.locks); + } + print_park(); + check_structure(); +} + +int main(int argc, char **argv) { + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "-v") == 0) { + g_verbose = 1; + } + } + test_boot(); + test_park_after_final_fail(); + test_park_while_locked(); + test_park_vs_tx(); + test_tractor_park_in_failed(); + test_unpark_resume(); + if (g_failures != 0) { + fprintf(stderr, "[FAIL] radio_park: %d failure(s)\n", g_failures); + return 1; + } + printf("[PASS] radio_park: RS-12.21 firmware park -- PARK_REQ/URC through the" + " real host_cmd.c; parked after a final FAIL, while LOCKED and in a" + " held FAILED, nothing re-arms or touches the modem for minutes; TX" + " refused while parked, park refused while a TX is keyed or queued;" + " ended by a raw RXCONT, a profile activation (and a reset); after" + " it a SCANNING node resumes its dwell, a LOCKED one parked past" + " the loss timer demotes at once; FIFO fault on 0x0F\n"); + return 0; +} diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_reverse_delivery.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_reverse_delivery.c index d56c7bf5..205487ca 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_reverse_delivery.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_reverse_delivery.c @@ -54,9 +54,10 @@ * is handed UNANCHORED and adopted; closing the window itself is a * grid-policy matter, sx1276_rx_grid_policy.c, not this harness's); * 8 scan FAILED: with no command for 150 s the tractor's scan machine - * runs out of retries into the absorbing FAILED state (it can never - * LOCK again); its own clock still drives the receiver and every - * clean, covered command is received; + * runs out of retries into FAILED, which a streaming node holds + * (RS-12.21 (b): FAILED is held only under originator authority) -- + * it never LOCKs there; its own clock still drives the receiver and + * every clean, covered command is received; * 9 final FAIL mid-command: the streaming tractor's final scan FAIL * fires while a clean command is on air. The fault is reported and * FAILED committed, but the receiver is not parked: the slot @@ -67,12 +68,26 @@ * crc_seen bit covers its own window (the last dwell) only, as on * a walking node, so neither of the first two FAILs reports one. * In EVERY scenario, per main-loop pass: the walker retunes exactly when - * the scan machine dispatches BEGIN_SCAN / ADVANCE (or a non-final FAIL) - * unless the node holds originator authority; no scan action takes the + * the scan machine dispatches BEGIN_SCAN / ADVANCE (or a FAIL, which + * re-enters SCANNING unless it is held) unless the node holds originator + * authority; no scan action takes the * receiver out of RX while the node holds authority; every slot-follow * retune is onto the clock's channel and allowed by the follow rule * (LOCKED on an adopted grid, or originator authority); and every own * transmission is keyed on the hop channel of the slot its header claims. + * RS-12.21 (2026-10-24, firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md + * section 5 item 2, adopted) adds four, in every scenario too: + * (b) the scan machine is FAILED after a scan tick only while the node + * holds originator authority (or a TX is keyed: the tick is frozen + * then); a FAIL is held exactly when it is final and the node holds + * authority, and every exit from FAILED is recorded as + * (FAILED, BEGIN_SCAN) -- the mirror below follows the records; + * d2 the walker steps through the channel table in order: channel 0 + * only after a BEGIN_SCAN out of BOOT or LOCKED, the next channel + * after a retry or a restart out of FAILED (the walk continues + * across attempts); + * L no LOCKED -> SCANNING demotion comes sooner than LOCK_LOSS_MS + * after the LOCK or the last accepted frame. * In the tractor scenarios 1-4 and 8 the tractor's grid never moves: one * phase anchor, no adoption, no clock reset, no SNAP. * @@ -81,11 +96,22 @@ * receiver, scan FAILs) and an FNV-1a trace hash over every * radio-affecting call with its time and caller. Scenario 5's hash is the * same against the pre-fix sx1276_rx.c: the follower path is - * call-for-call, microsecond-for-microsecond unchanged. + * call-for-call, microsecond-for-microsecond unchanged. The hashes are + * printed, not pinned. RS-12.21's 2 s scan dwell changed all ten + * (2026-10-24; the walker's timing is in every scenario), with every + * scenario's assertions unchanged; (b) and L alone change none but + * scenario 7's (L: its LOCKs come out of SCANNING). * * check-rx-scan-failed-follower (rx_scan_failed_follower.c) reuses this * simulation: it #includes this file with RX_REVERSE_DELIVERY_SIM_ONLY * defined, which leaves out the scenarios below and main(). + * check-radio-park (radio_park.c, RS-12.21 2026-10-24) does too, with + * RX_SIM_REAL_HOST also defined: it links the real host_cmd.c, host_cfg.c + * and host_cfg_profile.c, so the stubs for host_cmd_emit_fault(), + * host_uart_send_urc() and host_cfg_profile_active() drop out, and the + * mailbox step below also services host_cmd.c's own TX ring. While the + * host has the radio parked the scan tick is frozen, which the (b) check + * allows. */ #include @@ -209,6 +235,16 @@ static uint32_t toa_us(uint8_t on_air_len) { 1U, 0U, 8U, 0U); } +/* Channel table index of a centre frequency (0xFF: not a table channel). */ +static uint8_t chan_of_hz(uint32_t hz) { + for (uint8_t i = 0U; i < SX1276_FHSS_CHANNEL_COUNT; ++i) { + if (sx1276_fhss_chantab_center_hz(i) == hz) { + return i; + } + } + return 0xFFU; +} + /* Channel of an absolute slot, from the pure scheduler helpers (both ends * run farm_id = node_id = 0, the activation default). */ static uint32_t hop_hz(uint32_t abs_slot) { @@ -261,6 +297,14 @@ typedef struct { uint32_t locks, demotions; uint64_t walker_after_decay_us; /* first walker retune after a decay */ uint32_t walker_after_decay_viol; + /* RS-12.21 (2026-10-24) */ + uint32_t viol_fail_rule; /* (b): FAILED held / left off the rule */ + uint32_t viol_failed_unauth; /* (b): FAILED after a tick, no authority */ + uint32_t viol_walk_order; /* d2: walker step not the next channel */ + uint32_t viol_early_demotion; /* L: demoted < LOCK_LOSS after an anchor */ + uint32_t final_restarts; /* (b): final FAIL -> SCANNING at once */ + uint32_t tick_restarts; /* (b): FAILED -> SCANNING on a tick */ + uint64_t walker_seen; /* channels the walker tuned (bit n) */ uint32_t trace; /* FNV-1a over radio-affecting calls */ /* first clean covered command missed (0 = none) */ uint64_t first_miss_us; @@ -431,26 +475,61 @@ void sx1276_write_burst(uint8_t start_reg, const uint8_t *src, size_t len) { static uint32_t g_freq_calls, g_last_freq; /* Who last tuned the synth, and who had tuned it for the last frame heard. */ static ctx_t g_tuner = CTX_NONE, g_heard_tuner = CTX_NONE; +/* RS-12.21 d2: the channel the scan walker must tune next (reset to 0 by a + * BEGIN_SCAN out of BOOT or LOCKED, in the scan-counter hook below). */ +static uint8_t g_walk_next; +/* RS-12.21 (2026-10-24): every firmware call into the modem, and every scan + * record, monotonic and never reset, for check-radio-park's "nothing touched + * the parked modem, the scan machine stood still". */ +static struct { + uint32_t freq, standby, rx_cont, tx, sleep, scan_rec; +} g_calls; void sx1276_set_frequency_hz(uint32_t freq_hz) { sim_advance_to(g_now_us + COST_FREQ_US); + ++g_calls.freq; g_radio.freq_hz = freq_hz; g_radio.rx_since_us = g_now_us; /* a retune restarts any reception */ ++g_freq_calls; g_tuner = g_ctx; g_last_freq = freq_hz; + /* Every retune in the scan tick is a walker step; so is one in + * rx_service (a frame event in BOOT dispatches BEGIN_SCAN). */ + if (g_ctx == CTX_SCAN || g_ctx == CTX_RX_SERVICE) { + const uint8_t ch = chan_of_hz(freq_hz); + if (ch != g_walk_next) { + ++g_sc.viol_walk_order; + } + if (ch < SX1276_FHSS_CHANNEL_COUNT) { + g_sc.walker_seen |= (1ULL << ch); + } + g_walk_next = (uint8_t)((ch + 1U) % SX1276_FHSS_CHANNEL_COUNT); + } trace_mix(1U, freq_hz); } bool sx1276_modes_to_standby(void) { sim_advance_to(g_now_us + COST_MODE_US); + ++g_calls.standby; g_radio.mode = SX1276_STATE_STANDBY; trace_mix(2U, 0U); return true; } +/* RS-12.21 (2026-10-24): the firmware park (sx1276_rx_park) sleeps the + * modem. Nothing else in the linked firmware calls it, so the existing + * scenarios' traces are unchanged. */ +bool sx1276_modes_to_sleep(void) { + sim_advance_to(g_now_us + COST_MODE_US); + ++g_calls.sleep; + g_radio.mode = SX1276_STATE_SLEEP; + trace_mix(6U, 0U); + return true; +} + bool sx1276_modes_to_rx_cont(void) { sim_advance_to(g_now_us + COST_MODE_US); + ++g_calls.rx_cont; g_radio.mode = SX1276_STATE_RX_CONT; g_radio.rx_since_us = g_now_us; trace_mix(3U, g_radio.freq_hz); @@ -459,6 +538,7 @@ bool sx1276_modes_to_rx_cont(void) { bool sx1276_modes_to_tx(void) { sim_advance_to(g_now_us + COST_MODE_US); + ++g_calls.tx; g_radio.mode = SX1276_STATE_TX; g_radio.tx_len = g_radio.regs[REG_PAYLOAD_LENGTH]; memcpy(g_radio.tx_bytes, g_radio.fifo, g_radio.tx_len); /* TX base 0 */ @@ -522,9 +602,10 @@ static uint8_t g_last_dec = 0xFFU; static uint8_t g_last_fail_sub; static uint8_t g_fail_seen; static uint32_t g_pass_now_ms; /* the current pass's now_ms */ -static uint32_t g_fail_ms[4]; /* pass now_ms of the first four FAILs */ -static uint8_t g_fail_sub[4]; /* ... and their sub-bytes */ -static uint64_t g_final_fail_us; /* when the final FAIL fired (0 = not yet) */ +#define FAIL_LOG 16U /* RS-12.21 (b): a FAIL every 30 s unacquired */ +static uint32_t g_fail_ms[FAIL_LOG]; /* pass now_ms of the first FAILs */ +static uint8_t g_fail_sub[FAIL_LOG]; /* ... and their sub-bytes */ +static uint64_t g_final_fail_us; /* when the LAST final FAIL fired (0 = none) */ static uint64_t g_last_crc_us; /* last payload-CRC IRQ serviced */ void host_stats_radio_rx_ok(void) {} @@ -546,9 +627,12 @@ void host_stats_clk_demotion_note(bool reset) { void host_stats_tx_first_anchor(void) { ++g_sc.first_anchor; } void host_stats_tx_stream_streak_note(uint32_t streak) { (void)streak; } -void host_cmd_emit_fault(uint8_t code, uint8_t sub) { +/* Every FAULT the firmware emits. check-radio-park links the real + * host_cmd.c (RX_SIM_REAL_HOST), whose host_cmd_emit_fault() reaches this + * through its FAULT_URC; the other harnesses through the stub below. */ +static void sim_note_fault(uint8_t code, uint8_t sub) { if (code == HOST_FAULT_CODE_RX_SCAN_FAILED) { - if (g_sc.scan_fail_urc < 4U) { + if (g_sc.scan_fail_urc < FAIL_LOG) { g_fail_ms[g_sc.scan_fail_urc] = g_pass_now_ms; g_fail_sub[g_sc.scan_fail_urc] = sub; } @@ -561,45 +645,92 @@ void host_cmd_emit_fault(uint8_t code, uint8_t sub) { } } +/* RX_SIM_REAL_HOST (check-radio-park, 2026-10-24): the harness links the + * real host_cmd.c and host_cfg_profile.c, and provides host_uart_send_urc() + * itself, so these three stubs drop out. g_profile stays: scenario_begin() + * writes it, nothing reads it then (the real active profile is set through + * the real CFG_SET REG_PROFILE path). */ +static host_cfg_profile_req_t g_profile; +#ifndef RX_SIM_REAL_HOST +void host_cmd_emit_fault(uint8_t code, uint8_t sub) { + sim_note_fault(code, sub); +} + void host_uart_send_urc(uint8_t type, uint16_t seq, uint8_t flags, const uint8_t *payload, uint16_t payload_len) { (void)type; (void)seq; (void)flags; (void)payload; (void)payload_len; } -static host_cfg_profile_req_t g_profile; const host_cfg_profile_req_t *host_cfg_profile_active(void) { return &g_profile; } +#endif /* !RX_SIM_REAL_HOST */ /* --- scan-machine mirror: intercepts the counters TU's record hook --- */ static sx1276_rx_scan_state_t g_scan_mirror = SX1276_RX_SCAN_STATE_BOOT; -static uint8_t g_tick_recorded; +static uint8_t g_tick_recorded; /* records in this scan tick */ +static sx1276_rx_scan_state_t g_tick_state; /* ... the first one's */ static sx1276_rx_scan_action_t g_tick_action; +static uint8_t g_tick_left_failed; /* then (FAILED, BEGIN_SCAN) */ +static uint8_t g_tick_extra_bad; /* then anything else */ +static uint32_t g_lock_anchor_ms; /* LOCK / last accepted frame */ void sx1276_rx_scan_counter_record(sx1276_rx_scan_state_t state, sx1276_rx_scan_action_t action) { + ++g_calls.scan_rec; if (state != g_scan_mirror) { ++g_sc.mirror_mismatch; } if (g_ctx == CTX_SCAN) { - g_tick_recorded = 1U; - g_tick_action = action; + if (g_tick_recorded == 0U) { + g_tick_state = state; + g_tick_action = action; + } else if (state == SX1276_RX_SCAN_STATE_FAILED && + action == SX1276_RX_SCAN_ACTION_BEGIN_SCAN) { + g_tick_left_failed = 1U; + } else { + g_tick_extra_bad = 1U; + } + ++g_tick_recorded; } switch (action) { case SX1276_RX_SCAN_ACTION_BEGIN_SCAN: if (state == SX1276_RX_SCAN_STATE_LOCKED) { ++g_sc.demotions; + /* RS-12.21 L: the loss timer runs from the LOCK or the last + * accepted frame (the demotion is a TICK: the pass's ms). */ + if ((uint32_t)(g_pass_now_ms - g_lock_anchor_ms) < + SX1276_RX_SCAN_LOCK_LOSS_MS) { + ++g_sc.viol_early_demotion; + } + } + /* RS-12.21 d2: a fresh acquisition (out of BOOT or LOCKED) + * restarts the walk at channel 0; leaving FAILED continues it. */ + if (state != SX1276_RX_SCAN_STATE_FAILED) { + g_walk_next = 0U; } g_scan_mirror = SX1276_RX_SCAN_STATE_SCANNING; break; case SX1276_RX_SCAN_ACTION_LOCK: ++g_sc.locks; g_scan_mirror = SX1276_RX_SCAN_STATE_LOCKED; + /* A frame event: the firmware's now_ms is this same ms. */ + g_lock_anchor_ms = platform_now_ms(); + break; + case SX1276_RX_SCAN_ACTION_REANCHOR: + /* A frame event (now) or a tick: a frame read late in a pass + * carries a fresher ms than the pass's now_ms, so the scan tick + * after it sees time run backwards and re-anchors to the pass's + * ms (up to 1 ms earlier). */ + g_lock_anchor_ms = (g_ctx == CTX_SCAN) ? g_pass_now_ms + : platform_now_ms(); break; case SX1276_RX_SCAN_ACTION_FAIL: - /* Provisional: a non-final FAIL is overridden back to SCANNING - * by the dispatcher; resolved from the URC sub-byte below. */ + /* RS-12.21: the dispatcher records (FAILED, BEGIN_SCAN) when it + * leaves FAILED again -- a non-final retry, or (b) on a node + * without originator authority -- so the mirror follows the + * records alone; fw_pass checks the decision against (b). */ g_scan_mirror = SX1276_RX_SCAN_STATE_FAILED; break; default: @@ -1182,17 +1313,24 @@ static void fw_pass(void) { g_ctx = CTX_MAILBOX; fw_service_mailbox(); +#ifdef RX_SIM_REAL_HOST + host_cmd_service_tx_mailbox(); /* the real host_cmd.c TX ring */ +#endif /* Scan tick: the walker retunes on every BEGIN_SCAN / ADVANCE (and a - * non-final FAIL) unless the node follows its own clock, and no scan - * action takes the receiver out of RX while it does (the slot - * follower re-arms only at the next boundary). */ + * FAIL that re-enters SCANNING) unless the node follows its own clock, + * and no scan action takes the receiver out of RX while it does (the + * slot follower re-arms only at the next boundary). */ { /* Authority as the firmware evaluates it: at a fresh time (the * pass's now_ms can be a tick older than the last TX_DONE note). */ const uint8_t auth = auth_now_ms(platform_now_ms()); const sx1276_state_t mode_before = g_radio.mode; + const uint8_t busy_before = (uint8_t)sx1276_tx_busy(); + const uint8_t parked_before = (uint8_t)sx1276_rx_parked(); g_tick_recorded = 0U; + g_tick_left_failed = 0U; + g_tick_extra_bad = 0U; g_fail_seen = 0U; g_freq_calls = 0U; g_ctx = CTX_SCAN; @@ -1205,22 +1343,46 @@ static void fw_pass(void) { uint32_t expect = 0U; switch (g_tick_action) { case SX1276_RX_SCAN_ACTION_BEGIN_SCAN: + if (g_tick_state == SX1276_RX_SCAN_STATE_FAILED) { + /* RS-12.21 (b): FAILED -> SCANNING on a tick, only + * once the node has lost originator authority. */ + ++g_sc.tick_restarts; + if (auth != 0U) { ++g_sc.viol_fail_rule; } + } + /* fallthrough */ case SX1276_RX_SCAN_ACTION_ADVANCE_CHANNEL: expect = (auth != 0U) ? 0U : 1U; break; case SX1276_RX_SCAN_ACTION_FAIL: { + /* RS-12.21 (b): FAILED is held exactly when the FAIL is + * final and the node holds originator authority; any + * other FAIL re-enters SCANNING at once, recorded as + * (FAILED, BEGIN_SCAN), and its walker step retunes + * unless the node holds authority. (Before (b) a final + * FAIL always held, and parked a node without + * authority in STANDBY.) */ const uint8_t final_fail = (uint8_t)(g_fail_seen != 0U && (g_last_fail_sub & (1U << SX1276_RX_SCAN_FAIL_SUB_BIT_FINAL)) != 0U); - g_scan_mirror = (final_fail != 0U) ? SX1276_RX_SCAN_STATE_FAILED - : SX1276_RX_SCAN_STATE_SCANNING; - expect = (final_fail != 0U || auth != 0U) ? 0U : 1U; - if (final_fail != 0U) { ++g_sc.fail_final; } + const uint8_t hold = (uint8_t)(final_fail != 0U && auth != 0U); + if (g_tick_left_failed == hold) { + ++g_sc.viol_fail_rule; + } + expect = (auth != 0U) ? 0U : 1U; + if (final_fail != 0U) { + ++g_sc.fail_final; + if (g_tick_left_failed != 0U) { ++g_sc.final_restarts; } + } break; } default: expect = 0U; break; } + if (g_tick_extra_bad != 0U || + (g_tick_left_failed != 0U && + g_tick_action != SX1276_RX_SCAN_ACTION_FAIL)) { + ++g_sc.viol_fail_rule; + } if (g_freq_calls != expect) { ++g_sc.viol_walker; } @@ -1246,6 +1408,13 @@ static void fw_pass(void) { } else if (g_freq_calls != 0U) { ++g_sc.viol_walker; } + /* RS-12.21 (b): FAILED survives a scan tick only under originator + * authority, or while a TX is keyed or the host has the radio + * parked (the tick is frozen then). */ + if (g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED && auth == 0U && + busy_before == 0U && parked_before == 0U) { + ++g_sc.viol_failed_unauth; + } } /* Slot follow: every retune must be allowed by the follow rule and @@ -1277,9 +1446,11 @@ static void fw_pass(void) { if (auth != 0U) { ++g_sc.auth_passes; } else if (g_prev_auth != 0U) { - /* The one-dwell bound is for a SCANNING walker; LOCKED waits - * out LOCK_LOSS and FAILED never walks again. */ - g_decay_at_us = (g_scan_mirror == SX1276_RX_SCAN_STATE_SCANNING) + /* The one-dwell bound is for a SCANNING walker -- and, since + * RS-12.21 (b), a FAILED one, which scans again on its next + * tick; LOCKED waits out LOCK_LOSS. */ + g_decay_at_us = (g_scan_mirror == SX1276_RX_SCAN_STATE_SCANNING || + g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED) ? g_now_us : 0U; g_sc.walker_after_decay_us = 0U; } @@ -1364,6 +1535,8 @@ static void scenario_begin(const char *name, role_t role, uint32_t seed) { (void)sx1276_fhss_init(0ULL, 0ULL, 0U); sx1276_rx_scan_reset(); g_scan_mirror = SX1276_RX_SCAN_STATE_BOOT; + g_walk_next = 0U; + g_lock_anchor_ms = 0U; g_prev_adopted = 0U; g_prev_auth = 0U; g_decay_at_us = 0U; @@ -1418,6 +1591,19 @@ static void print_scenario(void) { g_sc.follow_locked, g_sc.follow_own, g_sc.viol_follow_gate, g_sc.window_follow, g_sc.viol_follow_chan, g_sc.viol_tx_chan, g_sc.mirror_mismatch, g_submit_fail, g_sc.trace); + { + uint32_t seen = 0U; + for (uint8_t i = 0U; i < SX1276_FHSS_CHANNEL_COUNT; ++i) { + seen += (uint32_t)((g_sc.walker_seen >> i) & 1ULL); + } + printf(" RS-12.21: FAILED -> SCANNING at a final FAIL=%u on a tick=%u |" + " walker channels %u/%u | viol fail_rule=%u failed_unauth=%u" + " walk_order=%u early_demotion=%u\n", + g_sc.final_restarts, g_sc.tick_restarts, seen, + (unsigned)SX1276_FHSS_CHANNEL_COUNT, g_sc.viol_fail_rule, + g_sc.viol_failed_unauth, g_sc.viol_walk_order, + g_sc.viol_early_demotion); + } } /* Structural checks every scenario must pass. */ @@ -1444,6 +1630,22 @@ static void check_structure(void) { "[%s] %u TX submissions refused", g_sc.name, g_submit_fail); CHECK(g_sc.recv_dup == 0U, "[%s] %u commands delivered twice", g_sc.name, g_sc.recv_dup); + /* RS-12.21 (2026-10-24): (b), d2 and L. */ + CHECK(g_sc.viol_fail_rule == 0U, + "[%s] RS-12.21 (b): %u scan FAILs held or left against the rule (held" + " exactly when final under originator authority; every exit recorded" + " as (FAILED, BEGIN_SCAN))", g_sc.name, g_sc.viol_fail_rule); + CHECK(g_sc.viol_failed_unauth == 0U, + "[%s] RS-12.21 (b): scan FAILED after %u scan ticks without originator" + " authority or a TX keyed", g_sc.name, g_sc.viol_failed_unauth); + CHECK(g_sc.viol_walk_order == 0U, + "[%s] RS-12.21 d2: %u walker steps not to the next channel (channel 0" + " only after a BEGIN_SCAN out of BOOT or LOCKED)", g_sc.name, + g_sc.viol_walk_order); + CHECK(g_sc.viol_early_demotion == 0U, + "[%s] RS-12.21 L: %u LOCKED -> SCANNING demotions sooner than" + " LOCK_LOSS_MS after the LOCK or the last accepted frame", g_sc.name, + g_sc.viol_early_demotion); } #ifndef RX_REVERSE_DELIVERY_SIM_ONLY @@ -1558,7 +1760,9 @@ static void test_scan_failed(void) { * absorbing FAILED state; then the light command plane of (1) starts. * FAILED never LOCKs again, so before the fix only an in-slot hit * could land; the originator's own clock must keep driving the - * receiver in FAILED too. */ + * receiver in FAILED too. (RS-12.21 (b), 2026-10-24: FAILED is held + * because the tractor holds originator authority -- it streams without + * a gap of 1 s -- and nothing parks the modem any more.) */ tractor_setup("8 scan FAILED, then commands", 0x89ABCDEU, 600U, 40U, k_short_trains, CMD_IN_GAP, 2500U, 3500U, 5U, 60U); g_base.next_allowed_us = g_now_us + 150000000ULL; diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_scan_failed_follower.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_scan_failed_follower.c index 1eb7779f..9f09a049 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_scan_failed_follower.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/rx_scan_failed_follower.c @@ -1,53 +1,50 @@ /* - * RS-12.21 / FHSS leg 6 (2b_f3, 2026-10-24): a node in the absorbing scan - * FAILED state, system-in-the-loop -- CHARACTERISATION OF KNOWN DEFECTS: + * RS-12.21 / FHSS leg 6 (2b_f3, 2026-10-24): ACCEPTANCE CHECK of the scan + * FAILED fix, system-in-the-loop against the real firmware. The fix is + * option (b) + L + d2 of firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md + * (section 5 item 2; adopted by the design owner on 2026-10-24): + * (b) scan FAILED is held only while the node holds originator + * authority (it follows its own clock): a final FAIL on any other + * node, and the first scan tick of a node in FAILED whose authority + * has decayed, re-enter SCANNING for a fresh 30 s attempt; + * L a LOCK starts the LOCKED loss timer at the locking frame; + * d2 a 2 s scan dwell, the channel walk continuing across the retries + * and (b)'s restarts. + * It replaces the characterisation of the two defects the fix removes + * (PR #167, `git log` of this file): * (1) a FOLLOWER (the base) whose cold-start scan ran out into FAILED - * never hops again (leg 6); - * (2) an ORIGINATOR (the tractor) in FAILED that adopts a grid it heard - * loses originator authority, and with it self-follow, for good. - * - * THESE CHECKS PIN THE CURRENT, DEFECTIVE BEHAVIOUR. A PASS does not mean - * the behaviour is right: it means main still behaves as characterised - * here. A FAIL means that behaviour changed: a firmware fix (the options, - * their SIL results and the recommendation are in - * firmware/murata_l072/FHSS_SCAN_FAILED_OPTIONS.md, sections 4-5; first - * listed in bench-evidence/bench_session_2026-10-24/RESULTS.md, "Next - * steps" 3), or an unrelated change -- to the scan constants, the walker, - * the TX path's timing or the shared simulation (rx_reverse_delivery.c). - * Read which check failed before replacing anything; the PR that lands a fix - * replaces these assertions with the fix's own acceptance checks (`-e` - * below prints the recovery measures to start from). They characterise - * the FIRMWARE path only: no host-side recovery runs in them, so a - * daemon-side watchdog (option (c)) never flips them. + * never hopped again (leg 6): parked in STANDBY on the walker's last + * channel, re-armed there by the daemon's raw RXCONT and later on each + * PROBE's channel, it adopted the grid but never LOCKed or followed it + * and heard a few percent of the stream; + * (2) an ORIGINATOR (the tractor) in FAILED that adopted a grid heard in a + * camera stall's decay window lost originator authority, and with it + * self-follow, for good: in FAILED nothing LOCKs, so nothing demotes, + * so nothing clears the adoption. + * Every check marked [main fails] below fails against the pre-fix + * sx1276_rx.c and 500 ms dwell (shown 2026-10-24 in a scratch copy), and + * every run here also passes the shared simulation's structural checks + * (rx_reverse_delivery.c check_structure), which include RS-12.21's own: + * FAILED never survives a scan tick without authority, every FAIL is held + * or left by (b)'s rule with each exit recorded, the walker steps through + * the channel table in order, and no LOCK demotes sooner than 2 s after + * its last anchor. * * WHY (1). On leg 6 the base heard 5 of the tractor's 606 frames, on * channels consistent with a receiver parked on the scan walker's last - * channel (ch 9) and then on each PROBE's TX channel (44, 6, 6, 18, 18); - * base d(fhss_dec_aligned) 0, d(fhss_dec_snapped) 3, d(rej_epoch_drift) 2, - * d(clk_demotion_reset) 0; post radio_state 4 (RX_CONT). Leading - * hypothesis H1: the scan used its 4 x 30 s budget without a catch and - * entered scan FAILED, which absorbs (sx1276_rx_scan_policy.c); there a - * valid frame cannot LOCK, and the slot follower runs only when LOCKED on - * an adopted grid or under originator authority, which a follower never - * holds (sx1276_rx_grid_policy.c, sx1276_rx_slot_follow()). The base keeps - * transmitting on hop channels (its PROBEs) while its receiver no longer - * follows the hop sequence: the "synced receivers" requirement in the - * 15.247(a)(1) row of the FCC notes (AI NOTES/2026-05-19_FCC_Part15_902- - * 928_Compliance_Notes), so this is a compliance-relevant state as well as - * a delivery defect. - * + * channel and then on each PROBE's TX channel; base d(fhss_dec_aligned) 0, + * d(clk_demotion_reset) 0; post radio_state 4 (RX_CONT). Hypothesis H1: + * the scan used its 4 x 30 s budget without a catch and entered FAILED, + * which absorbed. The base kept transmitting on hop channels (its PROBEs) + * while its receiver no longer followed the hop sequence -- the "synced + * receivers" of the FCC 15.247(a)(1) row in the FCC notes (AI NOTES/ + * 2026-05-19_FCC_Part15_902-928_Compliance_Notes) -- so it was a compliance + * state as well as a delivery defect. * WHY (2). On 2026-10-18 legs W/X the tractor's scan also ran out into - * FAILED, harmlessly: an originator follows its own clock in any scan - * state. But an accepted header is adopted whatever the scan state - * (sx1276_rx_service(): grid.adopt is 1 for a node without authority, with - * UNANCHORED health while nothing is adopted), only a LOCKED -> SCANNING - * demotion or a reset clears the adoption (sx1276_rx_grid_on_demotion()), - * FAILED never LOCKs so it never demotes, and no node holds authority - * while a grid is adopted (sx1276_fhss_authority_is_originator()). So one - * camera stall (authority decays 1 s after the last own TX) and one base - * command heard in it end self-follow for the rest of the session. The - * RS-12.15 open question is therefore not only whether self-follow should - * run in FAILED: adoption in FAILED is irreversible and strips authority. + * FAILED, harmlessly while it streamed: an originator follows its own clock + * in any scan state. (b) keeps exactly that, and restarts the scan once the + * authority decays, so a command heard in the decay window LOCKs and the + * LOCK's demotion clears the adoption, as before FAILED. * * WHAT. Reuses the simulated SX1276, clock, air and main-loop driver of * check-rx-reverse-delivery (rx_reverse_delivery.c is #included with @@ -69,79 +66,65 @@ * 300 s of commands with a 2.5 s camera stall every 9 s; and the same run * without stalls as the control. * - * (1) ASSUMED -- built into the scenario, not observed on leg 6: - * - no frame reaches the base for the whole scan budget: the stream is - * withheld until 1 s after the final FAIL (to the firmware the same as - * a stream whose frames never land on a dwelled channel). FAILED at - * +120 s and the fault sub-bytes 20 21 22 a3 follow from that by - * construction: they are H1's predictions, not leg-6 observations - * (leg 6 logged no base FAULT URC; image_rx_daemon.py drops them at - * DEBUG); - * - 0 ppm between the boards (E1 also runs 2750 ppm); - * - fixed 200 B frames keyed 13.4 ms into the slot, so a PROBE (~30 ms of - * daemon latency after the frame) always lands in the NEXT slot: the - * model cannot produce leg 6's same-slot PROBEs (PROBE 3 at (17,22), - * PROBE 5 at (24,43)). That needs variable frame sizes and clock drift - * in the originator model; - * - no model of the tractor's side of the link. - * (1) ASSERTED -- checks 1-5, the mechanism, for any seed (-s): - * 1 four RX_SCAN_FAILED faults, sub 0x20 0x21 0x22 0xA3, the final one - * 120 s after activation; the modem is parked in STANDBY on the - * walker's last channel (dwell 60 = index 59 mod 50 = 9) and the - * daemon's raw RXCONT re-arms it there, without a retune; - * 2 then valid frames: the scan never LOCKs and never demotes (FAILED - * absorbs); the first frame heard is on that channel, SNAPPED, and its - * grid adopted; neither the slot follower nor the walker ever retunes - * the receiver, and the shared scheduler moves only on an own TX or a - * SNAP; - * 3 each reactive PROBE is keyed in the originator's slot it claims (the - * adopted grid), and the firmware's post-TX re-arm leaves the - * receiver in RX_CONT on the PROBE's channel: every later frame heard - * is on the channel of the PROBE before it; - * 4 consider_remote() compares against a scheduler epoch frozen at the - * last own TX or SNAP: never ALIGNED; a catch within one epoch of it - * is SNAPPED, two or more is REJECTED_EPOCH_DRIFT, and - * REJECTED_LOCKED_OUT only while the adopted clock is at most - * SX1276_FHSS_CLOCK_FRESH_MS (2 s) old (anchored by the last SNAP or - * own TX); - * 5 so the base hears a few percent of the stream, and the run ends in - * RX_CONT, still FAILED. - * Independently of any seed these match leg 6: the first catch on the - * walker's last channel, ch 9 (from the constants); every later catch on - * the previous PROBE's channel (the post-TX re-arm); 0 ALIGNED, 0 - * demotions, ends in RX_CONT. Seeds 1-1000 pass checks 1-5 except 558 - * (one catch in 180 s; checks 2-4 need two). - * (1) SEED-FITTED -- a soft check, default seed 852 only, printed as a NOTE - * when it no longer holds and never failed: leg 6's first two catches - * slot for slot -- (13,17) on ch 9, PROBE (13,18) on ch 44, (14,40) on - * ch 44 -- and its counters: 5 heard, 5 PROBEs, 3 SNAPPED, 2 EPOCH_DRIFT, - * 0 LOCKED_OUT. (The stream's grid starts at epoch 13 so its slots carry - * leg 6's channels.) Over seeds 1-1000 that split comes up in 35, - * exactly 5 catches in 139, and 1-14 catches in all. - * (1) NOT REPRODUCED: leg 6's catches 3-5 -- SIL (16,29) (17,8) (25,15), - * leg 6 (17,22) (18,2) (24,43) -- and PROBE channels 3-5 (SIL 37, 49, 22; - * leg 6 6, 18, 18: its repeats are the same-slot PROBEs above); the first - * catch comes at +124.6 s, on leg 6 at +138 s. + * (1) ASSUMED, as for the characterisation: no frame reaches the base for + * the whole 120 s scan budget (the stream starts 1 s after the first + * final FAIL); 0 ppm between the boards; fixed 200 B frames. The stream + * runs 180 s, and on to 60 s past the LOCK (300 s at most). Default seed + * 852 (the characterisation's: it reproduced leg 6's first two catches + * under the defect); `-s ` runs another -- seeds 1-1000 all pass. + * (1) ASSERTED -- leg-6 replay: + * A1 the scan budget runs out as before -- four RX_SCAN_FAILED faults, sub + * 0x20 0x21 0x22 0xA3, the final 120 s after activation -- but the + * follower does not stay FAILED: in the same pass it is SCANNING again + * (the edge recorded), its receiver in RX_CONT on the walker's next + * channel, (4 x REDESIGN/DWELL) mod 50 = 10 [main fails: STANDBY on + * the walker's last channel]; + * A2 then the stream: the scan LOCKs on a frame heard on a walker channel + * (SNAPPED onto the stream's grid, adopted); every FAIL between the + * stream start and the LOCK is final (0xA3), 30 s after the one before + * [main fails: FAILED absorbs, no LOCK]; + * A3 the base follows the hop grid from the LOCK on: still LOCKED at the + * end, never demoted, no further FAIL; on every pass in mid-slot its + * clock is on the originator's slot and its receiver, when listening, + * on that slot's channel; every frame heard after the LOCK was tuned + * by the slot follower and is ALIGNED; it hears >= 95 % of the frames + * keyed from 1 s after the LOCK; every PROBE is keyed in the + * originator's slot its header claims [main fails]; + * A4 the receiver is never left out of RX (no pass ends outside RX_CONT + * with no TX keyed) and the daemon's raw RXCONT never has to re-arm it: + * the fix does not lean on a host re-arm [main fails]. + * (1b) ASSERTED -- a silent follower, 305 s, no stream: one RX_SCAN_FAILED + * every 30 s (sub 0x20 0x21 0x22, then 0xA3 every time; the warm bit, + * sticky for the boot, aside), each a restart + * into SCANNING, never FAILED on a scan tick; the walk covers all 50 + * channels (d2: it continues across the restarts -- the first prototype + * restarted it at channel 0 and rescanned channels 0-14 for ever); never + * out of RX, never re-armed by the daemon [main fails]. * * (2) ASSERTED -- default seed (scenario 7's); `-e` measures other seeds: - * T1 no command for 150 s: the scan runs out into FAILED (one final FAIL, - * no LOCK), nothing adopted, the tractor holds authority; + * T1 no command for 150 s: the streaming tractor's scan runs out into + * FAILED (one final FAIL, no LOCK) and holds it -- it holds authority; + * nothing adopted; * T2 control, no stalls: authority on every pass from then on, no * adoption, no LOCK, the own grid never renumbered, self-follow keeps - * running, and every clean command sent while it held authority is - * received (scenario 8's property); - * T3 with stalls: a command heard in a stall's decay window is adopted - * (UNANCHORED: any epoch accepted) and nothing undoes it: no LOCK, no - * demotion, still adopted and still FAILED at the end; the clock is - * never reset (tx_first_anchor 1) nor the own grid renumbered (a TX - * more than one slot off the grid of the TX before it); - * T4 from that adoption on: no authority on any pass, no self-follow, no - * LOCKED follow, and fewer than half of the commands received. + * running, every clean command sent while it held authority received, + * and FAILED held throughout (no restart) -- 2026-10-18 legs W/X; + * T3 with stalls: the first decay of authority in FAILED restarts the scan + * on the next tick, and the walker is back within one dwell of every + * decay; a command heard in a decay window LOCKs and is adopted, and a + * LOCK's demotion resets the adopted clock [main fails: adopted in + * FAILED for good, no LOCK, no demotion]; + * T4 after the first adoption the tractor holds authority again and + * self-follows again, and it receives at least half of the commands + * sent from then on [main fails: no authority, no self-follow, fewer + * than half received]. + * What T3/T4 recover to is the decay window before FAILED, which the 2 s + * dwell and L move (memo section 5 item 3, accepted with the fix): own- + * grid renumbering and phase restarts are not asserted here. * * `rx_scan_failed_follower -v` prints the runs (catches, PROBEs, counters). - * `rx_scan_failed_follower -e` skips the characterisations and runs the - * evaluation used to compare fix options (built against patched copies of - * the firmware, SIL only); it prints and never fails (`-e3`: E3 only): + * `rx_scan_failed_follower -e` skips the checks and runs the evaluation the + * memo's numbers come from; it prints and never fails (`-e3`: E3 only): * E1 leg-6 replay, seeded trials: silent for the scan budget, the * stream from 1 s after the final FAIL on a random grid phase: does * the base LOCK, how long after the stream starts, and what share of @@ -150,7 +133,8 @@ * 610-1400 ppm slow vs its host, tractor grid ~+1600 ppm); * E1w E1 with a host watchdog model, option (c): the rx daemon re-sends * REG_PROFILE=1 (profile activation: sx1276_fhss_init + - * sx1276_rx_scan_reset) 200 ms after a final RX_SCAN_FAILED fault; + * sx1276_rx_scan_reset) 200 ms after each final RX_SCAN_FAILED fault + * (not adopted; kept for comparison with the memo); * E2 cold start, the stream already on at activation (random grid * phase): P(no LOCK within the 120 s budget), the attempt that * catches, the per-attempt catch probability, at 2 fps and 1 fps; @@ -158,13 +142,16 @@ * in how many authority never returns after the first adoption, the * authority share of the phase's passes, commands received, LOCKs, * demotions, phase restarts (tx_first_anchor > 1) and own-grid - * renumbering; with stalls, the control without, and main's - * behaviour before FAILED (commands from 5 s, so the scan LOCKs on - * them and never fails out: scenario 7's decay window); - * E3w E3 with option (c)'s watchdog on the TRACTOR too. Not proposed -- - * (c) runs on the base only, which leaves E3 as it is on main -- but - * it shows why: a re-activation restarts the originator's own grid - * at epoch 0 under the base (renumbered by hundreds of slots). + * renumbering; with stalls, the control without, and the decay + * window before FAILED (commands from 5 s, so the scan LOCKs on them + * and never fails out: scenario 7's shape); + * E3w E3 with option (c)'s watchdog on the TRACTOR too: a re-activation + * restarts the originator's own grid at epoch 0 under the base + * (renumbered by hundreds of slots) -- why (c) was never proposed + * there. + * Memo numbers, 2026-10-24 (b+L+d2): E1 100/100, median 17.9 s, p90 + * 56.3 s, 99.8 % heard (2750 ppm: 100/100, 99.9 %); E2 1.3 % at 2 fps, + * 10.7 % at 1 fps; E3 with stalls: authority back in 50/50. */ #define RX_REVERSE_DELIVERY_SIM_ONLY 1 @@ -176,6 +163,12 @@ #include +/* The acceptance figures and the memo's evaluation are for this dwell; a + * different one needs `-e` re-run (and check-rx-reverse-delivery scenario + * 6's 6 s window rescaled for a dwell of 6 s or more). */ +_Static_assert(SX1276_RX_SCAN_DWELL_MS == 2000U, + "RS-12.21 d2: the acceptance check is sized for a 2 s scan dwell"); + /* ------------------------------------------------------------------ */ /* Originator (tractor) model: a camera stream on its own grid */ /* ------------------------------------------------------------------ */ @@ -294,9 +287,6 @@ typedef struct { uint32_t probes; uint32_t arms_idle, arms_after_tx; uint32_t reactivations; - uint64_t first_idle_arm_us; /* first raw arm (after the final FAIL) */ - uint32_t first_idle_arm_hz; - uint8_t first_idle_arm_tuner; } rxd_t; static rxd_t g_rxd; @@ -350,14 +340,7 @@ static void rxd_tick(void) { } if (g_now_us >= g_rxd.next_idle_us) { g_rxd.next_idle_us += RXD_IDLE_CHECK_US; - if (rxd_ensure_rxcont() != 0U) { - ++g_rxd.arms_idle; - if (g_rxd.first_idle_arm_us == 0U) { - g_rxd.first_idle_arm_us = g_now_us; - g_rxd.first_idle_arm_hz = g_radio.freq_hz; - g_rxd.first_idle_arm_tuner = (uint8_t)g_tuner; - } - } + g_rxd.arms_idle += rxd_ensure_rxcont(); } if (g_rxd.watchdog != 0U) { if (g_final_fail_us != 0U && g_final_fail_us != g_rxd.seen_final_us) { @@ -376,19 +359,19 @@ static void rxd_tick(void) { /* One run: activation, scan budget, stream */ /* ------------------------------------------------------------------ */ -#define L6_MAX_CATCH 256U +#define L6_MAX_CATCH 1024U /* every frame of a 300 s stream at 2 fps */ #define L6_DEC_DIM SX1276_RX_CONSIDER_REMOTE_COUNT_DIM +/* Mid-slot window for the on-grid check: both clocks are within a few ms + * of each other (ms-quantised anchors), so stay clear of the boundary. */ +#define L6_GRID_GUARD_US 10000ULL typedef struct { uint64_t t_us; uint32_t abs; uint32_t hz; - uint32_t local_epoch; /* scheduler epoch before the frame */ - uint32_t parked_hz; /* where the receiver had been left */ uint8_t dec; uint8_t tuner; /* who had tuned the synth for it */ - uint8_t adopted_before; - uint8_t fresh_before; /* adopted clock <= FRESH_MS old before it */ + uint8_t locked_before; /* scan LOCKED before the frame */ } l6_catch_t; typedef struct { @@ -398,7 +381,10 @@ typedef struct { uint8_t stream_at_activation; /* E2: stream on from activation */ uint32_t start_abs; /* E1: 0 = random */ uint32_t run_after_start_ms; /* E1 */ + uint32_t after_lock_ms; /* and at least this past the LOCK + * (no LOCK: up to L6_STREAM_CAP_MS) */ uint32_t cap_ms; /* E2: give up after this */ + uint32_t silent_ms; /* (1b): no stream at all */ uint8_t watchdog; uint8_t stop_at_lock; } l6_cfg_t; @@ -407,36 +393,27 @@ typedef struct { uint32_t act_ms; uint64_t act_us; uint64_t start_us; /* stream start (0 = not yet) */ - uint64_t final_pass_us; /* pass that saw the final FAIL */ + uint64_t final_pass_us; /* pass that saw the first final FAIL */ uint8_t final_mode; /* modem state right after it */ uint32_t final_hz; uint8_t final_tuner; + uint8_t final_scan; /* scan state (mirror) right after it */ uint64_t lock_us; /* first LOCK (0 = none) */ uint32_t fails_before_lock; - uint64_t follow_us; /* first slot-follow retune after the start */ - uint32_t parked_hz; /* receiver channel left by walker / own TX */ + uint8_t lock_dec, lock_tuner; + uint32_t adoptions_at_lock; uint32_t ncatch; l6_catch_t catches[L6_MAX_CATCH]; uint32_t dec[L6_DEC_DIM]; - uint32_t tx_done, tx_rearm_on_tx_ch, tx_on_grid; + uint32_t tx_done, tx_on_grid; uint32_t tx_hz[L6_MAX_CATCH]; uint32_t tx_abs[L6_MAX_CATCH]; - uint32_t sched_unexplained; - uint32_t walker_at_start, follow_at_start, locks_at_start, dem_at_start; + uint32_t pass_not_rx; /* passes ending outside RX, no TX keyed */ + uint32_t grid_checked, grid_off; /* after the LOCK, mid-slot passes */ } l6_t; static l6_t g_l6; -static uint8_t chan_of_hz(uint32_t hz) { - for (uint8_t i = 0U; i < SX1276_FHSS_CHANNEL_COUNT; ++i) { - if (sx1276_fhss_chantab_center_hz(i) == hz) { - return i; - } - } - return 0xFFU; -} - -static void l6_on_catch(uint32_t local_epoch, uint8_t adopted_before, - uint8_t fresh_before) { +static void l6_on_catch(uint8_t locked_before) { const uint16_t seq = (uint16_t)(g_frame.payload[1] | ((uint16_t)g_frame.payload[2] << 8)); if (seq >= g_os.sent) { @@ -452,12 +429,9 @@ static void l6_on_catch(uint32_t local_epoch, uint8_t adopted_before, k->t_us = g_now_us; k->abs = g_os.log[seq].abs; k->hz = g_os.log[seq].hz; - k->local_epoch = local_epoch; - k->parked_hz = g_l6.parked_hz; k->dec = g_last_dec; k->tuner = (uint8_t)g_heard_tuner; - k->adopted_before = adopted_before; - k->fresh_before = fresh_before; + k->locked_before = locked_before; } ++g_l6.ncatch; } @@ -470,17 +444,11 @@ static void l6_on_tx_done(void) { } { const uint32_t abs = sx1276_fhss_clock_abs_of(h.epoch, h.hop_idx); - const uint32_t hz = hop_hz(abs); if (g_l6.tx_done < L6_MAX_CATCH) { - g_l6.tx_hz[g_l6.tx_done] = hz; + g_l6.tx_hz[g_l6.tx_done] = hop_hz(abs); g_l6.tx_abs[g_l6.tx_done] = abs; } ++g_l6.tx_done; - /* The firmware's own post-TX re-arm (sx1276_tx_cleanup): RX_CONT, - * on the channel it just transmitted on. */ - if (g_radio.mode == SX1276_STATE_RX_CONT && g_radio.freq_hz == hz) { - ++g_l6.tx_rearm_on_tx_ch; - } /* Keyed in the originator's slot its header claims. */ if (g_os.on != 0U && g_radio.tx_start_us >= g_os.t0_us && os_abs_at(g_radio.tx_start_us) == abs) { @@ -490,55 +458,52 @@ static void l6_on_tx_done(void) { } static void l6_pass(void) { - const uint32_t ep0 = sx1276_fhss_current_epoch(); - const uint8_t sl0 = sx1276_fhss_current_slot(); const uint32_t frames0 = g_sc.frames_recv; const uint32_t txok0 = g_sc.fw_tx_ok; - const uint64_t txstart0 = g_radio.tx_start_us; - const uint8_t adopted0 = sx1276_rx_grid_adopted(); - /* The grid policy hands consider_remote FRESH (-> LOCKED_OUT) only for - * an adopted, valid clock at most FRESH_MS old at the frame; its age - * then is at least the age here (a REJECTED frame does not re-anchor; - * platform_now_ms() does not advance the simulated clock). */ - const uint8_t fresh0 = (uint8_t)(adopted0 != 0U && - sx1276_fhss_clock_valid() != 0U && - sx1276_fhss_clock_age_ms(platform_now_ms()) <= SX1276_FHSS_CLOCK_FRESH_MS); const uint32_t locks0 = g_sc.locks; - const uint32_t follow0 = g_sc.follow_locked + g_sc.follow_own; - uint8_t snapped_now = 0U; + const uint8_t locked0 = (uint8_t)(g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED); os_tick(); fw_pass(); if (g_sc.frames_recv != frames0) { - l6_on_catch(ep0, adopted0, fresh0); - snapped_now = (uint8_t)(g_last_dec == SX1276_FHSS_SNAP_DEC_SNAPPED); + l6_on_catch(locked0); } if (g_sc.fw_tx_ok != txok0) { l6_on_tx_done(); } - /* Who left the receiver where: the walker, or an own TX (a slot-follow - * retune is tracked by its own counter). */ - if (g_tuner == CTX_SCAN || g_tuner == CTX_HOST || g_tuner == CTX_MAILBOX) { - g_l6.parked_hz = g_last_freq; - } - if ((sx1276_fhss_current_epoch() != ep0 || sx1276_fhss_current_slot() != sl0) && - snapped_now == 0U && g_radio.tx_start_us == txstart0) { - ++g_l6.sched_unexplained; - } if (g_sc.locks != locks0 && g_l6.lock_us == 0U) { g_l6.lock_us = g_now_us; g_l6.fails_before_lock = g_sc.scan_fail_urc; - } - if (g_l6.start_us != 0U && g_l6.follow_us == 0U && - g_sc.follow_locked + g_sc.follow_own != follow0) { - g_l6.follow_us = g_now_us; + g_l6.lock_dec = g_last_dec; + g_l6.lock_tuner = (uint8_t)g_heard_tuner; + g_l6.adoptions_at_lock = g_sc.adoptions; } if (g_final_fail_us != 0U && g_l6.final_pass_us == 0U) { g_l6.final_pass_us = g_now_us; g_l6.final_mode = (uint8_t)g_radio.mode; g_l6.final_hz = g_radio.freq_hz; g_l6.final_tuner = (uint8_t)g_tuner; + g_l6.final_scan = (uint8_t)g_scan_mirror; + } + if (g_radio.mode != SX1276_STATE_RX_CONT && !sx1276_tx_busy()) { + ++g_l6.pass_not_rx; + } + /* From the LOCK on, in mid-slot: the clock on the originator's slot and + * the receiver, when listening, on that slot's channel. */ + if (g_l6.lock_us != 0U && g_os.on != 0U && g_now_us >= g_os.t0_us && + g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED) { + const uint32_t abs = os_abs_at(g_now_us); + const uint64_t in_slot = g_now_us - os_boundary_us(abs - g_os.abs0); + if (in_slot >= L6_GRID_GUARD_US && in_slot + L6_GRID_GUARD_US < SLOT_US) { + ++g_l6.grid_checked; + if (sx1276_fhss_clock_valid() == 0U || + sx1276_fhss_clock_abs_slot(platform_now_ms()) != abs || + (g_radio.mode == SX1276_STATE_RX_CONT && + g_radio.freq_hz != hop_hz(abs))) { + ++g_l6.grid_off; + } + } } rxd_tick(); } @@ -551,10 +516,27 @@ static void l6_start_stream(const l6_cfg_t *c, uint64_t t_us) { os_start(t_us + (uint64_t)rnd_range(0U, (uint32_t)SLOT_US - 1U), abs0, c->ppm, c->period_ms, (uint8_t)OS_FRAME_LEN); g_l6.start_us = g_now_us; - g_l6.walker_at_start = g_sc.walker_retunes; - g_l6.follow_at_start = g_sc.follow_locked + g_sc.follow_own; - g_l6.locks_at_start = g_sc.locks; - g_l6.dem_at_start = g_sc.demotions; +} + +#define L6_STREAM_CAP_MS 300000U + +/* Keep the stream running? run_after_start_ms after its start, then (with + * after_lock_ms) until that long after the LOCK, or the cap without one. */ +static uint8_t l6_more(const l6_cfg_t *c) { + const uint64_t t = g_now_us - g_l6.start_us; + if (c->stop_at_lock != 0U && g_l6.lock_us != 0U) { + return 0U; + } + if (t < (uint64_t)c->run_after_start_ms * 1000ULL) { + return 1U; + } + if (c->after_lock_ms == 0U) { + return 0U; + } + if (g_l6.lock_us == 0U) { + return (uint8_t)(t < (uint64_t)L6_STREAM_CAP_MS * 1000ULL); + } + return (uint8_t)(g_now_us < g_l6.lock_us + (uint64_t)c->after_lock_ms * 1000ULL); } static void l6_run(const char *name, const l6_cfg_t *c) { @@ -567,6 +549,12 @@ static void l6_run(const char *name, const l6_cfg_t *c) { g_rxd.next_idle_us = g_now_us + (uint64_t)rnd_range(0U, 4999U) * 1000ULL; g_l6.act_us = g_now_us; g_l6.act_ms = platform_now_ms(); + if (c->silent_ms != 0U) { + while (g_now_us < g_l6.act_us + (uint64_t)c->silent_ms * 1000ULL) { + l6_pass(); + } + return; + } if (c->stream_at_activation != 0U) { l6_start_stream(c, g_now_us); while (g_now_us < g_l6.act_us + (uint64_t)c->cap_ms * 1000ULL && @@ -575,8 +563,8 @@ static void l6_run(const char *name, const l6_cfg_t *c) { } return; } - /* Silent for the scan budget; the stream starts 1 s after the final - * FAIL (or at +121 s if a variant never reports one). */ + /* Silent for the scan budget; the stream starts 1 s after the first + * final FAIL (or at +121 s if a variant never reports one). */ while (g_l6.start_us == 0U) { l6_pass(); if ((g_final_fail_us != 0U && g_now_us >= g_final_fail_us + 1000000ULL) || @@ -584,14 +572,13 @@ static void l6_run(const char *name, const l6_cfg_t *c) { l6_start_stream(c, g_now_us); } } - while (g_now_us < g_l6.start_us + (uint64_t)c->run_after_start_ms * 1000ULL && - !(c->stop_at_lock != 0U && g_l6.lock_us != 0U)) { + while (l6_more(c) != 0U) { l6_pass(); } } /* ------------------------------------------------------------------ */ -/* The characterisation (leg 6 on main) */ +/* The acceptance check, base: leg 6 replayed, and a silent follower */ /* ------------------------------------------------------------------ */ static const char *dec_name(uint8_t d) { @@ -604,223 +591,219 @@ static const char *dec_name(uint8_t d) { } } -/* The default seed's camera timing reproduces leg 6's catches 1 and 2 slot - * for slot -- (13,17) on ch 9, PROBE (13,18) on ch 44, (14,40) on ch 44 -- - * and its counters: 5 heard, 5 PROBEs, 3 SNAPPED, 2 EPOCH_DRIFT, 0 ALIGNED, - * 0 LOCKED_OUT. A soft check (a NOTE, never a failure): a harness change - * that moves the random sequence needs a new default (scan seeds with - * -s -v). `-s ` runs another seed against checks 1-5 only. */ #define L6_LEG6_SEED 852U static uint32_t g_l6_seed = L6_LEG6_SEED; -/* " us after ", or "none" when t is unset or before ref. */ -static const char *us_after(char *buf, size_t n, uint64_t t_us, uint64_t ref_us) { - if (t_us == 0U || t_us < ref_us) { - snprintf(buf, n, "none"); - } else { - snprintf(buf, n, "%llu us", (unsigned long long)(t_us - ref_us)); - } - return buf; -} +/* Scan attempts in the budget, and the walker channel after them: each + * attempt dwells on REDESIGN / DWELL channels and the walk continues. */ +#define L6_ATTEMPTS (SX1276_RX_SCAN_MAX_RETRIES + 1U) +#define L6_DWELLS_PER_TRY (SX1276_RX_SCAN_REDESIGN_MS / SX1276_RX_SCAN_DWELL_MS) -static void test_leg6_characterisation(void) { - /* Leg 6's first catch was (epoch 13, hop 17) on ch 9; start the grid - * at epoch 13 so the same slots carry the same channels. */ +static void test_leg6_recovers(void) { + /* Leg 6's first catch was (epoch 13, hop 17); start the grid at epoch + * 13 as the characterisation did. */ const l6_cfg_t c = { .seed = g_l6_seed, .ppm = 0, .period_ms = 500U, .stream_at_activation = 0U, .start_abs = 13U * SX1276_FHSS_CHANNEL_COUNT, - .run_after_start_ms = 180000U, .cap_ms = 0U, + .run_after_start_ms = 180000U, .after_lock_ms = 60000U, + .cap_ms = 0U, .silent_ms = 0U, .watchdog = 0U, .stop_at_lock = 0U, }; - /* With a dwell restart at channel 0 on every attempt, the walker's last - * channel is dwell (REDESIGN / DWELL) - 1, mod 50: index 9 on main. */ - const uint8_t walker_last = (uint8_t)(((SX1276_RX_SCAN_REDESIGN_MS - / SX1276_RX_SCAN_DWELL_MS) - 1U) + const uint8_t walker_next = (uint8_t)((L6_ATTEMPTS * L6_DWELLS_PER_TRY) % SX1276_FHSS_CHANNEL_COUNT); - uint32_t heard = 0U, n_snap_near = 0U, n_drift_far = 0U, n_locked_out = 0U; - uint32_t bad_dec = 0U, bad_park = 0U, bad_tuner = 0U; - char arm_buf[32]; + uint32_t after_follow = 0U, after_aligned = 0U, after_n = 0U; + uint32_t late_sent = 0U, late_heard = 0U; + uint32_t fails_unlocked_bad = 0U; - l6_run("leg 6: follower in scan FAILED", &c); - /* A PROBE may be on air when the run ends: let it finish (its post-TX - * re-arm is part of the mechanism checked below). */ + l6_run("leg 6 replay: follower recovers", &c); + /* A PROBE may be on air when the run ends: let it finish. */ for (int i = 0; i < 1000 && sx1276_tx_busy(); ++i) { l6_pass(); } - heard = g_l6.ncatch; /* every frame heard, past L6_MAX_CATCH too */ print_scenario(); check_structure(); - /* 1 the scan budget runs out into FAILED and the modem is parked. */ - CHECK(g_sc.scan_fail_urc == 4U && g_sc.fail_final == 1U && - g_fail_sub[0] == 0x20U && g_fail_sub[1] == 0x21U && - g_fail_sub[2] == 0x22U && g_fail_sub[3] == 0xA3U, - "[%s] scan faults: %u (final %u), sub %02x %02x %02x %02x" - " (expect 4: 20 21 22 a3)", g_sc.name, g_sc.scan_fail_urc, - g_sc.fail_final, g_fail_sub[0], g_fail_sub[1], g_fail_sub[2], - g_fail_sub[3]); + /* A1 the budget runs out as before, but FAILED does not hold. */ + CHECK(g_sc.scan_fail_urc >= 4U && g_fail_sub[0] == 0x20U && + g_fail_sub[1] == 0x21U && g_fail_sub[2] == 0x22U && + g_fail_sub[3] == 0xA3U, + "[%s] scan faults: %u, sub %02x %02x %02x %02x (expect >= 4: 20 21 22" + " a3)", g_sc.name, g_sc.scan_fail_urc, g_fail_sub[0], g_fail_sub[1], + g_fail_sub[2], g_fail_sub[3]); CHECK((uint32_t)(g_fail_ms[3] - g_l6.act_ms) >= 120000U && (uint32_t)(g_fail_ms[3] - g_l6.act_ms) < 120100U, "[%s] final FAIL %u ms after activation (expect 120 s)", g_sc.name, (unsigned)(g_fail_ms[3] - g_l6.act_ms)); - CHECK(g_l6.final_mode == (uint8_t)SX1276_STATE_STANDBY && + CHECK(g_l6.final_scan == (uint8_t)SX1276_RX_SCAN_STATE_SCANNING && + g_l6.final_mode == (uint8_t)SX1276_STATE_RX_CONT && g_l6.final_tuner == (uint8_t)CTX_SCAN && - chan_of_hz(g_l6.final_hz) == walker_last, - "[%s] after the final FAIL: mode %u (expect STANDBY %u), on ch %u" - " (expect the walker's last, %u), last tuned by ctx %u", - g_sc.name, g_l6.final_mode, (unsigned)SX1276_STATE_STANDBY, - chan_of_hz(g_l6.final_hz), walker_last, g_l6.final_tuner); - CHECK(g_rxd.first_idle_arm_us > g_l6.final_pass_us && - g_rxd.first_idle_arm_us <= g_l6.final_pass_us + RXD_IDLE_CHECK_US && - g_rxd.first_idle_arm_hz == g_l6.final_hz && - g_rxd.first_idle_arm_tuner == (uint8_t)CTX_SCAN, - "[%s] the daemon's raw RXCONT did not re-arm the parked receiver in" - " place (first raw arm: %s after the final FAIL, ch %u, tuner %u)", - g_sc.name, - us_after(arm_buf, sizeof(arm_buf), g_rxd.first_idle_arm_us, - g_l6.final_pass_us), - chan_of_hz(g_rxd.first_idle_arm_hz), g_rxd.first_idle_arm_tuner); - - /* 2 no LOCK, no demotion, no follow, no walk; scheduler frozen. */ - CHECK(g_sc.locks == 0U && g_sc.demotions == 0U && g_sc.dem_reset == 0U && - g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED, - "[%s] FAILED did not absorb: locks=%u demotions=%u clk_demotion_reset=%u" - " scan state %u", g_sc.name, g_sc.locks, g_sc.demotions, - g_sc.dem_reset, (unsigned)g_scan_mirror); - CHECK(g_sc.walker_retunes == g_l6.walker_at_start && - g_sc.follow_locked + g_sc.follow_own == g_l6.follow_at_start && - g_sc.follow_locked + g_sc.follow_own == 0U, - "[%s] the receiver was retuned by the walker (%u) or the slot follower" - " (%u) once the stream ran", g_sc.name, - g_sc.walker_retunes - g_l6.walker_at_start, - g_sc.follow_locked + g_sc.follow_own); - CHECK(g_l6.sched_unexplained == 0U, - "[%s] the shared scheduler moved %u times without an own TX or a SNAP", - g_sc.name, g_l6.sched_unexplained); - CHECK(g_l6.ncatch >= 2U && sx1276_rx_grid_adopted() != 0U && - g_sc.adoptions >= 1U && g_sc.adopt_via_walker == 1U, - "[%s] catches=%u adopted=%u adoptions=%u (via walker %u)", g_sc.name, - g_l6.ncatch, sx1276_rx_grid_adopted(), g_sc.adoptions, - g_sc.adopt_via_walker); - + chan_of_hz(g_l6.final_hz) == walker_next, + "[%s] after the final FAIL: scan state %u (expect SCANNING %u), mode" + " %u (expect RX_CONT %u), on ch %u (expect the walker's next, %u)," + " last tuned by ctx %u", g_sc.name, g_l6.final_scan, + (unsigned)SX1276_RX_SCAN_STATE_SCANNING, g_l6.final_mode, + (unsigned)SX1276_STATE_RX_CONT, chan_of_hz(g_l6.final_hz), + walker_next, g_l6.final_tuner); + + /* A2 the stream: a LOCK on a walker channel; FAILs before it final. */ + for (uint32_t i = 4U; i < g_l6.fails_before_lock && i < FAIL_LOG; ++i) { + if (g_fail_sub[i] != 0xA3U || + (uint32_t)(g_fail_ms[i] - g_fail_ms[i - 1U]) < + SX1276_RX_SCAN_REDESIGN_MS || + (uint32_t)(g_fail_ms[i] - g_fail_ms[i - 1U]) >= + SX1276_RX_SCAN_REDESIGN_MS + 100U) { + ++fails_unlocked_bad; + } + } + CHECK(g_l6.lock_us != 0U && g_l6.lock_us >= g_l6.start_us && + g_l6.lock_tuner == (uint8_t)CTX_SCAN && + g_l6.lock_dec == (uint8_t)SX1276_FHSS_SNAP_DEC_SNAPPED && + g_l6.adoptions_at_lock == 1U && fails_unlocked_bad == 0U, + "[%s] no LOCK on the stream: LOCK %s, on a channel tuned by ctx %u" + " (expect the walker, %u), decision %s (expect SNAPPED), adoptions" + " %u (expect 1); %u of the FAILs before it not final 30 s apart", + g_sc.name, (g_l6.lock_us != 0U) ? "yes" : "never", g_l6.lock_tuner, + (unsigned)CTX_SCAN, dec_name(g_l6.lock_dec), g_l6.adoptions_at_lock, + fails_unlocked_bad); + + /* A3 from the LOCK on, the base follows the hop grid. */ for (uint32_t i = 0U; i < g_l6.ncatch && i < L6_MAX_CATCH; ++i) { const l6_catch_t *k = &g_l6.catches[i]; - const uint32_t ep = sx1276_fhss_clock_epoch_of(k->abs); - const int32_t d = (int32_t)(ep - k->local_epoch); - const uint32_t ad = (uint32_t)((d < 0) ? -d : d); - if (k->hz != k->parked_hz) { - ++bad_park; - } - /* first: on the walker's channel; later: on the last PROBE's. */ - if (i == 0U) { - if (k->tuner != (uint8_t)CTX_SCAN || chan_of_hz(k->hz) != walker_last || - k->dec != SX1276_FHSS_SNAP_DEC_SNAPPED || k->adopted_before != 0U) { - ++bad_tuner; - } + if (k->locked_before == 0U) { continue; } - if (k->tuner != (uint8_t)CTX_HOST && k->tuner != (uint8_t)CTX_MAILBOX) { - ++bad_tuner; - } - if (k->dec == SX1276_FHSS_SNAP_DEC_SNAPPED && ad <= 1U) { - ++n_snap_near; - } else if (k->dec == SX1276_FHSS_SNAP_DEC_REJECTED_EPOCH_DRIFT && ad >= 2U) { - ++n_drift_far; - } else if (k->dec == SX1276_FHSS_SNAP_DEC_REJECTED_LOCKED_OUT && - k->fresh_before != 0U) { - ++n_locked_out; - } else { - ++bad_dec; - } + ++after_n; + after_follow += (uint32_t)(k->tuner == (uint8_t)CTX_FOLLOW); + after_aligned += (uint32_t)(k->dec == (uint8_t)SX1276_FHSS_SNAP_DEC_ALIGNED); } - CHECK(bad_tuner == 0U && bad_park == 0U, - "[%s] %u catches not where the walker / the last PROBE left the" - " receiver, %u off the parked channel", g_sc.name, bad_tuner, bad_park); - - /* 3 PROBEs keyed on the adopted grid; post-TX re-arm on their channel. */ - CHECK(g_l6.tx_done >= 2U && g_l6.tx_rearm_on_tx_ch == g_l6.tx_done && - g_l6.tx_on_grid == g_l6.tx_done && g_rxd.arms_after_tx == 0U, - "[%s] PROBEs=%u: re-armed on their channel %u, on the originator's slot" - " %u, re-armed by the daemon after TX %u", g_sc.name, g_l6.tx_done, - g_l6.tx_rearm_on_tx_ch, g_l6.tx_on_grid, g_rxd.arms_after_tx); - - /* 4 frozen scheduler epoch: never ALIGNED; SNAPPED near, DRIFT far, - * LOCKED_OUT only on a clock at most FRESH_MS old. */ - CHECK(g_l6.dec[SX1276_FHSS_SNAP_DEC_ALIGNED] == 0U && bad_dec == 0U && - n_snap_near + n_drift_far >= 1U, - "[%s] consider_remote: aligned=%u snapped=%u (%u within one epoch)" - " epoch_drift=%u (%u two or more) locked_out=%u (%u on a fresh clock)," - " %u unexplained", g_sc.name, g_l6.dec[SX1276_FHSS_SNAP_DEC_ALIGNED], - g_l6.dec[SX1276_FHSS_SNAP_DEC_SNAPPED], n_snap_near, - g_l6.dec[SX1276_FHSS_SNAP_DEC_REJECTED_EPOCH_DRIFT], n_drift_far, - g_l6.dec[SX1276_FHSS_SNAP_DEC_REJECTED_LOCKED_OUT], n_locked_out, - bad_dec); - - /* 5 a few percent heard; ends in RX_CONT, still FAILED. */ - CHECK(heard * 10U < g_os.sent && g_radio.mode == SX1276_STATE_RX_CONT, - "[%s] heard %u of %u frames, final mode %u (expect < 10 %%, RX_CONT)", - g_sc.name, heard, g_os.sent, (unsigned)g_radio.mode); - - /* Leg 6's fingerprint, for the default seed: a soft check. */ - if (g_l6_seed == L6_LEG6_SEED) { - const uint32_t leg6_abs1 = 13U * SX1276_FHSS_CHANNEL_COUNT + 17U; - const uint32_t leg6_abs2 = 14U * SX1276_FHSS_CHANNEL_COUNT + 40U; - const uint8_t match = (uint8_t)( - g_l6.ncatch == 5U && g_l6.tx_done == 5U && - g_l6.dec[SX1276_FHSS_SNAP_DEC_SNAPPED] == 3U && - g_l6.dec[SX1276_FHSS_SNAP_DEC_REJECTED_EPOCH_DRIFT] == 2U && - g_l6.dec[SX1276_FHSS_SNAP_DEC_REJECTED_LOCKED_OUT] == 0U && - g_l6.catches[0].abs == leg6_abs1 && chan_of_hz(g_l6.catches[0].hz) == 9U && - g_l6.tx_abs[0] == leg6_abs1 + 1U && chan_of_hz(g_l6.tx_hz[0]) == 44U && - g_l6.catches[1].abs == leg6_abs2 && chan_of_hz(g_l6.catches[1].hz) == 44U); - if (match == 0U || g_verbose != 0) { - printf(" %s leg-6 fingerprint (seed %u, soft check, never a" - " failure): heard %u PROBEs %u snapped %u epoch_drift %u" - " locked_out %u (leg 6: 5 5 3 2 0); catch 1 (%u,%u) ch %u," - " PROBE 1 ch %u, catch 2 (%u,%u) ch %u (leg 6: (13,17) ch 9," - " ch 44, (14,40) ch 44)\n", - (match != 0U) ? "[MATCH]" : "[NOTE] no", (unsigned)g_l6_seed, - g_l6.ncatch, g_l6.tx_done, - g_l6.dec[SX1276_FHSS_SNAP_DEC_SNAPPED], - g_l6.dec[SX1276_FHSS_SNAP_DEC_REJECTED_EPOCH_DRIFT], - g_l6.dec[SX1276_FHSS_SNAP_DEC_REJECTED_LOCKED_OUT], - sx1276_fhss_clock_epoch_of(g_l6.catches[0].abs), - (unsigned)sx1276_fhss_clock_hop_of(g_l6.catches[0].abs), - chan_of_hz(g_l6.catches[0].hz), chan_of_hz(g_l6.tx_hz[0]), - sx1276_fhss_clock_epoch_of(g_l6.catches[1].abs), - (unsigned)sx1276_fhss_clock_hop_of(g_l6.catches[1].abs), - chan_of_hz(g_l6.catches[1].hz)); + for (uint32_t i = 0U; i < g_os.sent; ++i) { + if (g_l6.lock_us != 0U && g_os.log[i].key_us >= g_l6.lock_us + 1000000ULL) { + ++late_sent; + late_heard += g_os.log[i].heard; } } + CHECK(g_l6.lock_us != 0U && g_scan_mirror == SX1276_RX_SCAN_STATE_LOCKED && + g_sc.demotions == 0U && g_sc.scan_fail_urc == g_l6.fails_before_lock, + "[%s] after the LOCK: scan state %u (expect LOCKED), demotions %u," + " FAILs %u after it (expect 0 0)", g_sc.name, (unsigned)g_scan_mirror, + g_sc.demotions, g_sc.scan_fail_urc - g_l6.fails_before_lock); + CHECK(g_l6.grid_checked > 1000U && g_l6.grid_off == 0U, + "[%s] off the originator's hop grid on %u of %u mid-slot passes after" + " the LOCK (clock slot, or the receiver's channel)", g_sc.name, + g_l6.grid_off, g_l6.grid_checked); + CHECK(after_n >= 100U && after_follow == after_n && after_aligned == after_n, + "[%s] frames heard after the LOCK: %u, tuned by the slot follower %u," + " ALIGNED %u (expect all)", g_sc.name, after_n, after_follow, + after_aligned); + CHECK(late_sent != 0U && late_heard * 100U >= late_sent * 95U, + "[%s] heard %u of the %u frames keyed from 1 s after the LOCK" + " (expect >= 95 %%)", g_sc.name, late_heard, late_sent); + CHECK(g_l6.tx_done >= 10U && g_l6.tx_on_grid == g_l6.tx_done, + "[%s] PROBEs %u, keyed in the originator's slot they claim %u" + " (expect all, >= 10)", g_sc.name, g_l6.tx_done, g_l6.tx_on_grid); + + /* A4 never out of RX, never re-armed by the daemon. */ + CHECK(g_l6.pass_not_rx == 0U && g_rxd.arms_idle == 0U && + g_rxd.arms_after_tx == 0U, + "[%s] receiver out of RX at the end of %u passes with no TX keyed;" + " the daemon's raw RXCONT re-armed it %u times idle, %u after a TX" + " (expect 0 0 0)", g_sc.name, g_l6.pass_not_rx, g_rxd.arms_idle, + g_rxd.arms_after_tx); if (g_verbose != 0) { - printf(" activation -> final FAIL %u ms; parked on ch %u (%s), daemon" - " re-armed it: %s later; stream from +%llu ms, %u frames sent," - " %u heard (%.1f%%)\n", + char lock_buf[32]; + if (g_l6.lock_us != 0U) { + snprintf(lock_buf, sizeof(lock_buf), "+%.2f s", + (double)(g_l6.lock_us - g_l6.start_us) / 1e6); + } else { + snprintf(lock_buf, sizeof(lock_buf), "never"); + } + printf(" activation -> final FAIL %u ms, then SCANNING on ch %u;" + " stream from +%llu ms, LOCK %s after it (after %u FAILs);" + " %u frames sent, %u heard; after the LOCK %u heard (%u of %u" + " keyed from 1 s after it), %u mid-slot passes checked on grid;" + " PROBEs %u\n", (unsigned)(g_fail_ms[3] - g_l6.act_ms), chan_of_hz(g_l6.final_hz), - (g_l6.final_mode == (uint8_t)SX1276_STATE_STANDBY) ? "STANDBY" : "?", - us_after(arm_buf, sizeof(arm_buf), g_rxd.first_idle_arm_us, - g_l6.final_pass_us), (unsigned long long)((g_l6.start_us - g_l6.act_us) / 1000ULL), - g_os.sent, heard, 100.0 * heard / (double)g_os.sent); - for (uint32_t i = 0U; i < g_l6.ncatch && i < L6_MAX_CATCH; ++i) { + lock_buf, g_l6.fails_before_lock, g_os.sent, g_l6.ncatch, after_n, + late_heard, late_sent, g_l6.grid_checked, g_l6.tx_done); + for (uint32_t i = 0U; i < g_l6.ncatch && i < 3U; ++i) { const l6_catch_t *k = &g_l6.catches[i]; - printf(" catch %u at +%.2f s: (%u,%u) ch %u, scheduler epoch %u ->" - " %s; then PROBE (%u,%u) ch %u\n", i + 1U, - (double)(k->t_us - g_l6.act_us) / 1e6, + printf(" catch %u at +%.2f s: (%u,%u) ch %u -> %s, tuned by ctx %u\n", + i + 1U, (double)(k->t_us - g_l6.act_us) / 1e6, sx1276_fhss_clock_epoch_of(k->abs), (unsigned)sx1276_fhss_clock_hop_of(k->abs), chan_of_hz(k->hz), - k->local_epoch, dec_name(k->dec), - (i < g_l6.tx_done) ? sx1276_fhss_clock_epoch_of(g_l6.tx_abs[i]) : 0U, - (i < g_l6.tx_done) ? (unsigned)sx1276_fhss_clock_hop_of(g_l6.tx_abs[i]) : 0U, - (i < g_l6.tx_done) ? chan_of_hz(g_l6.tx_hz[i]) : 0U); + dec_name(k->dec), k->tuner); } printf(" daemon: probes=%u raw arms idle=%u after-TX=%u\n", g_rxd.probes, g_rxd.arms_idle, g_rxd.arms_after_tx); } } +#define SILENT_MS 305000U /* ten 30 s attempts */ + +static void test_silent_follower(void) { + const l6_cfg_t c = { + .seed = g_l6_seed, .ppm = 0, .period_ms = 500U, + .stream_at_activation = 0U, .start_abs = 0U, + .run_after_start_ms = 0U, .after_lock_ms = 0U, .cap_ms = 0U, + .silent_ms = SILENT_MS, + .watchdog = 0U, .stop_at_lock = 0U, + }; + const uint32_t expect_fails = SILENT_MS / SX1276_RX_SCAN_REDESIGN_MS; + uint32_t bad_sub = 0U, bad_gap = 0U, seen = 0U; + + l6_run("silent follower: report and keep scanning", &c); + print_scenario(); + check_structure(); + + for (uint32_t i = 0U; i < g_sc.scan_fail_urc && i < FAIL_LOG; ++i) { + /* The warm bit is sticky for the boot (sx1276_rx_scan_reset keeps + * it), so it is set here when an earlier run in this process + * LOCKed: compare without it, and require it unchanged. */ + const uint8_t warm = (uint8_t)(1U << SX1276_RX_SCAN_FAIL_SUB_BIT_WARM); + const uint8_t want = (i < L6_ATTEMPTS - 1U) + ? (uint8_t)(0x20U | i) + : (uint8_t)(0xA0U | (L6_ATTEMPTS - 1U)); + bad_sub += (uint32_t)((g_fail_sub[i] & (uint8_t)~warm) != want || + (g_fail_sub[i] & warm) != (g_fail_sub[0] & warm)); + if (i != 0U && + ((uint32_t)(g_fail_ms[i] - g_fail_ms[i - 1U]) < SX1276_RX_SCAN_REDESIGN_MS || + (uint32_t)(g_fail_ms[i] - g_fail_ms[i - 1U]) >= + SX1276_RX_SCAN_REDESIGN_MS + 100U)) { + ++bad_gap; + } + } + for (uint8_t i = 0U; i < SX1276_FHSS_CHANNEL_COUNT; ++i) { + seen += (uint32_t)((g_sc.walker_seen >> i) & 1ULL); + } + CHECK(g_sc.scan_fail_urc == expect_fails && bad_sub == 0U && bad_gap == 0U && + g_sc.fail_final == expect_fails - (L6_ATTEMPTS - 1U) && + g_sc.final_restarts == g_sc.fail_final && g_sc.tick_restarts == 0U, + "[%s] FAILs %u (expect %u, one per 30 s: %u off the 20 21 22 a3 a3 ..." + " sequence, %u not 30 s after the one before), final %u, each a" + " restart into SCANNING %u, restarts on a tick %u (expect 0)", + g_sc.name, g_sc.scan_fail_urc, expect_fails, bad_sub, bad_gap, + g_sc.fail_final, g_sc.final_restarts, g_sc.tick_restarts); + CHECK(seen == SX1276_FHSS_CHANNEL_COUNT, + "[%s] the walk covered %u of %u channels in %u s (d2: it must continue" + " across the restarts)", g_sc.name, seen, + (unsigned)SX1276_FHSS_CHANNEL_COUNT, SILENT_MS / 1000U); + CHECK(g_l6.pass_not_rx == 0U && g_rxd.arms_idle == 0U && + g_scan_mirror == SX1276_RX_SCAN_STATE_SCANNING, + "[%s] receiver out of RX at the end of %u passes, re-armed by the" + " daemon %u times, scan state %u at the end (expect 0 0 SCANNING)", + g_sc.name, g_l6.pass_not_rx, g_rxd.arms_idle, (unsigned)g_scan_mirror); + if (g_verbose != 0) { + printf(" FAIL sub-bytes:"); + for (uint32_t i = 0U; i < g_sc.scan_fail_urc && i < FAIL_LOG; ++i) { + printf(" %02x", g_fail_sub[i]); + } + printf("; walker channels %u/%u\n", seen, (unsigned)SX1276_FHSS_CHANNEL_COUNT); + } +} + /* ------------------------------------------------------------------ */ /* Tractor role: the originator in scan FAILED */ /* ------------------------------------------------------------------ */ @@ -965,10 +948,10 @@ static void check_tr_silent_phase(void) { g_tr.adoptions0, g_tr.auth0); } -static void test_tractor_failed_adoption(void) { +static void test_tractor_failed_recovers(void) { tr_cfg_t c = { .seed = TR_SEED, .stalls = 0U, .watchdog = 0U }; - /* T1 + T2: the control. */ + /* T1 + T2: the control -- FAILED held while the tractor streams. */ tr_run("tractor in scan FAILED, no stalls (control)", &c); print_scenario(); check_structure(); @@ -985,6 +968,12 @@ static void test_tractor_failed_adoption(void) { g_sc.auth_passes - g_tr.auth_passes0, g_sc.passes - g_tr.passes0, g_sc.follow_own - g_tr.follow_own0, g_sc.sent - g_tr.sent0, g_sc.covered_clean, g_sc.covered_clean_recv); + CHECK(g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED && + g_sc.tick_restarts == 0U && g_sc.final_restarts == 0U, + "[%s] control: scan state %u at the end (expect FAILED, held under" + " authority), restarts out of FAILED %u on a tick, %u at the final" + " FAIL (expect 0 0)", g_sc.name, (unsigned)g_scan_mirror, + g_sc.tick_restarts, g_sc.final_restarts); if (g_verbose != 0) { printf(" phase: cmds sent=%u recv=%u, authority on %u/%u passes\n", g_sc.sent - g_tr.sent0, g_sc.recv - g_tr.recv0, @@ -997,34 +986,32 @@ static void test_tractor_failed_adoption(void) { print_scenario(); check_structure(); check_tr_silent_phase(); - CHECK(g_tr.adopt_us != 0U && g_sc.adoptions == 1U && - sx1276_rx_grid_adopted() != 0U && g_sc.locks == 0U && - g_sc.demotions == 0U && g_sc.dem_reset == 0U && - g_sc.first_anchor == 1U && g_tr.renumbers == 0U && - g_scan_mirror == SX1276_RX_SCAN_STATE_FAILED, - "[%s] adoption in FAILED: adopted %s into the phase, adoptions %u," - " still adopted %u, locks %u demotions %u clk_demotion_reset %u," - " tx_first_anchor %u, own grid renumbered %u times, scan state %u" - " (expect adopted, 1, 1, 0 0 0, 1, 0, FAILED)", g_sc.name, - (g_tr.adopt_us != 0U) ? "yes" : "never", g_sc.adoptions, - sx1276_rx_grid_adopted(), g_sc.locks, g_sc.demotions, g_sc.dem_reset, - g_sc.first_anchor, g_tr.renumbers, (unsigned)g_scan_mirror); - CHECK(g_tr.adopt_us != 0U && g_sc.passes > g_tr.passes_at_adopt && - g_sc.auth_passes == g_tr.auth_at_adopt && - g_sc.follow_own == g_tr.follow_own_at_adopt && - g_sc.follow_locked == 0U && - (g_sc.recv - g_tr.recv_at_adopt) * 2U < g_sc.sent - g_tr.sent_at_adopt, - "[%s] after the adoption: authority on %u of %u passes, self-follow" - " retunes %u, LOCKED follow %u, cmds received %u of %u (expect 0, 0," - " 0, fewer than half)", g_sc.name, + CHECK(g_sc.tick_restarts >= 1U && g_sc.walker_after_decay_viol == 0U, + "[%s] restarts out of FAILED on a tick %u (expect >= 1: the first" + " decay of authority), walker later than one dwell after a decay" + " %u times (expect 0)", g_sc.name, g_sc.tick_restarts, + g_sc.walker_after_decay_viol); + CHECK(g_tr.adopt_us != 0U && g_sc.locks >= 1U && g_sc.demotions >= 1U && + g_sc.dem_reset >= 1U, + "[%s] a command heard in a decay window: adopted %s, locks %u," + " demotions %u, adopted clock reset at a demotion %u (expect yes," + " >= 1 each)", g_sc.name, (g_tr.adopt_us != 0U) ? "yes" : "never", + g_sc.locks, g_sc.demotions, g_sc.dem_reset); + CHECK(g_tr.adopt_us != 0U && g_sc.auth_passes > g_tr.auth_at_adopt && + g_sc.follow_own > g_tr.follow_own_at_adopt && + (g_sc.recv - g_tr.recv_at_adopt) * 2U >= g_sc.sent - g_tr.sent_at_adopt, + "[%s] after the first adoption: authority on %u of %u passes," + " self-follow retunes %u, cmds received %u of %u (expect > 0, > 0," + " at least half)", g_sc.name, g_sc.auth_passes - g_tr.auth_at_adopt, g_sc.passes - g_tr.passes_at_adopt, - g_sc.follow_own - g_tr.follow_own_at_adopt, g_sc.follow_locked, + g_sc.follow_own - g_tr.follow_own_at_adopt, g_sc.recv - g_tr.recv_at_adopt, g_sc.sent - g_tr.sent_at_adopt); if (g_verbose != 0) { printf(" phase: adopted at +%.1f s; before it cmds sent=%u recv=%u," " authority on %u/%u passes; after it cmds sent=%u recv=%u," - " authority on %u/%u passes\n", + " authority on %u/%u passes; own grid renumbered %u times (max" + " %u slots)\n", (g_tr.adopt_us != 0U) ? (double)(g_tr.adopt_us - g_tr.phase_us) / 1e6 : -1.0, g_tr.sent_at_adopt - g_tr.sent0, g_tr.recv_at_adopt - g_tr.recv0, @@ -1032,12 +1019,13 @@ static void test_tractor_failed_adoption(void) { g_tr.passes_at_adopt - g_tr.passes0, g_sc.sent - g_tr.sent_at_adopt, g_sc.recv - g_tr.recv_at_adopt, g_sc.auth_passes - g_tr.auth_at_adopt, - g_sc.passes - g_tr.passes_at_adopt); + g_sc.passes - g_tr.passes_at_adopt, g_tr.renumbers, + g_tr.renumber_max); } } /* ------------------------------------------------------------------ */ -/* Evaluation (-e): fix options, SIL only */ +/* Evaluation (-e): the memo's measures, SIL only */ /* ------------------------------------------------------------------ */ #define EV_MAX 1000U @@ -1089,7 +1077,8 @@ static void eval_e1(const char *label, uint32_t n, int32_t ppm, uint8_t watchdog const l6_cfg_t c = { .seed = 0xE1000001U + t * 7919U, .ppm = ppm, .period_ms = 500U, .stream_at_activation = 0U, .start_abs = 0U, - .run_after_start_ms = 300000U, .cap_ms = 0U, + .run_after_start_ms = 300000U, .after_lock_ms = 0U, .cap_ms = 0U, + .silent_ms = 0U, .watchdog = watchdog, .stop_at_lock = 0U, }; l6_run(label, &c); @@ -1124,8 +1113,9 @@ static void eval_e2(const char *label, uint32_t n, uint32_t period_ms) { const l6_cfg_t c = { .seed = 0xE2000001U + t * 104729U + period_ms, .ppm = 0, .period_ms = period_ms, .stream_at_activation = 1U, - .start_abs = 0U, .run_after_start_ms = 0U, .cap_ms = 300000U, - .watchdog = 0U, .stop_at_lock = 1U, + .start_abs = 0U, .run_after_start_ms = 0U, .after_lock_ms = 0U, + .cap_ms = 300000U, + .silent_ms = 0U, .watchdog = 0U, .stop_at_lock = 1U, }; l6_run(label, &c); if (g_l6.lock_us != 0U) { @@ -1218,7 +1208,7 @@ static void eval_e3(const char *label, uint32_t n, uint8_t stalls, uint8_t watch static void run_eval_tractor(void) { /* Commands from 5 s: the scan LOCKs on them and never fails out -- - * main's decay-window behaviour before FAILED (scenario 7's shape). */ + * the decay window before FAILED (scenario 7's shape). */ eval_e3("tractor SCANNING + stalls", 50U, 1U, 0U, 5000U); eval_e3("tractor FAILED, no stalls (control)", 20U, 0U, 0U, 0U); eval_e3("tractor FAILED + stalls", 50U, 1U, 0U, 0U); @@ -1255,20 +1245,23 @@ int main(int argc, char **argv) { run_eval(); return 0; } - test_leg6_characterisation(); - test_tractor_failed_adoption(); + test_leg6_recovers(); + test_silent_follower(); + test_tractor_failed_recovers(); if (g_failures != 0) { fprintf(stderr, "[FAIL] rx_scan_failed_follower: %d failure(s) -- the" - " characterised scan-FAILED behaviour changed: a firmware fix" - " for RS-12.21 (then replace these checks with its acceptance" - " checks) or an unrelated change to the scan constants, the" - " walker, the TX path or the shared sim (rx_reverse_delivery.c)" - " -- see which check failed\n", g_failures); + " RS-12.21 scan fix ((b) + L + d2) does not hold: a node without" + " originator authority stays in, or does not recover from, scan" + " FAILED (or an unrelated change to the scan constants, the" + " walker, the TX path or the shared sim, rx_reverse_delivery.c" + " -- see which check failed)\n", g_failures); return 1; } - printf("[PASS] rx_scan_failed_follower: KNOWN DEFECTS still present" - " (RS-12.21, firmware path) -- a follower in scan FAILED adopts the" - " grid but never LOCKs or hops (leg 6); an originator in scan FAILED" - " that adopts a grid never holds authority or self-follows again\n"); + printf("[PASS] rx_scan_failed_follower: RS-12.21 acceptance -- a follower" + " whose scan ran out reports, keeps scanning and LOCKs onto the" + " stream, then follows the hop grid (leg 6); a silent follower" + " reports every 30 s and walks all 50 channels; a tractor in scan" + " FAILED that loses authority scans again, and regains authority" + " and self-follow after an adoption\n"); return 0; } diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/tx_len_guard.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/tx_len_guard.c index ac635a7e..57ae4ac5 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/tx_len_guard.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/bench/host_proto/tx_len_guard.c @@ -76,6 +76,9 @@ bool sx1276_modes_to_tx(void) { return true; } uint8_t sx1276_modes_get_state(void) { return 0U; } void sx1276_rx_arm(uint8_t mode) { (void)mode; } void sx1276_rx_disarm(void) {} +/* RS-12.21 (2026-10-24): sx1276_tx_begin() refuses while the host has the + * radio parked; this test never parks (check-radio-park covers it). */ +bool sx1276_rx_parked(void) { return false; } void sx1276_set_frequency_hz(uint32_t freq) { (void)freq; } bool lora_pkt_hdr_pack(const lora_pkt_hdr_t *in, uint8_t *out) { (void)in; (void)out; return true; diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.bin b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.bin index b75877954a179d1684ee5141524c576661e10d47..36870cdacc3308b7b134310fae75c947964e1477 100644 GIT binary patch delta 7279 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

sY(&YMdo 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 A!2kdN diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.elf b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.elf index 954fb8dde7b8d1e2dded96abcad7d5bfe5259d0f..adf568d338e2237752ce44b0d12ee60482cb6700 100644 GIT binary patch delta 20246 zcmZvk349bq_Q$JxX2K){m?VIcdyW7J36KL6vS_kKF;S;On={dRrRXs zRaJM_gpEI#cK&2)aEMcs?Jn2U_LYoLvM_vXj0yC;k^W&0$gTZxQG%325?x=4OC-m4 z(380I$d>o}rJP^HtWTMk)wx(&ZkwGlY*aVS8rkxS?XVzjjI`9JFBQZIOQg3mE~l?} z#=XmGmN4s5B&rG}IZ&O0imuIKj(W&%N0N`;S94$rllz4%^3*t!r-zvQe&~#YnM8R^ z+#s^-VpaBI+{ z%Tpn*hWs9N0!xCdZ%~x~7iWdN1ET!jxau!tr&Y)C=H4(H1+$ygq{l5VOAk`gM$h3-iU*xJa3jCiS=q=g<>G3Ns#@Gk#SXh0%rMY6rCJ&uv8fI>mqe3 z)Zxf@Hk9?+oxDqw*K`STeO!>Ah_FI8a*RWcd{i<�s+m7Wwj!1(i)im4&G6=E(Re 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 delta 19558 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 diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.hex b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.hex index a31e7674..b7c55210 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.hex +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.hex @@ -4,13 +4,13 @@ :100020000000000000000000000000009D03000828 :1000300000000000000000009D030008DD0900082A :100040009D0300089D0300089D0300089D03000810 -:100050009D030008895700089D030008C157000848 +:100050009D030008E95800089D0300082159000885 :100060009D0300089D0300089D0300089D030008F0 :100070009D0300089D0300089D0300089D030008E0 :100080009D0300089D0300089D0300080000000078 :100090009D0300089D0300089D0300089D030008C0 -:1000A0009D0300089D0300089D030008D934000843 -:1000B0009D030008F93300089D0300089D03000814 +:1000A0009D0300089D0300089D0300082D350008EE +:1000B0009D0300084D3400089D0300089D030008BF :0400C0009D03000894 :1000C400A55AA55A5AA55AA500100E0000C201004F :0400D4008025000083 @@ -38,7 +38,7 @@ :10022800194352426346D34019437047002B11D1FA :10023800002A0FD1002900D1002802D00021C9438B :10024800080007B4024802A14018029003BDC04646 -:10025800A9FFFFFF03B4684601B5029805F0D0FA7C +:10025800A9FFFFFF03B4684601B5029805F080FBCB :10026800019B9E4602B00CBC7047C04610B50029E1 :1002780003D100F007F8203002E0080000F002F88F :1002880010BDC0461C2101231B04984201D3000C59 @@ -60,11 +60,11 @@ :10038800042172460A4202D1EFF3088001E0EFF33C :10039800098015E030BFFDE707B5FFF799FF07486B :1003A80000F0A0FA002102200B000A000191009140 -:1003B80000F0DEFA00F0D4FA30BFFDE7DE5900089D +:1003B80000F0DEFA00F0D4FA30BFFDE73E5B00083B :1003C80073B504000600051E02D046698569C46934 :1003D800FFF77EFF074800F085FA002133002A0066 :1003E80001200191009400F0C3FA00F0B9FA30BF7F -:1003F800FDE7C046DE5900087047000001221C4B8B +:1003F800FDE7C0463E5B00087047000001221C4B29 :1004080010B519680A431A60042219681142FCD011 :100418000323184A11689943023B0B430C211360CC :1004280013680B40042BFBD1FFF752FF1248FFF76C @@ -72,43 +72,43 @@ :1004480010D30021114B124A93420ED3114800F0E9 :1004580049FA1148FFF782FFFFF7CEFF00F01EF8B8 :1004680030BFFDE701CA01C3E9E702C3ECE7C046B4 -:10047800001002400C100240EC590008FC5900081A -:1004880078610008000000201400002030000020DF -:10049800581A0020085A0008135A0008F0B5834C6F -:1004A800A54400F00FF905F095FD00F047F900F0BC -:1004B800CBF9E120000302F00FFE05F00FF8041E4F -:1004C80000D1E6E003F062FF040004F028FF010019 -:1004D800200000F0C7FF00F0D1F9089000F0AEF955 -:1004E800002805D000F0AAF90100082001F050FC0E +:10047800001002400C1002404C5B00085C5B000856 +:10048800D86200080000002014000020300000207E +:10049800601A0020685B0008735B0008F0B5834CA5 +:1004A800A54400F00FF905F045FE00F047F900F00B +:1004B800CBF9E120000302F039FE05F0BFF8041E75 +:1004C80000D1E6E003F092FF040004F0D8FF010039 +:1004D800200000F0D1FF00F0D1F9089000F0AEF94B +:1004E800002805D000F0AAF90100082001F07AFCE4 :1004F800002601270696059604960396029600F0B4 -:10050800BDF9019002F046FB02F0F6FB02F092FC06 -:1005180002F044FF029B1843C3B2029302F05AFF51 +:10050800BDF9019002F070FB02F020FC02F0BCFC87 +:1005180002F06EFF029B1843C3B2029302F084FFFD :10052800039B1843C3B20393049B002B08D1029B7F -:10053800002B06D01900092001F02AFC00F080FFEA +:10053800002B06D01900092001F054FC00F08AFFB6 :100548000497002E08D1039B002B06D019000C201D -:1005580001F01EFC00F074FF3E00069B002B0ED13C -:1005680002F0C8FE00280BD002F0C4FEFF2800D914 -:10057800FF20C1B20B2001F00BFC00F061FF0697D1 +:1005580001F048FC00F07EFF3E00069B002B0ED108 +:1005680002F0F2FE00280BD002F0EEFEFF2800D9C0 +:10057800FF20C1B20B2001F035FC00F06BFF06979D :10058800059B002B12D1049B002B10D1089A019BCC -:100598009B1A474A93420AD902F094FE002806D1D2 -:1005A80004990A3001F0F4FB00F04AFF059702F0C5 -:1005B800E9FE002803D00021062001F0E9FB52A83B -:1005C80002F0A2FC051E6CD105F0BCF8040005F091 -:1005D800C5F80990200001F077FB20000AA904F073 -:1005E800DFFC0790002802D00AA801F071FB3C420A -:1005F8000DD004F0A5FD002809D120000CA903F0B6 -:10060800C5FF002802D00CA801F0D0FB3D002A004D -:100618000799099802F042F801F03EFE01F070FBDC -:10062800002823D004F08CFD00281FD105F0A2F883 -:1006380000281BD005F086F8050005F08FF80400A7 -:10064800280001F041FB3D4208D028000CA903F026 -:100658009DFF002802D00CA801F0A8FB200001F0A3 -:1006680027FE012C02D9601E01F016FE01F050FB96 -:10067800019804F0DBF8019804F0E0F8019804F020 -:100688008FF800F051FD0178019801F039FD30BF75 -:1006980035E70100032001F07BFB064800F022F952 -:1006A80013E752A800F046FF89E7C04674FDFFFF34 -:1006B800B70B0000225A0008014B18687047C04663 +:100598009B1A474A93420AD902F0BEFE002806D1A8 +:1005A80004990A3001F01EFC00F054FF059702F090 +:1005B80013FF002803D00021062001F013FC52A8E5 +:1005C80002F0CCFC051E6CD105F06CF9040005F0B6 +:1005D80075F90990200001F0A1FB20000AA904F098 +:1005E8008FFD0790002802D00AA801F09BFB3C422F +:1005F8000DD004F055FE002809D120000CA904F004 +:100608000FF8002802D00CA801F0FAFB3D002A00E0 +:100618000799099802F06CF801F068FE01F09AFB5E +:10062800002823D004F03CFE00281FD105F052F921 +:1006380000281BD005F036F9050005F03FF9040045 +:10064800280001F06BFB3D4208D028000CA903F0FC +:10065800E7FF002802D00CA801F0D2FB200001F02F +:1006680051FE012C02D9601E01F040FE01F07AFB18 +:10067800019804F031F9019804F036F9019804F072 +:10068800DFF800F053FD0178019801F063FD30BFF9 +:1006980035E70100032001F0A5FB064800F022F928 +:1006A80013E752A800F050FF89E7C04674FDFFFF2A +:1006B800B70B0000825B0008014B18687047C04602 :1006C8000400002010B513480378002B20D11249EC :1006D80003330A68940012D4013B54000FD4023348 :1006E800D4000CD40233540109D4013B140106D4BC @@ -164,31 +164,31 @@ :100A08001A79090211439A7912040A43D979090615 :100A1800114370470023064A1901515C814205D0F1 :100A28000133192BF8D10120404270471800FCE728 -:100A3800345A0008030010B50220012903D11878A0 -:100A480004F08CFC002010BD10B500F06DFB041EF6 +:100A3800945B0008030010B50220012903D118783F +:100A480004F03CFD002010BD10B500F06FFB041E43 :100A580007D0007800F056FA1422617DA25600F003 :100A68006FFA10BDF0B51A4B8DB01A4C1A4D0193A0 :100A780023786778AE1C2B70082200216F70300035 -:100A880004F098FF21003A003000083104F09AFF82 +:100A880005F048F821003A003000083105F04AF82E :100A9800019B10340A359C42EAD100240021282207 -:100AA80002A804F087FF02ABFF221C70002320007D +:100AA80002A805F037F802ABFF221C7000232000D3 :100AB8000492059300F030FA02AA084B0690100041 :100AC800938200F0A5FA064B1C700DB0F0BDC0462D -:100AD800C45B0008345A00083900002002110000E5 +:100AD800245D0008945B0008390000200211000022 :100AE800380000200023F0B593B007000D00160071 :100AF8000493059306930793FFF78CFF0124019055 :100B0800002800DAF6E0A44B02019B18DA780534D5 :100B18000292D20700D5EDE05B78043CB34200D0E6 :100B2800E8E0002D00D1E5E03200290004A804F037 -:100B380049FF781E13281CD8FFF7CAFA0A44444A0A +:100B3800F9FF781E13281CD8FFF7CAFA0A44444A5A :100B480044448244445156646A441B72771B7D882E :100B580004AB1B78012B31D8022304AA1370FFF7CA :100B680073FF002805D004AB1B78834201D904AB7E :100B780018700A25019B32005D43884B06A8ED18C2 -:100B8800290004F01FFF3200280004A904F01AFF0E +:100B8800290004F0CFFF3200280004A904F0CAFFAE :100B9800814B0393019B1F01039BDB195B68002BAF :100BA80000D1E1E0310028009847041E00D1DBE0C5 -:100BB8003200280006A904F005FF9BE0112BCED9CE +:100BB8003200280006A904F0B5FF9BE0112BCED91E :100BC8001123CAE704AB1B78012BD2D9032491E087 :100BD80004AB1B787833DBB2782BCAD9F6E704ABC1 :100BE8001B78072BC5D9F1E79622049B9201934203 @@ -198,7 +198,7 @@ :100C280004AB1B885F4A0A3BF6E704AB1B78013B21 :100C3800092B9ED9CAE704AB1B885B4A323BEBE71A :100C480004A8FFF7D1FE084393D1BFE704AB1B7894 -:100C5800022BBBD808AF28220021380004F0AAFED6 +:100C5800022BBBD808AF28220021380004F05AFF25 :100C680004AB1878387000F057F90C900720FFF79C :100C7800D1FE002800DA75E00A2460434B4D0230AB :100C88004019FFF7B1FE0A900B911520FFF7C2FE3D @@ -212,30 +212,30 @@ :100D0800F3D1FFF7A1FE07000120FFF783FE041EC1 :100D180028DB002F26D00A2213004343EB189978CA :100D28000B1CB94200D93B1C6243D8B21700AA1861 -:100D3800937004F013FB1E4B03931B78002B00D019 +:100D3800937004F0C3FB1E4B03931B78002B00D069 :100D480017E7154B24011A19023708345278E118AD -:100D5800E81904F021FE002800D10AE70123039ACC +:100D5800E81904F0D1FE002800D10AE70123039A1C :100D6800137006E70424C5E7114C2378002B0AD139 -:100D7800039B083732002800D91904F00DFE00281B +:100D7800039B083732002800D91904F0BDFE00286B :100D880001D0012323700223029C029A1C401A42BC -:100D9800B0D00524AEE7C046345A00083B00002016 +:100D9800B0D00524AEE7C046945B00083B000020B5 :100DA800561300007E1300009E0700003900002043 :100DB80038000020F7B51E0007000C000192FFF76D :100DC80029FE03000120002B13DB0018002C10D093 :100DD800002E0ED00C4A190152185578019A9542E6 :100DE80007D8822F06D10125084B1B7823700020D5 :100DF8003570FEBD0A215943054B2A002000C91849 -:100E080004F0E0FDF3E7C046345A0008380000203B +:100E080004F090FEF3E7C046945B00083800002029 :100E18003B000020F8B503000C00002841D0081E54 :100E28003FD01F00FF37797800200D00103595421C :100E380037D82170BA781E0062701A00FE32042179 :100E4800525EFC36A27092B2120AE270B268110ABF :100E580022716171110C120EA171E2717A788242CD -:100E680004D020001900083004F0ACFD7D78797DAD +:100E680004D020001900083004F05CFE7D78797DFC :100E78002B000833E018002914D0012202707A7A76 :100E880004334270BA7AE41882707A7CC27033698B :100E98001A0A237062701A0C1B0EA270E3702800E5 -:100EA8001030F8BD082204F085FDF8E703001000B3 +:100EA8001030F8BD082204F035FEF8E70300100002 :100EB800012B08D10A00803AD2B2012A01D985292A :100EC8000ED118000CE0002A0AD00020402B07D8C9 :100ED800252B06D8092B0BD8053098428041404273 @@ -243,8 +243,8 @@ :100EF800002001221D3BDBB29A424041C0B2EFE71D :100F08000128220403001120002B04D0013B0020FB :100F1800012B00D81E30704703000020022B02D896 -:100F2800014A9B0098587047D05B000803000420D2 -:100F3800092B01D8014AD05C7047C046C45B000841 +:100F2800014A9B0098587047305D00080300042070 +:100F3800092B01D8014AD05C7047C046245D0008DF :100F48000023062A01DD063A53B2C01A884200DDA2 :100F58000800012800DC0020C0B27047F8B5050081 :100F68000E00092400281AD00478022C41D814272E @@ -259,1309 +259,1331 @@ :100FF800E8E70124D3E70524D1E70624CFE7032453 :10100800CDE70724CBE7C046702FFCFFE05EF8FF72 :1010180070B50B4D0400010028222800002C0AD0CE -:1010280004F0D0FC28220021064804F0C3FC00226A -:10103800054B1A7070BD04F0BDFC2C70F2E7C04679 +:1010280004F080FD28220021064804F073FD002208 +:10103800054B1A7070BD04F06DFD2C70F2E7C046C8 :1010480060010020380100203301002070B5050040 :10105800FFF784FF2822041E074807D0002104F068 -:10106800A9FC0023054A2000137070BD290004F074 -:10107800A9FC0123F6E7C04638010020330100200F -:1010880073B5244D08202B78002B24D0224C234EF6 -:1010980028223100200004F095FC03F026F923787B -:1010A800012B19D1226A636AA069E16900240094BE -:1010B80002F0BEFC0522FA21072004F00BFA1848BA -:1010C80002F076FA03F008FC28220021300004F030 -:1010D80071FC0020287076BD022B0DD102F09EFC19 -:1010E80003F0FAFBFA2107200522490004F0F2F97F -:1010F8000C4802F05DFAE7E7002BE5D102F08EFC20 -:1011080003F0EAFB07200522FA2104F0E3F904487A -:10111800EFE7C046330100206001002038010020BD -:10112800801A0600F07E0E0000487047600100201B -:1011380070B5050004A80678059C181E0BD0201E63 -:1011480009D00020032E06D9D8701D7059709A70E6 -:1011580004230130237070BDF0B5050005A8077899 -:10116800069C081E12D0181E10D0201E0ED000207B -:10117800082A0BD8961CF6B2BE4207D81D705A70C2 -:101188000233C5B2AA4202D801202670F0BD0D5C18 -:101198001D540130F5E7030010B5421850789478D3 -:1011A8005B5C2404000220431843D3781B061843D1 -:1011B80010BD0B0A017043700B0C090E8370C170CF -:1011C80070470000F0B5C1880400C7B0012908D8ED -:1011D80002230278417880880093013301F0E6FE0B -:1011E80053E00C25037A05AE3370427A02ABFF3523 -:1011F800ED18931C6A70994205D00278417880886E -:1012080000930323EAE7002A06D001000D2002AB71 -:101218000A31C01804F0D6FB1D4F3B78002B0BD0C9 -:10122800A3883D780393032D1AD900232278617887 -:10123800039800930733D1E703F082FF0028EFD12A -:10124800687803F085FF0028EAD1300003F0CEFC6F -:10125800031E1AD122786178A08800930833BDE76D -:1012680003220C4B0C4C1B783100EB181340023254 -:10127800FF3253430222E4182000FF3204F0A2FB9D -:10128800039BFE340135A3803D7047B0F0BDC046D6 -:1012980088010020890100208A01002010B5024839 -:1012A80001F072FE10BDC046125F00080F2307B59B -:1012B800184003003733092800D8073B6A461371E2 -:1012C800002301A8537101F05FFE07BD30B504008B -:1012D80089B002AD18220021280004F06BFB238896 -:1012E800616803A82B80FFF764FFA16804A8FFF7D3 -:1012F80060FFE16805A8FFF75CFF216906A8FFF712 -:1013080058FFFFF7BBFA010007A8FFF752FF0022BA -:10131800182311000093F1202B0001F01FFE09B0E3 -:1013280030BD0000F0B5002485B0050001AEA04234 -:1013380006D1124801F028FE05B0F0BD0A2C0FD0E6 -:1013480028000A21FEF754FF3031A15528000A2150 -:10135800FEF7C8FE2F0001340500092FEED8E4B2CD -:101368006A46013CE4B2335D68461370002353704B -:1013780001F00AFE002CF3D1DEE7C046E55D000867 -:1013880010B50C0001F000FE034801F0FDFD20003F -:10139800FFF7C8FF10BDC046CE5C0008F0B50027B7 -:1013A800BEB2B14204D9C45D202C11D0092C0FD093 -:1013B8004C1E0419B1420DD80024C719135D301909 -:1013C80080B2002B0FD1081A43425841C0B2F0BD79 -:1013D8000137E5E72578202D01D0092DECD1013919 -:1013E80089B2013CE6E781420FD93D5D28006138AA -:1013F800192801D8203DEDB218006138192801D804 -:10140800203BDBB20134AB42D8D00020DFE700003C -:1014180070B5094C200001F0B7FD084D280001F017 -:10142800B3FD200001F0B0FD280001F0ADFD200063 -:1014380001F0AAFD70BDC046E55D0008DC5B000850 -:1014480000B5B7B0D02102A800F0B4FF002806D03C -:10145800002200901100C12002AB01F07FFD37B0DF -:1014680000BD0000F0B50025274B284F284E1D7001 -:10147800284B8BB031701D703870FFF7F3FA00F00D -:1014880065FE2548FFF72EFAFFF74AF93B7805ACC9 -:10149800637033782070A3700123E370DB18237125 -:1014A800FFF7CCF9062360712A00F0200093290089 -:1014B800230001F053FD2000FFF76EFAA84205D083 -:1014C8001648FFF70FFA2000FFF700FF1448FFF750 -:1014D80009FA144B002203937F230493773B1100EE -:1014E800F220009303AB01F039FDFFF7A7F90028BC -:1014F80007D10D48FFF7F6F90C48FFF7F3F90BB0E1 -:10150800F0BD0B48F6E7C046890100209B05002086 -:101518009A05002088010020DE5B0008EC5B0008CB -:10152800FB5B0008010102010A5C00082A5C000854 -:101538001A5C0008F0B50400BDB0002800D197E09F -:10154800BE48FFF7CFF92278012A0AD0BC48FFF736 -:10155800C9F9012322786178A088009301F026FD5B -:1015680086E06178202900D1EAE010D8032906D85E -:10157800481E02282FD8FEF7ABFD3B7D95001029A9 -:1015880000D192E0A088022301220091E6E73129E8 -:1015980000D1A5E00BD8212900D115E13029F1D1DE -:1015A800E388012B00D18DE0A0880092D7E0402984 -:1015B80000D17EE04129E5D1D02108A800F0FAFE4B -:1015C8000300A088002B00D076E0D0332278617821 -:1015D8000093CD3BC2E79B48FFF784F9E0882300DE -:1015E800A278A18800900020083301F0B7FC3FE002 -:1015F8009548FFF777F9042001F0DEFE934B1800B9 -:101608000493FEF75DFE0500914B2F1C1800EDB208 -:101618000593FEF755FE282D00D92827031CC0B2D4 -:10162800FFB2102800D91023DAB20392894A08AD14 -:1016380000260892AB717F233A006F710C3704992A -:101648002E71EE71FFB20BA80A9304F0BBF9039A4E -:101658000599E81904F0B6F9039B8120DF19A188E0 -:101668002B003200009701F079FC082001F0A4FE5D -:101678003DB0F0BD784B08A81968784B1E68784BC8 -:101688001D68FFF796FD310009A8FFF792FD2900B4 -:101698000AA8FFF78EFD0C23A18800220093822060 -:1016A80008ABA2E7FFF75CF9E2E72000FFF78AFD45 -:1016B800DEE700F04BFDDBE7010000930022C120CC -:1016C80008AB92E7207A04AB187403F0D7FD04AB9B -:1016D80058740223A18800220093B02008AB84E745 -:1016E800E388022B04D00223A0880093013335E756 -:1016F8000022617A207AFFF7D9FB237A002805D1E6 -:1017080022786178A0880093082327E71800617A77 -:1017180003F0C6FD237A012B0AD1607A02F0C4FDDA -:101728000722637A1340053B012B01D802F026FFFC -:1017380002222300A188B12000920833002254E736 -:101748000A2704ABFF1800233B70E0880128CAD998 -:10175800637A267A9D1CA84203D0A08800950323AB -:10176800FCE621001A0030000A31FFF7BBF9051E1C -:1017780003D0031FDBB2012B09D83B00082230003D -:1017880008A9FFF717FB002801D000233B703A781F -:101798000B2704ABFF18042329000093300007AB84 -:1017A8000197FFF7C5FC0300A088002B05D104337F -:1017B800227861780093013BD0E63B78009307AB31 -:1017C80001000022A02010E70B2604ABF618002326 -:1017D8003370E388012B00D0E6E61221277A04A8AB -:1017E8000918330008223800FFF7E4FA07AD0100B2 -:1017F8000A2300280ED00093002208AB3800019578 -:10180800FFF796FC0300A088002BD0D02B7800931C -:1018180008ABD5E7122104A83278091800933800DC -:1018280008AB0195FFF798FC0300A088002B19D19D -:101838003378227802336178009390E7385C0008A7 -:10184800455C0008515C00085A5C0008675C0008A9 -:10185800815C0008010200005000F81F5400F81FC6 -:101868005800F81F2B78010000930022A12008AB34 -:10187800BBE6C04670B504000D00B4B0002806D021 -:10188800DE4AFFF78BFD002803D0FFF707FD34B0D1 -:1018980070BD29002000DA4AFFF780FD002812D029 -:1018A800D84801F071FBD84C200001F06DFBFFF720 -:1018B800AFFD200001F068FBD44801F065FBD44877 -:1018C80001F062FBE1E729002000D24AFFF766FD3C -:1018D800002830D0D048FFF705F8102001F06CFD43 -:1018E800CE4801F051FBC74801F04EFBCC4801F04F -:1018F8004BFBFFF78DFDCB4801F046FBC34801F0D9 -:1019080043FBC94801F040FB0120FFF70BFDC74826 -:1019180001F03AFB0220FFF705FDC54801F034FB52 -:101928001C247F252800E040043CC0B2FFF7BEFC21 -:10193800231DF7D1C3E729002000BE4AFFF72EFD7B -:10194800061E27D0D021684600F034FDD02801D0EB -:10195800B9484FE1B94801F017FBB94BB9481B78B2 -:10196800002B00D0B84801F00FFBB84801F00CFB81 -:10197800B74B1C782009FFF799FC2000FFF796FC6D -:10198800B44801F001FB40216846FFF704FCFFF76B -:10199800C9FC94E729002000AF4AFFF7FFFC0028A4 -:1019A80000D10EE1D021684600F004FDD028CFD147 -:1019B80031006846FFF7EFFBA84B01001800FFF75E -:1019C800DFFC914C200001F0DFFA04216846FFF7A4 -:1019D800E2FBA34B01001800FFF7D2FC200001F046 -:1019E800D3FA08216846FFF7D6FB9E4B0100180082 -:1019F800FFF7C6FC864D280001F0C6FA0C216846A0 -:101A0800FFF7C9FB984B01001800FFF7B9FC20004D -:101A180001F0BAFA10216846FFF7BDFB934B0100AD -:101A28001800FFF7ADFC200001F0AEFA142168465B -:101A3800FFF7B1FB8E4B01001800FFF7A1FC200057 -:101A480001F0A2FA18216846FFF7A5FB894B0100AF -:101A58001800FFF795FC280001F096FA4421684623 -:101A6800FFF799FB844B01001800FFF789FC200061 -:101A780001F08AFA48216846FFF78DFB7F4B010089 -:101A88001800FFF77DFC200001F07EFA4C21684623 -:101A9800FFF781FB7A4B01001800FFF771FC280063 -:101AA80001F072FA50216846FFF775FB754B01008B -:101AB8001800FFF765FC200001F066FA542168461B -:101AC800FFF769FB704B01001800FFF759FC200075 -:101AD80001F05AFA58216846FFF75DFB6B4B01008D -:101AE8001800FFF74DFC200001F04EFA5C21684613 -:101AF800FFF751FB664B01001800FFF741FC280077 -:101B080001F042FA1C216846FFF745FB614B0100D2 -:101B18001800FFF735FC200001F036FA202168464E -:101B2800FFF739FB5C4B01001800FFF729FC200088 -:101B380001F02AFA24216846FFF72DFB574B0100D4 -:101B48001800FFF71DFC200001F01EFA2821684646 -:101B5800FFF721FB524B01001800FFF711FC28008A -:101B680001F012FA30216846FFF715FB4D4B0100D2 -:101B78001800FFF705FC200001F006FA342168463A -:101B8800FFF709FB484B01001800FFF7F9FB20009D -:101B980001F0FAF92C216846FFF7FDFA434B0100E2 -:101BA8001800FFF7EDFB200001F0EEF94021684630 -:101BB800FFF7F1FA3E4B01001800FFF7E1FB2800A0 -:101BC8007EE6290020003B4AFFF7E8FB002806D103 -:101BD80029002000384AFFF7E1FB002801D03748E8 -:101BE8006EE629002000364AFFF7D8FB002800D10E -:101BF800AEE6344801F0C8F949E6C0468A5C0008F8 -:101C08008D5C0008675C0008915C0008815C000836 -:101C1800C15E0008935C00089B5C0008AB5C000890 -:101C2800AF5C0008B95C0008BF5C0008C75C00082E -:101C3800D05C0008D85C0008E25C0008EA5C000898 -:101C48009B050020855C0008A05D0008F15C000889 -:101C58009A050020FD5C0008055D00080E5D00087F -:101C68001B5D0008275D0008375D0008455D00081A -:101C7800515D00085D5D0008695D0008775D00083A -:101C8800865D0008955D0008A35D0008B25D000848 -:101C9800C75D0008DC5D0008E75D0008F25D00082C -:101CA800FD5D0008085E0008145E0008205E00085C -:101CB8002E5E0008345E00083C5E0008455E0008A1 -:101CC800785E00087F5E000810B500F0FDFA10BDD0 -:101CD8001FB5041E12D0038841680A206A4668446A -:101CE8001381FFF766FA6A46A37890209373072357 -:101CF80000220093110002AB01F030F91FBD000073 -:101D0800024B1878431E9841C0B2704788010020E2 -:101D1800F0B5C5B01B4E3378002B01D145B0F0BDEE -:101D280003F00EFA0028F9D18222174F52003B78AF -:101D3800164D5343EB18FF33587803F009FA00287F -:101D4800ECD1822302223C785B006343ED18290022 -:101D5800FF3203A803F036FE032301341C40337816 -:101D6800FE35013B03A8AD883C70337002F03EFF9E -:101D78000028D3D10090082301221021280001F067 -:101D880015F9C7E788010020890100208A01002091 -:101D98007FB56B4618715971002301A8029303930C -:101DA80004930593FFF792FA07B000BD00B5C7B0DA -:101DB80000280DD0882202A95200FFF72BF800282E -:101DC80006D0002200901100912002AB01F0C6F865 -:101DD80047B000BD030010B500282BD0081E29D03D -:101DE800012008701A784A705A788A709A78CA70EE -:101DF800DA780A715A68140A4A718C71140C120E36 -:101E0800CC710A729A68140A4A728C72140C120EF7 -:101E1800CC720A73DA68140A4A738C73140C120EA3 -:101E28000A74CC731B691A0A4B748A741A0C1B0E39 -:101E3800CA740B7510BD0000002804D0FF2803D019 -:101E4800431E052B01D970470720044A4000835AD6 -:101E580003498B42F7D001338352F4E79E050020F3 -:101E6800FFFF000010B500280AD002000021054B32 -:101E780010321C8804800230198002339042F8D155 -:101E880010BDC0469E050020054A064913888B42AE -:101E980001D001331380002801D0034B187070471C -:101EA8009C050020FFFF000008000020F0B58BB063 -:101EB8000E00141E039003D1002528000BB0F0BDBE -:101EC800100004A9FFF786FF051EF5D067783800D3 -:101ED800FFF7B2FF002F02D1A07801F089FE6078E9 -:101EE800FFF7D2FF152332000093C320039904ABF8 -:101EF80001F034F8E1E700000021044A138811805A -:101F0800181CFF2B00D9FF20C0B270479C05002089 -:101F1800FF22024B18781A707047C046080000204C -:101F28000300F0B500286AD0081E68D001220A70A4 -:101F38005A784A709A788A70DA78CA701A790A7167 -:101F48005A794A7100228A71CA719A68100A0A720B -:101F58004872100C120E8872CA72DA68100A0A7374 -:101F68004873100C120E8873CA731A69100A0A741F -:101F78004874100C120ECA7414228874985C885421 -:101F88000132462AFAD14C1C985A8854000AA054A7 -:101F98000232AA2AF8D1985A8854000AA054023268 -:101FA800BA2AF8D11A00BA3210780A00BA32107078 -:101FB8001A00BB3210780A00BB321070BC331A7892 -:101FC8000B000C0008000826BC331A700C4D0D4B92 -:101FD800BD3402781202534032001FB25B009BB23C -:101FE800002F00DA6B40013AD2B2002AF5D1013055 -:101FF8008442EED101202370BE311B0A0B70F0BD64 -:1020080021100000FFFF0000F0B5384C0500A54482 -:102018000392022234ACFF32A3700F00228001F039 -:10202800B9FD0023E070322802D832331B1ADBB224 -:102038002371FFF76DFF0026039B6071A76039A825 -:1020480064AFE680E560236101F0E2FD380002F04C -:1020580095F87300FB58181E0AD0254A9818FA23D9 -:102068009B0498423FD2FA218900FEF73BF880B2E0 -:10207800872302AA5B009B1898530236642EE8D186 -:10208800B92040008018FFF7EDFEFFF735FFC123A8 -:1020980002AA5B009B1818708423164E02A9327896 -:1020A800FF335B181A70144B200003931B7863AF3F -:1020B80004A93B70FFF734FF041E0FD0BF233A7802 -:1020C800C4200093A9B204AB00F048FF0023039A90 -:1020D80013703378FF2B01D0013333702000A52310 -:1020E8009B009D44F0BD0548C2E7C0466CFDFFFF5C -:1020F800E7030000B805002009000020FFFF0000EA -:1021080070B50B4E0B0032680A490400821A8A42E5 -:1021180001D8002070BD084D01002868FFF774FF42 -:102128000028F6D02B68346001332B60F2E7C046F4 -:10213800B40500205FEA0000B00500200B0A01701A -:1021480043700B0C090E8370C170704700231B4A43 -:1021580010B513601A4A13601A4A13601A4A1360BA -:102168001A4A13601A4A13601A4A13601A4A13600B -:102178001A4A13601A4A13601A4A13601A4A1360FB -:102188001A4A13601A4A13601A4A13601A4A1360EB -:102198001A4A13601A4A13601A4A1360536093600C -:1021A800D36013615361184A1360184A1360184AC0 -:1021B8001360184A136000F00FFF10BD28060020B6 -:1021C80024060020200600201C06002018060020F7 -:1021D80014060020100600200C0600200806002027 -:1021E8000406002000060020FC050020F805002059 -:1021F800F4050020F0050020EC050020E80500208B -:10220800E4050020CC050020C8050020C4050020F6 -:10221800C0050020BC050020024A13680133136082 -:102228007047C04604060020024A13680133136051 -:102238007047C04600060020024A13680133136045 -:102248007047C046FC050020024A1368013313603A -:102258007047C04614060020024A13680133136011 -:102268007047C04618060020024A136801331360FD -:102278007047C04610060020024A136801331360F5 -:102288007047C0460C060020024A136801331360E9 -:102298007047C04608060020C143044A136818184E -:1022A800994201D20120404210607047F805002091 -:1022B800C143044A13681818994201D201204042C8 -:1022C80010607047F4050020C30703D50B4A136854 -:1022D80001331360830703D5094A13680133136078 -:1022E800430703D5074A136801331360000703D572 -:1022F800054A1368013313607047C046280600205A -:1023080024060020200600201C060020034A13682B -:10231800591C01D0013313607047C046F0050020F6 -:102328000B4B30B51C68844200D21860C543094C79 -:1023380023689D4209D31B182360002A04D0064B4A -:102348001A688A4200D2196030BD01235B42F3E764 -:10235800EC050020E8050020E4050020052806D843 -:10236800034A80008358591C01D0013383507047B9 -:10237800CC050020044A002800D1044A1368591CDF -:1023880001D0013313607047C8050020C405002040 -:10239800034A1368591C01D0013313607047C046C3 -:1023A800C0050020024B1A68824200D218607047AC -:1023B800BC050020F8B5041E00D13FE10020CF295C -:1023C80000D83BE1D0220100200003F0F3FA00F02E -:1023D8004FFF01002000FFF7B1FE00F04FFF231D63 -:1023E80001001800FFF7AAFE00F04EFF01002000D0 -:1023F8000830FFF7A3FE00F04DFF010020000C306D -:10240800FFF79CFE00F04CFF010020001030FFF7A2 -:1024180095FE00F04BFF010020001430FFF78EFE00 -:1024280000F04AFF010020001830FFF787FE200067 -:10243800834B1C301968FFF781FE2000814B203048 -:102448001968FFF77BFE20007F4B24301968FFF7DF -:1024580075FE20007D4B28301968FFF76FFE2000BD -:102468007B4B2C301968FFF769FE2000794B303020 -:102478001968FFF763FE2000774B34301968FFF7BF -:102488005DFE2000754B38301968FFF757FE2000B5 -:10249800734B3C301968FFF751FE01F0FFFE010055 -:1024A80020004030FFF74AFE00F00CFF010020003A -:1024B8004430FFF743FE00F00BFF010020004830D6 -:1024C800FFF73CFE00F00AFF010020004C30FFF748 -:1024D80035FE00F009FF010020005030FFF72EFE06 -:1024E80000F008FF010020005430FFF727FE00F03D -:1024F80007FF010020005830FFF720FE00F006FF1C -:10250800010020005C30FFF719FE00F005FF010014 -:1025180020006030FFF712FE00F004FF01002000E9 -:102528006430FFF70BFE00F003FF01002000683065 -:10253800FFF704FE00F002FF010020006C30FFF7F7 -:10254800FDFD00F001FF010020007030FFF7F6FDEF -:1025580000F000FF010020007430FFF7EFFD00F0ED -:10256800FFFE010020007830FFF7E8FD00F0FEFED6 -:10257800010020007C30FFF7E1FD00F0FDFE0100C6 -:1025880020008030FFF7DAFD2000364B84301968D0 -:10259800FFF7D4FD2000344B88301968FFF7CEFDD3 -:1025A8002000324B8C301968FFF7C8FD2000304BF3 -:1025B80090301968FFF7C2FD20002E4B943019683F -:1025C800FFF7BCFD20002C4B98301968FFF7B6FDCB -:1025D80020002A4B9C301968FFF7B0FD2000284BDB -:1025E800A0301968FFF7AAFD200025002700254B19 -:1025F800A4301968FFF7A2FD234EA835C03728007C -:1026080002CE0435FFF79AFDBD42F8D11F4B2800D2 -:102618001968FFF793FD20001D4BC4301968FFF7B8 -:102628008DFD20001B4BC8301968FFF787FD20007F -:10263800194BCC301968FFF781FDD020F8BDC04692 -:102648002806002024060020200600201C06002062 -:102658001806002014060020100600200C06002092 -:10266800080600200406002000060020FC050020C3 -:10267800F8050020F4050020F0050020EC050020F6 -:10268800E8050020E4050020CC050020C80500204E -:10269800C4050020C0050020BC0500200300501E12 -:1026A80082418A1800209A4200D2981A7047000086 -:1026B800024A1368034313607047C046300600207F -:1026C800F0B50022082504000A4E0B4893B299423F -:1026D80000D8F0BDA35C1B0258402B0007B2400095 -:1026E80080B2002F00DA7040013BDBB2002BF5D13D -:1026F8000132EBE721100000FFFF000080230349AF -:102708000A681A42FCD0024B186070471C48004007 -:102718002848004070B504004518AC4205D1402354 -:1027280004490A681A42FCD070BD2078FFF7E6FF1A -:102738000134F2E71C480040F0B5454C0D00A544B3 -:102748001900AAAB1E88012303AC2370E570330A75 -:102758002D0A257160700725A2706671A371002E7D -:1027680009D0002907D00B2002AB3200C01803F0B3 -:1027780029F97619B5B229002000FFF7A1FFAB1C93 -:1027880060550135000AADB26055002001249BB2A6 -:102798004426019322000300FF3603A9095C8C46F6 -:1027A80000292BD1A22189005B1802A95B18453B9F -:1027B800FF3B1A70B44246D0411C621C8BB2009396 -:1027C800230094B201220199009D0130A942E4D866 -:1027D800A22189005B1802A95B18453BFF3B00203A -:1027E8001A70FFF78BFF53A82100FFF793FF002013 -:1027F800FFF784FFA5239B009D44F0BDB44222D07F -:10280800A227411C89B2BF000091E71902A97F18CD -:102818006146453F0132651CFF3FD2B2ADB2397007 -:10282800FF2A0ED1A22189005B1802A95B18453B3B -:10283800FF3B1A70B54206D002342B00A4B2FE3A10 -:10284800C1E72C00BFE7034A136801331360D1E7DF -:102858006CFDFFFF94080020F0B56C4C6C4BA54450 -:102868000024220018886B4B019390426AD82300F9 -:102878009C22093B9BB25200934200D3ADE004A9CD -:102888008D7904AB5B792D021D43954200D3A4E0FA -:102898002B0009339BB29C4200D09EE0A11E89B256 -:1028A80004A8FFF70DFFA0239B00E41804ABE4186D -:1028B800594BE3181A78594B1202E418237813433A -:1028C800834200D089E004AB9B7804AA54AC1288F8 -:1028D800A37004AB2280DA781B79E5801B021343CE -:1028E800A380002D05D004AB2A00D91D56A803F0FB -:1028F80069F86578012D07D180204000FFF7D8FEE0 -:10290800474B1A7815431D7080208000FFF7D0FED2 -:10291800444A136801331360EFF3108672B6424DD0 -:102928002B78032B5BD9414A136801331360404A63 -:1029380013680133136086F310888020C000FFF706 -:10294800B7FE15E00199531C8D5C9BB2002D3BD15D -:102958002020FFF7ADFE374A136801331360364A6B -:1029680013680133136002232D4A11780B43137047 -:10297800A5239B009D44F0BD9042E9D0A026760097 -:10298800B342E5D2039B029E5B5C0132735439006B -:1029980092B24F1C6318FEB29BB2B542ECD8FF2D21 -:1029A80010D090420ED9A02149008B42D0D2A02449 -:1029B800A400591C1B1904AC1B19204C1B1900241A -:1029C8001C708BB21C0050E71A0004AEA3190293C6 -:1029D800019B00219B180393DBE78020B9E7A02225 -:1029E800174F52003B78174853432100C01802F094 -:1029F800E9FF03223B78013313403B702B78013306 -:102A0800DBB22B7086F31088B2E7C0466CFDFFFF7F -:102A1800060E0020080E00207FFDFFFF7EFDFFFF51 -:102A28003C080020700800209C0800209008002026 -:102A380098080020940800206C08002080FDFFFF03 -:102A48009E080020A008002070B5164D2E78002E94 -:102A580005D0002802D1144B2870188070BD124C84 -:102A68002388002809D1002B04D01030FFF720FE5E -:102A7800FFF7F2FE00232380F0E7A12252009342E1 -:102A88000AD94020FFF714FE0123084A2B70136867 -:102A9800268001331360E1E75A1C2280044AD0548F -:102AA800DCE7C046050E0020060E00209408002032 -:102AB800080E002070B503240223A0254600B44068 -:102AC800B340ED052A68134EA2431A432A60012534 -:102AD80085403268AA43104D32602A68A2431343E6 -:102AE8002B60072809D80F228000824081400B4CB8 -:102AF800236893431943216070BD0F240838C0B27E -:102B0800800084408140064A1368A343194311603A -:102B1800F2E7C046040000500800005020000050B2 -:102B2800240000500B4A10B513880B4C99B2248826 -:102B38000133DB05DB0DA34208D18020074A4001A1 -:102B4800136801331360FFF7B3FD10BD044C6054E4 -:102B58001380FAE73A0600203806002034060020E1 -:102B68003C06002010B50024074A1388A34204D865 -:102B780000231380054A137010BD054B185D0134FE -:102B8800FFF762FFA4B2EFE7A20D0020A10D00201D -:102B9800A40D0020F8B50F252026EFF3108472B697 -:102BA800404B1A68404B1B682A4229D0D30703D5EB -:102BB8003E490B6801330B60930703D53C490B680A -:102BC80001330B60530703D53A490B6801330B6097 -:102BD800120703D5384A1368013313600420FFF73E -:102BE80067FD364B364A1D60364B1B681368013348 -:102BF8001360354A13680133136084F31088CCE7F7 -:102C08002B4225D0DA0703D530490A6801320A6019 -:102C18009A0703D52E490A6801320A605A0703D574 -:102C28002C490A6801320A601B0703D52A4A13682F -:102C3800013313600820FFF73BFD0F22274B1A6072 -:102C48001F4A274B1B68136801331360254AD1E7D5 -:102C5800324214D00120244A1A4B1F6813680133EA -:102C68001360224A136801331360214A137803431F -:102C78001370FFF71DFDF8B2FFF754FFBDE73342AD -:102C88000DD0194A164B02201F68136801331360D0 -:102C9800184A136801331360154A1378E7E784F379 -:102CA8001088F8BD1C4800401C380140600800200E -:102CB8005C080020580800205408002020480040E4 -:102CC800940800202448004068080020500800208C -:102CD8004C08002048080020440800202038014003 -:102CE80024380140640800207C080020780800206F -:102CF8003D08002074080020F7B5444A444811886C -:102D080000888BB2814266D04249434ECC5C013385 -:102D1800DB05DB0D13803378E4B2002B0FD13F4B7A -:102D28001B78002B42D00D2C13D00A2C11D0002C6C -:102D380027D1FFF717FF2000FFF786FEDDE7002306 -:102D480033700A2CD9D0354B1B78002B2ED00D2C84 -:102D5800EDD180200027324B324D01931B880001B2 -:102D68000093EF54FFF7A4FC28000099FEF782FDBA -:102D7800019B0D3C1F80294B1F7063425C413470DE -:102D8800BBE7274A1388012B09D12021200088435B -:102D98005428CED1591C1180224AD454ADE72100C1 -:102DA80020395E29C5D85E2BC3D8F3E71E4B1B8894 -:102DB800002BC0D11D4B1B78002BBCD12300202237 -:102DC8009343412BB7D1174B1F3A1C70144B1A80F1 -:102DD800124B1A7091E7164B1A78002A15D0EFF3A8 -:102DE800108272B60021197082F310880B4B1B7881 -:102DF8008B420AD10C490D4B0A88002A02D11A7855 -:102E0800002A02D000220A801A70F7BD3806002076 -:102E18003A0600203C060020A00D0020A10D00204D -:102E2800A20D0020A40D0020060E0020050E002093 -:102E38002C06002010B5EFF3108272B60021244B47 -:102E48001C68196082F310888C4240D0E30702D5D1 -:102E58002048FDF747FDA30702D51F48FDF742FDAF -:102E6800630702D51D48FDF73DFD230702D51C4821 -:102E7800FDF738FDE30602D51A48FDF733FDA30632 -:102E880002D51948FDF72EFD630602D51748FDF750 -:102E980029FD230602D51648FDF724FDE30502D5D2 -:102EA8001448FDF71FFDA30502D51348FDF71AFDC9 -:102EB800630502D51148FDF715FD230502D5104815 -:102EC800FDF710FDE40402D50E48FDF70BFD10BD1B -:102ED80030060020885E0008975E0008A55E00089E -:102EE800B55E0008C45E0008D35E0008E05E000816 -:102EF800ED5E0008FB5E00080A5F0008175F000827 -:102F0800225F0008325F000870B5EFF3108572B6D3 -:102F18000F4C2278D3B2002A03D185F31088180009 -:102F280070BDA0220B4E520033780B495343C91889 -:102F380002F048FD032233780133134033702378BD -:102F4800013BDBB2237085F310880120E8E7C04617 -:102F58009C0800209D080020A008002037B59C256B -:102F680006AC24886D00AC4204D3064A13680133CA -:102F7800136037BD002C01D0002BF6D00094FFF76A -:102F8800DBFBF6E79408002010B5041E04D1064ABE -:102F980013680133136010BDFDF792F981B2200068 -:102FA800FFF7B8FBF7E7C0469408002030B585B0B6 -:102FB80008AC258802ACA3700523E5702D0A2170A2 -:102FC80062700100257100220093FE202300FFF7A4 -:102FD800B3FB05B030BD0000EFF3108172B60023DB -:102FE8001F4A13601F4A13601F4A13601F4A136069 -:102FF8001F4A13601F4A13601F4A13601F4A136059 -:103008001F4A13601F4A13601F4A13601F4A136048 -:103018001F4A13601F4A13601F4A13601F4A136038 -:103028001F4A13601F4A13601F4A13601F4A136028 -:103038001F4A13601F4A13601F4A13601F4A137008 -:103048001F4A13701F4A13801F4A13801F4A1360B8 -:103058001F4A13701F4A136081F310887047C046D7 -:103068009808002094080020900800208C08002070 -:103078008808002084080020800800207C080020A0 -:103088007808002074080020700800206C080020D0 -:103098006808002064080020600800205C08002000 -:1030A8005808002054080020500800204C08002030 -:1030B8004808002044080020400800203D0800205F -:1030C8003C0800203A06002038060020340600207C -:1030D8002C06002030060020F8B58023484ADB0182 -:1030E800116801240B4313608023464ADB021168F0 -:1030F80005000B43444913600A68444B02201A40F8 -:103108008023DB001343424A0B60136806212343E4 -:103118001360FFF7CFFC06210320FFF7CBFCC02389 -:103128003C4E0421326809209A43803B13433360A4 -:10313800FFF7C0FC04210A20FFF7BCFC3268364BBD -:10314800364F1A4080235B03134333600026344B09 -:103158001E60344B1E60344B1E60B54231D1334B78 -:10316800334A1C601F231360324B3D601D60324B95 -:103178001D601F22314B1C60314B1A600023314AFD -:103188001370314A1370314A1370314A1380314A2F -:103198001370314A1370314A1380314A1370314A1F -:1031A8001370FFF719FF3D231D4A012113602E4BB1 -:1031B8001D203B60FDF7B6FB01211B20FDF7B2FB8C -:1031C800F8BDFDF779FA6C08000229000019FCF730 -:1031D80089FF164B164A18601F231360154B3E6073 -:1031E8001E60154B1E60FDF767FA400000192900A4 -:1031F800FCF778FF1D4B0407640F18400443B8E739 -:1032080034100240381002404C100240FFF3FFFF18 -:103218002C1002400C000050FFFFCFFF0038014087 -:103228000048004004480040084800400C4800405E -:103238002048004004380140083801400C3801405B -:10324800203801409E0800209D0800209C0800208E -:10325800060E0020050E0020040E0020A20D0020FE -:10326800A10D0020A00D00203D800000F0FF00000F -:10327800014B18687047C04698080020014B186831 -:103288007047C04694080020014B18687047C04634 -:1032980090080020014B18687047C0468C08002031 -:1032A800014B18687047C04688080020014B186811 -:1032B8007047C04684080020014B18687047C04614 -:1032C80080080020014B18687047C0467C08002021 -:1032D800014B18687047C04678080020014B1868F1 -:1032E8007047C04674080020014B18687047C046F4 -:1032F80070080020014B18687047C0466C08002011 -:10330800014B18687047C04668080020014B1868D0 -:103318007047C04664080020014B18687047C046D3 -:1033280060080020014B18687047C0465C08002000 -:10333800014B18687047C04658080020014B1868B0 -:103348007047C04654080020014B18687047C046B3 -:1033580050080020014B18687047C0464C080020F0 -:10336800014B18687047C04648080020014B186890 -:103378007047C04644080020014B18687047C04693 -:1033880034060020EFF3108272B60021024B186851 -:10339800196082F31088704740080020EFF310820C -:1033A80072B60021034B18781970C0B282F31088E6 -:1033B8007047C0463D080020002808D0EFF3108170 -:1033C80072B6034A13780343137081F31088704769 -:1033D8003C080020EFF3108272B60021034B1878E6 -:1033E8001970C0B282F310887047C0463C080020AC -:1033F8001022F8B5284D2B68134208D027490A60D7 -:1034080027490A6801320A600121264A11701A0701 -:1034180019D1202601272B6833423CD0224A234B5E -:1034280001201C68136801331360214A13680133B3 -:103438001360204A13783B431370FFF739F9E0B261 -:10344800FFF770FBE7E7DA0703D51B490A68013283 -:103458000A609A0703D519490A6801320A605A07AF -:1034680003D517490A6801320A601B0703D5154AB4 -:103478001368013313600420FFF71AF90F22074B72 -:103488001A60114A094B1B681368013313600F4A0D -:10349800136801331360F8BD1C4800402048004001 -:1034A8008C0800202C0600207C08002024480040BE -:1034B800780800203D080020600800205C080020F3 -:1034C80058080020540800209408002068080020AC -:1034D8001022F8B5284D2B68134208D027490A60F6 -:1034E80027490A6801320A600121264A11701A0721 -:1034F80019D1202602272B6833423CD0224A234B7D -:1035080002201C68136801331360214A13680133D1 -:103518001360204A13783B431370FFF7C9F8E0B2F1 -:10352800FFF700FBE7E7DA0703D51B490A68013212 -:103538000A609A0703D519490A6801320A605A07CE -:1035480003D517490A6801320A601B0703D5154AD3 -:103558001368013313600820FFF7AAF80F22074BFE -:103568001A60114A094B1B681368013313600F4A2C -:10357800136801331360F8BD1C380140203801403E -:103588008C0800202C0600207C08002024380140EC -:10359800740800203D080020500800204C08002036 -:1035A80048080020440800209408002064080020EF -:1035B800054B064A984200D91800054B984200D29C -:1035C800180010607047C046F07E0E000C00002006 -:1035D80010270000F0B587B002930CAB1B780400ED -:1035E80003930DAB1B78080004930EAB1D880FAB3B -:1035F8001B7801920593002945D0002D00D108358C -:10360800211C0C2C41D90C21CCB20600220000212F -:103618002348FCF7FFFD002702000B000120404271 -:10362800C11780197941801859413B003200FCF7D5 -:10363800FDFD029B039A5B001B1B920007339B183E -:103648000522049952424A439B1807009B00082010 -:10365800002B13DD059A5100611A890000D1013150 -:10366800481EC018FCF73EFD6B4602001879019B06 -:10367800082B0FD90820C0B25043083004352D1844 -:103688007D43BF08E81907B0F0BDE3B2062BBBD2F3 -:103698000621B9E7C1B20529EDD20520EBE7C046FE -:1036A80040420F007FB5099D0E0000240A2803D967 -:1036B8007D21B1426441E4B2FA21039408248900CF -:1036C800714302940126002400960194FFF782FFBB -:1036D8000300A54200D028600020834203D02000C8 -:1036E800089A9A426041304004B070BDF0B587B086 -:1036F80005901D2001F0C2FD06001E2001F0BEFD50 -:103708000500262001F0BAFD0700202001F0B6FDD3 -:103718000400212001F0B2FD844601202B09FF0896 -:10372800AD083109320705400640074060462402CB -:10373800520F0443C9B2DBB20432A4B2082914D828 -:103748000839484241410C48494201400918181CAF -:103758000C2B0CD90C20059B03970294019600951D -:10376800C0B2FFF737FF07B0F0BD0449EFE7DBB29F -:10377800062BF0D20620EEE748E8010020A107005A -:1037880070B5040008001D001600FFF7AFFF030026 -:103798000220312C18D8FA200D49E4006258800024 -:1037A800B21A824203D300200A19665050600948B1 -:1037B80009194A6804680120A41A9C4204D3D21843 -:1037C8004A60002D01D1002070BD2B60FBE7C04688 -:1037D8004C0F00200C000020312807D8054BC000F2 -:1037E8001B185A688A4202D800225A607047521A37 -:1037F800FBE7C0464C0F002030B5312815D8FA2514 -:103808000C4BC000C458AD00141BAC4203D30024B9 -:10381800C2501A185460002907D0074A1B181468A8 -:103828005A68A01A884201D25C6030BD52185A60AA -:10383800FBE7C0464C0F00200C00002010B5084CD8 -:103848002378002B01D0002010BDFF21122001F0A9 -:1038580027FD00F05FFD0028F5D001232370F3E772 -:10386800DC10002010B5064B1A78002A07D0002279 -:10387800FF2112201A7001F013FD00F036FD10BD73 -:10388800DC10002070B50E4D2B78002B01D10320E1 -:1038980070BD122001F0F2FC0423040018401C4201 -:1038A800F6D02100122001F0FBFC00232B7000F061 -:1038B8001CFD0028EBD001231C40E018E8E7C046B7 -:1038C800DC100020031E14D0081E12D0012008703E -:1038D8001A784A705A788A701A7ACA705A680A71BD -:1038E8005A68120A4A715A68120C8A715B681B0E70 -:1038F800CB71704703000120002B1AD0002A18D082 -:103908000018072915D919780130012911D15978DA -:10391800117099785170D978117258791979000213 -:1039280008439979DB790904014300201B060B43FE -:103938005360704703000020312B03D802485843D6 -:10394800024BC0187047C04620A1070030DFCE35B3 -:10395800F0B507000E00002489B00092019303AD72 -:10396800220038003100FCF749FC22002870019938 -:103978000098FCF743FC083428720135402CEFD13D -:103988000E9B064807930023054A03AC195D0133D3 -:1039980048405043142BF9D109B0F0BDC59D1C8196 -:1039A80093010001F7B5002304000D00EB54013327 -:1039B800322BFBD1002C00D10C4C2E0001273136C4 -:1039C8007F1B630363405A0C53405C015C403378AF -:1039D800B91920000193FCF70BFC6B5C013E737076 -:1039E800019B6B54B542ECD1F7BDC046B979379EFF -:1039F80037B5084C2569A268E368206861680095B6 -:103A0800FFF7A6FF04496061FFF7CCFF00234A34A3 -:103A1800237037BDE0100020F8100020C42210B534 -:103A280000210248520001F0C5FF10BDE01000203F -:103A3800F7B51500C4220D4C07000E002000002128 -:103A48005200019301F0B6FF019B3222E360089B0C -:103A58002760236123004C336660A5601A80FFF756 -:103A6800C7FF01234B3400202370FEBDE010002067 -:103A7800F7B5214B009001911B7801201F4C002BBA -:103A880016D0009B0018002B12D0002910D0270058 -:103A980032254A373B78312B04D923690133236116 -:103AA800FFF7A6FF3B78E218167E312E01D90720D2 -:103AB800FEBD01333B70320001200021FCF7AAFB58 -:103AC800236D1840636D1940084304D0013DEDB2E1 -:103AD800002DDFD1EBE73000FFF72CFF0028E6D000 -:103AE800009B4C341E70019B18602388002B01D06A -:103AF800013B23800020DBE72B110020E010002091 -:103B080070B50A4A030012780D000120084C002AFB -:103B18000BD00230322908D82361FFF769FF230050 -:103B280000204A334C341D70208070BD2B110020BA -:103B3800E010002070B5154B154C1D780223002DA0 -:103B480021D0013331291ED823004A331B7831256F -:103B5800002B01D0013BDDB22469844202D100234D -:103B68008D4210D00523022A0DD0002A06D0041B4E -:103B7800E317E4185C400423012C04DC0131C9B2CA -:103B8800FFF7BEFF0123180070BDC0462B110020AF -:103B9800E010002073B5002601246442E5170C4BA1 -:103BA8001A6D5B6D1100194303D13220801BC0B21E -:103BB80076BD100019000019694100900191009824 -:103BC800019910401940013602000B00F6B2E9E7EE -:103BD800E0100020312807D8044A163080008358A6 -:103BE800591C01D0013383507047C046E0100020B3 -:103BF800312808D8044A90304000835A03498B4240 -:103C080001D0013383527047E0100020FFFF00000D -:103C180070B500280FD00400FF260025064B32346B -:103C280019880A1CFF2900D9321C027001301D8036 -:103C38000233A042F4D170BD00120020014B186974 -:103C48007047C046E0100020014B18787047C04606 -:103C58002A110020014B18787047C0462B1100200C -:103C68000022024B1A605A601A72704768120020CC -:103C78000122024B186059601A72704768120020BE -:103C8800014B187A7047C0466812002010B5044CE2 -:103C9800C8212368C01AFCF725FA6368C01810BD4C -:103CA80068120020044B10B51B68C821C01AFCF725 -:103CB8009FFA080010BDC04668120020F531FF3198 -:103CC80070B5841A0800FA2189001E00FCF70AFA68 -:103CD8000123034D241A6E602C602B7270BDC04600 -:103CE80068120020014B1B68C01A70476812002038 -:103CF80070B5094DF5316C68FF311C1BC8235C4356 -:103D0800041B0800FA212B688900E41AA41AFCF79E -:103D1800E9F9201AC00F70BD681200200022034B79 -:103D28001A605A609A601A737047C046741200206D -:103D38000C4B1A7B002A13D0FA211A688900821AC0 -:103D48008A420DD25A68511C01D001325A600122B0 -:103D580099681A735A6818608A4200D99A6070473D -:103D68000122F3E7741200200022014B5A607047C9 -:103D780074120020014B58687047C0467412002026 -:103D8800030010B5081E11D0002082420ED1084948 -:103D9800087B00280AD04C681000072C06D90A684E -:103DA8009B1AFA22920093429B41584210BDC0468A -:103DB8007412002010B5FFF753FFFFF7AFFF002282 -:103DC800014B1A7010BDC04684120020F0B587B0B0 -:103DD8001600029103930090FFF752FF1B4D01005C -:103DE800070004002A780098FFF7CAFF019000280E -:103DF80015D03200039B02990098FFF779FF06005F -:103E0800002F1ED02B78002B1BD00098FFF76AFFDD -:103E1800FA230024DB0083426441013405E00126D3 -:103E2800002F02D02C78002CEFD1FF232000019D19 -:103E38001E40184036021D402D043043284307B069 -:103E4800F0BD43425841441CEFE7C04684120020AD -:103E580010B5FFF733FF0122024B1A70FFF784FFFA -:103E680010BDC0468412002010B5064C20780028EA -:103E780004D0FFF7F5FEFFF751FF01200023237060 -:103E880010BDC0468412002010B50400FFF7F8FEEC -:103E9800034B01001A782000FFF772FF10BDC046DF -:103EA8008412002010B5002904D0054B1A7801238C -:103EB800002A02D1FFF7E8FF0300180010BDC04632 -:103EC8008412002070B50B4D2A78131C062A00D9DD -:103ED80006230A24DBB29C40844200D90400FCF784 -:103EE800CDFC054B001918602B78FF2B01D001334E -:103EF8002B7070BD851200208812002013B56B4608 -:103F08006A46997100230C00D37169466B460122F9 -:103F180007330631FCF74EFF002805D16B46DB79E5 -:103F2800012B01D16B469C79200016BDF0B501210B -:103F380085B00320FFF7E2FFA62107000420FFF762 -:103F4800DDFF002302AA9380D370130001900222A0 -:103F58001020033303A9FCF72DFF00281FD102AB63 -:103F6800DB78022B1BD102AB9E8804211120FFF7BE -:103F7800C5FF0500FCF788FC04000220002F0CD0C8 -:103F8800FCF77CFC284F3B68C21A0BD59BB280B269 -:103F9800834207D0FEF770F9032005B0F0BDFA267A -:103FA8007600E2E7002D21D0FFF748FC1F4BE4180C -:103FB800002814D0FFF766FC022806D13000FFF76E -:103FC80081FFFEF759F90120E7E7032807D0012808 -:103FD80007D1FCF759FC001B08D4FFF743FC042069 -:103FE800DBE7FCF751FC001BE4D4F2E700F08BF9A7 -:103FF8000028F4D0281C002D00D10120C0B2FCF705 -:1040080067FC1B2001F03AF9040000F06EF96B46DA -:104018001B799D3C5BB224B2A342CFDB0020044B4A -:1040280038601870B9E7C04688120020204E00009A -:1040380085120020F0B5040000203F2C17D80E4D43 -:104048004B1EAB4213D80D4B0D4EDD7A002D0BD015 -:104058009D7AA54208D11D68551BA94204D9C743BA -:104068005D68AF4204D340190C33B342EDD1F0BDC3 -:1040780001204042FBE7C0461F4E00008C12002082 -:104088008C180020F7B50D001900099B0400089E44 -:104098000192002B03D00123099A5B42138001226D -:1040A8003F2C2AD8264F4B1E0232BB4225D8013A54 -:1040B800002D22D0002E20D0B5421ED8019A200013 -:1040C800FFF7B8FF0422361AAE4216D31D4A002362 -:1040D8001100CE7A002E12D00133DBB20C31802BC6 -:1040E800F7D100231168019E711AB94207D801332C -:1040F800DBB20C32802BF5D105221000FEBD0C2757 -:104108005F43104EF1190A898C720132D205D20D23 -:104118000A819446019A8134BA510127FF34A400D8 -:104128004D60CF72A1592D18A94200D2A551099905 -:10413800002903D06246D2011A430A800022DCE734 -:104148001F4E00008C1200200A4B10B5984210D068 -:104158007F240C2204406243074B9B18DA7A002A1A -:1041680007D01A89C009824203D15A688A4200D905 -:10417800596010BDFFFF00008C12002010B5002808 -:1041880008D00021421C044BFF321C6810C002C337 -:104198009042FAD110BDC0468C1800200023012897 -:1041A8000CD00133022809D00133032806D001338B -:1041B800042803D00538431E9841031D05205843A1 -:1041C800014BC0187047C046425F000870B5040034 -:1041D800FCF75AFB0500FCF757FB401BA042FAD33B -:1041E80070BD0000F8B50400FF23002830D08022FD -:1041F80000792649264B920200282CD00A601A60C2 -:1042080004221A600A20FFF7E1FF4020A17801F09C -:1042180047F84120E17801F043F86178012001F086 -:104228003FF8FCF731FB1B4B0727C618012001F0AC -:1042380025F8050063786B403B4207D0FCF724FB68 -:10424800801B0ED463786B403B4219D10120237840 -:10425800114A1370F8BD022008601A608022D20249 -:10426800CFE7C820FFF7B2FF6178012001F018F806 -:104278003220FFF7ABFF012001F000F80500D9E775 -:1042880029000420FDF784FDE0E7C046180000502F -:1042980018080050881300008C190020014B18786A -:1042A8007047C0468C19002007230340581E00B5EC -:1042B80006280FD8FBF70CFF04040504080A0C00B5 -:1042C8000223054A137000BD0423FAE70523F8E723 -:1042D8000623F6E70123F4E78C19002010B5012026 -:1042E800FFF75CFFFFF77EFF10BD10B50220FFF758 -:1042F80055FFFFF777FF10BD10B50320FFF74EFFFE -:10430800FFF770FF10BD10B50420FFF747FFFFF758 -:1043180069FF10BD10B5FFF7E8FF002804D006209C -:10432800FFF73CFFFFF75EFF10BD000010B500244B -:104338000F4B4B201C7000F0A1FF10210143C9B2A4 -:104348004B2000F0ADFF0220FCF7C2FA210001204B -:1043580000F0A6FF0120FCF7BBFAFFF7BFFFA04261 -:1043680004D00120FCF7B4FAFFF7BFFF10BDC04628 -:104378008C19002070B5FCF787FAFA250400AD0007 -:10438800FCF782FA001BA842FAD370BD10B5FFF7FC -:10439800BAFF041E0FD0094800F0BCFA1E2000F036 -:1043A8006DFF042301001940184205D10520FDF7CF -:1043B800EFFCFFF79AFF0024200010BD9919002098 -:1043C8007FB501380C00032824D8FBF781FE0204CE -:1043D800242E00F081FBFCF751FAFFF755FD041E6F -:1043E80005D00022294B1A70294B1A7012E002A836 -:1043F800047000F077FB051E0CD0244B1C70244B76 -:104408001C70FFF772FF280000F08EFFFFF7B2FF65 -:10441800FFF7BCFF7FBD00231E4A13701B4A1370B1 -:104428001B4A137001221C4BDFE71A4D02A82B7898 -:1044380001AE037003234370144B1B788370144B35 -:104448001B78C370144B1B78037100F015FB308088 -:10445800C1B20D20FDF79CFC7378002B08D0FCF747 -:104468000DFAFFF711FD0028D4D1FFF73EFFD1E781 -:1044780001202B78210001332B70084B1870084B52 -:104488001C60084B1C60FFF79BFFC3E7A9190020BD -:10449800A81900208E1900208D19002098190020D5 -:1044A8009419002090190020F0B58BB00400150075 -:1044B8000391FFF7CFFB002843D000F041FE0290A4 -:1044C80000283ED1214B224E01931B7805A8214F8D -:1044D80003703368047483603B684560C360039B62 -:1044E800437400F05BFA04AB1880019BC4B221004E -:1044F800187800F0A9FA019B1B78022B12D1012C25 -:1045080010D1FFF7B1FC0390431E9841C0B2FDF7EC -:1045180031FF039B002B02D00F4B029A1A703560B3 -:104528003D6006E0022C0ED0042322009A43012AA3 -:10453800F5D004AB5B78019A290020001370FFF7CF -:104548003FFF0BB0F0BD3560F3E7C0469819002077 -:1045580094190020901900209B19002010B5040020 -:10456800FCF78CF902231B1B02002100D8B2FFF7CD -:104578009BFF10BD10B5FFF7B8FE10BD10B502487F -:1045880000F0C8F910BDC046991900200123F0B504 -:104598000C008DB0184252D0002950D07A4A1370BE -:1045A800FCF772F92300FC339860122000F066FED5 -:1045B800030003909B0645D5FDF74EFE132000F03F -:1045C8005DFE0600102000F059FE351C0700FB2E8A -:1045D80001D96E4B1D78320039006D48EDB200F0FC -:1045E8009FF9002801D0FDF791FE01232370039B5A -:1045F80019206370A67000F041FEE0701A2000F0E8 -:104608003DFE390020710D2000F04AFE002E04D036 -:104618002A000020611D00F054FE00220535230009 -:104628001100C5200095FEF799FC0020FFF796FFC2 -:104638000122584B1A701220039900F031FE002015 -:104648000DB0F0BD039B5B06F5D5132000F016FEF8 -:104658000490102000F012FE0600049A31004C4825 -:1046680000F05EF9002801D0FDF750FE192000F097 -:1046780005FE47B21A2000F001FE310005000D20AA -:1046880000F00EFE049B002B04D01A00210000202D -:1046980000F017FE26000322049BFF367370FB17F9 -:1046A8001340DB199B10B37023009D3DFE339D80A2 -:1046B800002509AF3A00200004997575FFF71AF92B -:1046C800A84203D02800FFF749FFB4E723003A00C7 -:1046D8000933FF3323CA23C301237375FCF7CEF8CC -:1046E80032227B68069053437A7804989B18059386 -:1046F800FEF7FCFF079001003A7A059B0698FFF742 -:1047080065FB030408AE1B0E737003021B0E3070AA -:10471800B370C2B279787868FFF70CFA050000F038 -:1047280011F92800FDF71AFE012D0ED87378002B19 -:1047380005D03A7A059B07990698FFF789FB164B2F -:10474800059A1A600122154B1A7001230020AB420A -:104758004041C0B2FFF702FF049B083BDBB2002BCD -:1047680009D1FF346370FDF76FFD1220039900F043 -:1047780097FD012064E72200117A11700132111BA4 -:10478800C9B28B42F8D8ECE7A9190020605F00088D -:1047980099190020A81900209C1900209B190020B5 -:1047A80073B51F4B05001B78022B03D000221D4B4D -:1047B8001A7073BDFFF764FA0028FAD1194B1A4E24 -:1047C8001A78002A02D135600132F1E700F0B8FC0E -:1047D8000400FFF73FFA431E98412200C3B2316834 -:1047E800280000F0BBF8040000F0CCF8002C03D03F -:1047F800042CDED13560DCE76B4601A9D81C019496 -:104808000470FFF735F90028F4D1FFF76EFD019821 -:1048180000F08AFDFFF7AEFDFFF7B8FDEAE7C046F6 -:1048280098190020A0190020A4190020002110B513 -:1048380002000800FFF738FE10BD0000F7B50400BD -:10484800FFF71EFA002840D000F07AFC00283CD17F -:10485800FCF714F81D4B197802394B425941C9B27B -:10486800FFF720FB002830D02000FFF70FFA184E82 -:1048780004003378174F002B02D03B68834224D0C2 -:1048880032212000FBF7B4FC2000CDB23221FBF727 -:1048980029FC2900FFF734F96B46D81C002301A92D -:1048A80003700193FFF7E4F800280BD1019B002B5C -:1048B80008D0FFF71AFD019800F036FDFFF75AFD02 -:1048C800FFF764FD01233C603370F7BD98190020A1 -:1048D8009B1900209C19002010B50024074B1C7060 -:1048E800074B1C70074B1C70074B1C70074B1C7048 -:1048F800FFF760FA064B1C7010BDC04698190020DF -:104908008E190020A9190020A8190020A01900203C -:104918009B190020002802D00023037043707047C1 -:1049280010B5041E0CD003780020834204D06378AD -:104938005B1A581E8341D8B201238918617023700D -:1049480010BD0000052806D8034A80008358591C6A -:1049580001D0013383507047AC19002010B5040012 -:104968000120002B0CD00018002A09D1611A02304E -:10497800002905DBBE2301385B00994200D31000F3 -:1049880010BD0000042806D8034A80008358591C2B -:1049980001D0013383507047C419002082B001ABA5 -:1049A800002806D1188018785B781B02184302B0DB -:1049B8007047027C0178012A0FD0012923D00229EF -:1049C80017D1002A07D142688168521A0FD4FA21F8 -:1049D800C9008A4210D2802228E00222407C121AA2 -:1049E800D2B201290AD0022903D1012AF3D1134AEC -:1049F80007E0C022002919D1022211E0012A04D1BE -:104A08000F4A1A80CFE7022A02D180225200F8E723 -:104A18004268C168511A02D48068121A02D5062267 -:104A2800FF32EEE70748814202D9C1229200E8E747 -:104A3800FA2149008A42E8D38122E7E7050200000B -:104A4800030200002F75000010B5032806D8084A95 -:104A5800800083585C1C01D001338350052906D897 -:104A6800044A89008B58581C01D001338B5010BD63 -:104A7800F0190020D819002037B5002828D00022C6 -:104A880003784178C5788B4252411C1CD2B20F2B57 -:104A980000D90F2401798078D301E4B2DBB2002871 -:104AA8000CD0002901D040210B43002D0DD1234308 -:104AB800FF210A400B40100218433EBD002901D0D7 -:104AC80040210B432021002D00D010210B43EEE79D -:104AD80080230122ECE700000022014B1A70704786 -:104AE800001A0020F8B50400051E0BD0064F3E78CA -:104AF8003000FEF71FFF32210500701CFBF778FB22 -:104B0800267039702800F8BD001A002070B586B0EC -:104B180006000D00FCF708FB031E00D0037801AC6B -:104B28002370104B66701B780495A3700E4B1E788B -:104B38000E4BE6701B6802930D4B1B680393FBF743 -:104B48009DFE0C4902003000FFF774FA0A4D0590EB -:104B5800220000212888FDF7A9F92B8801332B8032 -:104B680006B070BD181A0020201A0020141A002060 -:104B78001C1A002010270000021A002070B5FFF749 -:104B8800B4FB134C134E2378134D002B17D0FFF7AB -:104B9800FDFBFBF779FE030000203278824202D049 -:104BA8000E4A1068181A00212A788A4202D00C4945 -:104BB8000968591A531E9A41D2B2FDF7B1FB002376 -:104BC800084A33701370084A2B702370136070BD45 -:104BD8004C1A0020441A00203C1A0020481A0020D1 -:104BE800401A00204F1A0020241A0020F0B58FB078 -:104BF8000390FFF753FB039B0590002B16D0CE4B79 -:104C08001978002912D1039BCC4EFF335F78CC4D25 -:104C18000837CC4CFF2F0DD9CB4B31701970CB4BCB -:104C2800296021601970FF20FFF770FF0024200021 -:104C38000FB0F0BDFCF778FA002803D00222037801 -:104C480093433CD10023C04A33701370C04A2B6091 -:104C58001370BE4A23601370059BBE4A043BDBB247 -:104C6800012B00D987E001241470FFF783FCFBF7C0 -:104C78000BFEB94B1860B94B1C70B24B1868002872 -:104C88000BD000F051FBFBF7FFFDFA250400AD0047 -:104C9800FBF7FAFD001BA842FAD3FFF747F90225F4 -:104CA800A843C0B200286ED0A74B1878FEF792FF31 -:104CB80000212800FFF72AFFFFF760FFB6E7FBF7A0 -:104CC800DDFD0400FEF7DCFF00280CD1FEF7B6FF7F -:104CD8000500FEF7B9FF3221694309182000FEF7E5 -:104CE800C7FFFDF755FB2000FEF7D0FF3221050076 -:104CF800FBF77EFA2800CEB23221FBF7F3F9310038 -:104D0800FEF7FEFE2000FEF7CDFF2900201AFEF771 -:104D1800AFFF2000FEF7C6FF914B241A1C608B4B97 -:104D2800002404930123049A0BA9137006AB1C7387 -:104D380009A80B94FEF79CFE804E814D814B824A58 -:104D4800A04207D01C60049B210034702C601470B2 -:104D58001C7068E706A9097B11700B9A1A60FEF7A8 -:104D680073FF3123002801D00138C3B23370FEF736 -:104D780065FF286070E700231370FFF7B6FA002874 -:104D880000D07AE752E7FFB23800FEF7AFFC754B68 -:104D98000400984204D9FDF777FA2100012089E739 -:104DA800694B714E1C7804933060FBF767FD6F4BBD -:104DB8000200390020000693FEF7E2FC0590002867 -:104DC80006D0012801D1FDF75FFA0420316871E7A8 -:104DD800FCF7AAF90122664C524200281DD003783C -:104DE800AB431AD0049B22801D7833680793FBF7E6 -:104DF80045FD604B0200009328005F4B0799019422 -:104E0800FFF740F9002809D0049B1878069B196819 -:104E1800FEF7E2FC032031684CE7228000210E20D7 -:104E280000F03EFA00210D2000F03AFA3900222065 -:104E380000F036FAFCF778F9002801D003780593DA -:104E4800059B0BAC23703D4B1B7863703C4B1B6878 -:104E580063603E4B1878002808D0FBF70FFD404BE5 -:104E68001B68C01AFF2800D9FF20C0B2384B09AD13 -:104E78001870290020722000FEF724FD0822002067 -:104E8800290000F035FA00221F203C4B4343A95C5F -:104E98000132CB18082AF9D1394C039D2360FF351C -:104EA8006A78002A0CD0039B0020591C00F020FAD5 -:104EB8001F20039A697823688918914214D12360C6 -:104EC800304BFF2112201F7000F0EAF9FFF714FAA7 -:104ED800264D041E0DD11C4B29681878FEF77CFC62 -:104EE800214B06201968E5E6434355780132EB1853 -:104EF800E3E7039A244B127809201A7000F0BEF9F0 -:104F08000F22224B1040023018702B68204D2B6066 -:104F1800002B03D13800FEF7E9FB2860FBF7B4FC4F -:104F28001C4B1D4918602B681C4A5B181818106028 -:104F38000122014B1A707AE64F1A0020181A002035 -:104F4800141A00201C1A0020201A0020081A002019 -:104F5800101A00204C1A0020481A0020441A002079 -:104F68000C1A002060CC0500041A0020241A002026 -:104F780010000020801A0600102700000515000008 -:104F8800381A0020341A00204E1A00204D1A00202A -:104F98002C1A0020301A0020F0490200281A00209C -:104FA800F0B50C008BB0002902D100200BB0F0BD89 -:104FB800544B1E78012EF8D130427ED0122000F0DA -:104FC8005DF905000100122000F06AF92D0774D57B -:104FD800FBF75AFC4C4B18604C4B1E70FBF754FC0B -:104FE8004B4B4C4F1B68C01A4B4BC0183B68984240 -:104FF80006D2FDF721F9494B0E201978FCF7C8FEB7 -:10500800474B00210D201E7800F04AF9454D002E2F -:1050180033D1454B414E1B689D4247D1FDF7FCF803 -:105028000025337865702370404B1B78A3703B686C -:1050380063603F4B1C78FBF721FC2900020020002D -:10504800FEF7DAFBFDF710F93A4B39681888FFF7D5 -:105058007BF828003968FFF759FD374B1B78AB42BE -:1050680007D0FBF70BFCFEF763FEFEF783FEFDF7A8 -:1050780099F9FFF783FD012098E7331C202E00D90A -:105088002023DBB21A00002002A9019300F019F9CD -:1050980000231F22554302AAD25C019901335519F6 -:1050A800DAB28A42F5D3761AF6B2B0E7FDF7BCF861 -:1050B8000D203178FCF76CFEB2E7FBF7E5FB1F4BE0 -:1050C8001B68C01A00D570E7FF21122000F0E8F82D -:1050D800124B104D1B78237001236370134B1B7800 -:1050E800A3702B686360124B1C78FBF7C7FB002189 -:1050F80002002000FEF780FB05202968FFF706FD67 -:10510800B7E7C0464F1A0020401A00203C1A00207A -:10511800301A00202C1A0020881300004E1A002094 -:10512800341A002005150000381A00204D1A0020F6 -:10513800201A002010000020081A0020281A002039 -:10514800024B1878431E9841C0B270474F1A00208E -:1051580070B50500FBF7E8FF002801D1002070BDFD -:105168000378012BFAD1FEF78BFD0028F6D0FBF768 -:1051780085FBFA24FEF796FDA40044430B2802D8C9 -:105188000948001BEBE72B1CF72D01D9074B1B78AF -:105198000833D8B2FEF7AAFA054B064AE3181B18DB -:1051A8009342DBD90448ECE7E02E0000615F000879 -:1051B800983A0000400D0300203C0300012203231D -:1051C80070B515004C00A340A5408A4006689E4370 -:1051D80035430560456895430222A24045608168D1 -:1051E80099430A438260C2689A43C26070BD032330 -:1051F80049008B4002689A430260C2689A43C260C1 -:10520800704700000222084B19681142FCD00121A6 -:10521800064A107018680842FCD0802118680842B5 -:10522800FCD11078C0B27047083001400C30014002 -:105238008022044B002802D112021A60704712061D -:10524800FBE7C0461800005010B50A4C00212000AA -:10525800FFF7B4FF8022084B52021A600120FBF7C7 -:1052680037FB00212000FFF7C2FF0620FBF730FBC9 -:1052780010BDC046000800501808005010B50400C2 -:105288000120FFF7D5FF7F202040FFF7BBFF00205C -:10529800FFF7B8FF04000020FFF7CAFF200010BD89 -:1052A80070B5040001200D00FFF7C2FF80202043E5 -:1052B800FFF7A8FF2800FFF7A5FF0020FFF7B8FFBA -:1052C80070BD70B5050001200C001600FFF7B0FF97 -:1052D8007F202840FFF796FFA6190020B44202D18C -:1052E800FFF7A6FF70BDFFF78DFF20700134F4E7CC -:1052F80070B5050001200C001600FFF799FF80200B -:105308002843FFF77FFFA619B44203D10020FFF717 -:105318008FFF70BD2078FFF775FF0134F4E710B5F3 -:105328004220FFF7ABFF10BD10B500230A4A410B1E -:10533800C004FAF77BFF0400010CC9B20620FFF78E -:10534800AFFF210A0720C9B2FFF7AAFF0820E1B280 -:10535800FFF7A6FF10BDC0460048E80110B5011CC4 -:10536800112809D91121802302395B4219430920E8 -:10537800C9B2FFF795FF10BDC3B2022BF3D20221C9 -:10538800F1E70000F0B5FA230D0009218DB0060001 -:105398005B0004919D4205D2FB3BAB429B415B42C3 -:1053A8000733049300230793331C0C2E39D90C239D -:1053B800DBB20393171C082A38D90827FBB20593D8 -:1053C80007AB0193414B290000930398FF23FAB2DE -:1053D800FEF768F9041E2ED1FA2292006A43039957 -:1053E80008AB19705A80110C120E5A71FF229A716B -:1053F800079A1971110ADA711972110C120E9A7240 -:105408006022DA72324A59729A810F229873010027 -:105418005F7000920200C220FDF7A0FD20000DB0D1 -:10542800F0BDF1B20629C3D20623C1E7D3B2052BDA -:10543800C4D20527C2E704270599049B04391B0138 -:10544800490019431D20C9B2FFF72AFF039B1E20FC -:1054580019013943C9B2FFF723FF2620FFF70EFFD2 -:105468000A2E19D97D2D12D80C210143C9B2262044 -:10547800FFF716FF3120FFF701FF072303219843A9 -:105488000143C9B23120FFF70BFF0A2119E0F723C6 -:10549800390018400143EAE7F7233900184001436F -:1054A8002620FFF7FDFE062EE4D83120FFF7E6FEA2 -:1054B8000723052198430143C9B23120FFF7F0FEC5 -:1054C8000C213720FFF7ECFEA8E7C04660CC0500AA -:1054D800CC05000010B5FFF755FF10BDF8B5072340 -:1054E800794A8024116808200B431360C022774949 -:1054F800774F0B68774E934323430B6076490B68CD -:1055080083430B60754976480B68934323430B60CC -:105518007449A0240B68E4059343734A0B6013682D -:1055280072490B40136080224021236892013B405E -:105538001343236003688B436D4903600B683B404A -:105548001A436C4B0A601A683A401A606A4A15682E -:10555800354015602568694E354080263602B446C8 -:105568003543802625600568B543056065460868AB -:10557800624E304028438025086019686D0131402B -:1055880001261960116820000901090911601A68CB -:105598000F213A402A431A60FFF710FE584F0020A7 -:1055A800FFF746FE00215748FFF708FE31002000AC -:1055B8003E60FFF703FE31005248FFF7FFFD02216E -:1055C8005048FFF7FBFD02224F4B04211A6080234D -:1055D8009B023B608023DB023B603C48FFF707FEF1 -:1055E80031003A48FFF703FE00213848FFF7FFFD76 -:1055F80044480D21FFF7FBFD444A454813681D4305 -:105608001560136880218024036819402342F9D16A -:10561800B820404B400119603F49713C0860C521E2 -:105628008900196040211868014319600123116835 -:10563800F0201943116039490A68A2431A430A60E5 -:105648001021374A15688543294311601168A14321 -:105658000B431360202333490A68824313430B60CA -:105668003149324B0A6832481A430A6031490A689C -:105678001A430A6030490A6802400A602F4A032127 -:1056880013600520FBF74EF903210720FBF74AF9C1 -:10569800FFF7DAFDFEF74AFE041E02D100242000BF -:1056A800F8BDFFF73CFE0138C0B2FD28F6D8244803 -:1056B800FFF73AFE0522FA210720FFF763FE0E20C6 -:1056C800FFF74CFEEBE7C0462C10024000040050E8 -:1056D800FFCFFFFFFFFFFFF0040400500804005055 -:1056E800040000500C04005020040050FF0FFFFF7E -:1056F800080000500C00005020000050FF3FFFFF42 -:1057080018080050000800501800005034100240DB -:105718000830014000300140043001400C000140D5 -:10572800080001401400014000040140132000005B -:10573800ECDFFFFF080401400C04014014040140A1 -:10574800C0CA8936EFF3108272B60021024B18687E -:10575800196082F310887047541A0020EFF3108202 -:1057680072B60021024B1868196082F310887047DE -:10577800501A0020014B18687047C046501A002084 -:105788000B4A03211368180008400B420ED0106022 -:105798000222134203D00749086802430A60DB0764 -:1057A80004D50423034A11680B4313607047C046AD -:1057B80014040140541A00200D4A0E4913681800B9 -:1057C80008400B4212D01060DA0608D501220A49B7 -:1057D800086802430A6009490A6801320A609B04A2 -:1057E80004D50823044A11680B4313607047C04668 -:1057F8001404014010200000541A0020501A002000 -:10580800F0B557464E464546DE46E0B504000D0065 -:105818009246994683B08B4230D82DD0494650469F -:10582800FAF724FD290006002000FAF71FFD331AB5 -:105838009846203B34D49B4653465A4693404246AA -:105848001F00534693401E00AF423BD8AF4200D1E1 -:1058580079E05B46A41BBD41002B00DA76E000220C -:1058680000230092019301235A4693400193012398 -:1058780042469340009329E08242CFD90022002378 -:10588800009201930C9B002B01D01C605D60009876 -:10589800019903B0F0BCBB46B246A946A046F0BD8C -:1058A80042469B4620239B1A5246DA404146130043 -:1058B8004A468A40170042461F43534693401E00FB -:1058C800AF42C3D900220023009201934346002B24 -:1058D800D8D0FB0772081A4346467B080EE0AB4255 -:1058E80001D1A2420CD8A41A9D41012024196D416E -:1058F8000021013E24184D41002E06D0AB42EED9BE -:10590800013E24196D41002EF8D1009801995B469B -:1059180000196941002B24DB2B005A464446D3402A -:105928002A00E2401C005B461500002B2ADB2600FB -:105938009E40330026004746BE403200801A9941F7 -:10594800009001919EE7A342BCD882E742462023FB -:1059580000219B1A0022009101920122DA40019253 -:1059680085E7424620239B1A2A0046469A40230090 -:10597800F340444613432A00E2401C005B461500EE -:10598800002BD4DA42462F002023260097409B1A8A -:10599800DE403B003343CDE730B500240139A24255 -:1059A80001D1002005E0035D01340D5DAB42F6D066 -:1059B800581B30BD03008218934200D170471970FC -:1059C8000133F9E7002310B59A4200D110BDCC5C31 -:1059D800C4540133F8E74641554C543A52455345AF -:1059E800540D0A004541524C593A52455345545CAE -:1059F800725C6E005253543A53544152540D0A008B -:105A08005253543A4D41494E0D0A004541524C59A2 -:105A18003A4D41494E5C725C6E004D3A52414449E0 -:105A28004F5F4259504153530D0A000001010300D2 -:105A38003D0A00080E0000000000000002010100FD -:105A48000000000001000000000000000301010048 -:105A58000000000001000000000000000401020036 -:105A680000000000A6000000000000000501010081 -:105A78000000000001000000000000000601010015 -:105A8800ED090008000000000000000007080600FB -:105A980000000000FF0000000000000008010100F5 -:105AA800ED090008010000000000000009010100E4 -:105AB8000000000001000000000000000A010300CF -:105AC8000000000000000000000000000B040502B8 -:105AD8000000000000100E00000000000C01030090 -:105AE8000000000010000000000000000D0204008B -:105AF8000000000064000000000000000E0101002A -:105B0800ED09000800000000000000001002040079 -:105B180000000000F4010000000000001101030073 -:105B28000000000004000000000000001302040050 -:105B380000000000900100000000000014010300B4 -:105B48000000000000000000000000001501020035 -:105B58000000000002000000000000001601030021 -:105B680000000000110000000000000017080600F7 -:105B780000000000000000000000000018080600F7 -:105B88000000000000000000000000008001030188 -:105B98000000000001000000000000008101030176 -:105BA8000000000002000000000000008201010166 -:105BB80000000000000000000000000000030809C9 -:105BC8000A0B0C070D0E000048E8010090D00300F6 -:105BD80020A107002E00433A53454E445F424F4FE1 -:105BE800540D0A00433A53454E445F4641554C54C0 -:105BF8000D0A00433A53454E445F52454144590DFE -:105C08000A00433A434C4B5F4853455F33324D0DCE -:105C18000A00433A434C4B5F4853495F31364D0DB8 -:105C28000A00433A494E49545F444F4E450D0A0015 -:105C3800433A44495350415443480D0A00433A42B9 -:105C480041445F5645520D0A00433A50494E470DAC -:105C58000A00433A5645525F444953500D0A004FD3 -:105C680053452D4C696665547261634C4F52412D02 -:105C78004D75726174614657006465760046414906 -:105C88004C0041540041544900200041542B5645D2 -:105C9800523F00433A41545F5645525F444953507E -:105CA8000D0A0046573D002056455253494F4E3D78 -:105CB80000204749543D002050524F544F3D00208A -:105CC800534348454D413D00204341503D30780005 -:105CD80041542B524144494F3F004552524F520DB7 -:105CE8000A00524144494F3D002056455253494FFE -:105CF8004E3D3078002053544154453D0041542BCB -:105D0800535441543F00484F53545F44524F5050EE -:105D1800454400484F53545F4552524F5253004830 -:105D28004F53545F51554555455F46554C4C0048B7 -:105D38004F53545F4952515F49444C4500484F53B3 -:105D4800545F4952515F485400484F53545F495279 -:105D5800515F544300484F53545F4952515F544573 -:105D680000484F53545F52585F42595445530048B6 -:105D78004F53545F52585F4C505541525400484F4E -:105D880053545F52585F55534152543100484F5352 -:105D9800545F50415253455F4F4B00484F53545F37 -:105DA80050415253455F45525200484F53545F5536 -:105DB8004152545F4552525F4C505541525400482D -:105DC8004F53545F554152545F4552525F555341AA -:105DD80052543100524144494F5F44494F300052B8 -:105DE8004144494F5F44494F3100524144494F5F54 -:105DF80044494F3200524144494F5F44494F3300B0 -:105E0800524144494F5F52585F4F4B0052414449F9 -:105E18004F5F54585F4F4B00524144494F5F4352C4 -:105E2800435F4552520053544154450041542B4856 -:105E3800454C500041542B48454C503F004154209C -:105E48004154492041542B5645523F2041542B522E -:105E58004144494F3F2041542B535441543F204122 -:105E6800542B48454C502041542B42494E0D0A00B2 -:105E780041542B42494E004F4B2042494E0D0A00D7 -:105E8800483A52585F4C5055415254310D0A004817 -:105E98003A52585F5553415254310D0A00483A4519 -:105EA80052525F4C5055415254310D0A00483A4500 -:105EB80052525F5553415254310D0A00483A5052DC -:105EC8004F435F4652414D450D0A00483A434F4201 -:105ED800535F4552520D0A00483A434F42535F4FB1 -:105EE80056460D0A00483A50415253455F455252B2 -:105EF8000D0A00483A5645525F5245515F52580DB7 -:105F08000A00483A4652414D455F4F4B0D0A00483A -:105F18003A515F46554C4C0D0A00483A41545F448B -:105F2800495350415443480D0A00483A52585F5269 -:105F3800494E475F4F56460D0A0001800000000297 -:105F480081000000038340000104850000000586ED -:0C5F58000000000687A00000FBF700001E -:105F64001249134B70B50C68186872B600230326E7 -:105F7400104A114D1360114A1360114A1360114AFB -:105F84001360114A1360114A13602A6801331A43DB -:105F94002A600F4D2A68B24313432B600D4B1960DE -:105FA40084F30888804730BFFDE7C0460000F01F37 -:105FB4000400F01F00440040341002400444004038 -:105FC4000844004010E000E014E000E018E000E0C5 -:105FD4000000014008ED00E001224F4BF0B51968C4 -:105FE40085B00A431A60042219681142FCD00323C5 -:105FF4004A4A11689943023B0B430C21136013680E -:106004000B40042BFBD1464B464E05223360002344 -:10601400454D464C2B602260F032FF3201930392CF -:10602400A63302938020019B414A9B00D158414AE8 -:106034008002136800270343136001233E4A10685B -:106044000343A0201360F023C00502689A43503B29 -:10605400134303600C20394A1368834338481360A0 -:106064000268384B1A408823DB0113430360364A25 -:10607400364B48081F601760354A1760354A801848 -:10608400FAF732F81F21344B344A186011600D229C -:106094002E4B39001A603B00314F029A8A421BD8BA -:1060A40000232A4A1360284A1360294A1360019A7C -:1060B40001320192032A04D1236033602B6005B0BE -:1060C400F0BD039B029A9B1A0393A6230293019BA0 -:1060D400022BA7D1039BA4E7802022684002024040 -:1060E400501E8241202089181E4A12680242D4D0D0 -:1060F4001D483A68C05CD2B2904208D10133082BE3 -:10610400CBD10023236033602B60FFF729FFA53A2E -:1061140053425341C1E7C046001002400C100240F4 -:106124007F3E000014E000E018E000E010E000E032 -:10613400CC000008381002402C100240040000502B -:1061440020000050FF00FFFF0444004000440040D2 -:10615400084400400024F4000C4400402044004063 -:0C616400244400401C440040C40000081B -:0861700098F6FF7F010000001A -:10617800010000000024F400FF010000801A06005E -:04618800FFFF000015 +:1010680059FD0023054A2000137070BD290004F0C3 +:1010780059FD0123F6E7C04638010020330100205E +:1010880073B5254D08202B78002B26D0234C244EF1 +:1010980028223100200004F045FD03F0B1FC03F0E4 +:1010A8004EF92378012B19D1226A636AA069E16994 +:1010B8000024009402F0E6FC0522FA21072004F03F +:1010C800B9FA184802F09EFA03F062FC28220021BF +:1010D800300004F01FFD0020287076BD022B0DD1D2 +:1010E80002F0C6FC03F054FCFA210720052249004F +:1010F80004F0A0FA0C4802F085FAE7E7002BE5D1E6 +:1011080002F0B6FC03F044FC07200522FA2104F0A3 +:1011180091FA0448EFE7C04633010020600100203F +:1011280038010020801A0600F07E0E000048704743 +:101138006001002070B5050004A80678059C181EFB +:101148000BD0201E09D00020032E06D9D8701D70A0 +:1011580059709A7004230130237070BDF0B50500F2 +:1011680005A80778069C081E12D0181E10D0201E4D +:101178000ED00020082A0BD8961CF6B2BE4207D81B +:101188001D705A700233C5B2AA4202D801202670D7 +:10119800F0BD0D5C1D540130F5E7030010B5421891 +:1011A800507894785B5C2404000220431843D37879 +:1011B8001B06184310BD0B0A017043700B0C090E77 +:1011C8008370C17070470000F0B5C1880400C7B0D3 +:1011D800012908D8022302784178808800930133D6 +:1011E80001F00EFF5CE00C25037A05AE3370427AFD +:1011F80002ABFF35ED18931C6A70994205D002784E +:101208004178808800930323EAE7002A06D001008A +:101218000D2002AB0A31C01804F084FC03F01EFC58 +:10122800002806D0002322786178A088009308332C +:10123800D6E71C4F3B78002B0BD0A3883D7803934F +:10124800032D18D900232278617803980093073377 +:10125800C6E704F025F80028EFD1687804F028F8EC +:101268000028EAD1300003F06DFD031E18D1227862 +:101278006178A088DAE703220B4B0C4C1B7831000D +:10128800EB1813400232FF3253430222E4182000C5 +:10129800FF3204F047FC039BFE340135A3803D7008 +:1012A80047B0F0BD88010020890100208A01002094 +:1012B80010B5024801F092FE10BDC04672600008E9 +:1012C8000F2307B5184003003733092800D8073B18 +:1012D8006A461371002301A8537101F07FFE07BD10 +:1012E80030B5040089B002AD18220021280004F0AE +:1012F80011FC2388616803A82B80FFF75CFFA168B5 +:1013080004A8FFF758FFE16805A8FFF754FF216913 +:1013180006A8FFF750FFFFF7B1FA010007A8FFF78B +:101328004AFF0022182311000093F1202B0001F03E +:101338003FFE09B030BD0000F0B5002485B00500BF +:1013480001AEA04206D1124801F048FE05B0F0BD3A +:101358000A2C0FD028000A21FEF74AFF3031A15588 +:1013680028000A21FEF7BEFE2F0001340500092FD0 +:10137800EED8E4B26A46013CE4B2335D68461370C5 +:101388000023537001F02AFE002CF3D1DEE7C0469B +:10139800455F000810B50C0001F020FE034801F07D +:1013A8001DFE2000FFF7C8FF10BDC0462E5E0008D6 +:1013B800F0B50027BEB2B14204D9C45D202C11D0CB +:1013C800092C0FD04C1E0419B1420DD80024C7199E +:1013D800135D301980B2002B0FD1081A43425841CF +:1013E800C0B2F0BD0137E5E72578202D01D0092DE1 +:1013F800ECD1013989B2013CE6E781420FD93D5D64 +:1014080028006138192801D8203DEDB2180061384C +:10141800192801D8203BDBB20134AB42D8D00020D8 +:10142800DFE7000070B5094C200001F0D7FD084D3A +:10143800280001F0D3FD200001F0D0FD280001F0C4 +:10144800CDFD200001F0CAFD70BDC046455F000813 +:101458003C5D000800B5B7B0D02102A800F0D4FF69 +:10146800002806D0002200901100C12002AB01F034 +:101478009FFD37B000BD0000F0B50025274B284F71 +:10148800284E1D70284B8BB031701D703870FFF7D7 +:10149800E9FA00F085FE2548FFF724FAFFF740F93E +:1014A8003B7805AC637033782070A3700123E37038 +:1014B800DB182371FFF7C2F9062360712A00F020B8 +:1014C80000932900230001F073FD2000FFF764FA60 +:1014D800A84205D01648FFF705FA2000FFF700FFDD +:1014E8001448FFF7FFF9144B00220393FF230493DA +:1014F800F73B1100F220009303AB01F059FDFFF711 +:101508009DF9002807D10D48FFF7ECF90C48FFF7C3 +:10151800E9F90BB0F0BD0B48F6E7C0468901002099 +:101528009B0500209A050020880100203E5D0008E8 +:101538004C5D00085B5D0008010102016A5D00085E +:101548008A5D00087A5D0008F0B50400BDB0002887 +:1015580000D193E0B048FFF7C5F92278012A0AD0F4 +:10156800AE48FFF7BFF9012322786178A08800937D +:1015780001F046FD82E06178202900D108E10DD80C +:10158800102915D80800FEF7A3FD2C397B93961374 +:1015980013131313131313131313B200312900D1A8 +:1015A800C6E00AD8212900D14CE1302900D1ABE0AE +:1015B800A088022301220091DAE7402900D19AE0AD +:1015C8004129F5D1D02108A800F01EFF0300A0880A +:1015D800002B00D092E0D033227861780093CD3B85 +:1015E800C6E78F48FFF77EF9E0882300A278A18834 +:1015F80000900020083301F0DBFC3FE08948FFF74A +:1016080071F9042001F002FF874B18000493FEF7DC +:1016180057FE0500854B2F1C1800EDB20593FEF709 +:101628004FFE282D00D92827031CC0B2FFB210286E +:1016380000D91023DAB203927D4A08AD0026089239 +:10164800AB71FF233A006F710C3704992E71EE715C +:10165800FFB20BA80A9304F065FA039A0599E819F2 +:1016680004F060FA039B8120DF19A1882B00320067 +:10167800009701F09DFC082001F0C8FE3DB0F0BDC8 +:101688006C4B08A819686C4B1E686C4B1D68FFF7FB +:1016980092FD310009A8FFF78EFD29000AA8FFF77F +:1016A8008AFD0C23A18800220093822008ABA2E7C0 +:1016B800FFF756F9E2E703F0F3FD0223002808D10B +:1016C8005F4A1278002A04D103F080F901234340CD +:1016D800DBB204AA1374012003F080FE04AB587433 +:1016E8000223A18800220093842008AB83E720000E +:1016F800FFF76AFDC2E700F053FDBFE70100009362 +:101708000022C12008AB76E7E388012B02D0A0882D +:1017180000924CE0207A04AB187403F05FFE04AB2F +:1017280058740223A18800220093B02008AB62E716 +:10173800E388022B04D00223A0880093013317E723 +:101748000022617A207AFFF7B1FB237A002805D1BD +:1017580022786178A0880093082309E71800617A45 +:1017680003F04EFE237A012B0AD1607A02F0C6FDFF +:101778000722637A1340053B012B01D803F064F973 +:1017880002222300A188B12000920833002232E708 +:101798000A2704ABFF1800233B70E0880128CAD948 +:1017A800637A267A9D1CA84203D0A088009503235B +:1017B800DEE621001A0030000A31FFF793F9051E12 +:1017C80003D0031FDBB2012B09D83B0008223000ED +:1017D80008A9FFF7EFFA002801D000233B703A78F8 +:1017E8000B2704ABFF18042329000093300007AB34 +:1017F8000197FFF79FFC0300A088002B05D1043355 +:10180800227861780093013BB2E63B78009307ABFE +:1018180001000022A020EEE6985D0008A55D000802 +:10182800B15D0008BA5D0008C75D0008E15D000809 +:10183800010200005000F81F5400F81F5800F81F5C +:10184800880100200B2604ABF61800233370E388C8 +:10185800012B00D05BE71221277A04A8091833006E +:1018680008223800FFF7A6FA07AD01000A2300286E +:101878000ED00093002208AB38000195FFF75AFC00 +:101888000300A088002BBAD02B78009308ABBFE7E1 +:10189800122104A8327809180093380008AB019582 +:1018A800FFF75CFC0300A088002B05D13378227871 +:1018B8000233617800937AE72B78010000930022C5 +:1018C800A12008AB97E6C04670B504000D00B4B07F +:1018D800002806D0DE4AFFF76BFD002803D0FFF78B +:1018E800E7FC34B070BD29002000DA4AFFF760FD3C +:1018F800002812D0D84801F071FBD84C200001F024 +:101908006DFBFFF78FFD200001F068FBD44801F064 +:1019180065FBD44801F062FBE1E729002000D24AC8 +:10192800FFF746FD002830D0D048FEF7DBFF102037 +:1019380001F06CFDCE4801F051FBC74801F04EFBA9 +:10194800CC4801F04BFBFFF76DFDCB4801F046FB9F +:10195800C34801F043FBC94801F040FB0120FFF7F1 +:10196800EBFCC74801F03AFB0220FFF7E5FCC5484D +:1019780001F034FB1C24FF252800E040043CC0B2E1 +:10198800FFF79EFC231DF7D1C3E729002000BE4ABC +:10199800FFF70EFD061E27D0D021684600F034FD63 +:1019A800D02801D0B9484FE1B94801F017FBB94B2D +:1019B800B9481B78002B00D0B84801F00FFBB84895 +:1019C80001F00CFBB74B1C782009FFF779FC2000CD +:1019D800FFF776FCB44801F001FB40216846FFF7A9 +:1019E800DCFBFFF7A9FC94E729002000AF4AFFF7CA +:1019F800DFFC002800D10EE1D021684600F004FD8C +:101A0800D028CFD131006846FFF7C7FBA84B0100AB +:101A18001800FFF7BFFC914C200001F0DFFA042109 +:101A28006846FFF7BAFBA34B01001800FFF7B2FCAA +:101A3800200001F0D3FA08216846FFF7AEFB9E4B61 +:101A480001001800FFF7A6FC864D280001F0C6FA31 +:101A58000C216846FFF7A1FB984B01001800FFF71F +:101A680099FC200001F0BAFA10216846FFF795FBAF +:101A7800934B01001800FFF78DFC200001F0AEFA2F +:101A880014216846FFF789FB8E4B01001800FFF709 +:101A980081FC200001F0A2FA18216846FFF77DFBBF +:101AA800894B01001800FFF775FC280001F096FA31 +:101AB80044216846FFF771FB844B01001800FFF7CB +:101AC80069FC200001F08AFA48216846FFF765FBA7 +:101AD8007F4B01001800FFF75DFC200001F07EFA43 +:101AE8004C216846FFF759FB7A4B01001800FFF7B5 +:101AF80051FC280001F072FA50216846FFF74DFBAF +:101B0800754B01001800FFF745FC200001F066FA4C +:101B180054216846FFF741FB704B01001800FFF79E +:101B280039FC200001F05AFA58216846FFF735FBC6 +:101B38006B4B01001800FFF72DFC200001F04EFA56 +:101B48005C216846FFF729FB664B01001800FFF788 +:101B580021FC280001F042FA1C216846FFF71DFB12 +:101B6800614B01001800FFF715FC200001F036FA60 +:101B780020216846FFF711FB5C4B01001800FFF7B6 +:101B880009FC200001F02AFA24216846FFF705FB2A +:101B9800574B01001800FFF7FDFB200001F01EFA6B +:101BA80028216846FFF7F9FA524B01001800FFF7A1 +:101BB800F1FB280001F012FA30216846FFF7EDFA30 +:101BC8004D4B01001800FFF7E5FB200001F006FA75 +:101BD80034216846FFF7E1FA484B01001800FFF787 +:101BE800D9FB200001F0FAF92C216846FFF7D5FA55 +:101BF800434B01001800FFF7CDFB200001F0EEF980 +:101C080040216846FFF7C9FA3E4B01001800FFF76C +:101C1800C1FB28007EE6290020003B4AFFF7C8FBED +:101C2800002806D129002000384AFFF7C1FB002808 +:101C380001D037486EE629002000364AFFF7B8FB86 +:101C4800002800D1AEE6344801F0C8F949E6C0469C +:101C5800EA5D0008ED5D0008C75D0008F15D000859 +:101C6800E15D000821600008F35D0008FB5D0008E5 +:101C78000B5E00080F5E0008195E00081F5E000872 +:101C8800275E0008305E0008385E0008425E0008E3 +:101C98004A5E00089B050020E55D0008005F00081B +:101CA800515E00089A0500205D5E0008655E000828 +:101CB8006E5E00087B5E0008875E0008975E00087D +:101CC800A55E0008B15E0008BD5E0008C95E000898 +:101CD800D75E0008E65E0008F55E0008035F0008AE +:101CE800125F0008275F00083C5F0008475F000894 +:101CF800525F00085D5F0008685F0008745F0008B5 +:101D0800805F00088E5F0008945F00089C5F0008F1 +:101D1800A55F0008D85F0008DF5F000810B500F075 +:101D2800FDFA10BD1FB5041E12D0038841680A20B1 +:101D38006A4668441381FFF73EFA6A46A378902002 +:101D48009373072300220093110002AB01F030F9CE +:101D58001FBD0000024B1878431E9841C0B270475F +:101D680088010020F0B5C5B01B4E3378002B01D197 +:101D780045B0F0BD03F094FA0028F9D18222174F3C +:101D880052003B78164D5343EB18FF33587803F055 +:101D98008FFA0028ECD1822302223C785B0063434F +:101DA800ED182900FF3203A803F0BCFE0323013419 +:101DB8001C403378FE35013B03A8AD883C70337076 +:101DC80002F0C0FF0028D3D100900823012210217F +:101DD800280001F015F9C7E78801002089010020D3 +:101DE8008A0100207FB56B4618715971002301A83C +:101DF8000293039304930593FFF772FA07B000BDAB +:101E080000B5C7B000280DD0882202A95200FFF7FC +:101E180001F8002806D0002200901100912002ABA2 +:101E280001F0C6F847B000BD030010B500282BD05C +:101E3800081E29D0012008701A784A705A788A70CA +:101E48009A78CA70DA780A715A68140A4A718C71D9 +:101E5800140C120ECC710A729A68140A4A728C72A7 +:101E6800140C120ECC720A73DA68140A4A738C7353 +:101E7800140C120E0A74CC731B691A0A4B748A74F8 +:101E88001A0C1B0ECA740B7510BD0000002804D074 +:101E9800FF2803D0431E052B01D970470720044AA9 +:101EA8004000835A03498B42F7D001338352F4E749 +:101EB8009E050020FFFF000010B500280AD0020090 +:101EC8000021054B10321C8804800230198002332F +:101ED8009042F8D110BDC0469E050020054A06492B +:101EE80013888B4201D001331380002801D0034BA3 +:101EF800187070479C050020FFFF000008000020B4 +:101F0800F0B58BB00E00141E039003D100252800F5 +:101F18000BB0F0BD100004A9FFF786FF051EF5D031 +:101F280067783800FFF7B2FF002F02D1A07801F0E0 +:101F380089FE6078FFF7D2FF152332000093C32093 +:101F4800039904AB01F034F8E1E700000021044AEA +:101F580013881180181CFF2B00D9FF20C0B27047CE +:101F68009C050020FF22024B18781A707047C04663 +:101F7800080000200300F0B500286AD0081E68D0C9 +:101F880001220A705A784A709A788A70DA78CA7088 +:101F98001A790A715A794A7100228A71CA719A6843 +:101FA800100A0A724872100C120E8872CA72DA6825 +:101FB800100A0A734873100C120E8873CA731A69D0 +:101FC800100A0A744874100C120ECA741422887409 +:101FD800985C88540132462AFAD14C1C985A885485 +:101FE800000AA0540232AA2AF8D1985A8854000A42 +:101FF800A0540232BA2AF8D11A00BA3210780A006C +:10200800BA3210701A00BB3210780A00BB32107056 +:10201800BC331A780B000C0008000826BC331A7071 +:102028000C4D0D4BBD3402781202534032001FB2E2 +:102038005B009BB2002F00DA6B40013AD2B2002A53 +:10204800F5D101308442EED101202370BE311B0A44 +:102058000B70F0BD21100000FFFF0000F0B5384CF8 +:102068000500A5440392022234ACFF32A3700F008E +:10207800228001F0B9FD0023E070322802D8323303 +:102088001B1ADBB22371FFF76DFF0026039B6071FB +:10209800A76039A864AFE680E560236101F0E2FD3E +:1020A800380002F095F87300FB58181E0AD0254A2C +:1020B8009818FA239B0498423FD2FA218900FEF728 +:1020C80011F880B2872302AA5B009B189853023646 +:1020D800642EE8D1B92040008018FFF7EDFEFFF725 +:1020E80035FFC12302AA5B009B1818708423164E83 +:1020F80002A93278FF335B181A70144B200003933F +:102108001B7863AF04A93B70FFF734FF041E0FD0A0 +:10211800BF233A78C4200093A9B204AB00F048FF6B +:102128000023039A13703378FF2B01D001333370E7 +:102138002000A5239B009D44F0BD0548C2E7C0468A +:102148006CFDFFFFE7030000B80500200900002030 +:10215800FFFF000070B50B4E0B0032680A490400FF +:10216800821A8A4201D8002070BD084D01002868F3 +:10217800FFF774FF0028F6D02B68346001332B601A +:10218800F2E7C046B40500205FEA0000B005002071 +:102198000B0A017043700B0C090E8370C1707047F5 +:1021A80000231B4A10B513601A4A13601A4A1360B9 +:1021B8001A4A13601A4A13601A4A13601A4A1360BB +:1021C8001A4A13601A4A13601A4A13601A4A1360AB +:1021D8001A4A13601A4A13601A4A13601A4A13609B +:1021E8001A4A13601A4A13601A4A13601A4A13608B +:1021F80053609360D36013615361184A1360184A9F +:102208001360184A1360184A136000F00FFF10BDDE +:102218002806002024060020200600201C06002096 +:102228001806002014060020100600200C060020C6 +:10223800080600200406002000060020FC050020F7 +:10224800F8050020F4050020F0050020EC0500202A +:10225800E8050020E4050020CC050020C805002082 +:10226800C4050020C0050020BC050020024A1368F0 +:10227800013313607047C04604060020024A136801 +:10228800013313607047C04600060020024A1368F5 +:10229800013313607047C046FC050020024A1368EA +:1022A800013313607047C04614060020024A1368C1 +:1022B800013313607047C04618060020024A1368AD +:1022C800013313607047C04610060020024A1368A5 +:1022D800013313607047C0460C060020024A136899 +:1022E800013313607047C04608060020C143044A02 +:1022F80013681818994201D20120404210607047B3 +:10230800F8050020C143044A13681818994201D2FD +:102318000120404210607047F4050020C30703D530 +:102328000B4A136801331360830703D5094A1368FE +:1023380001331360430703D5074A13680133136059 +:10234800000703D5054A1368013313607047C04678 +:102358002806002024060020200600201C06002055 +:10236800034A1368591C01D0013313607047C046F3 +:10237800F00500200B4B30B51C68844200D2186071 +:10238800C543094C23689D4209D31B182360002AC2 +:1023980004D0064B1A688A4200D2196030BD012366 +:1023A8005B42F3E7EC050020E8050020E405002087 +:1023B800052806D8034A80008358591C01D00133E8 +:1023C80083507047CC050020044A002800D1044AF5 +:1023D8001368591C01D0013313607047C8050020E9 +:1023E800C4050020034A1368591C01D00133136047 +:1023F8007047C046C0050020024B1A68824200D2CE +:1024080018607047BC050020F8B5041E00D13FE1F4 +:102418000020CF2900D83BE1D0220100200003F0A2 +:1024280079FB00F04FFF01002000FFF7B1FE00F03C +:102438004FFF231D01001800FFF7AAFE00F04EFF12 +:10244800010020000830FFF7A3FE00F04DFF010057 +:1024580020000C30FFF79CFE00F04CFF010020002C +:102468001030FFF795FE00F04BFF010020001430FC +:10247800FFF78EFE00F04AFF010020001830FFF73A +:1024880087FE2000834B1C301968FFF781FE20006F +:10249800814B20301968FFF77BFE20007F4B2430EA +:1024A8001968FFF775FE20007D4B28301968FFF783 +:1024B8006FFE20007B4B2C301968FFF769FE200067 +:1024C800794B30301968FFF763FE2000774B3430C2 +:1024D8001968FFF75DFE2000754B38301968FFF763 +:1024E80057FE2000734B3C301968FFF751FE01F08E +:1024F800FFFE010020004030FFF74AFE00F00CFF0D +:10250800010020004430FFF743FE00F00BFF0100FC +:1025180020004830FFF73CFE00F00AFF01002000D1 +:102528004C30FFF735FE00F009FF01002000503065 +:10253800FFF72EFE00F008FF010020005430FFF7DF +:1025480027FE00F007FF010020005830FFF720FEAB +:1025580000F006FF010020005C30FFF719FE00F0D4 +:1025680005FF010020006030FFF712FE00F004FFB5 +:10257800010020006430FFF70BFE00F003FF0100AC +:1025880020006830FFF704FE00F002FF0100200081 +:102598006C30FFF7FDFD00F001FF010020007030F6 +:1025A800FFF7F6FD00F000FF010020007430FFF790 +:1025B800EFFD00F0FFFE010020007830FFF7E8FD96 +:1025C80000F0FEFE010020007C30FFF7E1FD00F086 +:1025D800FDFE010020008030FFF7DAFD2000364BB9 +:1025E80084301968FFF7D4FD2000344B883019680F +:1025F800FFF7CEFD2000324B8C301968FFF7C8FD7D +:102608002000304B90301968FFF7C2FD20002E4B98 +:1026180094301968FFF7BCFD20002C4B98301968DE +:10262800FFF7B6FD20002A4B9C301968FFF7B0FD74 +:102638002000284BA0301968FFF7AAFD20002500CC +:102648002700254BA4301968FFF7A2FD234EA835B3 +:10265800C037280002CE0435FFF79AFDBD42F8D1F5 +:102668001F4B28001968FFF793FD20001D4BC4304D +:102678001968FFF78DFD20001B4BC8301968FFF75C +:1026880087FD2000194BCC301968FFF781FDD02059 +:10269800F8BDC04628060020240600202006002099 +:1026A8001C06002018060020140600201006002032 +:1026B8000C06002008060020040600200006002062 +:1026C800FC050020F8050020F4050020F005002096 +:1026D800EC050020E8050020E4050020CC050020DA +:1026E800C8050020C4050020C0050020BC05002046 +:1026F8000300501E82418A1800209A4200D2981A7C +:1027080070470000024A1368034313607047C046CD +:1027180030060020F0B50022082504000A4E0B48B8 +:1027280093B2994200D8F0BDA35C1B0258402B001D +:1027380007B2400080B2002F00DA7040013BDBB2E4 +:10274800002BF5D10132EBE721100000FFFF00005C +:10275800802303490A681A42FCD0024B186070476C +:102768001C4800402848004070B504004518AC4299 +:1027780005D1402304490A681A42FCD070BD20786C +:10278800FFF7E6FF0134F2E71C480040F0B5454C7E +:102798000D00A5441900AAAB1E88012303AC2370C1 +:1027A800E570330A2D0A257160700725A2706671DD +:1027B800A371002E09D0002907D00B2002AB3200EC +:1027C800C01803F0AFF97619B5B229002000FFF759 +:1027D800A1FFAB1C60550135000AADB26055002061 +:1027E80001249BB24426019322000300FF3603A96B +:1027F800095C8C4600292BD1A22189005B1802A90B +:102808005B18453BFF3B1A70B44246D0411C621C22 +:102818008BB20093230094B201220199009D0130EC +:10282800A942E4D8A22189005B1802A95B18453B9C +:10283800FF3B00201A70FFF78BFF53A82100FFF71A +:1028480093FF0020FFF784FFA5239B009D44F0BD64 +:10285800B44222D0A227411C89B2BF000091E719D7 +:1028680002A97F186146453F0132651CFF3FD2B27D +:10287800ADB23970FF2A0ED1A22189005B1802A9D6 +:102888005B18453BFF3B1A70B54206D002342B005B +:10289800A4B2FE3AC1E72C00BFE7034A136801332C +:1028A8001360D1E76CFDFFFF94080020F0B56C4C75 +:1028B8006C4BA5440024220018886B4B019390426E +:1028C8006AD823009C22093B9BB25200934200D352 +:1028D800ADE004A98D7904AB5B792D021D439542C7 +:1028E80000D3A4E02B0009339BB29C4200D09EE0A9 +:1028F800A11E89B204A8FFF70DFFA0239B00E418CE +:1029080004ABE418594BE3181A78594B1202E4182F +:1029180023781343834200D089E004AB9B7804AA50 +:1029280054AC1288A37004AB2280DA781B79E58056 +:102938001B021343A380002D05D004AB2A00D91D28 +:1029480056A803F0EFF86578012D07D180204000E4 +:10295800FFF7D8FE474B1A7815431D70802080007A +:10296800FFF7D0FE444A136801331360EFF3108673 +:1029780072B6424D2B78032B5BD9414A1368013359 +:102988001360404A13680133136086F3108880206F +:10299800C000FFF7B7FE15E00199531C8D5C9BB290 +:1029A800002D3BD12020FFF7ADFE374A13680133D5 +:1029B8001360364A13680133136002232D4A1178D5 +:1029C8000B431370A5239B009D44F0BD9042E9D0B2 +:1029D800A0267600B342E5D2039B029E5B5C0132DF +:1029E8007354390092B24F1C6318FEB29BB2B542C1 +:1029F800ECD8FF2D10D090420ED9A02149008B426F +:102A0800D0D2A024A400591C1B1904AC1B19204CBB +:102A18001B1900241C708BB21C0050E71A0004AE6E +:102A2800A3190293019B00219B180393DBE78020E5 +:102A3800B9E7A022174F52003B78174853432100AB +:102A4800C01803F06FF803223B78013313403B7042 +:102A58002B780133DBB22B7086F31088B2E7C046BF +:102A68006CFDFFFF060E0020080E00207FFDFFFF13 +:102A78007EFDFFFF3C080020700800209C08002015 +:102A88009008002098080020940800206C08002076 +:102A980080FDFFFF9E080020A008002070B5164D9D +:102AA8002E78002E05D0002802D1144B28701880EB +:102AB80070BD124C2388002809D1002B04D0103097 +:102AC800FFF720FEFFF7F2FE00232380F0E7A122A4 +:102AD800520093420AD94020FFF714FE0123084A06 +:102AE8002B701368268001331360E1E75A1C22809B +:102AF800044AD054DCE7C046050E0020060E00202C +:102B080094080020080E002070B503240223A02595 +:102B18004600B440B340ED052A68134EA2431A4359 +:102B28002A60012585403268AA43104D32602A6820 +:102B3800A24313432B60072809D80F228000824044 +:102B480081400B4C236893431943216070BD0F24C7 +:102B58000838C0B2800084408140064A1368A34305 +:102B680019431160F2E7C046040000500800005005 +:102B780020000050240000500B4A10B513880B4C5D +:102B880099B224880133DB05DB0DA34208D18020EC +:102B9800074A4001136801331360FFF7B3FD10BD06 +:102BA800044C60541380FAE73A06002038060020E7 +:102BB800340600203C06002010B50024074A13887C +:102BC800A34204D800231380054A137010BD054B97 +:102BD800185D0134FFF762FFA4B2EFE7A20D0020F1 +:102BE800A10D0020A40D0020F8B50F252026EFF335 +:102BF800108472B6404B1A68404B1B682A4229D091 +:102C0800D30703D53E490B6801330B60930703D5FF +:102C18003C490B6801330B60530703D53A490B68ED +:102C280001330B60120703D5384A13680133136068 +:102C38000420FFF767FD364B364A1D60364B1B688C +:102C4800136801331360354A13680133136084F342 +:102C58001088CCE72B4225D0DA0703D530490A681B +:102C680001320A609A0703D52E490A6801320A60C0 +:102C78005A0703D52C490A6801320A601B0703D595 +:102C88002A4A1368013313600820FFF73BFD0F221F +:102C9800274B1A601F4A274B1B68136801331360C0 +:102CA800254AD1E7324214D00120244A1A4B1F6822 +:102CB800136801331360224A136801331360214AF1 +:102CC800137803431370FFF71DFDF8B2FFF754FFA5 +:102CD800BDE733420DD0194A164B02201F6813680E +:102CE80001331360184A136801331360154A1378C7 +:102CF800E7E784F31088F8BD1C4800401C38014001 +:102D0800600800205C0800205808002054080020B3 +:102D1800204800409408002024480040680800200B +:102D2800500800204C0800204808002044080020D3 +:102D38002038014024380140640800207C08002025 +:102D4800780800203D08002074080020F7B5444AA0 +:102D58004448118800888BB2814266D04249434E6C +:102D6800CC5C0133DB05DB0D13803378E4B2002B38 +:102D78000FD13F4B1B78002B42D00D2C13D00A2CBF +:102D880011D0002C27D1FFF717FF2000FFF786FE90 +:102D9800DDE7002333700A2CD9D0354B1B78002B84 +:102DA8002ED00D2CEDD180200027324B324D0193CF +:102DB8001B8800010093EF54FFF7A4FC280000993A +:102DC800FEF782FD019B0D3C1F80294B1F7063425B +:102DD8005C413470BBE7274A1388012B09D12021B5 +:102DE800200088435428CED1591C1180224AD4543B +:102DF800ADE7210020395E29C5D85E2BC3D8F3E79B +:102E08001E4B1B88002BC0D11D4B1B78002BBCD13F +:102E1800230020229343412BB7D1174B1F3A1C7034 +:102E2800144B1A80124B1A7091E7164B1A78002A25 +:102E380015D0EFF3108272B60021197082F3108852 +:102E48000B4B1B788B420AD10C490D4B0A88002A80 +:102E580002D11A78002A02D000220A801A70F7BD1F +:102E6800380600203A0600203C060020A00D00206D +:102E7800A10D0020A20D0020A40D0020060E0020A8 +:102E8800050E00202C06002010B5EFF3108272B654 +:102E98000021244B1C68196082F310888C4240D0B2 +:102EA800E30702D52048FDF71DFDA30702D51F48FB +:102EB800FDF718FD630702D51D48FDF713FD23072D +:102EC80002D51C48FDF70EFDE30602D51A48FDF7AA +:102ED80009FDA30602D51948FDF704FD630602D5CE +:102EE8001748FDF7FFFC230602D51648FDF7FAFC44 +:102EF800E30502D51448FDF7F5FCA30502D51348F0 +:102F0800FDF7F0FC630502D51148FDF7EBFC23053E +:102F180002D51048FDF7E6FCE40402D50E48FDF79B +:102F2800E1FC10BD30060020E85F0008F75F0008EC +:102F38000560000815600008246000083360000878 +:102F4800406000084D6000085B6000086A60000887 +:102F580077600008826000089260000870B5EFF39F +:102F6800108572B60F4C2278D3B2002A03D185F3AC +:102F78001088180070BDA0220B4E520033780B4900 +:102F88005343C91802F0CEFD0322337801331340AE +:102F980033702378013BDBB2237085F3108801205E +:102FA800E8E7C0469C0800209D080020A0080020F3 +:102FB80037B59C2506AC24886D00AC4204D3064A7C +:102FC80013680133136037BD002C01D0002BF6D0F5 +:102FD8000094FFF7DBFBF6E79408002010B5041E09 +:102FE80004D1064A13680133136010BDFDF768F970 +:102FF80081B22000FFF7B8FBF7E7C046940800202D +:1030080030B585B008AC258802ACA3700523E570FF +:103018002D0A217062700100257100220093FE20A4 +:103028002300FFF7B3FB05B030BD0000EFF31081BC +:1030380072B600231F4A13601F4A13601F4A1360A9 +:103048001F4A13601F4A13601F4A13601F4A136008 +:103058001F4A13601F4A13601F4A13601F4A1360F8 +:103068001F4A13601F4A13601F4A13601F4A1360E8 +:103078001F4A13601F4A13601F4A13601F4A1360D8 +:103088001F4A13601F4A13601F4A13601F4A1360C8 +:103098001F4A13701F4A13701F4A13801F4A138058 +:1030A8001F4A13601F4A13701F4A136081F3108868 +:1030B8007047C04698080020940800209008002017 +:1030C8008C08002088080020840800208008002040 +:1030D8007C08002078080020740800207008002070 +:1030E8006C080020680800206408002060080020A0 +:1030F8005C080020580800205408002050080020D0 +:103108004C080020480800204408002040080020FF +:103118003D0800203C0800203A0600203806002020 +:10312800340600202C06002030060020F8B5802345 +:10313800484ADB01116801240B4313608023464A87 +:10314800DB02116805000B43444913600A68444BCD +:1031580002201A408023DB001343424A0B601368A5 +:10316800062123431360FFF7CFFC06210320FFF756 +:10317800CBFCC0233C4E0421326809209A43803B93 +:1031880013433360FFF7C0FC04210A20FFF7BCFC9F +:103198003268364B364F1A4080235B031343336043 +:1031A8000026344B1E60344B1E60344B1E60B54203 +:1031B80031D1334B334A1C601F231360324B3D60BF +:1031C8001D60324B1D601F22314B1C60314B1A6051 +:1031D8000023314A1370314A1370314A1370314A4F +:1031E8001380314A1370314A1370314A1380314ABF +:1031F8001370314A1370FFF719FF3D231D4A01214F +:1032080013602E4B1D203B60FDF78CFB01211B201A +:10321800FDF788FBF8BDFDF74FFA6C08000229009E +:103228000019FCF75FFF164B164A18601F2313603E +:10323800154B3E601E60154B1E60FDF73DFA4000C1 +:1032480000192900FCF74EFF1D4B0407640F1840B6 +:103258000443B8E734100240381002404C100240D2 +:10326800FFF3FFFF2C1002400C000050FFFFCFFFC0 +:103278000038014000480040044800400848004029 +:103288000C480040204800400438014008380140FC +:103298000C380140203801409E0800209D0800207D +:1032A8009C080020060E0020050E0020040E0020B9 +:1032B800A20D0020A10D0020A00D00203D800000DF +:1032C800F0FF0000014B18687047C04698080020BE +:1032D800014B18687047C04694080020014B1868D5 +:1032E8007047C04690080020014B18687047C046D8 +:1032F8008C080020014B18687047C04688080020D9 +:10330800014B18687047C04684080020014B1868B4 +:103318007047C04680080020014B18687047C046B7 +:103328007C080020014B18687047C04678080020C8 +:10333800014B18687047C04674080020014B186894 +:103348007047C04670080020014B18687047C04697 +:103358006C080020014B18687047C04668080020B8 +:10336800014B18687047C04664080020014B186874 +:103378007047C04660080020014B18687047C04677 +:103388005C080020014B18687047C04658080020A8 +:10339800014B18687047C04654080020014B186854 +:1033A8007047C04650080020014B18687047C04657 +:1033B8004C080020014B18687047C0464808002098 +:1033C800014B18687047C04644080020014B186834 +:1033D8007047C04634060020EFF3108272B6002111 +:1033E800024B1868196082F3108870474008002063 +:1033F800EFF3108272B60021034B18781970C0B22F +:1034080082F310887047C0463D080020002808D085 +:10341800EFF3108172B6034A13780343137081F3F4 +:10342800108870473C080020EFF3108272B6002124 +:10343800034B18781970C0B282F310887047C046E1 +:103448003C0800201022F8B5284D2B68134208D0FC +:1034580027490A6027490A6801320A600121264A79 +:1034680011701A0719D1202601272B6833423CD046 +:10347800224A234B01201C68136801331360214A38 +:10348800136801331360204A13783B431370FFF726 +:1034980039F9E0B2FFF770FBE7E7DA0703D51B4914 +:1034A8000A6801320A609A0703D519490A68013285 +:1034B8000A605A0703D517490A6801320A601B07D0 +:1034C80003D5154A1368013313600420FFF71AF96E +:1034D8000F22074B1A60114A094B1B681368013306 +:1034E80013600F4A136801331360F8BD1C4800408D +:1034F800204800408C0800202C0600207C08002072 +:1035080024480040780800203D080020600800207A +:103518005C08002058080020540800209408002067 +:10352800680800201022F8B5284D2B68134208D0EF +:1035380027490A6027490A6801320A600121264A98 +:1035480011701A0719D1202602272B6833423CD064 +:10355800224A234B02201C68136801331360214A56 +:10356800136801331360204A13783B431370FFF745 +:10357800C9F8E0B2FFF700FBE7E7DA0703D51B4914 +:103588000A6801320A609A0703D519490A680132A4 +:103598000A605A0703D517490A6801320A601B07EF +:1035A80003D5154A1368013313600820FFF7AAF8FA +:1035B8000F22074B1A60114A094B1B681368013325 +:1035C80013600F4A136801331360F8BD1C380140BB +:1035D800203801408C0800202C0600207C080020A0 +:1035E80024380140740800203D08002050080020BD +:1035F8004C080020480800204408002094080020B7 +:1036080064080020054B064A984200D91800054B6B +:10361800984200D2180010607047C046F07E0E0035 +:103628000C00002010270000F0B587B002930CAB07 +:103638001B78040003930DAB1B78080004930EABB2 +:103648001D880FAB1B7801920593002945D0002DEA +:1036580000D10835211C0C2C41D90C21CCB2060014 +:10366800220000212348FCF7D5FD002702000B00AB +:1036780001204042C11780197941801859413B0007 +:103688003200FCF7D3FD029B039A5B001B1B9200E0 +:1036980007339B180522049952424A439B18070096 +:1036A8009B000820002B13DD059A5100611A890040 +:1036B80000D10131481EC018FCF714FD6B4602000A +:1036C8001879019B082B0FD90820C0B25043083045 +:1036D80004352D187D43BF08E81907B0F0BDE3B2E3 +:1036E800062BBBD20621B9E7C1B20529EDD20520C8 +:1036F800EBE7C04640420F007FB5099D0E0000244D +:103708000A2803D97D21B1426441E4B2FA21039425 +:10371800082489007143029401260024009601942C +:10372800FFF782FF0300A54200D0286000208342F3 +:1037380003D02000089A9A426041304004B070BD1E +:10374800F0B587B005901D2001F048FE06001E2048 +:1037580001F044FE0500262001F040FE070020206D +:1037680001F03CFE0400212001F038FE84460120CF +:103778002B09FF08AD08310932070540064007400C +:1037880060462402520F0443C9B2DBB20432A4B229 +:10379800082914D80839484241410C484942014097 +:1037A8000918181C0C2B0CD90C20059B03970294A4 +:1037B80001960095C0B2FFF737FF07B0F0BD044986 +:1037C800EFE7DBB2062BF0D20620EEE748E801006F +:1037D80020A1070070B5040008001D001600FFF7BF +:1037E800AFFF03000220312C18D8FA200D49E4005D +:1037F80062588000B21A824203D300200A19665028 +:103808005060094809194A6804680120A41A9C42B2 +:1038180004D3D2184A60002D01D1002070BD2B605E +:10382800FBE7C0464C0F00200C000020312807D8C9 +:10383800054BC0001B185A688A4202D800225A60F9 +:103848007047521AFBE7C0464C0F002030B53128AC +:1038580015D8FA250C4BC000C458AD00141BAC4257 +:1038680003D30024C2501A185460002907D0074A0D +:103878001B1814685A68A01A884201D25C6030BDCF +:1038880052185A60FBE7C0464C0F00200C0000207D +:1038980010B5084C2378002B01D0002010BDFF2163 +:1038A800122001F0ADFD00F05FFD0028F5D00123E6 +:1038B8002370F3E7DC10002010B5064B1A78002AB5 +:1038C80007D00022FF2112201A7001F099FD00F0A4 +:1038D80036FD10BDDC10002070B50E4D2B78002B86 +:1038E80001D1032070BD122001F078FD04230400EB +:1038F80018401C42F6D02100122001F081FD00235F +:103908002B7000F01CFD0028EBD001231C40E018B0 +:10391800E8E7C046DC100020031E14D0081E12D0B1 +:10392800012008701A784A705A788A701A7ACA7010 +:103938005A680A715A68120A4A715A68120C8A71CE +:103948005B681B0ECB71704703000120002B1AD057 +:10395800002A18D00018072915D91978013001292B +:1039680011D15978117099785170D97811725879A4 +:103978001979000208439979DB7909040143002089 +:103988001B060B435360704703000020312B03D8FC +:1039980002485843024BC0187047C04620A1070090 +:1039A80030DFCE35F0B507000E00002489B0009254 +:1039B800019303AD220038003100FCF71FFC220000 +:1039C800287001990098FCF719FC08342872013511 +:1039D800402CEFD10E9B064807930023054A03AC01 +:1039E800195D013348405043142BF9D109B0F0BD9B +:1039F800C59D1C8193010001F7B5002304000D004B +:103A0800EB540133322BFBD1002C00D10C4C2E008F +:103A1800012731367F1B630363405A0C53405C0116 +:103A28005C403378B91920000193FCF7E1FB6B5C2B +:103A3800013E7370019B6B54B542ECD1F7BDC04693 +:103A4800B979379E37B5084C2569A268E3682068BC +:103A580061680095FFF7A6FF04496061FFF7CCFF96 +:103A680000234A34237037BDE0100020F8100020EE +:103A7800C42210B500210248520002F04BF810BDD4 +:103A8800E0100020F7B51500C4220D4C07000E0009 +:103A9800200000215200019302F03CF8019B3222E1 +:103AA800E360089B2760236123004C336660A560B0 +:103AB8001A80FFF7C7FF01234B3400202370FEBD97 +:103AC800E0100020F7B5214B009001911B780120F0 +:103AD8001F4C002B16D0009B0018002B12D0002979 +:103AE80010D0270032254A373B78312B04D9236977 +:103AF80001332361FFF7A6FF3B78E218167E312ECB +:103B080001D90720FEBD01333B703200012000219E +:103B1800FCF780FB236D1840636D1940084304D0FF +:103B2800013DEDB2002DDFD1EBE73000FFF72CFFB0 +:103B38000028E6D0009B4C341E70019B1860238837 +:103B4800002B01D0013B23800020DBE72B11002054 +:103B5800E010002070B50A4A030012780D00012019 +:103B6800084C002A0BD00230322908D82361FFF70D +:103B780069FF230000204A334C341D70208070BD3B +:103B88002B110020E010002070B5154B154C1D7846 +:103B98000223002D21D0013331291ED823004A33B6 +:103BA8001B783125002B01D0013BDDB2246984420A +:103BB80002D100238D4210D00523022A0DD0002AFD +:103BC80006D0041BE317E4185C400423012C04DC32 +:103BD8000131C9B2FFF7BEFF0123180070BDC0460E +:103BE8002B110020E010002073B500260124644248 +:103BF800E5170C4B1A6D5B6D1100194303D1322088 +:103C0800801BC0B276BD10001900001969410090F0 +:103C180001910098019910401940013602000B00EB +:103C2800F6B2E9E7E0100020312807D8044A163038 +:103C380080008358591C01D0013383507047C04617 +:103C4800E0100020312808D8044A90304000835AF8 +:103C580003498B4201D0013383527047E0100020A2 +:103C6800FFFF000070B500280FD00400FF260025D4 +:103C7800064B323419880A1CFF2900D9321C0270FD +:103C880001301D800233A042F4D170BD0012002023 +:103C9800014B18697047C046E0100020014B1878A6 +:103CA8007047C0462A110020014B18787047C0465B +:103CB8002B1100200022024B1A605A601A727047BA +:103CC800681200200122024B186059601A7270476E +:103CD80068120020014B187A7047C046681200200D +:103CE80010B5044CC8212368C01AFCF7FBF96368B7 +:103CF800C01810BD68120020044B10B51B68C821FD +:103D0800C01AFCF775FA080010BDC04668120020FA +:103D1800F531FF3170B5841A0800FA2189001E00B8 +:103D2800FCF7E0F90123034D241A6E602C602B7216 +:103D380070BDC04668120020014B1B68C01A70474E +:103D48006812002070B5094DF5316C68FF311C1BF5 +:103D5800C8235C43041B0800FA212B688900E41A75 +:103D6800A41AFCF7BFF9201AC00F70BD6812002012 +:103D78000022034B1A605A609A601A737047C04653 +:103D8800741200200C4B1A7B002A13D0FA211A68EF +:103D98008900821A8A420DD25A68511C01D0013218 +:103DA8005A60012299681A735A6818608A4200D9C1 +:103DB8009A6070470122F3E7741200200022014B39 +:103DC8005A60704774120020014B58687047C0460B +:103DD80074120020030010B5081E11D00020824282 +:103DE8000ED10849087B00280AD04C681000072C1F +:103DF80006D90A689B1AFA22920093429B415842BC +:103E080010BDC0467412002010B5FFF753FFFFF72E +:103E1800AFFF0022014B1A7010BDC046841200206B +:103E2800F0B587B01600029103930090FFF752FF98 +:103E38001B4D0100070004002A780098FFF7CAFF0D +:103E48000190002815D03200039B02990098FFF7D3 +:103E580079FF0600002F1ED02B78002B1BD000986E +:103E6800FFF76AFFFA230024DB0083426441013430 +:103E780005E00126002F02D02C78002CEFD1FF237B +:103E88002000019D1E40184036021D402D0430437D +:103E9800284307B0F0BD43425841441CEFE7C046F1 +:103EA8008412002010B5FFF733FF0122024B1A706D +:103EB800FFF784FF10BDC0468412002010B5064CE1 +:103EC8002078002804D0FFF7F5FEFFF751FF012006 +:103ED8000023237010BDC0468412002010B50400D2 +:103EE800FFF7F8FE034B01001A782000FFF772FF76 +:103EF80010BDC0468412002010B5002904D0054B1F +:103F08001A780123002A02D1FFF7E8FF03001800FE +:103F180010BDC0468412002070B50B4D2A78131CC2 +:103F2800062A00D906230A24DBB29C40844200D921 +:103F38000400FCF7A3FC054B001918602B78FF2B35 +:103F480001D001332B7070BD85120020881200202B +:103F580013B56B466A46997100230C00D371694604 +:103F68006B46012207330631FCF724FF002805D1F0 +:103F78006B46DB79012B01D16B469C79200016BD7D +:103F8800F0B5012185B00320FFF7E2FFA621070065 +:103F98000420FFF7DDFF002302AA9380D3701300EB +:103FA800019002221020033303A9FCF703FF002825 +:103FB8001FD102ABDB78022B1BD102AB9E880421F8 +:103FC8001120FFF7C5FF0500FCF75EFC0400022086 +:103FD800002F0CD0FCF752FC284F3B68C21A0BD5B7 +:103FE8009BB280B2834207D0FEF770F9032005B078 +:103FF800F0BDFA267600E2E7002D21D0FFF748FC55 +:104008001F4BE418002814D0FFF766FC022806D1DD +:104018003000FFF781FFFEF759F90120E7E7032891 +:1040280007D0012807D1FCF72FFC001B08D4FFF7A5 +:1040380043FC0420DBE7FCF727FC001BE4D4F2E791 +:1040480000F08BF90028F4D0281C002D00D10120A5 +:10405800C0B2FCF73DFC1B2001F0C0F9040000F0E1 +:104068006EF96B461B799D3C5BB224B2A342CFDB51 +:104078000020044B38601870B9E7C0468812002049 +:10408800204E000085120020F0B5040000203F2CCF +:1040980017D80E4D4B1EAB4213D80D4B0D4EDD7A83 +:1040A800002D0BD09D7AA54208D11D68551BA94249 +:1040B80004D9C7435D68AF4204D340190C33B342F7 +:1040C800EDD1F0BD01204042FBE7C0461F4E000085 +:1040D8008C1200208C180020F7B50D001900099BE0 +:1040E8000400089E0192002B03D00123099A5B4229 +:1040F800138001223F2C2AD8264F4B1E0232BB4286 +:1041080025D8013A002D22D0002E20D0B5421ED845 +:10411800019A2000FFF7B8FF0422361AAE4216D3E0 +:104128001D4A00231100CE7A002E12D00133DBB2D3 +:104138000C31802BF7D100231168019E711AB94206 +:1041480007D80133DBB20C32802BF5D105221000E1 +:10415800FEBD0C275F43104EF1190A898C7201329B +:10416800D205D20D0A819446019A8134BA510127A9 +:10417800FF34A4004D60CF72A1592D18A94200D276 +:10418800A5510999002903D06246D2011A430A8031 +:104198000022DCE71F4E00008C1200200A4B10B5ED +:1041A800984210D07F240C2204406243074B9B188E +:1041B800DA7A002A07D01A89C009824203D15A68DC +:1041C8008A4200D9596010BDFFFF00008C12002000 +:1041D80010B5002808D00021421C044BFF321C688F +:1041E80010C002C39042FAD110BDC0468C180020FE +:1041F800002301280CD00133022809D001330328F9 +:1042080006D00133042803D00538431E9841031D06 +:1042180005205843014BC0187047C046A2600008EB +:1042280070B50400FCF730FB0500FCF72DFB401BC4 +:10423800A042FAD370BD0000F8B50400FF2300289F +:1042480030D0802200792649264B920200282CD0B3 +:104258000A601A6004221A600A20FFF7E1FF402072 +:10426800A17801F0CDF84120E17801F0C9F8617832 +:10427800012001F0C5F8FCF707FB1B4B0727C61800 +:10428800012001F0ABF8050063786B403B4207D092 +:10429800FCF7FAFA801B0ED463786B403B4219D1C5 +:1042A80001202378114A1370F8BD022008601A60B3 +:1042B8008022D202CFE7C820FFF7B2FF6178012041 +:1042C80001F09EF83220FFF7ABFF012001F086F8DD +:1042D8000500D9E729000420FDF784FDE0E7C04682 +:1042E8001800005018080050881300008C1900208E +:1042F800014B18787047C0468C19002007230340EB +:10430800581E00B506280FD8FBF7E2FE0404050482 +:10431800080A0C000223054A137000BD0423FAE7BB +:104328000523F8E70623F6E70123F4E78C190020B4 +:1043380010B50120FFF75CFFFFF77EFF10BD10B539 +:104348000220FFF755FFFFF777FF10BD10B50320D8 +:10435800FFF74EFFFFF770FF10BD10B50420FFF701 +:1043680047FFFFF769FF10BD10B5FFF7E8FF00280A +:1043780004D00620FFF73CFFFFF75EFF10BD0000EA +:1043880010B500240F4B4B201C7001F027F81021AA +:104398000143C9B24B2001F033F80220FCF798FA28 +:1043A8002100012001F02CF80120FCF791FAFFF719 +:1043B800BFFFA04204D00120FCF78AFAFFF7BFFF35 +:1043C80010BDC0468C19002010B5FCF757FAFFF74E +:1043D80085FD10BD70B5FCF757FAFA250400AD004D +:1043E800FCF752FA001BA842FAD370BD10B50D4B6A +:1043F8001B78002B13D1FFF7B0FF041E0FD00A481B +:1044080000F034FB1E2000F0E9FF042301001940EE +:10441800184205D10520FDF7E5FCFFF790FF0024C1 +:10442800200010BDA0190020A11900207FB5013877 +:104438000C00032822D8FBF74BFE0204222C00F0C4 +:10444800F7FBFFF7C1FF041E05D00022244B1A70AA +:10445800244B1A7012E002A8047000F0EFFB051E4E +:104468000CD01F4B1C701F4B1C70FFF768FF2800F7 +:1044780001F00AF8FFF7AEFFFFF7B8FF7FBD002392 +:10448800194A1370164A1370164A13700122174BF3 +:10449800DFE7154D02A82B7801AE037003234370A4 +:1044A8000F4B1B7883700F4B1B78C3700F4B1B7817 +:1044B800037100F08DFB3080C1B20D20FDF792FC36 +:1044C8007378002B07D0FFF77FFF0028D6D1200094 +:1044D80000F00EF8D2E72B7801332B70F7E7C046CF +:1044E800B1190020B01900208E1900208D19002064 +:1044F80010B501210400032000F052FB0122054BF6 +:1045080021001A70044B02201C60044B1C60FFF74A +:104518008DFF10BD981900209419002090190020D3 +:10452800F0B589B0040015000191FFF7BDFB002824 +:1045380014D000F0B5FE002810D12C4B1B78002BAE +:104548000CD12B4E3378002C0AD1032B08D1FFF75E +:104558003BFF002804D12800FFF7CAFF09B0F0BDCF +:104568003378244F03A803703B6804748360224B9C +:10457800456000931B68C360019B437400F0BAFA5E +:1045880002ABC4B218802100307800F009FB337800 +:10459800022B1FD1012C11D1FFF790FC0190431E73 +:1045A8009841C0B2FDF710FF019B002B02D00022FA +:1045B800124B1A70009B3D601D6003E0032C0FD85E +:1045C800012C10D802AB5B78290020003370FFF76C +:1045D8002DFFC3E7032C03D8012C04D8F2D1E9E757 +:1045E800052CEFD1E6E73D60ECE7C046A0190020B6 +:1045F800981900209419002090190020A319002070 +:1046080010B50400FCF73AF902231B1B0200210035 +:10461800D8B2FFF785FF10BD10B5FFF790FE10BDAB +:104628000123F0B50C008DB0184256D0002954D0A3 +:104638007C4A1278002A50D17B4A1370FCF724F97F +:104648002300FC339860122000F0C8FE030003909A +:104658009B0645D5FDF72AFE132000F0BFFE060095 +:10466800102000F0BBFE351C0700FB2E01D96F4B54 +:104678001D78320039006E48EDB200F0FDF90028CF +:1046880001D0FDF76DFE01232370039B1920637091 +:10469800A67000F0A3FEE0701A2000F09FFE39001B +:1046A80020710D2000F0ACFE002E04D02A0000205E +:1046B800611D00F0B6FE0022053523001100C5205B +:1046C8000095FEF775FC0020FFF79AFF0122594B71 +:1046D8001A701220039900F093FE00200DB0F0BD6F +:1046E800039B5B06F5D5132000F078FE049010209C +:1046F80000F074FE0600049A31004D4800F0BCF941 +:10470800002801D0FDF72CFE192000F067FE47B203 +:104718001A2000F063FE310005000D2000F070FE45 +:10472800049B002B04D01A002100002000F079FE21 +:1047380026000322049BFF367370FB171340DB1916 +:104748009B10B37023009D3DFE339D80002509AF6B +:104758003A00200004997575FFF7F6F8A84203D0CF +:104768002800FFF74DFFB4E723003A000933FF3371 +:1047780023CA23C301237375FCF780F832227B68B0 +:10478800069053437A7804989B180593FEF7D8FF50 +:10479800079001003A7A059B0698FFF741FB03044E +:1047A80008AE1B0E737003021B0E3070B370C2B2DA +:1047B80079787868FFF7E8F9050000F06FF92800C4 +:1047C800FDF7F6FD012D0ED87378002B05D03A7A47 +:1047D800059B07990698FFF765FB174B059A1A6022 +:1047E8000122164B1A7001230020AB424041C0B28F +:1047F800FFF706FF049B083BDBB2002B09D1FF340F +:104808006370FDF74BFD1220039900F0F9FD0120BC +:1048180064E72200117A11700132111BC9B28B4270 +:10482800F8D8ECE7A0190020B1190020C0600008F2 +:10483800A1190020B0190020A4190020A3190020F4 +:1048480073B5214B05001B78002B03D11F4B1B7838 +:10485800022B03D000221E4B1A7073BDFFF73AFAE1 +:104868000028FAD11A4B1B4E1A78002A02D135605B +:104878000132F1E700F014FD0400FFF715FA431EBA +:1048880098412200C3B23168280000F013F90400EF +:1048980000F024F9002C03D0042CDED13560DCE7CD +:1048A8006B4601A9D81C01940470FFF70BF9002886 +:1048B800F4D1FFF744FD019800F0E6FDFFF78AFD0B +:1048C800FFF794FDEAE7C046A019002098190020D8 +:1048D800A8190020AC190020002110B5020008001A +:1048E800FFF71EFE10BD0000F7B5254B04001B782E +:1048F800002B44D1FFF7EEF9002840D000F0D0FC9F +:1049080000283CD1FBF7BAFF1E4B197802394B42FD +:104918005941C9B2FFF7F0FA002830D02000FFF75C +:10492800DFF9194E04003378184F002B02D03B688A +:10493800834224D032212000FBF75AFC2000CDB25C +:104948003221FBF7CFFB2900FFF704F96B46D81C8F +:10495800002301A903700193FFF7B4F800280BD1D5 +:10496800019B002B08D0FFF7EAFC019800F08CFDB2 +:10497800FFF730FDFFF73AFD01233C603370F7BDC8 +:10498800A019002098190020A3190020A4190020BC +:1049980010B50024074B1C70074B1C70074B1C708C +:1049A800074B1C70074B1C70FFF72EFA064B1C7048 +:1049B80010BDC046981900208E190020B11900209A +:1049C800B0190020A8190020A319002010B50A4B1F +:1049D8001A7854425441E4B2002A05D101321A70BF +:1049E800FBF74CFF054B1860FFF7A2FCFF211220D4 +:1049F80000F006FD200010BDA01900209C19002021 +:104A080010B50C4B1A78002A12D000221A700A4BE3 +:104A18001B78012B0CD1FBF731FF084B084A1B68A8 +:104A2800C01A13681B181360064A13681B18136012 +:104A380010BDC046A0190020981900209C1900201C +:104A4800941900209019002010B5034800F00EF8C2 +:104A5800FFF7D6FF10BDC046A1190020024B1878F9 +:104A6800431E9841C0B27047A0190020002802D008 +:104A7800002303704370704710B5041E0CD00378F0 +:104A88000020834204D063785B1A581E8341D8B251 +:104A9800012389186170237010BD0000052806D80D +:104AA800034A80008358591C01D001338350704752 +:104AB800B419002010B504000120002B0CD00018F8 +:104AC800002A09D1611A0230002905DBBE2301380A +:104AD8005B00994200D3100010BD0000042806D8DE +:104AE800034A80008358591C01D001338350704712 +:104AF800CC19002082B001AB002806D118801878A4 +:104B08005B781B02184302B07047027C0178012AC7 +:104B18000FD0012923D0022917D1002A07D14268D2 +:104B28008168521A0FD4FA21C9008A4210D2802211 +:104B380028E00222407C121AD2B201290AD00229A6 +:104B480003D1012AF3D1134A07E0C022002919D161 +:104B5800022211E0012A04D10F4A1A80CFE7022A63 +:104B680002D180225200F8E74268C168511A02D483 +:104B78008068121A02D50622FF32EEE70748814202 +:104B880002D9C1229200E8E7FA21C9008A42E8D393 +:104B98008122E7E705020000030200002F750000EC +:104BA80010B5032806D8084A800083585C1C01D039 +:104BB80001338350052906D8044A89008B58581CAC +:104BC80001D001338B5010BDF8190020E0190020E6 +:104BD80037B5002828D0002203784178C5788B4261 +:104BE80052411C1CD2B20F2B00D90F2401798078B6 +:104BF800D301E4B2DBB200280CD0002901D0402157 +:104C08000B43002D0DD12343FF210A400B40100216 +:104C180018433EBD002901D040210B432021002D1F +:104C280000D010210B43EEE780230122ECE70000BF +:104C38000022014B1A707047081A0020F8B50400CA +:104C4800051E0BD0064F3E783000FEF79DFE322140 +:104C58000500701CFBF7CCFA267039702800F8BDE7 +:104C6800081A002070B586B006000D00FCF75EFA41 +:104C7800031E00D0037801AC2370104B66701B78BC +:104C88000495A3700E4B1E780E4BE6701B680293BA +:104C98000D4B1B680393FBF7F1FD0C490200300034 +:104CA800FFF7F2F90A4D0590220000212888FDF748 +:104CB80027F92B8801332B8006B070BD201A0020FD +:104CC800281A00201C1A0020241A0020102700008F +:104CD8000A1A002070B5FFF732FB134C134E2378E5 +:104CE800134D002B17D0FFF781FBFBF7CDFD030019 +:104CF80000203278824202D00E4A1068181A002129 +:104D08002A788A4202D00C490968591A531E9A41D6 +:104D1800D2B2FDF72FFB0023084A33701370084AFC +:104D28002B702370136070BD541A00204C1A002099 +:104D3800441A0020501A0020481A0020571A002050 +:104D48002C1A0020F0B58FB00390FFF7D1FA039B1F +:104D58000590002B1AD0D04B1B78002B16D1FFF7EB +:104D68007DFE011E12D1039BCC4EFF335F78CC4DE4 +:104D78000837CC4CFF2F0DD9CB4B30701870CB4B6C +:104D8800286020601870FF20FFF76CFF00242000C7 +:104D98000FB0F0BDFCF7CAF9002803D0022203784F +:104DA80093433CD10023C04A33701370C04A2B6030 +:104DB8001370BE4A23601370059BBE4A043BDBB2E6 +:104DC800012B00D987E001241470FFF725FCFBF7BD +:104DD8005BFDB94B1860B94B1C70B24B18680028C2 +:104DE8000BD000F051FBFBF74FFDFA250400AD0096 +:104DF800FBF74AFD001BA842FAD3FFF7C1F80225CA +:104E0800A843C0B200286ED0A74B1878FEF70CFF55 +:104E180000212800FFF726FFFFF75CFFB6E7FBF746 +:104E28002DFD0400FEF756FF00280CD1FEF730FFD9 +:104E38000500FEF733FF3221694309182000FEF709 +:104E480041FFFDF7CFFA2000FEF74AFF32210500A7 +:104E5800FBF7CEF92800CEB23221FBF743F9310037 +:104E6800FEF778FE2000FEF747FF2900201AFEF71C +:104E780029FF2000FEF740FF914B241A1C608B4B42 +:104E8800002404930123049A0BA9137006AB1C7326 +:104E980009A80B94FEF716FE804E814D814B824A7D +:104EA800A04207D01C60049B210034702C60147051 +:104EB8001C7068E706A9097B11700B9A1A60FEF747 +:104EC800EDFE3123002801D00138C3B23370FEF75C +:104ED800DFFE286070E700231370FFF730FA002820 +:104EE80000D07AE752E7FFB23800FEF729FC754B8D +:104EF8000400984204D9FDF7F1F92100012089E75F +:104F0800694B714E1C7804933060FBF7B7FC6F4B0C +:104F18000200390020000693FEF75CFC059000288B +:104F280006D0012801D1FDF7D9F90420316871E7CD +:104F3800FCF7FCF80122664C524200281DD0037889 +:104F4800AB431AD0049B22801D7833680793FBF784 +:104F580095FC604B0200009328005F4B0799019471 +:104F6800FFF7BAF8002809D0049B1878069B19683F +:104F7800FEF75CFC032031684CE7228000210E20FC +:104F880000F03EFA00210D2000F03AFA3900222004 +:104F980000F036FAFCF7CAF8002801D00378059328 +:104FA800059B0BAC23703D4B1B7863703C4B1B6817 +:104FB80063603E4B1878002808D0FBF75FFC404B35 +:104FC8001B68C01AFF2800D9FF20C0B2384B09ADB2 +:104FD8001870290020722000FEF79EFC082200208D +:104FE800290000F035FA00221F203C4B4343A95CFE +:104FF8000132CB18082AF9D1394C039D2360FF35BB +:105008006A78002A0CD0039B0020591C00F020FA73 +:105018001F20039A697823688918914214D1236064 +:10502800304BFF2112201F7000F0EAF9FFF78EF9CC +:10503800264D041E0DD11C4B29681878FEF7F6FB87 +:10504800214B06201968E5E6434355780132EB18F1 +:10505800E3E7039A244B127809201A7000F0BEF98E +:105068000F22224B1040023018702B68204D2B6005 +:10507800002B03D13800FEF763FB2860FBF704FC24 +:105088001C4B1D4918602B681C4A5B1818181060C7 +:105098000122014B1A707AE6571A0020201A0020C4 +:1050A8001C1A0020241A0020281A0020101A002098 +:1050B800181A0020541A0020501A00204C1A0020F8 +:1050C800141A002060CC05000C1A00202C1A0020AD +:1050D80010000020801A06001027000005150000A7 +:1050E800401A00203C1A0020561A0020551A0020A9 +:1050F800341A0020381A0020F0490200301A002023 +:10510800F0B50C008BB0002902D100200BB0F0BD27 +:10511800544B1E78012EF8D130427ED0122000F078 +:105128005DF905000100122000F06AF92D0774D519 +:10513800FBF7AAFB4C4B18604C4B1E70FBF7A4FB0B +:105148004B4B4C4F1B68C01A4B4BC0183B689842DE +:1051580006D2FDF79BF8494B0E201978FCF742FE62 +:10516800474B00210D201E7800F04AF9454D002ECE +:1051780033D1454B414E1B689D4247D1FDF776F828 +:105188000025337865702370404B1B78A3703B680B +:1051980063603F4B1C78FBF771FB2900020020007D +:1051A800FEF754FBFDF78AF83A4B39681888FEF782 +:1051B800F5FF28003968FFF755FD374B1B78AB42E0 +:1051C80007D0FBF75BFBFEF7DDFDFEF7FDFDFDF706 +:1051D80013F9FFF77FFD012098E7331C202E00D933 +:1051E8002023DBB21A00002002A9019300F019F96C +:1051F80000231F22554302AAD25C01990133551995 +:10520800DAB28A42F5D3761AF6B2B0E7FDF736F885 +:105218000F203178FCF7E6FDB2E7FBF735FB1F4BB3 +:105228001B68C01A00D570E7FF21122000F0E8F8CB +:10523800124B104D1B78237001236370134B1B789E +:10524800A3702B686360124B1C78FBF717FB0021D7 +:1052580002002000FEF7FAFA05202968FFF702FD90 +:10526800B7E7C046571A0020481A0020441A002001 +:10527800381A0020341A002088130000561A00201B +:105288003C1A002005150000401A0020551A00207D +:10529800281A002010000020101A0020301A0020C0 +:1052A800024B1878431E9841C0B27047571A002025 +:1052B80070B50500FBF73AFF002801D1002070BD4A +:1052C8000378012BFAD1FEF705FD0028F6D0FBF78D +:1052D800D5FAFA24FEF710FDA40044430B2802D89F +:1052E8000948001BEBE72B1CF72D01D9074B1B784E +:1052F8000833D8B2FEF724FA054B064AE3181B1800 +:105308009342DBD90448ECE7E02E0000C1600008B6 +:10531800983A0000400D0300203C030001220323BB +:1053280070B515004C00A340A5408A4006689E430E +:1053380035430560456895430222A240456081686F +:1053480099430A438260C2689A43C26070BD0323CE +:1053580049008B4002689A430260C2689A43C2605F +:10536800704700000222084B19681142FCD0012145 +:10537800064A107018680842FCD080211868084254 +:10538800FCD11078C0B27047083001400C300140A1 +:105398008022044B002802D112021A6070471206BC +:1053A800FBE7C0461800005010B50A4C0021200049 +:1053B800FFF7B4FF8022084B52021A600120FBF766 +:1053C80087FA00212000FFF7C2FF0620FBF780FACA +:1053D80010BDC046000800501808005010B5040061 +:1053E8000120FFF7D5FF7F202040FFF7BBFF0020FB +:1053F800FFF7B8FF04000020FFF7CAFF200010BD28 +:1054080070B5040001200D00FFF7C2FF8020204383 +:10541800FFF7A8FF2800FFF7A5FF0020FFF7B8FF58 +:1054280070BD70B5050001200C001600FFF7B0FF35 +:105438007F202840FFF796FFA6190020B44202D12A +:10544800FFF7A6FF70BDFFF78DFF20700134F4E76A +:1054580070B5050001200C001600FFF799FF8020A9 +:105468002843FFF77FFFA619B44203D10020FFF7B6 +:105478008FFF70BD2078FFF775FF0134F4E710B592 +:105488004220FFF7ABFF10BD10B500230A4A410BBD +:10549800C004FAF7CBFE0400010CC9B20620FFF7DE +:1054A800AFFF210A0720C9B2FFF7AAFF0820E1B21F +:1054B800FFF7A6FF10BDC0460048E80110B5011C63 +:1054C800112809D91121802302395B421943092087 +:1054D800C9B2FFF795FF10BDC3B2022BF3D2022168 +:1054E800F1E70000F0B5FA230D0009218DB00600A0 +:1054F8005B0004919D4205D2FB3BAB429B415B4262 +:105508000733049300230793331C0C2E39D90C233B +:10551800DBB20393171C082A38D90827FBB2059376 +:1055280007AB0193414B290000930398FF23FAB27C +:10553800FEF7E2F8041E2ED1FA2292006A4303997C +:1055480008AB19705A80110C120E5A71FF229A7109 +:10555800079A1971110ADA711972110C120E9A72DE +:105568006022DA72324A59729A810F2298730100C6 +:105578005F7000920200C220FDF71AFD20000DB0F6 +:10558800F0BDF1B20629C3D20623C1E7D3B2052B79 +:10559800C4D20527C2E704270599049B04391B01D7 +:1055A800490019431D20C9B2FFF72AFF039B1E209B +:1055B80019013943C9B2FFF723FF2620FFF70EFF71 +:1055C8000A2E19D97D2D12D80C210143C9B22620E3 +:1055D800FFF716FF3120FFF701FF07230321984348 +:1055E8000143C9B23120FFF70BFF0A2119E0F72365 +:1055F800390018400143EAE7F7233900184001430E +:105608002620FFF7FDFE062EE4D83120FFF7E6FE40 +:105618000723052198430143C9B23120FFF7F0FE63 +:105628000C213720FFF7ECFEA8E7C04660CC050048 +:10563800CC05000010B5FFF755FF10BDF8B50723DE +:10564800794A8024116808200B431360C0227749E7 +:10565800774F0B68774E934323430B6076490B686B +:1056680083430B60754976480B68934323430B606B +:105678007449A0240B68E4059343734A0B601368CC +:1056880072490B40136080224021236892013B40FD +:105698001343236003688B436D4903600B683B40E9 +:1056A8001A436C4B0A601A683A401A606A4A1568CD +:1056B800354015602568694E354080263602B44667 +:1056C8003543802625600568B5430560654608684A +:1056D800624E304028438025086019686D013140CA +:1056E80001261960116820000901090911601A686A +:1056F8000F213A402A431A60FFF710FE584F002046 +:10570800FFF746FE00215748FFF708FE310020004A +:105718003E60FFF703FE31005248FFF7FFFD02210C +:105728005048FFF7FBFD02224F4B04211A608023EB +:105738009B023B608023DB023B603C48FFF707FE8F +:1057480031003A48FFF703FE00213848FFF7FFFD14 +:1057580044480D21FFF7FBFD444A454813681D43A3 +:105768001560136880218024036819402342F9D109 +:10577800B820404B400119603F49713C0860C52181 +:1057880089001960402118680143196001231168D4 +:10579800F0201943116039490A68A2431A430A6084 +:1057A8001021374A15688543294311601168A143C0 +:1057B8000B431360202333490A68824313430B6069 +:1057C8003149324B0A6832481A430A6031490A683B +:1057D8001A430A6030490A6802400A602F4A0321C6 +:1057E80013600520FBF79EF803210720FBF79AF8C2 +:1057F800FFF7DAFDFEF7C4FD041E02D100242000E5 +:10580800F8BDFFF73CFE0138C0B2FD28F6D82448A1 +:10581800FFF73AFE0522FA210720FFF763FE0E2064 +:10582800FFF74CFEEBE7C0462C1002400004005086 +:10583800FFCFFFFFFFFFFFF00404005008040050F3 +:10584800040000500C04005020040050FF0FFFFF1C +:10585800080000500C00005020000050FF3FFFFFE0 +:10586800180800500008005018000050341002407A +:105878000830014000300140043001400C00014074 +:1058880008000140140001400004014013200000FA +:10589800ECDFFFFF080401400C0401401404014040 +:1058A800C0CA8936EFF3108272B60021024B18681D +:1058B800196082F3108870475C1A0020EFF3108299 +:1058C80072B60021024B1868196082F3108870477D +:1058D800581A0020014B18687047C046581A002013 +:1058E8000B4A03211368180008400B420ED01060C1 +:1058F8000222134203D00749086802430A60DB0703 +:1059080004D50423034A11680B4313607047C0464B +:10591800140401405C1A00200D4A0E49136818004F +:1059280008400B4212D01060DA0608D501220A4955 +:10593800086802430A6009490A6801320A609B0440 +:1059480004D50823044A11680B4313607047C04606 +:1059580014040140102000005C1A0020581A00208E +:10596800F0B557464E464546DE46E0B504000D0004 +:105978009246994683B08B4230D82DD0494650463E +:10598800FAF774FC290006002000FAF76FFC331AB6 +:105998009846203B34D49B4653465A469340424649 +:1059A8001F00534693401E00AF423BD8AF4200D180 +:1059B80079E05B46A41BBD41002B00DA76E00022AB +:1059C80000230092019301235A4693400193012337 +:1059D80042469340009329E08242CFD90022002317 +:1059E800009201930C9B002B01D01C605D60009815 +:1059F800019903B0F0BCBB46B246A946A046F0BD2B +:105A080042469B4620239B1A5246DA4041461300E1 +:105A18004A468A40170042461F43534693401E0099 +:105A2800AF42C3D900220023009201934346002BC2 +:105A3800D8D0FB0772081A4346467B080EE0AB42F3 +:105A480001D1A2420CD8A41A9D41012024196D410C +:105A58000021013E24184D41002E06D0AB42EED95C +:105A6800013E24196D41002EF8D1009801995B463A +:105A780000196941002B24DB2B005A464446D340C9 +:105A88002A00E2401C005B461500002B2ADB26009A +:105A98009E40330026004746BE403200801A994196 +:105AA800009001919EE7A342BCD882E7424620239A +:105AB80000219B1A0022009101920122DA400192F2 +:105AC80085E7424620239B1A2A0046469A4023002F +:105AD800F340444613432A00E2401C005B4615008D +:105AE800002BD4DA42462F002023260097409B1A29 +:105AF800DE403B003343CDE730B500240139A242F4 +:105B080001D1002005E0035D01340D5DAB42F6D004 +:105B1800581B30BD03008218934200D1704719709A +:105B28000133F9E7002310B59A4200D110BDCC5CCF +:105B3800C4540133F8E74641554C543A524553454D +:105B4800540D0A004541524C593A52455345545C4C +:105B5800725C6E005253543A53544152540D0A0029 +:105B68005253543A4D41494E0D0A004541524C5941 +:105B78003A4D41494E5C725C6E004D3A524144497F +:105B88004F5F4259504153530D0A00000101030071 +:105B98003D0A00080E00000000000000020101009C +:105BA80000000000010000000000000003010100E7 +:105BB80000000000010000000000000004010200D5 +:105BC80000000000A6000000000000000501010020 +:105BD80000000000010000000000000006010100B4 +:105BE800ED0900080000000000000000070806009A +:105BF80000000000FF000000000000000801010094 +:105C0800ED09000801000000000000000901010082 +:105C18000000000001000000000000000A0103006D +:105C28000000000000000000000000000B04050256 +:105C38000000000000100E00000000000C0103002E +:105C48000000000010000000000000000D02040029 +:105C58000000000064000000000000000E010100C8 +:105C6800ED09000800000000000000001002040018 +:105C780000000000F4010000000000001101030012 +:105C880000000000040000000000000013020400EF +:105C98000000000090010000000000001401030053 +:105CA80000000000000000000000000015010200D4 +:105CB80000000000020000000000000016010300C0 +:105CC8000000000011000000000000001708060096 +:105CD8000000000000000000000000001808060096 +:105CE8000000000000000000000000008001030127 +:105CF8000000000001000000000000008101030115 +:105D08000000000002000000000000008201010104 +:105D18000000000000000000000000000003080967 +:105D28000A0B0C070D0E000048E8010090D0030094 +:105D380020A107002E00433A53454E445F424F4F7F +:105D4800540D0A00433A53454E445F4641554C545E +:105D58000D0A00433A53454E445F52454144590D9C +:105D68000A00433A434C4B5F4853455F33324D0D6D +:105D78000A00433A434C4B5F4853495F31364D0D57 +:105D88000A00433A494E49545F444F4E450D0A00B4 +:105D9800433A44495350415443480D0A00433A4258 +:105DA80041445F5645520D0A00433A50494E470D4B +:105DB8000A00433A5645525F444953500D0A004F72 +:105DC80053452D4C696665547261634C4F52412DA1 +:105DD8004D757261746146570064657600464149A5 +:105DE8004C0041540041544900200041542B564571 +:105DF800523F00433A41545F5645525F444953501D +:105E08000D0A0046573D002056455253494F4E3D16 +:105E180000204749543D002050524F544F3D002028 +:105E2800534348454D413D00204341503D307800A3 +:105E380041542B524144494F3F004552524F520D55 +:105E48000A00524144494F3D002056455253494F9C +:105E58004E3D3078002053544154453D0041542B69 +:105E6800535441543F00484F53545F44524F50508D +:105E7800454400484F53545F4552524F52530048CF +:105E88004F53545F51554555455F46554C4C004856 +:105E98004F53545F4952515F49444C4500484F5352 +:105EA800545F4952515F485400484F53545F495218 +:105EB800515F544300484F53545F4952515F544512 +:105EC80000484F53545F52585F4259544553004855 +:105ED8004F53545F52585F4C505541525400484FED +:105EE80053545F52585F55534152543100484F53F1 +:105EF800545F50415253455F4F4B00484F53545FD6 +:105F080050415253455F45525200484F53545F55D4 +:105F18004152545F4552525F4C50554152540048CB +:105F28004F53545F554152545F4552525F55534148 +:105F380052543100524144494F5F44494F30005256 +:105F48004144494F5F44494F3100524144494F5FF2 +:105F580044494F3200524144494F5F44494F33004E +:105F6800524144494F5F52585F4F4B005241444998 +:105F78004F5F54585F4F4B00524144494F5F435263 +:105F8800435F4552520053544154450041542B48F5 +:105F9800454C500041542B48454C503F004154203B +:105FA8004154492041542B5645523F2041542B52CD +:105FB8004144494F3F2041542B535441543F2041C1 +:105FC800542B48454C502041542B42494E0D0A0051 +:105FD80041542B42494E004F4B2042494E0D0A0076 +:105FE800483A52585F4C5055415254310D0A0048B6 +:105FF8003A52585F5553415254310D0A00483A45B8 +:1060080052525F4C5055415254310D0A00483A459E +:1060180052525F5553415254310D0A00483A50527A +:106028004F435F4652414D450D0A00483A434F429F +:10603800535F4552520D0A00483A434F42535F4F4F +:1060480056460D0A00483A50415253455F45525250 +:106058000D0A00483A5645525F5245515F52580D55 +:106068000A00483A4652414D455F4F4B0D0A0048D9 +:106078003A515F46554C4C0D0A00483A41545F442A +:10608800495350415443480D0A00483A52585F5208 +:10609800494E475F4F56460D0A0001800000000236 +:1060A800810000000383400001048500000005868C +:0C60B8000000000687A00000FBF70000BD +:1060C4001249134B70B50C68186872B60023032686 +:1060D400104A114D1360114A1360114A1360114A9A +:1060E4001360114A1360114A13602A6801331A437A +:1060F4002A600F4D2A68B24313432B600D4B19607D +:1061040084F30888804730BFFDE7C0460000F01FD5 +:106114000400F01F004400403410024004440040D6 +:106124000844004010E000E014E000E018E000E063 +:106134000000014008ED00E001224F4BF0B5196862 +:1061440085B00A431A60042219681142FCD0032363 +:106154004A4A11689943023B0B430C2113601368AC +:106164000B40042BFBD1464B464E052233600023E3 +:10617400454D464C2B602260F032FF32019303926E +:10618400A63302938020019B414A9B00D158414A87 +:106194008002136800270343136001233E4A1068FA +:1061A4000343A0201360F023C00502689A43503BC8 +:1061B400134303600C20394A13688343384813603F +:1061C4000268384B1A408823DB0113430360364AC4 +:1061D400364B48081F601760354A1760354A8018E7 +:1061E400F9F782FF1F21344B344A186011600D22E5 +:1061F4002E4B39001A603B00314F029A8A421BD859 +:1062040000232A4A1360284A1360294A1360019A1A +:1062140001320192032A04D1236033602B6005B05C +:10622400F0BD039B029A9B1A0393A6230293019B3E +:10623400022BA7D1039BA4E78020226840020240DE +:10624400501E8241202089181E4A12680242D4D06E +:106254001D483A68C05CD2B2904208D10133082B81 +:10626400CBD10023236033602B60FFF729FFA53ACD +:1062740053425341C1E7C046001002400C10024093 +:106284007F3E000014E000E018E000E010E000E0D1 +:10629400CC000008381002402C10024004000050CA +:1062A40020000050FF00FFFF044400400044004071 +:1062B400084400400024F4000C4400402044004002 +:0C62C400244400401C440040C4000008BA +:0862D00098F6FF7F01000000B9 +:1062D800010000000024F400FF010000801A0600FD +:0462E800FFFF0000B4 :0400000508000405E6 :00000001FF diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.map b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.map index f8f1a7c8..14278905 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.map +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/build/firmware.map @@ -425,7 +425,7 @@ END GROUP .boot_rodata 0x080000c4 0x14 .boot_rodata 0x080000c4 0x14 build/boot/safe_mode.o -.text 0x080000d8 0x5e8c +.text 0x080000d8 0x5fec 0x080000d8 . = ALIGN (0x4) *(.text) .text 0x080000d8 0x14 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libgcc.a(_thumb1_case_uqi.o) @@ -599,722 +599,735 @@ END GROUP 0x08001054 0x34 build/host/host_cfg_profile.o 0x08001054 host_cfg_profile_stage .text.host_cfg_profile_activate - 0x08001088 0xa8 build/host/host_cfg_profile.o + 0x08001088 0xac build/host/host_cfg_profile.o 0x08001088 host_cfg_profile_activate .text.host_cfg_profile_active - 0x08001130 0x8 build/host/host_cfg_profile.o - 0x08001130 host_cfg_profile_active + 0x08001134 0x8 build/host/host_cfg_profile.o + 0x08001134 host_cfg_profile_active .text.host_cfg_wire_encode_ok - 0x08001138 0x28 build/host/host_cfg_wire.o - 0x08001138 host_cfg_wire_encode_ok + 0x0800113c 0x28 build/host/host_cfg_wire.o + 0x0800113c host_cfg_wire_encode_ok .text.host_cfg_wire_encode_data - 0x08001160 0x3e build/host/host_cfg_wire.o - 0x08001160 host_cfg_wire_encode_data + 0x08001164 0x3e build/host/host_cfg_wire.o + 0x08001164 host_cfg_wire_encode_data .text.at_get_u32_le - 0x0800119e 0x1c build/host/host_cmd.o + 0x080011a2 0x1c build/host/host_cmd.o .text.put_u32_le - 0x080011ba 0x10 build/host/host_cmd.o - *fill* 0x080011ca 0x2 + 0x080011be 0x10 build/host/host_cmd.o + *fill* 0x080011ce 0x2 .text.handle_tx_frame - 0x080011cc 0xd8 build/host/host_cmd.o + 0x080011d0 0xe8 build/host/host_cmd.o .text.at_send_ok - 0x080012a4 0x10 build/host/host_cmd.o + 0x080012b8 0x10 build/host/host_cmd.o .text.at_send_hex_nibble - 0x080012b4 0x20 build/host/host_cmd.o + 0x080012c8 0x20 build/host/host_cmd.o .text.host_send_fault_urc.part.0 - 0x080012d4 0x56 build/host/host_cmd.o - *fill* 0x0800132a 0x2 + 0x080012e8 0x56 build/host/host_cmd.o + *fill* 0x0800133e 0x2 .text.at_send_u32 - 0x0800132c 0x5c build/host/host_cmd.o + 0x08001340 0x5c build/host/host_cmd.o .text.at_send_key_u32 - 0x08001388 0x1c build/host/host_cmd.o + 0x0800139c 0x1c build/host/host_cmd.o .text.at_line_equals - 0x080013a4 0x72 build/host/host_cmd.o - *fill* 0x08001416 0x2 + 0x080013b8 0x72 build/host/host_cmd.o + *fill* 0x0800142a 0x2 .text.at_send_version_triplet - 0x08001418 0x30 build/host/host_cmd.o + 0x0800142c 0x30 build/host/host_cmd.o .text.host_cmd_emit_stats_snapshot - 0x08001448 0x22 build/host/host_cmd.o - 0x08001448 host_cmd_emit_stats_snapshot - *fill* 0x0800146a 0x2 + 0x0800145c 0x22 build/host/host_cmd.o + 0x0800145c host_cmd_emit_stats_snapshot + *fill* 0x0800147e 0x2 .text.host_cmd_init - 0x0800146c 0xd0 build/host/host_cmd.o - 0x0800146c host_cmd_init + 0x08001480 0xd0 build/host/host_cmd.o + 0x08001480 host_cmd_init .text.host_cmd_dispatch - 0x0800153c 0x340 build/host/host_cmd.o - 0x0800153c host_cmd_dispatch + 0x08001550 0x380 build/host/host_cmd.o + 0x08001550 host_cmd_dispatch .text.host_cmd_dispatch_at_line - 0x0800187c 0x454 build/host/host_cmd.o - 0x0800187c host_cmd_dispatch_at_line + 0x080018d0 0x454 build/host/host_cmd.o + 0x080018d0 host_cmd_dispatch_at_line .text.host_cmd_on_radio_events - 0x08001cd0 0x8 build/host/host_cmd.o - 0x08001cd0 host_cmd_on_radio_events + 0x08001d24 0x8 build/host/host_cmd.o + 0x08001d24 host_cmd_on_radio_events .text.host_cmd_emit_tx_done - 0x08001cd8 0x2e build/host/host_cmd.o - 0x08001cd8 host_cmd_emit_tx_done - *fill* 0x08001d06 0x2 + 0x08001d2c 0x2e build/host/host_cmd.o + 0x08001d2c host_cmd_emit_tx_done + *fill* 0x08001d5a 0x2 .text.host_cmd_tx_mailbox_pending - 0x08001d08 0x10 build/host/host_cmd.o - 0x08001d08 host_cmd_tx_mailbox_pending + 0x08001d5c 0x10 build/host/host_cmd.o + 0x08001d5c host_cmd_tx_mailbox_pending .text.host_cmd_service_tx_mailbox - 0x08001d18 0x80 build/host/host_cmd.o - 0x08001d18 host_cmd_service_tx_mailbox + 0x08001d6c 0x80 build/host/host_cmd.o + 0x08001d6c host_cmd_service_tx_mailbox .text.host_cmd_emit_fault - 0x08001d98 0x1c build/host/host_cmd.o - 0x08001d98 host_cmd_emit_fault + 0x08001dec 0x1c build/host/host_cmd.o + 0x08001dec host_cmd_emit_fault .text.host_cmd_emit_rx_frame - 0x08001db4 0x28 build/host/host_cmd.o - 0x08001db4 host_cmd_emit_rx_frame + 0x08001e08 0x28 build/host/host_cmd.o + 0x08001e08 host_cmd_emit_rx_frame .text.host_rfco_pertx_pack - 0x08001ddc 0x62 build/host/host_rfco.o - 0x08001ddc host_rfco_pertx_pack - *fill* 0x08001e3e 0x2 + 0x08001e30 0x62 build/host/host_rfco.o + 0x08001e30 host_rfco_pertx_pack + *fill* 0x08001e92 0x2 .text.host_rfco_blocked_attempts_record - 0x08001e40 0x2c build/host/host_rfco.o - 0x08001e40 host_rfco_blocked_attempts_record + 0x08001e94 0x2c build/host/host_rfco.o + 0x08001e94 host_rfco_blocked_attempts_record .text.host_rfco_blocked_attempts_snapshot_and_clear - 0x08001e6c 0x24 build/host/host_rfco.o - 0x08001e6c host_rfco_blocked_attempts_snapshot_and_clear + 0x08001ec0 0x24 build/host/host_rfco.o + 0x08001ec0 host_rfco_blocked_attempts_snapshot_and_clear .text.host_rfco_pertx_window_record - 0x08001e90 0x24 build/host/host_rfco.o - 0x08001e90 host_rfco_pertx_window_record + 0x08001ee4 0x24 build/host/host_rfco.o + 0x08001ee4 host_rfco_pertx_window_record .text.host_rfco_pertx_emit - 0x08001eb4 0x4a build/host/host_rfco.o - 0x08001eb4 host_rfco_pertx_emit - *fill* 0x08001efe 0x2 + 0x08001f08 0x4a build/host/host_rfco.o + 0x08001f08 host_rfco_pertx_emit + *fill* 0x08001f52 0x2 .text.host_rfco_pertx_count_in_window_snapshot_and_clear - 0x08001f00 0x18 build/host/host_rfco.o - 0x08001f00 host_rfco_pertx_count_in_window_snapshot_and_clear + 0x08001f54 0x18 build/host/host_rfco.o + 0x08001f54 host_rfco_pertx_count_in_window_snapshot_and_clear .text.host_rfco_last_clamp_reason_snapshot_and_clear - 0x08001f18 0x10 build/host/host_rfco.o - 0x08001f18 host_rfco_last_clamp_reason_snapshot_and_clear + 0x08001f6c 0x10 build/host/host_rfco.o + 0x08001f6c host_rfco_last_clamp_reason_snapshot_and_clear .text.host_rfco_summary_pack - 0x08001f28 0xe8 build/host/host_rfco_summary.o - 0x08001f28 host_rfco_summary_pack + 0x08001f7c 0xe8 build/host/host_rfco_summary.o + 0x08001f7c host_rfco_summary_pack .text.host_rfco_summary_emit - 0x08002010 0xf8 build/host/host_rfco_summary_emit.o - 0x08002010 host_rfco_summary_emit + 0x08002064 0xf8 build/host/host_rfco_summary_emit.o + 0x08002064 host_rfco_summary_emit .text.host_rfco_summary_tick - 0x08002108 0x3c build/host/host_rfco_summary_emit.o - 0x08002108 host_rfco_summary_tick + 0x0800215c 0x3c build/host/host_rfco_summary_emit.o + 0x0800215c host_rfco_summary_tick .text.put_u32_le - 0x08002144 0x10 build/host/host_stats.o + 0x08002198 0x10 build/host/host_stats.o .text.host_stats_reset - 0x08002154 0xcc build/host/host_stats.o - 0x08002154 host_stats_reset + 0x080021a8 0xcc build/host/host_stats.o + 0x080021a8 host_stats_reset .text.host_stats_tx_fifo_rb_ok - 0x08002220 0x10 build/host/host_stats.o - 0x08002220 host_stats_tx_fifo_rb_ok + 0x08002274 0x10 build/host/host_stats.o + 0x08002274 host_stats_tx_fifo_rb_ok .text.host_stats_tx_fifo_rb_bad - 0x08002230 0x10 build/host/host_stats.o - 0x08002230 host_stats_tx_fifo_rb_bad + 0x08002284 0x10 build/host/host_stats.o + 0x08002284 host_stats_tx_fifo_rb_bad .text.host_stats_tx_done_early - 0x08002240 0x10 build/host/host_stats.o - 0x08002240 host_stats_tx_done_early + 0x08002294 0x10 build/host/host_stats.o + 0x08002294 host_stats_tx_done_early .text.host_stats_radio_rx_ok - 0x08002250 0x10 build/host/host_stats.o - 0x08002250 host_stats_radio_rx_ok + 0x080022a4 0x10 build/host/host_stats.o + 0x080022a4 host_stats_radio_rx_ok .text.host_stats_radio_crc_err - 0x08002260 0x10 build/host/host_stats.o - 0x08002260 host_stats_radio_crc_err + 0x080022b4 0x10 build/host/host_stats.o + 0x080022b4 host_stats_radio_crc_err .text.host_stats_radio_tx_ok - 0x08002270 0x10 build/host/host_stats.o - 0x08002270 host_stats_radio_tx_ok + 0x080022c4 0x10 build/host/host_stats.o + 0x080022c4 host_stats_radio_tx_ok .text.host_stats_radio_tx_abort_lbt - 0x08002280 0x10 build/host/host_stats.o - 0x08002280 host_stats_radio_tx_abort_lbt + 0x080022d4 0x10 build/host/host_stats.o + 0x080022d4 host_stats_radio_tx_abort_lbt .text.host_stats_radio_tx_abort_airtime - 0x08002290 0x10 build/host/host_stats.o - 0x08002290 host_stats_radio_tx_abort_airtime + 0x080022e4 0x10 build/host/host_stats.o + 0x080022e4 host_stats_radio_tx_abort_airtime .text.host_stats_rx_urc_lost_add - 0x080022a0 0x18 build/host/host_stats.o - 0x080022a0 host_stats_rx_urc_lost_add + 0x080022f4 0x18 build/host/host_stats.o + 0x080022f4 host_stats_rx_urc_lost_add .text.host_stats_rx_pretx_drained_add - 0x080022b8 0x18 build/host/host_stats.o - 0x080022b8 host_stats_rx_pretx_drained_add + 0x0800230c 0x18 build/host/host_stats.o + 0x0800230c host_stats_rx_pretx_drained_add .text.host_stats_note_radio_events - 0x080022d0 0x44 build/host/host_stats.o - 0x080022d0 host_stats_note_radio_events + 0x08002324 0x44 build/host/host_stats.o + 0x08002324 host_stats_note_radio_events .text.host_stats_rx_fifo_skip - 0x08002314 0x14 build/host/host_stats.o - 0x08002314 host_stats_rx_fifo_skip + 0x08002368 0x14 build/host/host_stats.o + 0x08002368 host_stats_rx_fifo_skip .text.host_stats_tx_deaf_note - 0x08002328 0x3c build/host/host_stats.o - 0x08002328 host_stats_tx_deaf_note + 0x0800237c 0x3c build/host/host_stats.o + 0x0800237c host_stats_tx_deaf_note .text.host_stats_fhss_dec_note - 0x08002364 0x18 build/host/host_stats.o - 0x08002364 host_stats_fhss_dec_note + 0x080023b8 0x18 build/host/host_stats.o + 0x080023b8 host_stats_fhss_dec_note .text.host_stats_clk_demotion_note - 0x0800237c 0x1c build/host/host_stats.o - 0x0800237c host_stats_clk_demotion_note + 0x080023d0 0x1c build/host/host_stats.o + 0x080023d0 host_stats_clk_demotion_note .text.host_stats_tx_first_anchor - 0x08002398 0x14 build/host/host_stats.o - 0x08002398 host_stats_tx_first_anchor + 0x080023ec 0x14 build/host/host_stats.o + 0x080023ec host_stats_tx_first_anchor .text.host_stats_tx_stream_streak_note - 0x080023ac 0x10 build/host/host_stats.o - 0x080023ac host_stats_tx_stream_streak_note + 0x08002400 0x10 build/host/host_stats.o + 0x08002400 host_stats_tx_stream_streak_note .text.host_stats_serialize - 0x080023bc 0x2e8 build/host/host_stats.o - 0x080023bc host_stats_serialize + 0x08002410 0x2e8 build/host/host_stats.o + 0x08002410 host_stats_serialize .text.host_rx_urc_lost_eval - 0x080026a4 0x12 build/host/host_rx_urc_lost.o - 0x080026a4 host_rx_urc_lost_eval - *fill* 0x080026b6 0x2 + 0x080026f8 0x12 build/host/host_rx_urc_lost.o + 0x080026f8 host_rx_urc_lost_eval + *fill* 0x0800270a 0x2 .text.host_uart_note_trace - 0x080026b8 0x10 build/host/host_uart.o + 0x0800270c 0x10 build/host/host_uart.o .text.crc16_ccitt - 0x080026c8 0x3c build/host/host_uart.o + 0x0800271c 0x3c build/host/host_uart.o .text.usart2_write_byte - 0x08002704 0x18 build/host/host_uart.o + 0x08002758 0x18 build/host/host_uart.o .text.usart2_write_bytes - 0x0800271c 0x24 build/host/host_uart.o + 0x08002770 0x24 build/host/host_uart.o .text.host_uart_send_urc.part.0 - 0x08002740 0x120 build/host/host_uart.o + 0x08002794 0x120 build/host/host_uart.o .text.process_encoded_frame - 0x08002860 0x1f0 build/host/host_uart.o + 0x080028b4 0x1f0 build/host/host_uart.o .text.ingest_binary_byte - 0x08002a50 0x6c build/host/host_uart.o + 0x08002aa4 0x6c build/host/host_uart.o .text.gpio_set_alt.constprop.0 - 0x08002abc 0x70 build/host/host_uart.o + 0x08002b10 0x70 build/host/host_uart.o .text.rx_ring_push.isra.0 - 0x08002b2c 0x40 build/host/host_uart.o + 0x08002b80 0x40 build/host/host_uart.o .text.at_replay_candidate_to_binary - 0x08002b6c 0x30 build/host/host_uart.o + 0x08002bc0 0x30 build/host/host_uart.o .text.host_uart_poll_dma - 0x08002b9c 0x164 build/host/host_uart.o - 0x08002b9c host_uart_poll_dma + 0x08002bf0 0x164 build/host/host_uart.o + 0x08002bf0 host_uart_poll_dma .text.host_uart_service_rx - 0x08002d00 0x13c build/host/host_uart.o - 0x08002d00 host_uart_service_rx + 0x08002d54 0x13c build/host/host_uart.o + 0x08002d54 host_uart_service_rx .text.host_uart_flush_diag_traces - 0x08002e3c 0xd4 build/host/host_uart.o - 0x08002e3c host_uart_flush_diag_traces + 0x08002e90 0xd4 build/host/host_uart.o + 0x08002e90 host_uart_flush_diag_traces .text.host_uart_pop_frame - 0x08002f10 0x54 build/host/host_uart.o - 0x08002f10 host_uart_pop_frame + 0x08002f64 0x54 build/host/host_uart.o + 0x08002f64 host_uart_pop_frame .text.host_uart_send_urc - 0x08002f64 0x2c build/host/host_uart.o - 0x08002f64 host_uart_send_urc + 0x08002fb8 0x2c build/host/host_uart.o + 0x08002fb8 host_uart_send_urc .text.host_uart_send_ascii - 0x08002f90 0x24 build/host/host_uart.o - 0x08002f90 host_uart_send_ascii + 0x08002fe4 0x24 build/host/host_uart.o + 0x08002fe4 host_uart_send_ascii .text.host_uart_send_err_proto - 0x08002fb4 0x2a build/host/host_uart.o - 0x08002fb4 host_uart_send_err_proto - *fill* 0x08002fde 0x2 + 0x08003008 0x2a build/host/host_uart.o + 0x08003008 host_uart_send_err_proto + *fill* 0x08003032 0x2 .text.host_uart_stats_reset - 0x08002fe0 0x100 build/host/host_uart.o - 0x08002fe0 host_uart_stats_reset + 0x08003034 0x100 build/host/host_uart.o + 0x08003034 host_uart_stats_reset .text.host_uart_init - 0x080030e0 0x198 build/host/host_uart.o - 0x080030e0 host_uart_init + 0x08003134 0x198 build/host/host_uart.o + 0x08003134 host_uart_init .text.host_uart_stats_dropped - 0x08003278 0xc build/host/host_uart.o - 0x08003278 host_uart_stats_dropped + 0x080032cc 0xc build/host/host_uart.o + 0x080032cc host_uart_stats_dropped .text.host_uart_stats_errors - 0x08003284 0xc build/host/host_uart.o - 0x08003284 host_uart_stats_errors + 0x080032d8 0xc build/host/host_uart.o + 0x080032d8 host_uart_stats_errors .text.host_uart_stats_queue_full - 0x08003290 0xc build/host/host_uart.o - 0x08003290 host_uart_stats_queue_full + 0x080032e4 0xc build/host/host_uart.o + 0x080032e4 host_uart_stats_queue_full .text.host_uart_stats_irq_idle - 0x0800329c 0xc build/host/host_uart.o - 0x0800329c host_uart_stats_irq_idle + 0x080032f0 0xc build/host/host_uart.o + 0x080032f0 host_uart_stats_irq_idle .text.host_uart_stats_irq_ht - 0x080032a8 0xc build/host/host_uart.o - 0x080032a8 host_uart_stats_irq_ht + 0x080032fc 0xc build/host/host_uart.o + 0x080032fc host_uart_stats_irq_ht .text.host_uart_stats_irq_tc - 0x080032b4 0xc build/host/host_uart.o - 0x080032b4 host_uart_stats_irq_tc + 0x08003308 0xc build/host/host_uart.o + 0x08003308 host_uart_stats_irq_tc .text.host_uart_stats_irq_te - 0x080032c0 0xc build/host/host_uart.o - 0x080032c0 host_uart_stats_irq_te + 0x08003314 0xc build/host/host_uart.o + 0x08003314 host_uart_stats_irq_te .text.host_uart_stats_rx_bytes - 0x080032cc 0xc build/host/host_uart.o - 0x080032cc host_uart_stats_rx_bytes + 0x08003320 0xc build/host/host_uart.o + 0x08003320 host_uart_stats_rx_bytes .text.host_uart_stats_rx_lpuart_bytes - 0x080032d8 0xc build/host/host_uart.o - 0x080032d8 host_uart_stats_rx_lpuart_bytes + 0x0800332c 0xc build/host/host_uart.o + 0x0800332c host_uart_stats_rx_lpuart_bytes .text.host_uart_stats_rx_usart1_bytes - 0x080032e4 0xc build/host/host_uart.o - 0x080032e4 host_uart_stats_rx_usart1_bytes + 0x08003338 0xc build/host/host_uart.o + 0x08003338 host_uart_stats_rx_usart1_bytes .text.host_uart_stats_parse_ok - 0x080032f0 0xc build/host/host_uart.o - 0x080032f0 host_uart_stats_parse_ok + 0x08003344 0xc build/host/host_uart.o + 0x08003344 host_uart_stats_parse_ok .text.host_uart_stats_parse_err - 0x080032fc 0xc build/host/host_uart.o - 0x080032fc host_uart_stats_parse_err + 0x08003350 0xc build/host/host_uart.o + 0x08003350 host_uart_stats_parse_err .text.host_uart_stats_uart_err_lpuart - 0x08003308 0xc build/host/host_uart.o - 0x08003308 host_uart_stats_uart_err_lpuart + 0x0800335c 0xc build/host/host_uart.o + 0x0800335c host_uart_stats_uart_err_lpuart .text.host_uart_stats_uart_err_usart1 - 0x08003314 0xc build/host/host_uart.o - 0x08003314 host_uart_stats_uart_err_usart1 + 0x08003368 0xc build/host/host_uart.o + 0x08003368 host_uart_stats_uart_err_usart1 .text.host_uart_stats_uart_pe_lpuart - 0x08003320 0xc build/host/host_uart.o - 0x08003320 host_uart_stats_uart_pe_lpuart + 0x08003374 0xc build/host/host_uart.o + 0x08003374 host_uart_stats_uart_pe_lpuart .text.host_uart_stats_uart_fe_lpuart - 0x0800332c 0xc build/host/host_uart.o - 0x0800332c host_uart_stats_uart_fe_lpuart + 0x08003380 0xc build/host/host_uart.o + 0x08003380 host_uart_stats_uart_fe_lpuart .text.host_uart_stats_uart_ne_lpuart - 0x08003338 0xc build/host/host_uart.o - 0x08003338 host_uart_stats_uart_ne_lpuart + 0x0800338c 0xc build/host/host_uart.o + 0x0800338c host_uart_stats_uart_ne_lpuart .text.host_uart_stats_uart_ore_lpuart - 0x08003344 0xc build/host/host_uart.o - 0x08003344 host_uart_stats_uart_ore_lpuart + 0x08003398 0xc build/host/host_uart.o + 0x08003398 host_uart_stats_uart_ore_lpuart .text.host_uart_stats_uart_pe_usart1 - 0x08003350 0xc build/host/host_uart.o - 0x08003350 host_uart_stats_uart_pe_usart1 + 0x080033a4 0xc build/host/host_uart.o + 0x080033a4 host_uart_stats_uart_pe_usart1 .text.host_uart_stats_uart_fe_usart1 - 0x0800335c 0xc build/host/host_uart.o - 0x0800335c host_uart_stats_uart_fe_usart1 + 0x080033b0 0xc build/host/host_uart.o + 0x080033b0 host_uart_stats_uart_fe_usart1 .text.host_uart_stats_uart_ne_usart1 - 0x08003368 0xc build/host/host_uart.o - 0x08003368 host_uart_stats_uart_ne_usart1 + 0x080033bc 0xc build/host/host_uart.o + 0x080033bc host_uart_stats_uart_ne_usart1 .text.host_uart_stats_uart_ore_usart1 - 0x08003374 0xc build/host/host_uart.o - 0x08003374 host_uart_stats_uart_ore_usart1 + 0x080033c8 0xc build/host/host_uart.o + 0x080033c8 host_uart_stats_uart_ore_usart1 .text.host_uart_stats_rx_ring_ovf - 0x08003380 0xc build/host/host_uart.o - 0x08003380 host_uart_stats_rx_ring_ovf + 0x080033d4 0xc build/host/host_uart.o + 0x080033d4 host_uart_stats_rx_ring_ovf .text.host_uart_take_dma_te_events - 0x0800338c 0x18 build/host/host_uart.o - 0x0800338c host_uart_take_dma_te_events + 0x080033e0 0x18 build/host/host_uart.o + 0x080033e0 host_uart_take_dma_te_events .text.host_uart_take_rx_seen_flags - 0x080033a4 0x1c build/host/host_uart.o - 0x080033a4 host_uart_take_rx_seen_flags + 0x080033f8 0x1c build/host/host_uart.o + 0x080033f8 host_uart_take_rx_seen_flags .text.host_uart_note_diag_mark - 0x080033c0 0x1c build/host/host_uart.o - 0x080033c0 host_uart_note_diag_mark + 0x08003414 0x1c build/host/host_uart.o + 0x08003414 host_uart_note_diag_mark .text.host_uart_take_diag_marks - 0x080033dc 0x1c build/host/host_uart.o - 0x080033dc host_uart_take_diag_marks + 0x08003430 0x1c build/host/host_uart.o + 0x08003430 host_uart_take_diag_marks .text.RNG_LPUART1_IRQHandler - 0x080033f8 0xe0 build/host/host_uart.o - 0x080033f8 RNG_LPUART1_IRQHandler + 0x0800344c 0xe0 build/host/host_uart.o + 0x0800344c RNG_LPUART1_IRQHandler .text.USART1_IRQHandler - 0x080034d8 0xe0 build/host/host_uart.o - 0x080034d8 USART1_IRQHandler + 0x0800352c 0xe0 build/host/host_uart.o + 0x0800352c USART1_IRQHandler .text.sx1276_airtime_set_budget_us - 0x080035b8 0x24 build/radio/sx1276_airtime.o - 0x080035b8 sx1276_airtime_set_budget_us + 0x0800360c 0x24 build/radio/sx1276_airtime.o + 0x0800360c sx1276_airtime_set_budget_us .text.sx1276_airtime_compute_toa_us - 0x080035dc 0xd0 build/radio/sx1276_airtime.o - 0x080035dc sx1276_airtime_compute_toa_us + 0x08003630 0xd0 build/radio/sx1276_airtime.o + 0x08003630 sx1276_airtime_compute_toa_us .text.sx1276_airtime_config_invariant_ok - 0x080036ac 0x48 build/radio/sx1276_airtime.o - 0x080036ac sx1276_airtime_config_invariant_ok + 0x08003700 0x48 build/radio/sx1276_airtime.o + 0x08003700 sx1276_airtime_config_invariant_ok .text.sx1276_airtime_estimate_toa_us - 0x080036f4 0x94 build/radio/sx1276_airtime.o - 0x080036f4 sx1276_airtime_estimate_toa_us + 0x08003748 0x94 build/radio/sx1276_airtime.o + 0x08003748 sx1276_airtime_estimate_toa_us .text.sx1276_airtime_reserve - 0x08003788 0x58 build/radio/sx1276_airtime.o - 0x08003788 sx1276_airtime_reserve + 0x080037dc 0x58 build/radio/sx1276_airtime.o + 0x080037dc sx1276_airtime_reserve .text.sx1276_airtime_release - 0x080037e0 0x20 build/radio/sx1276_airtime.o - 0x080037e0 sx1276_airtime_release + 0x08003834 0x20 build/radio/sx1276_airtime.o + 0x08003834 sx1276_airtime_release .text.sx1276_airtime_commit - 0x08003800 0x44 build/radio/sx1276_airtime.o - 0x08003800 sx1276_airtime_commit + 0x08003854 0x44 build/radio/sx1276_airtime.o + 0x08003854 sx1276_airtime_commit .text.sx1276_cad_begin - 0x08003844 0x28 build/radio/sx1276_cad.o - 0x08003844 sx1276_cad_begin + 0x08003898 0x28 build/radio/sx1276_cad.o + 0x08003898 sx1276_cad_begin .text.sx1276_cad_abort - 0x0800386c 0x20 build/radio/sx1276_cad.o - 0x0800386c sx1276_cad_abort + 0x080038c0 0x20 build/radio/sx1276_cad.o + 0x080038c0 sx1276_cad_abort .text.sx1276_cad_poll - 0x0800388c 0x40 build/radio/sx1276_cad.o - 0x0800388c sx1276_cad_poll + 0x080038e0 0x40 build/radio/sx1276_cad.o + 0x080038e0 sx1276_cad_poll .text.lora_pkt_hdr_pack - 0x080038cc 0x30 build/radio/lora_pkt_hdr.o - 0x080038cc lora_pkt_hdr_pack + 0x08003920 0x30 build/radio/lora_pkt_hdr.o + 0x08003920 lora_pkt_hdr_pack .text.lora_pkt_hdr_unpack - 0x080038fc 0x40 build/radio/lora_pkt_hdr.o - 0x080038fc lora_pkt_hdr_unpack + 0x08003950 0x40 build/radio/lora_pkt_hdr.o + 0x08003950 lora_pkt_hdr_unpack .text.sx1276_fhss_chantab_center_hz - 0x0800393c 0x1c build/radio/sx1276_fhss_chantab.o - 0x0800393c sx1276_fhss_chantab_center_hz + 0x08003990 0x1c build/radio/sx1276_fhss_chantab.o + 0x08003990 sx1276_fhss_chantab_center_hz .text.sx1276_fhss_compute_seed - 0x08003958 0x54 build/radio/sx1276_fhss.o - 0x08003958 sx1276_fhss_compute_seed + 0x080039ac 0x54 build/radio/sx1276_fhss.o + 0x080039ac sx1276_fhss_compute_seed .text.sx1276_fhss_compute_permutation - 0x080039ac 0x4c build/radio/sx1276_fhss.o - 0x080039ac sx1276_fhss_compute_permutation + 0x08003a00 0x4c build/radio/sx1276_fhss.o + 0x08003a00 sx1276_fhss_compute_permutation .text.rebuild_epoch_permutation - 0x080039f8 0x2c build/radio/sx1276_fhss.o + 0x08003a4c 0x2c build/radio/sx1276_fhss.o .text.sx1276_fhss_reset - 0x08003a24 0x14 build/radio/sx1276_fhss.o - 0x08003a24 sx1276_fhss_reset + 0x08003a78 0x14 build/radio/sx1276_fhss.o + 0x08003a78 sx1276_fhss_reset .text.sx1276_fhss_init - 0x08003a38 0x40 build/radio/sx1276_fhss.o - 0x08003a38 sx1276_fhss_init + 0x08003a8c 0x40 build/radio/sx1276_fhss.o + 0x08003a8c sx1276_fhss_init .text.sx1276_fhss_next_channel - 0x08003a78 0x90 build/radio/sx1276_fhss.o - 0x08003a78 sx1276_fhss_next_channel + 0x08003acc 0x90 build/radio/sx1276_fhss.o + 0x08003acc sx1276_fhss_next_channel .text.sx1276_fhss_snap_to - 0x08003b08 0x34 build/radio/sx1276_fhss.o - 0x08003b08 sx1276_fhss_snap_to + 0x08003b5c 0x34 build/radio/sx1276_fhss.o + 0x08003b5c sx1276_fhss_snap_to .text.sx1276_fhss_consider_remote - 0x08003b3c 0x60 build/radio/sx1276_fhss.o - 0x08003b3c sx1276_fhss_consider_remote + 0x08003b90 0x60 build/radio/sx1276_fhss.o + 0x08003b90 sx1276_fhss_consider_remote .text.sx1276_fhss_active_count - 0x08003b9c 0x40 build/radio/sx1276_fhss.o - 0x08003b9c sx1276_fhss_active_count + 0x08003bf0 0x40 build/radio/sx1276_fhss.o + 0x08003bf0 sx1276_fhss_active_count .text.sx1276_fhss_record_lbt_block - 0x08003bdc 0x1c build/radio/sx1276_fhss.o - 0x08003bdc sx1276_fhss_record_lbt_block + 0x08003c30 0x1c build/radio/sx1276_fhss.o + 0x08003c30 sx1276_fhss_record_lbt_block .text.sx1276_fhss_record_hop - 0x08003bf8 0x20 build/radio/sx1276_fhss.o - 0x08003bf8 sx1276_fhss_record_hop + 0x08003c4c 0x20 build/radio/sx1276_fhss.o + 0x08003c4c sx1276_fhss_record_hop .text.sx1276_fhss_hop_count_snapshot_and_clear - 0x08003c18 0x2c build/radio/sx1276_fhss.o - 0x08003c18 sx1276_fhss_hop_count_snapshot_and_clear + 0x08003c6c 0x2c build/radio/sx1276_fhss.o + 0x08003c6c sx1276_fhss_hop_count_snapshot_and_clear .text.sx1276_fhss_current_epoch - 0x08003c44 0xc build/radio/sx1276_fhss.o - 0x08003c44 sx1276_fhss_current_epoch + 0x08003c98 0xc build/radio/sx1276_fhss.o + 0x08003c98 sx1276_fhss_current_epoch .text.sx1276_fhss_current_slot - 0x08003c50 0xc build/radio/sx1276_fhss.o - 0x08003c50 sx1276_fhss_current_slot + 0x08003ca4 0xc build/radio/sx1276_fhss.o + 0x08003ca4 sx1276_fhss_current_slot .text.sx1276_fhss_is_initialized - 0x08003c5c 0xc build/radio/sx1276_fhss.o - 0x08003c5c sx1276_fhss_is_initialized + 0x08003cb0 0xc build/radio/sx1276_fhss.o + 0x08003cb0 sx1276_fhss_is_initialized .text.sx1276_fhss_clock_reset - 0x08003c68 0x10 build/radio/sx1276_fhss_clock.o - 0x08003c68 sx1276_fhss_clock_reset + 0x08003cbc 0x10 build/radio/sx1276_fhss_clock.o + 0x08003cbc sx1276_fhss_clock_reset .text.sx1276_fhss_clock_anchor - 0x08003c78 0x10 build/radio/sx1276_fhss_clock.o - 0x08003c78 sx1276_fhss_clock_anchor + 0x08003ccc 0x10 build/radio/sx1276_fhss_clock.o + 0x08003ccc sx1276_fhss_clock_anchor .text.sx1276_fhss_clock_valid - 0x08003c88 0xc build/radio/sx1276_fhss_clock.o - 0x08003c88 sx1276_fhss_clock_valid + 0x08003cdc 0xc build/radio/sx1276_fhss_clock.o + 0x08003cdc sx1276_fhss_clock_valid .text.sx1276_fhss_clock_abs_slot - 0x08003c94 0x18 build/radio/sx1276_fhss_clock.o - 0x08003c94 sx1276_fhss_clock_abs_slot + 0x08003ce8 0x18 build/radio/sx1276_fhss_clock.o + 0x08003ce8 sx1276_fhss_clock_abs_slot .text.sx1276_fhss_clock_in_slot_ms - 0x08003cac 0x18 build/radio/sx1276_fhss_clock.o - 0x08003cac sx1276_fhss_clock_in_slot_ms + 0x08003d00 0x18 build/radio/sx1276_fhss_clock.o + 0x08003d00 sx1276_fhss_clock_in_slot_ms .text.sx1276_fhss_clock_anchor_rx - 0x08003cc4 0x28 build/radio/sx1276_fhss_clock.o - 0x08003cc4 sx1276_fhss_clock_anchor_rx + 0x08003d18 0x28 build/radio/sx1276_fhss_clock.o + 0x08003d18 sx1276_fhss_clock_anchor_rx .text.sx1276_fhss_clock_age_ms - 0x08003cec 0xc build/radio/sx1276_fhss_clock.o - 0x08003cec sx1276_fhss_clock_age_ms + 0x08003d40 0xc build/radio/sx1276_fhss_clock.o + 0x08003d40 sx1276_fhss_clock_age_ms .text.sx1276_fhss_clock_rx_leads - 0x08003cf8 0x2c build/radio/sx1276_fhss_clock.o - 0x08003cf8 sx1276_fhss_clock_rx_leads + 0x08003d4c 0x2c build/radio/sx1276_fhss_clock.o + 0x08003d4c sx1276_fhss_clock_rx_leads .text.sx1276_fhss_authority_reset - 0x08003d24 0x14 build/radio/sx1276_fhss_authority.o - 0x08003d24 sx1276_fhss_authority_reset + 0x08003d78 0x14 build/radio/sx1276_fhss_authority.o + 0x08003d78 sx1276_fhss_authority_reset .text.sx1276_fhss_authority_note_tx - 0x08003d38 0x38 build/radio/sx1276_fhss_authority.o - 0x08003d38 sx1276_fhss_authority_note_tx + 0x08003d8c 0x38 build/radio/sx1276_fhss_authority.o + 0x08003d8c sx1276_fhss_authority_note_tx .text.sx1276_fhss_authority_note_adopt - 0x08003d70 0xc build/radio/sx1276_fhss_authority.o - 0x08003d70 sx1276_fhss_authority_note_adopt + 0x08003dc4 0xc build/radio/sx1276_fhss_authority.o + 0x08003dc4 sx1276_fhss_authority_note_adopt .text.sx1276_fhss_authority_streak - 0x08003d7c 0xc build/radio/sx1276_fhss_authority.o - 0x08003d7c sx1276_fhss_authority_streak + 0x08003dd0 0xc build/radio/sx1276_fhss_authority.o + 0x08003dd0 sx1276_fhss_authority_streak .text.sx1276_fhss_authority_is_originator - 0x08003d88 0x34 build/radio/sx1276_fhss_authority.o - 0x08003d88 sx1276_fhss_authority_is_originator + 0x08003ddc 0x34 build/radio/sx1276_fhss_authority.o + 0x08003ddc sx1276_fhss_authority_is_originator .text.sx1276_rx_grid_reset - 0x08003dbc 0x18 build/radio/sx1276_rx_grid_policy.o - 0x08003dbc sx1276_rx_grid_reset + 0x08003e10 0x18 build/radio/sx1276_rx_grid_policy.o + 0x08003e10 sx1276_rx_grid_reset .text.sx1276_rx_grid_consider - 0x08003dd4 0x84 build/radio/sx1276_rx_grid_policy.o - 0x08003dd4 sx1276_rx_grid_consider + 0x08003e28 0x84 build/radio/sx1276_rx_grid_policy.o + 0x08003e28 sx1276_rx_grid_consider .text.sx1276_rx_grid_adopt - 0x08003e58 0x18 build/radio/sx1276_rx_grid_policy.o - 0x08003e58 sx1276_rx_grid_adopt + 0x08003eac 0x18 build/radio/sx1276_rx_grid_policy.o + 0x08003eac sx1276_rx_grid_adopt .text.sx1276_rx_grid_on_demotion - 0x08003e70 0x20 build/radio/sx1276_rx_grid_policy.o - 0x08003e70 sx1276_rx_grid_on_demotion + 0x08003ec4 0x20 build/radio/sx1276_rx_grid_policy.o + 0x08003ec4 sx1276_rx_grid_on_demotion .text.sx1276_rx_grid_follow_own - 0x08003e90 0x1c build/radio/sx1276_rx_grid_policy.o - 0x08003e90 sx1276_rx_grid_follow_own + 0x08003ee4 0x1c build/radio/sx1276_rx_grid_policy.o + 0x08003ee4 sx1276_rx_grid_follow_own .text.sx1276_rx_grid_follow - 0x08003eac 0x20 build/radio/sx1276_rx_grid_policy.o - 0x08003eac sx1276_rx_grid_follow + 0x08003f00 0x20 build/radio/sx1276_rx_grid_policy.o + 0x08003f00 sx1276_rx_grid_follow .text.lbt_arm_backoff - 0x08003ecc 0x38 build/radio/sx1276_lbt.o + 0x08003f20 0x38 build/radio/sx1276_lbt.o .text.cfg_read_u8 - 0x08003f04 0x30 build/radio/sx1276_lbt.o + 0x08003f58 0x30 build/radio/sx1276_lbt.o .text.sx1276_lbt_check_and_backoff - 0x08003f34 0x108 build/radio/sx1276_lbt.o - 0x08003f34 sx1276_lbt_check_and_backoff + 0x08003f88 0x108 build/radio/sx1276_lbt.o + 0x08003f88 sx1276_lbt_check_and_backoff .text.sx1276_legal_dwell_used_us - 0x0800403c 0x50 build/radio/sx1276_legal_dwell.o - 0x0800403c sx1276_legal_dwell_used_us + 0x08004090 0x50 build/radio/sx1276_legal_dwell.o + 0x08004090 sx1276_legal_dwell_used_us .text.sx1276_legal_dwell_reserve - 0x0800408c 0xc4 build/radio/sx1276_legal_dwell.o - 0x0800408c sx1276_legal_dwell_reserve + 0x080040e0 0xc4 build/radio/sx1276_legal_dwell.o + 0x080040e0 sx1276_legal_dwell_reserve .text.sx1276_legal_dwell_reconcile - 0x08004150 0x34 build/radio/sx1276_legal_dwell.o - 0x08004150 sx1276_legal_dwell_reconcile + 0x080041a4 0x34 build/radio/sx1276_legal_dwell.o + 0x080041a4 sx1276_legal_dwell_reconcile .text.sx1276_legal_dwell_peak_us_snapshot_and_clear - 0x08004184 0x20 build/radio/sx1276_legal_dwell.o - 0x08004184 sx1276_legal_dwell_peak_us_snapshot_and_clear + 0x080041d8 0x20 build/radio/sx1276_legal_dwell.o + 0x080041d8 sx1276_legal_dwell_peak_us_snapshot_and_clear .text.mode_desc_for_state - 0x080041a4 0x30 build/radio/sx1276_modes.o + 0x080041f8 0x30 build/radio/sx1276_modes.o .text.sx1276_delay_us - 0x080041d4 0x16 build/radio/sx1276_modes.o - *fill* 0x080041ea 0x2 + 0x08004228 0x16 build/radio/sx1276_modes.o + *fill* 0x0800423e 0x2 .text.sx1276_modes_apply - 0x080041ec 0xb8 build/radio/sx1276_modes.o + 0x08004240 0xb8 build/radio/sx1276_modes.o .text.sx1276_modes_get_state - 0x080042a4 0xc build/radio/sx1276_modes.o - 0x080042a4 sx1276_modes_get_state + 0x080042f8 0xc build/radio/sx1276_modes.o + 0x080042f8 sx1276_modes_get_state .text.sx1276_modes_sync_external - 0x080042b0 0x34 build/radio/sx1276_modes.o - 0x080042b0 sx1276_modes_sync_external + 0x08004304 0x34 build/radio/sx1276_modes.o + 0x08004304 sx1276_modes_sync_external .text.sx1276_modes_to_sleep - 0x080042e4 0xe build/radio/sx1276_modes.o - 0x080042e4 sx1276_modes_to_sleep + 0x08004338 0xe build/radio/sx1276_modes.o + 0x08004338 sx1276_modes_to_sleep .text.sx1276_modes_to_standby - 0x080042f2 0xe build/radio/sx1276_modes.o - 0x080042f2 sx1276_modes_to_standby + 0x08004346 0xe build/radio/sx1276_modes.o + 0x08004346 sx1276_modes_to_standby .text.sx1276_modes_to_tx - 0x08004300 0xe build/radio/sx1276_modes.o - 0x08004300 sx1276_modes_to_tx + 0x08004354 0xe build/radio/sx1276_modes.o + 0x08004354 sx1276_modes_to_tx .text.sx1276_modes_to_rx_cont - 0x0800430e 0xe build/radio/sx1276_modes.o - 0x0800430e sx1276_modes_to_rx_cont + 0x08004362 0xe build/radio/sx1276_modes.o + 0x08004362 sx1276_modes_to_rx_cont .text.sx1276_modes_to_cad - 0x0800431c 0x16 build/radio/sx1276_modes.o - 0x0800431c sx1276_modes_to_cad - *fill* 0x08004332 0x2 + 0x08004370 0x16 build/radio/sx1276_modes.o + 0x08004370 sx1276_modes_to_cad + *fill* 0x08004386 0x2 .text.sx1276_modes_init - 0x08004334 0x48 build/radio/sx1276_modes.o - 0x08004334 sx1276_modes_init + 0x08004388 0x48 build/radio/sx1276_modes.o + 0x08004388 sx1276_modes_init + .text.rx_follow_own_now + 0x080043d0 0xc build/radio/sx1276_rx.o .text.rx_pll_settle_busy_wait.constprop.0 - 0x0800437c 0x18 build/radio/sx1276_rx.o + 0x080043dc 0x18 build/radio/sx1276_rx.o .text.sx1276_rx_arm - 0x08004394 0x34 build/radio/sx1276_rx.o - 0x08004394 sx1276_rx_arm + 0x080043f4 0x40 build/radio/sx1276_rx.o + 0x080043f4 sx1276_rx_arm .text.scan_dispatch_action - 0x080043c8 0xe8 build/radio/sx1276_rx.o + 0x08004434 0xc4 build/radio/sx1276_rx.o + .text.scan_leave_failed + 0x080044f8 0x30 build/radio/sx1276_rx.o .text.scan_drive - 0x080044b0 0xb4 build/radio/sx1276_rx.o + 0x08004528 0xe0 build/radio/sx1276_rx.o .text.scan_feed_frame - 0x08004564 0x18 build/radio/sx1276_rx.o + 0x08004608 0x18 build/radio/sx1276_rx.o .text.sx1276_rx_disarm - 0x0800457c 0x8 build/radio/sx1276_rx.o - 0x0800457c sx1276_rx_disarm - .text.sx1276_rx_note_external_arm - 0x08004584 0x10 build/radio/sx1276_rx.o - 0x08004584 sx1276_rx_note_external_arm + 0x08004620 0x8 build/radio/sx1276_rx.o + 0x08004620 sx1276_rx_disarm .text.sx1276_rx_service - 0x08004594 0x214 build/radio/sx1276_rx.o - 0x08004594 sx1276_rx_service + 0x08004628 0x220 build/radio/sx1276_rx.o + 0x08004628 sx1276_rx_service .text.sx1276_rx_tick - 0x080047a8 0x8c build/radio/sx1276_rx.o - 0x080047a8 sx1276_rx_tick + 0x08004848 0x98 build/radio/sx1276_rx.o + 0x08004848 sx1276_rx_tick .text.sx1276_rx_scan_tick - 0x08004834 0xe build/radio/sx1276_rx.o - 0x08004834 sx1276_rx_scan_tick - *fill* 0x08004842 0x2 + 0x080048e0 0xe build/radio/sx1276_rx.o + 0x080048e0 sx1276_rx_scan_tick + *fill* 0x080048ee 0x2 .text.sx1276_rx_slot_follow - 0x08004844 0x9c build/radio/sx1276_rx.o - 0x08004844 sx1276_rx_slot_follow + 0x080048f0 0xa8 build/radio/sx1276_rx.o + 0x080048f0 sx1276_rx_slot_follow .text.sx1276_rx_scan_reset - 0x080048e0 0x3c build/radio/sx1276_rx.o - 0x080048e0 sx1276_rx_scan_reset + 0x08004998 0x3c build/radio/sx1276_rx.o + 0x08004998 sx1276_rx_scan_reset + .text.sx1276_rx_park + 0x080049d4 0x34 build/radio/sx1276_rx.o + 0x080049d4 sx1276_rx_park + .text.sx1276_rx_unpark + 0x08004a08 0x48 build/radio/sx1276_rx.o + 0x08004a08 sx1276_rx_unpark + .text.sx1276_rx_note_external_arm + 0x08004a50 0x14 build/radio/sx1276_rx.o + 0x08004a50 sx1276_rx_note_external_arm + .text.sx1276_rx_parked + 0x08004a64 0x10 build/radio/sx1276_rx.o + 0x08004a64 sx1276_rx_parked .text.sx1276_rx_fifo_track_reset - 0x0800491c 0xc build/radio/sx1276_rx_fifo_track.o - 0x0800491c sx1276_rx_fifo_track_reset + 0x08004a74 0xc build/radio/sx1276_rx_fifo_track.o + 0x08004a74 sx1276_rx_fifo_track_reset .text.sx1276_rx_fifo_track_note - 0x08004928 0x22 build/radio/sx1276_rx_fifo_track.o - 0x08004928 sx1276_rx_fifo_track_note - *fill* 0x0800494a 0x2 + 0x08004a80 0x22 build/radio/sx1276_rx_fifo_track.o + 0x08004a80 sx1276_rx_fifo_track_note + *fill* 0x08004aa2 0x2 .text.sx1276_rx_counter_record - 0x0800494c 0x18 build/radio/sx1276_rx_counters.o - 0x0800494c sx1276_rx_counter_record + 0x08004aa4 0x18 build/radio/sx1276_rx_counters.o + 0x08004aa4 sx1276_rx_counter_record .text.sx1276_rx_retune_eval - 0x08004964 0x26 build/radio/sx1276_rx_retune_policy.o - 0x08004964 sx1276_rx_retune_eval - *fill* 0x0800498a 0x2 + 0x08004abc 0x26 build/radio/sx1276_rx_retune_policy.o + 0x08004abc sx1276_rx_retune_eval + *fill* 0x08004ae2 0x2 .text.sx1276_rx_retune_counter_record - 0x0800498c 0x18 build/radio/sx1276_rx_retune_counters.o - 0x0800498c sx1276_rx_retune_counter_record + 0x08004ae4 0x18 build/radio/sx1276_rx_retune_counters.o + 0x08004ae4 sx1276_rx_retune_counter_record .text.sx1276_rx_scan_eval - 0x080049a4 0xac build/radio/sx1276_rx_scan_policy.o - 0x080049a4 sx1276_rx_scan_eval + 0x08004afc 0xac build/radio/sx1276_rx_scan_policy.o + 0x08004afc sx1276_rx_scan_eval .text.sx1276_rx_scan_counter_record - 0x08004a50 0x30 build/radio/sx1276_rx_scan_counters.o - 0x08004a50 sx1276_rx_scan_counter_record + 0x08004ba8 0x30 build/radio/sx1276_rx_scan_counters.o + 0x08004ba8 sx1276_rx_scan_counter_record .text.sx1276_rx_scan_fail_eval - 0x08004a80 0x5e build/radio/sx1276_rx_scan_fail.o - 0x08004a80 sx1276_rx_scan_fail_eval - *fill* 0x08004ade 0x2 + 0x08004bd8 0x5e build/radio/sx1276_rx_scan_fail.o + 0x08004bd8 sx1276_rx_scan_fail_eval + *fill* 0x08004c36 0x2 .text.sx1276_rx_scan_walker_reset - 0x08004ae0 0xc build/radio/sx1276_rx_scan_walker.o - 0x08004ae0 sx1276_rx_scan_walker_reset + 0x08004c38 0xc build/radio/sx1276_rx_scan_walker.o + 0x08004c38 sx1276_rx_scan_walker_reset .text.sx1276_rx_scan_walker_next - 0x08004aec 0x28 build/radio/sx1276_rx_scan_walker.o - 0x08004aec sx1276_rx_scan_walker_next + 0x08004c44 0x28 build/radio/sx1276_rx_scan_walker.o + 0x08004c44 sx1276_rx_scan_walker_next .text.tx_emit_rfco_pertx - 0x08004b14 0x70 build/radio/sx1276_tx.o + 0x08004c6c 0x70 build/radio/sx1276_tx.o .text.sx1276_tx_cleanup - 0x08004b84 0x70 build/radio/sx1276_tx.o + 0x08004cdc 0x70 build/radio/sx1276_tx.o .text.sx1276_tx_begin - 0x08004bf4 0x3b4 build/radio/sx1276_tx.o - 0x08004bf4 sx1276_tx_begin + 0x08004d4c 0x3bc build/radio/sx1276_tx.o + 0x08004d4c sx1276_tx_begin .text.sx1276_tx_poll - 0x08004fa8 0x1a0 build/radio/sx1276_tx.o - 0x08004fa8 sx1276_tx_poll + 0x08005108 0x1a0 build/radio/sx1276_tx.o + 0x08005108 sx1276_tx_poll .text.sx1276_tx_busy - 0x08005148 0x10 build/radio/sx1276_tx.o - 0x08005148 sx1276_tx_busy + 0x080052a8 0x10 build/radio/sx1276_tx.o + 0x080052a8 sx1276_tx_busy .text.sx1276_tx_slot_wait_us - 0x08005158 0x6c build/radio/sx1276_tx.o - 0x08005158 sx1276_tx_slot_wait_us + 0x080052b8 0x6c build/radio/sx1276_tx.o + 0x080052b8 sx1276_tx_slot_wait_us .text.gpio_mode_output - 0x080051c4 0x32 build/radio/sx1276.o + 0x08005324 0x32 build/radio/sx1276.o .text.gpio_mode_input - 0x080051f6 0x14 build/radio/sx1276.o - *fill* 0x0800520a 0x2 + 0x08005356 0x14 build/radio/sx1276.o + *fill* 0x0800536a 0x2 .text.spi1_transfer - 0x0800520c 0x2c build/radio/sx1276.o + 0x0800536c 0x2c build/radio/sx1276.o .text.sx1276_select - 0x08005238 0x18 build/radio/sx1276.o + 0x08005398 0x18 build/radio/sx1276.o .text.sx1276_radio_reset - 0x08005250 0x34 build/radio/sx1276.o - 0x08005250 sx1276_radio_reset + 0x080053b0 0x34 build/radio/sx1276.o + 0x080053b0 sx1276_radio_reset .text.sx1276_read_reg - 0x08005284 0x24 build/radio/sx1276.o - 0x08005284 sx1276_read_reg + 0x080053e4 0x24 build/radio/sx1276.o + 0x080053e4 sx1276_read_reg .text.sx1276_write_reg - 0x080052a8 0x22 build/radio/sx1276.o - 0x080052a8 sx1276_write_reg + 0x08005408 0x22 build/radio/sx1276.o + 0x08005408 sx1276_write_reg .text.sx1276_read_burst - 0x080052ca 0x2e build/radio/sx1276.o - 0x080052ca sx1276_read_burst + 0x0800542a 0x2e build/radio/sx1276.o + 0x0800542a sx1276_read_burst .text.sx1276_write_burst - 0x080052f8 0x2e build/radio/sx1276.o - 0x080052f8 sx1276_write_burst + 0x08005458 0x2e build/radio/sx1276.o + 0x08005458 sx1276_write_burst .text.sx1276_read_version - 0x08005326 0xa build/radio/sx1276.o - 0x08005326 sx1276_read_version + 0x08005486 0xa build/radio/sx1276.o + 0x08005486 sx1276_read_version .text.sx1276_set_frequency_hz - 0x08005330 0x34 build/radio/sx1276.o - 0x08005330 sx1276_set_frequency_hz + 0x08005490 0x34 build/radio/sx1276.o + 0x08005490 sx1276_set_frequency_hz .text.sx1276_set_tx_power_dbm - 0x08005364 0x26 build/radio/sx1276.o - 0x08005364 sx1276_set_tx_power_dbm - *fill* 0x0800538a 0x2 + 0x080054c4 0x26 build/radio/sx1276.o + 0x080054c4 sx1276_set_tx_power_dbm + *fill* 0x080054ea 0x2 .text.sx1276_set_sf_bw_cr_checked - 0x0800538c 0x150 build/radio/sx1276.o - 0x0800538c sx1276_set_sf_bw_cr_checked + 0x080054ec 0x150 build/radio/sx1276.o + 0x080054ec sx1276_set_sf_bw_cr_checked .text.sx1276_set_sf_bw_cr - 0x080054dc 0x8 build/radio/sx1276.o - 0x080054dc sx1276_set_sf_bw_cr + 0x0800563c 0x8 build/radio/sx1276.o + 0x0800563c sx1276_set_sf_bw_cr .text.sx1276_init - 0x080054e4 0x268 build/radio/sx1276.o - 0x080054e4 sx1276_init + 0x08005644 0x268 build/radio/sx1276.o + 0x08005644 sx1276_init .text.sx1276_take_irq_events - 0x0800574c 0x18 build/radio/sx1276.o - 0x0800574c sx1276_take_irq_events + 0x080058ac 0x18 build/radio/sx1276.o + 0x080058ac sx1276_take_irq_events .text.sx1276_take_dio0_edges - 0x08005764 0x18 build/radio/sx1276.o - 0x08005764 sx1276_take_dio0_edges + 0x080058c4 0x18 build/radio/sx1276.o + 0x080058c4 sx1276_take_dio0_edges .text.sx1276_peek_dio0_edges - 0x0800577c 0xc build/radio/sx1276.o - 0x0800577c sx1276_peek_dio0_edges + 0x080058dc 0xc build/radio/sx1276.o + 0x080058dc sx1276_peek_dio0_edges .text.EXTI0_1_IRQHandler - 0x08005788 0x38 build/radio/sx1276.o - 0x08005788 EXTI0_1_IRQHandler + 0x080058e8 0x38 build/radio/sx1276.o + 0x080058e8 EXTI0_1_IRQHandler .text.EXTI4_15_IRQHandler - 0x080057c0 0x48 build/radio/sx1276.o - 0x080057c0 EXTI4_15_IRQHandler + 0x08005920 0x48 build/radio/sx1276.o + 0x08005920 EXTI4_15_IRQHandler .text.__udivmoddi4 - 0x08005808 0x198 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libgcc.a(_udivmoddi4.o) - 0x08005808 __udivmoddi4 - .text.memcmp 0x080059a0 0x1c c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libc_nano.a(libc_a-memcmp.o) - 0x080059a0 memcmp - .text.memset 0x080059bc 0x10 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libc_nano.a(libc_a-memset.o) - 0x080059bc memset - .text.memcpy 0x080059cc 0x12 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libc_nano.a(libc_a-memcpy-stub.o) - 0x080059cc memcpy + 0x08005968 0x198 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libgcc.a(_udivmoddi4.o) + 0x08005968 __udivmoddi4 + .text.memcmp 0x08005b00 0x1c c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libc_nano.a(libc_a-memcmp.o) + 0x08005b00 memcmp + .text.memset 0x08005b1c 0x10 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libc_nano.a(libc_a-memset.o) + 0x08005b1c memset + .text.memcpy 0x08005b2c 0x12 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libc_nano.a(libc_a-memcpy-stub.o) + 0x08005b2c memcpy *(.rodata) *(.rodata.*) .rodata.NMI_Handler.str1.1 - 0x080059de 0xe build/startup.o + 0x08005b3e 0xe build/startup.o .rodata.Reset_Handler.str1.1 - 0x080059ec 0x36 build/startup.o + 0x08005b4c 0x36 build/startup.o .rodata.main.str1.1 - 0x08005a22 0x11 build/main.o - *fill* 0x08005a33 0x1 + 0x08005b82 0x11 build/main.o + *fill* 0x08005b93 0x1 .rodata.k_cfg_desc - 0x08005a34 0x190 build/host/host_cfg.o + 0x08005b94 0x190 build/host/host_cfg.o .rodata.CSWTCH.6 - 0x08005bc4 0xa build/host/host_cfg_profile.o - *fill* 0x08005bce 0x2 + 0x08005d24 0xa build/host/host_cfg_profile.o + *fill* 0x08005d2e 0x2 .rodata.CSWTCH.4 - 0x08005bd0 0xc build/host/host_cfg_profile.o + 0x08005d30 0xc build/host/host_cfg_profile.o .rodata.at_send_ok.str1.1 - 0x08005bdc 0x11 build/host/host_cmd.o + 0x08005d3c 0x11 build/host/host_cmd.o 0x5 (size before relaxing) .rodata.at_send_u32.str1.1 - 0x08005bdc 0x11 build/host/host_cmd.o + 0x08005d3c 0x11 build/host/host_cmd.o 0x2 (size before relaxing) .rodata.at_send_key_u32.str1.1 - 0x08005bdc 0x11 build/host/host_cmd.o + 0x08005d3c 0x11 build/host/host_cmd.o 0x2 (size before relaxing) .rodata.at_send_version_triplet.str1.1 - 0x08005bdc 0x2 build/host/host_cmd.o + 0x08005d3c 0x2 build/host/host_cmd.o .rodata.host_cmd_init.str1.1 - 0x08005bde 0x5a build/host/host_cmd.o + 0x08005d3e 0x5a build/host/host_cmd.o .rodata.host_cmd_dispatch.str1.1 - 0x08005c38 0x4d build/host/host_cmd.o + 0x08005d98 0x4d build/host/host_cmd.o .rodata.host_cmd_dispatch_at_line.str1.1 - 0x08005c85 0x203 build/host/host_cmd.o + 0x08005de5 0x203 build/host/host_cmd.o 0x209 (size before relaxing) .rodata.host_uart_flush_diag_traces.str1.1 - 0x08005e88 0xba build/host/host_uart.o + 0x08005fe8 0xba build/host/host_uart.o .rodata.k_mode_descs - 0x08005f42 0x1e build/radio/sx1276_modes.o + 0x080060a2 0x1e build/radio/sx1276_modes.o .rodata.sx1276_rx_service.cst1 - 0x08005f60 0x1 build/radio/sx1276_rx.o + 0x080060c0 0x1 build/radio/sx1276_rx.o .rodata.sx1276_tx_slot_wait_us.cst1 - 0x08005f61 0x1 build/radio/sx1276_tx.o + 0x080060c1 0x1 build/radio/sx1276_tx.o *(.glue_7) - .glue_7 0x08005f62 0x0 linker stubs + .glue_7 0x080060c2 0x0 linker stubs *(.glue_7t) - .glue_7t 0x08005f62 0x0 linker stubs + .glue_7t 0x080060c2 0x0 linker stubs *(.eh_frame) - 0x08005f64 . = ALIGN (0x4) - *fill* 0x08005f62 0x2 - 0x08005f64 _etext = . + 0x080060c4 . = ALIGN (0x4) + *fill* 0x080060c2 0x2 + 0x080060c4 _etext = . -.vfp11_veneer 0x08005f64 0x0 - .vfp11_veneer 0x08005f64 0x0 linker stubs +.vfp11_veneer 0x080060c4 0x0 + .vfp11_veneer 0x080060c4 0x0 linker stubs -.v4_bx 0x08005f64 0x0 - .v4_bx 0x08005f64 0x0 linker stubs +.v4_bx 0x080060c4 0x0 + .v4_bx 0x080060c4 0x0 linker stubs -.iplt 0x08005f64 0x0 - .iplt 0x08005f64 0x0 build/startup.o +.iplt 0x080060c4 0x0 + .iplt 0x080060c4 0x0 build/startup.o -.boot_text 0x08005f64 0x20c - .boot_text 0x08005f64 0x20c build/boot/safe_mode.o - 0x08005f64 jump_to_bootloader - 0x08005fdc safe_mode_listen +.boot_text 0x080060c4 0x20c + .boot_text 0x080060c4 0x20c build/boot/safe_mode.o + 0x080060c4 jump_to_bootloader + 0x0800613c safe_mode_listen .ARM.extab *(.ARM.extab* .gnu.linkonce.armextab.*) -.ARM 0x08006170 0x8 - 0x08006170 __exidx_start = . +.ARM 0x080062d0 0x8 + 0x080062d0 __exidx_start = . *(.ARM.exidx*) .ARM.exidx.text.__udivmoddi4 - 0x08006170 0x8 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libgcc.a(_udivmoddi4.o) - 0x08006178 __exidx_end = . - 0x08006178 _sidata = LOADADDR (.data) + 0x080062d0 0x8 c:/program files (x86)/arm gnu toolchain arm-none-eabi/12.2 mpacbti-rel1/bin/../lib/gcc/arm-none-eabi/12.2.1/thumb/v6-m/nofp\libgcc.a(_udivmoddi4.o) + 0x080062d8 __exidx_end = . + 0x080062d8 _sidata = LOADADDR (.data) -.rel.dyn 0x08006178 0x0 - .rel.iplt 0x08006178 0x0 build/startup.o +.rel.dyn 0x080062d8 0x0 + .rel.iplt 0x080062d8 0x0 build/startup.o -.data 0x20000000 0x14 load address 0x08006178 +.data 0x20000000 0x14 load address 0x080062d8 0x20000000 . = ALIGN (0x4) 0x20000000 _sdata = . *(.data) @@ -1337,17 +1350,17 @@ END GROUP *fill* 0x20000012 0x2 0x20000014 _edata = . -.igot.plt 0x20000014 0x0 load address 0x0800618c +.igot.plt 0x20000014 0x0 load address 0x080062ec .igot.plt 0x20000014 0x0 build/startup.o -.noinit 0x20000014 0x18 load address 0x0800618c +.noinit 0x20000014 0x18 load address 0x080062ec 0x20000014 . = ALIGN (0x4) *(.noinit) .noinit 0x20000014 0x18 build/hal/platform.o *(.noinit.*) 0x2000002c . = ALIGN (0x4) -.bss 0x20000030 0x1a28 load address 0x0800618c +.bss 0x20000030 0x1a30 load address 0x080062ec 0x20000030 . = ALIGN (0x4) 0x20000030 _sbss = . 0x20000030 __bss_start__ = _sbss @@ -1551,105 +1564,110 @@ END GROUP 0x20001994 0x4 build/radio/sx1276_rx.o .bss.s_scan_state 0x20001998 0x1 build/radio/sx1276_rx.o + *fill* 0x20001999 0x3 + .bss.s_host_parked_ms + 0x2000199c 0x4 build/radio/sx1276_rx.o + .bss.s_host_parked + 0x200019a0 0x1 build/radio/sx1276_rx.o .bss.s_rx_fifo_track - 0x20001999 0x2 build/radio/sx1276_rx.o + 0x200019a1 0x2 build/radio/sx1276_rx.o .bss.s_rx_last_followed_abs_valid - 0x2000199b 0x1 build/radio/sx1276_rx.o + 0x200019a3 0x1 build/radio/sx1276_rx.o .bss.s_rx_last_followed_abs - 0x2000199c 0x4 build/radio/sx1276_rx.o + 0x200019a4 0x4 build/radio/sx1276_rx.o .bss.s_rx_last_retune_ms_valid - 0x200019a0 0x1 build/radio/sx1276_rx.o - *fill* 0x200019a1 0x3 + 0x200019a8 0x1 build/radio/sx1276_rx.o + *fill* 0x200019a9 0x3 .bss.s_rx_last_retune_ms - 0x200019a4 0x4 build/radio/sx1276_rx.o + 0x200019ac 0x4 build/radio/sx1276_rx.o .bss.s_scan_crc_seen - 0x200019a8 0x1 build/radio/sx1276_rx.o + 0x200019b0 0x1 build/radio/sx1276_rx.o .bss.s_scan_got_any_irq - 0x200019a9 0x1 build/radio/sx1276_rx.o - *fill* 0x200019aa 0x2 + 0x200019b1 0x1 build/radio/sx1276_rx.o + *fill* 0x200019b2 0x2 .bss.s_consider_remote_counts - 0x200019ac 0x18 build/radio/sx1276_rx_counters.o + 0x200019b4 0x18 build/radio/sx1276_rx_counters.o .bss.s_retune_counts - 0x200019c4 0x14 build/radio/sx1276_rx_retune_counters.o + 0x200019cc 0x14 build/radio/sx1276_rx_retune_counters.o .bss.s_action_counts - 0x200019d8 0x18 build/radio/sx1276_rx_scan_counters.o + 0x200019e0 0x18 build/radio/sx1276_rx_scan_counters.o .bss.s_state_counts - 0x200019f0 0x10 build/radio/sx1276_rx_scan_counters.o + 0x200019f8 0x10 build/radio/sx1276_rx_scan_counters.o .bss.s_walker_idx - 0x20001a00 0x1 build/radio/sx1276_rx_scan_walker.o - *fill* 0x20001a01 0x1 + 0x20001a08 0x1 build/radio/sx1276_rx_scan_walker.o + *fill* 0x20001a09 0x1 .bss.s_rfco_pertx_seq - 0x20001a02 0x2 build/radio/sx1276_tx.o + 0x20001a0a 0x2 build/radio/sx1276_tx.o .bss.s_predicted_toa_us - 0x20001a04 0x4 build/radio/sx1276_tx.o - .bss.s_slot_boundary_valid - 0x20001a08 0x1 build/radio/sx1276_tx.o - *fill* 0x20001a09 0x3 - .bss.s_slot_boundary_ms 0x20001a0c 0x4 build/radio/sx1276_tx.o - .bss.s_hop_slot_offset_ms + .bss.s_slot_boundary_valid 0x20001a10 0x1 build/radio/sx1276_tx.o *fill* 0x20001a11 0x3 - .bss.s_hop_epoch + .bss.s_slot_boundary_ms 0x20001a14 0x4 build/radio/sx1276_tx.o - .bss.s_hop_idx + .bss.s_hop_slot_offset_ms 0x20001a18 0x1 build/radio/sx1276_tx.o *fill* 0x20001a19 0x3 - .bss.s_hop_freq_hz + .bss.s_hop_epoch 0x20001a1c 0x4 build/radio/sx1276_tx.o - .bss.s_channel_idx + .bss.s_hop_idx 0x20001a20 0x1 build/radio/sx1276_tx.o *fill* 0x20001a21 0x3 - .bss.s_reserved_airtime_us + .bss.s_hop_freq_hz 0x20001a24 0x4 build/radio/sx1276_tx.o + .bss.s_channel_idx + 0x20001a28 0x1 build/radio/sx1276_tx.o + *fill* 0x20001a29 0x3 + .bss.s_reserved_airtime_us + 0x20001a2c 0x4 build/radio/sx1276_tx.o .bss.s_deadline_us - 0x20001a28 0x4 build/radio/sx1276_tx.o + 0x20001a30 0x4 build/radio/sx1276_tx.o .bss.s_expected_toa_us - 0x20001a2c 0x4 build/radio/sx1276_tx.o + 0x20001a34 0x4 build/radio/sx1276_tx.o .bss.s_tx_start_us - 0x20001a30 0x4 build/radio/sx1276_tx.o - .bss.s_tx_ck_len - 0x20001a34 0x1 build/radio/sx1276_tx.o - *fill* 0x20001a35 0x3 - .bss.s_tx_load_ck 0x20001a38 0x4 build/radio/sx1276_tx.o - .bss.s_tx_done_seen_valid + .bss.s_tx_ck_len 0x20001a3c 0x1 build/radio/sx1276_tx.o *fill* 0x20001a3d 0x3 - .bss.s_tx_done_seen_us + .bss.s_tx_load_ck 0x20001a40 0x4 build/radio/sx1276_tx.o - .bss.s_deaf_start_valid + .bss.s_tx_done_seen_valid 0x20001a44 0x1 build/radio/sx1276_tx.o *fill* 0x20001a45 0x3 - .bss.s_deaf_start_us + .bss.s_tx_done_seen_us 0x20001a48 0x4 build/radio/sx1276_tx.o - .bss.s_rearm_rx + .bss.s_deaf_start_valid 0x20001a4c 0x1 build/radio/sx1276_tx.o + *fill* 0x20001a4d 0x3 + .bss.s_deaf_start_us + 0x20001a50 0x4 build/radio/sx1276_tx.o + .bss.s_rearm_rx + 0x20001a54 0x1 build/radio/sx1276_tx.o .bss.s_tx_power_dbm - 0x20001a4d 0x1 build/radio/sx1276_tx.o - .bss.s_tx_id 0x20001a4e 0x1 build/radio/sx1276_tx.o + 0x20001a55 0x1 build/radio/sx1276_tx.o + .bss.s_tx_id 0x20001a56 0x1 build/radio/sx1276_tx.o .bss.s_tx_state - 0x20001a4f 0x1 build/radio/sx1276_tx.o + 0x20001a57 0x1 build/radio/sx1276_tx.o .bss.s_dio0_edges - 0x20001a50 0x4 build/radio/sx1276.o + 0x20001a58 0x4 build/radio/sx1276.o .bss.s_irq_events - 0x20001a54 0x4 build/radio/sx1276.o + 0x20001a5c 0x4 build/radio/sx1276.o *(COMMON) - 0x20001a58 . = ALIGN (0x4) - 0x20001a58 _ebss = . - 0x20001a58 __bss_end__ = _ebss + 0x20001a60 . = ALIGN (0x4) + 0x20001a60 _ebss = . + 0x20001a60 __bss_end__ = _ebss -._user_heap 0x20001a58 0x0 load address 0x0800618c - 0x20001a58 . = ALIGN (0x8) - 0x20001a58 _end = . - 0x20001a58 _heap_start = . - 0x20001a58 . = (. + 0x0) - 0x20001a58 . = ALIGN (0x8) - 0x20001a58 _heap_end = . +._user_heap 0x20001a60 0x0 load address 0x080062ec + 0x20001a60 . = ALIGN (0x8) + 0x20001a60 _end = . + 0x20001a60 _heap_start = . + 0x20001a60 . = (. + 0x0) + 0x20001a60 . = ALIGN (0x8) + 0x20001a60 _heap_end = . -.stack_check 0x20001a58 0x0 load address 0x0800618c - 0x20001a58 . = ALIGN (0x8) - 0x20001a58 . = (. + 0x0) +.stack_check 0x20001a60 0x0 load address 0x080062ec + 0x20001a60 . = ALIGN (0x8) + 0x20001a60 . = (. + 0x0) 0x00000001 ASSERT (((_heap_end + _stack_size) <= _estack), Linker error: not enough RAM left for the reserved stack) .cfg_reserved 0x0802e000 0x2000 diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cfg_profile.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cfg_profile.c index dad2d726..cfc26532 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cfg_profile.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cfg_profile.c @@ -186,6 +186,10 @@ host_cfg_profile_reject_t host_cfg_profile_activate(void) { return HOST_CFG_PROFILE_REJECT_NOT_STAGED; } s_active = s_staged; + /* RS-12.21 (2026-10-24): an activation ends a firmware radio park + * (HOST_TYPE_RADIO_PARK_REQ) -- the host is configuring the radio for + * use again. Every branch below then resets the scan machine. */ + sx1276_rx_unpark(); /* D2: RegModemConfig writes require SLEEP/STANDBY (Semtech DS §4.1; * sx1276_apply_profile_full() sequences the same way). Activation * can be requested while the RX daemon has RXCONT armed. */ @@ -219,7 +223,10 @@ host_cfg_profile_reject_t host_cfg_profile_activate(void) { * budget on an empty band at boot, parks in the absorbing * FAILED state (standby), and no later activation ever * re-arms the receiver — fhss_rx_probe evidence 2026-07-24: - * 75 s under active TX hopping, zero RX_FRAME, zero FAULT. */ + * 75 s under active TX hopping, zero RX_FRAME, zero FAULT. + * (RS-12.21 (b), 2026-10-24: FAILED no longer parks a node without + * originator authority, so this is no longer its only way out; it + * is still the fresh acquisition an activation needs.) */ sx1276_rx_scan_reset(); } else if (s_active.profile_id == REG_PROFILE_FCC_15_247_DTS_BW500) { /* 2026-07-25 run-23 deafness: activation must fully DEFINE the diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cmd.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cmd.c index 98bd46d2..e10c881a 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cmd.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/host/host_cmd.c @@ -478,6 +478,20 @@ static void handle_tx_frame(const host_frame_t *frame) { memcpy(req.payload, &frame->payload[2], req.length); } + /* RS-12.21 (2026-10-24): the host has the radio parked + * (HOST_TYPE_RADIO_PARK_REQ). Refuse now, with the refusal any failed + * TX gets, rather than queue a frame that would be refused at release + * (sx1276_tx_begin() refuses while parked too) or, worse, go out after + * a later un-park the sender never saw. */ + if (sx1276_rx_parked()) { + host_uart_send_err_proto(frame->seq, + frame->type, + frame->ver, + HOST_ERR_PROTO_FORBIDDEN, + 0U); + return; + } + /* * v25.0.7 slot-clock: park when the modem is busy OR when the * frame does not fit the remaining hop-slot time. The ring @@ -590,6 +604,36 @@ static void handle_reg_write(const host_frame_t *frame) { 2U); } +/* + * RS-12.21 (2026-10-24): the firmware radio park, HOST_TYPE_RADIO_PARK_REQ + * (contract in host_types.h; the flag and the gates in radio/sx1276_rx.c). + * Refused while a TX is keyed or queued in the TX ring: SLEEP would cut a + * transmission short, and every queued frame would then be refused at its + * release. The host retries after the TX_DONE. A repeat request re-applies + * SLEEP (a raw STANDBY write does not end the park, so the modem may have + * left SLEEP). The opmode byte is read back from the chip AFTER the request + * (0x80 = LoRa SLEEP; 0x83 on a refusal mid-TX), not taken from the tracked + * state. STATS radio_state reads 1 (SX1276_STATE_SLEEP) after a park. + */ +static void handle_radio_park(const host_frame_t *frame) { + uint8_t payload[HOST_RADIO_PARK_URC_PAYLOAD_LEN]; + + if (sx1276_tx_busy() || s_txq_count > 0U) { + payload[0] = HOST_RADIO_PARK_STATUS_REFUSED; + } else if (sx1276_rx_park()) { + payload[0] = HOST_RADIO_PARK_STATUS_PARKED; + } else { + payload[0] = HOST_RADIO_PARK_STATUS_ALREADY; + } + payload[1] = sx1276_read_reg(0x01U); /* RegOpMode */ + + host_uart_send_urc(HOST_TYPE_RADIO_PARK_URC, + frame->seq, + 0U, + payload, + (uint16_t)sizeof(payload)); +} + static void handle_cfg_set(const host_frame_t *frame) { cfg_status_t status; uint8_t key; @@ -720,6 +764,7 @@ void host_cmd_init(bool radio_ok, uint8_t radio_version) { platform_fault_record_t replay; host_type_compile_time_uniqueness_check(); + host_fault_code_compile_time_uniqueness_check(); /* RS-12.21 */ s_radio_ok = radio_ok; s_radio_version = radio_version; @@ -786,6 +831,10 @@ void host_cmd_dispatch(const host_frame_t *frame) { platform_system_reset(); break; + case HOST_TYPE_RADIO_PARK_REQ: + handle_radio_park(frame); + break; + case HOST_TYPE_TX_FRAME_REQ: handle_tx_frame(frame); break; diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/host_types.h b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/host_types.h index 058cb79c..034e96c0 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/host_types.h +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/host_types.h @@ -3,6 +3,19 @@ #include +/* Version contract (RS-12.21 note, 2026-10-24). HOST_PROTOCOL_VER is the + * frame header's `ver` byte, refused on mismatch by host_cmd_dispatch + * (BAD_VERSION), so it changes only with the framing. HOST_WIRE_SCHEMA_VER + * is reported in BOOT/VER/READY and CFG key 0x81 (1 -> 2 on 2026-05-20, + * d4dfcb86, with the FHSS batch). Additive changes have kept both since: a + * new message type (0xC5 RX_CRC_DUMP_URC, 2026-08-02), a tail appended to a + * payload (the F8 RX_FRAME_URC tail, the STATS_URC tails), a new fault code + * or a fault code's sub-byte (the 2026-05-19 RX-scan-FAILED analysis §13.1 + * #7 contract: the FAULT_URC format, host_stats and schema_ver untouched). + * The 2026-10-24 RADIO_PARK_REQ/URC and the 0x0D split are such changes: + * no bump. A new command is advertised in the capability bitmap instead, + * and an older image answers it with ERR_PROTO UNKNOWN_TYPE (detail = the + * type). */ #define HOST_PROTOCOL_VER 1U #define HOST_WIRE_SCHEMA_VER 2U @@ -13,6 +26,10 @@ #define HOST_CAP_STATS (1UL << 4) #define HOST_CAP_RESET (1UL << 5) #define HOST_CAP_TX_FRAME (1UL << 6) +/* RS-12.21 (2026-10-24): RADIO_PARK_REQ/URC below. The image that added it + * also moved TX_FIFO_RB_MISMATCH to 0x0F, so a host may read this bit as + * "0x0D is RX_SCAN_FAILED only" (the 0x0D split, fault codes below). */ +#define HOST_CAP_RADIO_PARK (1UL << 7) #define HOST_CAPABILITY_BITMAP (HOST_CAP_PING | \ HOST_CAP_VERSION | \ @@ -20,13 +37,43 @@ HOST_CAP_REG_IO | \ HOST_CAP_STATS | \ HOST_CAP_RESET | \ - HOST_CAP_TX_FRAME) + HOST_CAP_TX_FRAME | \ + HOST_CAP_RADIO_PARK) /* Host-to-L072 commands. */ #define HOST_TYPE_PING_REQ 0x00U #define HOST_TYPE_VER_REQ 0x01U #define HOST_TYPE_UID_REQ 0x02U #define HOST_TYPE_RESET_REQ 0x03U +/* + * RADIO_PARK_REQ (RS-12.21, 2026-10-24): the firmware radio park, the bench's + * persistent radio-off state (FHSS_SCAN_FAILED_OPTIONS.md section 5 item 2, + * adopted with scan option (b): a node without originator authority now + * scans on after its scan budget, re-arming RX every dwell, so a host SLEEP + * write does not hold once the daemons stop). Payload: none (ignored). + * - Sets the host_parked flag (radio/sx1276_rx.c, sx1276_rx_park()) and + * puts the modem to LoRa SLEEP. While it is set nothing arms the + * receiver: the scan machine is frozen (no walker step, no FAIL fault, + * no (b) restart), the slot follower, the gamma-1 retune and the post-TX + * re-arm do nothing, and sx1276_rx_arm() refuses. + * - A TX_FRAME_REQ while parked is refused (ERR_PROTO FORBIDDEN, detail + * 0, as any refused TX): nothing goes on air. + * - Ends at a host raw RegOpMode RX write (REG_WRITE 0x01 = 0x85 RXCONT, + * the daemons' arm; 0x86 RXSINGLE in a diag build), a profile + * activation (CFG_SET REG_PROFILE) or any reset. The scan machine then + * resumes as it stood (an activation restarts it): a SCANNING node's + * dwell and 30 s attempt do not count the parked time; a LOCKED node's + * 2 s loss timer does, as the FHSS clock's freshness does, so a park + * longer than that demotes it at the first tick (a fresh acquisition, + * not a drifted grid). + * Reply: RADIO_PARK_URC, same seq, HOST_RADIO_PARK_URC_PAYLOAD_LEN bytes: + * u8 status HOST_RADIO_PARK_STATUS_*: 0 PARKED; 1 ALREADY_PARKED (SLEEP + * re-applied); 2 REFUSED -- a TX is on air or queued, nothing + * changed (retry after its TX_DONE) + * u8 opmode RegOpMode read back after the request (0x80 = LoRa SLEEP) + * Bench check: check-radio-park (bench/host_proto/radio_park.c). + */ +#define HOST_TYPE_RADIO_PARK_REQ 0x04U #define HOST_TYPE_TX_FRAME_REQ 0x10U @@ -42,6 +89,12 @@ /* L072-to-host URCs/responses. */ #define HOST_TYPE_VER_URC 0x81U #define HOST_TYPE_UID_URC 0x82U +/* RS-12.21 (2026-10-24): reply to RADIO_PARK_REQ, layout above. */ +#define HOST_TYPE_RADIO_PARK_URC 0x84U +#define HOST_RADIO_PARK_URC_PAYLOAD_LEN 2U +#define HOST_RADIO_PARK_STATUS_PARKED 0U +#define HOST_RADIO_PARK_STATUS_ALREADY 1U +#define HOST_RADIO_PARK_STATUS_REFUSED 2U #define HOST_TYPE_TX_DONE_URC 0x90U /* TX_DONE_URC payload: {u8 tx_id, u8 status, u32 time_on_air_us_le, u8 tx_power_dbm} */ @@ -248,36 +301,48 @@ #define HOST_FAULT_CODE_HOST_RX_INACTIVE 0x0AU #define HOST_FAULT_CODE_HOST_PARSE_ERROR 0x0BU #define HOST_FAULT_CODE_HOST_DIAG_MARK 0x0CU -/* RS-11.5 (2026-08-02) TX-path discriminators for the slot-(total-2) - * on-air CRC corruption. sub = the fragment's tx_id (== index). - * 0x0D: post-TX FIFO readback checksum != load-time checksum — the FIFO - * was altered between load and TX_DONE (corrupt at the source). - * 0x0E: TX_DONE arrived >5 ms EARLIER than the expected ToA — the - * radiation was truncated. */ -#define HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH 0x0DU -#define HOST_FAULT_CODE_TX_DONE_EARLY 0x0EU - /* - * RX_SCAN_FAILED — emitted by the SCANNING-state failure handler in - * radio/sx1276_rx.c when the FHSS rendezvous scan has failed to lock - * within REDESIGN_MS (per FCC-A6c-1 policy, sx1276_rx_scan_policy.c). - * - * The `sub` byte carries a four-class disambiguation encoding so the - * host can distinguish root cause at the fault edge without a wider - * URC payload (per AI NOTES/2026-05-19_RX_Scan_FAILED_State_Analysis - * §13.1 #2, A6c-3-a): + * RX_SCAN_FAILED (0x0D) -- emitted by the scan FAIL dispatch in + * radio/sx1276_rx.c (scan_dispatch_action, FCC-A6c-3-c) at the end of every + * 30 s scan attempt that did not LOCK (SX1276_RX_SCAN_REDESIGN_MS, + * radio/sx1276_rx_scan_policy.c). The `sub` byte, built by + * sx1276_rx_scan_fail_eval() (radio/sx1276_rx_scan_fail.c), carries a + * four-class disambiguation encoding so the host can tell the root cause at + * the fault edge without a wider URC payload (AI NOTES/2026-05-19_RX_Scan_ + * FAILED_State_Analysis §13.1 #2, A6c-3-a): * - * bit 7 : final — 1 == retries exhausted, FAILED entered + * bit 7 : final — 1 == the retries are spent (attempt nibble at + * SX1276_RX_SCAN_MAX_RETRIES). Since RS-12.21 (b) + * (2026-10-24) this does NOT mean "FAILED for + * good": only a node with originator authority + * holds FAILED; any other node scans on, and its + * final fault repeats every 30 s until a LOCK. * bit 6 : warm — 1 == this boot has previously LOCKED * bit 5 : hw_suspect — 1 == no DIO0 IRQ observed during scan * bit 4 : crc_seen — 1 == >=1 CRC-error IRQ observed during scan * bits 3..0 : attempt — 0..15 saturating, this attempt's index * - * Emission semantics (B-prime, per §13.1 #6) and the file-statics that - * back the `sub` bits land in FCC-A6c-3-b / A6c-3-c. This define is - * declaration-only (FCC-A6c-3-a); no code emits it yet. + * History (FHSS_SCAN_FAILED_OPTIONS.md section 7): this define took 0x0D on + * 2026-05-20 as a declaration-only step whose note read "no code emits it + * yet"; the emitter landed with A6c-3-c, the note stayed, and RS-11.5 took + * 0x0D again for TX_FIFO_RB_MISMATCH on 2026-08-02. The 2026-10-24 split + * keeps 0x0D for this fault (every logged 0x0D is one) and moves the FIFO + * fault to 0x0F. A host decoding an older image: a 0x0D whose sub is not a + * valid scan encoding is that image's FIFO fault (tx_id = sub); an image + * advertising HOST_CAP_RADIO_PARK has the split. */ #define HOST_FAULT_CODE_RX_SCAN_FAILED 0x0DU +/* RS-11.5 (2026-08-02) TX-path discriminators for the slot-(total-2) + * on-air CRC corruption (radio/sx1276_tx.c). sub = the TX's tx_id (the + * host's: a fragment index, or a command/PROBE id such as 0xFD/0xFE). + * 0x0E: TX_DONE arrived >5 ms EARLIER than the expected ToA — the + * radiation was truncated. + * 0x0F: post-TX FIFO readback checksum != load-time checksum — the FIFO + * was altered between load and TX_DONE (corrupt at the source). Was + * 0x0D until 2026-10-24 (the 0x0D split, above); the counter + * tx_fifo_rb_bad (STATS offset 136) stays its primary signal. */ +#define HOST_FAULT_CODE_TX_DONE_EARLY 0x0EU +#define HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH 0x0FU /* CFG status codes used in CFG_OK_URC payload. */ #define HOST_CFG_STATUS_OK 0U @@ -298,13 +363,16 @@ #define HOST_ERR_PROTO_QUEUE_FULL 7U #define HOST_ERR_PROTO_FORBIDDEN 8U -/* Duplicate case labels produce a compile-time error if any ID collides. */ +/* Duplicate case labels produce a compile-time error if any ID collides. + * RS-12.21 (2026-10-24): RADIO_PARK_REQ/URC added, and the 0xC3-0xC5 URC + * types the list had missed. */ static inline void host_type_compile_time_uniqueness_check(void) { switch (0U) { case HOST_TYPE_PING_REQ: case HOST_TYPE_VER_REQ: case HOST_TYPE_UID_REQ: case HOST_TYPE_RESET_REQ: + case HOST_TYPE_RADIO_PARK_REQ: case HOST_TYPE_TX_FRAME_REQ: case HOST_TYPE_CFG_SET_REQ: case HOST_TYPE_CFG_GET_REQ: @@ -314,6 +382,7 @@ static inline void host_type_compile_time_uniqueness_check(void) { case HOST_TYPE_STATS_DUMP_REQ: case HOST_TYPE_VER_URC: case HOST_TYPE_UID_URC: + case HOST_TYPE_RADIO_PARK_URC: case HOST_TYPE_TX_DONE_URC: case HOST_TYPE_RX_FRAME_URC: case HOST_TYPE_CFG_OK_URC: @@ -323,6 +392,9 @@ static inline void host_type_compile_time_uniqueness_check(void) { case HOST_TYPE_RADIO_IRQ_URC: case HOST_TYPE_STATS_URC: case HOST_TYPE_AIRTIME_INVARIANT_REJECT_URC: + case HOST_TYPE_RFCO_PERTX_URC: + case HOST_TYPE_RFCO_SUMMARY_URC: + case HOST_TYPE_RX_CRC_DUMP_URC: case HOST_TYPE_BOOT_URC: case HOST_TYPE_FAULT_URC: case HOST_TYPE_READY_URC: @@ -333,4 +405,32 @@ static inline void host_type_compile_time_uniqueness_check(void) { } } +/* RS-12.21 (2026-10-24, FHSS_SCAN_FAILED_OPTIONS.md section 7): the same + * guard for the FAULT_URC code space. RX_SCAN_FAILED and TX_FIFO_RB_MISMATCH + * shared 0x0D from 2026-08-02 to 2026-10-24 because nothing checked fault + * codes; every HOST_FAULT_CODE_* belongs in this list. Called from + * host_cmd_init() next to the type check. */ +static inline void host_fault_code_compile_time_uniqueness_check(void) { + switch (0U) { + case HOST_FAULT_CODE_HARDFAULT: + case HOST_FAULT_CODE_NMI: + case HOST_FAULT_CODE_RADIO_INIT_FAIL: + case HOST_FAULT_CODE_RADIO_OPMODE_DRIFT: + case HOST_FAULT_CODE_RX_CRC_DISABLED: + case HOST_FAULT_CODE_HOST_DMA_OVERRUN: + case HOST_FAULT_CODE_STACK_GUARD: + case HOST_FAULT_CODE_CLOCK_HSE_FAILED: + case HOST_FAULT_CODE_HOST_RX_SEEN: + case HOST_FAULT_CODE_HOST_RX_INACTIVE: + case HOST_FAULT_CODE_HOST_PARSE_ERROR: + case HOST_FAULT_CODE_HOST_DIAG_MARK: + case HOST_FAULT_CODE_RX_SCAN_FAILED: + case HOST_FAULT_CODE_TX_DONE_EARLY: + case HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH: + break; + default: + break; + } +} + #endif /* LIFETRAC_MURATA_L072_HOST_TYPES_H */ diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx.h b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx.h index 401a9505..f9ee5344 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx.h +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx.h @@ -110,9 +110,10 @@ void sx1276_rx_retune_counter_record(sx1276_rx_retune_decision_t dec); * event=TICK; records the (state, action) pair in the A6c-2-a * counters TU; and updates the file-static anchors per the * returned action (BEGIN_SCAN anchors both, ADVANCE_CHANNEL - * anchors channel only, REANCHOR anchors both, HOLD/LOCK/FAIL - * leave anchors alone). The next_state is stored back as the - * file-static state. + * anchors channel only, REANCHOR anchors both, LOCK anchors + * channel only -- RS-12.21 L, 2026-10-24: the loss timer starts + * at the locking frame -- and HOLD/FAIL leave anchors alone). The + * next_state is stored back as the file-static state. * * A6c-2-b SCOPE: bookkeeping + counters only. NO HW action is * driven by any scan-SM action. The existing γ-1 sx1276_rx_tick() @@ -131,6 +132,15 @@ void sx1276_rx_retune_counter_record(sx1276_rx_retune_decision_t dec); * REDESIGN_MS = 30 000 ms — no frame-event histograms until * A6c-2-c. * + * RS-12.21 (2026-10-24; the dwell is 2 s, scan HW dispatch long since + * wired): FAILED is held only while the node holds originator authority. + * A final FAIL on any other node, and the first tick of a node in FAILED + * whose authority has decayed, re-enter SCANNING for a fresh 30 s attempt + * that continues the channel walk -- recorded as (FAILED, BEGIN_SCAN) -- + * and the scan never parks the receiver (only the host's firmware park, + * sx1276_rx_park() below, does; it freezes this tick). See + * sx1276_rx_scan_policy.h. + * * When LIFETRAC_FHSS_TX_ROUTED is NOT defined, this is a no-op. */ void sx1276_rx_scan_tick(uint32_t now_ms); @@ -160,13 +170,50 @@ void sx1276_rx_slot_follow(uint32_t now_ms); * reached the absorbing FAILED state before the peer started radiating * (30 s empty-band budget at boot) stays parked in standby FOREVER and * the receiver is deaf regardless of later profile activations. + * RS-12.21 (b) (2026-10-24): FAILED no longer parks a node without + * originator authority, so this is no longer its only way out; it is + * still the fresh acquisition (walker at channel 0, retry budget, clock, + * authority and adoption cleared) an activation needs. * No-op when LIFETRAC_FHSS_TX_ROUTED is not defined. */ void sx1276_rx_scan_reset(void); /* RS-12.10 (2026-09-12): the host raw-armed RX (RegOpMode 0x85/0x86) behind * the mode tracker's back — reset the FIFO-skip tracker so the first packet - * after the re-arm is not scored as a hole. */ + * after the re-arm is not scored as a hole. RS-12.21 (2026-10-24): a host + * raw RX arm also ends a firmware park (sx1276_rx_unpark below). */ void sx1276_rx_note_external_arm(void); +/* + * RS-12.21 (2026-10-24): the firmware radio park (HOST_TYPE_RADIO_PARK_REQ, + * host_types.h; host/host_cmd.c answers it). sx1276_rx_park() sets the + * host_parked flag, puts the modem to SLEEP and clears its IRQ flags; it + * returns true when this call parked the radio, false when it was parked + * already (SLEEP is re-applied either way). The caller refuses the park + * while a TX is keyed or queued. While parked: + * - sx1276_rx_arm() refuses, modem untouched: the backstop for every arm + * path, sx1276_tx.c's post-TX re-arm included; + * - the scan machine is frozen (scan ticks and frame events do nothing: + * no walker step, no FAIL fault, no option-(b) restart), and the slot + * follower and the gamma-1 retune return before they touch the modem, + * so it stays in SLEEP; + * - sx1276_rx_service() drops a DIO0 edge latched before the park; + * - sx1276_tx_begin() refuses (nothing goes on air). + * sx1276_rx_unpark() ends it (a no-op when not parked): called by + * sx1276_rx_note_external_arm() (a host raw RX arm) and by + * host_cfg_profile_activate(); a reset clears the zero-initialised flag. + * A SCANNING node's anchors move on by the time parked, so it resumes where + * it stood: the parked time counts against neither its dwell nor its 30 s + * attempt. A LOCKED node's are left alone (2026-10-24 review): its 2 s loss + * timer counts the parked time, as the FHSS clock's F6 freshness does + * (FRESH_MS == LOCK_LOSS_MS), so after a park longer than LOCK_LOSS_MS it + * demotes on the first tick instead of following a grid that drifted while + * it slept. FAILED re-anchors at its next tick anyway (an activation + * restarts the scan in every state). + * check-radio-park (bench/host_proto/radio_park.c) pins all of it. + */ +bool sx1276_rx_park(void); +void sx1276_rx_unpark(void); +bool sx1276_rx_parked(void); + #endif /* LIFETRAC_MURATA_L072_SX1276_RX_H */ diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_fail.h b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_fail.h index e15f342f..1a86784e 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_fail.h +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_fail.h @@ -12,6 +12,13 @@ * whether to retry (SCANNING re-entry with anchor reset) or * absorb in FAILED. * + * RS-12.21 (b) (2026-10-24): `is_final` now means "the retry budget is + * spent" and nothing more. The dispatcher absorbs in FAILED only a node + * that holds originator authority (it follows its own clock); any other + * node re-enters SCANNING without counting a retry, so every later FAIL + * is final as well (attempt nibble at MAX) until a LOCK resets the count. + * The bit layout below is unchanged; "absorbing" in it reads "final". + * * Concern split mirrors the A6c-1 (policy) / A6c-2 (counters TU) / * A6c-2-c (HW dispatch) split: * diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_policy.h b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_policy.h index 7034ab4f..3152f365 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_policy.h +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_policy.h @@ -34,6 +34,25 @@ * where the FCC narrative ("cold-start ≤ a few seconds") still * holds. * + * RS-12.21 (2026-10-24, option (b) of firmware/murata_l072/ + * FHSS_SCAN_FAILED_OPTIONS.md, adopted by the design owner): the 30 s + * FAIL now means "REPORT and keep scanning (receive only)" for a node + * without originator authority. Scanning emits nothing, so it cannot hog + * the band; what the old absorbing FAILED did instead was strand a + * follower: leg 6 (2026-10-24) kept transmitting on hop channels (its + * PROBEs) while its receiver sat on one channel and no longer followed + * the hop sequence -- the "synced receivers" requirement in the + * FCC §15.247(a)(1) row of the FCC notes (AI NOTES/2026-05-19_FCC_ + * Part15_902-928_Compliance_Notes) -- and heard 5 of 606 frames. The + * "cold-start ≤ a few seconds" narrative never held for such a node. + * So the HW dispatcher (sx1276_rx.c) holds FAILED only while the node + * follows its own clock (originator authority: its own TX grid IS the + * hop sequence its peers follow) and otherwise re-enters SCANNING at + * once, emitting RX_SCAN_FAILED (final bit set) every 30 s while + * unacquired. THIS pure policy is unchanged -- FAILED absorbs here + * (vector 16): leaving it is the dispatcher's call, as the non-final + * retry always was. + * * Authenticated-header trust boundary: the FRAME_VALID event means * the caller has already verified the A6a header's MIC + profile_id * + schema_ver. This policy does NOT re-validate. Garbage frames @@ -59,8 +78,29 @@ * case preamble for the slowest active profile rounded down to a * conservative 100 ms. A6c-2 may parameterize this off the running * profile's preamble length. + * + * RS-12.21 (2026-10-24, option d2 of FHSS_SCAN_FAILED_OPTIONS.md): 500 ms + * -> 2 s, ten 200 ms slots per channel. A frame is caught only when it is + * hopped onto the dwelled channel and lies wholly inside the dwell, so a + * longer dwell catches a sparse stream more often: 2 fps one-fragment + * cold starts without a LOCK in the 120 s budget fall from 6.7 % to + * 1.3 %, 1 fps from 23.2 % to 10.7 % (SIL E2, memo section 4). An attempt + * now covers 15 channels, so the walk CONTINUES across attempts (the + * non-final retries and RS-12.21 (b)'s restarts out of FAILED; only a + * BEGIN_SCAN out of BOOT or LOCKED restarts it at channel 0) and a silent + * node's successive attempts cover all 50 channels: one sweep takes + * 100 s (25 s at 500 ms). A fixed constant, not the "adaptive scan dwell" + * that plan §7 Q6 names as the redesign path (the 2026-05-19 analysis + * §13.3 rejected that); receive side only -- a receiver's dwell is not + * the §15.247 channel-occupancy limit. It also runs on the tractor + * whenever its originator authority has decayed (the RS-12.15 decay + * window, memo section 5 item 3). The LOCKED loss timer must start at + * the locking frame (RS-12.21 L, sx1276_rx.c scan_drive): at a dwell this + * long a LOCK otherwise demotes on the next tick. Longer is better at + * 2 fps (10 s: 0.3 %) but needs check-rx-reverse-delivery scenario 6's + * 6 s window rescaled; re-run `rx_scan_failed_follower -e` on any change. */ -#define SX1276_RX_SCAN_DWELL_MS 500U +#define SX1276_RX_SCAN_DWELL_MS 2000U /* * 2026-07-24 FHSS bring-up (run-18 evidence): LOCKED used to be absorbing @@ -70,7 +110,10 @@ * seen a valid frame for this long is demoted back to SCANNING via a * fresh BEGIN_SCAN. The driver re-anchors channel_entry_ms on every * FRAME_VALID in LOCKED (REANCHOR action), so an active link never - * trips this. 2 s ≈ 12 packet times at the 165 ms bench cadence — + * trips this -- and, since RS-12.21 L (2026-10-24), at the LOCK itself: + * before that the timer ran from the last walker step, up to one dwell + * earlier, so at the 2 s dwell a LOCK could demote on its next tick. + * 2 s ≈ 12 packet times at the 165 ms bench cadence — * tolerant of QoS pacing gaps, fast enough that a video stream resumes * within one scan acquisition. */ @@ -87,6 +130,14 @@ * §7 Q6 redesign threshold: > 30 s ⇒ A6 redesign mandated. Enforced * here as a SCANNING → FAILED transition so the firmware doesn't hog * the band past the regulatory narrative's bounds. + * + * RS-12.21 (2026-10-24): the transition is a 30 s REPORTING boundary + * (one RX_SCAN_FAILED fault per attempt), not a stop: a receive-only scan + * occupies no channel, and a node without originator authority leaves + * FAILED at once and keeps scanning (see the RS-12.21 block at the top of + * this header). Measured first-frame times on the 27 archived FHSS legs: + * median 28.3 s, 13 over 30 s (memo section 2.4) -- the §7 Q6 redesign + * question stays open; this only stops a missed budget stranding a node. */ #define SX1276_RX_SCAN_REDESIGN_MS 30000U @@ -144,8 +195,18 @@ typedef enum { * FHSS scheduler from the decoded epoch/hop * index in the input header and hands the RX * path to the γ-1 retune loop. + * (RS-12.21 L, 2026-10-24: the caller also + * re-anchors `channel_entry_ms` to now_ms, so + * the LOCK_LOSS_MS timer starts at the locking + * frame, not at the last walker step; + * `cold_start_entry_ms` is unchanged.) * FAIL — SCANNING → FAILED: caller raises an alert * URC and stops the modem. Terminal state. + * (RS-12.21, 2026-10-24: terminal in this + * policy only. The caller re-enters SCANNING + * unless the retry budget is spent AND the node + * holds originator authority; the modem is + * never stopped. See the top of this header.) * REANCHOR — uint32 ms tick wrap detected on at least one * anchor: caller re-anchors BOTH anchors to * now_ms and stays in the same state. The next @@ -216,6 +277,10 @@ typedef struct { * BOOT collapses any event to BEGIN_SCAN / SCANNING (the caller * cannot yet have valid anchors). LOCKED and FAILED are absorbing * for the A6c-1 surface — any event yields HOLD and stays put. + * (LOCKED demotes after LOCK_LOSS_MS since 2026-07-24. FAILED still + * absorbs HERE -- vector 16 of check-rx-scan-policy pins it; since + * RS-12.21 (b) the dispatcher in sx1276_rx.c holds it only under + * originator authority.) */ sx1276_rx_scan_decision_t sx1276_rx_scan_eval(const sx1276_rx_scan_input_t *in); @@ -229,7 +294,11 @@ sx1276_rx_scan_decision_t sx1276_rx_scan_eval(const sx1276_rx_scan_input_t *in); * * Storage lives in radio/sx1276_rx_scan_counters.c. The RX loop * (A6c-2-b) calls sx1276_rx_scan_counter_record(state, action) once - * per sx1276_rx_scan_eval() call. Saturating-uint32. Out-of-range + * per sx1276_rx_scan_eval() call -- and, since RS-12.21 (2026-10-24), + * once per dispatcher exit from a committed FAILED (a non-final retry, + * or RS-12.21 (b)'s restart at a final FAIL or once authority decays), + * recorded as (FAILED, BEGIN_SCAN): no eval produces that pair, so + * every FAILED -> SCANNING edge is visible. Saturating-uint32. Out-of-range * arguments are dropped silently per-axis (independent guards) so a * malformed cast on one axis cannot blank the other. * diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_walker.h b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_walker.h index 000ecc63..3c4a50ec 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_walker.h +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/include/sx1276_rx_scan_walker.h @@ -44,7 +44,11 @@ /* Reset the walker to its starting channel (idx=0). Idempotent. * Called by the A6c-2-c dispatcher on every BEGIN_SCAN edge so a - * recovery re-scan (A6c-3 non-final retry) starts cleanly. */ + * recovery re-scan (A6c-3 non-final retry) starts cleanly. + * RS-12.21 d2 (2026-10-24): BEGIN_SCAN is now only a fresh acquisition + * (BOOT, a LOCKED loss demotion). A retry and RS-12.21 (b)'s restarts out + * of FAILED continue the walk: at the 2 s scan dwell an attempt covers + * 15 channels, and successive attempts must cover all 50. */ void sx1276_rx_scan_walker_reset(void); /* Return the next (channel_idx, center_hz) to listen on and advance diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_rx.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_rx.c index bd39ce5e..02de64b6 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_rx.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_rx.c @@ -79,7 +79,18 @@ static void rx_pll_settle_busy_wait(uint32_t delay_us); * teaches the pointer. */ static sx1276_rx_fifo_track_t s_rx_fifo_track; +/* RS-12.21 (2026-10-24): the firmware radio park (sx1276_rx.h; the API is at + * the end of this file). Zero-initialised, so every reset clears it. */ +static uint8_t s_host_parked; +static uint32_t s_host_parked_ms; /* platform_now_ms() at the park */ + bool sx1276_rx_arm(void) { + /* RS-12.21: the park's backstop. Every arm path also returns before its + * standby/retune while parked (so the modem stays in SLEEP); this + * catches any that does not, sx1276_tx.c's post-TX re-arm included. */ + if (s_host_parked != 0U) { + return false; + } if (!sx1276_modes_to_rx_cont()) { return false; } @@ -100,6 +111,8 @@ void sx1276_rx_disarm(void) { void sx1276_rx_note_external_arm(void) { sx1276_rx_fifo_track_reset(&s_rx_fifo_track); + /* RS-12.21: the host armed the receiver itself -- the park is over. */ + sx1276_rx_unpark(); } bool sx1276_rx_service(uint32_t events, sx1276_rx_frame_t *out_frame) { @@ -109,6 +122,14 @@ bool sx1276_rx_service(uint32_t events, sx1276_rx_frame_t *out_frame) { return false; } + /* RS-12.21 (2026-10-24): an edge latched before the park (host frames + * are dispatched before the radio events in main.c's pass). The modem + * is asleep and its IRQ flags were cleared at the park: read nothing, + * and let no packet adopt a grid or move the frozen scan machine. */ + if (s_host_parked != 0U) { + return false; + } + #ifdef LIFETRAC_FHSS_TX_ROUTED /* FCC-A6c-3-b: latch "any DIO0 IRQ observed this scan window" so * the A6c-3-c FAIL handler can derive `hw_suspect` (= !got_any_irq @@ -383,6 +404,9 @@ static uint32_t s_scan_cold_start_entry_ms; * Incremented by A6c-3-c FAIL handler; * reset on LOCK. Saturates so the * `sub` 4-bit attempt field is safe. + * RS-12.21 (b): stays at MAX across + * the restarts out of FAILED, so every + * later FAIL is final until a LOCK. * s_scan_got_any_irq — any DIO0 event observed by * sx1276_rx_service() since the last * BEGIN_SCAN/ADVANCE_CHANNEL. Used @@ -420,8 +444,12 @@ void sx1276_rx_tick(uint32_t now_ms) { * γ-1 must NOT also retune or the two loops will fight. The * counter is intentionally NOT recorded for the gated-out case * — γ-1 simply did not run, and the scan counters already - * capture what happened in that interval. */ - if (s_scan_state != SX1276_RX_SCAN_STATE_LOCKED) { + * capture what happened in that interval. + * RS-12.21 (2026-10-24): gated out the same way while the host has + * the radio parked (a parked node may sit LOCKED: the scan machine + * is frozen), before any standby/retune/re-arm. */ + if (s_host_parked != 0U || + s_scan_state != SX1276_RX_SCAN_STATE_LOCKED) { /* Drop the anchor so the first tick post-LOCK re-anchors * cleanly rather than firing a spurious DO_WRAP off stale * pre-LOCK timing. */ @@ -516,7 +544,11 @@ void sx1276_rx_tick(uint32_t now_ms) { * BEGIN_SCAN → channel + cold = now_ms (BOOT → SCANNING entry) * ADVANCE_CHANNEL → channel = now_ms (cold budget preserved) * REANCHOR → channel + cold = now_ms (clock-glitch recovery) - * LOCK / FAIL / HOLD → anchors unchanged (terminal states or no-op) + * LOCK → channel = now_ms (RS-12.21 L, 2026-10-24: + * the LOCKED loss timer starts at + * the locking frame) + * FAIL / HOLD → anchors unchanged (FAIL: the dispatcher re-anchors + * both when it leaves FAILED, scan_leave_failed) */ #ifdef LIFETRAC_FHSS_TX_ROUTED @@ -558,6 +590,8 @@ static uint8_t rx_follow_now(void) { * intended action; A6c-2-c-ii could add a saturating "dispatch * refused" sub-counter if observability demands it. */ +static void scan_leave_failed(uint32_t now_ms); + static void scan_dispatch_action(sx1276_rx_scan_action_t action, uint32_t now_ms) { switch (action) { @@ -567,7 +601,15 @@ static void scan_dispatch_action(sx1276_rx_scan_action_t action, * non-final retry resumes from a deterministic point * rather than wherever ADVANCE_CHANNEL last left the * walker mid-pass. ADVANCE_CHANNEL deliberately does - * NOT reset — it must keep marching through the table. */ + * NOT reset — it must keep marching through the table. + * + * RS-12.21 d2 (2026-10-24): a retry no longer comes through + * here. At the 2 s dwell an attempt covers only 15 channels, + * so the retries and (b)'s restarts out of FAILED continue + * the walk (scan_leave_failed dispatches ADVANCE_CHANNEL) + * and a silent node's attempts cover all 50 channels. Only + * a fresh acquisition -- BOOT (activation) or a LOCKED loss + * demotion -- restarts at channel 0. */ sx1276_rx_scan_walker_reset(); /* fallthrough */ case SX1276_RX_SCAN_ACTION_ADVANCE_CHANNEL: { @@ -651,7 +693,9 @@ static void scan_dispatch_action(sx1276_rx_scan_action_t action, * the HOST_FAULT_CODE_RX_SCAN_FAILED URC with the * §13.1 #2 sub-byte, and either re-enter SCANNING with * fresh anchors (non-final) or stay in the absorbing - * FAILED state the policy already committed. */ + * FAILED state the policy already committed. + * RS-12.21 (b) (2026-10-24): FAILED is now held only by a + * node with originator authority (below). */ const sx1276_rx_scan_fail_input_t fi = { .retries = s_scan_fail_retries, .max_retries = SX1276_RX_SCAN_MAX_RETRIES, @@ -665,7 +709,8 @@ static void scan_dispatch_action(sx1276_rx_scan_action_t action, host_cmd_emit_fault(HOST_FAULT_CODE_RX_SCAN_FAILED, fd.sub); if (fd.is_final) { - /* FCC-A6c-2-c-ii: park the modem in standby — no + /* FCC-A6c-2-c-ii (superseded by RS-12.21 (b), last + * paragraph): park the modem in standby — no * point burning power scanning a hopset the remote * provably is not on. Leave s_scan_state at FAILED * (the policy already committed it); external @@ -679,27 +724,60 @@ static void scan_dispatch_action(sx1276_rx_scan_action_t action, * follower re-arms only at the next slot boundary, so * a park here would abort a peer frame already on air * and miss the rest of the slot. The FAILED commit and - * the fault URC above are unchanged. */ - if (rx_follow_own_now() == 0U) { - (void)sx1276_modes_to_standby(); + * the fault URC above are unchanged. + * + * RS-12.21 (b) (2026-10-24, FHSS_SCAN_FAILED_OPTIONS.md + * section 4 (b), adopted by the design owner): the park + * is gone, and FAILED is held ONLY under that authority + * -- a streaming originator keeps following its own + * clock in FAILED, as on 2026-10-18 legs W/X, until the + * authority decays (scan_drive then restarts it). A + * node without it (a follower such as the base, or an + * originator whose authority has decayed) re-enters + * SCANNING at once with fresh anchors, as a non-final + * retry does, and the walk continues (scan_leave_failed). + * The fault above still goes out, final bit set and + * the attempt nibble at MAX (the retry count is not + * incremented), so the host hears "not acquired" every + * 30 s until a LOCK. + * Why: the park stranded a follower. Leg 6 (2026-10- + * 24): the base's receiver stayed on the walker's last + * channel (and later on each PROBE's), it LOCKed on + * nothing and heard 5 of 606 frames, while it kept + * transmitting on hop channels -- a receiver that no + * longer follows the hop sequence, against the "synced + * receivers" of FCC §15.247(a)(1) (FCC notes, 2026-05- + * 19). Only a reset or a profile re-activation got it + * out (host_cfg_profile.c records a 2026-07-24 case). + * Costs: continuous RX instead of STANDBY (fine for + * the base and the tractor; a battery node would want + * the back-off the 2026-05-19 RX-scan-FAILED analysis + * §13.2 deferred -- this replaces that analysis's + * absorbing-FAILED contract). And once the daemons stop + * both boards scan indefinitely, re-arming RX every + * dwell (a stopped tractor's authority decays after + * 1 s), so a host SLEEP write does not hold: the bench + * park is the RS-12.21 firmware park command in the + * same image. */ + if (rx_follow_own_now() != 0U) { + return; } + scan_leave_failed(now_ms); return; } /* Non-final: account the retry, override the policy's * FAILED commit back to SCANNING, reset BOTH anchors so * the next 30 s redesign budget restarts cleanly, and - * dispatch a BEGIN_SCAN-style HW re-entry (next channel - * + standby + set_freq + settle + arm). Per-window - * flags are cleared by the recursive BEGIN_SCAN case - * via this same dispatcher, preserving the invariant - * that got_any_irq + crc_seen reflect only the current - * window. */ - s_scan_fail_retries = (uint8_t)(s_scan_fail_retries + 1U); - s_scan_state = SX1276_RX_SCAN_STATE_SCANNING; - s_scan_channel_entry_ms = now_ms; - s_scan_cold_start_entry_ms = now_ms; - scan_dispatch_action(SX1276_RX_SCAN_ACTION_BEGIN_SCAN, now_ms); + * re-enter on the next channel (scan_leave_failed: since + * RS-12.21 d2 the walk continues across attempts; before + * it this dispatched BEGIN_SCAN, restarting at channel 0). + * Per-window flags are cleared by the walker step it + * dispatches through this same dispatcher, preserving the + * invariant that got_any_irq + crc_seen reflect only the + * current window. */ + s_scan_fail_retries = (uint8_t)(s_scan_fail_retries + 1U); + scan_leave_failed(now_ms); return; } case SX1276_RX_SCAN_ACTION_HOLD: @@ -712,6 +790,34 @@ static void scan_dispatch_action(sx1276_rx_scan_action_t action, } } +/* + * RS-12.21 (2026-10-24): leave a committed FAILED for a fresh 30 s scan + * attempt -- the one way out of FAILED short of a scan reset, taken by + * - a non-final FAIL (the A6c-3 retry; its caller counts the retry), + * - option (b): a final FAIL on a node without originator authority + * (scan_dispatch_action), and + * - option (b): a node in FAILED whose authority has decayed, on its + * next scan tick (scan_drive). + * Both anchors restart, so the attempt gets the full budget. The walk + * CONTINUES where it stopped (d2: ADVANCE_CHANNEL, not BEGIN_SCAN, which + * would reset the walker to channel 0), so a silent node's attempts cover + * the whole table; the step is suppressed like any other while the node + * holds originator authority (a non-final retry can arrive here with it). + * The edge is recorded as (FAILED, BEGIN_SCAN) -- it enters SCANNING with + * both anchors fresh, as BEGIN_SCAN does, and no policy eval produces + * that pair -- so the scan counters, and check-rx-reverse-delivery's + * mirror of them, see every FAILED -> SCANNING edge. (Until 2026-10-24 + * a non-final retry left FAILED without a record.) + */ +static void scan_leave_failed(uint32_t now_ms) { + sx1276_rx_scan_counter_record(SX1276_RX_SCAN_STATE_FAILED, + SX1276_RX_SCAN_ACTION_BEGIN_SCAN); + s_scan_state = SX1276_RX_SCAN_STATE_SCANNING; + s_scan_channel_entry_ms = now_ms; + s_scan_cold_start_entry_ms = now_ms; + scan_dispatch_action(SX1276_RX_SCAN_ACTION_ADVANCE_CHANNEL, now_ms); +} + /* * Shared driver for the A6c-1 SM. Builds the eval input from the * file-static (state, anchors) + the caller-supplied (event, @@ -759,6 +865,37 @@ static void scan_drive(sx1276_rx_scan_event_t event, if (sx1276_tx_busy()) { return; } + /* + * RS-12.21 (2026-10-24): the firmware park freezes the machine as a keyed + * TX does, for ticks and frame events alike: no walker step, no FAIL + * fault, no (b) restart below -- nothing re-arms the parked receiver, + * and a parked board sends no RX_SCAN_FAILED every 30 s. + * sx1276_rx_unpark() moves a SCANNING node's anchors on by the time + * parked, so its dwell and attempt resume where they stood (a LOCKED + * node's loss timer counts the park: see there). + */ + if (s_host_parked != 0U) { + return; + } + /* + * RS-12.21 (b) (2026-10-24): FAILED is held only while the node follows + * its own clock (originator authority, see the FAIL dispatch). Once that + * decays -- a stalled or stopped tractor loses it 1 s after its last own + * TX (sx1276_fhss_authority.h) -- the next tick scans again: a fresh 30 s + * attempt, the walk continuing (scan_leave_failed). TICK only, and only + * past the gates above (a parked node in FAILED restarts on its first + * tick after the park). Before this an originator in FAILED that + * adopted a grid heard in a stall's decay window never held authority, + * self-followed or LOCKed again: in FAILED nothing LOCKs, so nothing + * demotes, so nothing clears the adoption (sx1276_rx_grid_on_demotion) + * -- the tractor twin of leg 6 (check-rx-scan-failed-follower). + */ + if (event == SX1276_RX_SCAN_EVENT_TICK && + s_scan_state == SX1276_RX_SCAN_STATE_FAILED && + rx_follow_own_now() == 0U) { + scan_leave_failed(now_ms); + return; + } const sx1276_rx_scan_input_t in = { .state = s_scan_state, .now_ms = now_ms, @@ -820,8 +957,19 @@ static void scan_drive(sx1276_rx_scan_event_t event, case SX1276_RX_SCAN_ACTION_ADVANCE_CHANNEL: s_scan_channel_entry_ms = now_ms; break; - case SX1276_RX_SCAN_ACTION_HOLD: case SX1276_RX_SCAN_ACTION_LOCK: + /* RS-12.21 L (2026-10-24): the LOCKED 2 s loss timer runs from + * channel_entry_ms, so start it at the locking frame. A LOCK + * used to leave the anchors alone, and the timer then ran from + * the last walker step -- up to one dwell (now 2 s) before the + * LOCK -- so a LOCK late in its dwell demoted early, at a 2 s + * dwell often on the next tick (the SIL, memo section 5 item + * 3: the 2 s dwell without this makes 1.7x the LOCKs, many + * of them demoting at once). REANCHOR keeps doing this for + * every later frame. */ + s_scan_channel_entry_ms = now_ms; + break; + case SX1276_RX_SCAN_ACTION_HOLD: case SX1276_RX_SCAN_ACTION_FAIL: default: /* anchors unchanged */ @@ -863,9 +1011,13 @@ void sx1276_rx_scan_tick(uint32_t now_ms) { * - The TX-side fit guard (SX1276_FHSS_SLOT_TX_GUARD_US) guarantees * packets END before the boundary, so a boundary retune here * cannot clip a frame that is mid-air for us. + * - RS-12.21 (2026-10-24): the host park -- a parked node may sit LOCKED + * on an adopted grid, or hold authority for up to 1 s after its last + * TX; neither may retune or re-arm the sleeping modem. */ void sx1276_rx_slot_follow(uint32_t now_ms) { - if (sx1276_fhss_clock_valid() == 0U || + if (s_host_parked != 0U || + sx1276_fhss_clock_valid() == 0U || sx1276_tx_busy() || rx_follow_now() == 0U) { return; @@ -951,3 +1103,58 @@ void sx1276_rx_scan_reset(void) { } #endif /* LIFETRAC_FHSS_TX_ROUTED */ + +/* + * RS-12.21 (2026-10-24): the firmware radio park (HOST_TYPE_RADIO_PARK_REQ; + * contract in sx1276_rx.h, TODO.md RS-12.21). Why it is firmware: with scan + * option (b) a node without originator authority no longer parks its own + * receiver in scan FAILED, so once the daemons stop both boards scan on, + * re-arming RXCONT every dwell (a stopped tractor's authority decays after + * 1 s), and a host SLEEP write (radio_park.py) holds only until the next + * walker step. Every arm path in this file and sx1276_tx.c checks the flag + * before it touches the modem; sx1276_rx_arm() is the backstop. + */ +bool sx1276_rx_park(void) { + const bool newly = (s_host_parked == 0U); + if (newly) { + s_host_parked = 1U; + s_host_parked_ms = platform_now_ms(); + } + (void)sx1276_modes_to_sleep(); + /* No stale RxDone / CRC-error flag may pair with the first packet after + * the park ends. */ + sx1276_write_reg(SX1276_REG_IRQ_FLAGS, 0xFFU); + return newly; +} + +void sx1276_rx_unpark(void) { + if (s_host_parked == 0U) { + return; + } + s_host_parked = 0U; +#ifdef LIFETRAC_FHSS_TX_ROUTED + /* The scan machine was frozen, so a SCANNING node's timers must not + * count the time parked: it resumes its dwell and its 30 s attempt where + * they stood (no walker step and no RX_SCAN_FAILED at the first tick for + * a dwell or an attempt that never ran). Wrap-safe: both anchors were at + * or before the park, so they stay at or before now. + * LOCKED is NOT shifted (2026-10-24 review): its loss timer, like the + * FHSS clock's F6 freshness it must agree with (FRESH_MS == + * LOCK_LOSS_MS), counts from the last frame, parked time included. The + * adopted grid has drifted meanwhile (HSI16: ~1.5-2 s per 10 min, memo + * section 8.3), so a park longer than LOCK_LOSS_MS demotes on the + * first tick after it -- a fresh acquisition -- instead of following a + * stale grid for up to 2 s. FAILED and BOOT run no timer: FAILED is + * held under authority or left at the next tick, and both ways out + * re-anchor both (scan_leave_failed, BEGIN_SCAN). */ + if (s_scan_state == SX1276_RX_SCAN_STATE_SCANNING) { + const uint32_t parked_ms = platform_now_ms() - s_host_parked_ms; + s_scan_channel_entry_ms += parked_ms; + s_scan_cold_start_entry_ms += parked_ms; + } +#endif +} + +bool sx1276_rx_parked(void) { + return s_host_parked != 0U; +} diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_tx.c b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_tx.c index 44a17f91..15502652 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_tx.c +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072/radio/sx1276_tx.c @@ -156,6 +156,10 @@ static bool time_reached(uint32_t now, uint32_t deadline) { static void sx1276_tx_cleanup(void) { (void)sx1276_modes_to_standby(); if (s_rearm_rx != 0U) { + /* RS-12.21 (2026-10-24): the post-TX re-arm. It cannot meet a host + * park: the park is refused while a TX is keyed or queued + * (host_cmd.c) and sx1276_tx_begin() refuses while parked -- and + * sx1276_rx_arm() refuses while parked regardless. */ (void)sx1276_rx_arm(); /* RS-12.10: the receiver is listening again — book the deaf * window (disarm -> here) and the TX_DONE -> re-arm turnaround. */ @@ -184,6 +188,13 @@ bool sx1276_tx_begin(const sx1276_tx_request_t *req) { if (req == NULL || s_tx_state != SX1276_TX_STATE_IDLE) { return false; } + /* RS-12.21 (2026-10-24): nothing goes on air while the host has the + * radio parked (HOST_TYPE_RADIO_PARK_REQ) -- refused before any side + * effect (no clock anchor, no scheduler step, no RFCO snapshot); the + * host gets the same ERR_PROTO FORBIDDEN as for any refused TX. */ + if (sx1276_rx_parked()) { + return false; + } #ifdef LIFETRAC_FHSS_TX_ROUTED /* @@ -571,6 +582,8 @@ static void tx_fifo_readback_check(void) { host_stats_tx_fifo_rb_ok(); } else { host_stats_tx_fifo_rb_bad(); + /* 0x0F since 2026-10-24 (0x0D before: it collided with + * RX_SCAN_FAILED, host_types.h). */ host_cmd_emit_fault(HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH, s_tx_id); } } diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/tractor_h7/murata_host/mh_wire.h b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/tractor_h7/murata_host/mh_wire.h index 62853346..a9288f25 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/tractor_h7/murata_host/mh_wire.h +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/tractor_h7/murata_host/mh_wire.h @@ -12,6 +12,9 @@ #define HOST_TYPE_VER_REQ 0x01U #define HOST_TYPE_UID_REQ 0x02U #define HOST_TYPE_RESET_REQ 0x03U +/* RS-12.21 (2026-10-24) firmware radio park (mirror of host_types.h; payload + * none; reply RADIO_PARK_URC {u8 status, u8 RegOpMode readback}). */ +#define HOST_TYPE_RADIO_PARK_REQ 0x04U #define HOST_TYPE_TX_FRAME_REQ 0x10U @@ -26,6 +29,11 @@ #define HOST_TYPE_VER_URC 0x81U #define HOST_TYPE_UID_URC 0x82U +#define HOST_TYPE_RADIO_PARK_URC 0x84U +#define HOST_RADIO_PARK_URC_PAYLOAD_LEN 2U +#define HOST_RADIO_PARK_STATUS_PARKED 0U +#define HOST_RADIO_PARK_STATUS_ALREADY 1U +#define HOST_RADIO_PARK_STATUS_REFUSED 2U #define HOST_TYPE_TX_DONE_URC 0x90U #define HOST_TYPE_RX_FRAME_URC 0x91U #define HOST_RX_FRAME_URC_TAIL_LEN 8U @@ -41,6 +49,10 @@ #define HOST_TYPE_ERR_PROTO_URC 0xFEU #define HOST_FAULT_CODE_CLOCK_HSE_FAILED 0x08U +/* The 0x0D split (2026-10-24, mirror of host_types.h): 0x0D is RX_SCAN_FAILED + * only; TX_FIFO_RB_MISMATCH moved from 0x0D to 0x0F. */ +#define HOST_FAULT_CODE_RX_SCAN_FAILED 0x0DU +#define HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH 0x0FU #define HOST_STATS_OFFSET_HOST_DROPPED 0U #define HOST_STATS_OFFSET_HOST_ERRORS 4U 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 a72f6a31..0edc8358 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 @@ -35,14 +35,26 @@ 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 `13f20aa1` -(#152, the RS-12.15 reverse-delivery fix; merged as `main` `3c8ced60` with -the `firmware/murata_l072` source unchanged): +with WinLibs `mingw32-make` run from PowerShell. The source now builds the +RS-12.21 image (scan option (b) + L + d2, the firmware park command, the +0x0D split; built 2026-10-25 and again after `mingw32-make clean`, **not +flashed**). Its flash and the checks after it are planned step by step in +[FHSS_SCAN_FAILED_OPTIONS.md](../murata_l072/FHSS_SCAN_FAILED_OPTIONS.md) +section 6.1: + +| build | md5 | size | +|---|---|---| +| bench `build/firmware_bench_diag.bin` (not flashed yet) | `0daae27978089eb97ab35c18718b62b0` | 25 324 B | +| production `build/firmware.bin` (committed) | `2c5e055abff95126309a62409fafc654` | 25 324 B | + +On the boards until it is flashed, at `13f20aa1` (#152, the RS-12.15 +reverse-delivery fix; merged as `main` `3c8ced60` with the +`firmware/murata_l072` source unchanged): | build | md5 | size | |---|---|---| | bench `build/firmware_bench_diag.bin` (on both boards since 2026-10-18) | `e083ae46a64686031b39866eca42e094` | 24 972 B | -| production `build/firmware.bin` (committed) | `148de24e1f9acce417c46c2c69a5be6b` | 24 972 B | +| production `build/firmware.bin` | `148de24e1f9acce417c46c2c69a5be6b` | 24 972 B | Before #152 the pair was bench `0c1bb0a9573f813137f941dfa47177d0`, on both boards 2026-09-15 → 2026-10-18, and production @@ -220,6 +232,23 @@ 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. +**On the RS-12.21 image** (2026-10-25, not flashed yet) the VER reply's +capability bitmap reads `0x000000FF` (`0x0000007F` on `e083ae46` and +earlier; VER's version and name stay `0.0.0` / `dev`, so this bit is how +a host tells the images apart). `radio_park.py` then parks by the firmware +command: its JSON says `"cap_bitmap": "0x000000FF"`, `"path": "firmware"`, +`"park_status": "PARKED"` and `"persistent": true`, and the park holds +until a host RXCONT write, a profile activation (any daemon start) or a +reset. Run `rs116_health_probe.py` before the park, and confirm the park +with the read-only `radio_state.py` at least 2 min later: `0x80`. Right +after a flash the L072 is on its boot profile (FHSS not initialised, no +scan; the old register park held there too). After a leg, profile 1 stays +active, and on this image the scan of a node without authority never +fails out: only the firmware park holds then, and one that does not hold +is a firmware defect, not a timing matter. The flash-day plan: +[FHSS_SCAN_FAILED_OPTIONS.md](../murata_l072/FHSS_SCAN_FAILED_OPTIONS.md) +section 6.1. + ## 5. First flash from a factory board This section collects guidance from the 2026-05 bring-up notes that was not 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 index ae834105..6cf6b3f7 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/README.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/README.md @@ -47,7 +47,9 @@ instead of relying on the default. it with `mingw32-make bench` in [`../murata_l072/`](../murata_l072/). That gives `build/firmware_bench_diag.bin`, md5 `e083ae46a64686031b39866eca42e094` (#152; the previous build `0c1bb0a9` - is the rollback image); toolchain and expected hashes are in + is the rollback image; the RS-12.21 source of 2026-10-25 builds + `0daae27978089eb97ab35c18718b62b0`, not flashed yet); toolchain and + expected hashes are in [`../murata_l072/README.md`](../murata_l072/README.md). Saved copies of the earlier flashed bench builds are in [`bench-evidence/RS_13_vector_scene_2026-09-26/firmware/`](../../bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md); diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_BOARDS.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_BOARDS.md index daee14ba..f4047068 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_BOARDS.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_BOARDS.md @@ -56,7 +56,7 @@ goes with which module is not recorded. | chip | where | runs | LifeTrac-custom? | saved | notes | |---|---|---|---|---|---| -| STM32L072 in the Murata CMWX1ZZABZ (+ SX1276, no firmware) | carrier | `firmware/murata_l072` bench build **`e083ae46`** since 2026-10-18 (RS-12.15 reverse delivery, #152); before that `0c1bb0a9`, 2026-09-15 → 2026-10-18 | **yes** | Source on `main` (`13f20aa1`, merged as `3c8ced60`). `e083ae46` builds byte-identically with arm-none-eabi-gcc **12.2.1 (Arm GNU Toolchain 12.2.MPACBTI-Rel1)**; the copy flashed on 2026-10-18 is in [`RS_12_15…/firmware_flash/`](../../../bench-evidence/RS_12_15_reverse_delivery_2026-10-18/firmware_flash/). Older binaries, the `0c1bb0a9` rollback image among them, and the full flashed-build inventory are in [`RS_13…/firmware/`](../../../bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md). | The firmware never writes its own flash or EEPROM. Its option bytes were never recorded. The only proof of what is on the chip is the 2026-10-18 flash logs (Verify OK, `flash_rc=0` on both boards; copies in `RS_12_15…/firmware_flash/`; `run_flash_bench.sh` deletes the board copies at the next flash). | +| STM32L072 in the Murata CMWX1ZZABZ (+ SX1276, no firmware) | carrier | `firmware/murata_l072` bench build **`e083ae46`** since 2026-10-18 (RS-12.15 reverse delivery, #152); before that `0c1bb0a9`, 2026-09-15 → 2026-10-18. The RS-12.21 image (bench `0daae279`) is built, not flashed (2026-10-25; below) | **yes** | Source on `main` (`13f20aa1`, merged as `3c8ced60`). `e083ae46` builds byte-identically with arm-none-eabi-gcc **12.2.1 (Arm GNU Toolchain 12.2.MPACBTI-Rel1)**; the copy flashed on 2026-10-18 is in [`RS_12_15…/firmware_flash/`](../../../bench-evidence/RS_12_15_reverse_delivery_2026-10-18/firmware_flash/). Older binaries, the `0c1bb0a9` rollback image among them, and the full flashed-build inventory are in [`RS_13…/firmware/`](../../../bench-evidence/RS_13_vector_scene_2026-09-26/firmware/README.md). | The firmware never writes its own flash or EEPROM. Its option bytes were never recorded. The only proof of what is on the chip is the 2026-10-18 flash logs (Verify OK, `flash_rc=0` on both boards; copies in `RS_12_15…/firmware_flash/`; `run_flash_bench.sh` deletes the board copies at the next flash). | | STM32H747 Cortex-M7 (flash bank 1) | X8 module | **stock Arduino x8h7** SPI bridge, programmed at boot by `stm32h7-program.service` from `/usr/arduino/extra/STM32H747AII6_CM7.bin`. The image differs per board: base sha256 `d81eaa81…`, tractor `0b03cd9c…`. | no | Part of each LmP image | **The tractor reflashes it at every boot.** Its `program-h7.sh` cannot read the version (the sysfs read times out), so it reprograms bank 1, `0x08000000`–`0x080709a4`, ending "Programming Finished … Verified OK"; bank 2 is not touched. The base's newer script prints "matches, No Update" after the same failed read, so on the base it is not proof. LifeTrac overwrote this bank with `tractor_h7` on 2026-05-04; it is stock again. | | STM32H747 Cortex-M4 (flash bank 2, `0x08100000`) | X8 module | **unknown, probably a May 2026 LifeTrac bench sketch**, started at every boot by x8h7 | probably | Sources only: `firmware/x8_uart_route_probe`, `x8_lora_bootloader_helper`, `portenta_m7_l072_passthrough_ping`, `tractor_h7`. The flashed ELFs were never kept. | Last recorded uploads: x8_uart_route_probe → 2D0A, x8_lora_bootloader_helper → 2E2C (the May notes mix up COM ports, so the per-board mapping is uncertain). No erase is recorded. No visible effect, since both L072s boot normally, but it is undocumented code. Identify it with the openocd dump in *Open items*, then decide whether to blank the slot. | | i.MX8M Mini boot chain, kernel, device tree | X8 module | stock LmP (U-Boot arms the 60 s WDOG) | no (two May bootargs/BLS experiments were removed by later reflashes) | 934 bundle on the PC (private archive, sha256) | The i.MX's own Cortex-M4 is unused. | @@ -76,6 +76,16 @@ Repo L072 binaries that are on no board: "hello v0.2". - `board2_l072_flash_dump.bin` is a readback of an early LifeTrac build, not the factory image. +- `firmware/murata_l072/build/firmware.bin` and the untracked + `build/firmware_bench_diag.bin` are the RS-12.21 image since 2026-10-25 + (scan option (b) + L + d2, the firmware park command, the 0x0D split): + bench `0daae27978089eb97ab35c18718b62b0`, production + `2c5e055abff95126309a62409fafc654`, 25 324 B each + ([Expected md5s](../../murata_l072/README.md#expected-md5s)). **Built, not + flashed**: both boards run `e083ae46` until a flash GO. The flash and its + checks are planned in + [FHSS_SCAN_FAILED_OPTIONS.md](../../murata_l072/FHSS_SCAN_FAILED_OPTIONS.md) + §6.1; after it, update this file's L072 row and the date at the top. ## LifeTrac software living in the X8 Linux 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 index 80a342e6..582d147b 100644 --- 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 @@ -132,15 +132,23 @@ TI=$TRACTOR_PROBE_IMAGE # tractor probe image ## 3. Radio firmware (L072) Both boards run the bench build **`e083ae46`** (since 2026-10-18). The -production `build/firmware.bin` (`148de24e`) refuses the bench carrier pin, -so do not put it on a bench board. +production `build/firmware.bin` refuses the bench carrier pin, so do not put +it on a bench board. The source now builds the RS-12.21 image (scan option +(b) + L + d2, the firmware park command, the 0x0D split): bench +**`0daae279`**, production `2c5e055a` (25 324 B, built 2026-10-25). It is +**built, not flashed**; flashing it needs its own GO +([Expected md5s](../../murata_l072/README.md#expected-md5s)). The flash and +the checks after it are planned step by step in +[FHSS_SCAN_FAILED_OPTIONS.md](../../murata_l072/FHSS_SCAN_FAILED_OPTIONS.md) +§6.1. On that image the VER capability bitmap reads `0x000000FF` (step 8: +the firmware park). ```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 # E083AE46A64686031B39866ECA42E094 +(Get-FileHash build\firmware_bench_diag.bin -Algorithm MD5).Hash # 0DAAE27978089EB97AB35C18718B62B0 (e083ae46 before RS-12.21) ``` A different md5 means a different toolchain; see the toolchain section of @@ -517,14 +525,29 @@ void or re-fly it only as the pass rule written before leg 1 says (the template's "never locks" row is a proposal until the operator agrees it). A re-fly needs a GO. Details: BENCH_RUNBOOK, "A leg", Base acquisition. +On the RS-12.21 image (built 2026-10-25, not flashed yet) the base's scan +does not stay in FAILED: after its fourth failed attempt it scans on and +sends a final RX_SCAN_FAILED every 30 s until it locks, so a final there is +a 30 s report and a lock after it is a late acquisition. The report and +`rs13_probe_score.py` tell the images apart by `--scan-image` (default +`auto`: the base daemon's `L072 VER warm-up ok ... cap=0x000000FF` line); +give `--scan-image restarts` or `absorbing` when the log cannot say. + **FAULT lines** (2026-10-24; not yet flown). Both daemon logs now carry one `FAULT_URC: code=0x.. sub=0x.. name=...` line per FAULT the L072 sends (WARNING; INFO for 0x09 and 0x0C), with `uptime_ms=`, the L072's uptime. -Code 0x0D is either a failed scan attempt (`name=RX_SCAN_FAILED`, with -`attempt=` and `final=`; `final=1` means the scan went FAILED) or a TX FIFO -readback mismatch (`name=TX_FIFO_RB_MISMATCH`); `name=AMBIGUOUS` when the -daemon cannot tell. Count faults on these lines only: the same URC can also -leave an older line. Details: BENCH_RUNBOOK, "A leg", FAULT lines. +Code 0x0D is a failed scan attempt (`name=RX_SCAN_FAILED`, with `attempt=` +and `final=`; `final=1` means the retries are spent: FAILED on `e083ae46`, +another final every 30 s on the RS-12.21 image). The TX FIFO readback +mismatch is code **0x0F** (`name=TX_FIFO_RB_MISMATCH tx_id=0x..`) on the +RS-12.21 image, which split the two; `e083ae46` and older still send it as +0x0D, and the daemon then writes `name=TX_FIFO_RB_MISMATCH tx_id=0x.. +fw=old` (any 0x0D sub that is not a scan encoding). `name=AMBIGUOUS` comes +only from the daemons of 2026-10-24 before the split. Count faults on these +lines only: the same URC can also leave an older line. The harness trigger +`-InjectOn fault:0x0D:final` fires on the scan's attempt-3 fault only +(not on bit 7 alone). Details: BENCH_RUNBOOK, "A leg", FAULT lines and +command injectors. `bracket_deltas.py ` scores the L072 counter brackets (reset-aware, cross-checked against `leg_gaps.txt`; the RS-12.15 criteria on FHSS legs @@ -565,20 +588,32 @@ so (BENCH_RUNBOOK, `rs13_probe_score.py`). ## 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. `leg_post.sh` does this at the end of a leg; by hand: +Park both radios with [`radio_park.py`](radio_park.py). `leg_post.sh` does +this at the end of a leg; by hand: ```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), or run -`bash "$BT/legs/power_up_guard.sh" --check-only`. +The script picks one of two paths from the VER reply (its JSON says which, +`"path"`): + +- **firmware** (the RS-12.21 image, `"cap_bitmap": "0x000000FF"`; built + 2026-10-25, not flashed yet): the park command. The L072 stops every + receiver re-arm and sleeps until a host RXCONT write, a profile + activation (the next daemon start) or a reset: `"persistent": true`. It + works at once, with no wait. A `PARK_FAIL` (no answer, or refused while a + TX is on air) or a `PARK_TRANSIENT` on this path is a fault to report, + not something to wait out. +- **register** (`e083ae46` and older, `"cap_bitmap": "0x0000007F"`): the + old SLEEP write, which holds only once the firmware's scan has failed out, + about 2 minutes after the daemons stop. Park after that. `PARK_TRANSIENT` + (exit 5) means the scan was still walking: wait 60 s and repeat. + +Either way, before you call the bench off, re-check both boards at least +2 minutes later with the read-only [`radio_state.py`](radio_state.py) (same +command line), or run `bash "$BT/legs/power_up_guard.sh" --check-only`. To power down: 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 8c071e23..4de99fc4 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 @@ -21,7 +21,7 @@ per-session reference and the trap list. The rest of the bench kit: | file | runs on | purpose | |---|---|---| | `run_flash_bench.sh` + `stamp.py` + `kmsg_log.py` | board, `/home/fio` (persistent) | instrumented wrapper around `full_flash_pipeline.sh`: monotonic-stamped pipeline log + kernel log, fsync'd so a PMIC power-cycle cannot lose them. `REVIVE_MODE=reboot bash /home/fio/run_flash_bench.sh `; success = `Verify OK` + `flash_rc=0` in `/home/fio/pipeline_stamped.log` (~2.5 min incl. reboot). | -| `radio_park.py` | board, daemon container | STANDBY → SLEEP, reads RegOpMode back, waits 1.5 s, reads again; prints `PARK_OK` only if still `0x80` (`PARK_TRANSIENT`, exit 5, if the firmware re-armed it). **The readback is NOT a persistent-off guarantee** (PR #125 review): the firmware's scan walker, γ-1 retune and slot follower all call `sx1276_rx_arm()` and will bring the modem back to RXCONT (receive-only) while any of them still has work. The park holds once the scan SM has exhausted into FAILED — up to ~2 min after the daemons stop (500 ms/channel, hard FAIL at 30 s per attempt, 3 retries). Park after that, and confirm later with `radio_state.py`. Firmware park command = TODO RS-12.21. | +| `radio_park.py` | board, daemon container | Puts the radio in LoRa SLEEP (`0x80`) by one of two paths, picked by the VER reply's capability bitmap, then reads RegOpMode twice, 2.5 s apart (1.5 s until 2026-10-24); prints `PARK_OK` only if asleep at both reads. **Firmware path** (2026-10-24, RS-12.21; the image with capability bit 7, bitmap `0x000000FF`: built 2026-10-25, not flashed yet): the park command (`RADIO_PARK_REQ` 0x04, up to 3 tries) sets the L072's `host_parked` flag, which gates every RX arm path (scan walker, γ-1 retune, slot follower, the post-TX re-arm, the scan restarts), and puts the modem to SLEEP. **Persistent:** it holds until a host raw RXCONT write, a profile activation (any daemon start) or a reset. No answer from that image, or a refusal (a TX on air or queued), is `PARK_FAIL` (exit 4) with no register write; a `PARK_TRANSIENT` there is a firmware defect (an arm path the gate missed). **Register path** (`e083ae46` and older, which answer the command `UNKNOWN_TYPE` or not at all): STANDBY → SLEEP register writes. **Transient** (PR #125 review): the firmware's scan walker, γ-1 retune and slot follower all call `sx1276_rx_arm()` and bring the modem back to RXCONT (receive-only) while any of them still has work, so the park holds only once the scan SM has exhausted into FAILED — up to ~2 min after the daemons stop (500 ms/channel, hard FAIL at 30 s per attempt, 3 retries); `PARK_TRANSIENT` (exit 5) means it was still walking. Park after that, and confirm later with `radio_state.py`. The last line is `PARK_OK` / `PARK_TRANSIENT` / `PARK_FAIL` plus JSON: `path`, `persistent`, `holds`, `cap_bitmap`, `park_tries`, and `park_status` (PARKED / ALREADY_PARKED / REFUSED) or `park_cmd` (unknown_type / no_reply / err_proto). | | `radio_state.py` | board, daemon container | read-only RegOpMode decode — answers "is it off?" without touching it. | | `clear_retained.py` / `clear_retained_host.py ` | base (container) / PC | clear the retained control topics (`encode_mode_override`, `req_keyframe`, `radio_profile`) on the base broker / on another broker. A stale retained pin has flipped the camera and re-commanded a profile mid-leg; clear before EVERY leg. | | `kf_inject.py ` | base (container) | REQ_KEYFRAME storm; the unacked retries at 0.4/0.8 s make the 2–3-command bursts that LOCK the tractor's scan machine (the RS-12.15 trigger). | @@ -112,6 +112,12 @@ leg's `synth_fps` ("A leg", Base acquisition). `3c8ced60` with the L072 source unchanged; production twin `148de24e`). Flash record and a copy of the image: `bench-evidence/RS_12_15_reverse_delivery_2026-10-18/firmware_flash/`. + - **Built, not flashed (2026-10-25):** the RS-12.21 image (scan option + (b) + L + d2, the firmware park command, the 0x0D split; DECISIONS.md + D-FHSS1), bench `0daae279`, production `2c5e055a`, 25 324 B each. Its + flash and first legs are planned in + [`FHSS_SCAN_FAILED_OPTIONS.md`](../../murata_l072/FHSS_SCAN_FAILED_OPTIONS.md) + §6.1; "A leg", "First flash and legs on the RS-12.21 image" below. - The previous bench build, `0c1bb0a9` (RS-12.15 v2, on both boards 2026-09-15 → 2026-10-18), comes from `main` before #152 (re-checked at `f6e4b463` on 2026-10-14). It is the rollback image. @@ -202,7 +208,18 @@ line on the base from rx_smoke's own log (`inject_watch.sh`), not from the console tail that started leg W (2026-10-18) 2 min 41 s late. `-InjectOn fault:0x0D:final` starts it at the tractor's final RX_SCAN_FAILED (leg X); `fault:` and `fault::` also work -(the PC polls the tractor's tx_smoke log about every 2 s). At leg end the +(the PC polls the tractor's tx_smoke log about every 2 s). Since +2026-10-24 (the 0x0D split; offline-tested, not yet flown) `final` means +code 0x0D with a valid scan sub at attempt 3 (0x83, 0x93, 0xA3, 0xC3, +0xD3 or 0xE3), not bit 7 alone: the tractor's command tx_id 0xFE (the +base's 0xFD) has bit 7 too, so an old image's FIFO fault on a command +frame fired the old test. The 0x80 NULL-input marker no longer fires it, +and the harness refuses `fault::final` for any code but 0x0D. The +FIFO fault of an image with the split is `fault:0x0F` (or +`fault:0x0F:`). On the RS-12.21 image a tractor without authority +(its stream stalled for 1 s or more) restarts its scan after the final +fault and sends another final every 30 s; the trigger fires on the first +only. At leg end the harness stops the injector if it is still running, prints `INJECT ... trigger: ...`, and archives `inject.log` (trigger, injector output, exit) and `inject_trigger.txt` (PC-side timeline). Not yet @@ -335,29 +352,42 @@ list). `uptime_ms` is the L072's uptime when it sent the URC (payload bytes bfar=` print for a CPU fault (a record replayed from the previous boot) or when any of them is non-zero. -Code 0x0D is two faults in the firmware (`host_types.h`): +Code 0x0D was two faults in the firmware up to `e083ae46`: TX_FIFO_RB_MISMATCH (`sx1276_tx.c`: sub = that TX's tx_id, sent just before its TX_DONE) and RX_SCAN_FAILED (`sx1276_rx.c`: sub bit 7 `final`, the last -attempt, after which the scan is FAILED; bit 6 `warm`, this boot has LOCKED -before; bit 5 `hw_suspect`, the attempt saw no DIO0 IRQ and no CRC error; -bit 4 `crc_seen`; bits 3–0 the attempt, 0 to 3). Only 25 sub values are scan -encodings: attempts 0–2 not final, attempt 3 final, `hw_suspect` never with -`crc_seen`, plus 0x80, the firmware's defensive NULL-input marker. The -daemon decides with its own TX state and writes the decision as `name=`: - -| `name=` | `basis=` | when | +attempt, the retries spent; bit 6 `warm`, this boot has LOCKED before; bit +5 `hw_suspect`, the attempt saw no DIO0 IRQ and no CRC error; bit 4 +`crc_seen`; bits 3–0 the attempt, 0 to 3). **The 0x0D split** (2026-10-24, +the RS-12.21 image; built 2026-10-25, not flashed yet): RX_SCAN_FAILED +keeps 0x0D and TX_FIFO_RB_MISMATCH moves to **0x0F** (`host_types.h`). +Only 25 sub values are scan encodings: attempts 0–2 not final, attempt 3 +final, `hw_suspect` never with `crc_seen`, plus 0x80, the firmware's +defensive NULL-input marker. The daemons read 0x0D by its sub alone (their +FAULT hook has no VER reply to tell the images apart; on an image with the +split every 0x0D is a scan fault, so the rule is exact there): + +| code, sub | `name=` and detail | meaning | |---|---|---| -| `TX_FIFO_RB_MISMATCH` | `not_scan_encoding` | the sub is not a scan encoding (certain) | -| `RX_SCAN_FAILED` | `tx_id_not_in_flight` | a scan encoding, and no TX with that tx_id awaits its TX_DONE (the FIFO fault comes while its TX is in flight) | -| `AMBIGUOUS` | `tx_id_in_flight` | a scan encoding that is also an in-flight tx_id, e.g. sub 0x00 while fragment 0 is in flight | -| `AMBIGUOUS` | `no_tx_context` | no TX state to judge by | - -The 0x0D detail tokens are `attempt= final= warm= hw_suspect= crc_seen=` -when the scan reading is possible, `tx_id=0xSS` when the FIFO reading is, -then `basis=` and `tx_inflight=` (the tx_ids in flight; `-` for none, `?` -when unknown). A `final=1` RX_SCAN_FAILED (sub 0x83, 0x93, 0xA3, 0xC3, 0xD3 -or 0xE3, or the 0x80 marker) is the scan entering FAILED; the attempts come -about 30 s apart. Leg 6's first tractor 0x0D (`tx_daemon.log:237`, `FAULT +| 0x0D, a scan encoding | `RX_SCAN_FAILED attempt= final= warm= hw_suspect= crc_seen=`, then `basis=` | a scan attempt failed, on any image | +| 0x0D, any other sub | `TX_FIFO_RB_MISMATCH tx_id=0xSS fw=old basis=not_scan_encoding` | an old image's FIFO fault (`fw=old`: no image with the split sends it) | +| 0x0F | `TX_FIFO_RB_MISMATCH tx_id=0xSS` (no `basis=`, like 0x0E) | the FIFO fault of an image with the split | + +On a 0x0D line `basis=` keeps PR #166's in-flight test, which matters only +on an old image: `tx_id_not_in_flight` (no TX with tx_id = sub awaited its +TX_DONE: a scan fault on any image), `tx_id_in_flight` (one did: on an old +image it could also be that TX's FIFO fault, e.g. sub 0x00 while fragment 0 +is in flight; read the board's `tx_fifo_rb_bad`, 0 in every archived +reading), `no_tx_context` (no TX state to judge by). `tx_inflight=` lists +the tx_ids in flight (`-` for none, `?` when unknown). PR #166's daemons +(2026-10-24, before the split) wrote `name=AMBIGUOUS` for the last two +cases; the daemons no longer write it, and `rs12_leg_report.py` still reads +it in old logs. A `final=1` RX_SCAN_FAILED (sub 0x83, 0x93, 0xA3, 0xC3, +0xD3 or 0xE3, or the 0x80 marker) means the scan's retries are spent; the +attempts come about 30 s apart. On `e083ae46` and older the scan then +enters FAILED, which is absorbing. On the RS-12.21 image only a node with +originator authority (a streaming tractor) holds FAILED; any other node +scans on and sends another final RX_SCAN_FAILED every 30 s until it locks +(scan option (b)). Leg 6's first tractor 0x0D (`tx_daemon.log:237`, `FAULT during pipelined TX: code=0x0D sub=0x20`) reads like this in the new form, with fragment 0x00 in flight: @@ -384,8 +414,26 @@ it, and a follower there never hops, so it hears only what crosses the channel it is parked on. On 2026-10-24 leg 6 (2b_f3) the base heard 5 of 606 frames and fired 5 probes, and the rule written before leg 1 pooled its 4/5; its FAILED entry was inferred, not seen -(`bench-evidence/bench_session_2026-10-24/RESULTS.md`, Anomalies). Three -checks now flag such a leg: +(`bench-evidence/bench_session_2026-10-24/RESULTS.md`, Anomalies). + +That is `e083ae46` and older. **On the RS-12.21 image** (2026-10-25, built, +not flashed yet; DECISIONS.md D-FHSS1) the dwell is 2 s per channel and the +walk goes on across attempts, and a node without originator authority (the +base, always) does not stay in FAILED: after the fourth failure it sends +the final RX_SCAN_FAILED and scans on, with another final every 30 s until +it locks. A lock after a final is a late acquisition, not a failure. +`rs12_leg_report.py` and `rs13_probe_score.py` therefore take `--scan-image +auto|restarts|absorbing` (default `auto`): `auto` reads the image from the +base daemon's `L072 VER warm-up ok (attempt N) cap=0x...` line (capability +bit 7, `HOST_CAP_RADIO_PARK`, marks the RS-12.21 image), else from a +RX_SCAN_FAILED logged after a final one (an absorbing FAILED sends none). +With neither, the old absorbing rule below applies and the FAILED line says +that the image was not identified and how to override it. On the RS-12.21 +image a cold or early final is a 30 s report: the verdict is ACQUIRED (a +late acquisition) when a lock can be placed after the chain of finals; +when it cannot and the log ends within 35 s of the last final, it is +FAILED if no lock shows and NOT DETERMINED if one does. Three checks now +flag such a leg: - `rs12_leg_report.py`: an `acquisition:` line, after the loss lines, gives the base's first image reception (its first `frag_arrival` or `published @@ -522,6 +570,38 @@ and Base acquisition, above; not yet flown). Confirm: acquisition:` line, and `leg_digest_trace.txt` places its head too (it found the archive). +**First flash and legs on the RS-12.21 image** (bench `0daae279`; planned +2026-10-25, not yet flown; the full step list with its STOP rules is +[`FHSS_SCAN_FAILED_OPTIONS.md`](../../murata_l072/FHSS_SCAN_FAILED_OPTIONS.md) +§6.1). The flash needs its own GO and each leg its own. Fly from a +checkout that holds the image's host side (the harness pushes the daemons +and probe helpers from it). Confirm: + +- the build: `mingw32-make check` all `[PASS]`, bench md5 + `0daae27978089eb97ab35c18718b62b0`, production + `2c5e055abff95126309a62409fafc654`; `flash_l072.sh` names it + `RS-12.21 bench build`, then `Verify OK`, `flash_rc=0` on each board + (tractor first); +- after each flash: `rs116_health_probe.py` as before (`STATS-OK`, + `radio_state=4`, `tx_fifo_rb_bad` 0), then `radio_park.py` → `PARK_OK` + with `"cap_bitmap": "0x000000FF"`, `"path": "firmware"`, `"park_status": + "PARKED"`, and `radio_state.py` still `0x80` at least 2 min later + (`0x0000007F` or `"path": "register"` means the old image is still + there); +- R1, a profile-1 `2c` leg (`leg_prep.sh 2c_ image_bw250 + --reactive-fire 1`): the base's `L072 VER warm-up ok ... cap=0x000000FF` + line, `confirmed acquisition: ACQUIRED` within 120 s, every 0x0D line + `name=RX_SCAN_FAILED` with no `fw=old`, no 0x0F line and + Δ`tx_fifo_rb_bad` 0 on both boards, no `!! BASE NEVER LOCKED`, and the + leg-end park on the firmware path at the first attempt, still `0x80` at + the `--check-only` reading at least 2 min later (profile 1 is still + active there, so this is the test of the park gate); +- R2, the 2026-10-18 leg-X shape (480 s, KF requests off, no reactive + fire, `-InjectCmd "dual_inject.py 120" -InjectOn fault:0x0D:final`): + the tractor's attempts 0-2 and its final attempt 3 about 30 s apart, + the injector started on that attempt-3 line and on no earlier one, the + base's commands heard with the tractor in FAILED. + Counter deltas: the harness fires an SWD reset of the tractor's L072 at leg start. At leg end it judges that reset from its openocd log: `TRACTOR_L072_RESET landed|failed|unknown (...)` in the `=== LEG CHECKS ===` @@ -597,6 +677,18 @@ 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. +**On the RS-12.21 image** (built 2026-10-25, not flashed yet) the timing +above no longer applies. After an FHSS (profile 1) leg, with the daemons +down, the scan of both boards never fails out (scan option (b): neither +has originator authority), so a register park would never hold. +`radio_park.py` reads the VER capability bitmap (`0x000000FF`) and uses +the firmware park command instead, which holds right away: `PARK_OK` with `"path": "firmware"` and `"persistent": +true` at the first attempt, no wait. It holds until a host RXCONT write, a +profile activation (the next daemon start) or a reset. Still confirm with +`radio_state.py` (or `--check-only`) at least ~2 min later before calling +the bench off: on this image anything but `0x80` then is a firmware +defect, not a scan still walking. + ### L072 resets at leg start Analysis of 2026-10-18 (no board access). The harness reports both resets diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_SETUP.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_SETUP.md index f1e92521..a892d3f8 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_SETUP.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/BENCH_SETUP.md @@ -51,7 +51,11 @@ radio test. requests. If the radios must stay off, park them with `radio_park.py` (step 8). Its `0x80` readback is not a lasting guarantee while the firmware's scan is still running, so confirm a few minutes - later with the read-only `radio_state.py`. + later with the read-only `radio_state.py`. On the RS-12.21 image (built + 2026-10-25, not flashed yet; VER capability bitmap `0x000000FF`) + `radio_park.py` uses the firmware park command instead, which holds + until a host RXCONT write, a profile activation or a reset (still + confirm with `radio_state.py`). 3. **Flashing the L072 turns its receiver on.** A probe that connects to it (rs116, radio_park, radio_state) also wakes it. 4. **The tractor's camera unit takes the radio UART.** At every boot, and @@ -299,7 +303,10 @@ until you flash the LifeTrac build. 1. **Build** in PowerShell, in `LifeTrac-v25/DESIGN-CONTROLLER/firmware/murata_l072`: `mingw32-make bench` makes `build/firmware_bench_diag.bin`, md5 - `e083ae46a64686031b39866eca42e094` (before #152: `0c1bb0a9`). Use + `e083ae46a64686031b39866eca42e094` (before #152: `0c1bb0a9`; from the + RS-12.21 source of 2026-10-25 on, `0daae27978089eb97ab35c18718b62b0`, + built but not flashed yet, so the flown `e083ae46` copy is in + [`RS_12_15…/firmware_flash/`](../../../bench-evidence/RS_12_15_reverse_delivery_2026-10-18/firmware_flash/)). Use arm-none-eabi-gcc 12.2.1 (Arm GNU Toolchain 12.2.MPACBTI-Rel1); the toolchain and the expected md5 are in [`murata_l072/README.md`](../../murata_l072/README.md). The 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 index 059ee281..ada3d7f8 100644 --- 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 @@ -135,8 +135,10 @@ That key is the dummy key CI uses. Real fleet keys (the PC's gitignored 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 `e083ae46` and the production -image `148de24e` (before #152: `0c1bb0a9` / `589c1203`) byte for byte. Any other GCC, including CI's Ubuntu +Only this toolchain rebuilds the bench image and the production image byte +for byte: from the RS-12.21 source (2026-10-25, built, not flashed yet) +bench `0daae279` and production `2c5e055a`; the flown pair `e083ae46` / +`148de24e` (before #152: `0c1bb0a9` / `589c1203`). 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). diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/bracket_deltas.py b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/bracket_deltas.py index c0b01e03..e2d41564 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/bracket_deltas.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/bracket_deltas.py @@ -388,8 +388,9 @@ def never_locked(v, brackets): return (f"!! BASE NEVER LOCKED: base d(clk_demotion_reset)=0 and d(fhss_dec_aligned)=0 " f"while the tractor sent {streamed} (d(radio_tx_ok)); {state}. The fingerprint of a " f"base FHSS scan that ran out into FAILED (2026-10-24 leg 6; rs12_leg_report.py's " - f"acquisition line): the base never followed the hop grid and heard only what " - f"crossed the channel it was parked on. The criteria above are scored as before and " + f"acquisition line), or that scanned on without a lock on the RS-12.21 image: the " + f"base never followed the hop grid and heard only what crossed the channel it was " + f"parked or dwelling on. The criteria above are scored as before and " f"do not test FHSS reverse delivery on this leg: the tractor's clock counters stay " f"clean when its peer never hops, and command delivery counts only the probes the " f"base fired after a frame it happened to catch ({tr}/{bt}). Re-fly the leg, or void " 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 index bcf501f0..92ed43cf 100755 --- 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 @@ -86,6 +86,8 @@ 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 + 0daae27978089eb97ab35c18718b62b0) KIND="RS-12.21 bench build (scan option (b) + L + d2, firmware park command, 0x0D split; built 2026-10-25)" ;; + 2c5e055abff95126309a62409fafc654) KIND="RS-12.21 PRODUCTION build: refuses diag register writes, so -ForceFrfHz and channel_survey_sniff.py fail on it -- bench boards normally run the bench build" ;; e083ae46a64686031b39866eca42e094) KIND="bench build on both boards since 2026-10-18 (RS-12.15 reverse delivery, #152)" ;; 148de24e1f9acce417c46c2c69a5be6b) KIND="PRODUCTION build (main 3c8ced60): refuses diag register writes, so -ForceFrfHz and channel_survey_sniff.py fail on it -- bench boards normally run the bench build" ;; 0c1bb0a9573f813137f941dfa47177d0) KIND="previous bench build, on both boards 2026-09-15 -> 2026-10-18 (RS-12.15 v2; the rollback image)" ;; diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/legs/README.md b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/legs/README.md index 8d7b5291..c64011be 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/legs/README.md +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/legs/README.md @@ -231,6 +231,11 @@ that board with sudo and re-run (BENCH_RUNBOOK, "A leg"). least ~2 min after the daemons stopped: a park readback taken right after the stop is not authoritative (the scan can still re-arm the receiver; BENCH_RUNBOOK, `radio_park.py`). +- On the RS-12.21 image (built 2026-10-25, not flashed yet) `radio_park.py` + parks by the firmware command (`"path": "firmware"` in `leg_park.txt`), + which holds at the first attempt; a retry or a `PARK NOT CONFIRMED` there + is a fault to report, not a scan still walking. The `--check-only` + reading stays. **Not flying after the prep after all** (no GO, the harness refused, the round was aborted)? `leg_prep.sh` has woken both receivers (RXCONT) and left 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 index 7921f58d..7753588c 100644 --- 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 @@ -842,7 +842,7 @@ for r in "${RESULTS[@]}"; do say " $r"; done say "" say "Still to do by hand (BENCH_SETUP.md):" say " [ ] verify: HC-01 / HC-02 health checks, then '$(basename "$0") $ROLE --check' shows no TODO" -say " [ ] L072 bench firmware (build md5 e083ae46a64686031b39866eca42e094): ../FLASH_RUNBOOK.md" +say " [ ] L072 bench firmware (mingw32-make bench; the RS-12.21 source builds md5 0daae27978089eb97ab35c18718b62b0, not flashed yet; the bench boards run e083ae46a64686031b39866eca42e094 until then): ../FLASH_RUNBOOK.md" if [ "$ROLE" = base ]; then say " [ ] deploy the base tree, image lifetrac-v25 and the broker: DEPLOY.md (deploy_base.sh)" say " [ ] power: the base boots again after 'systemctl poweroff'; remove power to keep it down" diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/radio_park.py b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/radio_park.py index 7249578d..a2226675 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/radio_park.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/radio_park.py @@ -2,9 +2,55 @@ """radio_park.py — put the L072's SX1276 into LoRa SLEEP and verify. Mirrors channel_survey_sniff.py's link bring-up exactly (HostLink, boot -drain, VER handshake, drain), then walks RegOpMode STANDBY -> SLEEP and -reads it back. Prints PARK_OK/PARK_FAIL. Run in the daemon-shaped +drain, VER handshake, drain), parks the radio by one of two paths, then +reads RegOpMode back twice, SETTLE_S apart. Run in the daemon-shaped container on either board. + +Paths (2026-10-24, RS-12.21): + firmware the park command (HOST_TYPE_RADIO_PARK_REQ, answered by + RADIO_PARK_URC). The L072 sets its host_parked flag, which gates + every RX arm path (scan walker, gamma-1 retune, slot follower, + the post-TX re-arm, the scan restarts), and puts the modem to + SLEEP. PERSISTENT: it holds until a host raw RXCONT write, a + profile activation (a daemon start) or a reset. + register an image from before the command: it answers ERR_PROTO + UNKNOWN_TYPE, or nothing within RADIO_PARK_TIMEOUT_S (any other + ERR_PROTO for the command lands here too, with its text). The + old STANDBY -> SLEEP RegOpMode writes. TRANSIENT: a firmware RX + owner that still has work re-arms RXCONT (receive-only), so on + such an image it holds only once the scan has failed out into + FAILED, which parked it, up to ~2 min after the daemons stop + (PR #125 review). +The VER reply picks the path (2026-10-24 review): its capability bitmap's +bit 7 (HOST_CAP_RADIO_PARK) marks the image with the command. That image +also restarts its scan after a final RX_SCAN_FAILED (scan option (b)): once +the daemons stop it scans on, re-arming RXCONT every 2 s dwell, so a +register SLEEP write cannot hold on it. It gets the firmware path only: the +command up to PARK_CMD_TRIES times, a late answer to an earlier try counting +as the answer, and PARK_FAIL (no register write) if none comes. An image +without the bit, or a VER reply too short to say, gets the command once and, +unanswered or refused as unknown, the register path. + +Output: the LAST line (callers read `tail -1`) is PARK_OK, PARK_TRANSIENT +or PARK_FAIL and a JSON object: "path" (firmware / register), "persistent" +(true only for a firmware park that held through the settle), "holds" +(until what), "opmode_readback" (the park URC's readback, or the register +path's first read), "opmode_after_settle", "settle_s", "cap_bitmap" (the VER +reply's, "0x000000FF"; null when it carried none), "park_tries" (park +commands sent), and "park_status" (firmware path: PARKED / ALREADY_PARKED / +REFUSED) or "park_cmd" (the command's answer when it did not park: +unknown_type / no_reply / err_proto, with "park_cmd_err"); "late": true when +the answer came to an earlier try; "reason" on a firmware-path PARK_FAIL +without a park status. + +Exit codes (unchanged): 0 PARK_OK (asleep at both reads); 2 the VER +handshake failed; 3 a stage raised (the park command's send, a register +write or read); 4 PARK_FAIL (not asleep at the first read, the firmware +refused the park, or an image that advertises the command did not answer +it); 5 PARK_TRANSIENT (asleep, then re-armed within the settle. Register +path, an image from before the command: the scan was still walking -- wait +~60 s and park again, as leg_post.sh does. Firmware path: the gate missed an +arm path, a firmware defect). """ from __future__ import annotations @@ -21,64 +67,183 @@ read_reg, write_reg, drain_boot, drain_pending, ) +from method_g_stage1_probe import ( # type: ignore + HOST_CAP_RADIO_PARK, + RADIO_PARK_STATUS_PARKED, RADIO_PARK_STATUS_ALREADY_PARKED, + RADIO_PARK_TIMEOUT_S, + parse_version, +) OPMODE_LORA_STANDBY = 0x81 OPMODE_LORA_SLEEP = 0x80 +# Longer than the longest a firmware RX owner that still has work takes to +# re-arm RXCONT on any image: a scan channel dwell (2 s since RS-12.21 d2, +# 500 ms before) or a 200 ms slot, with margin. (1.5 s until the 2026-10-24 +# review: shorter than the 2 s dwell, so a register park could read PARK_OK +# before the walker's next step re-armed it.) +SETTLE_S = 2.5 +PARK_CMD_TIMEOUT_S = RADIO_PARK_TIMEOUT_S +# An image that advertises the command: tries before PARK_FAIL. +PARK_CMD_TRIES = 3 -def main() -> int: - uart = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttymxc3" - link = HostLink(uart, "921600") - drain_boot(link, settle_s=0.25) +HOLDS_FIRMWARE = "until a host RXCONT write, a profile activation or a reset" +HOLDS_REGISTER = ("until a firmware RX owner re-arms it: the scan walker " + "until the scan fails out into FAILED, or a host RXCONT write") + + +def asleep(value) -> bool: + return (int(value) & 0x87) == (OPMODE_LORA_SLEEP & 0x87) + + +def _hex(value) -> "str | None": + return None if value is None else f"0x{int(value):02X}" + + +def _cap_bitmap(ver_frame) -> "int | None": + """The VER reply's capability bitmap, or None when it carries none.""" try: - link.request(HOST_TYPE_VER_REQ, HOST_TYPE_VER_URC, timeout=1.0) - except Exception as exc: - print("PARK_FAIL " + json.dumps({"stage": "ver", "err": str(exc)}), - flush=True) - return 2 - drain_pending(link, quiet_s=0.25, max_s=1.0) - # PR #125 review (2026-09-15): the immediate readback proves only the - # instant of the write. The firmware's own RX owners -- the scan walker - # (ADVANCE_CHANNEL), the gamma-1 retune tick and, when LOCKED, the slot - # follower -- all call sx1276_rx_arm() and will put the modem back into - # RXCONT (receive-only) if any of them still has work. The park only - # holds once the scan SM has exhausted into FAILED (nothing left to arm), - # which is the normal state some tens of seconds after the daemons stop. - # So: write, wait longer than a scan channel dwell (500 ms) and a slot - # (200 ms), read AGAIN, and only call it parked if it stayed asleep. - # A firmware park command that gates those owners is TODO RS-12.21; - # until then PARK_OK means "asleep now and nothing re-armed it". - SETTLE_S = 1.5 + return parse_version(bytes(ver_frame["payload"]))["cap_bitmap"] + except (KeyError, TypeError, ValueError): + return None + + +def _settle_and_report(link, first: int, info: dict) -> int: + """Wait SETTLE_S, read RegOpMode again and print the verdict line.""" try: - write_reg(link, SX1276_REG_OP_MODE, OPMODE_LORA_STANDBY, timeout=0.5) - time.sleep(0.05) - write_reg(link, SX1276_REG_OP_MODE, OPMODE_LORA_SLEEP, timeout=0.5) - time.sleep(0.05) - first, _raw = read_reg(link, SX1276_REG_OP_MODE, timeout=0.5) time.sleep(SETTLE_S) drain_pending(link, quiet_s=0.1, max_s=0.5) second, _raw = read_reg(link, SX1276_REG_OP_MODE, timeout=0.5) except Exception as exc: - print("PARK_FAIL " + json.dumps({"stage": "opmode", "err": str(exc)}), - flush=True) + print("PARK_FAIL " + json.dumps(dict(info, stage="opmode", + err=str(exc))), flush=True) return 3 - asleep = lambda v: (int(v) & 0x87) == (OPMODE_LORA_SLEEP & 0x87) - info = {"opmode_readback": f"0x{int(first):02X}", - "opmode_after_settle": f"0x{int(second):02X}", - "settle_s": SETTLE_S} + info.update(opmode_readback=_hex(first), opmode_after_settle=_hex(second), + settle_s=SETTLE_S) if asleep(first) and asleep(second): + info["persistent"] = info["path"] == "firmware" print("PARK_OK " + json.dumps(info), flush=True) return 0 + info["persistent"] = False if asleep(first): - # The write took, then a firmware RX owner re-armed the modem: - # scan SM still walking or LOCKED. Not parked. Stop the daemons, - # wait for the scan to fail, run again -- and verify later with - # the read-only radio_state.py. + # Asleep, then re-armed within the settle. Register path (an image + # from before the command): the scan SM was still walking (or + # LOCKED) -- stop the daemons, wait for the scan to fail out, run + # again, verify later with the read-only radio_state.py. Firmware + # path: the park did not gate that arm. print("PARK_TRANSIENT " + json.dumps(info), flush=True) return 5 print("PARK_FAIL " + json.dumps(info), flush=True) return 4 +def _firmware_park(link, park: dict, info: dict) -> int: + info.update(path="firmware", persistent=False, holds=HOLDS_FIRMWARE, + park_status=park["status_name"]) + if park["answer_seq"] != park["seq"]: + info["late"] = True + if park["status"] not in (RADIO_PARK_STATUS_PARKED, + RADIO_PARK_STATUS_ALREADY_PARKED): + # REFUSED (or a status this tool does not know): the firmware kept + # the radio as it was. Not parked; run again once it is idle. + info.update(opmode_readback=_hex(park["opmode"])) + print("PARK_FAIL " + json.dumps(info), flush=True) + return 4 + first = park["opmode"] + if first is None: # a short URC: read it ourselves + try: + first, _raw = read_reg(link, SX1276_REG_OP_MODE, timeout=0.5) + except Exception as exc: + print("PARK_FAIL " + json.dumps(dict(info, stage="opmode", + err=str(exc))), flush=True) + return 3 + return _settle_and_report(link, first, info) + + +def _register_park(link, park: dict, info: dict) -> int: + # PR #125 review (2026-09-15): the immediate readback proves only the + # instant of the write. The firmware's own RX owners -- the scan walker + # (ADVANCE_CHANNEL), the gamma-1 retune tick and, when LOCKED, the slot + # follower -- all call sx1276_rx_arm() and will put the modem back into + # RXCONT (receive-only) if any of them still has work. On an image + # without the park command the park only holds once the scan SM has + # exhausted into FAILED (nothing left to arm), which is the normal + # state some tens of seconds after the daemons stop. So: write, wait + # SETTLE_S -- longer than a scan channel dwell (2 s since RS-12.21 d2, + # 500 ms before) and a slot (200 ms) -- read AGAIN, and only call it + # parked if it stayed asleep: PARK_OK on this path means "asleep now + # and nothing re-armed it". + info.update(path="register", persistent=False, holds=HOLDS_REGISTER, + park_cmd=park["answer"]) + if park["detail"]: + info["park_cmd_err"] = park["detail"] + try: + write_reg(link, SX1276_REG_OP_MODE, OPMODE_LORA_STANDBY, timeout=0.5) + time.sleep(0.05) + write_reg(link, SX1276_REG_OP_MODE, OPMODE_LORA_SLEEP, timeout=0.5) + time.sleep(0.05) + first, _raw = read_reg(link, SX1276_REG_OP_MODE, timeout=0.5) + except Exception as exc: + print("PARK_FAIL " + json.dumps(dict(info, stage="opmode", + err=str(exc))), flush=True) + return 3 + return _settle_and_report(link, first, info) + + +def main(argv=None) -> int: + argv = sys.argv[1:] if argv is None else argv + uart = argv[0] if argv else "/dev/ttymxc3" + link = HostLink(uart, "921600") + drain_boot(link, settle_s=0.25) + try: + ver = link.request(HOST_TYPE_VER_REQ, HOST_TYPE_VER_URC, timeout=1.0) + except Exception as exc: + print("PARK_FAIL " + json.dumps({"stage": "ver", "err": str(exc)}), + flush=True) + return 2 + cap = _cap_bitmap(ver) + has_cmd = cap is not None and bool(cap & HOST_CAP_RADIO_PARK) + drain_pending(link, quiet_s=0.25, max_s=1.0) + # RS-12.21 (2026-10-24): the firmware park command first. An image that + # answers it parks persistently; one from before it says UNKNOWN_TYPE + # (or nothing) and gets the register writes it has always had. An image + # that advertises it (VER cap bit 7) is asked again on silence, and an + # earlier try's late answer counts (2026-10-24 review). + seqs = [] + try: + for _try in range(PARK_CMD_TRIES if has_cmd else 1): + park = link.radio_park(timeout=PARK_CMD_TIMEOUT_S, earlier_seqs=seqs) + seqs.append(park["seq"]) + if park["answer"] != "no_reply": + break + except Exception as exc: + print("PARK_FAIL " + json.dumps({"stage": "park_cmd", "err": str(exc)}), + flush=True) + return 3 + info = {"cap_bitmap": None if cap is None else f"0x{cap:08X}", + "park_tries": len(seqs)} + if park["answer"] == "urc": + return _firmware_park(link, park, info) + if has_cmd: + # 2026-10-24 review: a register SLEEP write cannot hold on this image + # (its scan never fails out), so none is made: PARK_FAIL, and the + # caller runs again (leg_post.sh: 3 attempts, 60 s apart). + what = ("did not answer it" if park["answer"] == "no_reply" + else "refused it") + tries = "1 try" if len(seqs) == 1 else f"{len(seqs)} tries" + info.update(path="firmware", persistent=False, holds=None, + park_cmd=park["answer"], + reason=(f"the L072 advertises the park command (VER cap bit 7) " + f"but {what} in {tries}; a register SLEEP write cannot " + f"hold on this image (its scan restarts after a final " + f"RX_SCAN_FAILED and re-arms RX every dwell), so none " + f"was made")) + if park["detail"]: + info["park_cmd_err"] = park["detail"] + print("PARK_FAIL " + json.dumps(info), flush=True) + return 4 + return _register_park(link, park, info) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/rs13_probe_score.py b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/rs13_probe_score.py index 39e3e82b..42e86c2c 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/rs13_probe_score.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/rs13_probe_score.py @@ -9,7 +9,9 @@ PROBE TX count. The base's own `probe:` line counts echoes, so with -ProbeEcho 0 it reads 0 % and is ignored here. - py -3 rs13_probe_score.py [--margin PP] [--void-unlocked] [ ...] + py -3 rs13_probe_score.py [--margin PP] [--void-unlocked] + [--scan-image auto|restarts|absorbing] + [ ...] Prints one block per archive (keep it as legs/leg_probe_score.txt). Its `tail:` line lists the base PROBE seqs sent after the last one the tractor @@ -94,11 +96,20 @@ DETERMINED. ACQUIRED when a lock shows (the base decoded at least 25 % of the tractor's fragments on air on a hop set of 10 or more channels, the bench's 50 here, which only a grid follower does); NOT DETERMINED -otherwise. The block's line +otherwise. That is the rule of every L072 image before RS-12.21, whose scan +FAILED is absorbing. The RS-12.21 image (scan option (b); rs12_leg_report.py +tells it by the VER capability bitmap the base daemon logs, or by an +RX_SCAN_FAILED after a final one, and --scan-image overrides) restarts its +scan after a final and reports one every 30 s until a LOCK: there a final +is no FAILED on its own, a lock shown after it is a late ACQUIRED, and +silence counts over the whole window, not the budget; FAILED means the base +never locked onto the hop grid in the window (2026-10-24 review). The +block's line reads `base acquisition: ; ; evidence: `. A failed base never follows the hop grid: with -ReactiveFire 1 it fires a probe only after a frame it happened to -catch, from the channel it is parked on, so the leg measures the +catch, from the channel it is parked on (on the RS-12.21 image, wherever +its scan stands), so the leg measures the acquisition failure, not command delivery (leg 6: 4/5 pooled; a leg that catches nothing fires no probe and withholds the pooled verdict). By default nothing else changes: such a leg pools like any other, and one that @@ -370,11 +381,13 @@ def _pct(x: int, y: int) -> str: return f"{100.0 * x / y:.1f} %" if y else "n/a" -def leg_acquisition(params: dict[str, str], rx_text: str, tx_text: str) -> dict | None: - """rs12_leg_report.acquisition on an FHSS leg (params reg_profile=1), else None.""" +def leg_acquisition(params: dict[str, str], rx_text: str, tx_text: str, + image: str = "auto") -> dict | None: + """rs12_leg_report.acquisition on an FHSS leg (params reg_profile=1), else None; + ``image``: --scan-image (rs12_leg_report.scan_image).""" if params.get("reg_profile") != "1": return None - return rep.acquisition(rx_text, tx_text) + return rep.acquisition(rx_text, tx_text, image=image) def acquisition_lines(acq: dict | None) -> list[str]: @@ -388,6 +401,14 @@ def acquisition_lines(acq: dict | None) -> list[str]: seen = rep.reception_phrase(acq) if acq["status"] != rep.FAILED: return [f" base acquisition: {acq['status']}; {seen}; evidence: {acq['basis']}"] + if acq.get("restarts"): + # 2026-10-24 review: the RS-12.21 image scans on after a final + return [f" !! base ACQUISITION FAILED; {seen}; evidence: {acq['basis']}. Its scan " + f"scanned on (the RS-12.21 image restarts it after each final RX_SCAN_FAILED) " + f"but never locked onto the hop grid, so any probe it fired went out off the " + f"grid, after a frame it happened to catch; this leg measures that, not " + f"command delivery (--void-unlocked leaves it out of the pool, only under a " + f"pass rule written before leg 1)"] return [f" !! base ACQUISITION FAILED; {seen}; evidence: {acq['basis']}. Its scan ran out " f"into FAILED and never followed the hop grid, so any probe it fired went out from " f"the channel it was parked on, after a frame it happened to catch; this leg " @@ -539,6 +560,11 @@ def main(argv: list[str] | None = None) -> int: "evidence (rs12_leg_report.py's rule; not one NOT DETERMINED) out of " "the pool and list them; only under a pass rule written before leg 1 " "that says so") + ap.add_argument("--scan-image", choices=rep.SCAN_IMAGES, default="auto", + help="how the base's L072 image treats a final RX_SCAN_FAILED, for the " + "acquisition rule (rs12_leg_report.py --scan-image): 'restarts' " + "(the RS-12.21 image), 'absorbing' (every image before it), 'auto' " + "(default: the logged VER capability bitmap, else the faults)") a = ap.parse_args(argv) if a.margin is not None and not 0 < a.margin < 100: ap.error(f"--margin {a.margin:g}: give a margin between 0 and 100 percentage points") @@ -562,7 +588,7 @@ def main(argv: list[str] | None = None) -> int: continue # 2026-10-24: the base's FHSS acquisition (rs12_leg_report's rule); only a # leg FAILED on positive evidence voids, never one NOT DETERMINED - acq = leg_acquisition(params, rx_text, tx_text) + acq = leg_acquisition(params, rx_text, tx_text, a.scan_image) void = a.void_unlocked and bool(acq and acq.get("status") == rep.FAILED) if res["sent"] == 0: print(f"{name}: no PROBE TX line in rx_daemon.log (reactive_fire=" diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/test_run_live_radio_monitor.ps1 b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/test_run_live_radio_monitor.ps1 index 73a64ef8..36cb0366 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/test_run_live_radio_monitor.ps1 +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/bench_tools/test_run_live_radio_monitor.ps1 @@ -21,6 +21,10 @@ results and the daemons' SIGTERM shutdown lines, Get-LogWindowVerdict) and Invoke-DockerStop against a stub adb. + 2026-10-24 (the 0x0D split, FHSS_SCAN_FAILED_OPTIONS.md section 7): + fault:0x0D:final fires only on the scan's last attempt (a valid scan sub + with attempt 3), and fault:0x0F (TX_FIFO_RB_MISMATCH) works. + powershell -NoProfile -File bench_tools\test_run_live_radio_monitor.ps1 Exit 0 = all passed, 1 = a failure (listed), 2 = the harness did not parse. @@ -126,6 +130,43 @@ Check "a non-FAULT line never fires" (-not (Test-InjectFaultLine -Line "INFO sta $xl = @(ConvertFrom-AdbOutput (CmdLines "$x0`n$x2`n$x3`n")) Check "grep output through cmd /c: the final line found" (@($xl | Where-Object { Test-InjectFaultLine -Line $_ -Spec $final }).Count -eq 1) +# --- Test-InjectFaultLine after the 0x0D split (2026-10-24) ------------------------ +# fault:0x0D:final = the scan's last attempt: final bit, attempt 3, a valid scan sub +# (include/sx1276_rx_scan_fail.h). TX_FIFO_RB_MISMATCH moved to 0x0F. +function FaultLine([string]$Code, [string]$Sub) { + return "2026-10-25 09:23:03,425 WARNING image_tx_daemon: FAULT during pipelined TX: code=0x$Code sub=0x$Sub raw=$($Code.ToLower())$($Sub.ToLower())000000000000000000000000000000000000000016e30100" +} +foreach ($s in @("83", "A3", "C3", "E3", "93", "D3")) { + Check "final: attempt-3 scan sub 0x$s fires" (Test-InjectFaultLine -Line (FaultLine "0D" $s) -Spec $final) +} +foreach ($s in @("FE", "FD", "80", "84", "81", "82", "B3", "F3", "8F", "03", "A2")) { + Check "final: sub 0x$s (not the scan's last attempt) does not fire" (-not (Test-InjectFaultLine -Line (FaultLine "0D" $s) -Spec $final)) +} +$cmdFifo = "2026-10-25 09:23:04,001 WARNING image_tx_daemon: FAULT_URC: code=0x0D sub=0xFE name=TX_FIFO_RB_MISMATCH tx_id=0xFE fw=old basis=not_scan_encoding tx_inflight=0xFE uptime_ms=123671 raw=0dfe00000000000000000000000000000000000017e30100" +Check "final: a pre-split FIFO fault on the command tx_id 0xFE (bit 7) does not fire" (-not (Test-InjectFaultLine -Line $cmdFifo -Spec $final)) +$urcFinal = "2026-10-25 09:23:03,426 WARNING image_tx_daemon: FAULT_URC: code=0x0D sub=0xA3 name=RX_SCAN_FAILED attempt=3 final=1 warm=0 hw_suspect=1 crc_seen=0 basis=tx_id_not_in_flight tx_inflight=- uptime_ms=123670 raw=0da300000000000000000000000000000000000016e30100" +Check "final: the FAULT_URC line of the scan's last attempt fires" (Test-InjectFaultLine -Line $urcFinal -Spec $final) +$fifo0F = "2026-10-25 09:24:00,100 WARNING image_tx_daemon: FAULT_URC: code=0x0F sub=0x83 name=TX_FIFO_RB_MISMATCH tx_id=0x83 uptime_ms=180000 raw=0f8300000000000000000000000000000000000020bf0200" +Check "final: a 0x0F line never fires fault:0x0D:final (sub 0x83 or not)" (-not (Test-InjectFaultLine -Line $fifo0F -Spec $final)) +$s0F = ConvertTo-InjectSpec -Cmd "dual_inject.py 120" -On "fault:0x0F" +Check "fault:0x0F parses" ($s0F.On -eq "fault" -and $s0F.Code -eq 15 -and $s0F.Sub -eq -1 -and -not $s0F.Final -and $s0F.Label -eq "fault:0x0F") $s0F.Label +Check "fault:0x0F fires on the 0x0F FAULT_URC line" (Test-InjectFaultLine -Line $fifo0F -Spec $s0F) +Check "fault:0x0F fires on a legacy 0x0F line" (Test-InjectFaultLine -Line (FaultLine "0F" "02") -Spec $s0F) +Check "fault:0x0F does not fire on a 0x0D line" (-not (Test-InjectFaultLine -Line $urcFinal -Spec $s0F) -and -not (Test-InjectFaultLine -Line $cmdFifo -Spec $s0F)) +$s0F2 = ConvertTo-InjectSpec -Cmd "dual_inject.py 120" -On "fault:0x0F:0x02" +Check "fault:0x0F:0x02 fires only on tx_id 0x02" ((Test-InjectFaultLine -Line (FaultLine "0F" "02") -Spec $s0F2) -and -not (Test-InjectFaultLine -Line $fifo0F -Spec $s0F2)) +# 2026-10-24 review: final is RX_SCAN_FAILED's, so the parser refuses it on any +# other code (it would run the whole leg without injecting) ... +Check "fault:0x0F:final throws (final is code 0x0D's only)" (Throws { ConvertTo-InjectSpec -Cmd "dual_inject.py 120" -On "fault:0x0F:final" }) +Check "fault:0x0E:final throws" (Throws { ConvertTo-InjectSpec -Cmd "dual_inject.py 120" -On "fault:0E:final" }) +Check "fault:d:final (no 0x) parses" ((ConvertTo-InjectSpec -Cmd "dual_inject.py 120" -On "fault:d:final").Final) +# ... and the matcher still never fires `final` off code 0x0D (a hand-built spec) +$s0Ff = @{ On = "fault"; Code = 0x0F; Sub = -1; Final = $true; Label = "fault:0x0F:final" } +Check "a code-0x0F final spec never fires" (-not (Test-InjectFaultLine -Line $fifo0F -Spec $s0Ff) -and -not (Test-InjectFaultLine -Line (FaultLine "0F" "A3") -Spec $s0Ff)) +$xl2 = @(ConvertFrom-AdbOutput (CmdLines "$x0`n$cmdFifo`n$x2`n$urcFinal`n$x3`n")) +$hits = @($xl2 | Where-Object { Test-InjectFaultLine -Line $_ -Spec $final }) +Check "grep output with a 0xFE FIFO line ahead: the first hit is the scan's final" ($hits.Count -eq 2 -and $hits[0] -eq $urcFinal) "$($hits.Count) hit(s)" + # --- Get-InjectSummary ----------------------------------------------------------- $ok = CmdLines ("INJECT_WATCH 16:21:58 1792340518 mode=first_frame deadline_s=360 cmd=dual_inject.py 250`n" + "INJECT_TRIGGER 16:21:59 1792340519 first_frame: 2026-10-18 16:21:57,488 INFO image_rx_daemon: published frame_id=0 seq=1 2985 B`n" + diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/method_g_stage1_probe.py b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/method_g_stage1_probe.py index c5bed049..057d17c2 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/method_g_stage1_probe.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/firmware/x8_lora_bootloader_helper/method_g_stage1_probe.py @@ -43,17 +43,56 @@ HOST_TYPE_READY_URC = 0xF2 HOST_TYPE_ERR_PROTO_URC = 0xFE +# 2026-10-24 (RS-12.21, the firmware park command; include/host_types.h). +# RADIO_PARK_REQ (empty payload) sets the L072's host_parked flag, which +# gates EVERY RX arm path (scan walker, gamma-1 retune, slot follower, the +# post-TX re-arm in sx1276_tx.c, the scan restarts), and puts the modem to +# SLEEP. The flag holds until a host raw RXCONT write (REG_WRITE RegOpMode), +# a profile activation or a reset; a TX_FRAME_REQ while parked is refused +# (ERR_PROTO). The reply, RADIO_PARK_URC (same seq): {u8 status +# (RADIO_PARK_STATUS_*), u8 RegOpMode readback}. Firmware from before the +# command answers ERR_PROTO UNKNOWN_TYPE (host_cmd.c dispatch default) and +# leaves the radio alone. HostLink.radio_park() sends it and sorts the answer. +HOST_TYPE_RADIO_PARK_REQ = 0x04 +HOST_TYPE_RADIO_PARK_URC = 0x84 +RADIO_PARK_STATUS_PARKED = 0 +RADIO_PARK_STATUS_ALREADY_PARKED = 1 +RADIO_PARK_STATUS_REFUSED = 2 +RADIO_PARK_STATUS_NAMES = { + RADIO_PARK_STATUS_PARKED: "PARKED", + RADIO_PARK_STATUS_ALREADY_PARKED: "ALREADY_PARKED", + RADIO_PARK_STATUS_REFUSED: "REFUSED", +} +# The firmware answers a host request within one main-loop pass; old +# firmware's UNKNOWN_TYPE refusal is as quick. 1 s is the "no reply" bound. +RADIO_PARK_TIMEOUT_S = 1.0 +# VER_URC / READY_URC cap_bitmap bit 7 (host_types.h HOST_CAP_RADIO_PARK): the +# image answers RADIO_PARK_REQ. The same image moved TX_FIFO_RB_MISMATCH to +# 0x0F (the 0x0D split, below) and restarts its scan after a final +# RX_SCAN_FAILED (scan option (b)), so a host SLEEP write cannot hold on it: +# bench_tools/radio_park.py parks it by the command only (2026-10-24 review). +HOST_CAP_RADIO_PARK = 0x80 + HOST_FAULT_CODE_HOST_RX_SEEN = 0x09 HOST_FAULT_CODE_HOST_RX_INACTIVE = 0x0A HOST_FAULT_CODE_HOST_PARSE_ERROR = 0x0B HOST_FAULT_CODE_HOST_DIAG_MARK = 0x0C # 2026-10-24 (FAULT URC logging): the rest of host_types.h's fault codes, for -# the `FAULT_URC:` line (format_fault_urc_line). 0x0D is DOUBLE-BOOKED in the -# firmware: TX_FIFO_RB_MISMATCH (radio/sx1276_tx.c, sub = that TX's tx_id, -# emitted just BEFORE its TX_DONE_URC) and RX_SCAN_FAILED (radio/sx1276_rx.c, -# sub = the sx1276_rx_scan_fail.h bit field) share the value, so 0x0D has no -# entry in HOST_FAULT_CODE_NAMES -- decode_fault_0x0d() resolves it per URC. +# the `FAULT_URC:` line (format_fault_urc_line). +# 2026-10-24 (the 0x0D split, FHSS_SCAN_FAILED_OPTIONS.md section 7): 0x0D +# was DOUBLE-BOOKED -- RX_SCAN_FAILED (radio/sx1276_rx.c, sub = the +# sx1276_rx_scan_fail.h bit field; since 2026-05-20) and TX_FIFO_RB_MISMATCH +# (radio/sx1276_tx.c, sub = that TX's tx_id, emitted just BEFORE its +# TX_DONE_URC; RS-11.5, 2026-08-02). From the split RX_SCAN_FAILED keeps 0x0D +# and TX_FIFO_RB_MISMATCH moves to 0x0F. An image from before the split still +# sends its FIFO fault as 0x0D (..._PRE_SPLIT below). VER_URC's version and +# name do not tell the images apart (0.0.0 / "dev"), but its capability +# bitmap does: the image with the split also added the park command and sets +# HOST_CAP_RADIO_PARK (above). decode_fault_0x0d() is called where that bitmap +# is not at hand (the daemons' FAULT_URC hook), so it reads every 0x0D by its +# sub-byte, which is exact on a split image too: every 0x0D it sends is a scan +# fault with a valid sub, so "fw=old" can only mark a pre-split image. HOST_FAULT_CODE_HARDFAULT = 0x01 HOST_FAULT_CODE_NMI = 0x02 HOST_FAULT_CODE_RADIO_INIT_FAIL = 0x03 @@ -62,9 +101,10 @@ HOST_FAULT_CODE_HOST_DMA_OVERRUN = 0x06 HOST_FAULT_CODE_STACK_GUARD = 0x07 HOST_FAULT_CODE_CLOCK_HSE_FAILED = 0x08 -HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH = 0x0D HOST_FAULT_CODE_RX_SCAN_FAILED = 0x0D HOST_FAULT_CODE_TX_DONE_EARLY = 0x0E +HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH = 0x0F +HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH_PRE_SPLIT = 0x0D HOST_FAULT_CODE_NAMES = { HOST_FAULT_CODE_HARDFAULT: "HARDFAULT", @@ -79,7 +119,10 @@ HOST_FAULT_CODE_HOST_RX_INACTIVE: "HOST_RX_INACTIVE", HOST_FAULT_CODE_HOST_PARSE_ERROR: "HOST_PARSE_ERROR", HOST_FAULT_CODE_HOST_DIAG_MARK: "HOST_DIAG_MARK", + # format_fault_urc_line names a 0x0D by decode_fault_0x0d(), not here + HOST_FAULT_CODE_RX_SCAN_FAILED: "RX_SCAN_FAILED", HOST_FAULT_CODE_TX_DONE_EARLY: "TX_DONE_EARLY", + HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH: "TX_FIFO_RB_MISMATCH", } # RX_SCAN_FAILED `sub` layout + retry budget, mirrored from @@ -92,17 +135,18 @@ RX_SCAN_FAIL_SUB_ATTEMPT_MASK = 0x0F RX_SCAN_MAX_RETRIES = 3 # SX1276_RX_SCAN_MAX_RETRIES (4 attempts) -# TX bookkeeping for the 0x0D decode (HostLink.on_fault_urc): the request -# that radiates, and the URC that retires it (payload[0] = tx_id in both). -# An ERR_PROTO_URC whose payload[0] is TX_FRAME_REQ retires the request with -# its seq too: the L072 refused it (host_cmd.c), so no TX_DONE_URC follows. +# TX bookkeeping for the 0x0D decode's basis (HostLink.on_fault_urc): the +# request that radiates, and the URC that retires it (payload[0] = tx_id in +# both). An ERR_PROTO_URC whose payload[0] is TX_FRAME_REQ retires the +# request with its seq too: the L072 refused it (host_cmd.c; also every TX +# while the radio is parked, RS-12.21), so no TX_DONE_URC follows. HOST_TYPE_TX_FRAME_REQ = 0x10 HOST_TYPE_TX_DONE_URC = 0x90 # A tx_id with an unretired TX_FRAME_REQ counts as in flight until this long # after its NEWEST send (time.monotonic()) -- above every host TX timeout # (the daemons give up after 2-3 s), so a lost TX_DONE ages out instead of -# pinning its tx_id "in flight" (which would make every later 0x0D on it -# AMBIGUOUS). +# pinning its tx_id "in flight" (which would give every later 0x0D on it +# basis=tx_id_in_flight). TX_INFLIGHT_STALE_S = 5.0 HOST_ERR_PROTO_BAD_VERSION = 0x01 @@ -286,7 +330,8 @@ def _observe_frame(self, frame: dict) -> None: if frame["type"] == HOST_TYPE_TX_DONE_URC: # the OLDEST unretired send of that tx_id: the L072 serves its # TX ring head-first, and which copy a TX_DONE answers cannot be - # told -- a resent tx_id stays in flight (0x0D -> AMBIGUOUS) + # told -- a resent tx_id stays in flight (0x0D basis= + # tx_id_in_flight) if payload: self._tx_retire(payload[0], 0) return @@ -475,6 +520,77 @@ def request(self, req_type: int, rsp_type: int, payload: bytes = b"", timeout: f unmatched_local.extend(keep) pending = [] + def radio_park(self, timeout: float = RADIO_PARK_TIMEOUT_S, + earlier_seqs=()) -> dict: + """Send RADIO_PARK_REQ (RS-12.21, 2026-10-24) and wait up to + `timeout` s for the L072's answer to THAT request (its seq) -- or to + one of `earlier_seqs`, the seqs of this caller's earlier tries that + drew no reply (2026-10-24 review: a late answer to an earlier try is + the answer, not a stale frame). + + Not request(): an old image's ERR_PROTO is an answer here (it says + which firmware this is), not an error to raise. Returns + {"answer", "status", "status_name", "opmode", "detail", "seq", + "answer_seq"}: "seq" the seq this call sent, "answer_seq" the seq the + answer carried (None for "no_reply"); + "urc" RADIO_PARK_URC: status (RADIO_PARK_STATUS_*; None + on an empty payload), status_name, opmode (the + RegOpMode readback byte; None when absent); + "unknown_type" ERR_PROTO UNKNOWN_TYPE: an image from before the + command -- the radio was not touched; + "err_proto" any other ERR_PROTO for it: the image knows the + command but refused the frame (detail = the + decoded payload, format_err_proto_payload); + "no_reply" nothing within `timeout`. + Frames that answer something else go back on urc_queue in arrival + order, as request() leaves them (a FAULT_URC among them has already + been seen by on_fault_urc, once); a RADIO_PARK_URC with any other seq + (the late answer to a call that gave up and was not named in + `earlier_seqs`) is dropped. Raises only what send() raises (a serial + write timeout).""" + seq = self.send(HOST_TYPE_RADIO_PARK_REQ) + mine = {seq, *earlier_seqs} + deadline = time.time() + timeout + out = {"answer": "no_reply", "status": None, "status_name": None, + "opmode": None, "detail": "", "seq": seq, "answer_seq": None} + pending = list(self.urc_queue) + self.urc_queue.clear() + kept = [] + while True: + for idx, frame in enumerate(pending): + ftype = frame["type"] + if ftype == HOST_TYPE_RADIO_PARK_URC and frame["seq"] not in mine: + continue + if (ftype not in (HOST_TYPE_RADIO_PARK_URC, + HOST_TYPE_ERR_PROTO_URC) + or frame["seq"] not in mine): + kept.append(frame) + continue + self.urc_queue.extend(kept + pending[idx + 1:]) + out["answer_seq"] = frame["seq"] + payload = bytes(frame["payload"]) + if ftype == HOST_TYPE_RADIO_PARK_URC: + status = payload[0] if payload else None + out.update( + answer="urc", status=status, + status_name=(None if status is None else + RADIO_PARK_STATUS_NAMES.get( + status, f"UNKNOWN_0x{status:02X}")), + opmode=payload[1] if len(payload) >= 2 else None) + else: + # ERR_PROTO payload: offending_type, offending_ver, + # err_code, detail (format_err_proto_payload) + unknown = (len(payload) >= 3 + and payload[2] == HOST_ERR_PROTO_UNKNOWN_TYPE) + out.update(answer="unknown_type" if unknown else "err_proto", + detail=format_err_proto_payload(payload)) + return out + pending = [] + if time.time() >= deadline: + self.urc_queue.extend(kept) + return out + pending = self.read_frames(0.1) + def parse_boot(payload: bytes) -> dict: if len(payload) < 6: @@ -668,9 +784,12 @@ def rx_scan_fail_sub_valid(sub: int) -> bool: With SX1276_RX_SCAN_MAX_RETRIES = 3 the firmware sets `final` exactly when the attempt nibble reaches 3 (retries reset only on LOCK or - sx1276_rx_scan_reset), and `hw_suspect` only when `crc_seen` is clear -- - 25 of the 256 values (incl. the NULL-input marker 0x80). Every other - 0x0D sub can only be a TX_FIFO_RB_MISMATCH tx_id. + sx1276_rx_scan_reset; option (b)'s restarts of a node without + originator authority keep the count at 3, so every later 30 s attempt + is final with attempt 3 again), and `hw_suspect` only when `crc_seen` + is clear -- 25 of the 256 values (incl. the NULL-input marker 0x80). + Every other 0x0D sub can only be the TX_FIFO_RB_MISMATCH tx_id of an + image from before the 0x0D split. """ if sub == RX_SCAN_FAIL_SUB_FINAL: # defensive NULL-input marker return True @@ -683,23 +802,36 @@ def rx_scan_fail_sub_valid(sub: int) -> bool: def decode_fault_0x0d(sub: int, tx_ids_in_flight=None) -> dict: - """Resolve a code-0x0D FAULT_URC (2026-10-24, leg-6 lesson). - - The two firmware emitters share the code, so the only evidence is the - sub-byte and the host's own TX state: - * a sub RX_SCAN_FAILED cannot produce -> TX_FIFO_RB_MISMATCH - (basis=not_scan_encoding; certain whatever the TX state); - * a valid scan encoding while no TX with tx_id == sub is in flight - -> RX_SCAN_FAILED (basis=tx_id_not_in_flight): the FIFO fault fires - on that TX's TX_DONE, before its TX_DONE_URC, i.e. while in flight; - * a valid scan encoding that IS an in-flight tx_id -> AMBIGUOUS - (basis=tx_id_in_flight), e.g. sub 0x00 against fragment 0; - * no TX context from the caller (None) -> AMBIGUOUS (basis=no_tx_context). - Returns {"name", "basis", "scan" (dict or None), "tx_id" (int or None)}: - "scan" is set when the RX_SCAN_FAILED reading is possible, "tx_id" when - the TX_FIFO_RB_MISMATCH reading is. + """Resolve a code-0x0D FAULT_URC (2026-10-24, leg-6 lesson; the 0x0D + split, FHSS_SCAN_FAILED_OPTIONS.md section 7). + + 0x0D is RX_SCAN_FAILED; TX_FIFO_RB_MISMATCH moved to 0x0F. An image + from before the split sends both as 0x0D. Its VER capability bitmap says + which image it is (HOST_CAP_RADIO_PARK set: the split), but the FAULT_URC + hooks that call this do not have it, so the sub-byte decides (on a split + image every 0x0D sub is a valid scan encoding, so the first case below is + then the only one): + * a sub sx1276_rx_scan_fail_eval() can emit (rx_scan_fail_sub_valid) + -> RX_SCAN_FAILED, whatever the TX state. `basis` keeps the in-flight + check of PR #166, which still says something about a pre-split image + only (its FIFO fault fires on a TX's TX_DONE, before that TX's + TX_DONE_URC, i.e. while it is in flight): + tx_id_not_in_flight no TX with tx_id == sub awaited its TX_DONE: + a scan fault on any image; + tx_id_in_flight one did: on a pre-split image this URC could + also be that TX's FIFO readback mismatch (e.g. + sub 0x00 against fragment 0; PR #166's daemons + named it AMBIGUOUS). Read the board's + tx_fifo_rb_bad counter (0 in every archived + reading) if it matters; + no_tx_context the caller passed None; + * any other sub -> TX_FIFO_RB_MISMATCH of a pre-split image (tx_id = + sub, basis=not_scan_encoding, old_fw True): no image after the split + sends 0x0D with it. + Returns {"name", "basis", "scan" (dict or None), "tx_id" (int or None), + "old_fw" (bool)}: "scan" is set for RX_SCAN_FAILED, "tx_id" and + "old_fw" for the pre-split FIFO fault. """ - scan = None if rx_scan_fail_sub_valid(sub): scan = { "attempt": sub & RX_SCAN_FAIL_SUB_ATTEMPT_MASK, @@ -708,16 +840,16 @@ def decode_fault_0x0d(sub: int, tx_ids_in_flight=None) -> dict: "hw_suspect": int(bool(sub & RX_SCAN_FAIL_SUB_HW_SUSPECT)), "crc_seen": int(bool(sub & RX_SCAN_FAIL_SUB_CRC_SEEN)), } - if scan is None: - name, basis = "TX_FIFO_RB_MISMATCH", "not_scan_encoding" - elif tx_ids_in_flight is None: - name, basis = "AMBIGUOUS", "no_tx_context" - elif sub in tx_ids_in_flight: - name, basis = "AMBIGUOUS", "tx_id_in_flight" - else: - name, basis = "RX_SCAN_FAILED", "tx_id_not_in_flight" - return {"name": name, "basis": basis, "scan": scan, - "tx_id": None if name == "RX_SCAN_FAILED" else sub} + if tx_ids_in_flight is None: + basis = "no_tx_context" + elif sub in tx_ids_in_flight: + basis = "tx_id_in_flight" + else: + basis = "tx_id_not_in_flight" + return {"name": "RX_SCAN_FAILED", "basis": basis, "scan": scan, + "tx_id": None, "old_fw": False} + return {"name": "TX_FIFO_RB_MISMATCH", "basis": "not_scan_encoding", + "scan": None, "tx_id": sub, "old_fw": True} # Upper case on purpose: the case-sensitive FAULT greps (rs12_leg_report.py @@ -737,14 +869,17 @@ def format_fault_urc_line(payload: bytes, tx_ids_in_flight=None) -> str: during pipelined TX:`, or the untimestamped stdout `FAULT_URC during TX wait:` / `FAULT_URC during settle` (wait_for_tx_done, drain_boot; both daemons) -- and no legacy line carries `name=`. - NAME is HOST_FAULT_CODE_NAMES[code], UNKNOWN, or for 0x0D one of - RX_SCAN_FAILED / TX_FIFO_RB_MISMATCH / AMBIGUOUS (decode_fault_0x0d). - Detail tokens: 0x0D -> attempt= final= warm= hw_suspect= crc_seen= (when - the scan reading is possible), tx_id=0xSS (when the FIFO reading is), - basis=, tx_inflight= (comma list, '-' none, '?' unknown); 0x0E -> - tx_id=; 0x04 -> opmode=; 0x08 -> clock_source_id=; 0x09-0x0C -> the - format_fault_payload token; pc= lr= psr= bfar= when any is non-zero or - the code is a CPU fault (a replayed record from the previous boot). + NAME is HOST_FAULT_CODE_NAMES[code], UNKNOWN, or for 0x0D + RX_SCAN_FAILED or (a pre-split image's FIFO fault) TX_FIFO_RB_MISMATCH + (decode_fault_0x0d). Logs written by PR #166's daemons (2026-10-24, + before the 0x0D split) can also hold name=AMBIGUOUS; this function no + longer writes it. + Detail tokens: 0x0D -> attempt= final= warm= hw_suspect= crc_seen= (an + RX_SCAN_FAILED), or tx_id=0xSS fw=old (the pre-split FIFO fault), then + basis= and tx_inflight= (comma list, '-' none, '?' unknown); 0x0E and + 0x0F -> tx_id=; 0x04 -> opmode=; 0x08 -> clock_source_id=; 0x09-0x0C -> + the format_fault_payload token; pc= lr= psr= bfar= when any is non-zero + or the code is a CPU fault (a replayed record from the previous boot). uptime_ms is payload[20:24] -- the L072's platform_now_ms() when the URC was SENT (host_cmd.c), '?' on a short payload. """ @@ -763,6 +898,8 @@ def format_fault_urc_line(payload: bytes, tx_ids_in_flight=None) -> str: .format(**d["scan"])) if d["tx_id"] is not None: parts.append(f"tx_id=0x{d['tx_id']:02X}") + if d["old_fw"]: + parts.append("fw=old") parts.append(f"basis={d['basis']}") if tx_ids_in_flight is None: parts.append("tx_inflight=?") @@ -774,7 +911,8 @@ def format_fault_urc_line(payload: bytes, tx_ids_in_flight=None) -> str: detail = _fault_sub_detail(code, sub) if detail: parts.append(detail) - elif code == HOST_FAULT_CODE_TX_DONE_EARLY: + elif code in (HOST_FAULT_CODE_TX_DONE_EARLY, + HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH): parts.append(f"tx_id=0x{sub:02X}") elif code == HOST_FAULT_CODE_RADIO_OPMODE_DRIFT: parts.append(f"opmode=0x{sub:02X}") 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 3f9ff5b5..542e6f03 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 @@ -240,9 +240,10 @@ param( # polled every 0.5 s on the base from rx_smoke's log; # fault: the first tractor tx_smoke FAULT line with that # code; fault:: also needs that sub byte, - # fault::final needs sub bit 7 (the - # RX_SCAN_FAILED that enters FAILED: fault:0x0D:final - # is RS-12.15 leg X). Polled from the PC every ~2 s. + # fault:0x0D:final needs the RX_SCAN_FAILED sub of + # the scan's last attempt (final bit, attempt 3; + # Test-InjectFaultLine): RS-12.15 leg X. Polled from + # the PC every ~2 s. [string]$InjectOn = "", # Bench kit (2026-10-19): seconds between stopping the frame source and # stopping tx_smoke at leg end, so the frames already queued go on air and @@ -311,6 +312,13 @@ function ConvertTo-InjectSpec { $spec.Code = [Convert]::ToInt32($Matches[1], 16) $spec.Label = "fault:0x{0:X2}" -f $spec.Code if ($Matches[2]) { + # 2026-10-24 review: `final` is RX_SCAN_FAILED's (Test-InjectFaultLine + # fires it on code 0x0D only); on any other code it would run the + # whole leg without injecting. + if ($spec.Code -ne 0x0D) { + throw ("-InjectOn '$o': final is RX_SCAN_FAILED's, code 0x0D only " + + "(fault:0x0D:final); give a sub byte (fault::) or none.") + } $spec.Final = $true $spec.Label += ":final" } elseif ($Matches[3]) { @@ -326,16 +334,28 @@ function ConvertTo-InjectSpec { function Test-InjectFaultLine { # True when a tx_smoke log line is a FAULT matching the fault: spec. The # daemon prints `code=0x%02X sub=0x%02X` (method_g_stage1_probe.py - # format_fault_payload). Code 0x0D means both TX_FIFO_RB_MISMATCH (sub = - # fragment index) and RX_SCAN_FAILED (host_types.h); `final` tests sub - # bit 7, which only RX_SCAN_FAILED sets (fragment indexes stay < 0x80). + # format_fault_payload, and format_fault_urc_line's `FAULT_URC:` line). + # 2026-10-24 0x0D split (FHSS_SCAN_FAILED_OPTIONS.md section 7): 0x0D is + # RX_SCAN_FAILED, TX_FIFO_RB_MISMATCH (sub = the host tx_id) is 0x0F; an + # image from before the split sends both as 0x0D. `final` is the scan's + # last attempt: code 0x0D and a sub in the scan layout + # (include/sx1276_rx_scan_fail.h) with the final bit AND attempt 3 + # (SX1276_RX_SCAN_MAX_RETRIES), never hw_suspect (0x20) with crc_seen + # (0x10). Bit 7 alone was not enough: the tractor's command tx_id 0xFE + # (the base's 0xFD) has it, so a pre-split FIFO fault on a command frame + # fired the old test. The NULL-input marker 0x80 (attempt 0, a firmware + # caller bug, not the fourth failed attempt) does not fire; with any + # other code `final` never fires. param([string]$Line, [hashtable]$Spec) if ($Line -notmatch 'FAULT') { return $false } if ($Line -notmatch 'code=0x([0-9A-Fa-f]{2}) sub=0x([0-9A-Fa-f]{2})') { return $false } $code = [Convert]::ToInt32($Matches[1], 16) $sub = [Convert]::ToInt32($Matches[2], 16) if ($code -ne $Spec.Code) { return $false } - if ($Spec.Final) { return (($sub -band 0x80) -ne 0) } + if ($Spec.Final) { + return (($code -eq 0x0D) -and (($sub -band 0x80) -ne 0) -and + (($sub -band 0x0F) -eq 3) -and (($sub -band 0x30) -ne 0x30)) + } if ($Spec.Sub -ge 0) { return ($sub -eq $Spec.Sub) } return $true } diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/tools/check_mh_wire_sync.py b/LifeTrac-v25/DESIGN-CONTROLLER/tools/check_mh_wire_sync.py index a7311b74..f5e20371 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/tools/check_mh_wire_sync.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/tools/check_mh_wire_sync.py @@ -21,6 +21,8 @@ "HOST_TYPE_VER_REQ", "HOST_TYPE_UID_REQ", "HOST_TYPE_RESET_REQ", + # RS-12.21 (2026-10-24): the firmware radio park and its reply layout. + "HOST_TYPE_RADIO_PARK_REQ", "HOST_TYPE_TX_FRAME_REQ", "HOST_TYPE_CFG_SET_REQ", "HOST_TYPE_CFG_GET_REQ", @@ -30,6 +32,11 @@ "HOST_TYPE_STATS_DUMP_REQ", "HOST_TYPE_VER_URC", "HOST_TYPE_UID_URC", + "HOST_TYPE_RADIO_PARK_URC", + "HOST_RADIO_PARK_URC_PAYLOAD_LEN", + "HOST_RADIO_PARK_STATUS_PARKED", + "HOST_RADIO_PARK_STATUS_ALREADY", + "HOST_RADIO_PARK_STATUS_REFUSED", "HOST_TYPE_TX_DONE_URC", "HOST_TYPE_RX_FRAME_URC", "HOST_RX_FRAME_URC_TAIL_LEN", @@ -44,6 +51,9 @@ "HOST_TYPE_FAULT_URC", "HOST_TYPE_ERR_PROTO_URC", "HOST_FAULT_CODE_CLOCK_HSE_FAILED", + # The 0x0D split (2026-10-24): a drift of either code in the H7 copy fails. + "HOST_FAULT_CODE_RX_SCAN_FAILED", + "HOST_FAULT_CODE_TX_FIFO_RB_MISMATCH", "HOST_STATS_OFFSET_HOST_DROPPED", "HOST_STATS_OFFSET_HOST_ERRORS", "HOST_STATS_OFFSET_HOST_QUEUE_FULL", diff --git a/LifeTrac-v25/DESIGN-CONTROLLER/tools/rs12_leg_report.py b/LifeTrac-v25/DESIGN-CONTROLLER/tools/rs12_leg_report.py index 14523b29..8bd51243 100644 --- a/LifeTrac-v25/DESIGN-CONTROLLER/tools/rs12_leg_report.py +++ b/LifeTrac-v25/DESIGN-CONTROLLER/tools/rs12_leg_report.py @@ -28,7 +28,11 @@ LOCKED -> SCANNING demotion with --pre/--post, or a share of the tractor's fragments only a grid follower decodes); ``NOT DETERMINED`` otherwise, since a frame received is not a lock. The base's RX_SCAN_FAILED FAULT lines follow -when its log holds them. The capture's head (the frames before the first one +when its log holds them. On the RS-12.21 image (scan option (b), VER +capability bit 7: --scan-image, default auto) a final RX_SCAN_FAILED is a +30 s report and the scan goes on, so a lock shown after it is a late +acquisition, and silence counts over the whole window, not the budget +(2026-10-24 review). The capture's head (the frames before the first one heard, RS-13.1 A7) is placed on the tractor's TX log (``txdone_arrival``) by the frame's transport seq, read off rx_daemon.log's published line at its capture time (the TileDeltaFrame seq is the camera's own counter), instead of @@ -109,16 +113,31 @@ START_SKEW_S = (-5.0, 40.0) # 2026-10-24 (leg 6): the FHSS cold-start scan budget. A scan attempt dwells -# SX1276_RX_SCAN_DWELL_MS (500 ms) per channel and fails after +# SX1276_RX_SCAN_DWELL_MS per channel (500 ms; 2 s on the RS-12.21 image, +# option d2, a 50-channel sweep then taking 100 s) and fails after # SX1276_RX_SCAN_REDESIGN_MS (30000 ms, sx1276_rx_scan_policy.h); the first # attempt and SX1276_RX_SCAN_MAX_RETRIES (3, sx1276_rx_scan_fail.h) retries -# make 4, and the fourth FAIL enters scan FAILED, which is absorbing -# (sx1276_rx_scan_policy.c): only a reset or a profile re-activation leaves -# it, and a follower there never hops. A base that decodes no frame within -# 4 x 30 s of its profile-1 activation ran out of scan. +# make 4, and the fourth FAIL is final. Up to the RS-12.21 image it entered +# scan FAILED, which is absorbing (sx1276_rx_scan_policy.c): only a reset or +# a profile re-activation leaves it, and a follower there never hops, so a +# base that decodes no frame within 4 x 30 s of its profile-1 activation ran +# out of scan. The RS-12.21 image (scan option (b), FHSS_SCAN_FAILED_OPTIONS.md; +# VER capability bit 7, HOST_CAP_RADIO_PARK) restarts the scan of a node +# without originator authority at once -- the base always -- and sends a +# final RX_SCAN_FAILED every 30 s until a LOCK: there a final is a report, +# not a stop (scan_image() tells the images apart). SCAN_ATTEMPT_S = 30.0 SCAN_MAX_RETRIES = 3 SCAN_BUDGET_S = SCAN_ATTEMPT_S * (SCAN_MAX_RETRIES + 1) +# RS-12.21 (b): the restarted attempts' finals come SCAN_ATTEMPT_S apart (each +# attempt gets a fresh 30 s budget); a log that runs this much longer past +# the last one without another shows a LOCK within that attempt. +FINAL_REPEAT_SLACK_S = 5.0 +# The capability bitmap image_rx_daemon.py logs with its VER warm-up line +# (2026-10-24 review), and its bit for the RS-12.21 image (host_types.h). +VER_CAP_RE = re.compile(r"L072 VER warm-up ok \(attempt \d+\) cap=0x([0-9A-Fa-f]{8})") +HOST_CAP_RADIO_PARK = 0x80 +SCAN_IMAGES = ("auto", "restarts", "absorbing") # "CFG_SET_REQ(REG_PROFILE=1) OK: 14000100", printed (no timestamp) when the # daemon's profile activation is acknowledged; the next timestamped line (the # LBT_ENABLE line on every committed archive since 07-25) dates it. @@ -131,7 +150,8 @@ # per URC from both daemons (format_fault_urc_line, method_g_stage1_probe.py). # The same URC can leave a legacy line beside its FAULT_URC line, so when a # log holds FAULT_URC lines (with name=) only they are counted. Code 0x0D is -# HOST_FAULT_CODE_RX_SCAN_FAILED and also TX_FIFO_RB_MISMATCH (host_types.h): +# HOST_FAULT_CODE_RX_SCAN_FAILED; it was also TX_FIFO_RB_MISMATCH on images +# before the 0x0D split (e083ae46 and older; 0x0F since 2026-10-24): # the daemon's name= decides it when it names one of them, else (AMBIGUOUS, # or a legacy line) scan_fault_sub() reads the sub-byte by the scan layout. FAULT_RE = re.compile(r"\bFAULT\w*\b.*?\bcode=0x([0-9A-Fa-f]{2}) sub=0x([0-9A-Fa-f]{2})") @@ -152,12 +172,13 @@ ACQUIRED, FAILED, UNDETERMINED = "ACQUIRED", "FAILED", "NOT DETERMINED" # SX1276_RX_SCAN_LOCK_LOSS_MS (sx1276_rx_scan_policy.h): a LOCKED scan drops # back to SCANNING only once SX1276_RX_SCAN_LOCK_LOSS_MS has passed since its -# channel anchor, and a LOCK resets the attempt count but keeps that anchor -# (sx1276_rx.c scan_drive: LOCK leaves the anchors unchanged), which the -# activation's BEGIN_SCAN set (sx1276_rx_scan_reset -> BOOT -> BEGIN_SCAN) or -# a later ADVANCE moved on. So a lock and its loss take at least this long -# from the activation, and a final RX_SCAN_FAILED less than SCAN_BUDGET_S + -# this after it ends 4 attempts with no lock. For this rule the activation +# channel anchor, and a LOCK resets the attempt count. Up to the RS-12.21 +# image a LOCK kept that anchor (sx1276_rx.c scan_drive: LOCK left the anchors +# unchanged), which the activation's BEGIN_SCAN set (sx1276_rx_scan_reset -> +# BOOT -> BEGIN_SCAN) or a later ADVANCE moved on; from it (RS-12.21 L) a LOCK +# anchors it at the locking frame. Either way a lock and its loss take at +# least this long from the activation, and a final RX_SCAN_FAILED less than +# SCAN_BUDGET_S + this after it ends 4 attempts with no lock. For this rule the activation # is dated by the last timestamped line before it, never later than it, so # the fault's time from it is never read short (the timestamped lines either # side of it are 0.34-0.42 s apart on the 45 committed profile-1 archives). @@ -830,8 +851,8 @@ def scan_fault_sub(sub: int, strict: bool = True) -> dict | None: ``crc_seen`` (bit 4, never with hw_suspect), ``marker``: 0x80, the final attempt 0 sx1276_rx_scan_fail_eval sends for a NULL input (its defensive path). None when the byte does not fit that layout: code 0x0D - is also TX_FIFO_RB_MISMATCH (host_types.h), whose sub is the fragment's - tx_id. The same 25 values as rx_scan_fail_sub_valid + was also TX_FIFO_RB_MISMATCH on images before the 0x0D split (e083ae46 + and older), whose sub is the fragment's tx_id. The same 25 values as rx_scan_fail_sub_valid (method_g_stage1_probe.py, the daemons' decoder). ``strict`` False reads the bits whatever they hold (the daemon named the URC RX_SCAN_FAILED).""" attempt, final = sub & SCAN_SUB_ATTEMPT, bool(sub & SCAN_SUB_FINAL) @@ -869,8 +890,45 @@ def bracket_demotions(pre: dict | None, post: dict | None) -> tuple[int, int] | return None if d is None else (d["clk_demotion_reset"], d["clk_demotion_kept"]) +def scan_image(lines: list[str], act: int, faults: list[dict], + override: str = "auto") -> tuple[bool | None, str]: + """``(restarts, source)``: whether the base's image restarts its scan + after a final RX_SCAN_FAILED (the RS-12.21 image, scan option (b): True) + or keeps an absorbing FAILED (every image before it: False), or None when + nothing here says (then read as absorbing, the rule those images need), + and what says so. In order: ``override`` (--scan-image restarts / + absorbing; "auto" reads on); the capability bitmap on the last + image_rx_daemon.py VER warm-up line before the activation (logged from the + 2026-10-24 review on; bit 7, HOST_CAP_RADIO_PARK, is the RS-12.21 image's); + an RX_SCAN_FAILED after a final one in the window (``faults``, the scan + faults), which an absorbing FAILED never sends.""" + if override not in SCAN_IMAGES: + raise ValueError(f"scan image {override!r}: one of {', '.join(SCAN_IMAGES)}") + if override != "auto": + return override == "restarts", f"--scan-image {override} given" + for n in range(act - 1, -1, -1): + m = VER_CAP_RE.search(lines[n]) + if m: + cap = int(m.group(1), 16) + bit = "set" if cap & HOST_CAP_RADIO_PARK else "clear" + return bool(cap & HOST_CAP_RADIO_PARK), ( + f"rx_daemon.log line {n + 1} logs the L072's VER capability bitmap " + f"0x{cap:08X}, bit 7 (HOST_CAP_RADIO_PARK, the RS-12.21 image) {bit}") + finals = [f for f in faults if f["scan"]["final"]] + # (later by more than a second line of the same URC could be: the next + # report comes a 30 s attempt later at the soonest) + later = [f for f in faults if finals and f["line"] > finals[0]["line"] + and f["t"] > finals[0]["t"] + FINAL_REPEAT_SLACK_S] + if later: + return True, (f"the base sent an RX_SCAN_FAILED at rx_daemon.log line " + f"{later[0]['line']} after its final one at line {finals[0]['line']}, " + f"which only an image that restarts its scan does") + return None, ("rx_daemon.log logs no VER capability bitmap (image_rx_daemon.py " + "before the 2026-10-24 review) and no RX_SCAN_FAILED after a final one") + + def acquisition(rx: str, tx: str | None = None, pre: dict | None = None, - post: dict | None = None) -> dict: + post: dict | None = None, image: str = "auto") -> dict: """The base's FHSS cold-start acquisition (2026-10-24, leg 6), one rule for rs12_leg_report.py and rs13_probe_score.py. @@ -888,24 +946,42 @@ def acquisition(rx: str, tx: str | None = None, pre: dict | None = None, always on while the RX worker lives: ``crash``, when it died, ends both). ``status``, on positive evidence only (``basis`` says which; ``failed`` - True / False / None for FAILED / ACQUIRED / NOT DETERMINED): - - * FAILED on firmware evidence: a final RX_SCAN_FAILED that is cold - (warm=0: never locked since the L072 booted), or that came less than - SCAN_BUDGET_S + SCAN_LOCK_LOSS_S after the activation (4 attempts in a - row with no lock; the activation dated by the last timestamped line - before it). This wins over a lock shown; - * FAILED on silence: no reception shown within SCAN_BUDGET_S, and an - instrument known to be on that shows none (a stats line after the - budget still at the activation's rx_frames, the RX worker alive; or - per-fragment logging on, a frag_arrival line anywhere, with none in - the budget), and nothing that contradicts it: no lock shown (below), no - FAULT_URC-logging daemon (``logged``) without an RX_SCAN_FAILED line, - no base bracket whose d(radio_rx_ok) exceeds the host's count by more - than d(rx_urc_lost) + HOST_DROP_SLACK (in a window no switch cut, the - bracket spanning the leg). With a contradiction it is NOT - DETERMINED, naming it: the scan's FAILED is absorbing, so the silence - is the host's, not the scan's; + True / False / None for FAILED / ACQUIRED / NOT DETERMINED). The rules + depend on the base's image (``restarts`` and ``image``, scan_image(): + ``image`` "auto", or "restarts" / "absorbing" from --scan-image); an + image not identified is read as absorbing, every image before RS-12.21: + + * FAILED on firmware evidence, absorbing image: a final RX_SCAN_FAILED + that is cold (warm=0: never locked since the L072 booted), or that + came less than SCAN_BUDGET_S + SCAN_LOCK_LOSS_S after the activation + (4 attempts in a row with no lock; the activation dated by the last + timestamped line before it). This wins over a lock shown: FAILED is + absorbing, so nothing locks after it; + * the same finals on the RS-12.21 image (scan option (b)) are 30 s + reports: its scan restarts at once and sends a final every 30 s + until a LOCK. From the first such final the chain of finals runs to a + sign of a LOCK (a later RX_SCAN_FAILED that is not final, or warm + after a cold one), else to the last final. A lock shown is placed + after the chain by that sign, by a cold last final (no lock before + it), or by a FAULT_URC-logging log that runs more than SCAN_ATTEMPT_S + + FINAL_REPEAT_SLACK_S past it with no further final: then the lock + counts, as a late acquisition (``basis`` says after which final). + Without a placement, and with the log ending (or FAULT_URC logging + off) within that span of the last final, it is FAILED with no lock + shown, NOT DETERMINED with one (the bracket and the fragment share + span the leg). A sign of a LOCK is itself a lock shown; + * FAILED on silence: no reception shown within SCAN_BUDGET_S (on the + RS-12.21 image: in the whole window -- its scan does not strand), and + an instrument known to be on that shows none (a stats line after the + budget, or the window's last, still at the activation's rx_frames, the + RX worker alive; or per-fragment logging on, a frag_arrival line + anywhere, with none in the budget or the window), and nothing that + contradicts it: no lock shown (below), no FAULT_URC-logging daemon + (``logged``) without an RX_SCAN_FAILED line, no base bracket whose + d(radio_rx_ok) exceeds the host's count by more than d(rx_urc_lost) + + HOST_DROP_SLACK (in a window no switch cut, the bracket spanning the + leg). With a contradiction it is NOT DETERMINED, naming it: the + silence is the host's, not the scan's; * ACQUIRED: no FAILED, and a lock shown in a window no switch cut: the base bracket (``pre`` / ``post``, parse_stats) counts a LOCKED -> SCANNING demotion, or the base decoded at least GRID_SHARE of the @@ -1023,7 +1099,11 @@ def acquisition(rx: str, tx: str | None = None, pre: dict | None = None, for ln in lines[:acts[0]]: # the start-up configuration's mask mask = MASK_RE.search(ln) or mask channels = (int(mask.group(1)) if mask.group(1) else 1) if mask else None - out.update(applies=True, t_act=t_act, act_line=at + 1, + restarts, image_why = scan_image( + lines, acts[0], [f for f in faults if f["scan"] and not f["scan"]["marker"] + and f["name"] != "AMBIGUOUS"], image) + out.update(applies=True, t_act=t_act, act_line=at + 1, restarts=restarts, + image=image_why, lag=None if lo is None else t_act - _log_time(lines[lo]), lo_line=None if lo is None else lo + 1, cut=cut, crash=crash, first=first, first_line=first_line, first_frag=first_frag, heard=heard, @@ -1038,74 +1118,163 @@ def acquisition(rx: str, tx: str | None = None, pre: dict | None = None, return out +def _lock_evidence(acq: dict) -> tuple[list[str], list[str]]: + """``(locked, short)`` for :func:`_acquisition_verdict`: what shows a lock + in the window (a bracket demotion, the grid share), and what falls short + of showing one.""" + cut = acq["cut"] + locked, short = [], [] + if cut: + # the bracket and the tractor's fragments span the leg, not the scan + short.append(f"the base switched to profile {cut['profile']} at rx_daemon.log line " + f"{cut['line']} (+{cut['t']:.1f} s), which stops the FHSS scan: what it " + f"decoded after that is not the scan's, so neither its share of the " + f"tractor's fragments nor a bracket demotion is weighed") + return locked, short + dem = acq["demotions"] + if dem and sum(dem): + locked.append(f"the base's bracket counts {sum(dem)} LOCKED -> SCANNING " + f"demotion(s) (d(clk_demotion_reset) {dem[0]}, d(clk_demotion_kept) " + f"{dem[1]}), which only a locked scan makes") + elif dem: + short.append("the base's bracket counts no LOCKED -> SCANNING demotion") + rx_n, tx_n, ch = acq["rx_frags"], acq["tx_frags"], acq["channels"] + if tx_n and ch and ch >= GRID_MIN_CHANNELS: + least = "" if acq["rx_exact"] else "at least " + share = (f"the base decoded {least}{rx_n} of the tractor's {tx_n} fragments on air " + f"({least}{100 * rx_n / tx_n:.1f} %)") + if rx_n >= GRID_SHARE * tx_n: + locked.append(f"{share}: on its {ch}-channel hop set a receiver that does not " + f"follow the hop grid is on the tractor's channel about one slot " + f"in {ch}, and only a locked scan follows the grid") + elif not acq["rx_exact"]: + short.append(f"the base's fragment count is unknown: its stats thread died " + f"(rx_daemon.log holds '{STATS_CRASH}') and per-fragment logging " + f"was off, so its published frames give only a floor ({share})") + else: + short.append(f"{share}, under the {100 * GRID_SHARE:g} % that shows a receiver " + f"following its {ch}-channel hop grid") + elif not tx_n: + short.append("no tractor fragment count (tx_daemon.log) to weigh the base's against") + else: + short.append(f"rx_daemon.log names no FHSS hop set of {GRID_MIN_CHANNELS} or more " + f"channels to weigh the base's fragment count against") + return locked, short + + def _acquisition_verdict(acq: dict) -> tuple[str, str]: """``(status, basis)`` for :func:`acquisition` (its docstring has the rule).""" budget = SCAN_BUDGET_S - final = [f for f in acq["faults"] if f["scan"] and f["scan"]["final"] - and not f["scan"]["marker"] and f["name"] != "AMBIGUOUS"] - failed = [] + restarts = bool(acq["restarts"]) + scan = [f for f in acq["faults"] if f["scan"] and not f["scan"]["marker"] + and f["name"] != "AMBIGUOUS"] + final = [f for f in scan if f["scan"]["final"]] + # 2026-10-24 review: RS-12.21 L anchors the loss timer at the LOCK itself + anchor = ("on this image anchors the loss timer at the locking frame (RS-12.21 L)" + if restarts else "keeps the channel anchor the activation's BEGIN_SCAN set") + failed = [] # (fault, why): the finals the absorbing rule reads as FAILED for f in final: where = (f"the base's final RX_SCAN_FAILED at +{f['t']:.1f} s (rx_daemon.log line " f"{f['line']}, sub 0x{f['sub']:02X})") if not f["scan"]["warm"]: - failed.append(f"{where} is cold (warm=0): the base's L072 had not locked since it " - f"booted") + failed.append((f, f"{where} is cold (warm=0): the base's L072 had not locked " + f"since it booted")) elif acq["lag"] is not None and f["t"] + acq["lag"] < budget + SCAN_LOCK_LOSS_S: - failed.append(f"{where} came less than {budget + SCAN_LOCK_LOSS_S:g} s after the " - f"activation (at most +{f['t'] + acq['lag']:.1f} s from rx_daemon.log " - f"line {acq['lo_line']}, the last timestamped line before it): 4 scan " - f"attempts failed in a row from it (a lock resets the attempt count " - f"but keeps the channel anchor the activation's BEGIN_SCAN set, so a " - f"locked scan drops back to scanning no sooner than " - f"SX1276_RX_SCAN_LOCK_LOSS_MS {SCAN_LOCK_LOSS_S * 1000:.0f} after the " - f"activation)") - if failed: - return FAILED, "; ".join(failed) + failed.append((f, f"{where} came less than {budget + SCAN_LOCK_LOSS_S:g} s after " + f"the activation (at most +{f['t'] + acq['lag']:.1f} s from " + f"rx_daemon.log line {acq['lo_line']}, the last timestamped line " + f"before it): 4 scan attempts failed in a row from it (a lock " + f"resets the attempt count but {anchor}, so a locked scan drops " + f"back to scanning no sooner than SX1276_RX_SCAN_LOCK_LOSS_MS " + f"{SCAN_LOCK_LOSS_S * 1000:.0f} after the activation)")) + if failed and not restarts: + return FAILED, "; ".join(why for _f, why in failed) cut, crash = acq["cut"], acq["crash"] - locked, short = [], [] - if cut: - # the bracket and the tractor's fragments span the leg, not the scan - short.append(f"the base switched to profile {cut['profile']} at rx_daemon.log line " - f"{cut['line']} (+{cut['t']:.1f} s), which stops the FHSS scan: what it " - f"decoded after that is not the scan's, so neither its share of the " - f"tractor's fragments nor a bracket demotion is weighed") - else: - dem = acq["demotions"] - if dem and sum(dem): - locked.append(f"the base's bracket counts {sum(dem)} LOCKED -> SCANNING " - f"demotion(s) (d(clk_demotion_reset) {dem[0]}, d(clk_demotion_kept) " - f"{dem[1]}), which only a locked scan makes") - elif dem: - short.append("the base's bracket counts no LOCKED -> SCANNING demotion") - rx_n, tx_n, ch = acq["rx_frags"], acq["tx_frags"], acq["channels"] - if tx_n and ch and ch >= GRID_MIN_CHANNELS: - least = "" if acq["rx_exact"] else "at least " - share = (f"the base decoded {least}{rx_n} of the tractor's {tx_n} fragments on air " - f"({least}{100 * rx_n / tx_n:.1f} %)") - if rx_n >= GRID_SHARE * tx_n: - locked.append(f"{share}: on its {ch}-channel hop set a receiver that does not " - f"follow the hop grid is on the tractor's channel about one slot " - f"in {ch}, and only a locked scan follows the grid") - elif not acq["rx_exact"]: - short.append(f"the base's fragment count is unknown: its stats thread died " - f"(rx_daemon.log holds '{STATS_CRASH}') and per-fragment logging " - f"was off, so its published frames give only a floor ({share})") - else: - short.append(f"{share}, under the {100 * GRID_SHARE:g} % that shows a receiver " - f"following its {ch}-channel hop grid") - elif not tx_n: - short.append("no tractor fragment count (tx_daemon.log) to weigh the base's against") + locked, short = _lock_evidence(acq) + late = None + if failed: + # RS-12.21 (b), 2026-10-24 review: on this image a final is a 30 s + # report. The chain of finals from the first one the absorbing rule + # would read as FAILED runs to a sign of a LOCK, else to the last. + first = failed[0][0] + last, sign, n = first, None, 1 + for f in scan: + if f["line"] <= first["line"]: + continue + if f["sub"] == last["sub"] and f["t"] <= last["t"] + FINAL_REPEAT_SLACK_S: + continue # a second line of the same URC + if not f["scan"]["final"] or (f["scan"]["warm"] and not last["scan"]["warm"]): + sign = f + break + last, n = f, n + 1 + reports = (f"{failed[0][1]}, but on this image a final is a {SCAN_ATTEMPT_S:g} s " + f"report, not a stop (RS-12.21 scan option (b): the scan restarts at once " + f"and reports again every {SCAN_ATTEMPT_S:g} s until a LOCK; " + f"{acq['image']})" + + (f"; {n} final reports in a row, the last at +{last['t']:.1f} s " + f"(rx_daemon.log line {last['line']})" if n > 1 else "")) + gap = acq["span"] - last["t"] + quiet = (sign is None and acq["logged"] + and gap > SCAN_ATTEMPT_S + FINAL_REPEAT_SLACK_S) + if sign is not None: + s = sign["scan"] + locked.append(f"the base's RX_SCAN_FAILED at +{sign['t']:.1f} s (rx_daemon.log " + f"line {sign['line']}, sub 0x{sign['sub']:02X}) is " + + (f"attempt {s['attempt']}, not final: the attempt count restarts " + f"only at a LOCK" if not s["final"] else + "warm after a cold one: the L072 sets warm at a LOCK")) + placed = (f"the scan locked between +{last['t']:.1f} s and +{sign['t']:.1f} s") + elif locked and not last["scan"]["warm"]: + placed = (f"the last final is cold (warm=0: no lock since the L072 booted), so the " + f"lock shown came after +{last['t']:.1f} s") + elif locked and quiet: + placed = (f"no RX_SCAN_FAILED followed the last final in the {gap:.1f} s the log " + f"runs past it, so the lock shown came within {SCAN_ATTEMPT_S:g} s of " + f"+{last['t']:.1f} s") + else: + placed = None + if placed is not None: + late = f"a late acquisition: {reports}; {placed}" + elif not quiet: + tail = (f"the log ends {gap:.1f} s after the last final, within the " + f"{SCAN_ATTEMPT_S + FINAL_REPEAT_SLACK_S:g} s a next report may take" + if acq["logged"] else + "this daemon does not log every FAULT_URC (no FAULT_URC line), so no " + "missing report shows a lock") + if locked: + return UNDETERMINED, (f"{reports}; nothing places the lock shown after the " + f"last final ({tail}), and its evidence spans the leg: " + + "; ".join(locked)) + return FAILED, f"{reports}; nothing shows a lock after the last final ({tail})" else: - short.append(f"rx_daemon.log names no FHSS hop set of {GRID_MIN_CHANNELS} or more " - f"channels to weigh the base's fragment count against") + short.append(f"{reports}; no RX_SCAN_FAILED followed the last final in the " + f"{gap:.1f} s the log runs past it, which on this image means a LOCK " + f"within {SCAN_ATTEMPT_S:g} s of it unless its FAULT URCs were lost, " + f"but nothing else shows one") if crash: short.append(f"the base's image-rx-worker thread died at +{crash[0]:.1f} s " f"(rx_daemon.log line {crash[1]}): its stats counter and frag_arrival " f"lines stop counting there") first, heard, quiet = acq["first"], acq["heard"], acq["quiet"] - within = (first is not None and first <= budget) or bool(heard and heard[0] <= budget) + # RS-12.21 (b): a scan that misses its budget does not strand, so silence + # counts over the whole window, not the budget + if restarts: + within = first is not None or bool(heard) + scope = f"in the {acq['span']:.1f} s window" + else: + within = (first is not None and first <= budget) or bool(heard and heard[0] <= budget) + scope = f"within the {budget:g} s budget" silent = [] - if not within: + if not within and restarts: + if quiet and not crash and not acq["stats_dead"]: + silent.append(f"the base's stats counter still read rx_frames={acq['base_rx']} at " + f"+{quiet[0]:.1f} s (rx_daemon.log line {quiet[1]}), its last stats " + f"line in the window (always on, it counts every image fragment, " + f"logged or not)") + if acq["frag_on"] and acq["first_frag"] is None and not crash: + silent.append("per-fragment logging was on (rx_daemon.log holds frag_arrival " + f"lines) and none came in the {acq['span']:.1f} s window") + elif not within: if quiet and quiet[0] > budget: silent.append(f"the base's stats counter still read rx_frames={acq['base_rx']} at " f"+{quiet[0]:.1f} s (rx_daemon.log line {quiet[1]}; always on, it " @@ -1132,32 +1301,36 @@ def _acquisition_verdict(acq: dict) -> tuple[str, str]: doubt.append(f"the base's bracket counts d(radio_rx_ok) {rx_ok} against the host's " f"{rx_n} (FIRMWARE DROP {rx_ok - rx_n}, d(rx_urc_lost) " f"{acq['urc_lost']}): the L072 delivered frames the host never counted") - text = "; ".join(silent) + f": no image fragment reached the base within the {budget:g} s budget" + text = "; ".join(silent) + f": no image fragment reached the base {scope}" if not doubt: return FAILED, text + ", and its scan locks only on a frame it decodes" + why = ("A scan that runs out reports it and scans on (this image restarts it, " + "RS-12.21 (b)), so this silence is the host's, not the scan's" if restarts else + "The scan's FAILED is absorbing (only a re-activation leaves it), so this " + "silence is the host's, not the scan's") return UNDETERMINED, ( - text + " by the host's count, but " + "; ".join(doubt) + ". The scan's FAILED is " - "absorbing (only a re-activation leaves it), so this silence is the host's, not " - "the scan's: its RX path missed what the base received (UART contention, a " - "stalled RX worker, lost URCs), or the base locked on a command frame, which " - "rx_frames does not count") + text + " by the host's count, but " + "; ".join(doubt) + f". {why}: its RX path " + "missed what the base received (UART contention, a stalled RX worker, lost " + "URCs), or the base locked on a command frame, which rx_frames does not count") if locked: - return ACQUIRED, "; ".join(locked) + return ACQUIRED, "; ".join(locked + ([late] if late else [])) if within: lead = ("a frame received is not a lock (sx1276_rx.c still delivers a frame whose FHSS " "header it rejected), and nothing here shows one") - elif first is None and not heard and acq["span"] <= budget: + elif first is None and not heard and acq["span"] <= budget and not restarts: lead = (f"the scan ran {acq['span']:.1f} s before the base's profile switch, no more " f"than the budget" if cut else f"the base's log covers {acq['span']:.1f} s, no more than the budget") else: lead = (("the base's first frame line came past the budget" if first is not None or heard else "the base's log holds no frame line") - + ", but no instrument known to be on shows that no frame reached it within " - "the budget" - + ("" if crash else f" (no stats line after it still at " - f"rx_frames={acq['base_rx']}, no frag_arrival line to show " - f"per-fragment logging on)")) + + ", but no instrument known to be on shows that no frame reached it " + + ("in the window" if restarts else "within the budget") + + ("" if crash else + " (no live stats line in it, no frag_arrival line to show per-fragment " + "logging on)" if restarts else + f" (no stats line after it still at rx_frames={acq['base_rx']}, no " + f"frag_arrival line to show per-fragment logging on)")) return UNDETERMINED, "; ".join([lead] + short) @@ -1237,12 +1410,28 @@ def format_acquisition(acq: dict) -> list[str]: f" The tractor logged {acq['tractor']} frame(s) on air." + (" It sent nothing, so the scan had nothing to find." if not acq["tractor"] else "")) - out.append(f" !! ACQUISITION FAILED: {acq['basis']}. Its scan ran out into FAILED, " - "which is absorbing (sx1276_rx_scan_policy.c): a follower there never " - "hops and hears only what crosses the channel it is parked " - "on, until a reset or a profile re-activation. This leg's " - "loss and command figures measure that, not the link " - "(2026-10-24 leg 6)." + sent) + if acq["restarts"]: + # 2026-10-24 review: the RS-12.21 image does not strand the scan + out.append(f" !! ACQUISITION FAILED: {acq['basis']}. This image restarts the " + "scan after each final RX_SCAN_FAILED (RS-12.21 scan option (b)), so " + "the base was not stranded on one channel, but it never locked onto " + "the tractor's hop grid in the window: this leg's loss and command " + "figures measure an unacquired base, not the link." + sent) + else: + # 2026-10-24 review: an image not identified is read by the old + # rule; say so, and how to read it as the RS-12.21 image + unknown = ("" if acq["restarts"] is not None else + f" The base's image was not identified ({acq['image']}), so the " + "rule of every image before RS-12.21 read it; the RS-12.21 image " + "(VER capability bit 7) restarts its scan after a final " + "RX_SCAN_FAILED and counts silence over the whole window -- if it " + "flew, run again with --scan-image restarts.") + out.append(f" !! ACQUISITION FAILED: {acq['basis']}. Its scan ran out into " + "FAILED, which is absorbing on every image before RS-12.21 " + "(sx1276_rx_scan_policy.c): a follower there never hops and hears " + "only what crosses the channel it is parked on, until a reset or a " + "profile re-activation. This leg's loss and command figures measure " + "that, not the link (2026-10-24 leg 6)." + unknown + sent) else: out.append(f" confirmed acquisition: {acq['status']}: {acq['basis']}") scan = [f for f in acq["faults"] if f["scan"]] @@ -1269,7 +1458,10 @@ def cell(f): out.append(f" !! the base's final RX_SCAN_FAILED at +{f['t']:.1f} s " f"(rx_daemon.log line {f['line']}, sub 0x{f['sub']:02X}" + (", AMBIGUOUS with TX_FIFO_RB_MISMATCH" if amb else "") - + "): its scan entered FAILED there" + + ("): its scan reported FAILED there and scanned on (this image " + "restarts it at once and reports every 30 s until a LOCK, " + "RS-12.21 (b))" if acq["restarts"] else + "): its scan entered FAILED there") + (" if this URC was the scan's (the daemon could not rule " "out a TX FIFO readback mismatch on a TX with that tx_id)" if amb else "")) @@ -1320,6 +1512,12 @@ def main(argv: list[str] | None = None) -> int: ap.add_argument("--capture", type=pathlib.Path, help="base-side vector_dry_run capture JSONL: adds the " "frame loss from TileDeltaFrame sequence gaps") + ap.add_argument("--scan-image", choices=SCAN_IMAGES, default="auto", + help="how the base's L072 image treats a final RX_SCAN_FAILED: " + "'restarts' (the RS-12.21 image, scan option (b): a 30 s " + "report, the scan goes on) or 'absorbing' (every image " + "before it); 'auto' (default) reads the VER capability " + "bitmap image_rx_daemon.py logs, else the faults") args = ap.parse_args(argv) rx = (args.archive / "rx_daemon.log").read_text(encoding="utf-8", @@ -1346,7 +1544,7 @@ def main(argv: list[str] | None = None) -> int: if args.pre and args.post else (None, None)) except OSError: brackets = (None, None) - acq = acquisition(rx, tx, *brackets) + acq = acquisition(rx, tx, *brackets, image=args.scan_image) print("!! ACQUISITION FAILED (see the acquisition line below)" if acq.get("failed") else "") # 2026-10-19: every harness up to the 10-19 legs read the daemon logs