Added RXPS
This commit is contained in:
1 parent
ab2231be9b
commit
2195d20b31
12 files changed
+1548
-56
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user