mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-06 04:06:12 +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:
@@ -54,6 +54,8 @@ class SettingsScreen : public UIScreen {
|
||||
// Radio section
|
||||
SECTION_RADIO,
|
||||
TX_POWER,
|
||||
POWER_SAVE,
|
||||
TX_APC,
|
||||
// System section
|
||||
SECTION_SYSTEM,
|
||||
TIMEZONE,
|
||||
@@ -479,6 +481,14 @@ class SettingsScreen : public UIScreen {
|
||||
snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0);
|
||||
display.setCursor(display.valCol(), y);
|
||||
display.print(buf);
|
||||
} else if (item == POWER_SAVE) {
|
||||
display.print("Pwr save");
|
||||
display.setCursor(display.valCol(), y);
|
||||
display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
||||
} else if (item == TX_APC) {
|
||||
display.print("Auto pwr");
|
||||
display.setCursor(display.valCol(), y);
|
||||
display.print((p && p->tx_apc) ? "ON" : "OFF");
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
} else if (item == AUTO_OFF) {
|
||||
display.print("AutoOff");
|
||||
@@ -736,6 +746,18 @@ public:
|
||||
if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
|
||||
if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
||||
p->rx_powersave ^= 1;
|
||||
_task->applyPowerSave();
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == TX_APC && p && (left || right || enter)) {
|
||||
p->tx_apc ^= 1;
|
||||
_task->applyApc();
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
if (_selected == AUTO_OFF && p) {
|
||||
int idx = autoOffIndex();
|
||||
|
||||
@@ -555,7 +555,15 @@ public:
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
int rightEdge = renderBatteryIndicator(display, _task->getBattMilliVolts());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextEllipsized(0, 0, rightEdge - 2, filtered_name);
|
||||
if (_node_prefs && _node_prefs->tx_apc) {
|
||||
char pwr_buf[8];
|
||||
snprintf(pwr_buf, sizeof(pwr_buf), "%ddB", (int)radio_driver.getTxPower());
|
||||
int pwr_w = display.getTextWidth(pwr_buf);
|
||||
display.drawTextEllipsized(0, 0, rightEdge - 2 - pwr_w - 2, filtered_name);
|
||||
display.drawTextRightAlign(rightEdge - 2, 0, pwr_buf);
|
||||
} else {
|
||||
display.drawTextEllipsized(0, 0, rightEdge - 2, filtered_name);
|
||||
}
|
||||
}
|
||||
|
||||
// ensure current page is visible (e.g. after settings change)
|
||||
@@ -729,10 +737,14 @@ public:
|
||||
|
||||
// tx power, noise floor
|
||||
display.setCursor(0, content_y + step * 2);
|
||||
snprintf(tmp, sizeof(tmp),"TX: %ddBm", _node_prefs->tx_power_dbm);
|
||||
snprintf(tmp, sizeof(tmp),"TX: %ddBm", radio_driver.getTxPower()); // live value (reflects APC)
|
||||
display.print(tmp);
|
||||
display.setCursor(0, content_y + step * 3);
|
||||
snprintf(tmp, sizeof(tmp),"Noise floor: %d", radio_driver.getNoiseFloor());
|
||||
if (radio_driver.getPowerSaving()) { // duty-cycle RX doesn't sample the floor
|
||||
snprintf(tmp, sizeof(tmp),"Noise floor: n/a");
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp),"Noise floor: %d", radio_driver.getNoiseFloor());
|
||||
}
|
||||
display.print(tmp);
|
||||
} else if (_page == HomePage::BLUETOOTH) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -2063,9 +2075,21 @@ void UITask::toggleGPS() {
|
||||
|
||||
void UITask::applyTxPower() {
|
||||
if (_node_prefs == NULL) return;
|
||||
// With APC on, tx_power_dbm is the ceiling — re-baseline the controller to it
|
||||
// (which also sets the radio) so the live power tracks the new ceiling at once.
|
||||
if (_node_prefs->tx_apc) { the_mesh.applyApc(); return; }
|
||||
radio_driver.setTxPower(_node_prefs->tx_power_dbm);
|
||||
}
|
||||
|
||||
void UITask::applyPowerSave() {
|
||||
if (_node_prefs == NULL) return;
|
||||
radio_driver.setPowerSaving(_node_prefs->rx_powersave);
|
||||
}
|
||||
|
||||
void UITask::applyApc() {
|
||||
the_mesh.applyApc(); // (re)initialise Adaptive Power Control from prefs
|
||||
}
|
||||
|
||||
void UITask::applyBrightness() {
|
||||
if (_display != NULL && _node_prefs != NULL) {
|
||||
_display->setBrightness(_node_prefs->display_brightness);
|
||||
|
||||
@@ -249,6 +249,8 @@ public:
|
||||
void setBuzzerVolumeLevel(uint8_t level);
|
||||
uint8_t getBuzzerVolume() const { return _node_prefs ? _node_prefs->buzzer_volume : 4; }
|
||||
void applyTxPower();
|
||||
void applyPowerSave(); // hardware duty-cycle RX on/off from prefs
|
||||
void applyApc(); // Adaptive Power Control on/off from prefs
|
||||
void applyFont();
|
||||
void applyRotation();
|
||||
void applyFullRefreshInterval();
|
||||
|
||||
Reference in New Issue
Block a user