Skip to content

SF5/SF6 links work but score 0 — packetScoreInt() bails below SF7, thresholds unsourceable #5

Description

@ACETyr

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:

if (sf < 7) return 0.0f;

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions