← RFC

Stage 5 — PHY-agility & multi-cell HW validation (result, #73)

Status: hardware results (2026-06-23). Closes the documentation gap the #11 RFC audit found: the PHY-agility (§6) and multi-cell-backbone (§7) mechanisms were brought up on real radios but their results lived only in issue comments, so the paper could cite them only as mechanism/sim. This doc is the citable record; figures are reproducible from the cited logs via sim/results/chart_*_hw.py.

Bench/tower fleet. Coordinator = Raspberry Pi + Nebra Duo HAT (SX1262 spidev0.0, LR1121 spidev0.1) at −9 dBm chip power, 915 MHz. Clients: Heltec V3 093547 (ttyUSB0), Heltec V4 AFD03C (ttyACM1). Tower node MID (192.168.10.108, SX1262) ~80 ft from the bench for the backbone test. Each result was observed on a separate receiver (the coordinator hears the clients; the duomesh SX1301 concentrator and the tower cross-confirm on-air) per the project's confirm-on-another-radio rule.


1. Per-cell adaptive SF — worst-member-driven SF9→SF7 (#66 Phase 1)

A cell booted at SF9 with LW_AUTO_SF=1 and two members joined. From the members' registration SNR the coordinator ran sf_select (worst-member) and auto-selected SF7, announced the change, and switched; both members followed and served at SF7.

[auto-sf] SF9 -> SF7 (worst of 2 members); announcing, switch @epoch 15916
[phy-change] SWITCHED @epoch 15916 -> SF7 ch 915.0 MHz

Coordinator at SF9 with LW_PER_LINK=1 LW_FORCE_SF=093547:7,AFD03C:9 (forced mix, since the bench has no natural SNR spread). The beacon ran at the slowest member SF (SF9), the roster was SF-sorted with a per-member SF, and the coordinator retuned per SF-block to receive each member at its own SF.

[per-link] FRAME beacon@SF9 ["093547@SF7", "AFD03C@SF9"] telem_region=446ms

3. Multi-cell backbone — role-B half-frame stagger over real separation (#60, §7)

Two coordinators ~80 ft apart: tower MID = backbone role A (free-running anchor, node 7F55DB), bench = backbone role B (node 908382), both @915, FRAME_T 8800 ms (half 4400 ms).

bench role B:  role=B: coordinator present — staggering +4400 ms
               ADJACENCY UP: 7F55DB (rssi -62)
               LSA 1 sent (1 members, 1 adj)
tower role A:  ADJACENCY UP: 908382 (rssi -76)
               LSA from 908382: seq 1 members 1 adj 1

4. Frequency agility — autonomous DFS off a live jammer (#65)

A cell @915 ran the dual-radio autonomous-DFS monitor (LR1121 on spidev0.1, MIN-RSSI over a 1500 ms window + an absolute busy floor LW_DFS_BUSY_DBM=−100). With three candidate channels (915/916/917) all at the noise floor (~−117 to −120 dBm), the cell stayed put. A tower-sourced CW jammer (MID node, 22 dBm @915, ~80 ft) then raised 915's measured min-RSSI to −79…−83 dBm — a ~40 dB spike. The monitor flagged it (current ch −82 dBm vs quietest 917.0 MHz −118 dBm (>=12 dB busier) → announce move), the cell hopped 915→917 (the quietest candidate), and the member followed and resumed delivering on 917 — no operator action. The MIN-RSSI window + busy floor are what reject the cell's own beacon and noise-floor jitter (false hops); here the 36 dB margin over the gate is unambiguous. - Figure: dfs_off_jam_hw.png (per-candidate min-RSSI over time; 915 spikes through the busy floor at the jam). Source: sim/results/dfs_off_jam_hw.log.

5. Frequency agility — announced channel change, member follows (#65)

A running cell @915 announced a coordinator-driven change to 914 MHz (offset 140) via the PHY-change FSM: the upcoming (freq, switch_epoch) rides the beacon, so members pre-tune at the switch epoch instead of blind-rescanning. The member delivered 30 telemetry frames @915, then 31 @914 with no delivery gap across the switch — the unbroken delivery is itself the proof the member pre-tuned (a blind re-scan would cost a multi-second-to-minute discovery gap, cf. discovery-latency-result.md). - Figure: channel_change_hw.png (cumulative delivery, continuous across the 915→914 step). Source: sim/results/channel_change_hw.log.

6. PACT-lite membership round-trip — two-check delivery (#70, §5)

The rotating-pool + on-demand-grant message path was confirmed end-to-end on two V3s: a 112-byte body → 162-byte on-air message was granted a 900 ms message slot, relayed by the coordinator, and received + auto-receipted (both the message and the delivery receipt crossed), while both members' rotating-slot telemetry kept flowing — the membership scheme serves a message-sized frame and its ack without disturbing the steady telemetry roster.


Summary

Mechanism Result Data
Per-cell adaptive SF (§6) SF9→SF7 auto-select from +12 dB SNR; 78 frames served @SF7, members follow fresh chart adaptive_sf_hw.png
Per-link mixed-SF (§6) SF7 + SF9 members heard concurrently (n=38/42); 446 vs 640 ms region (−30%) fresh chart perlink_mixed_sf_hw.png
Backbone role-B stagger (§7) +4400 ms half-frame stagger @80 ft, adjacency −62/−76 dBm, LSA exchange measured (#60)
Autonomous DFS off jam (§6) jam spikes 915 to −82 dBm; cell hops 915→917 (36 dB over gate), member follows fresh chart dfs_off_jam_hw.png
Announced channel change (§6) 915→914 FSM; 30 frames @915 → 31 @914, no re-scan gap fresh chart channel_change_hw.png
PACT-lite two-check (§5) 162 B message + receipt round-trip on the rotating-pool/on-demand path verified (#70)

Rows 1–5 carry fresh 2026-06-23 measurement and charts; row 6 (PACT-lite two-check) is HW-verified and recorded in #70. All four PHY-agility figures (adaptive_sf_hw.png, perlink_mixed_sf_hw.png, dfs_off_jam_hw.png, channel_change_hw.png) reproduce from the committed logs via chart_*_hw.py.