Add RSSI-based listen-before-talk - #3383
Open
jirogit wants to merge 4 commits into
Open
Conversation
Hardware CAD only detects a LoRa preamble, so it is blind to any other modulation sharing the band. int.thresh does sample RSSI, but relative to the tracked noise floor and only once, which cannot express an absolute threshold or a sensing window. Add a third, independent channel-busy check to isChannelActive(): while enabled, sample getCurrentRSSI() continuously for sense_ms and report the channel busy if any sample exceeds an absolute dBm threshold. The loop always takes at least one sample, so sense_ms of 0 degrades to a single instantaneous reading rather than to no reading at all. The two existing checks are untouched and the return value stays a plain bool, so isReceiving() remains the single gate that Dispatcher::checkSend() consults. CAD and this check are fully independent; either, both, or neither may be active. Also enforce an optional quiet period after each transmit in onSendFinished(). delay() is used rather than a bare spin so the pause yields to the scheduler. Nothing calls setRssiLbtParams() yet, so this commit is inert on its own: the mechanism stays disabled until the prefs are wired up.
Add the six parameters to CommonRadioPrefs, which both NodePrefs implementations derive from, so companion, repeater, room server and sensor are all covered by one implementation: set rssi.lbt on|off set rssi.lbt.params <thr_dBm>,<sense_ms>,<maxwait_ms>,<txmax_ms>,<pause_ms> get rssi.lbt get rssi.lbt.params -> "> -80,5,0,4000,50" The params are comma separated in both directions, following `set radio` and `get radio`, so the output of the getter can be pasted straight back into the setter. Defaults are chosen so that a single `set rssi.lbt on` brings up a usable configuration. Dispatcher pushes the sensing parameters down to the radio from the same place it already refreshes the noise floor threshold and the CAD flag. Only thr_dbm, sense_ms and pause_ms are the radio's concern; maxwait_ms and txmax_ms are acted on by the dispatcher itself. The legacy binary prefs loader is deliberately left alone: these fields cannot exist in a file written by a build that predates them, so such a file simply leaves them at their defaults.
Two dispatcher-side limits, both gated on rssi.lbt being enabled so that the default-off path is byte-for-byte the previous behaviour: txmax_ms caps the airtime of a single transmit. A packet whose estimated airtime exceeds the cap is dropped just before startSendRaw(), reusing the existing send-failure cleanup. There is no counter for this; the drop is only visible through MESH_DEBUG_PRINTLN, so release builds stay silent. maxwait_ms feeds getCADFailMaxDuration(), which gates the same busy check in checkSend() as CAD's own fixed 4000ms wait. With rssi.lbt off, the function is untouched -- still a hard-coded 4000ms. With rssi.lbt on, its max-wait applies; if CAD is also enabled, the stricter (longer) of the two wins, since CAD's fixed wait must never shorten a wait rssi.lbt asked to be longer. A max-wait of zero means "never force the transmit", which is stricter than any finite value, hence the normalisation to the largest representable duration before comparing. The effective value is recomputed on every call rather than stored, so a derived number can never end up in prefs and disagree with what the CLI reports.
Follows the layout of the existing cad section. Spells out that rssi.lbt is independent of cad and int.thresh, that all of rssi.lbt.params is ignored while rssi.lbt is off, the 0-means-unlimited convention on each field that has it, and how its max-wait interacts with CAD's fixed 4000ms wait in getCADFailMaxDuration().
jirogit
added a commit
to jirogit/MeshCore
that referenced
this pull request
Sep 9, 2026
Updated the PR reference for the Japanese build to meshcore-dev#3383.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
MeshCore has two pre-transmit channel checks today, and neither can express the requirement this PR addresses.
caduses the radio's hardware Channel Activity Detection, which looks for a LoRa preamble. In a band shared with equipment using other modulations, that is exactly the traffic it cannot see.int.threshdoes sample RSSI, but relative to the tracked noise floor, and only once. A deployment that has to hold to an absolute dBm threshold, sense the channel for a defined window rather than instantaneously, or stay quiet for a fixed period after each transmit, has no way to say so.This PR adds RSSI-based listen-before-talk: modulation-agnostic energy detection with an absolute threshold and a configurable sensing window, plus three transmit-shaping limits that go with it.
What's new
set rssi.lbt on|off
get rssi.lbt -> "> on" / "> off"
set rssi.lbt.params <thr_dBm>,<sense_ms>,<maxwait_ms>,<txmax_ms>,<pause_ms>
get rssi.lbt.params -> "> -80,5,0,4000,50"
get rssi.lbt.paramsreports the values in the same ordersettakes them and with the same separator, so its output can be pasted straight back intoset— following the existingget radio/set radiopair. Its arguments are:thr_dBm-80sense_ms50takes a single instantaneous readingmaxwait_ms00= unlimitedtxmax_ms40000= unlimitedpause_ms50Throughout,
0means "unlimited". The defaults are chosen so that a singleset rssi.lbt onyields a usable configuration without touchingrssi.lbt.params.Behaviour when disabled
This is the first thing worth checking, so to state it plainly:
rssi.lbtisoff— the default — every one of therssi.lbt.paramsvalues is ignored. The sensing loop, the post-transmit pause, the airtime cap and the max-wait are each gated on the enable flag, not merely on their own value being zero. A node that never issues these commands transmits exactly as it does ondev.getCADFailMaxDuration()returns the original hard-coded 4000ms wheneverrssi.lbtis off, whether or notcadis on.Design notes
Independent of CAD. The RSSI check is added as a third branch of
RadioLibWrapper::isChannelActive(), next toint.threshand_cad_enabled. The two existing branches are unchanged and the return value stays a plainbool, soisReceiving()remains the single gateDispatcher::checkSend()consults. Any combination of the three may be active.Max-wait.
rssi.lbt'smaxwait_msand CAD's fixed 4000ms both bound the same busy state incheckSend(). Withrssi.lbtoff the function returns 4000 unchanged. With it on, the stricter — longer — wait applies, so CAD's fixed value never shortens a wait thatrssi.lbtasked to be longer. Since0means "never force the transmit", it is normalised to the largest representable duration before any comparison, so it does not lose to a finite value.The effective value is derived on every call and never written back to prefs, so a computed number cannot end up stored and disagree with what the CLI reports.
Airtime cap. Enforced immediately before
startSendRaw(), reusing the existing send-failure cleanup path. There is no counter for dropped packets; the drop is visible only throughMESH_DEBUG_PRINTLN, so release builds stay silent.One implementation, four node types. The prefs live in
CommonRadioPrefs, which bothNodePrefs::RadioPrefsimplementations derive from, so companion, repeater, room server and sensor are all covered without per-target code.Commits
The PR is one change but is split into four commits that can be taken separately if review stalls on any one of them:
RadioLibWrapper. Inert on its own; nothing enables it yet.Testing
Firmware builds and basic on-device smoke testing:
Nodes come up, advertise and forward packets as before, and with
rssi.lbtleft at its default ofoffI saw no behaviour change from currentdev. This was smoke testing rather than a measured comparison; I have not run the mechanisms under sustained traffic.Open question
pause_msis currently a blockingdelay()inonSendFinished(). At the default of 50ms that seemed acceptable, but I am happy to make it non-blocking if you prefer.