Conversation
There was a problem hiding this comment.
Pull request overview
This pull request implements noise floor calculation for Meshtastic firmware by sampling the radio's RSSI when idle, maintaining a sliding window average of up to 20 measurements, and exposing the value through local stats telemetry.
Changes:
- Added noise floor tracking infrastructure to RadioLibInterface with rolling window averaging
- Implemented getCurrentRSSI() for all radio chip variants (SX126x, SX128x, LR11x0, RF95, SimRadio)
- Integrated noise floor measurement into telemetry module for reporting
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/mesh/RadioLibInterface.h | Adds noise floor tracking member variables, methods declarations, and getCurrentRSSI() pure virtual method |
| src/mesh/RadioLibInterface.cpp | Implements updateNoiseFloor(), getAverageNoiseFloor(), and resetNoiseFloor() with rolling window logic |
| src/mesh/SX126xInterface.h/cpp | Implements getCurrentRSSI() for SX126x radios |
| src/mesh/SX128xInterface.h/cpp | Implements getCurrentRSSI() for SX128x radios |
| src/mesh/LR11x0Interface.h/cpp | Implements getCurrentRSSI() for LR11x0 radios |
| src/mesh/RF95Interface.h/cpp | Implements getCurrentRSSI() for RF95 radios |
| src/mesh/RadioInterface.h | Adds default getCurrentRSSI() implementation returning 0 |
| src/platform/portduino/SimRadio.cpp | Implements getCurrentRSSI() for simulated radio returning -120 dBm |
| src/modules/Telemetry/DeviceTelemetry.cpp | Integrates noise floor value into local stats telemetry and logging |
| // Update noise floor after transmitting (radio is now in a good state to sample) | ||
| updateNoiseFloor(); |
There was a problem hiding this comment.
Logic issue: After transmitting, sendingPacket is set to NULL at line 498, and then updateNoiseFloor() is called at line 514. However, updateNoiseFloor() checks if sendingPacket != NULL at line 265 and returns early. This means the noise floor will never be updated after transmitting, contradicting the comment that says "Update noise floor after transmitting (radio is now in a good state to sample)".
There was a problem hiding this comment.
Well, if sendingPacket == NULL then it doesn't return early.
But indeed I don't think we should do it here, we should first return to receive mode as fast as possible after transmitting.
src/mesh/RadioLibInterface.cpp
Outdated
|
|
||
| updateNoiseFloor(); |
There was a problem hiding this comment.
Performance concern: updateNoiseFloor() is called at the beginning of every onNotify() callback (line 351), which is invoked for every ISR event (ISR_TX, ISR_RX, TRANSMIT_DELAY_COMPLETED). This could be inefficient. The function already has rate limiting (5 second interval), but it still performs multiple checks on every notification. Consider moving the call to only the ISR_RX case or after startReceive() calls where it's most relevant.
There was a problem hiding this comment.
Copilot has a valid comment here. We shouldn't call this here, because every delay added here can cause the LoRa radio queue to get full (we have to read it out immediately because it can hold only 256 bytes). Doing it after entering receive mode is fine, and a period call e.g. at the end of Router::runOnce() or something.
src/mesh/RadioLibInterface.cpp
Outdated
| // Get current RSSI from the radio | ||
| int16_t rssi = getCurrentRSSI(); | ||
|
|
||
| if (rssi == 0 || rssi < NOISE_FLOOR_MIN) { |
There was a problem hiding this comment.
Validation logic issue: The condition checks rssi == 0 || rssi < NOISE_FLOOR_MIN to skip invalid readings. However, RSSI values are typically negative (e.g., -120 to -30 dBm), so checking for rssi == 0 might be too strict. A value of 0 dBm is technically valid though extremely unlikely. Consider whether this check is necessary, or if it should instead check for positive values which would truly indicate an error.
| if (rssi == 0 || rssi < NOISE_FLOOR_MIN) { | |
| if (rssi > 0 || rssi < NOISE_FLOOR_MIN) { |
…nter - Use robust busyTx/busyRx checks instead of simple isReceiving check - Initialize noiseFloorSamples to NOISE_FLOOR_MIN instead of 0 - Move noise_floor assignment inside null check to prevent potential crash - Change getNoiseFloor() and getAverageNoiseFloor() to return int32_t - Fix RSSI validation to check for positive values (rssi > 0) - Fix format specifier from %.1f to %d for int32_t - Update comments to accurately reflect the sampling logic
|
Very interesting! We're not measuring Noise Floor (NF) in the sense of just random (stationary random process) background: thermal, amplifier, etc. We're really interested in interference (potentially non-LoRa as well as LoRa, though excluding samples where a receive preamble is already detected by the radio). It might be interesting to extract other statistics of these NF samples, not just the average. For example: if we also extract the 10th-percentile and 90th-percentile values, this could tell us whether the interference is present all the time (10th and 90th close together), versus occasional (10th and 90th far apart). If they're far apart, it might actually inform us to delay a packet transmit if we measure a |
|
Another thought: about how long does the Assuming it's quick, have you looked at taking a burst of consecutive samples (say, 10) and averaging them before storing in the array? (Maybe not just averaging the 10 samples as dBm values, but instead converting them to linear power mW values, averaging in mW, and then converting back to dBm.) This might give a cleaner short-term NF measurement which then goes into the longer-term array. |
|
any update? |
|
Hi can provide Seeed Xiao Esp32, LILYGO T3v1.6.1, Heltec v4 and Heltec Pocket for testing. |
|
Hi, this is a super cool feature. It would help a lot with installations in locations with high noise. But it would be possible to know this ahead of time, instead of hitting it during debugging why a node is deaf… it would be super cool to see in the app. cheers, |
Per PR review feedback, calling updateNoiseFloor() in onNotify() for every ISR event (ISR_TX, ISR_RX, TRANSMIT_DELAY_COMPLETED) can cause the LoRa radio queue to get full. The noise floor sampling still happens in startReceive() and after transmitting.
|
@GUVWAF I had to enable "GodMode" to access that fucntion for lr11 |
|
@RCGV1 Ah, I see. We shouldn't do that, I'll see if I can make that a public method in RadioLib. |
| protected: | ||
| // Noise floor tracking - rolling window of samples | ||
| static const uint8_t NOISE_FLOOR_SAMPLES = 20; | ||
| static const int32_t NOISE_FLOOR_MIN = -120; // Minimum noise floor clamp in dBm |
There was a problem hiding this comment.
Why clamping to -120dBm? If there is only thermal noise, at 0 degrees Celsius and with a small bandwidth (62.5kHz), you can go down to -126dBm.
There was a problem hiding this comment.
When testing with the LR1110, I noticed that a value of -128 is returned when it is invalid (i.e. not in receive mode while calling the function). Hence I would say let's check for this specific value, and not clamp it to -120.
|
@RCGV1 For LR11x0, when we update RadioLib to include jgromes/RadioLib#1714, we can use For the rest, please see my comment on clamping. Furthermore, we should still remove updating the noise floor from here, as we need the radio to go the receive mode as fast as possible in order not to lose packets. Moreover, at least for the LR11x0, it is necessary to have it in receive mode before doing the RSSI measurement. Lastly, in |
|
|
||
| LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i", | ||
| LOG_INFO("Sending local stats: uptime=%i, channel_utilization=%f, air_util_tx=%f, num_online_nodes=%i, num_total_nodes=%i, " | ||
| "noise_floor=%f", |
There was a problem hiding this comment.
telemetry.variant.local_stats.noise_floor is int32_t, so %d should be used
| uint8_t sampleCount = getNoiseFloorSampleCount(); | ||
|
|
||
| if (sampleCount == 0) { | ||
| return 0; // Return 0 if no samples |
There was a problem hiding this comment.
API/docs state uncalibrated should be -120 dBm (src/mesh/RadioLibInterface.h:129). This leaks impossible telemetry (0 dBm) via src/modules/Telemetry/DeviceTelemetry.cpp:122 and hurts data accuracy for fresh boot / low-traffic nodes.
| RadioLibInterface::configHardwareForSend(); | ||
| } | ||
|
|
||
| void RF95Interface::startReceive() |
There was a problem hiding this comment.
RF95Interface::startReceive() does not call RadioLibInterface::startReceive(), so the new noise-floor sampling hook in base (src/mesh/RadioLibInterface.cpp:616) is bypassed on RF95. Result: RF95 devices may never refresh noise floor except after TX-complete path, which biases or stalls measurements on RX-heavy nodes.
| LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCount(), rssi); | ||
| } | ||
|
|
||
| int32_t RadioLibInterface::getAverageNoiseFloor() |
There was a problem hiding this comment.
Noise-floor buffer/index state is read and written without synchronization (getAverageNoiseFloor() vs updateNoiseFloor()), and telemetry calls into this from a different module thread (src/modules/Telemetry/DeviceTelemetry.cpp:122).
| template <typename T> int16_t LR11x0Interface<T>::getCurrentRSSI() | ||
| { | ||
| float rssi = -110; | ||
| lora.getRssiInst(&rssi); |
There was a problem hiding this comment.
return value should be checked instead of always assuming success
I implemented a noise floor calculation by getting the radios current RSSI if its not transmitting nor receiving, this value is then added to a sliding window average which can hold up to 20 measurements. Noise Floor is sent via local stats.
This is extremely useful to determine if a cavity filter is needed for a particular location.
Depends on:
meshtastic/protobufs#846
iOS Interface:

🤝 Attestations