mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 17:41:19 +00:00
RXPS: use measured preamble-symbol capture model
This commit is contained in:
1 parent
fde20d37d0
commit
6bbd34a296
15 files changed
+1090
-189
No files matched your search
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user