diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 4fec06da4..2d391fe2b 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -350,4 +350,10 @@ template bool LR11x0Interface::sleep() return true; } + +template int16_t LR11x0Interface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(); + 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 new file mode 100644 index 000000000..0d496acc1 --- /dev/null +++ b/src/mesh/LR20x0Interface.cpp @@ -0,0 +1,383 @@ +#include "configuration.h" + +#if defined(USE_LR2021) && RADIOLIB_EXCLUDE_LR2021 != 1 +#include "LR20x0Interface.h" +#include "error.h" +#include "mesh/NodeDB.h" + +// Keep LR20x0 naming while RadioLib exposes LR2021 symbols. +#ifndef LR20x0 +#define LR20x0 LR2021 +#endif + +#ifdef LR2021_DIO_AS_RF_SWITCH +#include "rfswitch.h" +#elif ARCH_PORTDUINO +#include "PortduinoGlue.h" +#define lr20x0_rfswitch_dio_pins portduino_config.rfswitch_dio_pins +#define lr20x0_rfswitch_table portduino_config.rfswitch_table +#else +static const uint32_t lr20x0_rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; +static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = { + {LR20x0::MODE_STBY, {}}, {LR20x0::MODE_RX, {}}, {LR20x0::MODE_TX, {}}, + {LR20x0::MODE_RX_HF, {}}, {LR20x0::MODE_TX_HF, {}}, END_OF_MODE_TABLE, +}; +#endif + +// Particular boards might define a different max power based on what their hardware can do, default to max power output if not +// specified (may be dangerous if using external PA and LR20x0 power config forgotten) +#if ARCH_PORTDUINO +#define LR2021_MAX_POWER portduino_config.lr2021_max_power +#endif +#ifndef LR2021_MAX_POWER +#define LR2021_MAX_POWER 22 +#endif + +// the 2.4G part maxes at 12dBm + +#if ARCH_PORTDUINO +#define LR2021_MAX_POWER_HF portduino_config.lr2021_max_power_hf +#endif +#ifndef LR2021_MAX_POWER_HF +#define LR2021_MAX_POWER_HF 12 +#endif + +template +LR20x0Interface::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy) + : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) +{ + LOG_WARN("LR20x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); +} + +/// Initialise the Driver transport hardware and software. +/// Make sure the Driver is properly configured before calling init(). +/// \return true if initialisation succeeded. +template bool LR20x0Interface::init() +{ +#ifdef LR2021_POWER_EN + pinMode(LR2021_POWER_EN, OUTPUT); + digitalWrite(LR2021_POWER_EN, HIGH); +#endif + +#if ARCH_PORTDUINO + float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; +// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR20x0_DIO3_TCXO_VOLTAGE +#elif defined(LR2021_DIO3_TCXO_VOLTAGE) + float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE; + LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", LR2021_DIO3_TCXO_VOLTAGE); + // (DIO3 is not free to be used as an IRQ) +#elif defined(TCXO_OPTIONAL) + float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure + LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE defined, trying default TCXO Vref 1.6 V first"); +#else + float tcxoVoltage = + 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per + // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 + // (DIO3 is free to be used as an IRQ) + LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); +#endif + + RadioLibInterface::init(); + +#ifdef LR2021_IRQ_DIO_NUM + lora.irqDioNum = LR2021_IRQ_DIO_NUM; + LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum); +#elif defined(IRQ_DIO_NUM) + lora.irqDioNum = IRQ_DIO_NUM; + LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum); +#else + LOG_DEBUG("Use default irqDioNum %d", lora.irqDioNum); +#endif + + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR2021_MAX_POWER_HF); + } else { + limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range + } + +#ifdef LR2021_RF_SWITCH_SUBGHZ + pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT); + digitalWrite(LR2021_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); + LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); +#endif + +#ifdef LR2021_RF_SWITCH_2_4GHZ + pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT); + digitalWrite(LR2021_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); + LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); +#endif + + // Allow extra time for TCXO to stabilize after power-on + delay(10); + + int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + + // Retry if we get SPI command failed - some units need extra TCXO stabilization time + if (res == RADIOLIB_ERR_SPI_CMD_FAILED) { + LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retrying after delay...", res); + delay(100); + res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + } + +#if defined(TCXO_OPTIONAL) + // If init failed for any reason other than chip not found, retry without TCXO (XTAL mode) + if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) { + LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retrying without TCXO", tcxoVoltage, res); + tcxoVoltage = 0; + res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + if (res == RADIOLIB_ERR_NONE) + LOG_INFO("LR20x0 init success without TCXO (XTAL mode)"); + } +#endif + + // \todo Display actual typename of the adapter, not just `LR20x0` + LOG_INFO("LR20x0 init result %d", res); + if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) + return false; + + LOG_INFO("Frequency set to %f", getFreq()); + LOG_INFO("Bandwidth set to %f", bw); + LOG_INFO("Power output set to %d", power); + + if (res == RADIOLIB_ERR_NONE) + res = lora.setCRC(2); + +#ifdef LR2021_DIO_AS_RF_SWITCH + bool dioAsRfSwitch = true; +#elif defined(ARCH_PORTDUINO) + bool dioAsRfSwitch = portduino_config.has_rfswitch_table; +#else + bool dioAsRfSwitch = false; +#endif + + if (dioAsRfSwitch) { + lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table); + LOG_DEBUG("Set DIO RF switch"); + } + + if (res == RADIOLIB_ERR_NONE) { + if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate + res = lora.setRxBoostedGainMode(true); + LOG_INFO("Set RX gain to boosted mode; result: %d", res); + } else { + res = lora.setRxBoostedGainMode(false); + LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); + } + } + + if (res == RADIOLIB_ERR_NONE) + startReceive(); // start receiving + + return res == RADIOLIB_ERR_NONE; +} + +template bool LR20x0Interface::reconfigure() +{ + RadioLibInterface::reconfigure(); + + // set mode to standby + setStandby(); + + // configure publicly accessible settings + int err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setBandwidth(bw); // different form than LR11xx + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setSyncWord(syncWord); + assert(err == RADIOLIB_ERR_NONE); + + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + limitPower(LR2021_MAX_POWER_HF); + } else { + limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range + } + + err = lora.setPreambleLength(preambleLength); + assert(err == RADIOLIB_ERR_NONE); + + err = lora.setFrequency(getFreq()); + if (err != RADIOLIB_ERR_NONE) + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + + err = lora.setOutputPower(power); + assert(err == RADIOLIB_ERR_NONE); + + // Apply RX gain mode — valid in STDBY, matches resetAGC() pattern + err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + if (err != RADIOLIB_ERR_NONE) + LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err); + + startReceive(); // restart receiving + + return true; +} + +template void LR20x0Interface::disableInterrupt() +{ + lora.clearIrqAction(); +} + +template void LR20x0Interface::setStandby() +{ + checkNotification(); // handle any pending interrupts before we force standby + + int err = lora.standby(); + + if (err != RADIOLIB_ERR_NONE) { + LOG_DEBUG("LR20x0 standby failed with error %d", err); + } + + assert(err == RADIOLIB_ERR_NONE); + + isReceiving = false; // If we were receiving, not any more + activeReceiveStart = 0; + disableInterrupt(); + completeSending(); // If we were sending, not anymore + RadioLibInterface::setStandby(); +} + +/** + * Add SNR data to received messages + */ +template void LR20x0Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) +{ + // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); + mp->rx_snr = lora.getSNR(); + mp->rx_rssi = lround(lora.getRSSI()); + // LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0 + // too(!) +} + +/** We override to turn on transmitter power as needed. + */ +template void LR20x0Interface::configHardwareForSend() +{ + RadioLibInterface::configHardwareForSend(); +} + +// For power draw measurements, helpful to force radio to stay sleeping +// #define SLEEP_ONLY + +template void LR20x0Interface::startReceive() +{ +#ifdef SLEEP_ONLY + sleep(); +#else + + setStandby(); + + lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed. + + // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. + int err = + lora.startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + if (err) + LOG_ERROR("StartReceive error: %d", err); + assert(err == RADIOLIB_ERR_NONE); + + RadioLibInterface::startReceive(); + + // Must be done AFTER starting receive, because startReceive clears (possibly stale) interrupt pending register bits + enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); +#endif +} + +/** Is the channel currently active? */ +template bool LR20x0Interface::isChannelActive() +{ + // check if we can detect a LoRa preamble on the current channel + ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, + .detPeak = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, + .detMin = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, + .exitMode = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, + .timeout = 0, + .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, + .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; + int16_t result; + + setStandby(); + result = lora.scanChannel(cfg); + if (result == RADIOLIB_LORA_DETECTED) + return true; + + assert(result != RADIOLIB_ERR_WRONG_MODEM); + + return false; +} + +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ +template bool LR20x0Interface::isActivelyReceiving() +{ + // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet + // received and handled the interrupt for reading the packet/handling errors. + return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID, RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED); +} + +#ifdef LR20X0_AGC_RESET +template void LR20x0Interface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)"); + + // 1. Warm sleep — powers down the analog frontend, resetting AGC state + lora.sleep(true, 0); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_LR20X0_STANDBY_RC, true); + + // 3. Calibrate all blocks (PLL, ADC, image, RC oscillators) + // calibrate() is protected on LR20x0, so use raw SPI (same as internal implementation) + uint8_t calData = RADIOLIB_LR20X0_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_LR20X0_CMD_CALIBRATE, &calData, 1, true, true); + + // 4. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f); + + // 5. Re-apply RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 6. Resume receiving + startReceive(); +} +#endif + +template bool LR20x0Interface::sleep() +{ + // \todo Display actual typename of the adapter, not just `LR20x0` + LOG_DEBUG("LR20x0 entering sleep mode"); + setStandby(); // Stop any pending operations + + // turn off TCXO if it was powered + lora.setTCXO(0); + + // put chipset into sleep mode (we've already disabled interrupts by now) + bool keepConfig = false; + lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed + +#ifdef LR2021_POWER_EN + digitalWrite(LR2021_POWER_EN, LOW); +#endif + + return true; +} + +template int16_t LR20x0Interface::getCurrentRSSI() +{ + float rssi = lora.getRSSI(); + return (int16_t)round(rssi); +} +#endif diff --git a/src/mesh/LR20x0Interface.h b/src/mesh/LR20x0Interface.h new file mode 100644 index 000000000..263c83429 --- /dev/null +++ b/src/mesh/LR20x0Interface.h @@ -0,0 +1,77 @@ +#pragma once +#if RADIOLIB_EXCLUDE_LR2021 != 1 +#include "RadioLibInterface.h" + +/** + * \brief Adapter for LR20x0 radio family. Implements common logic for child classes. + * \tparam T RadioLib module type for LR20x0, e.g. LR2021. + */ +template class LR20x0Interface : public RadioLibInterface +{ + public: + LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, + RADIOLIB_PIN_TYPE busy); + + /// Initialise the Driver transport hardware and software. + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool init() override; + + /// Apply any radio provisioning changes + /// Make sure the Driver is properly configured before calling init(). + /// \return true if initialisation succeeded. + virtual bool reconfigure() override; + + /// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep. + virtual bool sleep() override; + + bool isIRQPending() override { return lora.getIrqFlags() != 0; } + +#ifdef LR20X0_AGC_RESET + void resetAGC() override; +#endif + + protected: + /** + * Specific module instance + */ + T lora; + + int16_t getCurrentRSSI() override; + + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() override; + + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setIrqAction(callback); } + + /** can we detect a LoRa preamble on the current channel? */ + virtual bool isChannelActive() override; + + /** are we actively receiving a packet (only called during receiving state) */ + virtual bool isActivelyReceiving() override; + + /** + * Start waiting to receive a message + */ + virtual void startReceive() override; + + /** + * We override to turn on transmitter power as needed. + */ + virtual void configHardwareForSend() override; + + /** + * Add SNR data to received messages + */ + virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; + + virtual void setStandby() override; + + uint32_t getPacketTime(uint32_t pl, bool received) override { return computePacketTime(lora, pl, received); } +}; +#endif diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index b3aa72f7a..479c22532 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -342,4 +342,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 8f793f47a..55505fbe1 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -264,6 +264,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 5121ac433..d255d8655 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 9ee608214..cfbfbe1b5 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -99,11 +99,42 @@ 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 */ static RadioLibInterface *instance; + /** + * 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 */ @@ -172,6 +203,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. @@ -179,6 +232,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 971aee31e..073b87f91 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -258,6 +258,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 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 0e882ef05..12b792d33 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -326,4 +326,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 1c2d18c71..8bd70494e 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(); @@ -125,6 +126,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; @@ -133,6 +136,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; @@ -148,10 +153,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); @@ -194,4 +200,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;