Status: sim/analysis result (2026-06-23). Answers #76: should the coordinator's modulation authority
extend from SF-only (the shipped per-cell/per-link adaptation, #66) to SF + bandwidth (+ coding
rate), and does it net capacity/robustness beyond the SF gains already shipped? Experiment:
sim/experiments/bw_cr_adaptation.py (uses the sim's real PHY: phy.time_on_air_ms airtime + the per-SF
DEMOD_SNR_FLOOR_DB + the BW-dependent noise floor). Figure: bw_cr_adaptation.png.
A link (SF, BW) closes iff rssi − noise_floor(BW) − DEMOD_SNR_FLOOR[SF] ≥ margin (6 dB, the sf_select
default), where noise_floor(BW) = −174 + 10·log10(BW) + NF (BW125 −117 dBm, BW250 −114, BW500 −111 — wider
bandwidth raises the floor ~3 dB/doubling). Airtime halves per SF step and per BW doubling. So the two
levers trade sensitivity for speed at different exchange rates, which is exactly why they are not
redundant:
| Lever | airtime ×0.5 costs | granularity |
|---|---|---|
| SF step (e.g. SF9→SF8) | ~2.5 dB sensitivity | coarse (factor-2 jumps), floors at SF7 |
| BW double (e.g. 125→250) | ~3.0 dB sensitivity | a second axis, usable below the SF7 floor |
| CR (4/5→4/8) | adds airtime (+12/+23/+35%) | not a speed lever — coding gain (robustness) |
SF-only adaptation saturates at SF7/BW125 = 85 ms for the telemetry frame: SF7 is the fastest spreading
factor, so once a link is strong enough for SF7 at BW125, SF has no faster gear. Every strong link — the
near-field members, the median good-geometry link, the whole bench — is pinned there. Adding bandwidth takes
that same link to SF7/BW500 = 21 ms, a −75% airtime cut (×4 capacity) (bw_cr_adaptation.py §1). Across
a −124…−88 dBm sweep, SF+BW cuts telemetry airtime at every sampled RSSI (mean −57% where it helps, max
−75%). Because cell capacity (members served, message slots per frame) scales as 1/airtime, this is a
direct ×1.5–4 capacity multiplier for strong-link cells — larger than the ×1.6–2.0 the per-link SF work
nets (#66), and structurally orthogonal: SF picks the gear, BW picks the lane width.
Even away from the SF7 floor, BW gives finer control: at RSSI −118 SF-only must use SF7/BW125 (85 ms) but
SF+BW reaches SF8/BW250 (80 ms) at the same 6 dB margin; at −116, SF9/BW500 (70 ms) beats SF7/BW125 (85 ms).
A member with an intermediate margin no longer rounds down to the next whole SF — it spends the margin on
the lever that fits (bw_cr_adaptation.py §2).
4/5 (cr=1) is the fastest coding rate; 4/6/4/7/4/8 cost +12/+23/+35% airtime (bw_cr_adaptation.py §3).
So CR is never selected for capacity. Its value is the orthogonal axis the SNR-floor model doesn't capture:
forward-error-correction coding gain against burst interference and fading — exactly the partial-collision
and deep-fade conditions a hidden-node hub sees. CR adaptation is therefore a reliability knob: raise CR on
a member whose link is marginal or whose frames are being clobbered, paying airtime for resilience; leave it
at 4/5 for the clean, capacity-bound common case.
Extend the coordinator's modulation authority from SF to the (SF, BW) pair, reusing the #66
machinery:
- Per-cell: worst-member-driven (SF, BW) — the cell runs the fastest pair the worst member supports.
A drop-in extension of per-cell auto-SF: the selector already ranks by airtime; widen its candidate set
from {SF}×{125} to {SF}×{125,250,500}.
- Per-link (the high-capacity case): each member's slot at its own (SF, BW); the beacon at the
most-sensitive pair (slowest SF and narrowest BW = BW125, so every member hears it), members TX at
their own pair, the coordinator retunes (SF, BW) per block — the same per-block retune the per-link SF
path already does (set_modulation already re-sends BW with SF), just widening the per-member descriptor.
- CR: a separate, conservative robustness override (default 4/5; raise per-member on measured
loss/marginality), not part of the capacity selection.
{125,250,500}.lora_toa(sf, bw, pl) already take
BW, so slot widths recompute correctly; the new cost is the per-member BW in the roster wire format (+ bits)
and the per-block BW retune. This is a contained extension of #66, not a new subsystem.Yes — BW adaptation nets substantially beyond SF-only, primarily by lifting the SF7 capacity floor (×4
for strong links) and secondarily by filling the inter-SF gaps. It is the larger capacity lever for
high-link-budget cells and composes with the shipped per-link SF work. CR is worth adding as a separate
robustness override but never as a capacity move. Recommend implementing (SF, BW) selection as the next
modulation-authority increment (a new issue off #66), CR as a follow-on reliability knob.