Added RXPS

This commit is contained in:
Kevin Le committed 2026-10-18 18:18:13 +07:00
1 parent ab2231be9b
commit 2195d20b31
12 files changed
+1548 -56

No files matched your search

+65 -2
View File
@@ -3,6 +3,34 @@
#include "Utils.h"
#include "target.h"
// PowerSaving variant
#include "radiolib/RXPowerSavingCLI.h"
static RxPowerSavingControl* rxpsControl() {
#ifdef WRAPPER_CLASS
return &radio_driver;
#else
return nullptr;
#endif
}
// PowerSaving variant
bool CommonRadioPrefs::applyRxPowerSaving(uint8_t sf, float bw) {
_active_sf = sf;
_active_bw = bw;
RxPowerSavingConfig* config = getRxPowerSavingConfig();
if (!config) return false;
// Temporary radio parameters must not replace the persisted automatic periods.
RxPowerSavingConfig active = *config;
return applyRxPowerSavingConfig(&active, sf, bw, rxpsControl());
}
// PowerSaving variant
bool CommonRadioPrefs::isRxPowerSavingCalibrationActive() const {
const auto* control = rxpsControl();
return control && control->isRxPowerSavingCalibrationActive();
}
bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) {
if (strcmp(key, "fem_rxgain") == 0) {
snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain());
@@ -29,6 +57,30 @@ bool CommonRadioPrefs::setByKey(const char* key, const char* value) {
}
bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
// PowerSaving variant
if (strncmp(command, "get radio.rxps", 14) == 0 &&
(command[14] == 0 || command[14] == ' ')) {
const char* suffix = command + 14;
while (*suffix == ' ') ++suffix;
if (*suffix) return false;
auto* config = getRxPowerSavingConfig();
if (!config) { strcpy(reply, "Error: unsupported"); return true; }
const uint8_t sf = _active_sf ? _active_sf : getSpreadFactor();
const float bw = _active_sf ? _active_bw : getBandwidth();
RxPowerSavingConfig active = *config;
normalizeRxPowerSavingConfig(&active, sf, bw,
rxPowerSavingCaptureCost(rxpsControl()), rxPowerSavingTransition(rxpsControl()));
RXPowerSavingCLI::get(&active, rxpsControl(), sf, bw, reply, 160);
return true;
}
if (strncmp(command, "set radio.rxps ", 15) == 0) {
auto* config = getRxPowerSavingConfig();
if (!config) { strcpy(reply, "Error: unsupported"); return true; }
if (RXPowerSavingCLI::set(command + 15,
_active_sf ? _active_sf : getSpreadFactor(), _active_sf ? _active_bw : getBandwidth(),
config, rxpsControl(), reply, 160)) markDirty();
return true;
}
if (strcmp(command, "get radio") == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(getFreq()));
@@ -50,7 +102,16 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest
setCodingRate(cr);
setFreq(freq);
setBandwidth(bw);
strcpy(reply, "OK - reboot to apply");
// PowerSaving variant
auto* rxps = getRxPowerSavingConfig();
const bool timing_ok = !rxps || !rxps->enabled || normalizeRxPowerSavingConfig(
rxps, sf, bw, rxPowerSavingCaptureCost(rxpsControl()), rxPowerSavingTransition(rxpsControl()));
if (!timing_ok && rxpsControl()) {
rxpsControl()->setRxPowerSaving(false, rxps->rx_us, rxps->sleep_us);
rxpsControl()->setRxPowerSavingConfigError(true);
}
strcpy(reply, timing_ok ? "OK - reboot to apply" : "OK - reboot to apply; RXPS continuous: invalid timing");
} else {
strcpy(reply, "Error, invalid radio params");
}
@@ -110,7 +171,9 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest
}
if (strcmp(command, "get cad") == 0) {
sprintf(reply, "> %s", isCadEnabled() ? "on" : "off");
// PowerSaving variant
sprintf(reply, "> %s; channel busy: %lu", isCadEnabled() ? "on" : "off",
(unsigned long)radio_driver.getCADBusyCount());
return true;
}
if (memcmp(command, "set cad ", 8) == 0) {
+86
View File
@@ -3,6 +3,9 @@
#include <RadioLib.h>
#include "MeshCore.h"
// PowerSaving variant
#include "RXPowerSaving.h"
class CustomLR1110 : public LR1110 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
@@ -13,16 +16,30 @@ class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
// PowerSaving variant
// See CustomSX1262::begin - same reasoning, LR11x0 flags and calibration.
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
// PowerSaving variant
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
// RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
}
// PowerSaving variant
int16_t applyMeshCoreTcxoDelay() {
if (tcxoVoltage <= 0.0f) return RADIOLIB_ERR_NONE;
int16_t state = setTCXO(tcxoVoltage, MC_TCXO_DELAY_US);
RADIOLIB_ASSERT(state);
state = calibrate(0x3F);
delay(50);
return state;
}
size_t getPacketLength(bool update) override {
size_t len = LR1110::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
@@ -38,6 +55,69 @@ class CustomLR1110 : public LR1110 {
float getFreqMHz() const { return freqMHz; }
// PowerSaving variant
// The sleep -> RX transition RadioLib subtracts from a duty-cycle sleep.
// Protected in the RadioLib base, so the RXPS layer reads it through here.
uint32_t getTcxoDelay() const { return tcxoDelay; }
// Restores the LF clock configuration RadioLib's begin() installs. Arming
// the duty cycle switches it to RC + BUSY-release (required by Semtech for
// SetRxDutyCycle); without this the change would outlive RXPS being turned
// off, and it also alters what the BUSY pin means to isChipBusy().
int16_t restoreLfClock() {
return configLfClock(
RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED | RADIOLIB_LR11X0_LF_CLK_XOSC);
}
// effectiveRxPeriod reports the RX window actually programmed, which may be
// stretched above the requested value to satisfy the extended-period rule.
int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod,
RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS,
RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK,
uint32_t* effectiveRxPeriod = nullptr) {
// Keep the conversion local until RadioLib uses 64-bit multiplication;
// its pinned implementation overflows for longer microsecond periods.
uint32_t symbolPeriod = (uint32_t)(((1000.0f * (float)(1UL << this->spreadingFactor)) /
this->bandwidthKhz) + 0.999f);
uint32_t transitionTime = this->tcxoDelay + 1000;
if (sleepPeriod <= transitionTime) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
uint32_t programmedSleepPeriod = sleepPeriod - transitionTime;
uint64_t requiredExtendedPeriod =
((uint64_t)this->preambleLengthLoRa + 11ULL) * symbolPeriod + 1000ULL;
uint64_t extendedPeriod = 2ULL * rxPeriod + programmedSleepPeriod;
if (extendedPeriod < requiredExtendedPeriod) {
rxPeriod = (uint32_t)((requiredExtendedPeriod - programmedSleepPeriod + 1ULL) / 2ULL);
}
if (effectiveRxPeriod) *effectiveRxPeriod = rxPeriod;
uint32_t rxPeriodRaw = (uint32_t)(((uint64_t)rxPeriod * 32768UL) / 1000000UL);
uint32_t sleepPeriodRaw =
(uint32_t)(((uint64_t)programmedSleepPeriod * 32768UL) / 1000000UL);
if ((rxPeriodRaw & 0xFF000000) || rxPeriodRaw == 0) return RADIOLIB_ERR_INVALID_RX_PERIOD;
if ((sleepPeriodRaw & 0xFF000000) || sleepPeriodRaw == 0) {
return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
}
int16_t state = standby(RADIOLIB_LR11X0_STANDBY_RC);
RADIOLIB_ASSERT(state);
// Semtech requires the RC standby/RTC setup before SetRxDutyCycle.
state = configLfClock(RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED);
RADIOLIB_ASSERT(state);
RadioModeConfig_t cfg = {
.receive = {
.timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF,
.irqFlags = irqFlags,
.irqMask = irqMask,
.len = 0,
}
};
state = stageMode(RADIOLIB_RADIO_MODE_RX, &cfg);
RADIOLIB_ASSERT(state);
return setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX);
}
int16_t setRxBoostedGainMode(bool en) {
_rx_boosted = en;
return LR1110::setRxBoostedGainMode(en);
@@ -50,6 +130,12 @@ class CustomLR1110 : public LR1110 {
return LR1110::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
// PowerSaving variant
bool isChipBusy() {
uint32_t busy = this->mod->getGpio();
return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy);
}
bool isReceiving() {
uint32_t irq = getIrqStatus();
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
+59 -2
View File
@@ -6,9 +6,28 @@
class CustomLR1110Wrapper : public RadioLibWrapper {
public:
// PowerSaving variant
// Measured on ThinkNode M3 and T1000-E: this family needs roughly one symbol
// more margin than SX126x before a duty-cycled receiver latches a preamble.
float rxPowerSavingCaptureCostSymbols() const override {
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS_LR11X0;
}
// PowerSaving variant
// Same reasoning as the SX126x wrapper. This family never showed either
// duty-cycle failure on the bench, which is unsurprising: its driver has
// always enforced an extended-period rule equivalent to the timer guard the
// SX126x side only just gained.
uint32_t rxPowerSavingTransitionUs() const override {
return ((CustomLR1110 *)_radio)->getTcxoDelay() + 1000;
}
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
// PowerSaving variant
prepareForRadioConfig();
((CustomLR1110 *)_radio)->setFrequency(freq);
((CustomLR1110 *)_radio)->setSpreadingFactor(sf);
((CustomLR1110 *)_radio)->setBandwidth(bw);
@@ -20,8 +39,18 @@ public:
}
bool isReceivingPacket() override {
// PowerSaving variant
// While duty-cycling, BUSY marks the sleep phase: probing IRQ status over
// SPI there could wake the chip and break the cycle. Only skip the probe
// in that case - outside RXPS this must stay a real channel check, since
// Dispatcher::checkSend() relies on it to avoid transmitting over others.
if (_rx_ps_armed && ((CustomLR1110 *)_radio)->isChipBusy()) return false;
return ((CustomLR1110 *)_radio)->isReceiving();
}
// PowerSaving variant
bool isChipBusy() override {
return ((CustomLR1110 *)_radio)->isChipBusy();
}
float getCurrentRSSI() override {
float rssi = -110;
((CustomLR1110 *)_radio)->getRssiInst(&rssi);
@@ -38,12 +67,40 @@ public:
_radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts
}
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
bool supportsRxPowerSaving() const override { return true; }
protected:
// PowerSaving variant
int16_t startContinuousReceive() override {
// Also covers a stopDutyCycleHardware() whose clock restore failed.
int16_t err = _radio->standby();
if (err == RADIOLIB_ERR_NONE) err = ((CustomLR1110*)_radio)->restoreLfClock();
return err == RADIOLIB_ERR_NONE ? _radio->startReceive() : err;
}
// PowerSaving variant
int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, RadioLibIrqFlags_t irq_mask,
uint32_t* eff_rx_us, uint32_t* eff_sleep_us) override {
*eff_sleep_us = _rx_ps_sleep_us; // only the RX window can get clamped
return ((CustomLR1110 *)_radio)->startReceiveDutyCycle(
_rx_ps_rx_us, _rx_ps_sleep_us, irq_flags, irq_mask, eff_rx_us);
}
// PowerSaving variant
int16_t stopDutyCycleHardware() override {
const int16_t state = _radio->standby();
const int16_t clock = ((CustomLR1110 *)_radio)->restoreLfClock();
return state != RADIOLIB_ERR_NONE ? state : clock;
}
public:
uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); }
bool setRxBoostedGainMode(bool en) override {
// PowerSaving variant
prepareForRadioConfig();
return ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
}
bool getRxBoostedGainMode() const override {
+6
View File
@@ -68,6 +68,12 @@ class CustomLR2021 : public LR2021 {
float getFreqMHz() const { return freqMHz; }
// PowerSaving variant
int16_t setRxBoostedGainMode(uint8_t level) {
const int16_t status = LR2021::setRxBoostedGainMode(level);
if (status == RADIOLIB_ERR_NONE) _rx_boosted = level != 0;
return status;
}
bool getRxBoostedGainMode() const { return _rx_boosted; }
int16_t startReceive() override {
+72
View File
@@ -3,6 +3,9 @@
#include <RadioLib.h>
#include "MeshCore.h"
// PowerSaving variant
#include "RXPowerSaving.h" // MC_TCXO_DELAY_US
class CustomSX1262 : public SX1262 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
@@ -10,8 +13,43 @@ class CustomSX1262 : public SX1262 {
bool _headerSeen = false;
public:
// PowerSaving variant
// The sleep -> RX transition RadioLib subtracts from a duty-cycle sleep.
// Protected in the RadioLib base, so the RXPS layer reads it through here.
uint32_t getTcxoDelay() const { return tcxoDelay; }
CustomSX1262(Module *mod) : SX1262(mod) { }
// PowerSaving variant
// RadioLib's begin() installs its own 5000 us TCXO startup delay and
// setTCXO() does not recalibrate, so MeshCore's value has to be written
// afterwards and the RC calibrations redone against the new clock-gating
// window. Overriding begin() puts that in one place: every variant reaches
// the radio through this class, whether it calls std_init() or begin()
// directly, so no target.cpp or variant .ini has to know about it.
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_SX126X_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6,
bool useRegulatorLDO = false) {
int16_t state = SX1262::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage, useRegulatorLDO);
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
return state;
}
// PowerSaving variant
// Read tcxoVoltage back from the chip object rather than from the argument:
// begin() zeroes it when it falls back to an XTAL, and re-asserting a TCXO
// supply on DIO3 for a board that has none would be worse than a long delay.
int16_t applyMeshCoreTcxoDelay() {
if (tcxoVoltage <= 0.0f) return RADIOLIB_ERR_NONE;
int16_t state = setTCXO(tcxoVoltage, MC_TCXO_DELAY_US);
RADIOLIB_ASSERT(state);
state = calibrate(RADIOLIB_SX126X_CALIBRATE_ALL);
delay(50);
return state;
}
#ifdef RP2040_PLATFORM
bool std_init(SPIClassRP2040* spi = NULL)
#else
@@ -105,6 +143,26 @@ class CustomSX1262 : public SX1262 {
return SX1262::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
}
// PowerSaving variant
int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod,
RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS,
RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK) {
int16_t state = SX1262::startReceiveDutyCycle(rxPeriod, sleepPeriod, irqFlags, irqMask);
if (state == RADIOLIB_ERR_NONE) {
// RadioLib stages RX through standby, which leaves a host-controlled
// RXEN switch in IDLE. Set it back to RX for the whole duty-cycle;
// boards without an external RXEN table are unaffected.
this->mod->setRfSwitchState(Module::MODE_RX);
}
return state;
}
// PowerSaving variant
bool isChipBusy() {
uint32_t busy = this->mod->getGpio();
return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy);
}
bool isReceiving() {
uint32_t irq = getIrqFlags();
bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2
@@ -148,6 +206,20 @@ class CustomSX1262 : public SX1262 {
return false;
}
// PowerSaving variant
int16_t stopRTC() {
uint8_t rtcStop = 0x00;
int16_t state = writeRegister(RADIOLIB_SX126X_REG_RTC_CTRL, &rtcStop, 1);
RADIOLIB_ASSERT(state);
uint8_t rtcEvent = 0;
state = readRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1);
RADIOLIB_ASSERT(state);
rtcEvent |= 0x02;
return writeRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1);
}
void setPreambleMillis(uint32_t preambleMillis) {
_preambleMillis = preambleMillis;
MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis);
+50 -3
View File
@@ -10,9 +10,21 @@
class CustomSX1262Wrapper : public RadioLibWrapper {
public:
// PowerSaving variant
// Ask the radio rather than assume RadioLib's 5000 us default: startReceive-
// DutyCycle subtracts tcxoDelay + 1000 from the sleep and the register
// underflows below that, so the floor is a property of this board's TCXO. A
// board without one can duty cycle with a far shorter sleep. The 100 us on
// top clears the 15.625 us register tick that must remain.
uint32_t rxPowerSavingTransitionUs() const override {
return ((CustomSX1262 *)_radio)->getTcxoDelay() + 1000;
}
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
// PowerSaving variant
prepareForRadioConfig();
((CustomSX1262 *)_radio)->setFrequency(freq);
((CustomSX1262 *)_radio)->setSpreadingFactor(sf);
((CustomSX1262 *)_radio)->setBandwidth(bw);
@@ -23,14 +35,22 @@ public:
((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
}
bool isReceivingPacket() override {
bool isReceivingPacket() override {
// PowerSaving variant
// While duty-cycling, BUSY marks the sleep phase: probing IRQ flags over
// SPI there could wake the chip and break the cycle. Only skip the probe
// in that case - outside RXPS this must stay a real channel check, since
// Dispatcher::checkSend() relies on it to avoid transmitting over others.
if (_rx_ps_armed && ((CustomSX1262 *)_radio)->isChipBusy()) return false;
return ((CustomSX1262 *)_radio)->isReceiving();
}
// PowerSaving variant
bool isChipBusy() override {
return ((CustomSX1262 *)_radio)->isChipBusy();
}
float getCurrentRSSI() override {
return ((CustomSX1262 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
int sf = ((CustomSX1262 *)_radio)->spreadingFactor;
@@ -38,10 +58,37 @@ public:
}
uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; }
virtual void powerOff() override {
// PowerSaving variant
if (_rx_ps_armed) stopReceiveDutyCycle();
((CustomSX1262 *)_radio)->sleep(false);
}
// PowerSaving variant
bool supportsRxPowerSaving() const override { return true; }
protected:
// PowerSaving variant
int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, RadioLibIrqFlags_t irq_mask,
uint32_t* eff_rx_us, uint32_t* eff_sleep_us) override {
// RadioLib programs the SX126x with exactly what we ask for (it only
// subtracts the wake-up transition from the sleep period internally).
*eff_rx_us = _rx_ps_rx_us;
*eff_sleep_us = _rx_ps_sleep_us;
return ((CustomSX1262 *)_radio)->startReceiveDutyCycle(
_rx_ps_rx_us, _rx_ps_sleep_us, irq_flags, irq_mask);
}
// PowerSaving variant
int16_t stopDutyCycleHardware() override {
int16_t standby_err = _radio->standby();
int16_t rtc_err = ((CustomSX1262 *)_radio)->stopRTC();
return standby_err != RADIOLIB_ERR_NONE ? standby_err : rtc_err;
}
public:
bool setRxBoostedGainMode(bool en) override {
// PowerSaving variant
prepareForRadioConfig();
return ((CustomSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
}
bool getRxBoostedGainMode() const override {
+245
View File
@@ -0,0 +1,245 @@
#include "RXPowerSaving.h"
#include <math.h>
static uint32_t ceilRxPowerSavingValue(float value) {
uint32_t rounded = (uint32_t)value;
return value > (float)rounded ? rounded + 1 : rounded;
}
// Both radios program their duty cycle in 15.625 us ticks and truncate the
// microsecond value on the way in, so a period of 10013 us is really 10000 us
// on air while the CLI happily reports 10013. Snapping to the nearest tick and
// returning the smallest microsecond value that still lands on it makes the
// reported periods the ones the hardware runs - which matters here, because the
// register value turned out to be what a duty-cycle defect keys on, not the
// microseconds we asked for.
static uint32_t rxPowerSavingTickToUs(uint64_t tick) {
if (tick == 0) tick = 1;
return (uint32_t)((tick * 125 + 7) / 8); // ceil(tick * 15.625)
}
static uint32_t snapRxPowerSavingToTick(uint32_t us) {
return rxPowerSavingTickToUs(((uint64_t)us * 8 + 62) / 125); // nearest
}
// Rounding the listen window to the *nearest* tick can shave a few microseconds
// off, and the timer guard below has no room to give: it must hold with the
// register values the radio actually runs. So that one rounds up.
static uint32_t snapRxPowerSavingUpToTick(uint32_t us) {
return rxPowerSavingTickToUs(((uint64_t)us * 8 + 124) / 125); // ceil
}
bool isValidRxPowerSavingPeriod(uint32_t us) {
return us >= RX_POWERSAVING_MIN_PERIOD_US && us <= RX_POWERSAVING_MAX_PERIOD_US;
}
uint8_t rxPowerSavingPreambleForSF(uint8_t sf) {
return sf <= 8 ? 32 : 16;
}
bool isRxPowerSavingNumeric(const char* value) {
if (value == nullptr || *value == 0) return false;
while (*value) {
if (*value < '0' || *value > '9') return false;
value++;
}
return true;
}
float rxPowerSavingCaptureCost(const RxPowerSavingControl* control) {
return control != nullptr ? control->rxPowerSavingCaptureCostSymbols()
: RX_POWERSAVING_CAPTURE_COST_SYMBOLS;
}
uint32_t rxPowerSavingTransition(const RxPowerSavingControl* control) {
return control != nullptr ? control->rxPowerSavingTransitionUs()
: RX_POWERSAVING_TRANSITION_US;
}
// rxPeriod register values measured to break SetRxDutyCycle on SX1262: the
// radio latches the preamble and then never validates a header, losing 35% of
// packets at SF6 and all of them at SF7. Both were reproduced on two boards
// with an LR1110 witnessing 100% of the same transmissions, and both bands are
// exactly one tick wide - 639 and 641 are lossless.
//
// This list is certainly incomplete: about thirty values were sampled out of
// the thousands a profile can generate. Levels 1-10 do not need it because the
// timer guard makes any value safe; it exists only for the unguarded maximum,
// where it is the one protection left.
static const uint32_t RX_POWERSAVING_BAD_RX_TICKS[] = {320, 640};
static uint32_t avoidBadRxPowerSavingTick(uint32_t rx_us) {
const uint32_t tick = (rx_us * 8) / 125;
for (uint32_t bad : RX_POWERSAVING_BAD_RX_TICKS) {
// One tick longer costs 15.625 us of listening and steps clear of the band.
if (tick == bad) return (uint32_t)((((uint64_t)tick + 1) * 125 + 7) / 8);
}
return rx_us;
}
bool calcRxPowerSavingLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble,
uint32_t* rx_us, uint32_t* sleep_us,
float capture_cost_symbols, uint32_t transition_us) {
if (rx_us == nullptr || sleep_us == nullptr || level < 1 ||
level > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL || sf < 5 || sf > 12 ||
!isfinite(bw) || bw <= 0.0f || !isfinite(capture_cost_symbols) ||
capture_cost_symbols <= 0 || capture_cost_symbols >= preamble || transition_us >= RX_POWERSAVING_MAX_PERIOD_US || (preamble != 16 && preamble != 32)) {
return false;
}
const bool unguarded = isRxPowerSavingUnguardedLevel(level);
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
if (!isfinite(symbol_us) || symbol_us * 64.0f > (float)(UINT32_MAX / 8)) return false;
// Worst case a preamble starts the instant an RX window closes, so the part
// that survives into the next window is `preamble - sleep`. The absolute
// limit is therefore reached when that equals the chip's capture cost; the
// unguarded levels are pinned to geometries measured at or past that point,
// which is why they still need it.
const float sleep_edge_symbols = (float)preamble - capture_cost_symbols;
float sleep_symbols;
float rx_symbols = 0.0f; // only used by the unguarded levels
if (unguarded) {
// Kept on the old interpolation because that is the geometry the bench
// measured: overdrive is the top of the profile, and `riskyWorkingMax` is
// the point past it where delivery started to fall.
float amount = 1.0f;
if (isRxPowerSavingRiskyWorkingMaxLevel(level)) {
const float virtual_level = preamble == 16 ? RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P16
: RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P32;
amount = (virtual_level - 1.0f) / 9.0f;
}
const float rx_start_symbols = preamble == 16 ? 12.0f : 16.0f;
const float sleep_start_symbols = preamble == 16 ? 2.0f : 15.0f;
rx_symbols = rx_start_symbols + amount * (8.0f - rx_start_symbols);
sleep_symbols = sleep_start_symbols + amount * (sleep_edge_symbols - sleep_start_symbols);
} else {
// Guarded levels state their catch directly, so the sleep is just what is
// left of the preamble - no capture cost involved. The pad makes the node
// catch a fifth of a symbol more than promised, which is what keeps the
// generated register pairs off the defective ones.
const float catch_symbols = rxPowerSavingLevelCatch(level, preamble);
// A level that undertakes to catch less than this radio needs to latch is
// not usable: say so rather than return a geometry that cannot work.
if (catch_symbols < capture_cost_symbols) return false;
sleep_symbols = (float)preamble - catch_symbols - RX_POWERSAVING_MARGIN_PAD_SYMBOLS;
if (sleep_symbols < 0.0f) sleep_symbols = 0.0f;
}
*sleep_us = (uint32_t)(sleep_symbols * symbol_us);
// Below the hardware floor the duty cycle cannot be armed at all: the driver
// returns -708 and the wrapper quietly runs continuous RX, so the node keeps
// reporting power saving while spending full RX current. Just above that the
// SX1262 arms with ERR_NONE and then detects no preambles at all, which is
// worse. Measured floor: 6050-6100 us dead, 6200 us and up lossless.
const uint32_t min_sleep_us = transition_us + RX_POWERSAVING_MIN_SLEEP_MARGIN_US;
if (*sleep_us < min_sleep_us) {
if ((float)min_sleep_us > sleep_edge_symbols * symbol_us) {
// Even zero margin would not fit above the floor: no level of this
// profile can duty cycle at this SF/BW. Say so instead of returning
// something that will silently fall back.
return false;
}
// The floor eats into the margin. Levels below it collapse onto one point;
// the reply still reports the periods, so the loss of margin is visible.
*sleep_us = min_sleep_us;
}
*sleep_us = transition_us + snapRxPowerSavingToTick(*sleep_us - transition_us);
// SetRxDutyCycle timer guard, straight out of the SX1261/2 datasheet: on
// preamble detection the radio restarts its timer with 2*rxPeriod +
// sleepPeriod (register values), and the packet dies if the header does not
// arrive inside it. The datasheet states the requirement as
// Tpreamble + Theader <= 2 * rxPeriod + sleepPeriod
// Measured: when it holds, no period value causes trouble. When it is broken
// the SX1262 usually gets away with it - but not always, and the exceptions
// are a single register tick wide. rxPeriod 640 (exactly 10.000 ms) loses
// 60-65% of packets at SF6 and everything at SF7, on two different boards,
// while 639 and 641 are lossless; rxPeriod 320 behaves the same way. Nothing
// in the register value predicts which ticks misbehave, so satisfy the
// condition instead of dodging the values. The listen window is therefore not
// a free parameter on the guarded scale - it is whatever the condition needs.
const float sync_symbols =
sf <= 6 ? RX_POWERSAVING_SYNC_SYMBOLS_LOW_SF : RX_POWERSAVING_SYNC_SYMBOLS;
const float need_us =
((float)preamble + sync_symbols + RX_POWERSAVING_HEADER_SYMBOLS +
RX_POWERSAVING_TIMER_SLACK_SYMBOLS) * symbol_us;
// Work in the radio's own units: the driver truncates (sleep - transition) to
// 15.625 us ticks, so the microsecond difference overstates the programmed
// sleep by up to one tick - enough to leave the condition short on every
// level of a profile.
const uint32_t sleep_ticks = ((*sleep_us - transition_us) * 8) / 125;
const float programmed_sleep_us = (float)sleep_ticks * RX_POWERSAVING_TICK_US;
if (unguarded) {
*rx_us = ceilRxPowerSavingValue(rx_symbols * symbol_us);
} else {
const float min_rx_us = (need_us - programmed_sleep_us) / 2.0f;
*rx_us = ceilRxPowerSavingValue(min_rx_us > 0.0f ? min_rx_us : symbol_us);
}
*rx_us = snapRxPowerSavingUpToTick(*rx_us);
if (unguarded) *rx_us = avoidBadRxPowerSavingTick(*rx_us);
return true;
}
void ensureRxPowerSavingDefaults(uint32_t* rx_us, uint32_t* sleep_us) {
if (!isValidRxPowerSavingPeriod(*rx_us)) *rx_us = RX_POWERSAVING_DEFAULT_RX_US;
if (!isValidRxPowerSavingPeriod(*sleep_us)) *sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
}
bool recalcRxPowerSavingFromLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble,
uint32_t* rx_us, uint32_t* sleep_us,
float capture_cost_symbols, uint32_t transition_us) {
if (level < 1 || level > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL) return false;
if (preamble == 0) preamble = rxPowerSavingPreambleForSF(sf);
uint32_t calculated_rx_us = 0;
uint32_t calculated_sleep_us = 0;
if (!calcRxPowerSavingLevel(level, sf, bw, preamble, &calculated_rx_us, &calculated_sleep_us,
capture_cost_symbols, transition_us) ||
!isValidRxPowerSavingPeriod(calculated_rx_us) ||
!isValidRxPowerSavingPeriod(calculated_sleep_us)) {
return false;
}
*rx_us = calculated_rx_us;
*sleep_us = calculated_sleep_us;
return true;
}
bool normalizeRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw,
float capture_cost_symbols, uint32_t transition_us) {
if (config == nullptr || sf < 5 || sf > 12 || !isfinite(bw) || bw <= 0) return false;
RxPowerSavingConfig proposed = *config;
proposed.enabled = proposed.enabled ? 1 : 0;
if (proposed.level > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL) {
proposed.level = RX_POWERSAVING_BALANCED_LEVEL;
}
if (proposed.preamble != 0 && proposed.preamble != 16 && proposed.preamble != 32) {
proposed.preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
}
if (proposed.level == 0) {
if (!isValidRxPowerSavingPeriod(proposed.rx_us) ||
!isValidRxPowerSavingPeriod(proposed.sleep_us)) return false;
} else if (!recalcRxPowerSavingFromLevel(proposed.level, sf, bw, proposed.preamble,
&proposed.rx_us, &proposed.sleep_us,
capture_cost_symbols, transition_us)) {
return false;
}
*config = proposed;
return true;
}
bool applyRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw,
RxPowerSavingControl* control) {
if (config == nullptr) return false;
const bool valid = !config->enabled || normalizeRxPowerSavingConfig(
config, sf, bw, rxPowerSavingCaptureCost(control), rxPowerSavingTransition(control));
if (control == nullptr) return !config->enabled;
const bool accepted = control->setRxPowerSaving(
config->enabled && valid, config->rx_us, config->sleep_us);
control->setRxPowerSavingConfigError(!valid);
return valid && accepted;
}
+275
View File
@@ -0,0 +1,275 @@
#pragma once
#include <stdint.h>
static constexpr uint32_t RX_POWERSAVING_DEFAULT_RX_US = 65625UL;
static constexpr uint32_t RX_POWERSAVING_DEFAULT_SLEEP_US = 60000UL;
static constexpr uint32_t RX_POWERSAVING_MIN_PERIOD_US = 1000UL;
static constexpr uint32_t RX_POWERSAVING_MAX_PERIOD_US = 30000000UL;
// The level scale is a catch dial, in symbols of the sender's preamble.
//
// Level N means: "at least this many symbols of the sender's preamble land
// inside an open RX window." That is the quantity the radio actually needs -
// a preamble is latched once enough of it has been heard - so the level states
// the requirement directly instead of stating a distance from an edge.
//
// The arithmetic follows from that in one step. Worst case, a preamble starts
// the instant an RX window closes, so what survives into the next window is
// `preamble - sleep`. Requiring that to be at least the level's catch gives
//
// sleep = preamble - catch
//
// and the chip's capture cost never enters the geometry. It becomes a
// validation instead: a level is usable when its catch is at least the cost.
// The floor of the scale is 8 symbols precisely because that is what the
// slower of the two families needs (LR11x0 8, SX126x 6), so **every level
// means the same geometry on both radios** - which the previous margin-based
// scale could not do, because the cost sat inside its formula.
//
// The two profiles get their own ladders. On a 16-symbol preamble there are
// only 8 symbols to spend between the floor and the whole preamble, so the
// steps are single symbols; on a 32-symbol preamble there are 24, so the low
// levels can take much larger strides where reception is guaranteed anyway.
static constexpr uint8_t RX_POWERSAVING_GUARDED_LEVELS = 8;
static constexpr float RX_POWERSAVING_LEVEL_CATCH_P16[RX_POWERSAVING_GUARDED_LEVELS] = {
15.0f, 14.0f, 13.0f, 12.0f, 11.0f, 10.0f, 9.0f, 8.0f
};
static constexpr float RX_POWERSAVING_LEVEL_CATCH_P32[RX_POWERSAVING_GUARDED_LEVELS] = {
24.0f, 20.0f, 16.0f, 14.0f, 12.0f, 10.0f, 9.0f, 8.0f
};
// Smallest catch any guarded level asks for, and the reason it is 8: the
// LR11x0 needs 8 symbols to latch where the SX126x needs 6.
static constexpr float RX_POWERSAVING_MIN_CATCH_SYMBOLS = 8.0f;
// On top of the ladder, every guarded level sleeps this much less than the
// arithmetic demands - so it catches 0.2 symbols more than its level promises.
// The reason is not capture safety: it is that specific register pairs the
// calculation lands on are defective (preamble latched, header never
// validated) in the same way isolated rxPeriod ticks 320 and 640 are. Nudging
// every generated geometry off those points cost 0.05 mA and took the worst
// measured cell from 3.33% packet loss to 0.11% across the whole ladder.
// Levels 9 and 10 are exempt - their geometries are pinned to bench
// measurements and shifting them would invalidate that data.
static constexpr float RX_POWERSAVING_MARGIN_PAD_SYMBOLS = 0.2f;
// Past the guarded scale sit two settings that break the datasheet's
// SetRxDutyCycle timer condition on purpose. Overdrive is lossless and 4-12
// percentage points cheaper in duty cycle; `riskyWorkingMax` is the measured
// working edge and drops ~2% of packets by design.
static constexpr uint8_t RX_POWERSAVING_OVERDRIVE_LEVEL = 9;
static constexpr uint8_t RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL = 10;
static constexpr float RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P32 = 11.0f;
static constexpr float RX_POWERSAVING_RISKY_WORKING_MAX_VIRTUAL_P16 = 10.25f;
// Top of the guarded scale: catches the ladder floor of 8 symbols (8.2 with
// the pad), the cheapest setting that still meets the datasheet condition and
// still works on both radio families. The CLI calls it `max`.
static constexpr uint8_t RX_POWERSAVING_MAX_LEVEL = RX_POWERSAVING_GUARDED_LEVELS;
static constexpr uint8_t RX_POWERSAVING_CONSERVATIVE_LEVEL = 3; // catches 13 (P16) / 16 (P32)
static constexpr uint8_t RX_POWERSAVING_BALANCED_LEVEL = 6; // catches 10 (P16) / 10 (P32)
static constexpr uint8_t RX_POWERSAVING_PROFILE_PREAMBLE = 16;
static constexpr uint8_t RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES = 3;
// Symbols of a sender's preamble consumed before a packet can be latched:
// ~2 to latch the preamble plus ~2 lost to RX-window startup, measured on
// SX1262 at SF8/BW62.5. Capture needs sleep <= sender_preamble - this.
// Note this is well below the 8 symbols RadioLib quotes (SX126x.h) - that
// figure is the datasheet's margin for *reliable* latching, not the floor.
//
// Chip dependent, so it belongs with the driver rather than with each board:
// a wrapper overrides rxPowerSavingCaptureCostSymbols() and every variant using
// that radio picks it up. LR1110 needs about one symbol more - on the bench it
// held 300/300 at a margin of 7.78 symbols and started dropping packets at
// 6.89, for both profiles, where SX1262 is clean down to 6.
static constexpr float RX_POWERSAVING_CAPTURE_COST_SYMBOLS = 6.0f;
static constexpr float RX_POWERSAVING_CAPTURE_COST_SYMBOLS_LR11X0 = 8.0f;
// TCXO startup delay handed to SetDIO3AsTCXOCtrl (SX126x DS 13.3.6) /
// SetTcxoMode (LR11x0 UM 6.3.2). RadioLib defaults it to 5000 us and nothing
// overrode it, so every board paid 5 ms on every duty-cycle wake whatever
// crystal was fitted. Measured lowest working value on five modules: T096 400,
// Tracker V2 300, Waveshare 150, ThinkNode M3 200, T1000-E 200 us - so 1600
// keeps 4x margin over the worst of them.
//
// One knob for the whole tree, in `arduino_base`, because it is a property of
// the parts MeshCore is built with rather than of any one board. A variant that
// needs its own value defines it in its own build_flags and the later -D wins.
#ifndef MC_TCXO_DELAY_US
#define MC_TCXO_DELAY_US 1600
#endif
// Sleep -> RX transition: RadioLib subtracts tcxoDelay + 1000 us from the
// requested sleep before writing the register, and the hardware spends that
// long saving context, restarting the XTAL and locking the PLL. Everything
// below derives from it, so a board that knows its real TCXO delay overrides
// one method and both the sleep floor and the timer guard follow.
static constexpr uint32_t RX_POWERSAVING_TRANSITION_US = MC_TCXO_DELAY_US + 1000;
// Slack the timer guard leaves on top of the datasheet condition, in symbols.
//
// Closing the condition exactly is not enough. The calculator lands on register
// pairs that satisfy it by a hair and still lose packets: rx=2476/sleep=2455
// ticks clears it by 22 us at 0.12 symbol and drops 2-6% on every SX1262 tried,
// while moving any one component clears the fault. It is the same family as the
// isolated bad ticks 320 and 640 - the condition does not predict which pairs
// misbehave, so the answer is to stop generating geometries that sit on its
// edge. One whole symbol of slack is what the bench evidence supports.
#ifndef MC_RXPS_TIMER_SLACK_SYMBOLS
#define MC_RXPS_TIMER_SLACK_SYMBOLS 1.0f
#endif
static constexpr float RX_POWERSAVING_TIMER_SLACK_SYMBOLS = MC_RXPS_TIMER_SLACK_SYMBOLS;
// Both radio families program their duty cycle in these units, and truncate the
// microsecond value on the way in. Everything the timer guard compares has to
// be expressed in whole ticks or it comes out short by a fraction of one.
static constexpr float RX_POWERSAVING_TICK_US = 15.625f;
// Margin above the transition before a duty cycle actually works; see below.
static constexpr uint32_t RX_POWERSAVING_MIN_SLEEP_MARGIN_US = 250;
// Symbols between the end of the sender's preamble and the end of the header:
// the LoRa sync word plus the 8-symbol explicit header. SF5/SF6 use a longer
// sync word than SF7 and above. Used by the SetRxDutyCycle timer guard.
static constexpr float RX_POWERSAVING_SYNC_SYMBOLS_LOW_SF = 6.25f;
static constexpr float RX_POWERSAVING_SYNC_SYMBOLS = 4.25f;
static constexpr float RX_POWERSAVING_HEADER_SYMBOLS = 8.0f;
// Shortest sleep that actually produces a working duty cycle.
//
// RadioLib subtracts tcxoDelay + 1000 us from the requested sleep before
// writing the register. At or below that the register underflows and arming
// fails with RADIOLIB_ERR_INVALID_SLEEP_PERIOD (-708), after which the wrapper
// falls back to continuous RX while the config still says power saving is on.
// Measured on SX1262: the P16 profile fails to arm on levels 1-5 at SF6 and on
// levels 1-2 at SF7, exactly where that bound predicts.
//
// But arming successfully is NOT the boundary. Just above it the SX1262 accepts
// the command, returns ERR_NONE, and then detects **zero** preambles - a
// silently deaf receiver, which is worse than the honest -708. Measured twice,
// on two different SF and RX windows: sleep 6050-6100 us is dead, 6200 us and
// everything above it is lossless (40/40 at every value up to 21617 us). The
// chip needs roughly 200 us of *programmed* sleep, i.e. ~12 register ticks, so
// this leaves 250 us of margin on top of the transition time.
//
// The default assumes the 5000 us TCXO delay RadioLib installs; a wrapper that
// can read its radio overrides this with the board's real value.
static constexpr uint32_t RX_POWERSAVING_MIN_SLEEP_US =
RX_POWERSAVING_TRANSITION_US + RX_POWERSAVING_MIN_SLEEP_MARGIN_US;
class RxPowerSavingArmRetryState {
uint8_t _consecutive_failures = 0;
public:
bool canAttempt() const {
return _consecutive_failures < RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES;
}
void recordSuccess() { _consecutive_failures = 0; }
void recordFailure() {
if (_consecutive_failures < RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES) {
_consecutive_failures++;
}
}
void reset() { _consecutive_failures = 0; }
uint8_t consecutiveFailures() const { return _consecutive_failures; }
};
struct RxPowerSavingConfig {
uint8_t enabled = 0;
uint32_t rx_us = RX_POWERSAVING_DEFAULT_RX_US;
uint32_t sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
uint8_t level = RX_POWERSAVING_BALANCED_LEVEL;
uint8_t preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
};
struct RxPowerSavingStatus {
bool supported = false;
bool armed = false;
int16_t last_error = 0;
uint32_t arm_failures = 0;
// Periods the radio actually runs with. A driver may clamp the requested
// values to satisfy a hardware constraint, so these can differ from the
// configured rx_us/sleep_us. Zero means "nothing armed yet".
uint32_t effective_rx_us = 0;
uint32_t effective_sleep_us = 0;
};
class RxPowerSavingControl {
public:
virtual ~RxPowerSavingControl() = default;
virtual bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
(void)rx_us;
(void)sleep_us;
return !enabled;
}
virtual RxPowerSavingStatus getRxPowerSavingStatus() const { return {}; }
virtual void setRxPowerSavingConfigError(bool invalid) { (void)invalid; }
virtual bool isRxPowerSavingCalibrationActive() const { return false; }
// Symbols of the sender's preamble consumed before a packet can be latched.
// Overridden per radio family; see RX_POWERSAVING_CAPTURE_COST_SYMBOLS.
virtual float rxPowerSavingCaptureCostSymbols() const {
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS;
}
// Sleep -> RX transition of this radio, in microseconds. The sleep floor and
// the SetRxDutyCycle timer guard are both derived from it, so a wrapper that
// can read its own tcxoDelay only needs to override this one method.
virtual uint32_t rxPowerSavingTransitionUs() const {
return RX_POWERSAVING_TRANSITION_US;
}
};
// Margin this level asks for, in symbols. Only meaningful on the guarded scale.
// Symbols of the sender's preamble this level undertakes to catch. The two
// profiles have separate ladders, so the profile has to be passed in; anything
// other than 16 or 32 is treated as the 16-symbol profile, matching the
// validation in calcRxPowerSavingLevel().
inline float rxPowerSavingLevelCatch(uint8_t level, uint8_t preamble) {
if (level < 1 || level > RX_POWERSAVING_GUARDED_LEVELS) return 0.0f;
return preamble == 32 ? RX_POWERSAVING_LEVEL_CATCH_P32[level - 1]
: RX_POWERSAVING_LEVEL_CATCH_P16[level - 1];
}
inline bool isRxPowerSavingOverdriveLevel(uint8_t level) {
return level == RX_POWERSAVING_OVERDRIVE_LEVEL;
}
inline bool isRxPowerSavingRiskyWorkingMaxLevel(uint8_t level) {
return level == RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL;
}
// Both levels past the guarded scale run outside the datasheet's timer
// condition. Worth surfacing wherever a level is reported.
inline bool isRxPowerSavingUnguardedLevel(uint8_t level) {
return level >= RX_POWERSAVING_OVERDRIVE_LEVEL;
}
bool isValidRxPowerSavingPeriod(uint32_t us);
uint8_t rxPowerSavingPreambleForSF(uint8_t sf);
bool isRxPowerSavingNumeric(const char* value);
// capture_cost_symbols defaults to the SX126x figure so callers without a radio
// handy still compile; anything holding a RxPowerSavingControl should pass
// rxPowerSavingCaptureCost(control) instead.
float rxPowerSavingCaptureCost(const RxPowerSavingControl* control);
uint32_t rxPowerSavingTransition(const RxPowerSavingControl* control);
bool calcRxPowerSavingLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble,
uint32_t* rx_us, uint32_t* sleep_us,
float capture_cost_symbols = RX_POWERSAVING_CAPTURE_COST_SYMBOLS,
uint32_t transition_us = RX_POWERSAVING_TRANSITION_US);
void ensureRxPowerSavingDefaults(uint32_t* rx_us, uint32_t* sleep_us);
bool recalcRxPowerSavingFromLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble,
uint32_t* rx_us, uint32_t* sleep_us,
float capture_cost_symbols = RX_POWERSAVING_CAPTURE_COST_SYMBOLS,
uint32_t transition_us = RX_POWERSAVING_TRANSITION_US);
bool normalizeRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw,
float capture_cost_symbols = RX_POWERSAVING_CAPTURE_COST_SYMBOLS,
uint32_t transition_us = RX_POWERSAVING_TRANSITION_US);
// The requested configuration survives a timing failure; only the radio falls back.
bool applyRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw,
RxPowerSavingControl* control);
static constexpr int16_t RX_POWERSAVING_ERR_TIMING = -10001;
+268
View File
@@ -0,0 +1,268 @@
#include "RXPowerSavingCLI.h"
#include <Utils.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
// Suffix printed after the level so an unguarded profile is never mistaken for
// an ordinary one, in both the `set` echo and the `get` report.
static const char* rxPowerSavingLevelTag(uint8_t level) {
if (isRxPowerSavingRiskyWorkingMaxLevel(level)) return "(riskyWorkingMax)";
if (isRxPowerSavingOverdriveLevel(level)) return "(overdrive)";
return "";
}
static bool parseRxPowerSavingUint32(const char* value, uint32_t* parsed) {
if (parsed == nullptr || !isRxPowerSavingNumeric(value)) return false;
uint32_t result = 0;
while (*value) {
uint32_t digit = (uint32_t)(*value++ - '0');
if (result > (UINT32_MAX - digit) / 10U) return false;
result = result * 10U + digit;
}
*parsed = result;
return true;
}
bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
RxPowerSavingConfig* config, RxPowerSavingControl* control,
char* reply, size_t reply_size) {
if (value == nullptr || config == nullptr || reply == nullptr || reply_size == 0) return false;
RxPowerSavingConfig proposed = *config;
uint8_t level = 0;
uint8_t preamble = rxPowerSavingPreambleForSF(sf);
bool level_requested = false;
bool preamble_overridden = false;
ensureRxPowerSavingDefaults(&proposed.rx_us, &proposed.sleep_us);
if (strcmp(value, "off") == 0) {
proposed.enabled = 0;
} else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) {
proposed.enabled = 1;
level = RX_POWERSAVING_CONSERVATIVE_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
preamble_overridden = true;
} else if (strcmp(value, "max") == 0) {
// Top of the guarded scale. Like every named preset it assumes the worst
// case a mixed network can present - a sender using a 16-symbol preamble -
// so at SF<=8 it is markedly less economical than `level 10`, which follows
// the SF and picks the 32-symbol profile. `max preamble 32` gets that back.
proposed.enabled = 1;
level = RX_POWERSAVING_MAX_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
preamble_overridden = true;
} else if (strcmp(value, "overdrive") == 0) {
// One step past the guarded scale: measured lossless, outside the
// datasheet's timer condition. Same worst-case preamble as the presets above.
proposed.enabled = 1;
level = RX_POWERSAVING_OVERDRIVE_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
preamble_overridden = true;
} else if (strcmp(value, "riskyWorkingMax") == 0) {
// This one is not lossless, so the risk is explicit in its name. Same
// worst-case preamble as the presets above.
proposed.enabled = 1;
level = RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
preamble_overridden = true;
} else if (strcmp(value, "balanced") == 0) {
proposed.enabled = 1;
level = RX_POWERSAVING_BALANCED_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
preamble_overridden = true;
} else {
char input[128];
if (strlen(value) >= sizeof(input)) {
snprintf(reply, reply_size,
"ERROR: use off|on|conservative|balanced|max|overdrive|riskyWorkingMax|level <1-10>|<rx_us> <sleep_us>");
return false;
}
strcpy(input, value);
const char* parts[4];
int count = mesh::Utils::parseTextParts(input, parts, 4, ' ');
uint32_t first = 0;
uint32_t second = 0;
if (count == 1 && parseRxPowerSavingUint32(parts[0], &first)) {
if (first < 1 || first > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL) {
snprintf(reply, reply_size, "ERROR: level range is 1-%u (or max|overdrive|riskyWorkingMax); preamble is 16 or 32",
RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL);
return false;
}
level = (uint8_t)first;
level_requested = true;
proposed.enabled = 1;
} else if (count >= 2 && strcmp(parts[1], "preamble") == 0 &&
(strcmp(parts[0], "max") == 0 || strcmp(parts[0], "overdrive") == 0 ||
strcmp(parts[0], "riskyWorkingMax") == 0)) {
if (count != 3 || !parseRxPowerSavingUint32(parts[2], &first) ||
(first != 16 && first != 32)) {
snprintf(reply, reply_size, "ERROR: use %s preamble <16|32>", parts[0]);
return false;
}
level = strcmp(parts[0], "overdrive") == 0 ? RX_POWERSAVING_OVERDRIVE_LEVEL
: strcmp(parts[0], "max") == 0 ? RX_POWERSAVING_MAX_LEVEL
: RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL;
preamble = (uint8_t)first;
level_requested = true;
preamble_overridden = true;
proposed.enabled = 1;
} else if (count == 2 && strcmp(parts[0], "level") == 0 &&
parseRxPowerSavingUint32(parts[1], &first)) {
if (first < 1 || first > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL) {
snprintf(reply, reply_size, "ERROR: level range is 1-%u (or max|overdrive|riskyWorkingMax); preamble is 16 or 32",
RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL);
return false;
}
level = (uint8_t)first;
level_requested = true;
proposed.enabled = 1;
} else if (count == 4 && strcmp(parts[0], "level") == 0 &&
parseRxPowerSavingUint32(parts[1], &first) &&
strcmp(parts[2], "preamble") == 0 &&
parseRxPowerSavingUint32(parts[3], &second)) {
if (first < 1 || first > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL ||
(second != 16 && second != 32)) {
snprintf(reply, reply_size, "ERROR: level range is 1-%u (or max|overdrive|riskyWorkingMax); preamble is 16 or 32",
RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL);
return false;
}
level = (uint8_t)first;
preamble = (uint8_t)second;
level_requested = true;
preamble_overridden = true;
proposed.enabled = 1;
} else if (count == 2 && parseRxPowerSavingUint32(parts[0], &first) &&
parseRxPowerSavingUint32(parts[1], &second)) {
proposed.rx_us = first;
proposed.sleep_us = second;
proposed.enabled = 1;
} else {
snprintf(reply, reply_size,
"ERROR: use off|on|conservative|balanced|max|overdrive|riskyWorkingMax|level <1-10>|<rx_us> <sleep_us>");
return false;
}
}
if (proposed.enabled && (sf < 5 || sf > 12 || !isfinite(bw) || bw <= 0)) {
if (control) {
control->setRxPowerSaving(false, proposed.rx_us, proposed.sleep_us);
control->setRxPowerSavingConfigError(true);
}
snprintf(reply, reply_size, "ERROR: invalid SF/BW for RXPS");
return false;
}
if (level_requested &&
!recalcRxPowerSavingFromLevel(level, sf, bw, preamble,
&proposed.rx_us, &proposed.sleep_us,
rxPowerSavingCaptureCost(control),
rxPowerSavingTransition(control))) {
// Level and preamble were validated above, so the only way to get here is
// that no sleep exists which both arms the hardware and still lets a
// preamble of this length be caught - i.e. the symbol is too short for RXPS
// at this SF/BW. Saying "level range is 1-10" would send the user hunting
// for the wrong thing.
if (control) {
control->setRxPowerSaving(false, proposed.rx_us, proposed.sleep_us);
control->setRxPowerSavingConfigError(true);
}
snprintf(reply, reply_size, "ERROR: RXPS does not fit this SF/BW with preamble %lu",
(unsigned long)preamble);
return false;
}
if (!isValidRxPowerSavingPeriod(proposed.rx_us) ||
!isValidRxPowerSavingPeriod(proposed.sleep_us)) {
snprintf(reply, reply_size, "ERROR: range is %lu-%lu us",
(unsigned long)RX_POWERSAVING_MIN_PERIOD_US,
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
return false;
}
bool applied = control != nullptr
? control->setRxPowerSaving(proposed.enabled != 0, proposed.rx_us, proposed.sleep_us)
: proposed.enabled == 0;
if (!applied) {
snprintf(reply, reply_size, "ERROR: RX powersaving unsupported");
return false;
}
if (control) control->setRxPowerSavingConfigError(false);
if (level_requested) {
proposed.level = level;
proposed.preamble = preamble_overridden ? preamble : 0;
} else if (strcmp(value, "off") != 0) {
proposed.level = 0;
proposed.preamble = 0;
}
*config = proposed;
snprintf(reply, reply_size, "OK - %s,level=%lu%s,preamble=%lu,rx=%lu,sleep=%lu",
config->enabled ? "on" : "off", (unsigned long)config->level,
rxPowerSavingLevelTag(config->level),
(unsigned long)config->preamble, (unsigned long)config->rx_us,
(unsigned long)config->sleep_us);
return true;
}
void RXPowerSavingCLI::get(const RxPowerSavingConfig* config,
const RxPowerSavingControl* control,
uint8_t sf, float bw, char* reply, size_t reply_size) {
if (config == nullptr || reply == nullptr || reply_size == 0) return;
RxPowerSavingStatus status = control != nullptr
? control->getRxPowerSavingStatus()
: RxPowerSavingStatus{};
// Preamble symbols the node actually catches. Not simply the number the level
// asked for: the sleep floor can shorten the sleep further, and at SF6 with
// the 16-symbol profile that collapses the first few levels onto the same
// point. Reporting the requested value there would be a lie.
char margin[24];
margin[0] = 0;
if (status.last_error == 0 && isValidRxPowerSavingPeriod(config->sleep_us) &&
config->level >= 1 && config->level <= RX_POWERSAVING_GUARDED_LEVELS &&
sf >= 5 && sf <= 12 && isfinite(bw) && bw > 0.0f) {
const uint8_t preamble = config->preamble ? config->preamble : rxPowerSavingPreambleForSF(sf);
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
const float effective = (float)preamble - (float)config->sleep_us / symbol_us;
// No %f on this platform's printf, and rounding the whole and fractional
// parts separately overflows: 4.999 came out as "4.10".
const float scaled = effective * 10.0f + (effective < 0.0f ? -0.5f : 0.5f);
if (isfinite(scaled) && scaled > (float)INT_MIN && scaled < (float)INT_MAX) {
const int tenths = (int)scaled;
snprintf(margin, sizeof(margin), ",catch=%d.%d", tenths / 10,
tenths < 0 ? -(tenths % 10) : tenths % 10);
}
}
// Overdrive has to be visible here, otherwise it reads as an ordinary level
// and nobody remembers it runs outside the datasheet rule.
int len = snprintf(reply, reply_size,
"> desired=%s,effective=%s,supported=%s,err=%d,fail=%lu,level=%lu%s%s,preamble=%lu,rx=%lu,sleep=%lu",
config->enabled ? "on" : "off", status.armed ? "armed" : "continuous",
status.supported ? "yes" : "no", (int)status.last_error,
(unsigned long)status.arm_failures, (unsigned long)config->level,
rxPowerSavingLevelTag(config->level), margin,
(unsigned long)config->preamble, (unsigned long)config->rx_us,
(unsigned long)config->sleep_us);
// Only report the periods the hardware really runs with when a driver had to
// clamp them - printing them always would not fit the 160 byte reply buffer.
if (len > 0 && (size_t)len < reply_size && status.armed &&
(status.effective_rx_us != config->rx_us ||
status.effective_sleep_us != config->sleep_us)) {
snprintf(reply + len, reply_size - (size_t)len, ",erx=%lu,eslp=%lu",
(unsigned long)status.effective_rx_us,
(unsigned long)status.effective_sleep_us);
}
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <stddef.h>
#include "RXPowerSaving.h"
class RXPowerSavingCLI {
public:
static bool set(const char* value, uint8_t sf, float bw, RxPowerSavingConfig* config,
RxPowerSavingControl* control, char* reply, size_t reply_size);
// sf/bw are needed to turn the stored periods back into symbols, which is
// what the level scale is expressed in.
static void get(const RxPowerSavingConfig* config, const RxPowerSavingControl* control,
uint8_t sf, float bw, char* reply, size_t reply_size);
};
+320 -45
View File
@@ -11,6 +11,13 @@
#define NUM_NOISE_FLOOR_SAMPLES 64
#define SAMPLING_THRESHOLD 14
// PowerSaving variant
#define NF_CALIB_INTERVAL_MS 60000UL
#define NF_CALIB_TIMEOUT_MS 5000UL
#define NF_CALIB_SETTLE_MS 20UL
#define NF_CALIB_SAMPLE_INTERVAL_MS 1UL
#define NF_CALIB_MAX_SAMPLE_ATTEMPTS (NUM_NOISE_FLOOR_SAMPLES * 4U)
static volatile uint8_t state = STATE_IDLE;
// this function is called when a complete packet
@@ -48,17 +55,39 @@ uint32_t RadioLibWrapper::getRngSeed() {
}
void RadioLibWrapper::setTxPower(int8_t dbm) {
// PowerSaving variant
prepareForRadioConfig();
#if defined(USE_LR2021)
// PowerSaving variant
// On LR2021, setOutputPower() writes the PA config and TxParams, which are
// standby-only commands. recvRaw() keeps state == STATE_RX after readData on
// this platform ("LR2021 stays in Rx"), so - unlike the SX126x path, which
// falls back to STATE_IDLE and re-arms on the next recvRaw() - nothing calls
// startReceive() again by itself. Writing the PA config while listening could
// therefore leave the receiver down until the next reboot. Drop to standby
// first and let checkRecv() re-arm Rx, as resetAGC() and
// applySideDetectorConfig() already do.
idle();
#endif
_radio->setOutputPower(dbm);
}
void RadioLibWrapper::idle() {
// PowerSaving variant
if (_rx_ps_armed) stopReceiveDutyCycle();
_radio->standby();
state = STATE_IDLE; // need another startReceive()
}
// PowerSaving variant
void RadioLibWrapper::powerOff() {
if (_rx_ps_armed) stopReceiveDutyCycle();
_radio->sleep();
}
void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
_threshold = threshold;
if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling
@@ -72,40 +101,150 @@ void RadioLibWrapper::doResetAGC() {
}
void RadioLibWrapper::resetAGC() {
// make sure we're not mid-receive of packet!
if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return;
// PowerSaving variant
// make sure we're not mid-receive or mid-transmit of a packet
if (isPacketPendingOrReceiving() || state == STATE_TX_WAIT) return;
// PowerSaving variant
if (_rx_ps_armed) stopReceiveDutyCycle();
doResetAGC();
state = STATE_IDLE; // trigger a startReceive()
state = STATE_IDLE;
// Reset noise floor sampling so it reconverges from scratch.
// Without this, a stuck _noise_floor of -120 makes the sampling threshold
// too low (-106) to accept normal samples (~-105), self-reinforcing the
// stuck value even after the receiver has recovered.
// PowerSaving variant
// Recalibrate synchronously so callers never observe the temporary zero
// used to bypass a stale sampling threshold after an AGC reset.
const int16_t previous_noise_floor = _noise_floor;
_noise_floor = 0;
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
void RadioLibWrapper::loop() {
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
if (!isReceivingPacket()) {
int rssi = getCurrentRSSI();
if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD
_num_floor_samples++;
_floor_sample_sum += rssi;
// PowerSaving variant
_nf_calib_active = true; // force startReceiveMode() into continuous RX
bool packet_in_progress = false;
int16_t err = startReceiveMode();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
delay(NF_CALIB_SETTLE_MS);
uint16_t sample_attempts = 0;
while (_num_floor_samples < NUM_NOISE_FLOOR_SAMPLES &&
sample_attempts++ < NF_CALIB_MAX_SAMPLE_ATTEMPTS) {
if (isPacketPendingOrReceiving()) {
packet_in_progress = true;
break;
}
sampleNoiseFloorOnce();
if (_num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
delay(NF_CALIB_SAMPLE_INTERVAL_MS);
}
}
} else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) {
_noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
}
_floor_sample_sum = 0;
}
#ifdef MESH_DEBUG_NOISE_FLOOR
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
#endif
if (!publishNoiseFloor()) {
_noise_floor = previous_noise_floor;
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
_nf_calib_active = false;
_nf_last_calib = millis();
// A frame can start after resetAGC()'s initial idle check. Keep continuous
// RX alive until recvRaw() consumes it; restarting RX here would abort a
// frame that is between preamble detection and RX_DONE.
if (!packet_in_progress) packet_in_progress = isPacketPendingOrReceiving();
if (!packet_in_progress) requestRestartRecv();
}
// PowerSaving variant
// Clear the RX/idle state so the next loop calls startRecv() again, without
// dropping an STATE_INT_READY that the ISR may raise while we are in here.
// A plain `state = STATE_IDLE` loses that flag (and with it, a received
// packet) when setFlag() fires between the test and the store.
void RadioLibWrapper::requestRestartRecv() {
noInterrupts();
if ((state & ~STATE_INT_READY) != STATE_TX_WAIT) {
state &= STATE_INT_READY; // STATE_IDLE, but keep a pending interrupt
}
interrupts();
}
// PowerSaving variant
bool RadioLibWrapper::isPacketPendingOrReceiving() {
return (state & STATE_INT_READY) != 0 || isReceivingPacket();
}
// PowerSaving variant
void RadioLibWrapper::sampleNoiseFloorOnce() {
int rssi = getCurrentRSSI();
if (rssi < _noise_floor + SAMPLING_THRESHOLD) {
_num_floor_samples++;
_floor_sample_sum += rssi;
}
}
// PowerSaving variant
bool RadioLibWrapper::publishNoiseFloor() {
if (_num_floor_samples < NUM_NOISE_FLOOR_SAMPLES || _floor_sample_sum == 0) return false;
_noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
}
_floor_sample_sum = 0;
#ifdef MESH_DEBUG_NOISE_FLOOR
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
#endif
return true;
}
// PowerSaving variant
void RadioLibWrapper::noiseFloorCalibCheck() {
unsigned long now = millis();
if (_nf_calib_active) {
if (!_rx_ps_enabled || (long)(now - _nf_calib_deadline) >= 0) {
endNoiseFloorCalib(now);
} else if (_rx_ps_armed && !isPacketPendingOrReceiving()) {
// A packet may have delayed the RXPS-to-continuous-RX transition.
requestRestartRecv();
}
} else if (_rx_ps_enabled && _rx_ps_armed && state == STATE_RX &&
(_nf_last_calib == 0 || now - _nf_last_calib >= NF_CALIB_INTERVAL_MS) &&
!isPacketPendingOrReceiving()) {
_nf_calib_active = true;
_nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS;
_nf_sample_from = now + NF_CALIB_SETTLE_MS;
_num_floor_samples = 0;
_floor_sample_sum = 0;
if (!isPacketPendingOrReceiving()) requestRestartRecv();
}
}
// PowerSaving variant
void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) {
_nf_calib_active = false;
_nf_last_calib = now;
if (!isPacketPendingOrReceiving()) requestRestartRecv();
}
// PowerSaving variant
void RadioLibWrapper::loop() {
if (_rx_ps_restart_pending && (state & ~STATE_INT_READY) != STATE_TX_WAIT &&
!isPacketPendingOrReceiving()) {
_rx_ps_restart_pending = false;
requestRestartRecv();
}
noiseFloorCalibCheck();
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
if (!_rx_ps_armed && !(_nf_calib_active && (long)(millis() - _nf_sample_from) < 0) &&
!isReceivingPacket()) {
sampleNoiseFloorOnce();
}
} else if (publishNoiseFloor()) {
if (_nf_calib_active) endNoiseFloorCalib(millis());
}
}
@@ -113,7 +252,9 @@ void RadioLibWrapper::startRecv() {
#if defined(USE_LR2021)
_radio->standby(); // without this LR2021 can throw -706 when calling startReceive after hardware CAD when side detectors are enabled
#endif
int err = _radio->startReceive();
// PowerSaving variant
int err = startReceiveMode();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
} else {
@@ -121,6 +262,123 @@ void RadioLibWrapper::startRecv() {
}
}
// PowerSaving variant
int16_t RadioLibWrapper::startReceiveMode() {
// Periodic calibration switches RXPS to continuous RX. Re-check at the
// hardware transition so a preamble that arrived after the scheduler's
// idle check is not aborted by stopReceiveDutyCycle().
if (_nf_calib_active && _rx_ps_armed && isPacketPendingOrReceiving()) {
return RADIOLIB_ERR_NONE;
}
if (_rx_ps_armed) stopReceiveDutyCycle();
if (!_rx_ps_enabled || _nf_calib_active) {
_rx_ps_armed = false;
const int16_t err = startContinuousReceive();
_rx_ps_last_error = err;
return err;
}
if (!_rx_ps_arm_retry.canAttempt()) {
// arming has failed repeatedly; stop paying for a doomed SPI round-trip on
// every RX restart. A config change (setRxPowerSaving) re-enables retries.
_rx_ps_armed = false;
const int16_t err = startContinuousReceive();
if (err != RADIOLIB_ERR_NONE) _rx_ps_last_error = err;
return err;
}
const RadioLibIrqFlags_t irq_flags =
RADIOLIB_IRQ_RX_DEFAULT_FLAGS |
(1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED);
const RadioLibIrqFlags_t irq_mask =
(1UL << RADIOLIB_IRQ_RX_DONE) |
(1UL << RADIOLIB_IRQ_TIMEOUT) |
(1UL << RADIOLIB_IRQ_CRC_ERR) |
(1UL << RADIOLIB_IRQ_HEADER_ERR);
uint32_t eff_rx_us = _rx_ps_rx_us;
uint32_t eff_sleep_us = _rx_ps_sleep_us;
int16_t err = armDutyCycle(irq_flags, irq_mask, &eff_rx_us, &eff_sleep_us);
if (err == RADIOLIB_ERR_NONE) {
_rx_ps_armed = true;
_rx_ps_last_error = RADIOLIB_ERR_NONE;
_rx_ps_arm_retry.recordSuccess();
_rx_ps_eff_rx_us = eff_rx_us;
_rx_ps_eff_sleep_us = eff_sleep_us;
return err;
}
_rx_ps_armed = false;
_rx_ps_last_error = err;
_rx_ps_eff_rx_us = 0;
_rx_ps_eff_sleep_us = 0;
n_rx_ps_arm_failures++;
_rx_ps_arm_retry.recordFailure();
MESH_DEBUG_PRINTLN("RadioLibWrapper: startReceiveDutyCycle(%d), continuous RX fallback", err);
stopDutyCycleHardware();
int16_t fallback_err = startContinuousReceive();
if (fallback_err != RADIOLIB_ERR_NONE) _rx_ps_last_error = fallback_err;
return fallback_err;
}
// PowerSaving variant
void RadioLibWrapper::stopReceiveDutyCycle() {
int16_t err = stopDutyCycleHardware();
_rx_ps_armed = false;
_rx_ps_eff_rx_us = 0;
_rx_ps_eff_sleep_us = 0;
if (err != RADIOLIB_ERR_NONE) _rx_ps_last_error = err;
}
// PowerSaving variant
bool RadioLibWrapper::isPacketReady() {
if (!_rx_ps_armed) return true;
return _radio->checkIrq(RADIOLIB_IRQ_RX_DONE) != 0;
}
// PowerSaving variant
void RadioLibWrapper::prepareForRadioConfig() {
if (!_rx_ps_armed) return;
stopReceiveDutyCycle();
requestRestartRecv();
}
// PowerSaving variant
bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) {
if (enabled && (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us))) {
_rx_ps_enabled = false;
_rx_ps_last_error = RX_POWERSAVING_ERR_TIMING;
requestRestartRecv();
return false;
}
if (enabled && !supportsRxPowerSaving()) return false;
_rx_ps_enabled = enabled;
_rx_ps_rx_us = rx_us;
_rx_ps_sleep_us = sleep_us;
_rx_ps_last_error = RADIOLIB_ERR_NONE;
_rx_ps_arm_retry.reset(); // new config, give the hardware a fresh chance
_rx_ps_restart_pending = true;
if ((state & ~STATE_INT_READY) != STATE_TX_WAIT && !isPacketPendingOrReceiving()) {
_rx_ps_restart_pending = false;
requestRestartRecv();
}
return true;
}
// PowerSaving variant
RxPowerSavingStatus RadioLibWrapper::getRxPowerSavingStatus() const {
RxPowerSavingStatus status;
status.supported = supportsRxPowerSaving();
status.armed = _rx_ps_armed;
status.last_error = _rx_ps_config_error ? RX_POWERSAVING_ERR_TIMING : _rx_ps_last_error;
status.arm_failures = n_rx_ps_arm_failures;
status.effective_rx_us = _rx_ps_eff_rx_us;
status.effective_sleep_us = _rx_ps_eff_sleep_us;
return status;
}
bool RadioLibWrapper::isInRecvMode() const {
return (state & ~STATE_INT_READY) == STATE_RX;
}
@@ -128,17 +386,25 @@ bool RadioLibWrapper::isInRecvMode() const {
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
int len = 0;
if (state & STATE_INT_READY) {
len = _radio->getPacketLength();
if (len > 0) {
if (len > sz) { len = sz; }
int err = _radio->readData(bytes, len);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
len = 0;
n_recv_errors++;
} else {
// Serial.print(" readData() -> "); Serial.println(len);
n_recv++;
// PowerSaving variant
if (isPacketReady()) {
if (_rx_ps_armed) stopReceiveDutyCycle();
len = _radio->getPacketLength();
if (len > 0) {
if (len > sz) { len = sz; }
// PowerSaving variant
_last_snr = _radio->getSNR();
_last_rssi = _radio->getRSSI();
_last_metrics_valid = true;
int err = _radio->readData(bytes, len);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
len = 0;
n_recv_errors++;
} else {
// Serial.print(" readData() -> "); Serial.println(len);
n_recv++;
}
}
}
#if defined(USE_LR2021)
@@ -149,12 +415,8 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
}
if (state != STATE_RX) {
int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
} else {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
}
// PowerSaving variant
startRecv();
}
return len;
}
@@ -164,6 +426,9 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {
}
bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
// PowerSaving variant
if (_rx_ps_armed) stopReceiveDutyCycle();
_board->onBeforeTransmit();
int err = _radio->startTransmit((uint8_t *) bytes, len);
if (err == RADIOLIB_ERR_NONE) {
@@ -196,17 +461,23 @@ int16_t RadioLibWrapper::performChannelScan() {
}
bool RadioLibWrapper::isChannelActive() {
// PowerSaving variant
// int.thresh: RSSI-based interference detection (relative to noise floor)
if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true;
if (_threshold != 0 && !(_rx_ps_armed && isChipBusy()) &&
getCurrentRSSI() > _noise_floor + _threshold) return true;
// cad: hardware channel activity detection
if (_cad_enabled) {
// PowerSaving variant
if (_rx_ps_armed) stopReceiveDutyCycle();
int16_t result = performChannelScan();
// scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY
// via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't
// try to read a non-existent packet and count a spurious recv error.
state = STATE_IDLE;
startRecv();
// PowerSaving variant
if (result == RADIOLIB_LORA_DETECTED) ++_cad_busy_count;
if (result != RADIOLIB_CHANNEL_FREE) return true;
}
@@ -214,10 +485,14 @@ bool RadioLibWrapper::isChannelActive() {
}
float RadioLibWrapper::getLastRSSI() const {
return _radio->getRSSI();
// PowerSaving variant
if (_last_metrics_valid) return _last_rssi;
return _rx_ps_armed ? 0 : _radio->getRSSI();
}
float RadioLibWrapper::getLastSNR() const {
return _radio->getSNR();
// PowerSaving variant
if (_last_metrics_valid) return _last_snr;
return _rx_ps_armed ? 0 : _radio->getSNR();
}
// Approximate SNR threshold per SF for successful reception (based on Semtech datasheets)
+87 -4
View File
@@ -2,6 +2,8 @@
#include <Mesh.h>
#include <RadioLib.h>
// PowerSaving variant
#include "RXPowerSaving.h"
#ifdef USE_CC310_HW_CRYPTO
#include <Adafruit_nRFCrypto.h>
@@ -11,28 +13,89 @@ struct PacketMillis {
uint32_t payloadMillis; // header-valid -> rx-done deadline
};
class RadioLibWrapper : public mesh::Radio {
// PowerSaving variant
class RadioLibWrapper : public mesh::Radio, public RxPowerSavingControl {
protected:
PhysicalLayer* _radio;
mesh::MainBoard* _board;
uint32_t n_recv, n_sent, n_recv_errors;
// PowerSaving variant
uint32_t _cad_busy_count = 0;
int16_t _noise_floor, _threshold;
// PowerSaving variant
float _last_rssi, _last_snr;
bool _last_metrics_valid;
bool _cad_enabled;
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
uint8_t _preamble_sf;
// PowerSaving variant
bool _rx_ps_restart_pending = false;
bool _rx_ps_config_error = false;
bool _rx_ps_enabled;
bool _rx_ps_armed;
uint32_t _rx_ps_rx_us;
uint32_t _rx_ps_sleep_us;
uint32_t _rx_ps_eff_rx_us; // periods the driver actually armed with
uint32_t _rx_ps_eff_sleep_us;
int16_t _rx_ps_last_error;
uint32_t n_rx_ps_arm_failures;
RxPowerSavingArmRetryState _rx_ps_arm_retry;
bool _nf_calib_active;
unsigned long _nf_last_calib;
unsigned long _nf_calib_deadline;
unsigned long _nf_sample_from;
void idle();
void startRecv();
// PowerSaving variant
void requestRestartRecv();
bool isPacketPendingOrReceiving();
void prepareForRadioConfig();
void sampleNoiseFloorOnce();
bool publishNoiseFloor();
void noiseFloorCalibCheck();
void endNoiseFloorCalib(unsigned long now);
int16_t startReceiveMode();
void stopReceiveDutyCycle();
// eff_rx_us/eff_sleep_us report the periods the driver really programmed,
// which may be clamped to satisfy a hardware constraint.
virtual int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags,
RadioLibIrqFlags_t irq_mask,
uint32_t* eff_rx_us, uint32_t* eff_sleep_us) {
(void)irq_flags;
(void)irq_mask;
(void)eff_rx_us;
(void)eff_sleep_us;
return RADIOLIB_ERR_UNSUPPORTED;
}
virtual int16_t stopDutyCycleHardware() { return _radio->standby(); }
virtual int16_t startContinuousReceive() { return _radio->startReceive(); }
virtual bool isPacketReady();
virtual bool isChipBusy() { return false; }
float packetScoreInt(float snr, int sf, int packet_len);
virtual bool isReceivingPacket() =0;
virtual void doResetAGC();
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }
// PowerSaving variant
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board)
: _radio(&radio), _board(&board), _last_rssi(0), _last_snr(0), _last_metrics_valid(false),
_preamble_sf(0), _rx_ps_enabled(false), _rx_ps_armed(false),
_rx_ps_rx_us(RX_POWERSAVING_DEFAULT_RX_US),
_rx_ps_sleep_us(RX_POWERSAVING_DEFAULT_SLEEP_US), _rx_ps_eff_rx_us(0),
_rx_ps_eff_sleep_us(0), _rx_ps_last_error(RADIOLIB_ERR_NONE),
n_rx_ps_arm_failures(0), _nf_calib_active(false),
_nf_last_calib(0), _nf_calib_deadline(0), _nf_sample_from(0) {
n_recv = n_sent = n_recv_errors = 0;
}
void begin() override;
virtual void powerOff() { _radio->sleep(); }
virtual void powerOff();
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;
bool startSendRaw(const uint8_t* bytes, int len) override;
@@ -41,6 +104,15 @@ public:
bool isInRecvMode() const override;
bool isChannelActive();
// PowerSaving variant
virtual bool supportsRxPowerSaving() const { return false; }
void setRxPowerSavingConfigError(bool invalid) override { _rx_ps_config_error = invalid; }
bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) override;
RxPowerSavingStatus getRxPowerSavingStatus() const override;
bool isRxPowerSavingCalibrationActive() const override { return _nf_calib_active; }
bool hasPendingReceive() { return isPacketPendingOrReceiving(); }
bool isReceiving() override {
if (isReceivingPacket()) return true;
@@ -65,10 +137,21 @@ public:
void loop() override;
// PowerSaving variant
uint32_t getCADBusyCount() const { return _cad_busy_count; }
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsRecvErrors() const { return n_recv_errors; }
uint32_t getPacketsSent() const { return n_sent; }
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
// PowerSaving variant
void resetStats() {
n_recv = n_sent = n_recv_errors = n_rx_ps_arm_failures = 0;
_cad_busy_count = 0;
// Clearing diagnostics also grants a fresh set of arm attempts. If they
// fail again, the normal three-failure continuous-RX fallback applies.
_rx_ps_arm_retry.reset();
}
virtual float getLastRSSI() const override;
virtual float getLastSNR() const override;