mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-31 20:40:53 -04:00
renovate/python-3.x
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e069a8d491 |
LR2021: time received frames on the configured coding rate (#12183)
* LR2021: time received frames on the configured coding rate The LR2021's reported RX coding rate can pick up extra bits at a bad status read right after our own TX and keep them until a hard reset. Explicit-header frames still decode, since the rate travels in the header, but the wrong value inflates RX airtime: a latched 4/8 against a configured 4/5 times a 200-byte frame about 55% long. That feeds channel utilisation, the CSMA contention window and the reliable-router retransmit timers. computePacketTime() gains useRxHeaderInfo; LR20x0Interface passes false, so received frames are timed on the configured coding rate and CRC, and the readout task no longer reads the header info on that chip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw * LR2021: describe the coding-rate accumulator in the comments The reported RX coding rate was described as latching after a post-TX status read. That attribution was wrong: GetLoraPacketStatus accumulates the bitwise OR of every received header's coding rate since reset, which an SX1262 injector reproduces on demand - frames at 4/5, 4/6 and 4/7 each OR their bit in, a frame at the configured rate changes nothing, and the field reaches values DS rev 2.2 Table 9-4 does not define. It is invisible on a single-coding-rate mesh, since cr | cr == cr. No behaviour change: comments only. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dc9999280f |
Don't let a stale RX flag re-arm RX over a live frame (#12153)
* Don't let a stale HEADER_ERR re-arm RX over a live frame
Before: a header that fails on noise, a collision or a foreign frame
latches HEADER_ERR and no RX_DONE follows it. `checkStaleRxFlags()`
counts HEADER_VALID and HEADER_ERR as one boolean, so the error starts
the max-packet window that re-arms RX. A good frame arriving inside that
window raises HEADER_VALID beside the error, which does not restart the
window - a header was already up. One max packet after the error was
first seen, the poll re-arms, and `startReceive()`'s standby aborts the
frame still on the air.
After: the window is timed from the valid header alone. A HEADER_ERR
beside a HEADER_VALID is retired at once and decides nothing, because one
header raises one flag or the other (SX1262 DS rev 2.1 table 13-29), so
the two together are two frames and the error is the older one.
A lone stale HEADER_ERR past its window is now cleared rather than
re-armed on every chip but SX128x. The re-arm exists for the SX1280 wedge
of DS rev 3.3 16.2, which a clear does not unwedge; on SX126x, LR11x0 and
LR20x0 the demodulator is already hunting for a preamble again, and a
clear never aborts a frame that may be in one. `headerErrWedgesRx()` is
the per-chip hook, true only on SX128x. SX126x already clears HEADER_ERR
unconditionally in `resumeRunningReceive()` for the same reason.
The busy decision is unaffected: `receiveDetected()` reads only
HEADER_VALID and PREAMBLE_DETECTED, so the cost of the bug was a dropped
reception, never a talk-over.
How: `staleRxFlagAction()` takes HEADER_VALID and HEADER_ERR separately
plus the per-chip wedge flag, and gains a ClearHeaderErr verdict.
`checkStaleRxFlags()` keeps a second stamp bit for "the window was
started by a valid header", so a valid header restarts a failed one's
window the way develop already has a header restart a bare preamble's.
Five native cases cover it, two of them the regression itself.
Found and analysed by Tom (NomDeTom), who also wrote the commit the
behaviour came from,
|
||
|
|
e9c046547d |
LoRa: size the preamble hold for the worst-case frame, and let the CAD settle an old bare preamble (#12125)
* LoRa: size the preamble hold for the frame that could be arriving The hold ran for getPacketTime(DATA_PAYLOAD_LEN + sizeof(PacketHeader)): 249 bytes at our own coding rate. A sender's header carries its own CR, up to 4/8, and that header is unreadable when the hold starts, so a legal 255-byte frame at CR 4/8 with CRC outlives the hold and we key up over it. That is the case the hold exists for, since LoRa we cannot decode - a foreign sync word - never produces HEADER_VALID at all. maxRxFrameMsec() times the 255-byte PHY frame at CR 4/8 with CRC from the configured modem, through the radio's own calculateTimeOnAir(), and falls back to getPacketTime(MAX_LORA_PAYLOAD_LEN) if the radio answers an error code (#11935). At SHORT_TURBO that is 159 ms rather than 100 ms, at LONG_FAST 3.3 s rather than 2.1 s. The HEADER_VALID ageing in receiveDetected() takes the same bound, for the same reason. checkStaleRxFlags() and checkCadHandoffTimeout() keep the shorter window: they decide a latched flag is dead and recover the chip, where waiting longer delays the recovery rather than protecting a frame. Three test cases in test/test_radio pin the sizing, the error fallback and that the hold outlives the flag clear that starts it. FakeIrqRadio now answers calculateTimeOnAir() and honours a clear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE * LoRa: let the pre-TX CAD settle an old bare-preamble hold A hold runs for one max frame whether or not anything is really arriving: 159 ms at SHORT_TURBO, but 2.1 s at LONG_FAST and 14 s at LONG_SLOW. For all of it the TX path only re-rolls its backoff - canSendImmediately() is false, so the pre-TX CAD never runs and nothing re-examines the channel. A noise preamble therefore costs seconds of TX and can overflow the Tx queue, which is what @GUVWAF objected to on #11968. A frame we could decode latches HEADER_VALID within the rest of the preamble plus the ~12.25 symbols of sync word, SFD and explicit header. Once that grace has passed with no header, the sighting is noise or LoRa we cannot read, and neither deserves the rest of the hold: preambleHoldPeekable() lets the TX path fall through to its CAD and send if the channel is clear. At LONG_FAST the grace is 248 ms against the old code's own 262 ms deadline, and 8 ms at SHORT_TURBO, so a spurious preamble now blocks TX for slightly less than develop did before the hold existed - and then actually listens. HEADER_VALID keeps the full hold: that is a real frame on our own modem. receiveDetected() notes a header even while the hold short-circuits, so a frame whose header arrives mid-hold is never peeked past, and a new hold forgets the previous one's header. Our own transmit ends the hold, since the CAD has just judged the channel. preambleHeaderGraceMsec() computes the grace from sf and bw rather than RadioInterface::preambleTimeMsec, which is whole milliseconds: above 2 GHz a 12-symbol preamble is 0.95 ms, so that value is 0 and the grace would be 0, making every sighting peekable the moment it was seen. Six more cases in test/test_radio: the grace at wide-LoRa and sub-GHz presets, its rounding, and the peek inside the grace, after it, behind a header and across a new hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE * LoRa: ask for the frame bound before reading back the CR it timed test_preambleHold_isSizedForTheWorstCaseFrame read phy.lastCodingRate straight after the first preamble sighting, where nothing has yet asked the chip to time anything: preambleHoldActive() short-circuits on the unset stamp, so the sighting that starts a hold never reaches calculateTimeOnAir() and the fake still reports a coding rate of 0. Ask maxRxFrameMsec() for the bound first, then assert the CR and CRC it was asked for. The case pins the same thing it always meant to - that the hold is timed at CR 4/8 with CRC rather than at our own CR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CStem6K6MQRBNR8YCFgtCE * LoRa: scale the preamble hold's airtime fallback to CR 4/8 When calculateTimeOnAir() cannot time the worst-case frame, maxRxFrameMsec() falls back to getPacketTime(), which times it at our own coding rate. A configured rate below 4/8 then produced a hold shorter than the frame the hold exists to cover. Scale that figure up to 4/8; the preamble does not scale with CR, so the result runs long, which is the safe direction for a hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013mdEBL5t2xdysijGG6ejQR --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1c366c2d05 |
Radio: end a CAD handoff whose RX hits a damaged header (#12144)
* Radio: end a CAD handoff whose RX hits a damaged header When CAD hands off to RX and that RX ends on a damaged header (CRC_ERR or HEADER_ERR with no RX_DONE), the RX readout task cleared those flags and returned. Nothing was notified and cadHandoffRxStart stayed set, so the radio thread never re-armed. Where the chip had stopped listening, the node stayed deaf until the main loop's backstop logged "CAD>RX timeout" and re-armed: 200 ms to over a second later. Before the readout task, the same edge went to handleReceiveInterrupt(), which read the error, cleared the flags and re-armed. The bench saw this on an LR2021 (X1) running develop: 5 "CAD>RX timeout" lines across 3 runs, where pre-#11983 develop logged none in 4 runs. Now the task hands every frameless end of a handoff's RX (TIMEOUT, CRC_ERR, HEADER_ERR) to the radio thread, which clears the flags through handleReceiveInterrupt() and re-arms, as it already did for TIMEOUT. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 * Radio: don't strand a damaged header's flags when its notification is dropped Three follow-ups to the handoff end, from review of the first commit. The readout task's notify(ISR_RX, !rxArmedBeforeTxDone) deliberately does not overwrite a pending TX notification, so the RX notification can be dropped. That is pre-existing for the TIMEOUT case, and the ISR_TX path does put the chip back into RX, but the first commit moved the clearing of CRC_ERR / HEADER_ERR to the radio thread, so in the dropped case nothing clears them until checkCadHandoffTimeout(). notify() returns false exactly when it dropped, so clear them at that site in that case, as the code did before, and leave the open handoff to the backstop. handleReceiveInterrupt() logged "CAD>RX pkt" for a frameless end, and read a length that is not a frame's. A damaged header now takes the same exit as an empty window, with its own log line, so a capture says which of the two happened and getPacketLength() is not asked about a packet that never arrived. rxBad is unchanged either way: neither case counted before. An empty window inside a handoff can carry a damaged header's flags alongside the timeout, both routed to DIO1, so that exit clears them together. Plain RX is untouched there and still leaves HEADER_ERR to checkStaleRxFlags(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
48d812536d |
LR20x0: empty the RX FIFO when a frame is dropped unread
The LR2021 receives into a FIFO that RadioLib empties only at the end of readData(). Every path that gives up on a frame without reading it (the readout task's bad length and full ring, the thread's early outs) cleared only the IRQ flags, so the dropped frame's bytes stayed at the front of the FIFO and the next frame was read out on top of them. On an X1, 37 of 37 forced drops were followed by a "Decryptable packet failed decoding", so each drop cost two frames. clearReadIrqs(), which every such path already calls, now also asks the driver to discard the frame. LR20x0 sends CLEAR_RX_FIFO (private in RadioLib); the other chips read from a buffer offset and need nothing. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 |
||
|
|
95c57c27b2 |
ch341 server fixes (#12016)
* LoRa LBT: fix SX128x/SX126x CAD encoding + slot model, ESP32 sleep race
Three fixes from the "are we really listening?" LBT audit:
- SX128x CAD: pass RADIOLIB_SX128X_CAD_ON_4_SYMB instead of the raw count 4.
The raw value set bits 7:5 = 000, i.e. a 1-symbol scan, leaving LBT nearly
blind on 2.4 GHz. Now runs the intended 4-symbol CAD.
- SX126x CAD / slot time: name the symNum value explicitly as CAD_ON_4_SYMB
(identical 0x02 raw value, so no on-air change) and add a virtual
getCadSymbolCount() so computeSlotTimeMsec() models the real ~4.5-symbol
scan for the SX126x/LLCC68/STM32WL family only, leaving LR11x0/SX127x at
their correct 2.5 symbols. Fixes the too-short contention-window slots.
- ESP32 light sleep: if the DIO1/RF95_IRQ line is already asserted just before
esp_light_sleep_start(), skip the sleep so the main loop services the pending
packet instead of stranding it for up to a full sleep chunk (the
detachInterrupt-clears-the-GPIO-wake disarm race).
clod helped too
* LoRa LBT: keep software-poll preamble flags latched while idle (anti-stomp)
Finding #10 from the "are we really listening?" audit, LORA_DIO1_SOFTWARE_POLL
boards (meshnology-w10).
The 1 ms poll cleared latched PREAMBLE_DETECTED|HEADER_VALID whenever they were
the only bits set, so for the rest of a reception isActivelyReceiving() read idle.
A TX queued during the payload phase then passed the send gate and startTransmit
overwrote the shared chip buffer, losing the inbound packet.
That clear only exists to stop a noise preamble from permanently blocking our own
TX (starvation). Starvation can only happen when we actually have something to
send, so gate the clear on a non-empty txQueue. While the queue is empty the bits
stay latched, isActivelyReceiving() reports the in-progress packet, and a later TX
defers instead of stomping it. A queued TX still gets the escape hatch. The poll
cadence is a fixed 1 ms reschedule either way, so latched bits don't spin the loop;
readData() clears everything once a real RX_DONE arrives.
clod helped
* LoRa LBT: optional RSSI energy second-gate before TX (default off)
Findings #4/#5 of the "are we really listening?" audit.
CAD only detects a preamble. A neighbour's packet whose preamble already aired -
during our own TX, a CAD scan, or a standby gap - is mid-payload and invisible to
CAD, so we key up on top of it. Add an RSSI energy check as a second listen-before-
talk gate in the TX-decision path: channelBusyByRSSI() compares the instantaneous
in-RX RSSI against the rolling noise floor (getNoiseFloor() + margin) and defers if
the channel is hot. It reads RSSI before isChannelActive()'s setStandby() via the
|| short-circuit, so the sample is taken while still in RX where GET_RSSI_INST is
valid - no mode juggling.
Orthogonal to CAD: CAD has processing gain and catches weak preambles below the
noise floor; RSSI catches strong energy at any packet phase, including the mid-
payload case CAD is blind to. Neither subsumes the other.
DEFAULT OFF. A mis-set threshold makes a node read "busy" on noise and stop
transmitting entirely, which is worse than the bug, so it must be tuned on hardware
before it ships live. Compiled out unless LORA_RSSI_LBT_MARGIN_DB is defined at
build time; when off it is a compile-time `return false` (zero behaviour change).
Fails open: returns not-busy unless in RX with a full noise-floor window and a sane
dBm reading, so it can never persistently silence a node.
Native build (gate off) links clean.
clod helped
* LoRa LBT: RSSI margin profiler to tune RSSI_LBT_MARGIN_DB (default off)
Instrumentation to pick the #4/#5 RSSI second-gate margin from measured data
instead of guessing. Build with -DLORA_RSSI_LBT_PROFILE; compiled out otherwise.
It accumulates (RSSI - rolling noise floor) in two channel states:
- idle (reuses updateNoiseFloor()'s existing RX sample, no extra SPI): the noise-
jitter distribution. Its high tail is the floor the margin must clear.
- signal (decoded-packet RSSI, hooked in handleReceiveInterrupt): the real-traffic
rise. Its low tail is the ceiling the margin must stay under.
A throttled log dump prints both distributions (min/mean/max + percentiles) and the
derived usable window (idle p99, signal p05).
Placement: the stats/histogram engine is a dependency-free, radio-agnostic header
(RssiProfiler.h) so it stays out of the core file and is testable on its own; the
two hooks and the RSSI/log I/O live on RadioLibInterface where the state is.
Independent of the gate flag (LORA_RSSI_LBT_MARGIN_DB) on purpose: profile first to
choose the margin, then build the gate on. Native build (profiler on) compiles clean.
clod helped
* docs: describe the RSSI LBT margin profiler
Feature doc for the RSSI listen-before-talk margin profiler
(-DLORA_RSSI_LBT_PROFILE): what it measures (idle noise-jitter vs decoded-signal
distributions of RSSI-minus-noise-floor), how it's wired (RssiProfiler.h engine +
RadioLibInterface hooks, idle sample reused from updateNoiseFloor), how to build and
run it, and how to read the log output to choose RSSI_LBT_MARGIN_DB.
clod helped
* LoRa LBT: draft #3/#6 fixes behind MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE (inactive)
Firmware side of the CAD->RX handoff (#3) and ISR_RX re-arm (#6) from the LBT
audit. Both need to re-arm RX WITHOUT the standby() that startReceive() forces
(which aborts an in-flight reception) - unavailable in stock RadioLib, provided by
resumeReceive() in NomDeTom/RadioLib@rxResume.
Guarded by MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE, left UNDEFINED: against stock
RadioLib these paths compile out and behaviour is unchanged. Define it (via
build_flags) only when linking a RadioLib that has resumeReceive().
- resumeReceiveInPlace(bool startRx): base-class helper - iface->resumeReceive()
(remap RX IRQ, optional SetRx, no standby) + re-attach the MCU ISR + base RX
bookkeeping. One place, chip-agnostic via iface.
- #3: SX126x/LR11x0 isChannelActive() CAD exitMode -> GOTO_RX; the TX-decision path
splits the busy check into rssi/cad flags and, on a CAD detect, hands the packet
to RX via resumeReceiveInPlace(false) instead of startReceive(). Macro-off, the
split is exactly the old `channelBusyByRSSI() || isChannelActive()` short-circuit.
- #6: ISR_RX re-arms via resumeReceiveInPlace(true).
Open (flagged in-code, for review + HW validation): whether re-issuing SetRx (#6)
disturbs an on-chip in-flight reception, and the #3 handoff's interaction with a
beacon-target packet on the home config.
clod helped
* LoRa LBT #4: synthesize GPIO wake cause when light sleep is skipped
doLightSleep() skips esp_light_sleep_start() when a LoRa IRQ is already
asserted, but still read esp_sleep_get_wakeup_cause() unconditionally - so
the skip path returned the previous real sleep's cause (e.g. TIMER) and
missed the ESP_SLEEP_WAKEUP_GPIO branch that sets runASAP. Capture whether we
skipped and return a synthetic GPIO cause in that case so PowerFSM takes the
packet-service path. Reads the ESP-IDF cause only after an actual sleep.
clod helped too
* LoRa LBT #10: keep HEADER_VALID latched in the software IRQ poll
The LORA_DIO1_SOFTWARE_POLL anti-starvation escape hatch cleared both
PREAMBLE_DETECTED and HEADER_VALID whenever a TX was queued. HEADER_VALID is
evidence of a real inbound frame, so clearing it made isActivelyReceiving()
read idle mid-payload and let the queued TX stomp the frame. Gate the hatch on
a bare-preamble IRQ (PREAMBLE set, HEADER_VALID clear) and clear only
PREAMBLE_DETECTED. A stuck header still can't block TX forever - receiveDetected()
ages it out after maxPacketTimeMsec - and readData() clears everything on RX_DONE.
clod helped too
* LoRa LBT #3/#6: CAD->RX handoff + continuous-RX re-arm on stock RadioLib
Reimplements the two audit fixes that previously needed the NomDeTom/RadioLib
@rxResume fork (MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE), using only stock
RadioLib, behind a new default-off flag MESHTASTIC_LBT_CAD_TO_RX. Per the
RadioLib maintainer's feedback, no new RX API is needed.
(GOTO_RX) with the RX IRQs pre-mapped in the scan config, so the chip's own
CAD->RX transition delivers RX_DONE with no library call in between. irqFlags
(status-enable) carries the CAD verdict + full RX set incl. PREAMBLE/HEADER so
isActivelyReceiving() can read them; irqMask (DIO trigger) carries the CAD
verdict + terminal RX events only - NOT PREAMBLE/HEADER, which would fire the
ISR mid-frame and run readData() on an incomplete packet. On detection it hands
off in place: clear the latched CAD verdict, re-attach the MCU ISR, mark
receiving - no standby (which would abort the packet just detected).
standby), so re-attach the MCU ISR instead of re-issuing SetRx (which could
disturb a second packet already arriving on-chip).
Removes resumeReceiveInPlace() and the fork macro. SX126x-only: LR11x0's
startChannelScan conflates irqFlags/irqMask (drives the DIO from irqFlags), so
it keeps the safe GOTO_STDBY path, re-arming on detect to stay coherent with
onNotify when the flag is on.
Default-off: needs on-hardware validation and assumes continuous RX (the
default preamble=16/minSymbols=8 degenerates startReceiveDutyCycleAuto to
continuous). Native builds pass both flag-off and flag-on.
clod helped too
* LoRa LBT #1: re-arm RX unconditionally when deferring a busy channel
The post-CAD/RSSI defer path wrapped its startReceive() re-arm in a
!hasTargetRadioSettings(txp) guard, so a beacon-target packet that hit a busy
channel took setTransmitDelay() with the radio left in standby. When the
beacon's target config coincides with the home config the radio is on the home
channel, so the node went deaf on home traffic until the next TX cycle - and
kept skipping the re-arm every cycle the channel stayed busy. Drop the guard:
re-arm unconditionally and keep listening while we wait to key up. A beacon's
job is to get a packet out, but it should still mirror the main RX path.
(No effect under MESHTASTIC_LBT_CAD_TO_RX, where the chip is already left in RX.)
clod helped too
* docs: fix RSSI profiler fences (MD040) + RAM figure, note negative-bin limit
- Tag the formula and sample-output fenced blocks as `text` so markdownlint
MD040 passes.
- Correct the RAM figure: RssiDeltaStats is 148 bytes (12 scalar + 34*4
histogram), so two instances are ~296 bytes, not ~280.
- Add a limitations note that all negative deltas collapse into bin[0], so a
percentile landing there returns the minimum sample. This does not affect the
margin (keyed off the always-positive idle p99) but can understate signal p05
when a node decodes below-floor packets; never biases toward a smaller margin.
clod helped too
* docs: RSSI profiler - record histogram bucketing design choices
Add a "Histogram bucketing" section laying out why a fixed histogram is used
over exact percentiles, what the axis has to resolve (the margin window sits at
roughly -a few..+20 dB), and the arguments for five bucketing schemes: the
current positive-only layout (A), a cost-neutral re-centred layout with
negative-range resolution (B, recommended if signal p05 must be trusted),
wider full-range bins (C), more bins (D), and exact/off-box (E). Explains the
current choice and when to switch.
clod helped too
* LoRa LBT: move RSSI profiler + energy second-gate to listening-now-rssi
Split the RSSI work off this branch so the active CAD/sleep fixes and the #3/#6
CAD->RX rework can land without the tuning-dependent RSSI pieces. Removed here
(preserved intact on branch listening-now-rssi):
- RSSI margin profiler: RssiProfiler.h, the LORA_RSSI_LBT_PROFILE hooks, and
docs/rssi_lbt_margin_profiler.md.
- RSSI energy second-gate: channelBusyByRSSI() and the LORA_RSSI_LBT_MARGIN_DB
gate.
The pre-existing noise-floor sampler (updateNoiseFloor/getNoiseFloor, already on
develop and consumed by telemetry) is untouched. The TX-decision block reverts
to a plain CAD scan; #3/#6 (behind MESHTASTIC_LBT_CAD_TO_RX) and #1 unchanged.
Native build green.
clod helped too
* reduce to bare essentials
* LoRa LBT: calibrate CAD symbol count and detPeak against vendor data
NUM_SYM_CAD carried two incompatible meanings: computeSlotTimeMsec() read it
as a literal symbol count, while the drivers passed it into scanChannel(),
where symNum is a register encoding on SX126x/SX128x. On SX126x the raw 2
encodes CAD_ON_4_SYMB, so the chip has scanned 4 symbols since #5749/#5772
while the slot time was priced at 2.5 - a 1.8x mis-scaling of the CSMA
backoff unit, live since January 2025.
Give the constant one meaning (a literal count of 4) and have each driver
translate at its own call site. getCadSymbolCount() reports the real scan
length so slotTimeMsec matches it; RF95 overrides it because SX127x CAD is
fixed in hardware and its legacy scanChannel() takes no symNum.
AN1200.48 Table 1 recommends 4 symbols for SF9-SF12, which is where most
shipped presets sit, so 4 is the right target. The comment claiming 2 was
reading that note's SF7/SF8 row.
detPeak is calibrated per (SF, symbol count), and on LR11x0 per bandwidth
as well. Both LR drivers ran thresholds meant for a 2-symbol scan, leaving
them less sensitive than measured-best and biased toward missed detections.
Now sourced explicitly:
LR20x0 LR20xx datasheet rev 2.2 Table 6-19, indexed by symbols x SF
LR11x0 Semtech SWSD003 lr11xx/apps/cad/main_cad.c optimized_parameters,
indexed by symbols x bandwidth x SF
SX126x RadioLib's SF+13, which sits at or below every AN1200.48 value and
so errs toward deferring rather than transmitting
Combinations the vendor data does not characterise fall back to the RadioLib
default rather than guessing. LR2021 also pins fastCad, which scanChannel()
reads off the radio object rather than the config struct.
Also fix the 2.4 GHz slot term: (2 * sf + 3) / 32 is integer division that
truncates to 0 for every legal SF, so the AN1200.22 fractional term never
contributed anything.
Not hardware-verified.
* LoRa LBT: drop audit-doc pointers and cross-chip asides from comments
The LBT comments referenced an external audit document by item number (#1,
#3, #3/#6, #10, and a see-.notes/lbt pointer), which means nothing to anyone
reading the tree on its own. They also described what sibling drivers do,
which goes stale silently the moment one of them changes and belongs in that
driver's own file anyway.
Keep the reasoning, drop the pointer and the comparison. Where a comment
explained a rule that genuinely spans drivers - NUM_SYM_CAD's register-
encoding caveat - state it as a rule rather than a roll-call of chips.
No behaviour change; comments only.
* LoRa LBT: don't index the LR11x0 CAD table with a 2.4 GHz bandwidth
The detPeak lookup picked its bandwidth row by range, so wideLora's 406.25 kHz
fell into the sub-GHz 500 kHz row - SWSD003's table is the sub-GHz LoRa modem
(BW enums 62.5-500), and 2.4 GHz is a separate modem with its own calibration.
Match the four bandwidths exactly instead, so anything unmatched falls back to
the RadioLib default rather than borrowing a row it has no claim to.
Those four are the whole sub-GHz set today - LR11x0::setBandwidth() rejects
anything else - so note that a newly unlocked bandwidth, or a symbol count
outside 2/4/8/16, needs a row adding here and fails silently if it doesn't get
one. Same note on LR20x0 for symbol counts above 4.
Also correct two citations. The 2.4 GHz slot term is the SX1280 datasheet's own
CAD duration formula, and the CAD symbol-count guidance is AN1200.77, not
AN1200.22 - which is "LoRa Modulation Basics" from 2015 and predates the part.
Record that SX1280 ignores detPeak/detMin/exitMode entirely: SetCadParams
carries only cadSymbolNum, and the PNR threshold lives in register 0x942.
* LoRa LBT: set the SX1280 CAD PNR threshold and scan 8 symbols
SetCadParams on this part carries only cadSymbolNum - detPeak/detMin in the
scan config are inert - so CAD sensitivity is governed entirely by register
0x942, which nothing was writing. The scan therefore ran at the chip's reset
value of 0x32, against the 19-36 that AN1200.77 Table 1 calibrates per SF and
bandwidth: at LONG_FAST wideLora the recommendation is 27 and we were using 50.
A threshold that high needs a much stronger correlation peak, so 2.4 GHz LBT
was under-detecting on top of the symbol-count bug already fixed.
Write the table value before each scan, indexed by bandwidth x SF. Unmatched
bandwidths write nothing and leave the chip's default, as on the LR parts.
This goes through our own Module rather than lora.writeRegister(), which
RadioLib keeps protected unless RADIOLIB_LOW_LEVEL unlocks raw register access
across every driver. SX128x::writeRegister() is a straight SPIwriteRegisterBurst
on the same Module we already own, and begin() has pointed spiConfig at the
0x18 WriteRegister opcode long before any scan runs, so the transaction is
identical - no subclass needed.
Also raise the CAD window from 4 to 8 symbols, matching RadioLib's own SX128x
default. AN1200.77 measures a longer window as moving P(Detection) further from
P(False Detection); its thresholds are stated to hold for any window duration,
so the table above is unaffected. NUM_SYM_CAD_24GHZ moves in step so the slot
time still reflects the real scan.
Not hardware-verified.
* LoRa LBT: hand CAD straight to RX on the LR parts
CAD exited to standby, and the busy-channel path then issued a full
startReceive() - which standbys the radio and re-enters RX from scratch,
throwing away the reception CAD had just detected. Both LR families expose a
CAD-to-RX exit mode that avoids the round trip, and it reaches the chip through
the public startChannelScan(cfg), so nothing here needs a library patch.
A note in an earlier commit claimed this was impossible because RadioLib
collapses irqFlags and irqMask into one set. That reasoning is SX126x's, where
irqMask is a status gate and an event omitted from it never latches at all.
The LR parts have no such gate: SetDioIrqParams takes a mask per interrupt pin
and SetDioIrqConfig maps IRQs to one DIO, while getIrqStatus() reads the status
bytes returned on every SPI transaction and consults no pin mapping. So the CAD
IRQ set now carries the verdict plus terminal RX events only - preamble and
header stay off the pin, where they would fire the ISR mid-frame and run
readData() on a partial packet - and isActivelyReceiving() still sees them.
rearmReceive() cannot be unconditionally standby-free the way SX126x's is.
cad_timeout 0 means "RX until one packet arrives, then standby", so the two
re-arm paths leave the chip in different states: already listening after the
handoff, back in standby after that packet's RX_DONE. It branches on a flag set
at detection and cleared on use, so the first re-arm skips the standby and the
next restores continuous RX. Forcing continuous RX through the timeout instead
is not reliable - RadioLib converts it to RTC ticks through a float divide, so
the 0xFFFFFF "no timeout" encoding is not addressable.
On a CAD false positive the chip now waits in single-shot RX rather than
returning to standby. Still listening, but a state the old code never entered.
Not hardware-verified.
* LoRa LBT: size the 2.4 GHz CAD slot per chip, not per band
computeSlotTimeMsec() branches on myRegion->wideLora, which is a region rather
than a part. LR1120 and LR2021 both run there, and both scan NUM_SYM_CAD
symbols - but that branch read the flat NUM_SYM_CAD_24GHZ, so raising it to 8
for SX128x moved the slot for two families whose scan never changed. At
LONG_FAST wideLora that priced ~22 ms of CAD against a ~10 ms scan.
getCadSymbolCount() exists precisely so the slot tracks the real scan, but only
the sub-GHz branch consulted it. Use it in both, and give SX128x an override
returning NUM_SYM_CAD_24GHZ. That constant stops being a fleet-wide 2.4 GHz
value and becomes what one driver reports; SX128x is unchanged at 8.78 symbols,
while the LR parts on 2.4 GHz drop from a wrong 8.78 to 4.78.
The error was conservative - slots too long, so throughput lost rather than
collisions caused - but it is the same scan/slot mismatch the rest of this work
removes.
* LoRa LBT: declare CAD symbols per chip and per band, 8 on 2.4 GHz
A 2.4 GHz region is shared: an LR1120 and an SX1280 can sit on the same mesh.
Leaving the LR parts at 4 symbols there while SX1280 scanned 8 gave them
different slot times, and so a different CSMA backoff scale between neighbours
- the cross-family asymmetry this branch removed for sub-GHz, reintroduced one
band up. Both LR families now scan 8 on 2.4 GHz, matching SX1280.
Rather than switch on the band inside a shared constant, each driver states its
own count for each band it works in, and derives the symNum it passes from the
same accessor - so the scan and the slot cannot drift apart:
SX126x 4 sub-GHz
SX128x 8 on 2.4 GHz
LR11x0 4 sub-GHz, 8 on 2.4 GHz
LR20x0 4 sub-GHz, 8 on 2.4 GHz
RF95 2 sub-GHz, its CAD being fixed in hardware and not settable
NUM_SYM_CAD and NUM_SYM_CAD_24GHZ become base-class defaults nothing falls
through to; they now record the fleet convention rather than any part's
behaviour.
LR20xx Table 6-19 stops at 4 symbols, so an 8-symbol scan reuses that row -
the last measured point, erring high and so slightly less sensitive than a
true 8-symbol value. Noted at the table.
Also a comment pass over the branch: drop retrospective phrasing that describes
what the code used to do, cut trailing justification clauses, and fix comments
the per-band change left stale - one still claimed the LR parts scan fewer
symbols than SX1280 on 2.4 GHz, which is now the opposite of the truth.
* LoRa LBT: sweep up a CAD->RX handoff that delivers nothing
A handoff leaves the chip listening by its own exit mode, so nothing in the
driver arms the radio and no return value says whether it worked. If the
detection was false, or the exit mode was not honoured, the node waits in a
state we never verified - and on the LR parts that is single-shot RX, which
does not return to continuous listening on its own.
Stamp the handoff and let the existing ~1s poll re-arm if a whole max-length
airtime passes with no packet, the same bound receiveDetected() uses. Clearing
it on a real RX_DONE is obvious; clearing it in startSend() is not, but a TX
can intervene before any packet arrives and its completion re-arms RX anyway,
so leaving the marker set would have the poll re-arm a radio already listening.
Reads the clock through Time::getMillis(), the same source Throttle compares
against, so the site stays testable across the millis() wrap. 0 is the
"none outstanding" sentinel and is tested before the elapsed compare.
Also document on rearmReceive() that an override must never call the chip's own
startReceive() - its standby+SetRx aborts the packet CAD just detected, which
is the entire reason the override exists.
* define txen pin to allow sx128x testing
* LoRa LBT: bound the CAD->RX handoff in hardware, and re-arm after it delivers
cadTimeout only applies to the CAD->RX exit mode. Every driver on develop
exited CAD to standby, so the field was ignored and its value - 0, which no
datasheet defines - was dead data. Switching the exit mode to RX made it
load-bearing while the line itself never changed, so the diff showed nothing.
All three handoff parts now bound that RX at one max-length airtime, taken
from getPacketTime() so it tracks the preset. A detection that delivers
nothing expires to standby and raises TIMEOUT, already mapped to the pin, so
the existing ISR re-arms and the queued TX retries on its own backoff.
handleReceiveInterrupt() returns early on TIMEOUT without RX_DONE; otherwise
each dud detection would reach readData() and be logged as a bad packet.
That RX is single-shot, so the arm after it delivers must be a full one.
SX126x re-armed unconditionally without a standby, which left the chip in
standby while the interface believed it was listening - and nothing swept it,
since RX_DONE has already cleared the handoff stamp. A node that received a
handoff packet then went deaf until its next transmit. It now carries the
same cadHandedToRx flag the LR drivers already had.
Sourcing, since the parts disagree and RadioLib's uniform ChannelScanConfig_t
hides it: SX126x DS_SX1261-2 p98 (15.625us steps), LR11x0 UM Table 8-8
(31.25us), LR2021 lr20xx_driver cad_timeout_in_pll_step (31.25us, contra the
datasheet's "32 MHz periods"). RadioLib scales both LR parts as 30.52us, so
the programmed value lands ~2% long - harmless, and left for upstream.
Also marks the three SWSD003 cells at 2 symbols/BW500/SF10-12 as that SDK's
fallback rather than measurements.
* LoRa LBT: own the CAD->RX handoff in one place, and void it on standby
The handoff state was split across two classes: cadHandoffRxStart in
RadioLibInterface, cadHandedToRx in each of SX126x/LR11x0/LR20x0, whose
rearmReceive() overrides were byte-identical. Neither field was cleared by
setStandby(), so a standby taken while a handoff was outstanding could strand
cadHandedToRx set - and the next rearmReceive() would then re-attach the ISR to
a chip sitting in standby, marking the interface as receiving with no RX_DONE
ever to come and no recovery path. A stale cadHandoffRxStart was milder but
still re-armed the radio over whatever was arriving at the time.
Hold both in the base, arm them together in noteCadHandoffToRx(), clear both in
setStandby(), and collapse the three overrides into one non-virtual base
rearmReceive(). SX128x and RF95 never arm the flag, so they keep taking the
plain startReceive() branch. Document on isChannelActive() that a true return
must be followed by rearmReceive() - the invariant the whole scheme rests on.
Two fixes fall out of the shape:
- The handoff re-arm attaches a rising-edge ISR to a line that is not known to
be low, unlike startReceive() which clears the IRQ status first. Follow it
with checkRxDoneIrqFlag(), as startReceive() already does.
- checkCadHandoffTimeout() used one max-length airtime, the same figure the
chip's own cadTimeout now uses, so the two expired together. Losing that race
left a queued ISR_RX to run against flags the software path had just cleared,
falling past the TIMEOUT guard into readData() on an empty buffer. Double it
so the hardware always expires first; the backstop then only covers an RX
whose timer stopped on a header that never completed.
* LoRa LBT: re-arm RX when a queued packet is dropped for tx_enabled=false
Pre-existing on develop, not introduced by this branch, but it is the same
failure this series exists to remove. TRANSMIT_DELAY_COMPLETED reaches
startSend() only through isChannelActive(), which leaves the chip in standby
for the transmit about to follow. When that transmit is then dropped because
LoRa TX is disabled, the early return frees the packet and returns without
arming anything: isReceiving stays false, so pollMissedIrqs() skips the radio
too, and the node is deaf until something else re-arms it.
Narrow - tx_enabled has to change between enqueue and dequeue, since send()
checks it as well - but the fix is one call on a path that already exists.
* LoRa LBT: say what the CAD slot time actually does per chip
Two comments in RadioInterface.h described intentions the code does not carry
out.
NUM_SYM_CAD/NUM_SYM_CAD_24GHZ claimed the per-band values keep the CW backoff
scale equal across a mesh. They do not: the slot has to match the scan each part
really runs, and RF95's CAD is fixed in hardware at 2.25 symbols while
SX126x/LR scan 4. A mixed sub-GHz mesh therefore runs two slot lengths, which
is correct per part and worth stating rather than implying otherwise.
The slotTimeMsec initializer runs during RadioInterface's own construction, so
the virtual getCadSymbolCount() it calls always resolves to the fallbacks here,
never a driver override. That is harmless only because applyModemConfig()
recomputes it before anything reads it - left as-is deliberately, since
initializing to 0 would make random(0, 2^CW) * slotTimeMsec fire an immediate
transmit if anything ever did read it early.
* LoRa LBT: set the SX128x CAD threshold at config time, not per scan
isChannelActive() looked up the AN1200.77 PNR row and wrote register 0x0942 on
every scan, but sf and bw only change in applyModemConfig(). That is an SPI
round trip between the channel scan and the transmit that follows it, on the
one path this file's own comments ask to keep short.
Move it to applyCadDetPeak(), called from reconfigure() after the sf/bw setters
it depends on, and from init() as well - init() does not route through
reconfigure(), so it needs its own call.
* ESP32: stop skipping light sleep forever on a stuck LoRa IRQ
The skip added earlier in this series returns instead of sleeping whenever
DIO1 already reads high, so the main loop can service the packet rather than
strand it for the sleep chunk. It had no bound: a line that latches high with
nothing clearing it makes every subsequent call skip too, and the node quietly
stops sleeping at all, announced only by a LOG_INFO each time.
Count consecutive skips - warn at 3, sleep through at 5, reset on any real
sleep. If the IRQ has survived five attempts the main loop is not going to
clear it, and never sleeping costs more than one stranded packet.
Also drop the LORA_DIO1_EXTENDED_IO branch in loraWakeIrqAsserted(): those
boards return from doLightSleep() before it is ever called.
* nrf52 promicro: gate the P1.06 TXEN pin, and add an optional SX1280 build
P1.06 was wired to SX126X_TXEN unconditionally earlier in this series, to drive
an external PA on a bench unit. That is not safe to ship: three envs share this
variant.h, and P1.06 is PIN_WIRE_SCL on the EasyProMicro pinout and
PIN_EINK_BUSY on the InkHUD one. The default builds go back to RADIOLIB_NC.
Put it behind -D PROMICRO_SX126X_TXEN, refused wherever the pin is already
claimed. The EasyProMicro arrangement has its own SX126X_TXEN on P1.00 in the
#else branch and never reaches this, so only the e-ink exclusion is needed.
The pin was wanted for SX1280 in the first place - every other SX128X line is
already assigned on this board and TXEN was the one missing - so add that too,
behind -D PROMICRO_SX1280 and a new nrf52_promicro_diy-sx1280 env. Both opt-ins
claim P1.06, so asking for both is an #error rather than a silent pick.
Costs about 10.7 KB: the image ends at 0xE5140, 19 KB clear of the warm region,
against 0xE2710 and 30 KB for the base env.
* LoRa LBT: fix two defects in the CAD->RX handoff found in review
Both reported by Copilot against 1fcd219af, both confirmed against the code as
it stands after the handoff was hoisted into RadioLibInterface.
rearmReceive() armed the MCU ISR before marking the interface as receiving. An
ISR firing in that window queues ISR_RX while isReceiving is still false, so
handleReceiveInterrupt() bails with "called while not in rx mode" and the packet
is dropped - the exact packet the CAD handoff existed to catch. Reversed, which
also makes this path match the order the drivers' own startReceive() has always
used: base state, then arm, then checkRxDoneIrqFlag().
noteCadHandoffToRx() stored Time::getMillis() raw, but 0 is the "no handoff
outstanding" sentinel that checkCadHandoffTimeout() tests before its elapsed
comparison. A handoff armed on the one millisecond per wrap where getMillis()
returns 0 reads as "none outstanding" and the backstop never fires. Step past
it to 1, the same hand-remap Throttle.h's own TODO records GPS.cpp using for
fixHoldEnds. One millisecond of error, once per 49.7 days.
* ESP32: wake on SX128X_DIO1, which five SX1280 boards define instead of LORA_DIO1
enableLoraInterrupt() only ever looked at LORA_DIO1 and RF95_IRQ. RadioInterface
builds an SX1280Interface from SX128X_DIO1, and several variants name the IRQ
pin only that way, or put it on a different pin from LORA_DIO1:
tlora_v2_1_18 LORA_DIO1 undefined SX128X_DIO1 26
betafpv_2400_tx_micro LORA_DIO1 undefined SX128X_DIO1 4
t-eth-elite LORA_DIO1 16 SX128X_DIO1 8
tlora_t3s3_v1 LORA_DIO1 33 // TCXO_EN? SX128X_DIO1 9
tlora_t3s3_epaper LORA_DIO1 33 // TCXO_EN? SX128X_DIO1 9
Every branch of the function is gated on defined(LORA_DIO1), so on the first two
it preprocessed to an empty function: esp_sleep_enable_gpio_wakeup() still
enabled GPIO as a wake class, but no pin was ever armed for the radio. On the
other three it armed the wrong pin - GPIO33 there being the line the variant's
own comment questions as TCXO_EN. Those boards could not be woken from light
sleep by an incoming packet; they waited out the 30s chunk and pollMissedIrqs()
recovered the latched RX_DONE about a second after waking. Delayed, not lost,
but deaf for the duration.
loraWakeIrqAsserted(), added earlier in this series, inherited the same blind
spot and was dead code on the two boards with no LORA_DIO1 at all - so the
skip-the-sleep fix never once fired on an SX1280.
Resolve the pin per radio in loraWakeDio1Pin() and use it for arming, polling
and teardown alike, so the three cannot drift apart again, and widen their
guards to accept either macro. Behaviour is unchanged wherever it already
worked: for any non-SX1280 radio the helper returns LORA_DIO1, including on
RF95 boards where the SOC_PM_SUPPORT_EXT_WAKEUP branch arms it regardless of
radio type. LORA_DIO1_EXTENDED_IO still short-circuits first.
Note this is a power-profile change for those five boards, not only a fix: they
gain a LoRa wake source they have never had, so on a busy channel their light
sleep will now be cut short by traffic like every other board's.
* LoRa LBT: trim three-line comments to the repo's two-line limit
copilot-instructions.md:340 says one or two lines; the P1.06 availability note
and the LR2021 DIO routing note were both three. CodeRabbit flagged both.
* LoRa LBT: trace the CAD scan, its exits and the handoff outcome
Eight short lines, all in the base class so they cost one set of strings
rather than one per chip driver. The scan logs arm/busy/free; the in-chip
GOTO_RX handoff logs started, then exactly one of pkt, empty, timeout or
void - so a dropped or double-counted handoff is visible by inspection.
void and timeout are LOG_WARN because both mean an invariant broke: a
standby discarded a live handoff, or the chip's own timer failed to fire
and the software backstop had to re-arm. The rest are LOG_DEBUG, so a node
at normal log level stays quiet unless something is actually wrong.
The two pre-existing handoff strings were 44 and 41 bytes of flash; cut to
"CAD>RX timeout" and "CAD>RX empty", they pay for most of the new set. The
shared "CAD" prefix does not dedup - only string suffixes merge - and
splitting it out behind a %s measured 8 bytes worse on Cortex-M once the
extra pointer argument per call site is counted.
Also fixes the delivered-packet trace, which sat above the TIMEOUT check
and so claimed a packet on a window that expired empty.
* LoRa LBT: restore the SX1280 CAD threshold after a chip-state recovery
The calibrated AN1200.77 PNR threshold moved from a per-scan register write to
a config-time one, so it now has to be re-applied wherever the chip's state is
rebuilt. recoverChipStateLoss() hardware-resets the chip and reprograms only
the modem params, which left a recovered SX1280 doing CAD against the vendor
default threshold until the next reconfigure() - and on a node whose config
never changes, that is the rest of its uptime.
* LoRa LBT: retire RX flags that plain RX never completes
Plain RX runs with no chip timeout (startReceiveDutyCycleAuto(16, 8)
falls back to RX_TIMEOUT_INF) and the DIO mask carries RX_DONE only, so
a PREAMBLE_DETECTED, HEADER_VALID or HEADER_ERR that never completes
stays latched until something re-arms RX. On a node with nothing to
send, nothing does. The CAD->RX handoff already has this recovery
(cadTimeout plus checkCadHandoffTimeout()); plain RX had none.
checkStaleRxFlags() is its twin on the ~1 s pollMissedIrqs() tick. It
notes when flags are first seen, and if no RX_DONE has consumed them one
max-length packet later:
- a header (valid or errored) re-arms RX. RX_DONE clears every flag in
readData(), so a header still latched has no frame behind it. SX1280
DS rev 3.3 section 16.2: a header error leaves RX wedged with no
RX_DONE or timeout, and only a re-arm recovers it.
- a bare preamble is only cleared. ClearIrqStatus/ClearIrq changes the
IRQ register alone on SX126x, SX1280, LR11x0 and LR20x0, so it never
aborts a reception; startReceive() goes through standby and would.
HEADER_VALID is never cleared on its own: RadioLib's readData() reads
it at RX_DONE, and on LR2021 its absence reports the packet as header
damaged.
Skipped while a CAD handoff is outstanding (its own timeout owns that
RX) and while RX_DONE is pending. RF95 opts out: its carrier sense reads
modem status rather than latched flags, and SX127x maps neither
PREAMBLE_DETECTED nor HEADER_ERR.
The decision is staleRxFlagAction() in RadioInterface.h, pinned in
test_radio at both sides of the window and for each action.
* LoRa LBT: hold TX on a seen preamble instead of judging it by a deadline
receiveDetected() declared a bare PREAMBLE_DETECTED false once
2 * preambleTimeMsec passed without HEADER_VALID - about 8 ms at
SF7/BW500. Nothing reads the flags on that cadence: they are latched
status bits, never routed to DIO, and the only reader is the TX path,
once per CSMA backoff (0-56 ms at SF7/BW500). The verdict was usually
reached by a read older than its own deadline, and the queued TX went
out over the frame. Reported in #11933 by @jp-bennett, whose two-node
bench measured own TX a median 43 ms after the verdict, inside a ~102 ms
frame. This branch's longer CAD slot widens it to LONG_FAST: 0-308 ms of
backoff at CWmin against a 262 ms deadline.
A read that sees a bare preamble now clears PREAMBLE_DETECTED and holds
TX for one max-length airtime. The clear touches only the IRQ register
(ClearIrqStatus/ClearIrq on SX126x, SX1280, LR11x0, LR20x0), so it never
aborts a reception. Reads during the hold report busy, so the normal
random backoff still spreads retries, and any RX interrupt ends the hold
early. A preamble that fires again during the hold stays latched, and
the first read after it holds again: a source that keeps triggering keeps
deferring TX, one airtime at a time, while a single stale flag cannot
block it. HEADER_VALID is never cleared here - RadioLib's readData()
reads it at RX_DONE, and on LR2021 its absence reports the packet as
header-damaged - and keeps its one-airtime ageing.
The SX126x software IRQ poll routes its bare-preamble case through the
same hold instead of clearing on every tick while a TX is queued, which
would have released TX before the next read could see a refire.
That poll read the private txQueue directly, so meshnology_w10 - the
only env that defines LORA_DIO1_SOFTWARE_POLL - did not compile.
hasQueuedTx() exposes the one fact it needs.
#11937 addresses the same defect by deferring the verdict when the
previous read was stale; the two conflict in receiveDetected().
* LoRa: re-arm RX before logging the packet that just went out
After TX_DONE the chip sits in standby, deaf, until startReceive(). The ISR_TX
path used to log airtime ("Packet TX") and printPacket() the sent packet
first, which kept it deaf for ~2-5 ms more depending on where the log goes.
Measured on an nRF52 SX1262 bench node: the TX_DONE-to-RX gap drops from
~8.8 ms to ~3.7 ms with USB serial logging (~11.3 to ~6.2 ms with the log
also going to a phone over BLE).
completeSending() is split into detachSentPacket(), which clears sendingPacket
and runs packetReleased() so RX re-arms on the home config, and
finishSentPacket(), which does the airtime, counters, log and pool release.
setStandby() callers keep the combined completeSending().
* SX126x: keep the TCXO powered in standby
RadioLib leaves standbyXOSC off, so standby and the post-TX/RX fallback both
use STDBY_RC, which powers a DIO3-controlled TCXO down. Every SetRx and SetTx
after that first waits out the TCXO start-up delay programmed into the chip
(RadioLib's setTCXO() default, 5000 us). On an nRF52 SX1262 bench node the RX
start after TX_DONE took ~6.0 ms at SF7, SF9 and SF11 alike; with
setStandbyXOSC(true) it takes ~0.95 ms, and the TX_DONE-to-RX gap falls to
~1.1 ms together with the previous commit. The result held across the
periodic AGC reset.
Only boards that drive a TCXO from DIO3 change. The cost is the TCXO and XOSC
current while the chip sits in standby, which the firmware keeps brief;
duty-cycled RX still sleeps, and powers the TCXO down, between windows.
* LoRa: skip the periodic AGC reset while a packet is queued
resetAGC() ends in startReceive(), and trySetStandby() first runs
checkNotification() to drain pending events. With a packet queued that can be
TRANSMIT_DELAY_COMPLETED: CAD passes, startSend() keys up, and trySetStandby()
then puts the chip in standby over the transmission it just started.
completeSending() still reports the packet as sent and counts its airtime.
A bench node logged this twice in 20 minutes; one was a packet's last retry,
so it never went on air.
Skip the reset for this tick when a TX is queued. The queue drains within
seconds, and the reset comes round again on the next interval.
* GPS: re-enable on a LoRa config save only when the region was unset
#10386 keeps the GPS off until a region is set and turns it on when a LoRa
config save sets one. The check never looked at the old region, so a GPS the
probe had disabled ("GPS not detected; not present this boot") was
re-enabled by every LoRa config save, preset changes included, and the probe
ran again. The probe blocks the cooperative main loop: on a bench node with
no GPS fitted, TX_DONE handling stalled by 0.34-0.89 s on 4 of 9 packets
after a preset change, leaving the radio deaf in standby for that long.
* SX126x: retry a standby that reports a stale command timeout
After a CAD-to-RX handoff's bounded RX times out, the status byte that
comes back with the next SET_STANDBY still carries command status 0x3
(timeout). RadioLib reports that as RADIOLIB_ERR_SPI_CMD_TIMEOUT although
the chip took the command, so every "CAD>RX empty" was followed by a
failed standby, a failed RX start and a full chip re-init. Send
SET_STANDBY once more on that error; the second one sees a fresh status.
Measured on a CH341 host (SHORT_TURBO): chip re-inits went from 3 to 0,
and all 7 retries returned 0.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* SX126x: on a CH341 host, pick a running RX back up instead of restarting it
In continuous mode the SX126x stays in RX after RX_DONE, and RadioLib's
readData() never puts it in standby. rearmReceive() restarted it anyway:
a standby and a full startReceive(), about ten commands. On a CH341 host
each command is a USB round trip, and the chip is deaf for all of them.
On a CH341 host (irqPolledOverUsb(): Lora.spidev is ch341), startReceive()
now arms continuous RX and records that in rxArmedContinuous. Any standby
clears it, including the scan's before a CAD-to-RX handoff, so the next
re-arm after one takes the full path. While it holds, rearmReceive()
resumes instead: it clears the terminal IRQ flags, re-attaches the ISR
and checks for an RX_DONE that beat the arm. PREAMBLE_DETECTED and
HEADER_VALID stay latched, since they can belong to the next frame.
Hosts with a real DIO1 interrupt are unchanged.
Measured on a CH341 host (SHORT_TURBO, 3 runs per arm): post-RX deaf
time 17.8 s -> 0.8 s per run, frame miss rate 11.4% -> 8.5% (p=0.024).
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* Portduino: settable SX126x TCXO start-up delay
A new Lora.DIO3_TCXO_DELAY_US key reprograms the DIO3 TCXO start-up
delay, which begin() leaves at RadioLib's 5000 us. It applies to an
SX126x with DIO3_TCXO_VOLTAGE set, and takes 1 to 10000 us. Absent or 0
keeps 5000 us, so no existing config changes.
The delay matters after a clear CAD: the chip falls back to STDBY_RC,
which stops the TCXO, so the SET_TX that follows waits out the full
start-up before the PA ramps. That wait sits between the channel
verdict and the air, where another node can key up unheard.
Measured on a CH341 host at 1000 us (SHORT_TURBO):
the launch's BUSY wait 5 -> 1 ms, frame miss rate 6.28% -> 3.63%
(p=0.003), with no decode cost at the observer.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* SX126x: on a CH341 host, write the TX payload before the scan's verdict
Between a clear CAD verdict and the air, RadioLib's TX staging sends
about ten commands: the buffer write, the buffer base, packet params,
the IQ and sensitivity register fixes, the DIO IRQ map, an IRQ clear and
SET_TX. On a CH341 host each is a USB round trip, and the channel goes
unwatched for all of them.
On a CH341 host, isChannelActive() now writes the payload of the packet
it is scanning for (scanForTx) into the chip's buffer in the scan's
standby, before SET_CAD. The CAD leaves the buffer alone. After a clear
verdict, launchTransmit() sends only SET_PACKET_PARAMS,
SET_DIO_IRQ_PARAMS, CLEAR_IRQ_STATUS and SET_TX, then waits on BUSY as
RadioLib's launchMode() does.
RadioLib's full staging still runs:
- on a busy verdict, a failed standby, or a packet that is not the one
staged;
- for the first TX after a chip reset, sleep or AGC reset, so the
register fixes are in place before any short launch relies on them.
beginSending() is split so that encodeRadioBuffer() can fill radioBuffer
without claiming sendingPacket. Hosts with a real DIO1 interrupt are
unchanged.
Measured on a CH341 host (SHORT_TURBO): the post-verdict staging went
6 -> 1 ms and the verdict-to-air window 15 -> 10 ms; overlapping frames
fell from 32 to 18 a run (p=0.032).
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* SX126x: on a CH341 host, start the channel scan in three commands
RadioLib's scanChannel() sends six commands before the CAD starts: a
packet-type read, a standby, the DIO IRQ map, an IRQ clear, the CAD
parameters and SET_CAD. Its verdict read adds a second packet-type read.
On a CH341 host each is a USB round trip, and the chip hears nothing
from the scan's standby until the CAD runs.
On a CH341 host the scan now sends the DIO IRQ map, the IRQ clear and
SET_CAD, and the CAD parameters only when they differ from the last ones
sent (they are sent again after a chip reset, sleep or AGC reset). The
standby is already done by trySetStandby() just before. The verdict is
read from the IRQ flags alone. The parameter bytes are the ones
RadioLib's setCad() would send.
Hosts with a real DIO1 interrupt still call lora.scanChannel().
Measured on a CH341 host (SHORT_TURBO), together with the staging in RX
that follows: the deaf part of the scan went 4.87 -> 1.48 ms, and board
frames starting in it 23 -> 0 a run.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* SX126x: on a CH341 host, stage the TX payload during its backoff, while RX runs
Staging in the scan's standby still leaves the payload write between
the backoff's end and the air. On a CH341 host a 250-byte write takes a
few milliseconds, and in that time another node's frame can start and
be missed, or the two nodes' key-ups can line up.
On a CH341 host the payload is now written while RX still runs:
- A backoff of 5 ms or more is brought forward: the TX timer fires at
once, the radio thread writes the payload, then waits out the rest.
At the scan, the packet is encoded again and compared, and a match
skips the write.
- Otherwise the scan writes it while RX runs, and skips the scan
(reporting busy, with no standby) if a frame is arriving or unread
then, since the standby would abort that frame.
- The launch points the TX base at the staged payload.
SX126x continuous RX writes each frame right after the last one and
wraps at the end of the 256-byte buffer. The payload goes just behind
RX's write point, the last place RX reaches again, or at the top of the
buffer when it does not fit there. Each readout reads the frame's
offset to track the write point, and an RX start resets it to 0.
A frame long enough will still reach the payload. Two checks cover it:
- A frame that lands on a staged payload drops the stage, which is
written again at the next backoff or at the scan.
- A frame that was arriving or unread when a write ended is checked at
its readout. The chip writes a frame at about 0.37 ms a byte, far
slower than our write sweeps, so any damage starts at the first byte
the two share. If that byte is ours the frame is dropped with a WARN;
if it is the frame's, the frame is whole and kept.
Hosts with a real DIO1 interrupt are unchanged.
Measured on a CH341 host (SHORT_TURBO, T1000-E peer, 6 v 6):
- Staging during the backoff: the server's CAD arm to air 10.23 -> 6.23
ms, total loss 4.12% -> 2.77% (p=0.002), the peer's miss of the
server's frames 6.00% -> 1.92%. The server's own miss rose 1.16% ->
4.12%, from frames the stage overwrote at the top of the buffer.
- Then staging behind the write point with the byte check: total loss
2.66% -> 1.51%, the server's miss 3.97% -> 1.16%, frames dropped for a
stage 17.3 -> 0 a run. The byte check matched the observer's copy of
the frame in 30 of 30 cases.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* SX126x: drop a stale stage-overlap marker when RX restarts
A TX stage written while a frame was arriving marks that frame for the
first-shared-byte check at its readout. If RX is restarted from standby
first (startReceive(), or a CAD that hands off to RX), that frame is
gone and the marker outlived it, so a later intact frame could be
checked against the old stage and dropped on a 1-in-256 byte match.
Clear it wherever the write pointer is reset.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* LoRa: time a stale RX header from when the header shows
checkStaleRxFlags() stamps rxFlagsSeenMs at the first poll that sees
PREAMBLE_DETECTED, HEADER_VALID or HEADER_ERR, and re-arms once a header
has been latched for one max packet. If a bare preamble set the stamp
and a real frame's header then arrived, the header was judged against
the older stamp: a poll more than one max packet later re-armed RX in
the middle of that frame and lost it. Restart the stamp when a header
first shows, so a header is only re-armed after it alone has been
latched for a full max packet.
Found by CodeRabbit on #12019.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* test: time a stale RX header from when it shows, through checkStaleRxFlags()
The staleRxFlagAction() cases only pin the decision. This drives
checkStaleRxFlags() with a fake IRQ source and the test clock: a header
that shows after a bare preamble restarts the window, so it is not
re-armed one max packet after the preamble.
Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9
* Radio: drop a duplicated STDBY_XOSC block from the develop merge
The merge added the STDBY_XOSC-keeps-the-TCXO-powered block twice at
different offsets in reinitChip(), so every init and recovery sent
SET_STANDBY_XOSC twice and logged it twice - one extra USB round trip on a
CH341 host. Keep one copy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112S3QXU8WUGSCcMQHnwRKZ
* SX126x: keep a staged-over frame's RX_DONE across the resume
A CAD skipped because a frame was in flight set keepRxIrqsAtResume only
when RX_DONE or CRC_ERR was already up. With a header but no RX_DONE yet,
the resume cleared RX_DONE, CRC_ERR, HEADER_ERR and TIMEOUT - and on a
CH341 host the flag read in stageTxInRx() and that clear are a USB round
trip apart, so a frame completing in between lost its RX_DONE. The poll
then had no edge to deliver and the frame waited for checkStaleRxFlags()
to retire the latched HEADER_VALID, one max packet later.
Keep RX_DONE and CRC_ERR for every busy verdict, so the resume's
checkRxDoneIrqFlag() drives the readout. HEADER_ERR and TIMEOUT are still
cleared unconditionally: no readout waits on them, and a stale one costs
checkStaleRxFlags() a re-arm. Only RX_DONE is mapped to DIO1, so leaving
the other terminal flag latched holds no interrupt line.
Found by CodeRabbit on #12016; the blanket keep it proposed would have left
a stale HEADER_ERR or TIMEOUT latched instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112S3QXU8WUGSCcMQHnwRKZ
* SX126x: record the stage over a bare preamble, and name the keep's catch
Two changes the r105 bench round asked for, both common to its arms.
noteStagedOverFrame() was gated on HEADER_VALID or a terminal flag, so a
busy verdict on PREAMBLE_DETECTED alone - which the bench observes - recorded
nothing. That frame can still grow into the staged bytes, no later look
records the check, and its readout then has nothing to compare against: a
frame our write damaged is accepted silently instead of being dropped with
the WARN. Every busy verdict now records it. The record is spent on the next
readout, so it cannot outlive the frame it was taken for, and the stage sits
behind the write point where an unrelated frame reaches it only after the
wrap.
"caught missed RX_DONE" cannot say which look found the flag: pollMissedIrqs(),
rearmReceive(), startReceive() and resumeRunningReceive() all log it, and the
first fires on a timer, so the line counts the ordinary backstop. The resume
now logs its own line when the kept RX_DONE is what it caught, which is the
only case the keep is responsible for. checkRxDoneIrqFlag() returns whether it
notified, so this costs no extra IRQ read - a USB round trip on a CH341 host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112S3QXU8WUGSCcMQHnwRKZ
* SX126x: never clear a frame's RX_DONE at the RX resume
resumeRunningReceive() cleared RX_DONE and CRC_ERR unless a busy verdict had
asked it not to, and readData() has already cleared the flags of the frame it
read. So the clear could only ever erase a *later* frame's RX_DONE - one that
finished during the readout, which on a CH341 host is several USB round trips
wide, the staged-payload overlap check among them. The poll then had no edge to
deliver and the frame waited for checkStaleRxFlags() to retire its latched
HEADER_VALID, one max packet later.
|
||
|
|
07008058c0 |
nrf52 promicro: LR2021 LF PA table from the LR20xx datasheet (#11980)
* nrf52 promicro: LR2021 LF PA table from the LR20xx datasheet RadioLib's default LR2021 LF PA table never programs tx_power above 20.5 dBm, so output stops rising above 14 dBm. This board table takes 10-22 dBm from LR20xx DS rev 2.2 Table 7-19 and keeps RadioLib's rows below 10 dBm, where that table stops. * LR2021: install the board LF PA table only at 500-1000 MHz The datasheet table is the 915 MHz reference design, but applyCustomLfPaTable() installed it for every sub-GHz frequency, so 433 and 490 MHz regions used it too. Outside 500-1000 MHz the LF table now goes back to RadioLib's default. RadioLib keeps the table pointer, so the same-band reconfigure path, which retunes without a full begin(), also reselects the table before setOutputPower(): a 915 -> 433 MHz region change would otherwise have kept the 915 MHz table. isLr20x0CustomLfPaBand() carries the range and is covered in test_radio. * fix(lr2021): restore receive metadata after develop merge Restore the virtual method lost in the merge; CI native and LR2021 links otherwise fail. --------- Co-authored-by: Tom <116762865+Nestpebble@users.noreply.github.com> |
||
|
|
0fc168d875 |
Improved LBT behaviour - CAD-to-RX, correct symbol times, better thresholds (#11137)
* LoRa LBT: fix SX128x/SX126x CAD encoding + slot model, ESP32 sleep race Three fixes from the "are we really listening?" LBT audit: - SX128x CAD: pass RADIOLIB_SX128X_CAD_ON_4_SYMB instead of the raw count 4. The raw value set bits 7:5 = 000, i.e. a 1-symbol scan, leaving LBT nearly blind on 2.4 GHz. Now runs the intended 4-symbol CAD. - SX126x CAD / slot time: name the symNum value explicitly as CAD_ON_4_SYMB (identical 0x02 raw value, so no on-air change) and add a virtual getCadSymbolCount() so computeSlotTimeMsec() models the real ~4.5-symbol scan for the SX126x/LLCC68/STM32WL family only, leaving LR11x0/SX127x at their correct 2.5 symbols. Fixes the too-short contention-window slots. - ESP32 light sleep: if the DIO1/RF95_IRQ line is already asserted just before esp_light_sleep_start(), skip the sleep so the main loop services the pending packet instead of stranding it for up to a full sleep chunk (the detachInterrupt-clears-the-GPIO-wake disarm race). clod helped too * LoRa LBT: keep software-poll preamble flags latched while idle (anti-stomp) Finding #10 from the "are we really listening?" audit, LORA_DIO1_SOFTWARE_POLL boards (meshnology-w10). The 1 ms poll cleared latched PREAMBLE_DETECTED|HEADER_VALID whenever they were the only bits set, so for the rest of a reception isActivelyReceiving() read idle. A TX queued during the payload phase then passed the send gate and startTransmit overwrote the shared chip buffer, losing the inbound packet. That clear only exists to stop a noise preamble from permanently blocking our own TX (starvation). Starvation can only happen when we actually have something to send, so gate the clear on a non-empty txQueue. While the queue is empty the bits stay latched, isActivelyReceiving() reports the in-progress packet, and a later TX defers instead of stomping it. A queued TX still gets the escape hatch. The poll cadence is a fixed 1 ms reschedule either way, so latched bits don't spin the loop; readData() clears everything once a real RX_DONE arrives. clod helped * LoRa LBT: optional RSSI energy second-gate before TX (default off) Findings #4/#5 of the "are we really listening?" audit. CAD only detects a preamble. A neighbour's packet whose preamble already aired - during our own TX, a CAD scan, or a standby gap - is mid-payload and invisible to CAD, so we key up on top of it. Add an RSSI energy check as a second listen-before- talk gate in the TX-decision path: channelBusyByRSSI() compares the instantaneous in-RX RSSI against the rolling noise floor (getNoiseFloor() + margin) and defers if the channel is hot. It reads RSSI before isChannelActive()'s setStandby() via the || short-circuit, so the sample is taken while still in RX where GET_RSSI_INST is valid - no mode juggling. Orthogonal to CAD: CAD has processing gain and catches weak preambles below the noise floor; RSSI catches strong energy at any packet phase, including the mid- payload case CAD is blind to. Neither subsumes the other. DEFAULT OFF. A mis-set threshold makes a node read "busy" on noise and stop transmitting entirely, which is worse than the bug, so it must be tuned on hardware before it ships live. Compiled out unless LORA_RSSI_LBT_MARGIN_DB is defined at build time; when off it is a compile-time `return false` (zero behaviour change). Fails open: returns not-busy unless in RX with a full noise-floor window and a sane dBm reading, so it can never persistently silence a node. Native build (gate off) links clean. clod helped * LoRa LBT: RSSI margin profiler to tune RSSI_LBT_MARGIN_DB (default off) Instrumentation to pick the #4/#5 RSSI second-gate margin from measured data instead of guessing. Build with -DLORA_RSSI_LBT_PROFILE; compiled out otherwise. It accumulates (RSSI - rolling noise floor) in two channel states: - idle (reuses updateNoiseFloor()'s existing RX sample, no extra SPI): the noise- jitter distribution. Its high tail is the floor the margin must clear. - signal (decoded-packet RSSI, hooked in handleReceiveInterrupt): the real-traffic rise. Its low tail is the ceiling the margin must stay under. A throttled log dump prints both distributions (min/mean/max + percentiles) and the derived usable window (idle p99, signal p05). Placement: the stats/histogram engine is a dependency-free, radio-agnostic header (RssiProfiler.h) so it stays out of the core file and is testable on its own; the two hooks and the RSSI/log I/O live on RadioLibInterface where the state is. Independent of the gate flag (LORA_RSSI_LBT_MARGIN_DB) on purpose: profile first to choose the margin, then build the gate on. Native build (profiler on) compiles clean. clod helped * docs: describe the RSSI LBT margin profiler Feature doc for the RSSI listen-before-talk margin profiler (-DLORA_RSSI_LBT_PROFILE): what it measures (idle noise-jitter vs decoded-signal distributions of RSSI-minus-noise-floor), how it's wired (RssiProfiler.h engine + RadioLibInterface hooks, idle sample reused from updateNoiseFloor), how to build and run it, and how to read the log output to choose RSSI_LBT_MARGIN_DB. clod helped * LoRa LBT: draft #3/#6 fixes behind MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE (inactive) Firmware side of the CAD->RX handoff (#3) and ISR_RX re-arm (#6) from the LBT audit. Both need to re-arm RX WITHOUT the standby() that startReceive() forces (which aborts an in-flight reception) - unavailable in stock RadioLib, provided by resumeReceive() in NomDeTom/RadioLib@rxResume. Guarded by MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE, left UNDEFINED: against stock RadioLib these paths compile out and behaviour is unchanged. Define it (via build_flags) only when linking a RadioLib that has resumeReceive(). - resumeReceiveInPlace(bool startRx): base-class helper - iface->resumeReceive() (remap RX IRQ, optional SetRx, no standby) + re-attach the MCU ISR + base RX bookkeeping. One place, chip-agnostic via iface. - #3: SX126x/LR11x0 isChannelActive() CAD exitMode -> GOTO_RX; the TX-decision path splits the busy check into rssi/cad flags and, on a CAD detect, hands the packet to RX via resumeReceiveInPlace(false) instead of startReceive(). Macro-off, the split is exactly the old `channelBusyByRSSI() || isChannelActive()` short-circuit. - #6: ISR_RX re-arms via resumeReceiveInPlace(true). Open (flagged in-code, for review + HW validation): whether re-issuing SetRx (#6) disturbs an on-chip in-flight reception, and the #3 handoff's interaction with a beacon-target packet on the home config. clod helped * LoRa LBT #4: synthesize GPIO wake cause when light sleep is skipped doLightSleep() skips esp_light_sleep_start() when a LoRa IRQ is already asserted, but still read esp_sleep_get_wakeup_cause() unconditionally - so the skip path returned the previous real sleep's cause (e.g. TIMER) and missed the ESP_SLEEP_WAKEUP_GPIO branch that sets runASAP. Capture whether we skipped and return a synthetic GPIO cause in that case so PowerFSM takes the packet-service path. Reads the ESP-IDF cause only after an actual sleep. clod helped too * LoRa LBT #10: keep HEADER_VALID latched in the software IRQ poll The LORA_DIO1_SOFTWARE_POLL anti-starvation escape hatch cleared both PREAMBLE_DETECTED and HEADER_VALID whenever a TX was queued. HEADER_VALID is evidence of a real inbound frame, so clearing it made isActivelyReceiving() read idle mid-payload and let the queued TX stomp the frame. Gate the hatch on a bare-preamble IRQ (PREAMBLE set, HEADER_VALID clear) and clear only PREAMBLE_DETECTED. A stuck header still can't block TX forever - receiveDetected() ages it out after maxPacketTimeMsec - and readData() clears everything on RX_DONE. clod helped too * LoRa LBT #3/#6: CAD->RX handoff + continuous-RX re-arm on stock RadioLib Reimplements the two audit fixes that previously needed the NomDeTom/RadioLib @rxResume fork (MESHTASTIC_RADIOLIB_HAS_RESUME_RECEIVE), using only stock RadioLib, behind a new default-off flag MESHTASTIC_LBT_CAD_TO_RX. Per the RadioLib maintainer's feedback, no new RX API is needed. (GOTO_RX) with the RX IRQs pre-mapped in the scan config, so the chip's own CAD->RX transition delivers RX_DONE with no library call in between. irqFlags (status-enable) carries the CAD verdict + full RX set incl. PREAMBLE/HEADER so isActivelyReceiving() can read them; irqMask (DIO trigger) carries the CAD verdict + terminal RX events only - NOT PREAMBLE/HEADER, which would fire the ISR mid-frame and run readData() on an incomplete packet. On detection it hands off in place: clear the latched CAD verdict, re-attach the MCU ISR, mark receiving - no standby (which would abort the packet just detected). standby), so re-attach the MCU ISR instead of re-issuing SetRx (which could disturb a second packet already arriving on-chip). Removes resumeReceiveInPlace() and the fork macro. SX126x-only: LR11x0's startChannelScan conflates irqFlags/irqMask (drives the DIO from irqFlags), so it keeps the safe GOTO_STDBY path, re-arming on detect to stay coherent with onNotify when the flag is on. Default-off: needs on-hardware validation and assumes continuous RX (the default preamble=16/minSymbols=8 degenerates startReceiveDutyCycleAuto to continuous). Native builds pass both flag-off and flag-on. clod helped too * LoRa LBT #1: re-arm RX unconditionally when deferring a busy channel The post-CAD/RSSI defer path wrapped its startReceive() re-arm in a !hasTargetRadioSettings(txp) guard, so a beacon-target packet that hit a busy channel took setTransmitDelay() with the radio left in standby. When the beacon's target config coincides with the home config the radio is on the home channel, so the node went deaf on home traffic until the next TX cycle - and kept skipping the re-arm every cycle the channel stayed busy. Drop the guard: re-arm unconditionally and keep listening while we wait to key up. A beacon's job is to get a packet out, but it should still mirror the main RX path. (No effect under MESHTASTIC_LBT_CAD_TO_RX, where the chip is already left in RX.) clod helped too * docs: fix RSSI profiler fences (MD040) + RAM figure, note negative-bin limit - Tag the formula and sample-output fenced blocks as `text` so markdownlint MD040 passes. - Correct the RAM figure: RssiDeltaStats is 148 bytes (12 scalar + 34*4 histogram), so two instances are ~296 bytes, not ~280. - Add a limitations note that all negative deltas collapse into bin[0], so a percentile landing there returns the minimum sample. This does not affect the margin (keyed off the always-positive idle p99) but can understate signal p05 when a node decodes below-floor packets; never biases toward a smaller margin. clod helped too * docs: RSSI profiler - record histogram bucketing design choices Add a "Histogram bucketing" section laying out why a fixed histogram is used over exact percentiles, what the axis has to resolve (the margin window sits at roughly -a few..+20 dB), and the arguments for five bucketing schemes: the current positive-only layout (A), a cost-neutral re-centred layout with negative-range resolution (B, recommended if signal p05 must be trusted), wider full-range bins (C), more bins (D), and exact/off-box (E). Explains the current choice and when to switch. clod helped too * LoRa LBT: move RSSI profiler + energy second-gate to listening-now-rssi Split the RSSI work off this branch so the active CAD/sleep fixes and the #3/#6 CAD->RX rework can land without the tuning-dependent RSSI pieces. Removed here (preserved intact on branch listening-now-rssi): - RSSI margin profiler: RssiProfiler.h, the LORA_RSSI_LBT_PROFILE hooks, and docs/rssi_lbt_margin_profiler.md. - RSSI energy second-gate: channelBusyByRSSI() and the LORA_RSSI_LBT_MARGIN_DB gate. The pre-existing noise-floor sampler (updateNoiseFloor/getNoiseFloor, already on develop and consumed by telemetry) is untouched. The TX-decision block reverts to a plain CAD scan; #3/#6 (behind MESHTASTIC_LBT_CAD_TO_RX) and #1 unchanged. Native build green. clod helped too * reduce to bare essentials * LoRa LBT: calibrate CAD symbol count and detPeak against vendor data NUM_SYM_CAD carried two incompatible meanings: computeSlotTimeMsec() read it as a literal symbol count, while the drivers passed it into scanChannel(), where symNum is a register encoding on SX126x/SX128x. On SX126x the raw 2 encodes CAD_ON_4_SYMB, so the chip has scanned 4 symbols since #5749/#5772 while the slot time was priced at 2.5 - a 1.8x mis-scaling of the CSMA backoff unit, live since January 2025. Give the constant one meaning (a literal count of 4) and have each driver translate at its own call site. getCadSymbolCount() reports the real scan length so slotTimeMsec matches it; RF95 overrides it because SX127x CAD is fixed in hardware and its legacy scanChannel() takes no symNum. AN1200.48 Table 1 recommends 4 symbols for SF9-SF12, which is where most shipped presets sit, so 4 is the right target. The comment claiming 2 was reading that note's SF7/SF8 row. detPeak is calibrated per (SF, symbol count), and on LR11x0 per bandwidth as well. Both LR drivers ran thresholds meant for a 2-symbol scan, leaving them less sensitive than measured-best and biased toward missed detections. Now sourced explicitly: LR20x0 LR20xx datasheet rev 2.2 Table 6-19, indexed by symbols x SF LR11x0 Semtech SWSD003 lr11xx/apps/cad/main_cad.c optimized_parameters, indexed by symbols x bandwidth x SF SX126x RadioLib's SF+13, which sits at or below every AN1200.48 value and so errs toward deferring rather than transmitting Combinations the vendor data does not characterise fall back to the RadioLib default rather than guessing. LR2021 also pins fastCad, which scanChannel() reads off the radio object rather than the config struct. Also fix the 2.4 GHz slot term: (2 * sf + 3) / 32 is integer division that truncates to 0 for every legal SF, so the AN1200.22 fractional term never contributed anything. Not hardware-verified. * LoRa LBT: drop audit-doc pointers and cross-chip asides from comments The LBT comments referenced an external audit document by item number (#1, #3, #3/#6, #10, and a see-.notes/lbt pointer), which means nothing to anyone reading the tree on its own. They also described what sibling drivers do, which goes stale silently the moment one of them changes and belongs in that driver's own file anyway. Keep the reasoning, drop the pointer and the comparison. Where a comment explained a rule that genuinely spans drivers - NUM_SYM_CAD's register- encoding caveat - state it as a rule rather than a roll-call of chips. No behaviour change; comments only. * LoRa LBT: don't index the LR11x0 CAD table with a 2.4 GHz bandwidth The detPeak lookup picked its bandwidth row by range, so wideLora's 406.25 kHz fell into the sub-GHz 500 kHz row - SWSD003's table is the sub-GHz LoRa modem (BW enums 62.5-500), and 2.4 GHz is a separate modem with its own calibration. Match the four bandwidths exactly instead, so anything unmatched falls back to the RadioLib default rather than borrowing a row it has no claim to. Those four are the whole sub-GHz set today - LR11x0::setBandwidth() rejects anything else - so note that a newly unlocked bandwidth, or a symbol count outside 2/4/8/16, needs a row adding here and fails silently if it doesn't get one. Same note on LR20x0 for symbol counts above 4. Also correct two citations. The 2.4 GHz slot term is the SX1280 datasheet's own CAD duration formula, and the CAD symbol-count guidance is AN1200.77, not AN1200.22 - which is "LoRa Modulation Basics" from 2015 and predates the part. Record that SX1280 ignores detPeak/detMin/exitMode entirely: SetCadParams carries only cadSymbolNum, and the PNR threshold lives in register 0x942. * LoRa LBT: set the SX1280 CAD PNR threshold and scan 8 symbols SetCadParams on this part carries only cadSymbolNum - detPeak/detMin in the scan config are inert - so CAD sensitivity is governed entirely by register 0x942, which nothing was writing. The scan therefore ran at the chip's reset value of 0x32, against the 19-36 that AN1200.77 Table 1 calibrates per SF and bandwidth: at LONG_FAST wideLora the recommendation is 27 and we were using 50. A threshold that high needs a much stronger correlation peak, so 2.4 GHz LBT was under-detecting on top of the symbol-count bug already fixed. Write the table value before each scan, indexed by bandwidth x SF. Unmatched bandwidths write nothing and leave the chip's default, as on the LR parts. This goes through our own Module rather than lora.writeRegister(), which RadioLib keeps protected unless RADIOLIB_LOW_LEVEL unlocks raw register access across every driver. SX128x::writeRegister() is a straight SPIwriteRegisterBurst on the same Module we already own, and begin() has pointed spiConfig at the 0x18 WriteRegister opcode long before any scan runs, so the transaction is identical - no subclass needed. Also raise the CAD window from 4 to 8 symbols, matching RadioLib's own SX128x default. AN1200.77 measures a longer window as moving P(Detection) further from P(False Detection); its thresholds are stated to hold for any window duration, so the table above is unaffected. NUM_SYM_CAD_24GHZ moves in step so the slot time still reflects the real scan. Not hardware-verified. * LoRa LBT: hand CAD straight to RX on the LR parts CAD exited to standby, and the busy-channel path then issued a full startReceive() - which standbys the radio and re-enters RX from scratch, throwing away the reception CAD had just detected. Both LR families expose a CAD-to-RX exit mode that avoids the round trip, and it reaches the chip through the public startChannelScan(cfg), so nothing here needs a library patch. A note in an earlier commit claimed this was impossible because RadioLib collapses irqFlags and irqMask into one set. That reasoning is SX126x's, where irqMask is a status gate and an event omitted from it never latches at all. The LR parts have no such gate: SetDioIrqParams takes a mask per interrupt pin and SetDioIrqConfig maps IRQs to one DIO, while getIrqStatus() reads the status bytes returned on every SPI transaction and consults no pin mapping. So the CAD IRQ set now carries the verdict plus terminal RX events only - preamble and header stay off the pin, where they would fire the ISR mid-frame and run readData() on a partial packet - and isActivelyReceiving() still sees them. rearmReceive() cannot be unconditionally standby-free the way SX126x's is. cad_timeout 0 means "RX until one packet arrives, then standby", so the two re-arm paths leave the chip in different states: already listening after the handoff, back in standby after that packet's RX_DONE. It branches on a flag set at detection and cleared on use, so the first re-arm skips the standby and the next restores continuous RX. Forcing continuous RX through the timeout instead is not reliable - RadioLib converts it to RTC ticks through a float divide, so the 0xFFFFFF "no timeout" encoding is not addressable. On a CAD false positive the chip now waits in single-shot RX rather than returning to standby. Still listening, but a state the old code never entered. Not hardware-verified. * LoRa LBT: size the 2.4 GHz CAD slot per chip, not per band computeSlotTimeMsec() branches on myRegion->wideLora, which is a region rather than a part. LR1120 and LR2021 both run there, and both scan NUM_SYM_CAD symbols - but that branch read the flat NUM_SYM_CAD_24GHZ, so raising it to 8 for SX128x moved the slot for two families whose scan never changed. At LONG_FAST wideLora that priced ~22 ms of CAD against a ~10 ms scan. getCadSymbolCount() exists precisely so the slot tracks the real scan, but only the sub-GHz branch consulted it. Use it in both, and give SX128x an override returning NUM_SYM_CAD_24GHZ. That constant stops being a fleet-wide 2.4 GHz value and becomes what one driver reports; SX128x is unchanged at 8.78 symbols, while the LR parts on 2.4 GHz drop from a wrong 8.78 to 4.78. The error was conservative - slots too long, so throughput lost rather than collisions caused - but it is the same scan/slot mismatch the rest of this work removes. * LoRa LBT: declare CAD symbols per chip and per band, 8 on 2.4 GHz A 2.4 GHz region is shared: an LR1120 and an SX1280 can sit on the same mesh. Leaving the LR parts at 4 symbols there while SX1280 scanned 8 gave them different slot times, and so a different CSMA backoff scale between neighbours - the cross-family asymmetry this branch removed for sub-GHz, reintroduced one band up. Both LR families now scan 8 on 2.4 GHz, matching SX1280. Rather than switch on the band inside a shared constant, each driver states its own count for each band it works in, and derives the symNum it passes from the same accessor - so the scan and the slot cannot drift apart: SX126x 4 sub-GHz SX128x 8 on 2.4 GHz LR11x0 4 sub-GHz, 8 on 2.4 GHz LR20x0 4 sub-GHz, 8 on 2.4 GHz RF95 2 sub-GHz, its CAD being fixed in hardware and not settable NUM_SYM_CAD and NUM_SYM_CAD_24GHZ become base-class defaults nothing falls through to; they now record the fleet convention rather than any part's behaviour. LR20xx Table 6-19 stops at 4 symbols, so an 8-symbol scan reuses that row - the last measured point, erring high and so slightly less sensitive than a true 8-symbol value. Noted at the table. Also a comment pass over the branch: drop retrospective phrasing that describes what the code used to do, cut trailing justification clauses, and fix comments the per-band change left stale - one still claimed the LR parts scan fewer symbols than SX1280 on 2.4 GHz, which is now the opposite of the truth. * LoRa LBT: sweep up a CAD->RX handoff that delivers nothing A handoff leaves the chip listening by its own exit mode, so nothing in the driver arms the radio and no return value says whether it worked. If the detection was false, or the exit mode was not honoured, the node waits in a state we never verified - and on the LR parts that is single-shot RX, which does not return to continuous listening on its own. Stamp the handoff and let the existing ~1s poll re-arm if a whole max-length airtime passes with no packet, the same bound receiveDetected() uses. Clearing it on a real RX_DONE is obvious; clearing it in startSend() is not, but a TX can intervene before any packet arrives and its completion re-arms RX anyway, so leaving the marker set would have the poll re-arm a radio already listening. Reads the clock through Time::getMillis(), the same source Throttle compares against, so the site stays testable across the millis() wrap. 0 is the "none outstanding" sentinel and is tested before the elapsed compare. Also document on rearmReceive() that an override must never call the chip's own startReceive() - its standby+SetRx aborts the packet CAD just detected, which is the entire reason the override exists. * define txen pin to allow sx128x testing * LoRa LBT: bound the CAD->RX handoff in hardware, and re-arm after it delivers cadTimeout only applies to the CAD->RX exit mode. Every driver on develop exited CAD to standby, so the field was ignored and its value - 0, which no datasheet defines - was dead data. Switching the exit mode to RX made it load-bearing while the line itself never changed, so the diff showed nothing. All three handoff parts now bound that RX at one max-length airtime, taken from getPacketTime() so it tracks the preset. A detection that delivers nothing expires to standby and raises TIMEOUT, already mapped to the pin, so the existing ISR re-arms and the queued TX retries on its own backoff. handleReceiveInterrupt() returns early on TIMEOUT without RX_DONE; otherwise each dud detection would reach readData() and be logged as a bad packet. That RX is single-shot, so the arm after it delivers must be a full one. SX126x re-armed unconditionally without a standby, which left the chip in standby while the interface believed it was listening - and nothing swept it, since RX_DONE has already cleared the handoff stamp. A node that received a handoff packet then went deaf until its next transmit. It now carries the same cadHandedToRx flag the LR drivers already had. Sourcing, since the parts disagree and RadioLib's uniform ChannelScanConfig_t hides it: SX126x DS_SX1261-2 p98 (15.625us steps), LR11x0 UM Table 8-8 (31.25us), LR2021 lr20xx_driver cad_timeout_in_pll_step (31.25us, contra the datasheet's "32 MHz periods"). RadioLib scales both LR parts as 30.52us, so the programmed value lands ~2% long - harmless, and left for upstream. Also marks the three SWSD003 cells at 2 symbols/BW500/SF10-12 as that SDK's fallback rather than measurements. * LoRa LBT: own the CAD->RX handoff in one place, and void it on standby The handoff state was split across two classes: cadHandoffRxStart in RadioLibInterface, cadHandedToRx in each of SX126x/LR11x0/LR20x0, whose rearmReceive() overrides were byte-identical. Neither field was cleared by setStandby(), so a standby taken while a handoff was outstanding could strand cadHandedToRx set - and the next rearmReceive() would then re-attach the ISR to a chip sitting in standby, marking the interface as receiving with no RX_DONE ever to come and no recovery path. A stale cadHandoffRxStart was milder but still re-armed the radio over whatever was arriving at the time. Hold both in the base, arm them together in noteCadHandoffToRx(), clear both in setStandby(), and collapse the three overrides into one non-virtual base rearmReceive(). SX128x and RF95 never arm the flag, so they keep taking the plain startReceive() branch. Document on isChannelActive() that a true return must be followed by rearmReceive() - the invariant the whole scheme rests on. Two fixes fall out of the shape: - The handoff re-arm attaches a rising-edge ISR to a line that is not known to be low, unlike startReceive() which clears the IRQ status first. Follow it with checkRxDoneIrqFlag(), as startReceive() already does. - checkCadHandoffTimeout() used one max-length airtime, the same figure the chip's own cadTimeout now uses, so the two expired together. Losing that race left a queued ISR_RX to run against flags the software path had just cleared, falling past the TIMEOUT guard into readData() on an empty buffer. Double it so the hardware always expires first; the backstop then only covers an RX whose timer stopped on a header that never completed. * LoRa LBT: re-arm RX when a queued packet is dropped for tx_enabled=false Pre-existing on develop, not introduced by this branch, but it is the same failure this series exists to remove. TRANSMIT_DELAY_COMPLETED reaches startSend() only through isChannelActive(), which leaves the chip in standby for the transmit about to follow. When that transmit is then dropped because LoRa TX is disabled, the early return frees the packet and returns without arming anything: isReceiving stays false, so pollMissedIrqs() skips the radio too, and the node is deaf until something else re-arms it. Narrow - tx_enabled has to change between enqueue and dequeue, since send() checks it as well - but the fix is one call on a path that already exists. * LoRa LBT: say what the CAD slot time actually does per chip Two comments in RadioInterface.h described intentions the code does not carry out. NUM_SYM_CAD/NUM_SYM_CAD_24GHZ claimed the per-band values keep the CW backoff scale equal across a mesh. They do not: the slot has to match the scan each part really runs, and RF95's CAD is fixed in hardware at 2.25 symbols while SX126x/LR scan 4. A mixed sub-GHz mesh therefore runs two slot lengths, which is correct per part and worth stating rather than implying otherwise. The slotTimeMsec initializer runs during RadioInterface's own construction, so the virtual getCadSymbolCount() it calls always resolves to the fallbacks here, never a driver override. That is harmless only because applyModemConfig() recomputes it before anything reads it - left as-is deliberately, since initializing to 0 would make random(0, 2^CW) * slotTimeMsec fire an immediate transmit if anything ever did read it early. * LoRa LBT: set the SX128x CAD threshold at config time, not per scan isChannelActive() looked up the AN1200.77 PNR row and wrote register 0x0942 on every scan, but sf and bw only change in applyModemConfig(). That is an SPI round trip between the channel scan and the transmit that follows it, on the one path this file's own comments ask to keep short. Move it to applyCadDetPeak(), called from reconfigure() after the sf/bw setters it depends on, and from init() as well - init() does not route through reconfigure(), so it needs its own call. * ESP32: stop skipping light sleep forever on a stuck LoRa IRQ The skip added earlier in this series returns instead of sleeping whenever DIO1 already reads high, so the main loop can service the packet rather than strand it for the sleep chunk. It had no bound: a line that latches high with nothing clearing it makes every subsequent call skip too, and the node quietly stops sleeping at all, announced only by a LOG_INFO each time. Count consecutive skips - warn at 3, sleep through at 5, reset on any real sleep. If the IRQ has survived five attempts the main loop is not going to clear it, and never sleeping costs more than one stranded packet. Also drop the LORA_DIO1_EXTENDED_IO branch in loraWakeIrqAsserted(): those boards return from doLightSleep() before it is ever called. * nrf52 promicro: gate the P1.06 TXEN pin, and add an optional SX1280 build P1.06 was wired to SX126X_TXEN unconditionally earlier in this series, to drive an external PA on a bench unit. That is not safe to ship: three envs share this variant.h, and P1.06 is PIN_WIRE_SCL on the EasyProMicro pinout and PIN_EINK_BUSY on the InkHUD one. The default builds go back to RADIOLIB_NC. Put it behind -D PROMICRO_SX126X_TXEN, refused wherever the pin is already claimed. The EasyProMicro arrangement has its own SX126X_TXEN on P1.00 in the #else branch and never reaches this, so only the e-ink exclusion is needed. The pin was wanted for SX1280 in the first place - every other SX128X line is already assigned on this board and TXEN was the one missing - so add that too, behind -D PROMICRO_SX1280 and a new nrf52_promicro_diy-sx1280 env. Both opt-ins claim P1.06, so asking for both is an #error rather than a silent pick. Costs about 10.7 KB: the image ends at 0xE5140, 19 KB clear of the warm region, against 0xE2710 and 30 KB for the base env. * LoRa LBT: fix two defects in the CAD->RX handoff found in review Both reported by Copilot against 1fcd219af, both confirmed against the code as it stands after the handoff was hoisted into RadioLibInterface. rearmReceive() armed the MCU ISR before marking the interface as receiving. An ISR firing in that window queues ISR_RX while isReceiving is still false, so handleReceiveInterrupt() bails with "called while not in rx mode" and the packet is dropped - the exact packet the CAD handoff existed to catch. Reversed, which also makes this path match the order the drivers' own startReceive() has always used: base state, then arm, then checkRxDoneIrqFlag(). noteCadHandoffToRx() stored Time::getMillis() raw, but 0 is the "no handoff outstanding" sentinel that checkCadHandoffTimeout() tests before its elapsed comparison. A handoff armed on the one millisecond per wrap where getMillis() returns 0 reads as "none outstanding" and the backstop never fires. Step past it to 1, the same hand-remap Throttle.h's own TODO records GPS.cpp using for fixHoldEnds. One millisecond of error, once per 49.7 days. * ESP32: wake on SX128X_DIO1, which five SX1280 boards define instead of LORA_DIO1 enableLoraInterrupt() only ever looked at LORA_DIO1 and RF95_IRQ. RadioInterface builds an SX1280Interface from SX128X_DIO1, and several variants name the IRQ pin only that way, or put it on a different pin from LORA_DIO1: tlora_v2_1_18 LORA_DIO1 undefined SX128X_DIO1 26 betafpv_2400_tx_micro LORA_DIO1 undefined SX128X_DIO1 4 t-eth-elite LORA_DIO1 16 SX128X_DIO1 8 tlora_t3s3_v1 LORA_DIO1 33 // TCXO_EN? SX128X_DIO1 9 tlora_t3s3_epaper LORA_DIO1 33 // TCXO_EN? SX128X_DIO1 9 Every branch of the function is gated on defined(LORA_DIO1), so on the first two it preprocessed to an empty function: esp_sleep_enable_gpio_wakeup() still enabled GPIO as a wake class, but no pin was ever armed for the radio. On the other three it armed the wrong pin - GPIO33 there being the line the variant's own comment questions as TCXO_EN. Those boards could not be woken from light sleep by an incoming packet; they waited out the 30s chunk and pollMissedIrqs() recovered the latched RX_DONE about a second after waking. Delayed, not lost, but deaf for the duration. loraWakeIrqAsserted(), added earlier in this series, inherited the same blind spot and was dead code on the two boards with no LORA_DIO1 at all - so the skip-the-sleep fix never once fired on an SX1280. Resolve the pin per radio in loraWakeDio1Pin() and use it for arming, polling and teardown alike, so the three cannot drift apart again, and widen their guards to accept either macro. Behaviour is unchanged wherever it already worked: for any non-SX1280 radio the helper returns LORA_DIO1, including on RF95 boards where the SOC_PM_SUPPORT_EXT_WAKEUP branch arms it regardless of radio type. LORA_DIO1_EXTENDED_IO still short-circuits first. Note this is a power-profile change for those five boards, not only a fix: they gain a LoRa wake source they have never had, so on a busy channel their light sleep will now be cut short by traffic like every other board's. * LoRa LBT: trim three-line comments to the repo's two-line limit copilot-instructions.md:340 says one or two lines; the P1.06 availability note and the LR2021 DIO routing note were both three. CodeRabbit flagged both. * LoRa LBT: trace the CAD scan, its exits and the handoff outcome Eight short lines, all in the base class so they cost one set of strings rather than one per chip driver. The scan logs arm/busy/free; the in-chip GOTO_RX handoff logs started, then exactly one of pkt, empty, timeout or void - so a dropped or double-counted handoff is visible by inspection. void and timeout are LOG_WARN because both mean an invariant broke: a standby discarded a live handoff, or the chip's own timer failed to fire and the software backstop had to re-arm. The rest are LOG_DEBUG, so a node at normal log level stays quiet unless something is actually wrong. The two pre-existing handoff strings were 44 and 41 bytes of flash; cut to "CAD>RX timeout" and "CAD>RX empty", they pay for most of the new set. The shared "CAD" prefix does not dedup - only string suffixes merge - and splitting it out behind a %s measured 8 bytes worse on Cortex-M once the extra pointer argument per call site is counted. Also fixes the delivered-packet trace, which sat above the TIMEOUT check and so claimed a packet on a window that expired empty. * LoRa LBT: restore the SX1280 CAD threshold after a chip-state recovery The calibrated AN1200.77 PNR threshold moved from a per-scan register write to a config-time one, so it now has to be re-applied wherever the chip's state is rebuilt. recoverChipStateLoss() hardware-resets the chip and reprograms only the modem params, which left a recovered SX1280 doing CAD against the vendor default threshold until the next reconfigure() - and on a node whose config never changes, that is the rest of its uptime. * LoRa LBT: retire RX flags that plain RX never completes Plain RX runs with no chip timeout (startReceiveDutyCycleAuto(16, 8) falls back to RX_TIMEOUT_INF) and the DIO mask carries RX_DONE only, so a PREAMBLE_DETECTED, HEADER_VALID or HEADER_ERR that never completes stays latched until something re-arms RX. On a node with nothing to send, nothing does. The CAD->RX handoff already has this recovery (cadTimeout plus checkCadHandoffTimeout()); plain RX had none. checkStaleRxFlags() is its twin on the ~1 s pollMissedIrqs() tick. It notes when flags are first seen, and if no RX_DONE has consumed them one max-length packet later: - a header (valid or errored) re-arms RX. RX_DONE clears every flag in readData(), so a header still latched has no frame behind it. SX1280 DS rev 3.3 section 16.2: a header error leaves RX wedged with no RX_DONE or timeout, and only a re-arm recovers it. - a bare preamble is only cleared. ClearIrqStatus/ClearIrq changes the IRQ register alone on SX126x, SX1280, LR11x0 and LR20x0, so it never aborts a reception; startReceive() goes through standby and would. HEADER_VALID is never cleared on its own: RadioLib's readData() reads it at RX_DONE, and on LR2021 its absence reports the packet as header damaged. Skipped while a CAD handoff is outstanding (its own timeout owns that RX) and while RX_DONE is pending. RF95 opts out: its carrier sense reads modem status rather than latched flags, and SX127x maps neither PREAMBLE_DETECTED nor HEADER_ERR. The decision is staleRxFlagAction() in RadioInterface.h, pinned in test_radio at both sides of the window and for each action. * LoRa LBT: hold TX on a seen preamble instead of judging it by a deadline receiveDetected() declared a bare PREAMBLE_DETECTED false once 2 * preambleTimeMsec passed without HEADER_VALID - about 8 ms at SF7/BW500. Nothing reads the flags on that cadence: they are latched status bits, never routed to DIO, and the only reader is the TX path, once per CSMA backoff (0-56 ms at SF7/BW500). The verdict was usually reached by a read older than its own deadline, and the queued TX went out over the frame. Reported in #11933 by @jp-bennett, whose two-node bench measured own TX a median 43 ms after the verdict, inside a ~102 ms frame. This branch's longer CAD slot widens it to LONG_FAST: 0-308 ms of backoff at CWmin against a 262 ms deadline. A read that sees a bare preamble now clears PREAMBLE_DETECTED and holds TX for one max-length airtime. The clear touches only the IRQ register (ClearIrqStatus/ClearIrq on SX126x, SX1280, LR11x0, LR20x0), so it never aborts a reception. Reads during the hold report busy, so the normal random backoff still spreads retries, and any RX interrupt ends the hold early. A preamble that fires again during the hold stays latched, and the first read after it holds again: a source that keeps triggering keeps deferring TX, one airtime at a time, while a single stale flag cannot block it. HEADER_VALID is never cleared here - RadioLib's readData() reads it at RX_DONE, and on LR2021 its absence reports the packet as header-damaged - and keeps its one-airtime ageing. The SX126x software IRQ poll routes its bare-preamble case through the same hold instead of clearing on every tick while a TX is queued, which would have released TX before the next read could see a refire. That poll read the private txQueue directly, so meshnology_w10 - the only env that defines LORA_DIO1_SOFTWARE_POLL - did not compile. hasQueuedTx() exposes the one fact it needs. #11937 addresses the same defect by deferring the verdict when the previous read was stale; the two conflict in receiveDetected(). * LoRa: re-arm RX before logging the packet that just went out After TX_DONE the chip sits in standby, deaf, until startReceive(). The ISR_TX path used to log airtime ("Packet TX") and printPacket() the sent packet first, which kept it deaf for ~2-5 ms more depending on where the log goes. Measured on an nRF52 SX1262 bench node: the TX_DONE-to-RX gap drops from ~8.8 ms to ~3.7 ms with USB serial logging (~11.3 to ~6.2 ms with the log also going to a phone over BLE). completeSending() is split into detachSentPacket(), which clears sendingPacket and runs packetReleased() so RX re-arms on the home config, and finishSentPacket(), which does the airtime, counters, log and pool release. setStandby() callers keep the combined completeSending(). * SX126x: keep the TCXO powered in standby RadioLib leaves standbyXOSC off, so standby and the post-TX/RX fallback both use STDBY_RC, which powers a DIO3-controlled TCXO down. Every SetRx and SetTx after that first waits out the TCXO start-up delay programmed into the chip (RadioLib's setTCXO() default, 5000 us). On an nRF52 SX1262 bench node the RX start after TX_DONE took ~6.0 ms at SF7, SF9 and SF11 alike; with setStandbyXOSC(true) it takes ~0.95 ms, and the TX_DONE-to-RX gap falls to ~1.1 ms together with the previous commit. The result held across the periodic AGC reset. Only boards that drive a TCXO from DIO3 change. The cost is the TCXO and XOSC current while the chip sits in standby, which the firmware keeps brief; duty-cycled RX still sleeps, and powers the TCXO down, between windows. * LoRa: skip the periodic AGC reset while a packet is queued resetAGC() ends in startReceive(), and trySetStandby() first runs checkNotification() to drain pending events. With a packet queued that can be TRANSMIT_DELAY_COMPLETED: CAD passes, startSend() keys up, and trySetStandby() then puts the chip in standby over the transmission it just started. completeSending() still reports the packet as sent and counts its airtime. A bench node logged this twice in 20 minutes; one was a packet's last retry, so it never went on air. Skip the reset for this tick when a TX is queued. The queue drains within seconds, and the reset comes round again on the next interval. * GPS: re-enable on a LoRa config save only when the region was unset #10386 keeps the GPS off until a region is set and turns it on when a LoRa config save sets one. The check never looked at the old region, so a GPS the probe had disabled ("GPS not detected; not present this boot") was re-enabled by every LoRa config save, preset changes included, and the probe ran again. The probe blocks the cooperative main loop: on a bench node with no GPS fitted, TX_DONE handling stalled by 0.34-0.89 s on 4 of 9 packets after a preset change, leaving the radio deaf in standby for that long. * LR20x0: CAD exit to RX is 0x01 on the LR2021 RadioLib (510e00cf) defines RADIOLIB_LR2021_CAD_EXIT_MODE_RX as 0x02. The LR2021 takes SetLoraCadParams exit modes 0x00 CAD only, 0x01 RX on detection and 0x10 TX when clear, as the LR11x0 does, and refuses 0x02 with a processing error (-706). With the CAD>RX handoff passing 0x02, every scan failed before the CAD ran and isChannelActive() reported the channel clear: on a Seeed Mesh-Tracker X1, 539 scans against live traffic gave no busy verdict, so the node transmitted with no carrier sense. Pass 0x01, and warn when a scan fails rather than reporting it clear in silence. * LoRa LBT: take out the preamble hold on a seen preamble Revert |
||
|
|
8ef996f5b4 |
fix(radio): never read a RadioLib error code as a packet's time on air (#11940)
* fix(radio): never read a RadioLib error code as a packet's time on air
getTimeOnAir() reads the packet type back over SPI on every chip and answers RADIOLIB_ERR_WRONG_MODEM when the chip is no longer in the mode we left it in (a reset or brownout leaves it in FSK). RadioLib returns times as an unsigned microsecond count and its negative int16_t error codes through that same value, so computePacketTime() divided -20 by 1000 and handed 4294967ms up as the time one packet spent on the air.
That single sample is enough to take the node off the air until it is rebooted:
- channelUtilizationPercent() reads 7158% and utilizationTXPercent() 119%,
matching the 7000%+ ChUtil spike and the 120% AirUtil in #11935, so both TX
gates stay shut for the hour it takes that bucket to age out;
- receiveDetected() uses the same figure as its false-header timeout, so
activeReceiveStart is never cleared and canSendImmediately() sees a busy
channel for 72 minutes.
Classify the returned value instead of trusting it. A failed getTimeOnAir() now falls back to calculateTimeOnAir() on the modem parameters we configured - arithmetic with no readback that can fail - and the RX path, which already used calculateTimeOnAir(), is guarded the same way.
Fixes #11935
* style: trunk fmt RadioLibInterface.h
|
||
|
|
7e11bde8c8 |
fix(beacon): repair the MeshBeacon radio switch/restore regression from #11573 (#11596)
* fix(radio): put the beacon restore back inside completeSending's if (p) Reverts the RadioLibInterface and RadioInterface changes from #11573 ( |
||
|
|
8a15d9258f |
fix(test): unbreak test_radio under ASan (#11589)
* test_radio: prove the rejected packet was released via pool accounting, not pointer identity * test-state.sh: silence the shell's own open failure when scanning /proc for survivors * Trim the comments added with the test_radio and test-state fixes |
||
|
|
ac330e6a6b |
fix(radio): MeshBeacon heap leak and runtime packet payload size check (#11573)
* Fix for MeshBeacon packet leakage * fix: add runtime payload size check against radiobuffer * review fix for PR#11573: clear target radio settings before MeshBeacon packet release * add unit test for radio buffer capacity check, removing related assert for the test * review fix for PR#11573: add explicit verifaction against rejected packets |
||
|
|
bc035bb812 |
feat(lora): state a pinned userPrefs preset as the unset region's intent (#11507)
* feat(lora): state a pinned userPrefs preset as the unset region's intent A vendor build can pin USERPREFS_LORACONFIG_MODEM_PRESET while leaving the region unset, so a fresh flash comes up as region UNSET plus a deliberate preset. Stock installs come up as region UNSET plus the LONG_FAST placeholder, and nothing in FromRadio told the two apart - so clients treat every unset-region node as factory-fresh and replace its preset with the region default as soon as the user picks a region. A mesh pinned to SHORT_TURBO loses every new node to LONG_FAST or LONG_TURBO, silently. getRegionPresetMap() now emits an UNSET entry when, and only when, the build pins a preset, stating that preset as both the group's sole entry and its default. Stock builds are unchanged on the wire: no UNSET entry, which clients already read as unconstrained. This is intent, not enforcement. supportsPreset() still accepts any known preset while the region is unset (#11496) and the radio is held silent either way, so the device continues to honour whatever the user or an admin sets. Costs one group slot and one region slot on pinned builds only (6->7 of 8, 34->35 of 38); exhaustion is logged and degrades to the existing unconstrained behaviour. * Trim comments to the project's one-to-two-line limit |
||
|
|
08722da3d9 |
fix(lr2021): live LF↔HF reconfigure via full begin() (#11279)
* fix(lr2021): full begin() on live LF/HF reconfigure.Avoid RadioLib -706 / assert when live-switching Sub-GHz ↔ LORA_24. * fix(lr2021): align band-hop begin() with init() robustnessMirror. RF-switch GPIOs, SPI/TCXO retries, and log CRC/RX-gain errors. * chore: trunk fmt LR20x0Interface bandHop line wrap * fix(lr2021): harden live band reconfigure (companion #1) * fix(lr2021): reject invalid freq in band-hop path select Require requestedMHz > 0 in isLr20x0BandHop, expose lr20x0ReconfigurePathfor FullBegin vs incremental selection, and extend native radio tests. --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
b8b5582943 |
Regioninfo (#11056)
* advertise a superset of EU regional presets to client devices. * trunk |
||
|
|
c5355641d3 |
Add MEDIUM_TURBO modem preset (#10988)
* Protobufs * Wire up MEDIUM_TURBO modem preset MEDIUM_TURBO (500 kHz, SF9, CR 4/5) already existed in the protobuf enum but was never wired into firmware, so selecting it silently fell through to the LONG_FAST default and rendered an "Invalid" display name. Add its bw/sf/cr mapping (modemPresetToParams), display name (MediumTurbo/MedT), PRESETS_STD membership (standard regions only — 500 kHz does not fit EU868's 250 kHz band, so it stays out of PRESETS_EU_868 and is rejected/clamped there), and the MEDIUM SNR-grading bucket. Includes positive coverage in test_radio, EU868-reject + US-accept coverage in test_admin_radio and test_mesh_beacon, the STD preset count 9->10, an extended fuzz range, and the client-spec doc. * Address review feedback on MEDIUM_TURBO tests - test_mesh_beacon: assert has_mesh_beacon before checking the invalid preset was cleared, so the EU868-cleared test can't pass on a dropped message (matches the existing SHORT_TURBO test). - test_fuzz_packets: draw modem presets from _ModemPreset_ARRAYSIZE instead of a hard-coded 17 so the fuzz range tracks future enum additions automatically. |
||
|
|
d846780a9b |
More fuzz tests and small fixes for the findings (#10864)
* first pass tests * more tests * Fix two crafted-admin-packet crashes found by the E5 fuzzer Both are reachable from an authorized admin (local from==0, admin channel, or PKC) - remote DoS: 1. SIGFPE in LoRa config validation. A set_config LoRaConfig with use_preset=false and bandwidth=0 makes freqSlotWidth 0, so numFreqSlots is 0 and `hash(name) % numFreqSlots` (RadioInterface.cpp) divides by zero. Guard the modulo; the existing channel_num check then rejects/ clamps the config. 2. Stack overflow in Channels::getKey. A SECONDARY channel at the primary slot with an empty PSK recursed into getKey(primaryIndex) forever. Skip the primary-key borrow when chIndex == primaryIndex. Re-enable the E5 admin fuzzer to hit both triggers again (use_preset both ways incl. bandwidth 0, plus the set_channel tag) as regression guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Correct fuzz-test invariants after the crash fixes - E5 admin fuzz: node eviction under a filling NodeDB is legitimate, so assert only the bounded-count invariant, not that a specific seed node survives 6000 mutating ops. - TMM blitz: scope off the nodeinfo direct-response send path (it needs a fully-wired MeshService/phone queue the fixture doesn't provide; the deterministic directResponse tests cover it). The crafted-nodenum rate/unknown/position cache stress is unchanged. clod helped too * realistic tests * test: dedup fuzz RNG into shared test/support/DeterministicRng.h The four in-tree fuzz suites (test_fuzz_decode, test_fuzz_packets, test_hop_scaling, test_traffic_management) each carried a byte-identical copy of the seeded 64-bit LCG (rngSeed/rngNext/rngByte/rngRange). Hoist it into one shared header so there is a single generator to reason about and no risk of the copies drifting. static inline keeps per-suite state per translation unit and avoids -Wunused-function for suites that don't use every helper. Also corrects a stale comment in test_traffic_management (the blitz's nodeinfo direct-response path is intentionally left off). No behavioral change: same constants, same per-suite seeds. clod helped too * test: fuzz uncovered ProtobufModule handlers and the MQTT downlink ingress Extend the in-tree fuzz coverage to packet sources that previously had none: - test_fuzz_packets E8/E9/E10: drive PositionModule, DeviceTelemetryModule and NeighborInfoModule at handleReceivedProtobuf directly (via using-shims, bypassing the ProtobufModule reply/send path so no router is needed). The fixture already stands up nodeDB/service/channels, and nodeStatus/powerStatus are auto-initialized in main.cpp, so no new globals are required. Adds a shared fuzzRxHeader() helper for crafting adversarial RX packet headers. - test_fuzz_decode: add meshtastic_KeyVerification to the decode table. The KeyVerification and StoreForward handler paths are documented as decode-level only, with the concrete reason each is intrinsic (private-state gating / PSRAM + self-pointer wiring), not a fixture gap. - test_mqtt: test_receiveFuzzServiceEnvelope blitzes the non-RF broker-push ingress (onReceiveProto) two ways - raw garbage bytes that must fail envelope decode cleanly, and a well-formed ServiceEnvelope wrapping a crafted inner MeshPacket over crafted channel_id/gateway_id - exercising the channel match, isFromUs, XEdDSA receive policy and perhapsDecode chain. Adds a deliverRaw() passthrough to MQTTUnitTest. All under the coverage env (ASan/LSan). No firmware/src changes. Full sweep GREEN 27/27, 544 cases. clod helped too * Harden LoRa/channel config against crafted admin messages; consolidate test helpers Production (review findings on the hot-fuzz crash fixes): - Clamp bandwidth at the source (clampBandwidthKHz) in checkOrClampConfigLora and applyModemConfig so numFreqSlots can never be 0 for any consumer; a bandwidth-0 set_config previously passed validation and re-armed the SIGFPE on the next applyModemConfig. - Guard applyModemConfig's hash % numFreqSlots (the validator's sibling modulo was fixed earlier but this one was still unguarded). - Enforce the primary-channel invariant in Channels::onConfigChanged: a config demoting every slot now re-promotes the stale SECONDARY slot (keeping its key) or restores the default channel if the slot is DISABLED, instead of leaving every getPrimaryIndex() reader on a non-primary slot. The getKey recursion guard stays as defense-in-depth. Tests: - New test/support/MockMeshService.h and AdminModuleTestShim.h replace four byte-identical mocks and three divergent admin shims (test_mqtt's capturing mock is genuinely different and stays). - DeterministicRng.h: add rngFill() (replaces 14 hand-rolled fill loops) and rngEdgeNodeNum() (unifies the three NodeNum boundary pools). - Extract fuzzChannelSettings() shared by the set_channel case and fuzzBeacon. - fuzzBeacon: the un-terminated branch now fills the whole buffer with non-NUL bytes so the strnlen bound is actually stressed (~50% of iterations, not ~4%). - E6 beacon fuzz: replace the TEST_ASSERT_TRUE(true) tautology with real invariants (handler never consumes; offers land in lastReceivedOffer keyed to the sender). - Trim the seven over-long comment blocks flagged against the 1-2 line rule; the FINDINGS trailer moves to this commit message (see production notes). Full native suite GREEN 27/27 under the coverage (ASan/LSan) env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Clamp UTF-8 char length in the emote walkers; add test_fuzz_emotes A TEXT_MESSAGE payload is opaque protobuf bytes, so PB_VALIDATE_UTF8 never screens it - invalid UTF-8 and truncated multi-byte lead bytes reach the emote/width render path verbatim. EmoteRenderer's walkers advanced by utf8CharLen(lead) without clamping to the bytes actually remaining, so a truncated lead (e.g. a lone 0xF0, which claims 4 bytes) near the end of the buffer made getUtf8ChunkWidth's memcpy read past the string. ASan confirms a heap-buffer-overflow READ from measureStringWithEmotes. Add utf8CharLenClamped() and use it at every walk site (width measure, truncation cut-loop, and the draw-path text-run/chunk builders); the one already-guarded site (matchAtIgnoringModifiers) is unchanged. New test/test_fuzz_emotes drives measureStringWithEmotes and truncateToWidth over adversarial byte strings (biased to embed/end in truncated multi-byte leads) in exact-sized heap buffers so any over-read is a hard ASan fault. Its headless display uses a synthetic font (firstChar 0, fontData centered in a large buffer) so the stock OLEDDisplay::getStringWidth - which indexes the font jump table with a signed char and over-reads for any byte >= 0x80 - does not mask the finding. native-suite-count bumped 27 -> 28. Full native suite GREEN 28/28 under the coverage (ASan/LSan) env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep emote width measurement in-bounds for non-ASCII bytes OLEDDisplay::getStringWidth (the utf8=false path EmoteRenderer uses on default builds) indexes the font jump table by (c - firstChar) with a signed char and no bounds check, so any byte outside printable ASCII - high bytes from UTF-8 text, but also a stray control byte like 0x0A - reads outside the font array. On-device this reads adjacent flash and returns a garbage width; under ASan the test_fuzz_emotes fuzzer flags it as a global-buffer-overflow, and it made the non-ASCII width measurement meaningless either way. The OLED driver is a pinned upstream dependency, so guard it firmware-side in EmoteRenderer's getStringWidth helper: measure a sanitized copy where any byte outside [0x20, 0x7E] counts as a '?' placeholder. Printable ASCII is unchanged and the UA/RU lookup path is untouched. test_fuzz_emotes now drives a real ArialMT font instead of the synthetic in-bounds font it needed before this fix, so the suite exercises the true production width path (utf8CharLen clamp + this sanitizer) end to end. The same fuzzer tripped the global-buffer-overflow before this change. Full native suite GREEN 28/28 under the coverage (ASan/LSan) env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3becaf2d95 | emdashes begone (#10847) | ||
|
|
dd1ec9d462 |
Lora region preset map (#10736)
* Added lora region and preset maps
* Protos
* Address PR review feedback
- Log (and break/skip) when the region preset map exceeds its array bounds
instead of silently dropping regions
- Derive test bounds from the generated nanopb array sizes via sizeof()
instead of hard-coded magic numbers
- Fix want_config sequence comment (missing comma, STATE_SEND_MODULECONFIG)
- Specify a language on the spec's fenced code blocks (markdownlint MD040)
* Fix want_config stall: handle STATE_SEND_REGION_PRESETS in PhoneAPI::available()
available() had a separate per-state switch that wasn't updated for the new
state, so it returned false ('unexpected state 5') and getFromRadio() was
never called - the config handshake stalled after metadata and the client
timed out. Verified via the native simulator integration test.
|
||
|
|
ef51c7ec11 |
Add 2 Meter (~144mhz) Amateur Radio Regions (#10623)
Default slots: ITU1_2M: Slot 26 (144.510 MHz) ITU2_2M: Slot 51 (145.010 MHz) ITU3_2M: Slot 33 (144.650 MHz) |
||
|
|
3e873c51b7 | Add TinyFast and TinySlow presets to modem configuration and menu actions (#10597) | ||
|
|
cbfa8d7ffd |
Add low bandwidth conversions to MeshRadio (#10595)
* Add low bandwidth conversions to MeshRadio * Add test cases for new bandwidth conversion values (8/10/16/21/42) and round-trip tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> |
||
|
|
222d3e6b62 |
Fix zero CR and add unit tests for applyModemConfig coding rate behavior (#10070)
* Fix zero CR and add unit tests for applyModemConfig coding rate behavior * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: initialize region configuration in setUp for RadioInterface tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |
||
|
|
22d63fa69c |
Lora settings expansion and validation logic improvement (#9878)
* Enhance LoRa configuration with modem presets and validation logic * Rename bootstrapLoRaConfigFromPreset tests to validateModemConfig for clarity and consistency * additional tidy-ups to the validateModemConfig - still fundamentally broken at this point * Enhance region validation by adding numPresets to RegionInfo and implementing validateRegionConfig in RadioInterface * Add validation for modem configuration in applyModemConfig * Fix region unset handling and improve modem config validation in handleSetConfig * Refactor LoRa configuration validation methods and introduce clamping method for invalid settings * Update handleSetConfig to use fromOthers parameter to either correct or reject invalid settings * Fix some of the copilot review comments for LoRa configuration validation and clamping methods; add tests for region and preset handling * Redid the slot default checking and calculation. Should resolve the outstanding comments. * Add bandwidth calculation for LoRa modem preset fallback in clampConfigLora * Remove unused preset name variable in validateConfigLora and fix default frequency slot check in applyModemConfig * update tests for region handling * Got the synthetic colleague to add mock service for testing * Flash savings... hopefully * Refactor modem preset handling to use sentinel values and improve default preset retrieval * Refactor region handling to use profile structures for modem presets and channel calculations * added comments for clarity on parameters * Add shadow table tests and validateConfigLora enhancements for region presets * Add isFromUs tests for preset validation in AdminModule * Respond to copilot github review * address copilot comments * address null poointers * fix build errors * Fix the fix, undo the silly suggestions from synthetic reviewer. * we all float here * Fix include path for AdminModule in test_main.cpp * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * More suggestion fixes * admin module merge conflicts * admin module fixes from merge hell * fix: initialize default frequency slot and custom channel name; update LNA mode handling * save some bytes... * fix: simplify error logging for bandwidth checks in LoRa configuration * Update src/mesh/MeshRadio.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
8894a0b711 |
Consolidate LoRa params / preset logic and fix display of preset values (#9413)
* Consolidate LoRa params / preset logic and fix display of preset values * Move preset coercion logic to RadioInterface * Fix some warnings * Fix warnings * STM32 fix * Add unit tests * Update src/mesh/RadioInterface.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |