mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-31 01:08:14 +00:00
feat(power): battery saving — hardware duty-cycle RX + adaptive TX power
Two independent, default-off toggles under Settings › Radio. Pwr save: hardware RX duty-cycle (SX126x SetRxDutyCycle via startReceiveDutyCycleAuto). The chip cycles RX↔sleep and wakes on a preamble — no MCU state machine; recvRaw reads the packet exactly as in continuous RX. Falls back to continuous RX on non-SX126x. (Replaces an earlier software-CAD state machine that fought the hardware: polling a warm-sleeping chip gave a phantom-busy channel that stalled TX ~4 s and dropped ACKs in the scan gaps.) Auto pwr: Adaptive Power Control. tx_power_dbm becomes a ceiling; actual TX power tracks the reverse-link SNR margin (measured above the per-SF demod floor, EWMA-smoothed, proportional step with a deadband). Feedback comes from direct / room-server ACKs and, for channels (no ACK), from hearing a repeater rebroadcast our own flood; a lost confirmation ramps power back up so channel sends can't get stranded below what the repeaters can hear. Prefs schema 0xC0DE0009 (rx_powersave, tx_apc). Radio page / name bar show the live TX power; noise floor reads n/a while duty-cycling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,17 @@ public:
|
||||
|
||||
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
|
||||
|
||||
// Power-save RX = hardware RX duty-cycle (SX126x SetRxDutyCycle, datasheet
|
||||
// 13.1.7). The chip's sequencer cycles RX↔sleep on its own, latches a preamble
|
||||
// of the configured length and then stays in RX to receive the packet, raising
|
||||
// RX_DONE on DIO1 — handled by the normal recvRaw() path, no MCU polling.
|
||||
// minSymbols=8 is the reliable preamble-latch count for SF7-12. If the
|
||||
// configured preamble is too short for a real duty-cycle (senderPreamble <
|
||||
// 2*minSymbols+1), RadioLib transparently falls back to a continuous receive.
|
||||
int16_t startPowerSaveRecv() override {
|
||||
return ((SX126x *)_radio)->startReceiveDutyCycleAuto(preambleLengthForSF(_preamble_sf), 8);
|
||||
}
|
||||
|
||||
void setRxBoostedGainMode(bool en) override {
|
||||
((CustomSX1262 *)_radio)->setRxBoostedGainMode(en);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ uint32_t RadioLibWrapper::getRngSeed() {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::setTxPower(int8_t dbm) {
|
||||
_tx_dbm = dbm;
|
||||
_radio->setOutputPower(dbm);
|
||||
}
|
||||
|
||||
@@ -84,6 +85,17 @@ void RadioLibWrapper::resetAGC() {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::loop() {
|
||||
// Power-save toggled vs the currently-armed RX mode: re-arm into the other mode
|
||||
// once the radio is idle (don't interrupt an in-flight TX or an unread RX-done).
|
||||
if (_power_save != _ps_active) {
|
||||
if (state != STATE_TX_WAIT && !(state & STATE_INT_READY) && !isReceivingPacket()) armRecv();
|
||||
return;
|
||||
}
|
||||
// In power-save the SX126x hardware duty-cycles RX on its own — nothing to poll
|
||||
// here, and noise-floor sampling (used only by the disabled interference check)
|
||||
// would read a chip that is asleep most of the time.
|
||||
if (_power_save) return;
|
||||
|
||||
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
|
||||
if (!isReceivingPacket()) {
|
||||
int rssi = getCurrentRSSI();
|
||||
@@ -104,6 +116,21 @@ void RadioLibWrapper::loop() {
|
||||
}
|
||||
|
||||
void RadioLibWrapper::startRecv() {
|
||||
armRecv();
|
||||
}
|
||||
|
||||
// Arm the receiver. In power-save mode this starts the SX126x hardware RX
|
||||
// duty-cycle (the chip cycles RX↔sleep and latches a preamble on its own);
|
||||
// otherwise a continuous RX. Falls back to continuous RX if the modem doesn't
|
||||
// support duty-cycle (base startPowerSaveRecv() returns UNSUPPORTED).
|
||||
void RadioLibWrapper::armRecv() {
|
||||
if (_power_save) {
|
||||
int16_t e = startPowerSaveRecv();
|
||||
if (e == RADIOLIB_ERR_NONE) { state = STATE_RX; _ps_active = true; return; }
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: RX duty-cycle unsupported (%d) — power-save off", (int)e);
|
||||
_power_save = false;
|
||||
}
|
||||
_ps_active = false;
|
||||
int err = _radio->startReceive();
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
state = STATE_RX;
|
||||
@@ -136,12 +163,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
|
||||
}
|
||||
|
||||
if (state != STATE_RX) {
|
||||
int err = _radio->startReceive();
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
state = STATE_RX;
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
|
||||
}
|
||||
armRecv(); // continuous RX, or re-arm the duty-cycle in power-save mode
|
||||
}
|
||||
return len;
|
||||
}
|
||||
@@ -191,22 +213,13 @@ float RadioLibWrapper::getLastSNR() const {
|
||||
return _radio->getSNR();
|
||||
}
|
||||
|
||||
// Approximate SNR threshold per SF for successful reception (based on Semtech datasheets)
|
||||
static float snr_threshold[] = {
|
||||
-7.5, // SF7 needs at least -7.5 dB SNR
|
||||
-10, // SF8 needs at least -10 dB SNR
|
||||
-12.5, // SF9 needs at least -12.5 dB SNR
|
||||
-15, // SF10 needs at least -15 dB SNR
|
||||
-17.5,// SF11 needs at least -17.5 dB SNR
|
||||
-20 // SF12 needs at least -20 dB SNR
|
||||
};
|
||||
|
||||
float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {
|
||||
if (sf < 7) return 0.0f;
|
||||
|
||||
if (snr < snr_threshold[sf - 7]) return 0.0f; // Below threshold, no chance of success
|
||||
|
||||
auto success_rate_based_on_snr = (snr - snr_threshold[sf - 7]) / 10.0;
|
||||
float floor = snrFloorForSF(sf); // min SNR for a chance of success
|
||||
if (snr < floor) return 0.0f; // below the demod floor → no chance
|
||||
|
||||
auto success_rate_based_on_snr = (snr - floor) / 10.0;
|
||||
auto collision_penalty = 1 - (packet_len / 256.0); // Assuming max packet of 256 bytes
|
||||
|
||||
return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty));
|
||||
|
||||
@@ -19,10 +19,27 @@ protected:
|
||||
virtual bool isReceivingPacket() =0;
|
||||
virtual void doResetAGC();
|
||||
|
||||
// Power-save RX: hardware SX126x RX duty-cycle (SetRxDutyCycle). Instead of a
|
||||
// continuous receive the chip itself cycles RX↔sleep, latches a preamble, then
|
||||
// stays in RX to receive the packet (RX_DONE on DIO1) — no MCU state machine,
|
||||
// average RX current cut several-fold. Driven from armRecv()/loop(); falls back
|
||||
// to continuous RX if the modem doesn't support it.
|
||||
bool _power_save = false;
|
||||
bool _ps_active = false; // is the radio currently armed in duty-cycle mode
|
||||
int8_t _tx_dbm = 0; // last TX power applied (tracks APC's live value)
|
||||
void armRecv(); // arm RX: duty-cycle in power-save, else continuous
|
||||
// Arm the hardware RX duty-cycle. Base returns UNSUPPORTED → armRecv() falls
|
||||
// back to continuous RX; SX126x overrides with startReceiveDutyCycleAuto().
|
||||
virtual int16_t startPowerSaveRecv() { return RADIOLIB_ERR_UNSUPPORTED; }
|
||||
|
||||
public:
|
||||
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; }
|
||||
|
||||
void begin() override;
|
||||
// Enable/disable hardware duty-cycle RX. Takes effect on the next RX re-arm
|
||||
// (loop() re-arms once the live mode differs from this request).
|
||||
void setPowerSaving(bool en) { _power_save = en; }
|
||||
bool getPowerSaving() const { return _power_save; }
|
||||
virtual void powerOff() { _radio->sleep(); }
|
||||
int recvRaw(uint8_t* bytes, int sz) override;
|
||||
uint32_t getEstAirtimeFor(int len_bytes) override;
|
||||
@@ -32,7 +49,7 @@ public:
|
||||
bool isInRecvMode() const override;
|
||||
bool isChannelActive();
|
||||
|
||||
bool isReceiving() override {
|
||||
bool isReceiving() override {
|
||||
if (isReceivingPacket()) return true;
|
||||
|
||||
return isChannelActive();
|
||||
@@ -41,10 +58,17 @@ public:
|
||||
virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0;
|
||||
uint32_t getRngSeed();
|
||||
void setTxPower(int8_t dbm);
|
||||
int8_t getTxPower() const { return _tx_dbm; } // actual current power (reflects APC)
|
||||
|
||||
virtual float getCurrentRSSI() =0;
|
||||
virtual uint8_t getSpreadingFactor() const { return LORA_SF; }
|
||||
static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; }
|
||||
// Approx SNR demod floor per SF (Semtech): SF7 -7.5 dB … SF12 -20 dB, -2.5 dB/SF.
|
||||
// Single source for both packetScore() and the APC link-margin target.
|
||||
static float snrFloorForSF(uint8_t sf) {
|
||||
if (sf < 7) sf = 7; else if (sf > 12) sf = 12;
|
||||
return -7.5f - 2.5f * (float)(sf - 7);
|
||||
}
|
||||
void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); }
|
||||
|
||||
int getNoiseFloor() const override { return _noise_floor; }
|
||||
|
||||
Reference in New Issue
Block a user