diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 1d1616ed6..9aefb7a8a 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -368,4 +368,10 @@ template bool LR11x0Interface::sleep() return true; } + +template int16_t LR11x0Interface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(false, true); + return (int16_t)round(rssi); +} #endif diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 1a6b92520..ee8761177 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -37,6 +37,8 @@ template class LR11x0Interface : public RadioLibInterface */ T lora; + int16_t getCurrentRSSI() override; + /** * Glue functions called from ISR land */ diff --git a/src/mesh/LR20x0Interface.cpp b/src/mesh/LR20x0Interface.cpp index 699663cf0..b4ada641b 100644 --- a/src/mesh/LR20x0Interface.cpp +++ b/src/mesh/LR20x0Interface.cpp @@ -136,17 +136,6 @@ template bool LR20x0Interface::init() if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; - // Some basic info about the module's explicit firmware version - no other info available - // Currently requires radiolib godmode - -#if RADIOLIB_GODMODE - uint8_t fwMajor = 0; - uint8_t fwMinor = 0; - int versionRes = lora.getVersion(&fwMajor, &fwMinor); - if (versionRes == RADIOLIB_ERR_NONE) - LOG_DEBUG("LR20x0 FW %d.%d", fwMajor, fwMinor); -#endif - LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Power output set to %d", power); @@ -154,17 +143,6 @@ template bool LR20x0Interface::init() if (res == RADIOLIB_ERR_NONE) res = lora.setCRC(2); - // Standard DCDC ramp timing from RadioLib workarounds (register 0x00F20024) - // Currently requires radiolib godmode -#if RADIOLIB_GODMODE - if (res == RADIOLIB_ERR_NONE) { - uint8_t rampTimes[4] = {15, 15, 15, 15}; // Standard case for all conditions - res = lora.setRegMode(RADIOLIB_LR2021_REG_MODE_SIMO_NORMAL, rampTimes); - if (res != RADIOLIB_ERR_NONE) - LOG_WARN("LR2021 setRegMode failed: %d", res); - } -#endif - #ifdef LR2021_DIO_AS_RF_SWITCH bool dioAsRfSwitch = true; #elif defined(ARCH_PORTDUINO) @@ -396,4 +374,10 @@ template bool LR20x0Interface::sleep() return true; } + +template int16_t LR20x0Interface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(false, true); + return (int16_t)round(rssi); +} #endif diff --git a/src/mesh/LR20x0Interface.h b/src/mesh/LR20x0Interface.h index 6176cd0b8..263c83429 100644 --- a/src/mesh/LR20x0Interface.h +++ b/src/mesh/LR20x0Interface.h @@ -37,6 +37,8 @@ template class LR20x0Interface : 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 43149ef8b..65549207d 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 ffd8ae008..222606764 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 a1c692e24..d987ef766 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -283,6 +283,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 ef9a17f8d..2e671bf25 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 unused sample slots to a sane default; sample count controls averaging. + for (uint8_t i = 0; i < NOISE_FLOOR_SAMPLES; i++) { + noiseFloorSamples[i] = NOISE_FLOOR_DEFAULT; + } + #if defined(ARCH_STM32WL) && defined(USE_SX1262) module.setCb_digitalWrite(stm32wl_emulate_digitalWrite); module.setCb_digitalRead(stm32wl_emulate_digitalRead); @@ -246,6 +254,87 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id) return txQueue.find(from, id); } +void RadioLibInterface::updateNoiseFloor() +{ + // Only sample from idle receive mode. TX/RX-critical paths must return to radio work quickly. + if (!isReceiving || sendingPacket != NULL || isActivelyReceiving() || isIRQPending()) { + return; + } + + uint32_t now = millis(); + if (now - lastNoiseFloorUpdate < NOISE_FLOOR_UPDATE_INTERVAL_MS) { + return; + } + lastNoiseFloorUpdate = now; + + int16_t rssi = getCurrentRSSI(); + if (rssi == NOISE_FLOOR_INVALID || rssi >= 0 || rssi < NOISE_FLOOR_VALID_MIN) { + LOG_DEBUG("Skipping invalid RSSI reading: %d", rssi); + return; + } + + noiseFloorSamples[currentSampleIndex] = (int32_t)rssi; + currentSampleIndex++; + + if (currentSampleIndex >= NOISE_FLOOR_SAMPLES) { + currentSampleIndex = 0; + isNoiseFloorBufferFull = true; + } + + currentNoiseFloor = getAverageNoiseFloorInternal(); + + LOG_DEBUG("Noise floor: %d dBm (samples: %d, latest: %d dBm)", currentNoiseFloor, getNoiseFloorSampleCountInternal(), rssi); +} + +uint8_t RadioLibInterface::getNoiseFloorSampleCountInternal() const +{ + return isNoiseFloorBufferFull ? NOISE_FLOOR_SAMPLES : currentSampleIndex; +} + +int32_t RadioLibInterface::getAverageNoiseFloorInternal() const +{ + uint8_t sampleCount = getNoiseFloorSampleCountInternal(); + + if (sampleCount == 0) { + return NOISE_FLOOR_DEFAULT; + } + + int32_t sum = 0; + for (uint8_t i = 0; i < sampleCount; i++) { + sum += noiseFloorSamples[i]; + } + + return sum / sampleCount; +} + +int32_t RadioLibInterface::getAverageNoiseFloor() +{ + return getAverageNoiseFloorInternal(); +} + +int32_t RadioLibInterface::getNoiseFloor() +{ + return currentNoiseFloor; +} + +bool RadioLibInterface::hasNoiseFloorSamples() +{ + return getNoiseFloorSampleCountInternal() > 0; +} + +uint8_t RadioLibInterface::getNoiseFloorSampleCount() +{ + return getNoiseFloorSampleCountInternal(); +} + +void RadioLibInterface::resetNoiseFloor() +{ + currentSampleIndex = 0; + isNoiseFloorBufferFull = false; + currentNoiseFloor = NOISE_FLOOR_DEFAULT; + LOG_INFO("Noise floor reset - rolling window collection will restart"); +} + bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length) { if (!buffer || length == 0 || !iface) { @@ -273,6 +362,7 @@ currently active. */ void RadioLibInterface::onNotify(uint32_t notification) { + switch (notification) { case ISR_TX: handleTransmitInterrupt(); @@ -404,11 +494,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 diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 5b850d675..2bf4d6d3c 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -99,6 +99,26 @@ 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_DEFAULT = -120; + static const int32_t NOISE_FLOOR_VALID_MIN = -127; + static const int32_t NOISE_FLOOR_INVALID = -128; + 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_DEFAULT; + + /** + * Pure virtual hook for derived radio interfaces to provide instantaneous RSSI. + * Implementations should return dBm, or an invalid value that updateNoiseFloor() + * can reject. + */ + virtual int16_t getCurrentRSSI() = 0; + public: /** Our ISR code currently needs this to find our active instance */ @@ -111,6 +131,17 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified instance = nullptr; } + /** + * Get the current calculated noise floor in dBm + * Returns -120 dBm if not yet calibrated + */ + int32_t getNoiseFloor(); + + /** + * Calculate the average noise floor from collected samples + */ + int32_t getAverageNoiseFloor(); + /** * Glue functions called from ISR land */ @@ -179,6 +210,28 @@ 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; + /** + * Update the noise floor measurement by sampling RSSI from a slow path. + * This should not be called from radio interrupt or TX/RX critical paths. + */ + void updateNoiseFloor(); + + /** + * Check if we have collected any noise floor samples + */ + bool hasNoiseFloorSamples(); + + /** + * Get the number of samples in the rolling window + */ + uint8_t getNoiseFloorSampleCount(); + + /** + * Reset the noise floor calibration + * Will automatically restart collection + */ + void resetNoiseFloor(); + /** * Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface. * @return true if len bytes were produced, false otherwise. @@ -186,6 +239,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified bool randomBytes(uint8_t *buffer, size_t length); private: + uint8_t getNoiseFloorSampleCountInternal() const; + int32_t getAverageNoiseFloorInternal() const; + /** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually * doing the transmit */ void setTransmitDelay(); diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index d499e4a8c..920df6000 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -265,6 +265,12 @@ template bool SX126xInterface::reconfigure() return true; } +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 67625e115..b7d3675ab 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -41,6 +41,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 64d71921a..608cd37e0 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 acdcbbb27..cf44bcb89 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 9e0cc50c9..06e220296 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -20,6 +20,7 @@ static constexpr uint16_t TX_HISTORY_KEY_DEVICE_TELEMETRY = 0x8001; int32_t DeviceTelemetryModule::runOnce() { + refreshUptime(); uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_DEVICE_TELEMETRY) : 0; bool isImpoliteRole = isSensorOrRouterRole(); @@ -129,6 +130,8 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() telemetry.variant.local_stats.num_online_nodes = numOnlineNodes; telemetry.variant.local_stats.num_total_nodes = nodeDB->getNumMeshNodes(); if (RadioLibInterface::instance) { + RadioLibInterface::instance->updateNoiseFloor(); + 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; @@ -137,6 +140,8 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() } #ifdef ARCH_PORTDUINO if (SimRadio::instance) { + if (!RadioLibInterface::instance) + telemetry.variant.local_stats.noise_floor = SimRadio::instance->getCurrentRSSI(); telemetry.variant.local_stats.num_packets_tx = SimRadio::instance->txGood; telemetry.variant.local_stats.num_packets_rx = SimRadio::instance->rxGood + SimRadio::instance->rxBad; telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; @@ -152,10 +157,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=%d", 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); @@ -198,4 +204,4 @@ bool DeviceTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) service->sendToMesh(p, RX_SRC_LOCAL, true); } return true; -} \ No newline at end of file +} diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index 6e7fe24cb..f9fabb617 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 diff --git a/src/platform/portduino/SimRadio.h b/src/platform/portduino/SimRadio.h index 6f80989da..43b99e92e 100644 --- a/src/platform/portduino/SimRadio.h +++ b/src/platform/portduino/SimRadio.h @@ -48,6 +48,8 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr // Convert Compressed_msg to normal msg and receive it void unpackAndReceive(meshtastic_MeshPacket &p); + int16_t getCurrentRSSI() override; + /** * Debugging counts */ @@ -93,4 +95,4 @@ class SimRadio : public RadioInterface, protected concurrency::NotifiedWorkerThr virtual uint32_t getPacketTime(uint32_t pl, bool received = false) override; }; -extern SimRadio *simRadio; \ No newline at end of file +extern SimRadio *simRadio; diff --git a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini index d111b011f..82a4e1953 100644 --- a/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini +++ b/variants/nrf52840/diy/nrf52_promicro_diy_tcxo/platformio.ini @@ -15,7 +15,6 @@ board = promicro-nrf52840 build_flags = ${nrf52840_base.build_flags} -I variants/nrf52840/diy/nrf52_promicro_diy_tcxo -D NRF52_PROMICRO_DIY - ; -D RADIOLIB_GODMODE=1 ; needed for some LR2021 items, but not enabled by default. build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/diy/nrf52_promicro_diy_tcxo> debug_tool = jlink