RXPS: use measured preamble-symbol capture model

This commit is contained in:
Jarosław Domański committed 2026-09-05 00:02:13 +02:00
1 parent fde20d37d0
commit 6bbd34a296
15 files changed
+1090 -189

No files matched your search

+10 -7
View File
@@ -655,8 +655,6 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
strcpy(reply, "Error: state must be on or off");
}
} else if (strncmp(config, "radio.rxps.rfrx_disabled ", 25) == 0) {
RXPowerSavingCLI::setRfRxDisabled(&config[25], _rxps_control, reply, 160);
} else if (memcmp(config, "radio.rxps ", 11) == 0) {
if (RXPowerSavingCLI::set(&config[11], _prefs->sf, _prefs->bw, &_prefs->rxps,
_rxps_control, reply, 160)) {
@@ -677,7 +675,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->bw = bw;
bool rxps_retuned = recalcRxPowerSavingFromLevel(
_prefs->rxps.level, _prefs->sf, _prefs->bw, _prefs->rxps.preamble,
&_prefs->rxps.rx_us, &_prefs->rxps.sleep_us);
&_prefs->rxps.rx_us, &_prefs->rxps.sleep_us,
rxPowerSavingCaptureCost(_rxps_control), rxPowerSavingTransition(_rxps_control));
_callbacks->savePrefs();
strcpy(reply, rxps_retuned ? "OK - reboot to apply (rxps retuned)" : "OK - reboot to apply");
} else {
@@ -949,10 +948,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
} else {
sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off");
}
} else if (strcmp(config, "radio.rxps.rfrx_disabled") == 0) {
RXPowerSavingCLI::getRfRxDisabled(_rxps_control, reply, 160);
} else if (strcmp(config, "radio.rxps") == 0) {
RXPowerSavingCLI::get(&_prefs->rxps, _rxps_control, reply, 160);
} else if (memcmp(config, "radio.rxps", 10) == 0 &&
(config[10] == 0 || config[10] == ' ')) {
// Exact match plus an optional trailing space, the same shape as the `tx`
// key below. A plain strcmp() looks stricter but is a trap here: `get
// radio.rxps ` fails it, falls through to the `radio` prefix branch further
// down and cheerfully answers with the frequency and bandwidth. Every other
// key in this chain is a prefix match and so never noticed the space.
RXPowerSavingCLI::get(&_prefs->rxps, _rxps_control, _prefs->sf, _prefs->bw, reply, 160);
} else if (memcmp(config, "radio", 5) == 0) {
char freq[16], bw[16];
strcpy(freq, StrHelper::ftoa(_prefs->freq));
+21
View File
@@ -14,6 +14,23 @@ class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
// 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);
if (state == RADIOLIB_ERR_NONE) applyMeshCoreTcxoDelay();
return state;
}
void applyMeshCoreTcxoDelay() {
if (tcxoVoltage <= 0.0f) return;
setTCXO(tcxoVoltage, MC_TCXO_DELAY_US);
calibrate(0x3F); // all blocks; setTCXO moved the gating window
delay(50);
}
size_t getPacketLength(bool update) override {
size_t len = LR1110::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
@@ -29,6 +46,10 @@ class CustomLR1110 : public LR1110 {
float getFreqMHz() const { return freqMHz; }
// 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
@@ -6,6 +6,20 @@
class CustomLR1110Wrapper : public RadioLibWrapper {
public:
// 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;
}
// 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 {
+32 -9
View File
@@ -2,17 +2,47 @@
#include <RadioLib.h>
#include "MeshCore.h"
#include "RXPowerSaving.h" // MC_TCXO_DELAY_US
class CustomSX1262 : public SX1262 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_ps_rf_rx_disabled = false;
public:
// 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) { }
// 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) applyMeshCoreTcxoDelay();
return state;
}
// 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.
void applyMeshCoreTcxoDelay() {
if (tcxoVoltage <= 0.0f) return;
setTCXO(tcxoVoltage, MC_TCXO_DELAY_US);
calibrate(RADIOLIB_SX126X_CALIBRATE_ALL);
delay(50);
}
#ifdef RP2040_PLATFORM
bool std_init(SPIClassRP2040* spi = NULL)
#else
@@ -110,7 +140,7 @@ class CustomSX1262 : public SX1262 {
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 && !_rx_ps_rf_rx_disabled) {
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.
@@ -119,13 +149,6 @@ class CustomSX1262 : public SX1262 {
return state;
}
void setRxPowerSavingRfRxDisabled(bool disabled) {
_rx_ps_rf_rx_disabled = disabled;
}
bool isRxPowerSavingRfRxDisabled() const {
return _rx_ps_rf_rx_disabled;
}
bool isChipBusy() {
uint32_t busy = this->mod->getGpio();
return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy);
+9 -19
View File
@@ -10,6 +10,15 @@
class CustomSX1262Wrapper : public RadioLibWrapper {
public:
// 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 {
@@ -51,25 +60,6 @@ public:
bool supportsRxPowerSaving() const override { return true; }
bool supportsRxPowerSavingRfRxDisable() const override {
#if defined(SX126X_RXEN)
return SX126X_RXEN != RADIOLIB_NC;
#else
return false;
#endif
}
bool setRxPowerSavingRfRxDisabled(bool disabled) override {
if (!supportsRxPowerSavingRfRxDisable()) return false;
prepareForRadioConfig();
((CustomSX1262 *)_radio)->setRxPowerSavingRfRxDisabled(disabled);
return true;
}
bool isRxPowerSavingRfRxDisabled() const override {
return ((CustomSX1262 *)_radio)->isRxPowerSavingRfRxDisabled();
}
protected:
int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, RadioLibIrqFlags_t irq_mask,
uint32_t* eff_rx_us, uint32_t* eff_sleep_us) override {
+159 -16
View File
@@ -5,6 +5,29 @@ static uint32_t ceilRxPowerSavingValue(float 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;
}
@@ -22,25 +45,139 @@ bool isRxPowerSavingNumeric(const char* 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) {
if (rx_us == nullptr || sleep_us == nullptr || level < 1 || level > 10 || sf < 5 || sf > 12 ||
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 ||
bw <= 0.0f || (preamble != 16 && preamble != 32)) {
return false;
}
const bool unguarded = isRxPowerSavingUnguardedLevel(level);
const float symbol_us = (1000.0f * (float)(1UL << sf)) / bw;
const float amount = (float)(level - 1) / 9.0f;
const float rx_start_symbols = preamble == 16 ? 12.0f : 16.0f;
const float sleep_start_symbols = preamble == 16 ? 2.0f : 15.0f;
const float rx_edge_symbols = 8.0f;
const float sleep_edge_symbols = (float)preamble + 4.25f - 8.0f;
// 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;
const float rx_symbols = rx_start_symbols + amount * (rx_edge_symbols - rx_start_symbols);
const float sleep_symbols = sleep_start_symbols + amount * (sleep_edge_symbols - sleep_start_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;
}
*rx_us = ceilRxPowerSavingValue(rx_symbols * symbol_us);
*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;
}
@@ -50,13 +187,15 @@ 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) {
if (level < 1 || level > 10) return false;
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) ||
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;
@@ -67,15 +206,19 @@ bool recalcRxPowerSavingFromLevel(uint8_t level, uint8_t sf, float bw, uint8_t p
return true;
}
void normalizeRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw) {
void normalizeRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw,
float capture_cost_symbols, uint32_t transition_us) {
if (config == nullptr) return;
config->enabled = config->enabled ? 1 : 0;
if (config->level > 10) config->level = RX_POWERSAVING_BALANCED_LEVEL;
if (config->level > RX_POWERSAVING_RISKY_WORKING_MAX_LEVEL) {
config->level = RX_POWERSAVING_BALANCED_LEVEL;
}
if (config->preamble != 0 && config->preamble != 16 && config->preamble != 32) {
config->preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
}
ensureRxPowerSavingDefaults(&config->rx_us, &config->sleep_us);
recalcRxPowerSavingFromLevel(config->level, sf, bw, config->preamble,
&config->rx_us, &config->sleep_us);
&config->rx_us, &config->sleep_us, capture_cost_symbols,
transition_us);
}
+201 -11
View File
@@ -7,11 +7,158 @@ 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;
static constexpr uint8_t RX_POWERSAVING_CONSERVATIVE_LEVEL = 1;
static constexpr uint8_t RX_POWERSAVING_BALANCED_LEVEL = 5;
// 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;
@@ -59,21 +206,64 @@ public:
return !enabled;
}
virtual RxPowerSavingStatus getRxPowerSavingStatus() const { return {}; }
virtual bool supportsRxPowerSavingRfRxDisable() const { return false; }
virtual bool setRxPowerSavingRfRxDisabled(bool disabled) {
(void)disabled;
return false;
}
virtual bool isRxPowerSavingRfRxDisabled() const { return false; }
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);
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);
void normalizeRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw);
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 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);
+96 -44
View File
@@ -5,6 +5,14 @@
#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;
@@ -39,6 +47,32 @@ bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
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;
@@ -49,7 +83,7 @@ bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
char input[128];
if (strlen(value) >= sizeof(input)) {
snprintf(reply, reply_size,
"ERROR: use off|on|conservative|balanced|level <1-10>|<rx_us> <sleep_us>");
"ERROR: use off|on|conservative|balanced|max|overdrive|riskyWorkingMax|level <1-10>|<rx_us> <sleep_us>");
return false;
}
strcpy(input, value);
@@ -59,17 +93,34 @@ bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
uint32_t first = 0;
uint32_t second = 0;
if (count == 1 && parseRxPowerSavingUint32(parts[0], &first)) {
if (first < 1 || first > 10) {
snprintf(reply, reply_size, "ERROR: level range is 1-10; preamble is 16 or 32");
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 > 10) {
snprintf(reply, reply_size, "ERROR: level range is 1-10; preamble is 16 or 32");
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;
@@ -79,8 +130,10 @@ bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
parseRxPowerSavingUint32(parts[1], &first) &&
strcmp(parts[2], "preamble") == 0 &&
parseRxPowerSavingUint32(parts[3], &second)) {
if (first < 1 || first > 10 || (second != 16 && second != 32)) {
snprintf(reply, reply_size, "ERROR: level range is 1-10; preamble is 16 or 32");
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;
@@ -95,15 +148,23 @@ bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
proposed.enabled = 1;
} else {
snprintf(reply, reply_size,
"ERROR: use off|on|conservative|balanced|level <1-10>|<rx_us> <sleep_us>");
"ERROR: use off|on|conservative|balanced|max|overdrive|riskyWorkingMax|level <1-10>|<rx_us> <sleep_us>");
return false;
}
}
if (level_requested &&
!calcRxPowerSavingLevel(level, sf, bw, preamble,
&proposed.rx_us, &proposed.sleep_us)) {
snprintf(reply, reply_size, "ERROR: level range is 1-10; preamble is 16 or 32");
&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.
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) ||
@@ -131,8 +192,9 @@ bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
}
*config = proposed;
snprintf(reply, reply_size, "OK - %s,level=%lu,preamble=%lu,rx=%lu,sleep=%lu",
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;
@@ -140,16 +202,37 @@ bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw,
void RXPowerSavingCLI::get(const RxPowerSavingConfig* config,
const RxPowerSavingControl* control,
char* reply, size_t reply_size) {
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 (config->level >= 1 && config->level <= RX_POWERSAVING_GUARDED_LEVELS &&
sf >= 5 && sf <= 12 && 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 int tenths = (int)(effective * 10.0f + (effective < 0.0f ? -0.5f : 0.5f));
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,level=%lu,preamble=%lu,rx=%lu,sleep=%lu,err=%d,fail=%lu",
"> desired=%s,effective=%s,supported=%s,level=%lu%s%s,preamble=%lu,rx=%lu,sleep=%lu,err=%d,fail=%lu",
config->enabled ? "on" : "off", status.armed ? "armed" : "continuous",
status.supported ? "yes" : "no", (unsigned long)config->level,
rxPowerSavingLevelTag(config->level), margin,
(unsigned long)config->preamble, (unsigned long)config->rx_us,
(unsigned long)config->sleep_us, (int)status.last_error,
(unsigned long)status.arm_failures);
@@ -164,34 +247,3 @@ void RXPowerSavingCLI::get(const RxPowerSavingConfig* config,
(unsigned long)status.effective_sleep_us);
}
}
void RXPowerSavingCLI::setRfRxDisabled(const char* value, RxPowerSavingControl* control,
char* reply, size_t reply_size) {
if (value == nullptr || reply == nullptr || reply_size == 0) return;
bool disabled;
if (strcmp(value, "on") == 0) {
disabled = true;
} else if (strcmp(value, "off") == 0) {
disabled = false;
} else {
snprintf(reply, reply_size, "Error: state must be on or off");
return;
}
if (control == nullptr || !control->setRxPowerSavingRfRxDisabled(disabled)) {
snprintf(reply, reply_size, "Error: unsupported");
} else {
snprintf(reply, reply_size, "OK - radio.rxps.rfrx_disabled %s", disabled ? "on" : "off");
}
}
void RXPowerSavingCLI::getRfRxDisabled(const RxPowerSavingControl* control,
char* reply, size_t reply_size) {
if (reply == nullptr || reply_size == 0) return;
if (control == nullptr || !control->supportsRxPowerSavingRfRxDisable()) {
snprintf(reply, reply_size, "Error: unsupported");
} else {
snprintf(reply, reply_size, "> %s", control->isRxPowerSavingRfRxDisabled() ? "on" : "off");
}
}
+3 -5
View File
@@ -8,10 +8,8 @@ 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,
char* reply, size_t reply_size);
static void setRfRxDisabled(const char* value, RxPowerSavingControl* control,
char* reply, size_t reply_size);
static void getRfRxDisabled(const RxPowerSavingControl* control,
char* reply, size_t reply_size);
uint8_t sf, float bw, char* reply, size_t reply_size);
};
-3
View File
@@ -93,9 +93,6 @@ public:
virtual bool supportsRxPowerSaving() const { return false; }
bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) override;
RxPowerSavingStatus getRxPowerSavingStatus() const override;
virtual bool supportsRxPowerSavingRfRxDisable() const override { return false; }
virtual bool setRxPowerSavingRfRxDisabled(bool) override { return false; }
virtual bool isRxPowerSavingRfRxDisabled() const override { return false; }
bool isRxPowerSavingCalibrationActive() const override { return _nf_calib_active; }
bool isReceiving() override {