diff --git a/platformio.ini b/platformio.ini index 77e9cf214a6..4c591821407 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 7c73b56cd41..bbba8dac01b 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -317,4 +317,11 @@ template bool LR11x0Interface::sleep() return true; } + +template int16_t LR11x0Interface::getCurrentRSSI() +{ + float rssi = -110; + lora.getRssiInst(&rssi); + return (int16_t)round(rssi); +} #endif diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 840184bbf60..8a9bec2e145 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -33,6 +33,8 @@ template class LR11x0Interface : public RadioLibInterface */ T lora; + int16_t getCurrentRSSI() override; + /** * Glue functions called from ISR land */ diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 0c12401ca9f..f354e53f65c 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -341,4 +341,10 @@ bool RF95Interface::sleep() return true; } + +int16_t RF95Interface::getCurrentRSSI() +{ + float rssi = lora->getRSSI(false); + return (int16_t)round(rssi); +} #endif \ No newline at end of file diff --git a/src/mesh/RF95Interface.h b/src/mesh/RF95Interface.h index ffd8ae0082b..2226067646e 100644 --- a/src/mesh/RF95Interface.h +++ b/src/mesh/RF95Interface.h @@ -37,6 +37,8 @@ class RF95Interface : public RadioLibInterface */ virtual void disableInterrupt() override; + int16_t getCurrentRSSI() override; + /** * Enable a particular ISR callback glue function */ diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index cb092bc6de1..9a139b559b7 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -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... diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 80e51b8bcbd..6ef3d0e2542 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -15,6 +15,7 @@ #include "PortduinoGlue.h" #include "meshUtils.h" #endif + void LockingArduinoHal::spiBeginTransaction() { spiLock->lock(); @@ -28,6 +29,7 @@ void LockingArduinoHal::spiEndTransaction() spiLock->unlock(); } + #if ARCH_PORTDUINO void LockingArduinoHal::spiTransfer(uint8_t *out, size_t len, uint8_t *in) { @@ -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); @@ -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() +{ + uint8_t sampleCount = getNoiseFloorSampleCount(); + + if (sampleCount == 0) { + return 0; // Return 0 if no samples + } + + 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. @@ -255,6 +351,7 @@ currently active. */ void RadioLibInterface::onNotify(uint32_t notification) { + switch (notification) { case ISR_TX: handleTransmitInterrupt(); @@ -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 @@ -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(); } } @@ -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() @@ -563,4 +661,4 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) return res == RADIOLIB_ERR_NONE; } -} \ No newline at end of file +} diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 833c887108e..a3ef28edb6c 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -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 + 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 */ @@ -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 */ @@ -264,4 +314,4 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; -}; +}; \ No newline at end of file diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 9dfc46bee9d..60270516041 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -256,6 +256,12 @@ template bool SX126xInterface::reconfigure() return RADIOLIB_ERR_NONE; } +template int16_t SX126xInterface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(false); + return (int16_t)round(rssi); +} + template void SX126xInterface::disableInterrupt() { lora.clearDio1Action(); diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index b8f16ac6da9..02649b4ebc8 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -39,6 +39,8 @@ template class SX126xInterface : public RadioLibInterface */ T lora; + int16_t getCurrentRSSI() override; + /** * Glue functions called from ISR land */ diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 9fcedfe4951..fbd35f14826 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -325,4 +325,10 @@ template bool SX128xInterface::sleep() return true; } + +template int16_t SX128xInterface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(false); + return (int16_t)round(rssi); +} #endif \ No newline at end of file diff --git a/src/mesh/SX128xInterface.h b/src/mesh/SX128xInterface.h index acdcbbb27c8..cf44bcb89ce 100644 --- a/src/mesh/SX128xInterface.h +++ b/src/mesh/SX128xInterface.h @@ -35,6 +35,8 @@ template class SX128xInterface : public RadioLibInterface */ T lora; + int16_t getCurrentRSSI() override; + /** * Glue functions called from ISR land */ diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 066b9361d69..52eab322157 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -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); @@ -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; @@ -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", 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); diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 6e7fe24cbaf..f9fabb6170d 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -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; } \ No newline at end of file