Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ build_flags = -Wno-missing-field-initializers
-DRADIOLIB_EXCLUDE_FSK4=1
-DRADIOLIB_EXCLUDE_APRS=1
-DRADIOLIB_EXCLUDE_LORAWAN=1
-DRADIOLIB_GODMODE=1
-DMESHTASTIC_EXCLUDE_DROPZONE=1
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
Expand Down
7 changes: 7 additions & 0 deletions src/mesh/LR11x0Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,11 @@ template <typename T> bool LR11x0Interface<T>::sleep()

return true;
}

template <typename T> int16_t LR11x0Interface<T>::getCurrentRSSI()
{
float rssi = -110;
lora.getRssiInst(&rssi);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return value should be checked instead of always assuming success

return (int16_t)round(rssi);
}
#endif
2 changes: 2 additions & 0 deletions src/mesh/LR11x0Interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ template <class T> class LR11x0Interface : public RadioLibInterface
*/
T lora;

int16_t getCurrentRSSI() override;

/**
* Glue functions called from ISR land
*/
Expand Down
6 changes: 6 additions & 0 deletions src/mesh/RF95Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,10 @@ bool RF95Interface::sleep()

return true;
}

int16_t RF95Interface::getCurrentRSSI()
{
float rssi = lora->getRSSI(false);
return (int16_t)round(rssi);
}
#endif
2 changes: 2 additions & 0 deletions src/mesh/RF95Interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class RF95Interface : public RadioLibInterface
*/
virtual void disableInterrupt() override;

int16_t getCurrentRSSI() override;

/**
* Enable a particular ISR callback glue function
*/
Expand Down
6 changes: 6 additions & 0 deletions src/mesh/RadioInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ class RadioInterface
*/
virtual void saveChannelNum(uint32_t savedChannelNum);

/**
* Get current RSSI reading from the radio.
* Returns 0 if not available.
*/
virtual int16_t getCurrentRSSI() { return 0; }

private:
/**
* Convert our modemConfig enum into wf, sf, etc...
Expand Down
110 changes: 104 additions & 6 deletions src/mesh/RadioLibInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "PortduinoGlue.h"
#include "meshUtils.h"
#endif

void LockingArduinoHal::spiBeginTransaction()
{
spiLock->lock();
Expand All @@ -28,6 +29,7 @@ void LockingArduinoHal::spiEndTransaction()

spiLock->unlock();
}

#if ARCH_PORTDUINO
void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in)
{
Expand All @@ -40,6 +42,12 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
: NotifiedWorkerThread("RadioIf"), module(hal, cs, irq, rst, busy), iface(_iface)
{
instance = this;

// Initialize noise floor samples array with 0
for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) {
noiseFloorSamples[i] = 0;
}

#if defined(ARCH_STM32WL) && defined(USE_SX1262)
module.setCb_digitalWrite(stm32wl_emulate_digitalWrite);
module.setCb_digitalRead(stm32wl_emulate_digitalRead);
Expand Down Expand Up @@ -246,6 +254,94 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id)
return txQueue.find(from, id);
}

void RadioLibInterface::updateNoiseFloor()
{
// Only sample when the radio is not actively transmitting or receiving
// This allows sampling both when truly idle and after transmitting (when isReceiving may be false)
bool busyTx = sendingPacket != NULL;
bool busyRx = isReceiving && isActivelyReceiving();

if (busyTx || busyRx) {
return;
}

// Also check for pending interrupts
if (isIRQPending()) {
return;
}

// Rate limit updates
uint32_t now = millis();
if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) {
return;
}
lastNoiseFloorUpdate = now;

// Get current RSSI from the radio
int16_t rssi = getCurrentRSSI();

if (rssi >= 0 || rssi < NOISE_FLOOR_MIN) {
LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi);
return;
}

// Store the sample in the rolling window
noiseFloorSamples[currentSampleIndex] = (int32_t)rssi;
currentSampleIndex++;

// Wrap around when we reach the buffer size - this creates the rolling window
if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) {
currentSampleIndex = 0;
isNoiseFloorBufferFull = true;
}

// Calculate the new average using the rolling window
currentNoiseFloor = getAverageNoiseFloor();

LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCount(), rssi);
}

int32_t RadioLibInterface::getAverageNoiseFloor()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

{
uint8_t sampleCount = getNoiseFloorSampleCount();

if (sampleCount == 0) {
return 0; // Return 0 if no samples
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}

int32_t sum = 0;

// Calculate sum using the rolling window
if (isNoiseFloorBufferFull) {
// Buffer is full - sum all samples
for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) {
sum += noiseFloorSamples[i];
}
} else {
// Buffer not yet full - sum only collected samples
for (uint8_t i = 0; i < currentSampleIndex; i++) {
sum += noiseFloorSamples[i];
}
}

int32_t average = sum / sampleCount;

// Clamp to minimum of -120 dBm
if (average < NOISE_FLOOR_MIN) {
average = NOISE_FLOOR_MIN;
}

return average;
}

void RadioLibInterface::resetNoiseFloor()
{
currentSampleIndex = 0;
isNoiseFloorBufferFull = false;
currentNoiseFloor = NOISE_FLOOR_MIN;
LOG_INFO("Noise floor reset - rolling window collection will restart");
}

/** radio helper thread callback.
We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of
'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision.
Expand All @@ -255,6 +351,7 @@ currently active.
*/
void RadioLibInterface::onNotify(uint32_t notification)
{

switch (notification) {
case ISR_TX:
handleTransmitInterrupt();
Expand Down Expand Up @@ -386,11 +483,6 @@ bool RadioLibInterface::removePendingTXPacket(NodeNum from, PacketId id, uint32_
return false;
}

/**
* Remove a packet that is eligible for replacement from the TX queue
*/
// void RadioLibInterface::removePending

void RadioLibInterface::handleTransmitInterrupt()
{
// This can be null if we forced the device to enter standby mode. In that case
Expand Down Expand Up @@ -419,6 +511,9 @@ void RadioLibInterface::completeSending()

// We are done sending that packet, release it
packetPool.release(p);

// Update noise floor after transmitting (radio is now in a good state to sample)
updateNoiseFloor();
Comment on lines +515 to +516
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)".

Copilot uses AI. Check for mistakes.
Copy link
Member

@GUVWAF GUVWAF Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}
}

Expand Down Expand Up @@ -516,6 +611,9 @@ void RadioLibInterface::startReceive()
{
isReceiving = true;
powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn);

// Opportunistically update noise floor when entering receive mode
updateNoiseFloor();
}

void RadioLibInterface::configHardwareForSend()
Expand Down Expand Up @@ -563,4 +661,4 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)

return res == RADIOLIB_ERR_NONE;
}
}
}
52 changes: 51 additions & 1 deletion src/mesh/RadioLibInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,45 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
/// are _trying_ to receive a packet currently (note - we might just be waiting for one)
bool isReceiving = false;

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

int32_t noiseFloorSamples[NOISE_FLOOR_SAMPLES];
uint8_t currentSampleIndex = 0;
bool isNoiseFloorBufferFull = false;
uint32_t lastNoiseFloorUpdate = 0;
static const uint32_t NOISE_FLOOR_UPDATE_INTERVAL_MS = 5000;
int32_t currentNoiseFloor = NOISE_FLOOR_MIN;

/**
* Update the noise floor measurement by sampling RSSI when receiving
* Uses a rolling window approach to maintain recent samples
*/
void updateNoiseFloor();

/**
* Override from NotifiedWorkerThread - called periodically by the thread
*/
virtual int16_t getCurrentRSSI() = 0;

public:
/** Our ISR code currently needs this to find our active instance
*/
static RadioLibInterface *instance;

/**
* Get the current calculated noise floor in dBm
* Returns -120 dBm if not yet calibrated
*/
int32_t getNoiseFloor() { return currentNoiseFloor; }

/**
* Calculate the average noise floor from collected samples
* Clamps result to minimum of -120 dBm
*/
int32_t getAverageNoiseFloor();

/**
* Glue functions called from ISR land
*/
Expand Down Expand Up @@ -158,6 +192,22 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
virtual bool findInTxQueue(NodeNum from, PacketId id) override;

/**
* Check if we have collected any noise floor samples
*/
bool hasNoiseFloorSamples() { return isNoiseFloorBufferFull || currentSampleIndex > 0; }

/**
* Get the number of samples in the rolling window
*/
uint8_t getNoiseFloorSampleCount() { return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex; }

/**
* Reset the noise floor calibration
* Will automatically restart collection
*/
void resetNoiseFloor();

private:
/** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually
* doing the transmit */
Expand Down Expand Up @@ -264,4 +314,4 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
*/

bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override;
};
};
6 changes: 6 additions & 0 deletions src/mesh/SX126xInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ template <typename T> bool SX126xInterface<T>::reconfigure()
return RADIOLIB_ERR_NONE;
}

template <typename T> int16_t SX126xInterface<T>::getCurrentRSSI()
{
float rssi = lora.getRSSI(false);
return (int16_t)round(rssi);
}

template <typename T> void SX126xInterface<T>::disableInterrupt()
{
lora.clearDio1Action();
Expand Down
2 changes: 2 additions & 0 deletions src/mesh/SX126xInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ template <class T> class SX126xInterface : public RadioLibInterface
*/
T lora;

int16_t getCurrentRSSI() override;

/**
* Glue functions called from ISR land
*/
Expand Down
6 changes: 6 additions & 0 deletions src/mesh/SX128xInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,10 @@ template <typename T> bool SX128xInterface<T>::sleep()

return true;
}

template <typename T> int16_t SX128xInterface<T>::getCurrentRSSI()
{
float rssi = lora.getRSSI(false);
return (int16_t)round(rssi);
}
#endif
2 changes: 2 additions & 0 deletions src/mesh/SX128xInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ template <class T> class SX128xInterface : public RadioLibInterface
*/
T lora;

int16_t getCurrentRSSI() override;

/**
* Glue functions called from ISR land
*/
Expand Down
7 changes: 5 additions & 2 deletions src/modules/Telemetry/DeviceTelemetry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

int32_t DeviceTelemetryModule::runOnce()
{

refreshUptime();
bool isImpoliteRole =
IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_SENSOR, meshtastic_Config_DeviceConfig_Role_ROUTER);
Expand Down Expand Up @@ -118,6 +119,7 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry()
telemetry.variant.local_stats.num_online_nodes = numOnlineNodes;
telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes();
if (RadioLibInterface::instance) {
telemetry.variant.local_stats.noise_floor = RadioLibInterface::instance->getAverageNoiseFloor();
telemetry.variant.local_stats.num_packets_tx = RadioLibInterface::instance->txGood;
telemetry.variant.local_stats.num_packets_rx = RadioLibInterface::instance->rxGood + RadioLibInterface::instance->rxBad;
telemetry.variant.local_stats.num_packets_rx_bad = RadioLibInterface::instance->rxBad;
Expand All @@ -141,10 +143,11 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry()
telemetry.variant.local_stats.num_tx_relay_canceled = router->txRelayCanceled;
}

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",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

telemetry.variant.local_stats.noise_floor is int32_t, so %d should be used

telemetry.variant.local_stats.uptime_seconds, telemetry.variant.local_stats.channel_utilization,
telemetry.variant.local_stats.air_util_tx, telemetry.variant.local_stats.num_online_nodes,
telemetry.variant.local_stats.num_total_nodes);
telemetry.variant.local_stats.num_total_nodes, telemetry.variant.local_stats.noise_floor);

LOG_INFO("num_packets_tx=%i, num_packets_rx=%i, num_packets_rx_bad=%i", telemetry.variant.local_stats.num_packets_tx,
telemetry.variant.local_stats.num_packets_rx, telemetry.variant.local_stats.num_packets_rx_bad);
Expand Down
6 changes: 6 additions & 0 deletions src/platform/portduino/SimRadio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,10 @@ uint32_t SimRadio::getPacketTime(uint32_t pl, bool received)

uint32_t msecs = tPacket * 1000;
return msecs;
}

int16_t SimRadio::getCurrentRSSI()
{
// Simulated radio - return a reasonable default noise floor
return -120;
}
Loading