Summary
SF5 and SF6 links work on our hardware, but packetScoreInt() returns 0.0f for sf < 7, so every
packet received at those spreading factors scores zero.
src/helpers/radiolib/RadioLibWrappers.cpp:266:
The guard is necessary as written — snr_threshold[] holds only six entries (SF7…SF12) and is
indexed sf - 7, so SF5/SF6 would read out of bounds without it.
Measured on the bench (2026-08-10)
RAK4631 + Heltec V3, both SX1262, 869.618 MHz / 62.5 kHz / CR5, MESH_DEBUG=1 MESH_PACKET_LOGGING=1:
| SF |
decoded |
SNR |
score |
airtime |
| 8 (control) |
8/8 |
11–13 |
1000 |
836 ms |
| 6 |
8/8 |
9–10 |
0 |
267 ms |
| 5 |
8/8 |
7–8 |
0 |
156 ms |
| 5 (flood) |
6/6 |
7–8 |
0 |
156 ms |
| 8 (restore) |
3/3 |
11 |
1000 |
836 ms |
The links themselves are fine — no losses, healthy SNR. RadioLib allows SF5–12 on SX126x
(RADIOLIB_CHECK_RANGE(sf, 5, 12)), and our CLI already accepts sf >= 5 && sf <= 12.
Impact today: none by default
MyMesh::calcRxDelay (examples/simple_repeater/MyMesh.cpp:614) overrides the base Dispatcher and
short-circuits when rx_delay_base <= 0; MyMesh.cpp:1008 sets it to 0.0f by default
("turn off by default, was 10.0"). Measured: all six flood packets at SF5 logged
score delay below threshold (0) and were processed immediately.
It only bites if an operator sets rxdelay non-zero, where score 0 is the worst case:
(rxdelay^0.85 - 1) * airtime.
Why this is not fixed yet
Extending snr_threshold[] needs SF5/SF6 values we cannot currently source or measure:
- SX1261/2 datasheet Rev 1.2 Table 3-8 lists LoRa sensitivity for SF7 and SF12 only, no
SNR-limit column.
- Semtech AN1200.22 Rev 2 contains exactly one table (FSK link budget) — no SNR-per-SF table.
- Not measurable on our bench: at minimum commandable TX power the signal is still ~51 dB above the
noise floor, 22 dB of commanded power change yields only ~10 dB of RSSI change, and reported SNR
rises as power falls (receiver compression). ~58 dB short.
A calibrated step attenuator (0–60 dB, ideally conducted with a fixed pad) would make this
measurable, and the measured value would be better than a datasheet figure: the code compares
getLastSNR(), so the radio's own reported SNR at the failure point is the correct quantity.
Also noted
set radio replies OK - reboot to apply — persisted but not pushed live.
- Zero-hop adverts never reach the
calcRxDelay branch (isRouteFlood() is false).
- Neither wrapper checks the return of
setSpreadingFactor(), so on an SX127x board (SF6–12 only)
set radio ...,5,... would be accepted and stored while the radio silently keeps its old SF.
Scripts: bench_sf_test.py, bench_sf_dynrange.py.
Summary
SF5 and SF6 links work on our hardware, but
packetScoreInt()returns0.0fforsf < 7, so everypacket received at those spreading factors scores zero.
src/helpers/radiolib/RadioLibWrappers.cpp:266:The guard is necessary as written —
snr_threshold[]holds only six entries (SF7…SF12) and isindexed
sf - 7, so SF5/SF6 would read out of bounds without it.Measured on the bench (2026-08-10)
RAK4631 + Heltec V3, both SX1262, 869.618 MHz / 62.5 kHz / CR5,
MESH_DEBUG=1 MESH_PACKET_LOGGING=1:The links themselves are fine — no losses, healthy SNR. RadioLib allows SF5–12 on SX126x
(
RADIOLIB_CHECK_RANGE(sf, 5, 12)), and our CLI already acceptssf >= 5 && sf <= 12.Impact today: none by default
MyMesh::calcRxDelay(examples/simple_repeater/MyMesh.cpp:614) overrides the base Dispatcher andshort-circuits when
rx_delay_base <= 0;MyMesh.cpp:1008sets it to0.0fby default("turn off by default, was 10.0"). Measured: all six flood packets at SF5 logged
score delay below threshold (0)and were processed immediately.It only bites if an operator sets
rxdelaynon-zero, where score 0 is the worst case:(rxdelay^0.85 - 1) * airtime.Why this is not fixed yet
Extending
snr_threshold[]needs SF5/SF6 values we cannot currently source or measure:SNR-limit column.
noise floor, 22 dB of commanded power change yields only ~10 dB of RSSI change, and reported SNR
rises as power falls (receiver compression). ~58 dB short.
A calibrated step attenuator (0–60 dB, ideally conducted with a fixed pad) would make this
measurable, and the measured value would be better than a datasheet figure: the code compares
getLastSNR(), so the radio's own reported SNR at the failure point is the correct quantity.Also noted
set radiorepliesOK - reboot to apply— persisted but not pushed live.calcRxDelaybranch (isRouteFlood()is false).setSpreadingFactor(), so on an SX127x board (SF6–12 only)set radio ...,5,...would be accepted and stored while the radio silently keeps its old SF.Scripts:
bench_sf_test.py,bench_sf_dynrange.py.