From 285cf10a7ad60ff826ed55aa71ede1c4e07c3091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Doma=C5=84ski?= Date: Fri, 14 Aug 2026 16:11:21 +0200 Subject: [PATCH 01/35] RX power saving using rx duty cycle --- docs/cli_commands.md | 66 +++ examples/companion_radio/MyMesh.cpp | 37 +- examples/simple_repeater/MyMesh.cpp | 30 +- platformio.ini | 2 + src/helpers/CommonCLI.cpp | 17 +- src/helpers/CommonCLI.h | 15 +- src/helpers/radiolib/CustomLR1110.h | 75 +++ src/helpers/radiolib/CustomLR1110Wrapper.h | 29 +- src/helpers/radiolib/CustomSX1262.h | 41 +- src/helpers/radiolib/CustomSX1262Wrapper.h | 54 ++- src/helpers/radiolib/RXPowerSaving.cpp | 81 ++++ src/helpers/radiolib/RXPowerSaving.h | 102 ++++ src/helpers/radiolib/RXPowerSavingCLI.cpp | 197 ++++++++ src/helpers/radiolib/RXPowerSavingCLI.h | 17 + src/helpers/radiolib/RadioLibWrappers.cpp | 202 +++++++- src/helpers/radiolib/RadioLibWrappers.h | 67 ++- .../test_rx_power_saving.cpp | 444 ++++++++++++++++++ 17 files changed, 1436 insertions(+), 40 deletions(-) create mode 100644 src/helpers/radiolib/RXPowerSaving.cpp create mode 100644 src/helpers/radiolib/RXPowerSaving.h create mode 100644 src/helpers/radiolib/RXPowerSavingCLI.cpp create mode 100644 src/helpers/radiolib/RXPowerSavingCLI.h create mode 100644 test/test_rx_power_saving/test_rx_power_saving.cpp diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929..f1e618de 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -307,6 +307,72 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change RX power saving +**Usage:** +- `get radio.rxps` +- `set radio.rxps off` +- `set radio.rxps on` +- `set radio.rxps conservative` +- `set radio.rxps balanced` +- `set radio.rxps ` +- `set radio.rxps level ` +- `set radio.rxps level preamble ` +- `set radio.rxps ` + +**Parameters:** +- `level`: `1-10`; higher levels use shorter receive windows and longer sleep windows. +- `symbols`: `16` or `32` preamble symbols. +- `rx_us`: receive-window duration in microseconds, `1000-30000000`. +- `sleep_us`: radio sleep duration in microseconds, `1000-30000000`. + +**Repeater default:** `off` + +**Profiles:** +- `on` and `conservative`: level 1 with a 16-symbol preamble. +- `balanced`: level 5 with a 16-symbol preamble. +- A numeric level, or `level `, automatically uses 32 preamble symbols for SF5-SF8 and 16 for SF9-SF12. +- `level preamble ` explicitly fixes the preamble used in the calculation. +- Explicit `rx_us sleep_us` values select manual timing (`level=0`). + +Level-based settings are recalculated after SF or bandwidth changes. Manual timings are not recalculated. Settings are persisted in `/prefs.json`. Companion firmware does not expose this text command and applies its fixed level 5 / preamble 16 profile at startup and after radio-parameter changes. + +`get radio.rxps` reports: + +```text +desired=,effective=,supported=, +level=<0-10>,preamble=<0|16|32>,rx=,sleep=, +err=,fail=[,erx=,eslp=] +``` + +- `desired` is the saved user setting. +- `effective=armed` means receive duty-cycle is active. +- `effective=continuous` means RXPS is disabled, unsupported, or the last arm attempt fell back to continuous RX. +- `fail` counts failed arm operations; each one falls back to continuous RX. `clear stats` resets both this total and the consecutive-failure backoff, granting three fresh arm attempts. +- `erx` and `eslp` appear only when the driver had to clamp the requested periods, and report the effective periods after driver clamping. On LR1110 the RX window is stretched when `2*rx + sleep` would not cover the extended period Semtech requires, so the real duty cycle can be less economical than `rx`/`sleep` suggest. +- RXPS is currently supported by the SX1262 and LR1110 wrappers. Other radios remain in continuous RX and reject attempts to enable RXPS. +- There is intentionally no RXPS watchdog, watchdog command, or periodic recovery. Recovery is limited to the immediate continuous-RX fallback after an arm error. After 3 consecutive arm failures the node stops retrying on every RX restart and stays in continuous RX until the RXPS configuration is set again or `clear stats` grants a fresh set of attempts. +- On boards with a host-controlled RXEN pin, the RF switch is held in receive mode for the whole duty cycle (otherwise the node would be deaf). An external LNA on that pin therefore stays biased during the sleep windows, so the real power saving is smaller than the `rx`/`sleep` ratio implies. + +--- + +#### Disable the host-controlled RF receive switch during RX power saving +**Usage:** +- `get radio.rxps.rfrx_disabled` +- `set radio.rxps.rfrx_disabled ` + +**Parameters:** +- `state`: `on`|`off` + +**Default:** `off` + +**Notes:** +- This is a runtime-only diagnostic setting and resets to `off` after reboot. +- `on` reproduces the missing RF_RX assertion during SX1262 receive duty-cycle mode. +- Supported only on SX1262 targets with a host-controlled RX enable pin. +- Enabling it can significantly reduce receive sensitivity and make remote commands harder to receive. + +--- + ### System #### View or change this node's name diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2c334066..65fefddc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2,6 +2,7 @@ #include // needed for PlatformIO #include +#include "helpers/radiolib/RXPowerSaving.h" #define CMD_APP_START 1 #define CMD_SEND_TXT_MSG 2 @@ -265,6 +266,33 @@ bool MyMesh::getCADEnabled() const { return false; // hardware CAD before TX (disabled by default, until configurable) } +static void applyCompanionRxPowerSaving(uint8_t sf, float bw) { +#ifdef WRAPPER_CLASS + RxPowerSavingControl* control = &radio_driver; + + // setRxPowerSaving() rejects out-of-range periods without touching the + // wrapper's state, so on any failure we must explicitly stand the duty cycle + // down. Otherwise it would stay armed with the *previous* SF/BW timings - + // e.g. SF5/BW500 yields rx=655us (below the 1ms minimum), and the radio would + // keep sleeping in windows sized for SF11, missing every preamble. + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + bool ok = calcRxPowerSavingLevel(RX_POWERSAVING_BALANCED_LEVEL, sf, bw, + RX_POWERSAVING_PROFILE_PREAMBLE, &rx_us, &sleep_us) && + control->setRxPowerSaving(true, rx_us, sleep_us); + if (!ok) { + control->setRxPowerSaving(false, RX_POWERSAVING_DEFAULT_RX_US, + RX_POWERSAVING_DEFAULT_SLEEP_US); + } + MESH_DEBUG_PRINTLN("RX Power Saving: companion level=5,preamble=16,rx=%lu,sleep=%lu,%s", + (unsigned long)rx_us, (unsigned long)sleep_us, + ok ? "accepted" : "unavailable - continuous RX"); +#else + (void)sf; + (void)bw; +#endif +} + int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); @@ -980,6 +1008,7 @@ void MyMesh::begin(bool has_display) { board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); + applyCompanionRxPowerSaving(_prefs.sf, _prefs.bw); } const char *MyMesh::getNodeName() { @@ -1403,6 +1432,7 @@ void MyMesh::handleCmdFrame(size_t len) { savePrefs(); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + applyCompanionRxPowerSaving(_prefs.sf, _prefs.bw); MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, (uint32_t)cr); @@ -2263,5 +2293,10 @@ bool MyMesh::advert() { // To check if there is pending work bool MyMesh::hasPendingWork() const { - return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0; + bool calibration_active = false; +#ifdef WRAPPER_CLASS + const RxPowerSavingControl* rxps_control = &radio_driver; + calibration_active = rxps_control->isRxPowerSavingCalibrationActive(); +#endif + return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0 || calibration_active; } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7d0179f3..831f023f 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,28 @@ #include "MyMesh.h" #include +#include "helpers/radiolib/RXPowerSaving.h" + +static RxPowerSavingControl* getRxPowerSavingControl() { +#ifdef WRAPPER_CLASS + return &radio_driver; +#else + return nullptr; +#endif +} + +static void applyRxPowerSavingConfig(NodePrefs& prefs, uint8_t sf, float bw) { + normalizeRxPowerSavingConfig(&prefs.rxps, sf, bw); + RxPowerSavingControl* control = getRxPowerSavingControl(); + bool ok = control != nullptr + ? control->setRxPowerSaving( + prefs.rxps.enabled != 0, prefs.rxps.rx_us, prefs.rxps.sleep_us) + : prefs.rxps.enabled == 0; + MESH_DEBUG_PRINTLN("RX Power Saving: desired=%s, rx=%lu, sleep=%lu, %s", + prefs.rxps.enabled ? "on" : "off", + (unsigned long)prefs.rxps.rx_us, + (unsigned long)prefs.rxps.sleep_us, + ok ? "accepted" : "unsupported"); +} /* ------------------------------ Config -------------------------------- */ @@ -862,7 +885,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc mesh::RTCClock &rtc, mesh::MeshTables &tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), region_map(key_store), temp_map(key_store), - _cli(board, rtc, sensors, region_map, acl, &_prefs, this), + _cli(board, rtc, sensors, region_map, acl, &_prefs, this, getRxPowerSavingControl()), telemetry(MAX_PACKET_PAYLOAD - 4), discover_limiter(4, 120), // max 4 every 2 minutes anon_limiter(4, 180) // max 4 every 3 minutes @@ -983,6 +1006,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); + applyRxPowerSavingConfig(_prefs, _prefs.sf, _prefs.bw); updateAdvertTimer(); updateFloodAdvertTimer(); @@ -1308,12 +1332,14 @@ void MyMesh::loop() { if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params set_radio_at = 0; // clear timer radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr); + applyRxPowerSavingConfig(_prefs, pending_sf, pending_bw); MESH_DEBUG_PRINTLN("Temp radio params"); } if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig revert_radio_at = 0; // clear timer radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + applyRxPowerSavingConfig(_prefs, _prefs.sf, _prefs.bw); MESH_DEBUG_PRINTLN("Radio params restored"); } @@ -1334,5 +1360,7 @@ bool MyMesh::hasPendingWork() const { #if defined(WITH_BRIDGE) if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep #endif + const RxPowerSavingControl* control = getRxPowerSavingControl(); + if (control != nullptr && control->isRxPowerSavingCalibrationActive()) return true; return _mgr->getOutboundTotal() > 0; } diff --git a/platformio.ini b/platformio.ini index e78124a4..b0af24a3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -170,6 +170,8 @@ build_src_filter = +<../src/Utils.cpp> +<../src/Packet.cpp> +<../src/helpers/ConfigSerializer.cpp> + +<../src/helpers/radiolib/RXPowerSaving.cpp> + +<../src/helpers/radiolib/RXPowerSavingCLI.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58..f40f4ce0 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3,6 +3,7 @@ #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" +#include "radiolib/RXPowerSavingCLI.h" #include #ifndef BRIDGE_MAX_BAUD @@ -585,6 +586,13 @@ 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)) { + savePrefs(); + } } else if (memcmp(config, "radio ", 6) == 0) { strcpy(tmp, &config[6]); const char *parts[4]; @@ -598,8 +606,11 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _prefs->cr = cr; _prefs->freq = freq; _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); _callbacks->savePrefs(); - strcpy(reply, "OK - reboot to apply"); + strcpy(reply, rxps_retuned ? "OK - reboot to apply (rxps retuned)" : "OK - reboot to apply"); } else { strcpy(reply, "Error, invalid radio params"); } @@ -856,6 +867,10 @@ 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", 5) == 0) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e..835c8c7f 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -6,6 +6,7 @@ #include #include #include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -70,6 +71,7 @@ public: uint8_t loop_detect = 0; uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) uint8_t extra_sf[4]; + RxPowerSavingConfig rxps; private: class RadioPrefs : public ConfigSerializer { @@ -93,6 +95,11 @@ private: def("agc_int", _parent->agc_reset_interval); def("hash_mode", _parent->path_hash_mode); def("multi_ack", _parent->multi_acks); + def("rxps_en", _parent->rxps.enabled); + def("rxps_rx_us", _parent->rxps.rx_us); + def("rxps_sleep_us", _parent->rxps.sleep_us); + def("rxps_level", _parent->rxps.level); + def("rxps_preamble", _parent->rxps.preamble); } public: RadioPrefs(NodePrefs* parent) : _parent(parent) { } @@ -253,6 +260,7 @@ class CommonCLI { mesh::RTCClock* _rtc; NodePrefs* _prefs; CommonCLICallbacks* _callbacks; + RxPowerSavingControl* _rxps_control; mesh::MainBoard* _board; SensorManager* _sensors; RegionMap* _region_map; @@ -268,8 +276,11 @@ class CommonCLI { void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply); public: - CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks) - : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(®ion_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { } + CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, + RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, + CommonCLICallbacks* callbacks, RxPowerSavingControl* rxps_control = nullptr) + : _rtc(&rtc), _prefs(prefs), _callbacks(callbacks), _rxps_control(rxps_control), + _board(&board), _sensors(&sensors), _region_map(®ion_map), _acl(&acl) { } void loadPrefs(FILESYSTEM* _fs); bool savePrefs(FILESYSTEM* _fs); diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 75674a1d..d4fc31d3 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -2,6 +2,7 @@ #include #include "MeshCore.h" +#include "RXPowerSaving.h" class CustomLR1110 : public LR1110 { uint32_t _preambleMillis = 66; @@ -28,6 +29,75 @@ class CustomLR1110 : public LR1110 { float getFreqMHz() const { return freqMHz; } + // Restores the LF clock configuration RadioLib's begin() installs. Arming + // the duty cycle switches it to RC + BUSY-release (required by Semtech for + // SetRxDutyCycle); without this the change would outlive RXPS being turned + // off, and it also alters what the BUSY pin means to isChipBusy(). + int16_t restoreLfClock() { + return configLfClock( + RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED | RADIOLIB_LR11X0_LF_CLK_XOSC); + } + + // effectiveRxPeriod reports the RX window actually programmed, which may be + // stretched above the requested value to satisfy the extended-period rule. + int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod, + RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS, + RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK, + uint32_t* effectiveRxPeriod = nullptr) { + // Keep the conversion local until RadioLib uses 64-bit multiplication; + // its pinned implementation overflows for longer microsecond periods. + uint32_t symbolPeriod = (uint32_t)(((1000.0f * (float)(1UL << this->spreadingFactor)) / + this->bandwidthKhz) + 0.999f); + uint32_t transitionTime = this->tcxoDelay + 1000; + if (sleepPeriod <= transitionTime) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD; + uint32_t programmedSleepPeriod = sleepPeriod - transitionTime; + + uint64_t requiredExtendedPeriod = + ((uint64_t)this->preambleLengthLoRa + 11ULL) * symbolPeriod + 1000ULL; + uint64_t extendedPeriod = 2ULL * rxPeriod + programmedSleepPeriod; + if (extendedPeriod < requiredExtendedPeriod) { + rxPeriod = (uint32_t)((requiredExtendedPeriod - programmedSleepPeriod + 1ULL) / 2ULL); + } + if (effectiveRxPeriod) *effectiveRxPeriod = rxPeriod; + + uint32_t rxPeriodRaw = (uint32_t)(((uint64_t)rxPeriod * 32768UL) / 1000000UL); + uint32_t sleepPeriodRaw = + (uint32_t)(((uint64_t)programmedSleepPeriod * 32768UL) / 1000000UL); + if ((rxPeriodRaw & 0xFF000000) || rxPeriodRaw == 0) return RADIOLIB_ERR_INVALID_RX_PERIOD; + if ((sleepPeriodRaw & 0xFF000000) || sleepPeriodRaw == 0) { + return RADIOLIB_ERR_INVALID_SLEEP_PERIOD; + } + + int16_t state = standby(RADIOLIB_LR11X0_STANDBY_RC); + RADIOLIB_ASSERT(state); + // Semtech requires the RC standby/RTC setup before SetRxDutyCycle. + RadioModeConfig_t cfg = { + .receive = { + .timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF, + .irqFlags = irqFlags, + .irqMask = irqMask, + .len = 0, + } + }; + return runRxPowerSavingArmTransaction( + [this]() { + return configLfClock( + RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED); + }, + [this, &cfg]() { + return this->stageMode(RADIOLIB_RADIO_MODE_RX, &cfg); + }, + [this, rxPeriodRaw, sleepPeriodRaw]() { + return this->setRxDutyCycle( + rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX); + }, + [this]() { + // Preserve the arm error even if this best-effort rollback fails. + // A failed ConfigLfClock response may still mean the command ran. + restoreLfClock(); + }); + } + int16_t setRxBoostedGainMode(bool en) { _rx_boosted = en; return LR1110::setRxBoostedGainMode(en); @@ -40,6 +110,11 @@ class CustomLR1110 : public LR1110 { return LR1110::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } + bool isChipBusy() { + uint32_t busy = this->mod->getGpio(); + return busy != RADIOLIB_NC && this->mod->hal->digitalRead(busy); + } + bool isReceiving() { uint32_t irq = getIrqStatus(); bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index e7aaeb93..d93ccca9 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -9,6 +9,7 @@ public: CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { + prepareForRadioConfig(); ((CustomLR1110 *)_radio)->setFrequency(freq); ((CustomLR1110 *)_radio)->setSpreadingFactor(sf); ((CustomLR1110 *)_radio)->setBandwidth(bw); @@ -20,8 +21,16 @@ public: } bool isReceivingPacket() override { + // While duty-cycling, BUSY marks the sleep phase: probing IRQ status over + // SPI there could wake the chip and break the cycle. Only skip the probe + // in that case - outside RXPS this must stay a real channel check, since + // Dispatcher::checkSend() relies on it to avoid transmitting over others. + if (_rx_ps_armed && ((CustomLR1110 *)_radio)->isChipBusy()) return false; return ((CustomLR1110 *)_radio)->isReceiving(); } + bool isChipBusy() override { + return ((CustomLR1110 *)_radio)->isChipBusy(); + } float getCurrentRSSI() override { float rssi = -110; ((CustomLR1110 *)_radio)->getRssiInst(&rssi); @@ -38,12 +47,28 @@ public: _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts } - float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } + bool supportsRxPowerSaving() const override { return true; } + +protected: + int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, RadioLibIrqFlags_t irq_mask, + uint32_t* eff_rx_us, uint32_t* eff_sleep_us) override { + *eff_sleep_us = _rx_ps_sleep_us; // only the RX window can get clamped + return ((CustomLR1110 *)_radio)->startReceiveDutyCycle( + _rx_ps_rx_us, _rx_ps_sleep_us, irq_flags, irq_mask, eff_rx_us); + } + + int16_t stopDutyCycleHardware() override { + int16_t standby_err = _radio->standby(); + int16_t lf_err = ((CustomLR1110 *)_radio)->restoreLfClock(); + return standby_err != RADIOLIB_ERR_NONE ? standby_err : lf_err; + } + +public: uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } bool setRxBoostedGainMode(bool en) override { + prepareForRadioConfig(); return ((CustomLR1110 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index b4ee6c97..3f6977db 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -8,6 +8,7 @@ class CustomSX1262 : public SX1262 { uint32_t _maxPayloadMillis = 3934; uint32_t _activityAt = 0; bool _headerSeen = false; + bool _rx_ps_rf_rx_disabled = false; public: CustomSX1262(Module *mod) : SX1262(mod) { } @@ -105,6 +106,31 @@ class CustomSX1262 : public SX1262 { return SX1262::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } + int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod, + RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS, + RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK) { + int16_t state = SX1262::startReceiveDutyCycle(rxPeriod, sleepPeriod, irqFlags, irqMask); + if (state == RADIOLIB_ERR_NONE && !_rx_ps_rf_rx_disabled) { + // RadioLib stages RX through standby, which leaves a host-controlled + // RXEN switch in IDLE. Set it back to RX for the whole duty-cycle; + // boards without an external RXEN table are unaffected. + this->mod->setRfSwitchState(Module::MODE_RX); + } + return state; + } + + 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); + } + bool isReceiving() { uint32_t irq = getIrqFlags(); bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 @@ -148,6 +174,19 @@ class CustomSX1262 : public SX1262 { return false; } + int16_t stopRTC() { + uint8_t rtcStop = 0x00; + int16_t state = writeRegister(RADIOLIB_SX126X_REG_RTC_CTRL, &rtcStop, 1); + RADIOLIB_ASSERT(state); + + uint8_t rtcEvent = 0; + state = readRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1); + RADIOLIB_ASSERT(state); + + rtcEvent |= 0x02; + return writeRegister(RADIOLIB_SX126X_REG_EVENT_MASK, &rtcEvent, 1); + } + void setPreambleMillis(uint32_t preambleMillis) { _preambleMillis = preambleMillis; MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); @@ -162,4 +201,4 @@ class CustomSX1262 : public SX1262 { readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1); return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED); } -}; \ No newline at end of file +}; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index be314471..6d307a6c 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -13,6 +13,7 @@ public: CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { + prepareForRadioConfig(); ((CustomSX1262 *)_radio)->setFrequency(freq); ((CustomSX1262 *)_radio)->setSpreadingFactor(sf); ((CustomSX1262 *)_radio)->setBandwidth(bw); @@ -23,14 +24,20 @@ public: ((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } - bool isReceivingPacket() override { + bool isReceivingPacket() override { + // While duty-cycling, BUSY marks the sleep phase: probing IRQ flags over + // SPI there could wake the chip and break the cycle. Only skip the probe + // in that case - outside RXPS this must stay a real channel check, since + // Dispatcher::checkSend() relies on it to avoid transmitting over others. + if (_rx_ps_armed && ((CustomSX1262 *)_radio)->isChipBusy()) return false; return ((CustomSX1262 *)_radio)->isReceiving(); } + bool isChipBusy() override { + return ((CustomSX1262 *)_radio)->isChipBusy(); + } float getCurrentRSSI() override { return ((CustomSX1262 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); } float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1262 *)_radio)->spreadingFactor; @@ -38,10 +45,51 @@ public: } uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; } virtual void powerOff() override { + if (_rx_ps_armed) stopReceiveDutyCycle(); ((CustomSX1262 *)_radio)->sleep(false); } + 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 { + // RadioLib programs the SX126x with exactly what we ask for (it only + // subtracts the wake-up transition from the sleep period internally). + *eff_rx_us = _rx_ps_rx_us; + *eff_sleep_us = _rx_ps_sleep_us; + return ((CustomSX1262 *)_radio)->startReceiveDutyCycle( + _rx_ps_rx_us, _rx_ps_sleep_us, irq_flags, irq_mask); + } + + int16_t stopDutyCycleHardware() override { + int16_t standby_err = _radio->standby(); + int16_t rtc_err = ((CustomSX1262 *)_radio)->stopRTC(); + return standby_err != RADIOLIB_ERR_NONE ? standby_err : rtc_err; + } + +public: bool setRxBoostedGainMode(bool en) override { + prepareForRadioConfig(); return ((CustomSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { diff --git a/src/helpers/radiolib/RXPowerSaving.cpp b/src/helpers/radiolib/RXPowerSaving.cpp new file mode 100644 index 00000000..f11e07fc --- /dev/null +++ b/src/helpers/radiolib/RXPowerSaving.cpp @@ -0,0 +1,81 @@ +#include "RXPowerSaving.h" + +static uint32_t ceilRxPowerSavingValue(float value) { + uint32_t rounded = (uint32_t)value; + return value > (float)rounded ? rounded + 1 : rounded; +} + +bool isValidRxPowerSavingPeriod(uint32_t us) { + return us >= RX_POWERSAVING_MIN_PERIOD_US && us <= RX_POWERSAVING_MAX_PERIOD_US; +} + +uint8_t rxPowerSavingPreambleForSF(uint8_t sf) { + return sf <= 8 ? 32 : 16; +} + +bool isRxPowerSavingNumeric(const char* value) { + if (value == nullptr || *value == 0) return false; + while (*value) { + if (*value < '0' || *value > '9') return false; + value++; + } + return true; +} + +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 || + bw <= 0.0f || (preamble != 16 && preamble != 32)) { + return false; + } + + 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; + + 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); + + *rx_us = ceilRxPowerSavingValue(rx_symbols * symbol_us); + *sleep_us = (uint32_t)(sleep_symbols * symbol_us); + return true; +} + +void ensureRxPowerSavingDefaults(uint32_t* rx_us, uint32_t* sleep_us) { + if (!isValidRxPowerSavingPeriod(*rx_us)) *rx_us = RX_POWERSAVING_DEFAULT_RX_US; + if (!isValidRxPowerSavingPeriod(*sleep_us)) *sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US; +} + +bool recalcRxPowerSavingFromLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble, + uint32_t* rx_us, uint32_t* sleep_us) { + if (level < 1 || level > 10) 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) || + !isValidRxPowerSavingPeriod(calculated_rx_us) || + !isValidRxPowerSavingPeriod(calculated_sleep_us)) { + return false; + } + + *rx_us = calculated_rx_us; + *sleep_us = calculated_sleep_us; + return true; +} + +void normalizeRxPowerSavingConfig(RxPowerSavingConfig* config, uint8_t sf, float bw) { + if (config == nullptr) return; + + config->enabled = config->enabled ? 1 : 0; + if (config->level > 10) 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); +} diff --git a/src/helpers/radiolib/RXPowerSaving.h b/src/helpers/radiolib/RXPowerSaving.h new file mode 100644 index 00000000..473e5f29 --- /dev/null +++ b/src/helpers/radiolib/RXPowerSaving.h @@ -0,0 +1,102 @@ +#pragma once + +#include + +static constexpr uint32_t RX_POWERSAVING_DEFAULT_RX_US = 65625UL; +static constexpr uint32_t RX_POWERSAVING_DEFAULT_SLEEP_US = 60000UL; +static constexpr uint32_t RX_POWERSAVING_MIN_PERIOD_US = 1000UL; +static constexpr uint32_t RX_POWERSAVING_MAX_PERIOD_US = 30000000UL; + +static constexpr uint8_t RX_POWERSAVING_CONSERVATIVE_LEVEL = 1; +static constexpr uint8_t RX_POWERSAVING_BALANCED_LEVEL = 5; +static constexpr uint8_t RX_POWERSAVING_PROFILE_PREAMBLE = 16; +static constexpr uint8_t RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES = 3; + +class RxPowerSavingArmRetryState { + uint8_t _consecutive_failures = 0; + +public: + bool canAttempt() const { + return _consecutive_failures < RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES; + } + void recordSuccess() { _consecutive_failures = 0; } + void recordFailure() { + if (_consecutive_failures < RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES) { + _consecutive_failures++; + } + } + void reset() { _consecutive_failures = 0; } + uint8_t consecutiveFailures() const { return _consecutive_failures; } +}; + +// Runs the part of a hardware arm sequence that starts by changing persistent +// radio configuration. Every failing step rolls that configuration back, while +// the original arm error remains the result returned to the caller. +template +int16_t runRxPowerSavingArmTransaction(Configure configure, Stage stage, + Arm arm, Rollback rollback) { + int16_t state = configure(); + if (state != 0) { + rollback(); + return state; + } + + state = stage(); + if (state != 0) { + rollback(); + return state; + } + + state = arm(); + if (state != 0) rollback(); + return state; +} + +struct RxPowerSavingConfig { + uint8_t enabled = 0; + uint32_t rx_us = RX_POWERSAVING_DEFAULT_RX_US; + uint32_t sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US; + uint8_t level = RX_POWERSAVING_BALANCED_LEVEL; + uint8_t preamble = RX_POWERSAVING_PROFILE_PREAMBLE; +}; + +struct RxPowerSavingStatus { + bool supported = false; + bool armed = false; + int16_t last_error = 0; + uint32_t arm_failures = 0; + // Periods the radio actually runs with. A driver may clamp the requested + // values to satisfy a hardware constraint, so these can differ from the + // configured rx_us/sleep_us. Zero means "nothing armed yet". + uint32_t effective_rx_us = 0; + uint32_t effective_sleep_us = 0; +}; + +class RxPowerSavingControl { +public: + virtual ~RxPowerSavingControl() = default; + + virtual bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) { + (void)rx_us; + (void)sleep_us; + return !enabled; + } + virtual RxPowerSavingStatus getRxPowerSavingStatus() const { return {}; } + virtual 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; } +}; + +bool isValidRxPowerSavingPeriod(uint32_t us); +uint8_t rxPowerSavingPreambleForSF(uint8_t sf); +bool isRxPowerSavingNumeric(const char* value); +bool calcRxPowerSavingLevel(uint8_t level, uint8_t sf, float bw, uint8_t preamble, + uint32_t* rx_us, uint32_t* sleep_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); diff --git a/src/helpers/radiolib/RXPowerSavingCLI.cpp b/src/helpers/radiolib/RXPowerSavingCLI.cpp new file mode 100644 index 00000000..bf32bf30 --- /dev/null +++ b/src/helpers/radiolib/RXPowerSavingCLI.cpp @@ -0,0 +1,197 @@ +#include "RXPowerSavingCLI.h" + +#include +#include +#include +#include + +static bool parseRxPowerSavingUint32(const char* value, uint32_t* parsed) { + if (parsed == nullptr || !isRxPowerSavingNumeric(value)) return false; + + uint32_t result = 0; + while (*value) { + uint32_t digit = (uint32_t)(*value++ - '0'); + if (result > (UINT32_MAX - digit) / 10U) return false; + result = result * 10U + digit; + } + *parsed = result; + return true; +} + +bool RXPowerSavingCLI::set(const char* value, uint8_t sf, float bw, + RxPowerSavingConfig* config, RxPowerSavingControl* control, + char* reply, size_t reply_size) { + if (value == nullptr || config == nullptr || reply == nullptr || reply_size == 0) return false; + + RxPowerSavingConfig proposed = *config; + uint8_t level = 0; + uint8_t preamble = rxPowerSavingPreambleForSF(sf); + bool level_requested = false; + bool preamble_overridden = false; + + ensureRxPowerSavingDefaults(&proposed.rx_us, &proposed.sleep_us); + + if (strcmp(value, "off") == 0) { + proposed.enabled = 0; + } else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) { + proposed.enabled = 1; + level = RX_POWERSAVING_CONSERVATIVE_LEVEL; + preamble = RX_POWERSAVING_PROFILE_PREAMBLE; + level_requested = true; + preamble_overridden = true; + } else if (strcmp(value, "balanced") == 0) { + proposed.enabled = 1; + level = RX_POWERSAVING_BALANCED_LEVEL; + preamble = RX_POWERSAVING_PROFILE_PREAMBLE; + level_requested = true; + preamble_overridden = true; + } else { + char input[128]; + if (strlen(value) >= sizeof(input)) { + snprintf(reply, reply_size, + "ERROR: use off|on|conservative|balanced|level <1-10>| "); + return false; + } + strcpy(input, value); + + const char* parts[4]; + int count = mesh::Utils::parseTextParts(input, parts, 4, ' '); + uint32_t first = 0; + uint32_t second = 0; + if (count == 1 && parseRxPowerSavingUint32(parts[0], &first)) { + if (first < 1 || first > 10) { + snprintf(reply, reply_size, "ERROR: level range is 1-10; preamble is 16 or 32"); + return false; + } + level = (uint8_t)first; + level_requested = 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"); + return false; + } + level = (uint8_t)first; + level_requested = true; + proposed.enabled = 1; + } else if (count == 4 && strcmp(parts[0], "level") == 0 && + parseRxPowerSavingUint32(parts[1], &first) && + strcmp(parts[2], "preamble") == 0 && + parseRxPowerSavingUint32(parts[3], &second)) { + if (first < 1 || first > 10 || (second != 16 && second != 32)) { + snprintf(reply, reply_size, "ERROR: level range is 1-10; preamble is 16 or 32"); + return false; + } + level = (uint8_t)first; + preamble = (uint8_t)second; + level_requested = true; + preamble_overridden = true; + proposed.enabled = 1; + } else if (count == 2 && parseRxPowerSavingUint32(parts[0], &first) && + parseRxPowerSavingUint32(parts[1], &second)) { + proposed.rx_us = first; + proposed.sleep_us = second; + proposed.enabled = 1; + } else { + snprintf(reply, reply_size, + "ERROR: use off|on|conservative|balanced|level <1-10>| "); + 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"); + return false; + } + if (!isValidRxPowerSavingPeriod(proposed.rx_us) || + !isValidRxPowerSavingPeriod(proposed.sleep_us)) { + snprintf(reply, reply_size, "ERROR: range is %lu-%lu us", + (unsigned long)RX_POWERSAVING_MIN_PERIOD_US, + (unsigned long)RX_POWERSAVING_MAX_PERIOD_US); + return false; + } + + bool applied = control != nullptr + ? control->setRxPowerSaving(proposed.enabled != 0, proposed.rx_us, proposed.sleep_us) + : proposed.enabled == 0; + if (!applied) { + snprintf(reply, reply_size, "ERROR: RX powersaving unsupported"); + return false; + } + + if (level_requested) { + proposed.level = level; + proposed.preamble = preamble_overridden ? preamble : 0; + } else if (strcmp(value, "off") != 0) { + proposed.level = 0; + proposed.preamble = 0; + } + + *config = proposed; + snprintf(reply, reply_size, "OK - %s,level=%lu,preamble=%lu,rx=%lu,sleep=%lu", + config->enabled ? "on" : "off", (unsigned long)config->level, + (unsigned long)config->preamble, (unsigned long)config->rx_us, + (unsigned long)config->sleep_us); + return true; +} + +void RXPowerSavingCLI::get(const RxPowerSavingConfig* config, + const RxPowerSavingControl* control, + char* reply, size_t reply_size) { + if (config == nullptr || reply == nullptr || reply_size == 0) return; + + RxPowerSavingStatus status = control != nullptr + ? control->getRxPowerSavingStatus() + : RxPowerSavingStatus{}; + int len = snprintf(reply, reply_size, + "> desired=%s,effective=%s,supported=%s,level=%lu,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, + (unsigned long)config->preamble, (unsigned long)config->rx_us, + (unsigned long)config->sleep_us, (int)status.last_error, + (unsigned long)status.arm_failures); + + // Only report the periods the hardware really runs with when a driver had to + // clamp them - printing them always would not fit the 160 byte reply buffer. + if (len > 0 && (size_t)len < reply_size && status.armed && + (status.effective_rx_us != config->rx_us || + status.effective_sleep_us != config->sleep_us)) { + snprintf(reply + len, reply_size - (size_t)len, ",erx=%lu,eslp=%lu", + (unsigned long)status.effective_rx_us, + (unsigned long)status.effective_sleep_us); + } +} + +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"); + } +} diff --git a/src/helpers/radiolib/RXPowerSavingCLI.h b/src/helpers/radiolib/RXPowerSavingCLI.h new file mode 100644 index 00000000..dfd0eb98 --- /dev/null +++ b/src/helpers/radiolib/RXPowerSavingCLI.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "RXPowerSaving.h" + +class RXPowerSavingCLI { +public: + static bool set(const char* value, uint8_t sf, float bw, RxPowerSavingConfig* config, + RxPowerSavingControl* control, char* reply, size_t reply_size); + 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); +}; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b9c095ac..0fdc19d4 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -11,6 +11,10 @@ #define NUM_NOISE_FLOOR_SAMPLES 64 #define SAMPLING_THRESHOLD 14 +#define NF_CALIB_INTERVAL_MS 60000UL +#define NF_CALIB_TIMEOUT_MS 5000UL +#define NF_CALIB_SETTLE_MS 20UL + static volatile uint8_t state = STATE_IDLE; // this function is called when a complete packet @@ -48,14 +52,21 @@ uint32_t RadioLibWrapper::getRngSeed() { } void RadioLibWrapper::setTxPower(int8_t dbm) { + prepareForRadioConfig(); _radio->setOutputPower(dbm); } void RadioLibWrapper::idle() { + if (_rx_ps_armed) stopReceiveDutyCycle(); _radio->standby(); state = STATE_IDLE; // need another startReceive() } +void RadioLibWrapper::powerOff() { + if (_rx_ps_armed) stopReceiveDutyCycle(); + _radio->sleep(); +} + void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) { _threshold = threshold; if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling @@ -69,8 +80,10 @@ void RadioLibWrapper::doResetAGC() { } void RadioLibWrapper::resetAGC() { - // make sure we're not mid-receive of packet! - if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return; + // make sure we're not mid-receive or mid-transmit of a packet + if ((state & STATE_INT_READY) != 0 || isReceivingPacket() || state == STATE_TX_WAIT) return; + + if (_rx_ps_armed) stopReceiveDutyCycle(); doResetAGC(); state = STATE_IDLE; // trigger a startReceive() @@ -84,9 +97,48 @@ void RadioLibWrapper::resetAGC() { _floor_sample_sum = 0; } +// Clear the RX/idle state so the next loop calls startRecv() again, without +// dropping an STATE_INT_READY that the ISR may raise while we are in here. +// A plain `state = STATE_IDLE` loses that flag (and with it, a received +// packet) when setFlag() fires between the test and the store. +void RadioLibWrapper::requestRestartRecv() { + noInterrupts(); + if ((state & ~STATE_INT_READY) != STATE_TX_WAIT) { + state &= STATE_INT_READY; // STATE_IDLE, but keep a pending interrupt + } + interrupts(); +} + +void RadioLibWrapper::noiseFloorCalibCheck() { + unsigned long now = millis(); + if (_nf_calib_active) { + if (!_rx_ps_enabled || (long)(now - _nf_calib_deadline) >= 0) { + endNoiseFloorCalib(now); + } + } else if (_rx_ps_enabled && _rx_ps_armed && state == STATE_RX && + (_nf_last_calib == 0 || now - _nf_last_calib >= NF_CALIB_INTERVAL_MS) && + !isReceivingPacket()) { + _nf_calib_active = true; + _nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS; + _nf_sample_from = now + NF_CALIB_SETTLE_MS; + _num_floor_samples = 0; + _floor_sample_sum = 0; + requestRestartRecv(); + } +} + +void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) { + _nf_calib_active = false; + _nf_last_calib = now; + requestRestartRecv(); +} + void RadioLibWrapper::loop() { + noiseFloorCalibCheck(); + if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { - if (!isReceivingPacket()) { + if (!_rx_ps_armed && !(_nf_calib_active && (long)(millis() - _nf_sample_from) < 0) && + !isReceivingPacket()) { int rssi = getCurrentRSSI(); if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD _num_floor_samples++; @@ -103,6 +155,8 @@ void RadioLibWrapper::loop() { #ifdef MESH_DEBUG_NOISE_FLOOR MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); #endif + + if (_nf_calib_active) endNoiseFloorCalib(millis()); } } @@ -110,7 +164,7 @@ void RadioLibWrapper::startRecv() { #if defined(USE_LR2021) _radio->standby(); // without this LR2021 can throw -706 when calling startReceive after hardware CAD when side detectors are enabled #endif - int err = _radio->startReceive(); + int err = startReceiveMode(); if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; } else { @@ -118,6 +172,98 @@ void RadioLibWrapper::startRecv() { } } +int16_t RadioLibWrapper::startReceiveMode() { + if (_rx_ps_armed) stopReceiveDutyCycle(); + if (!_rx_ps_enabled || _nf_calib_active) { + _rx_ps_armed = false; + _rx_ps_last_error = RADIOLIB_ERR_NONE; + return _radio->startReceive(); + } + + if (!_rx_ps_arm_retry.canAttempt()) { + // arming has failed repeatedly; stop paying for a doomed SPI round-trip on + // every RX restart. A config change (setRxPowerSaving) re-enables retries. + _rx_ps_armed = false; + return _radio->startReceive(); + } + + const RadioLibIrqFlags_t irq_flags = + RADIOLIB_IRQ_RX_DEFAULT_FLAGS | + (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED); + const RadioLibIrqFlags_t irq_mask = + (1UL << RADIOLIB_IRQ_RX_DONE) | + (1UL << RADIOLIB_IRQ_TIMEOUT) | + (1UL << RADIOLIB_IRQ_CRC_ERR) | + (1UL << RADIOLIB_IRQ_HEADER_ERR); + + uint32_t eff_rx_us = _rx_ps_rx_us; + uint32_t eff_sleep_us = _rx_ps_sleep_us; + int16_t err = armDutyCycle(irq_flags, irq_mask, &eff_rx_us, &eff_sleep_us); + if (err == RADIOLIB_ERR_NONE) { + _rx_ps_armed = true; + _rx_ps_last_error = RADIOLIB_ERR_NONE; + _rx_ps_arm_retry.recordSuccess(); + _rx_ps_eff_rx_us = eff_rx_us; + _rx_ps_eff_sleep_us = eff_sleep_us; + return err; + } + + _rx_ps_armed = false; + _rx_ps_last_error = err; + _rx_ps_eff_rx_us = 0; + _rx_ps_eff_sleep_us = 0; + n_rx_ps_arm_failures++; + _rx_ps_arm_retry.recordFailure(); + MESH_DEBUG_PRINTLN("RadioLibWrapper: startReceiveDutyCycle(%d), continuous RX fallback", err); + int16_t fallback_err = _radio->startReceive(); + if (fallback_err != RADIOLIB_ERR_NONE) _rx_ps_last_error = fallback_err; + return fallback_err; +} + +void RadioLibWrapper::stopReceiveDutyCycle() { + int16_t err = stopDutyCycleHardware(); + _rx_ps_armed = false; + _rx_ps_eff_rx_us = 0; + _rx_ps_eff_sleep_us = 0; + if (err != RADIOLIB_ERR_NONE) _rx_ps_last_error = err; +} + +bool RadioLibWrapper::isPacketReady() { + if (!_rx_ps_armed) return true; + return _radio->checkIrq(RADIOLIB_IRQ_RX_DONE) != 0; +} + +void RadioLibWrapper::prepareForRadioConfig() { + if (!_rx_ps_armed) return; + + stopReceiveDutyCycle(); + requestRestartRecv(); +} + +bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) { + if (!isValidRxPowerSavingPeriod(rx_us) || !isValidRxPowerSavingPeriod(sleep_us)) return false; + if (enabled && !supportsRxPowerSaving()) return false; + + _rx_ps_enabled = enabled; + _rx_ps_rx_us = rx_us; + _rx_ps_sleep_us = sleep_us; + _rx_ps_last_error = RADIOLIB_ERR_NONE; + _rx_ps_arm_retry.reset(); // new config, give the hardware a fresh chance + requestRestartRecv(); + return true; +} + +RxPowerSavingStatus RadioLibWrapper::getRxPowerSavingStatus() const { + RxPowerSavingStatus status; + status.supported = supportsRxPowerSaving(); + status.armed = _rx_ps_armed; + status.last_error = _rx_ps_last_error; + status.arm_failures = n_rx_ps_arm_failures; + status.effective_rx_us = _rx_ps_eff_rx_us; + status.effective_sleep_us = _rx_ps_eff_sleep_us; + return status; +} + bool RadioLibWrapper::isInRecvMode() const { return (state & ~STATE_INT_READY) == STATE_RX; } @@ -125,17 +271,23 @@ bool RadioLibWrapper::isInRecvMode() const { int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { int len = 0; if (state & STATE_INT_READY) { - len = _radio->getPacketLength(); - if (len > 0) { - if (len > sz) { len = sz; } - int err = _radio->readData(bytes, len); - if (err != RADIOLIB_ERR_NONE) { - MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err); - len = 0; - n_recv_errors++; - } else { - // Serial.print(" readData() -> "); Serial.println(len); - n_recv++; + if (isPacketReady()) { + if (_rx_ps_armed) stopReceiveDutyCycle(); + len = _radio->getPacketLength(); + if (len > 0) { + if (len > sz) { len = sz; } + _last_snr = _radio->getSNR(); + _last_rssi = _radio->getRSSI(); + _last_metrics_valid = true; + int err = _radio->readData(bytes, len); + if (err != RADIOLIB_ERR_NONE) { + MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err); + len = 0; + n_recv_errors++; + } else { + // Serial.print(" readData() -> "); Serial.println(len); + n_recv++; + } } } #if defined(USE_LR2021) @@ -146,12 +298,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); - } + startRecv(); } return len; } @@ -161,6 +308,7 @@ uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { } bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { + if (_rx_ps_armed) stopReceiveDutyCycle(); _board->onBeforeTransmit(); int err = _radio->startTransmit((uint8_t *) bytes, len); if (err == RADIOLIB_ERR_NONE) { @@ -194,10 +342,12 @@ int16_t RadioLibWrapper::performChannelScan() { bool RadioLibWrapper::isChannelActive() { // int.thresh: RSSI-based interference detection (relative to noise floor) - if (_threshold != 0 && getCurrentRSSI() > _noise_floor + _threshold) return true; + if (_threshold != 0 && !(_rx_ps_armed && isChipBusy()) && + getCurrentRSSI() > _noise_floor + _threshold) return true; // cad: hardware channel activity detection if (_cad_enabled) { + if (_rx_ps_armed) stopReceiveDutyCycle(); int16_t result = performChannelScan(); // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't @@ -211,10 +361,12 @@ bool RadioLibWrapper::isChannelActive() { } float RadioLibWrapper::getLastRSSI() const { - return _radio->getRSSI(); + if (_last_metrics_valid) return _last_rssi; + return _rx_ps_armed ? 0 : _radio->getRSSI(); } float RadioLibWrapper::getLastSNR() const { - return _radio->getSNR(); + if (_last_metrics_valid) return _last_snr; + return _rx_ps_armed ? 0 : _radio->getSNR(); } // Approximate SNR threshold per SF for successful reception (based on Semtech datasheets) @@ -254,4 +406,4 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; -} \ No newline at end of file +} diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 77dd9311..0e2142f7 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -2,6 +2,7 @@ #include #include +#include "RXPowerSaving.h" #ifdef USE_CC310_HW_CRYPTO #include @@ -11,28 +12,74 @@ struct PacketMillis { uint32_t payloadMillis; // header-valid -> rx-done deadline }; -class RadioLibWrapper : public mesh::Radio { +class RadioLibWrapper : public mesh::Radio, public RxPowerSavingControl { protected: PhysicalLayer* _radio; mesh::MainBoard* _board; uint32_t n_recv, n_sent, n_recv_errors; int16_t _noise_floor, _threshold; + float _last_rssi, _last_snr; + bool _last_metrics_valid; bool _cad_enabled; uint16_t _num_floor_samples; int32_t _floor_sample_sum; uint8_t _preamble_sf; + bool _rx_ps_enabled; + bool _rx_ps_armed; + uint32_t _rx_ps_rx_us; + uint32_t _rx_ps_sleep_us; + uint32_t _rx_ps_eff_rx_us; // periods the driver actually armed with + uint32_t _rx_ps_eff_sleep_us; + int16_t _rx_ps_last_error; + uint32_t n_rx_ps_arm_failures; + RxPowerSavingArmRetryState _rx_ps_arm_retry; + + bool _nf_calib_active; + unsigned long _nf_last_calib; + unsigned long _nf_calib_deadline; + unsigned long _nf_sample_from; + void idle(); void startRecv(); + void requestRestartRecv(); + void prepareForRadioConfig(); + void noiseFloorCalibCheck(); + void endNoiseFloorCalib(unsigned long now); + int16_t startReceiveMode(); + void stopReceiveDutyCycle(); + // eff_rx_us/eff_sleep_us report the periods the driver really programmed, + // which may be clamped to satisfy a hardware constraint. + virtual int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, + RadioLibIrqFlags_t irq_mask, + uint32_t* eff_rx_us, uint32_t* eff_sleep_us) { + (void)irq_flags; + (void)irq_mask; + (void)eff_rx_us; + (void)eff_sleep_us; + return RADIOLIB_ERR_UNSUPPORTED; + } + virtual int16_t stopDutyCycleHardware() { return _radio->standby(); } + virtual bool isPacketReady(); + virtual bool isChipBusy() { return false; } float packetScoreInt(float snr, int sf, int packet_len); virtual bool isReceivingPacket() =0; virtual void doResetAGC(); public: - RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; } + RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) + : _radio(&radio), _board(&board), _last_rssi(0), _last_snr(0), _last_metrics_valid(false), + _preamble_sf(0), _rx_ps_enabled(false), _rx_ps_armed(false), + _rx_ps_rx_us(RX_POWERSAVING_DEFAULT_RX_US), + _rx_ps_sleep_us(RX_POWERSAVING_DEFAULT_SLEEP_US), _rx_ps_eff_rx_us(0), + _rx_ps_eff_sleep_us(0), _rx_ps_last_error(RADIOLIB_ERR_NONE), + n_rx_ps_arm_failures(0), _nf_calib_active(false), + _nf_last_calib(0), _nf_calib_deadline(0), _nf_sample_from(0) { + n_recv = n_sent = n_recv_errors = 0; + } void begin() override; - virtual void powerOff() { _radio->sleep(); } + virtual void powerOff(); int recvRaw(uint8_t* bytes, int sz) override; uint32_t getEstAirtimeFor(int len_bytes) override; bool startSendRaw(const uint8_t* bytes, int len) override; @@ -40,6 +87,13 @@ public: void onSendFinished() override; bool isInRecvMode() const override; bool isChannelActive(); + 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 { if (isReceivingPacket()) return true; @@ -68,7 +122,12 @@ public: uint32_t getPacketsRecv() const { return n_recv; } uint32_t getPacketsRecvErrors() const { return n_recv_errors; } uint32_t getPacketsSent() const { return n_sent; } - void resetStats() { n_recv = n_sent = n_recv_errors = 0; } + void resetStats() { + n_recv = n_sent = n_recv_errors = n_rx_ps_arm_failures = 0; + // Clearing diagnostics also grants a fresh set of arm attempts. If they + // fail again, the normal three-failure continuous-RX fallback applies. + _rx_ps_arm_retry.reset(); + } virtual float getLastRSSI() const override; virtual float getLastSNR() const override; diff --git a/test/test_rx_power_saving/test_rx_power_saving.cpp b/test/test_rx_power_saving/test_rx_power_saving.cpp new file mode 100644 index 00000000..3c769e0a --- /dev/null +++ b/test/test_rx_power_saving/test_rx_power_saving.cpp @@ -0,0 +1,444 @@ +#include + +#include + +#include "helpers/radiolib/RXPowerSaving.h" +#include "helpers/radiolib/RXPowerSavingCLI.h" + +class FakeRxPowerSavingControl : public RxPowerSavingControl { +public: + bool accept = true; + bool set_called = false; + bool requested_enabled = false; + uint32_t requested_rx_us = 0; + uint32_t requested_sleep_us = 0; + bool rf_rx_supported = false; + bool rf_rx_disabled = false; + RxPowerSavingStatus status; + + bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) override { + set_called = true; + requested_enabled = enabled; + requested_rx_us = rx_us; + requested_sleep_us = sleep_us; + return accept; + } + + RxPowerSavingStatus getRxPowerSavingStatus() const override { return status; } + bool supportsRxPowerSavingRfRxDisable() const override { return rf_rx_supported; } + bool setRxPowerSavingRfRxDisabled(bool disabled) override { + if (!rf_rx_supported) return false; + rf_rx_disabled = disabled; + return true; + } + bool isRxPowerSavingRfRxDisabled() const override { return rf_rx_disabled; } +}; + +TEST(RxPowerSaving, DefaultsKeepRepeaterDisabledWithBalancedIntent) { + const RxPowerSavingConfig config; + + EXPECT_EQ(config.enabled, 0); + EXPECT_EQ(config.level, RX_POWERSAVING_BALANCED_LEVEL); + EXPECT_EQ(config.preamble, RX_POWERSAVING_PROFILE_PREAMBLE); + EXPECT_EQ(config.rx_us, RX_POWERSAVING_DEFAULT_RX_US); + EXPECT_EQ(config.sleep_us, RX_POWERSAVING_DEFAULT_SLEEP_US); +} + +TEST(RxPowerSaving, BaseControlRejectsEnableAndAcceptsContinuousRx) { + RxPowerSavingControl control; + + EXPECT_FALSE(control.setRxPowerSaving(true, 12345, 23456)); + EXPECT_TRUE(control.setRxPowerSaving(false, 12345, 23456)); + + const RxPowerSavingStatus status = control.getRxPowerSavingStatus(); + EXPECT_FALSE(status.supported); + EXPECT_FALSE(status.armed); + EXPECT_EQ(status.arm_failures, 0U); + EXPECT_EQ(status.effective_rx_us, 0U); + EXPECT_EQ(status.effective_sleep_us, 0U); +} + +TEST(RxPowerSaving, ArmRetryStopsAfterThreeFailuresAndSuccessResetsIt) { + RxPowerSavingArmRetryState retry; + + EXPECT_TRUE(retry.canAttempt()); + for (uint8_t expected = 1; expected <= RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES; expected++) { + retry.recordFailure(); + EXPECT_EQ(retry.consecutiveFailures(), expected); + } + EXPECT_FALSE(retry.canAttempt()); + + // Saturate instead of wrapping if a caller records another failure. + retry.recordFailure(); + EXPECT_EQ(retry.consecutiveFailures(), RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES); + + retry.recordSuccess(); + EXPECT_EQ(retry.consecutiveFailures(), 0); + EXPECT_TRUE(retry.canAttempt()); +} + +TEST(RxPowerSaving, ClearingRetryStateGrantsThreeFreshAttempts) { + RxPowerSavingArmRetryState retry; + for (uint8_t i = 0; i < RX_POWERSAVING_MAX_CONSEC_ARM_FAILURES; i++) { + retry.recordFailure(); + } + ASSERT_FALSE(retry.canAttempt()); + + retry.reset(); + EXPECT_TRUE(retry.canAttempt()); + EXPECT_EQ(retry.consecutiveFailures(), 0); +} + +TEST(RxPowerSaving, ArmTransactionRollsBackEveryFailureAndPreservesItsError) { + for (int failing_step = 0; failing_step < 3; failing_step++) { + int calls[3] = {0, 0, 0}; + int rollback_calls = 0; + auto step = [&](int index) -> int16_t { + calls[index]++; + return index == failing_step ? (int16_t)(-700 - index) : (int16_t)0; + }; + + int16_t result = runRxPowerSavingArmTransaction( + [&]() { return step(0); }, + [&]() { return step(1); }, + [&]() { return step(2); }, + [&]() { rollback_calls++; }); + + EXPECT_EQ(result, -700 - failing_step); + EXPECT_EQ(rollback_calls, 1); + for (int index = 0; index < 3; index++) { + EXPECT_EQ(calls[index], index <= failing_step ? 1 : 0); + } + } +} + +TEST(RxPowerSaving, ArmTransactionDoesNotRollBackSuccessfulArm) { + int step_calls = 0; + int rollback_calls = 0; + + int16_t result = runRxPowerSavingArmTransaction( + [&]() -> int16_t { step_calls++; return 0; }, + [&]() -> int16_t { step_calls++; return 0; }, + [&]() -> int16_t { step_calls++; return 0; }, + [&]() { rollback_calls++; }); + + EXPECT_EQ(result, 0); + EXPECT_EQ(step_calls, 3); + EXPECT_EQ(rollback_calls, 0); +} + +TEST(RxPowerSaving, RejectsInvalidProfileInputs) { + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + + EXPECT_FALSE(calcRxPowerSavingLevel(0, 10, 250.0f, 16, &rx_us, &sleep_us)); + EXPECT_FALSE(calcRxPowerSavingLevel(11, 10, 250.0f, 16, &rx_us, &sleep_us)); + EXPECT_FALSE(calcRxPowerSavingLevel(5, 4, 250.0f, 16, &rx_us, &sleep_us)); + EXPECT_FALSE(calcRxPowerSavingLevel(5, 10, 0.0f, 16, &rx_us, &sleep_us)); + EXPECT_FALSE(calcRxPowerSavingLevel(5, 10, 250.0f, 24, &rx_us, &sleep_us)); +} + +TEST(RxPowerSaving, AcceptsPeriodBoundariesOnly) { + EXPECT_FALSE(isValidRxPowerSavingPeriod(RX_POWERSAVING_MIN_PERIOD_US - 1)); + EXPECT_TRUE(isValidRxPowerSavingPeriod(RX_POWERSAVING_MIN_PERIOD_US)); + EXPECT_TRUE(isValidRxPowerSavingPeriod(RX_POWERSAVING_MAX_PERIOD_US)); + EXPECT_FALSE(isValidRxPowerSavingPeriod(RX_POWERSAVING_MAX_PERIOD_US + 1)); +} + +TEST(RxPowerSaving, RepairsInvalidPersistedPeriodsIndependently) { + uint32_t rx_us = 0; + uint32_t sleep_us = 12345; + + ensureRxPowerSavingDefaults(&rx_us, &sleep_us); + EXPECT_EQ(rx_us, RX_POWERSAVING_DEFAULT_RX_US); + EXPECT_EQ(sleep_us, 12345U); + + rx_us = 23456; + sleep_us = RX_POWERSAVING_MAX_PERIOD_US + 1; + ensureRxPowerSavingDefaults(&rx_us, &sleep_us); + EXPECT_EQ(rx_us, 23456U); + EXPECT_EQ(sleep_us, RX_POWERSAVING_DEFAULT_SLEEP_US); +} + +TEST(RxPowerSaving, NormalizesPersistedConfigBeforeApplying) { + RxPowerSavingConfig config; + config.enabled = 7; + config.level = 255; + config.preamble = 24; + config.rx_us = 0; + config.sleep_us = 0; + + normalizeRxPowerSavingConfig(&config, 10, 250.0f); + + EXPECT_EQ(config.enabled, 1); + EXPECT_EQ(config.level, RX_POWERSAVING_BALANCED_LEVEL); + EXPECT_EQ(config.preamble, RX_POWERSAVING_PROFILE_PREAMBLE); + EXPECT_EQ(config.rx_us, 41871U); + EXPECT_EQ(config.sleep_us, 26851U); +} + +TEST(RxPowerSaving, NumericInputIsStrictDecimal) { + EXPECT_TRUE(isRxPowerSavingNumeric("0")); + EXPECT_TRUE(isRxPowerSavingNumeric("123456")); + EXPECT_FALSE(isRxPowerSavingNumeric(nullptr)); + EXPECT_FALSE(isRxPowerSavingNumeric("")); + EXPECT_FALSE(isRxPowerSavingNumeric("-1")); + EXPECT_FALSE(isRxPowerSavingNumeric("1.5")); + EXPECT_FALSE(isRxPowerSavingNumeric("12x")); + EXPECT_FALSE(isRxPowerSavingNumeric(" 12")); +} + +TEST(RxPowerSaving, CompanionProfileIsLevelFivePreambleSixteen) { + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + + ASSERT_TRUE(calcRxPowerSavingLevel(5, 10, 250.0f, 16, &rx_us, &sleep_us)); + EXPECT_EQ(rx_us, 41871U); + EXPECT_EQ(sleep_us, 26851U); +} + +TEST(RxPowerSaving, NamedProfileConstantsRemainStable) { + EXPECT_EQ(RX_POWERSAVING_CONSERVATIVE_LEVEL, 1); + EXPECT_EQ(RX_POWERSAVING_BALANCED_LEVEL, 5); + EXPECT_EQ(RX_POWERSAVING_PROFILE_PREAMBLE, 16); +} + +TEST(RxPowerSaving, HigherLevelTradesListenTimeForSleepTime) { + uint32_t conservative_rx_us = 0; + uint32_t conservative_sleep_us = 0; + uint32_t aggressive_rx_us = 0; + uint32_t aggressive_sleep_us = 0; + + ASSERT_TRUE(calcRxPowerSavingLevel(1, 10, 250.0f, 16, + &conservative_rx_us, &conservative_sleep_us)); + ASSERT_TRUE(calcRxPowerSavingLevel(10, 10, 250.0f, 16, + &aggressive_rx_us, &aggressive_sleep_us)); + EXPECT_GT(conservative_rx_us, aggressive_rx_us); + EXPECT_LT(conservative_sleep_us, aggressive_sleep_us); +} + +TEST(RxPowerSaving, AutoPreambleTracksSpreadingFactor) { + EXPECT_EQ(rxPowerSavingPreambleForSF(7), 32); + EXPECT_EQ(rxPowerSavingPreambleForSF(8), 32); + EXPECT_EQ(rxPowerSavingPreambleForSF(9), 16); + EXPECT_EQ(rxPowerSavingPreambleForSF(12), 16); +} + +TEST(RxPowerSaving, LevelIntentRetunesAfterRadioChange) { + uint32_t rx_us = 0; + uint32_t sleep_us = 0; + + ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 8, 62.5f, 16, &rx_us, &sleep_us)); + uint32_t old_rx_us = rx_us; + uint32_t old_sleep_us = sleep_us; + + ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 9, 62.5f, 16, &rx_us, &sleep_us)); + EXPECT_NEAR((double)rx_us, (double)old_rx_us * 2.0, 1.0); + EXPECT_NEAR((double)sleep_us, (double)old_sleep_us * 2.0, 1.0); +} + +TEST(RxPowerSaving, AutomaticPreambleRetunesAcrossSfBoundary) { + uint32_t automatic_rx_us = 0; + uint32_t automatic_sleep_us = 0; + uint32_t explicit_rx_us = 0; + uint32_t explicit_sleep_us = 0; + + ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 8, 250.0f, 0, + &automatic_rx_us, &automatic_sleep_us)); + ASSERT_TRUE(calcRxPowerSavingLevel(5, 8, 250.0f, 32, + &explicit_rx_us, &explicit_sleep_us)); + EXPECT_EQ(automatic_rx_us, explicit_rx_us); + EXPECT_EQ(automatic_sleep_us, explicit_sleep_us); + + ASSERT_TRUE(recalcRxPowerSavingFromLevel(5, 9, 250.0f, 0, + &automatic_rx_us, &automatic_sleep_us)); + ASSERT_TRUE(calcRxPowerSavingLevel(5, 9, 250.0f, 16, + &explicit_rx_us, &explicit_sleep_us)); + EXPECT_EQ(automatic_rx_us, explicit_rx_us); + EXPECT_EQ(automatic_sleep_us, explicit_sleep_us); +} + +TEST(RxPowerSaving, ManualTimingsAreNotRetuned) { + uint32_t rx_us = 12345; + uint32_t sleep_us = 23456; + + EXPECT_FALSE(recalcRxPowerSavingFromLevel(0, 10, 250.0f, 16, &rx_us, &sleep_us)); + EXPECT_EQ(rx_us, 12345U); + EXPECT_EQ(sleep_us, 23456U); +} + +TEST(RxPowerSavingCLI, AppliesNamedAndManualProfiles) { + RxPowerSavingConfig config; + FakeRxPowerSavingControl control; + char reply[192]; + + ASSERT_TRUE(RXPowerSavingCLI::set("balanced", 10, 250.0f, &config, &control, + reply, sizeof(reply))); + EXPECT_TRUE(control.set_called); + EXPECT_TRUE(control.requested_enabled); + EXPECT_EQ(config.enabled, 1); + EXPECT_EQ(config.level, RX_POWERSAVING_BALANCED_LEVEL); + EXPECT_EQ(config.preamble, RX_POWERSAVING_PROFILE_PREAMBLE); + EXPECT_EQ(config.rx_us, 41871U); + EXPECT_EQ(config.sleep_us, 26851U); + + control.set_called = false; + ASSERT_TRUE(RXPowerSavingCLI::set("12345 23456", 10, 250.0f, &config, &control, + reply, sizeof(reply))); + EXPECT_TRUE(control.set_called); + EXPECT_EQ(config.level, 0); + EXPECT_EQ(config.preamble, 0); + EXPECT_EQ(config.rx_us, 12345U); + EXPECT_EQ(config.sleep_us, 23456U); +} + +TEST(RxPowerSavingCLI, StoresAutomaticPreambleIntentForLevel) { + RxPowerSavingConfig config; + FakeRxPowerSavingControl control; + char reply[192]; + + ASSERT_TRUE(RXPowerSavingCLI::set("level 5", 8, 250.0f, &config, &control, + reply, sizeof(reply))); + EXPECT_EQ(config.level, 5); + EXPECT_EQ(config.preamble, 0); + + uint32_t expected_rx_us = 0; + uint32_t expected_sleep_us = 0; + ASSERT_TRUE(calcRxPowerSavingLevel(5, 8, 250.0f, 32, + &expected_rx_us, &expected_sleep_us)); + EXPECT_EQ(config.rx_us, expected_rx_us); + EXPECT_EQ(config.sleep_us, expected_sleep_us); +} + +TEST(RxPowerSavingCLI, DoesNotPersistRejectedOrInvalidChanges) { + RxPowerSavingConfig config; + RxPowerSavingConfig original = config; + FakeRxPowerSavingControl control; + char reply[192]; + + EXPECT_FALSE(RXPowerSavingCLI::set("level 0", 10, 250.0f, &config, &control, + reply, sizeof(reply))); + EXPECT_EQ(config.enabled, original.enabled); + EXPECT_EQ(config.level, original.level); + EXPECT_STREQ(reply, "ERROR: level range is 1-10; preamble is 16 or 32"); + + control.accept = false; + EXPECT_FALSE(RXPowerSavingCLI::set("balanced", 10, 250.0f, &config, &control, + reply, sizeof(reply))); + EXPECT_EQ(config.enabled, original.enabled); + EXPECT_EQ(config.level, original.level); + EXPECT_STREQ(reply, "ERROR: RX powersaving unsupported"); +} + +TEST(RxPowerSavingCLI, RejectsValuesBeforeNarrowingOrOverflow) { + RxPowerSavingConfig config; + const RxPowerSavingConfig original = config; + FakeRxPowerSavingControl control; + char reply[192]; + + const char* invalid_values[] = { + "261", // used to alias to level 5 after uint8_t truncation + "level 266", // used to alias to level 10 + "level 5 preamble 272", // used to alias to preamble 16 + "4294967296 1000", // one above UINT32_MAX + "999999999999999999999999999", // must not wrap during parsing + }; + + for (const char* value : invalid_values) { + control.set_called = false; + EXPECT_FALSE(RXPowerSavingCLI::set(value, 10, 250.0f, &config, &control, + reply, sizeof(reply))) << value; + EXPECT_FALSE(control.set_called) << value; + EXPECT_EQ(config.enabled, original.enabled) << value; + EXPECT_EQ(config.level, original.level) << value; + EXPECT_EQ(config.preamble, original.preamble) << value; + EXPECT_EQ(config.rx_us, original.rx_us) << value; + EXPECT_EQ(config.sleep_us, original.sleep_us) << value; + } +} + +TEST(RxPowerSavingCLI, DisablingKeepsTimingIntent) { + RxPowerSavingConfig config; + config.enabled = 1; + config.level = 7; + config.preamble = 32; + config.rx_us = 34567; + config.sleep_us = 45678; + FakeRxPowerSavingControl control; + char reply[192]; + + ASSERT_TRUE(RXPowerSavingCLI::set("off", 10, 250.0f, &config, &control, + reply, sizeof(reply))); + EXPECT_FALSE(control.requested_enabled); + EXPECT_EQ(config.enabled, 0); + EXPECT_EQ(config.level, 7); + EXPECT_EQ(config.preamble, 32); + EXPECT_EQ(config.rx_us, 34567U); + EXPECT_EQ(config.sleep_us, 45678U); +} + +TEST(RxPowerSavingCLI, FormatsDesiredAndEffectiveStateSeparately) { + RxPowerSavingConfig config; + config.enabled = 1; + FakeRxPowerSavingControl control; + control.status = {true, false, -706, 2, 0, 0}; + char reply[192]; + + RXPowerSavingCLI::get(&config, &control, reply, sizeof(reply)); + + EXPECT_NE(std::strstr(reply, "desired=on,effective=continuous,supported=yes"), nullptr); + EXPECT_NE(std::strstr(reply, "err=-706,fail=2"), nullptr); + EXPECT_EQ(std::strstr(reply, "erx="), nullptr); // nothing armed, nothing to report +} + +TEST(RxPowerSavingCLI, ReportsClampedPeriodsOnlyWhenTheyDiffer) { + RxPowerSavingConfig config; + config.enabled = 1; + FakeRxPowerSavingControl control; + char reply[192]; + + // driver armed exactly what was asked for -> no extra fields + control.status = {true, true, 0, 0, config.rx_us, config.sleep_us}; + RXPowerSavingCLI::get(&config, &control, reply, sizeof(reply)); + EXPECT_NE(std::strstr(reply, "effective=armed"), nullptr); + EXPECT_EQ(std::strstr(reply, "erx="), nullptr); + + // driver had to stretch the RX window -> surface the real values + control.status = {true, true, 0, 0, config.rx_us + 4000, config.sleep_us}; + RXPowerSavingCLI::get(&config, &control, reply, sizeof(reply)); + EXPECT_NE(std::strstr(reply, "erx=69625,eslp=60000"), nullptr); +} + +TEST(RxPowerSavingCLI, GetDoesNotMutateStoredConfig) { + RxPowerSavingConfig config; + config.rx_us = 0; // invalid, e.g. from an older persisted prefs file + config.sleep_us = 999; + FakeRxPowerSavingControl control; + char reply[192]; + + RXPowerSavingCLI::get(&config, &control, reply, sizeof(reply)); + + EXPECT_EQ(config.rx_us, 0U); + EXPECT_EQ(config.sleep_us, 999U); +} + +TEST(RxPowerSavingCLI, RfRxDiagnosticIsRuntimeOnlyAndCapabilityGated) { + FakeRxPowerSavingControl control; + char reply[192]; + + RXPowerSavingCLI::setRfRxDisabled("on", &control, reply, sizeof(reply)); + EXPECT_STREQ(reply, "Error: unsupported"); + + control.rf_rx_supported = true; + RXPowerSavingCLI::setRfRxDisabled("on", &control, reply, sizeof(reply)); + EXPECT_TRUE(control.rf_rx_disabled); + EXPECT_STREQ(reply, "OK - radio.rxps.rfrx_disabled on"); + + RXPowerSavingCLI::getRfRxDisabled(&control, reply, sizeof(reply)); + EXPECT_STREQ(reply, "> on"); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From afcebdd043c7457dd47938d87bfe7526506edf27 Mon Sep 17 00:00:00 2001 From: IoTThinks Date: Mon, 27 Jul 2026 09:54:21 +0700 Subject: [PATCH 02/35] Update GitHub funding username to IoTThinks --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 262a9ee4..c19645bc 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: meshcore-dev +github: IoTThinks From d302651eb671c56650321bc91b2fbadaf38c24ee Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Tue, 11 Aug 2026 22:43:12 +0700 Subject: [PATCH 03/35] Disabled USE_CC310_HW_CRYPTO: https://github.com/meshcore-dev/MeshCore/pull/2824 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index e78124a4..636abad1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -91,7 +91,7 @@ build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM -D LFS_NO_ASSERT=1 -D EXTRAFS=1 - -D USE_CC310_HW_CRYPTO=1 +; -D USE_CC310_HW_CRYPTO=1 lib_deps = ${arduino_base.lib_deps} https://github.com/oltaco/CustomLFS#0.2.3 From ea246a621c64f4b5ede5342d58884b11515668ee Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 12 Aug 2026 19:28:11 +0700 Subject: [PATCH 04/35] Funding --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c19645bc..262a9ee4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: IoTThinks +github: meshcore-dev From 2384af19b678eab97178a2e052cbaf709001324a Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 12 Aug 2026 19:29:49 +0700 Subject: [PATCH 05/35] RAM slot to keep time for NRF52 --- boards/nrf52840_s140_v6.ld | 15 +++++++++++++-- boards/nrf52840_s140_v6_extrafs.ld | 15 +++++++++++++-- boards/nrf52840_s140_v7.ld | 15 +++++++++++++-- boards/nrf52840_s140_v7_extrafs.ld | 15 +++++++++++++-- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/boards/nrf52840_s140_v6.ld b/boards/nrf52840_s140_v6.ld index 6dad975b..d0c7d1dc 100644 --- a/boards/nrf52840_s140_v6.ld +++ b/boards/nrf52840_s140_v6.ld @@ -7,6 +7,9 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000 + /* To keep data in RAM across resets */ + PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 + /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -14,11 +17,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +44,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v6_extrafs.ld b/boards/nrf52840_s140_v6_extrafs.ld index 35261067..bd454747 100644 --- a/boards/nrf52840_s140_v6_extrafs.ld +++ b/boards/nrf52840_s140_v6_extrafs.ld @@ -7,6 +7,9 @@ MEMORY { FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xD4000 - 0x26000 + /* To keep data in RAM across resets */ + PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 + /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -14,11 +17,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +44,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7.ld b/boards/nrf52840_s140_v7.ld index 6aaeb403..2333238f 100644 --- a/boards/nrf52840_s140_v7.ld +++ b/boards/nrf52840_s140_v7.ld @@ -7,6 +7,9 @@ MEMORY { FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000 + /* To keep data in RAM across resets */ + PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 + /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -14,11 +17,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +44,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7_extrafs.ld b/boards/nrf52840_s140_v7_extrafs.ld index 5956183a..ef6ebba1 100644 --- a/boards/nrf52840_s140_v7_extrafs.ld +++ b/boards/nrf52840_s140_v7_extrafs.ld @@ -7,6 +7,9 @@ MEMORY { FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xD4000 - 0x27000 + /* To keep data in RAM across resets */ + PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 8 + /* SRAM required by Softdevice depend on * - Attribute Table Size (Number of Services and Characteristics) * - Vendor UUID count @@ -14,11 +17,19 @@ MEMORY * - Concurrent connection peripheral + central + secure links * - Event Len, HVN queue, Write CMD queue */ - RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 0x20040000 - 0x20006000 + RAM (rwx) : ORIGIN = 0x20006000 + 8, LENGTH = 0x20040000 - 0x20006000 - 8 } SECTIONS { + . = ALIGN(4); + .persistent (NOLOAD) : + { + KEEP(*(.persistent_magic)) + KEEP(*(.persistent_data)) + . = ALIGN(4); + } > PERSISTENT_RAM + . = ALIGN(4); .svc_data : { @@ -33,6 +44,6 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM -} INSERT AFTER .data; +} INCLUDE "nrf52_common.ld" From 0024226990b3f56438c26a851ece72fb6f897c41 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 12 Aug 2026 19:41:20 +0700 Subject: [PATCH 06/35] PS: rxps, reboot.interval, outpath --- docs/cli_commands.md | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8772b929..6f1e8f64 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -277,6 +277,38 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change RX duty-cycle power saving +**Usage:** +- `get radio.rxps` +- `get radio.rxps.rfrx_disabled` +- `get rxps.wd` +- `set radio.rxps.rfrx_disabled ` +- `set radio.rxps off` +- `set radio.rxps on` +- `set radio.rxps conservative` +- `set radio.rxps balanced` +- `set radio.rxps <1-10>` +- `set radio.rxps level <1-10>` +- `set radio.rxps level <1-10> preamble <16|32>` +- `set radio.rxps ` + +**Parameters:** +- `rx_us`, `sleep_us`: Receive and sleep durations in microseconds (`1000`-`30000000`). +- `level`: A power-saving level from `1` (most conservative) to `10` (least power saving). +- `preamble`: LoRa preamble length in symbols; `16` or `32`. +- `state`: `on` or `off`. + +**Notes:** +- `get rxps.wd` reports the RXPS watchdog's soft and hard recovery counts. +- `radio.rxps.rfrx_disabled` is a runtime-only diagnostic setting and resets to `off` after reboot. +- Its default `off` state keeps the host-controlled SX1262 receive path enabled during RX duty-cycle mode. Setting it to `on` reproduces the old missing-RF_RX behavior and can significantly reduce receive sensitivity, making remote commands harder to receive. +- `radio.rxps.rfrx_disabled` is supported only on SX1262 targets with a host-controlled RX enable pin. +- `on` and `conservative` select level `1` with a 16-symbol preamble; `balanced` selects level `5` with a 16-symbol preamble. +- Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom ` ` timings remain fixed. +- The selected mode is applied immediately, persisted, and restored after reboot. + +--- + #### View or change the LoRa FEM receive-path gain state on supported boards **Usage:** - `get radio.fem.rxgain` @@ -461,6 +493,18 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or set reboot interval (Repeater and room server) +**Usage:** +- `get reboot.interval` +- `set reboot.interval ` + +**Parameters:** +- `hours`: 0-255. 0 is disabled + +**Default:** `0` (disabled) + +--- + ### Routing #### View or change this node's repeat flag @@ -821,6 +865,27 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or set the direct path override for the current remote client +**Usage:** +- `get outpath` +- `set outpath ` +- `set outpath direct` +- `set outpath clear` +- `set outpath flood` + +**Parameters:** +- `hopN_hex`: Hop hash, `2`, `4`, or `6` hex characters. All hops must use the same width. + +**Notes:** +- These commands require remote client context (they target the caller's ACL entry). +- The path hash size is inferred from the hop hash width. +- `outpath` overrides the primary direct route used for replies to the caller. +- `direct` sets a zero-hop direct route for a caller reachable without repeaters. +- `clear` forgets the current direct path and allows normal path discovery to repopulate it. +- `flood` forces replies to use flood packets until the client logs in again. + +--- + #### Create a new region **Usage:** - `region put [parent_name]` From f8a90a492669a43bd177436f496e036faa05d3dc Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 12 Aug 2026 20:05:11 +0700 Subject: [PATCH 07/35] PS: file naming --- build.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index 80ff4cdf..b5044ad4 100755 --- a/build.sh +++ b/build.sh @@ -140,7 +140,8 @@ build_firmware() { # set firmware version string # e.g: v1.0.0-abcdef - FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + # FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}-${COMMIT_HASH}" + FIRMWARE_VERSION_STRING="${FIRMWARE_VERSION}" # craft filename # e.g: RAK_4631_Repeater-v1.0.0-SHA @@ -158,8 +159,8 @@ build_firmware() { # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then pio run -t mergebin -e $1 - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}-upgrade.bin 2>/dev/null || true + cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-freshInstall-merged.bin 2>/dev/null || true fi # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) From e7bd0a8904aea7ab2a6ac1545d2e480607b8c678 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 12 Aug 2026 22:21:22 +0700 Subject: [PATCH 08/35] PS: Disabled Blue led in T-echo Lite --- variants/lilygo_techo_lite/variant.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/lilygo_techo_lite/variant.cpp b/variants/lilygo_techo_lite/variant.cpp index 3cd82d70..2a56e586 100644 --- a/variants/lilygo_techo_lite/variant.cpp +++ b/variants/lilygo_techo_lite/variant.cpp @@ -22,8 +22,8 @@ void initVariant() { pinMode(LED_RED, OUTPUT); pinMode(LED_GREEN, OUTPUT); - pinMode(LED_BLUE, OUTPUT); - digitalWrite(LED_BLUE, HIGH); + // pinMode(LED_BLUE, OUTPUT); + // digitalWrite(LED_BLUE, HIGH); digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_RED, HIGH); From ba1a2ff598551376e8b949d79d496b9b7901d976 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 12 Aug 2026 23:00:03 +0700 Subject: [PATCH 09/35] PS: Added sensor support for Wio L1 Pro --- variants/wio-tracker-l1/platformio.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index fc958ea2..c1fe6c62 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -36,6 +36,8 @@ build_flags = -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 -D DISPLAY_CLASS=SH1106Display + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = ${WioTrackerL1.lib_deps} @@ -50,6 +52,8 @@ build_flags = ${WioTrackerL1.build_flags} -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' -D DISPLAY_CLASS=SH1106Display + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = ${WioTrackerL1.lib_deps} @@ -68,7 +72,6 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 - -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${WioTrackerL1.build_src_filter} From 21fc3bf24ded48e15eac4b8bd7399bf1fab5d672 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 18:10:52 +0700 Subject: [PATCH 10/35] Disabled NRF52_POWER_MANAGEMENT and added board_build.ldscript for time keep in NRF52 --- variants/gat562_30s_mesh_kit/platformio.ini | 3 ++- variants/gat562_mesh_evb_pro/platformio.ini | 2 +- variants/gat562_mesh_tracker_pro/platformio.ini | 3 ++- variants/gat562_mesh_watch13/platformio.ini | 2 +- variants/heltec_t114/platformio.ini | 4 +++- variants/promicro/platformio.ini | 1 + variants/rak_wismesh_tag/platformio.ini | 1 + variants/sensecap_solar/platformio.ini | 2 +- variants/xiao_nrf52/platformio.ini | 2 +- 9 files changed, 13 insertions(+), 7 deletions(-) diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 89276a49..47539743 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -2,12 +2,13 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index b3e89417..d7de585a 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index 142cfe4b..a7a2124a 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -2,10 +2,11 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index f3510b74..f457424f 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 48fb4b0f..79034be8 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 @@ -173,6 +173,7 @@ build_flags = -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; -D POWERSAVING_DEBUG=1 [env:Heltec_t114_repeater_bridge_rs232] extends = Heltec_t114 @@ -222,6 +223,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; -D POWERSAVING_DEBUG=1 build_src_filter = ${Heltec_t114_with_display.build_src_filter} + +<../examples/companion_radio/*.cpp> diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 90cb475f..38a7e206 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -1,6 +1,7 @@ [Promicro] extends = nrf52_base board = promicro_nrf52840 +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} -I variants/promicro -D PROMICRO diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index d6140187..edfe1b70 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -2,6 +2,7 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak_wismesh_tag diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index 70405218..718dd90b 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=12 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 1076bd8d..26acc866 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262 From 594d77096819639e7f27a1b76a7ce74b1ecbcbce Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:12:40 +0700 Subject: [PATCH 11/35] PowerSaving for companions --- examples/companion_radio/main.cpp | 40 +++++++++++++++++++++++ platformio.ini | 3 ++ src/helpers/ArduinoSerialInterface.cpp | 4 +++ src/helpers/ArduinoSerialInterface.h | 1 + src/helpers/BaseSerialInterface.h | 1 + src/helpers/MultiSerialInterface.h | 17 ++++++++++ src/helpers/esp32/SerialBLEInterface.cpp | 4 +++ src/helpers/esp32/SerialBLEInterface.h | 1 + src/helpers/esp32/SerialWifiInterface.cpp | 4 +++ src/helpers/esp32/SerialWifiInterface.h | 1 + src/helpers/nrf52/SerialBLEInterface.cpp | 4 +++ src/helpers/nrf52/SerialBLEInterface.h | 1 + 12 files changed, 81 insertions(+) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 89f0e6cb..951f60ea 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2,6 +2,11 @@ #include #include "MyMesh.h" +#ifdef ESP32_PLATFORM +#include "esp_pm.h" +#include "esp_bt.h" +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -242,6 +247,37 @@ void setup() { #endif board.onBootComplete(); + +#ifdef ESP32_PLATFORM +#if !CONFIG_IDF_TARGET_ESP32C6 + // Enable BLE sleep + esp_err_t errBLESleep = esp_bt_sleep_enable(); + if (errBLESleep == ESP_OK) { + Serial.println("Bluetooth sleep enabled successfully"); + } else { + Serial.printf("Bluetooth sleep enable failed: %s\n", esp_err_to_name(errBLESleep)); + } +#endif + +#if CONFIG_IDF_TARGET_ESP32C3 + esp_pm_config_esp32c3_t pm_config; +#elif CONFIG_IDF_TARGET_ESP32S3 + esp_pm_config_esp32s3_t pm_config; +#elif CONFIG_IDF_TARGET_ESP32 + esp_pm_config_esp32_t pm_config; +#elif CONFIG_IDF_TARGET_ESP32C6 + esp_pm_config_t pm_config; +#endif + + // Configure Power Management + pm_config = { .max_freq_mhz = 80, .min_freq_mhz = 40, .light_sleep_enable = true }; + esp_err_t errPM = esp_pm_configure(&pm_config); + if (errPM == ESP_OK) { + Serial.println("Power Management configured successfully"); + } else { + Serial.printf("Power Management failed to configure: %d\r\n", errPM); + } +#endif } void loop() { @@ -259,6 +295,10 @@ void loop() { if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible +#elif defined(ESP32_PLATFORM) + if (!bluetooth_interface.isReadBusy() && !bluetooth_interface.isWriteBusy()) { // BLE is not busy + vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep + } #endif } diff --git a/platformio.ini b/platformio.ini index 636abad1..efcca795 100644 --- a/platformio.ini +++ b/platformio.ini @@ -58,6 +58,9 @@ build_src_filter = [esp32_base] extends = arduino_base platform = platformio/espressif32@6.11.0 +; PowerSaving Arduino Core 2.0.17 for low power ESP32 boards +; https://github.com/IoTThinks/powersaving-arduino-lib +platform_packages = platformio/framework-arduinoespressif32@https://github.com/IoTThinks/powersaving-arduino-lib/releases/download/2.0.17/esp32-2.0.17.zip monitor_filters = esp32_exception_decoder extra_scripts = merge-bin.py build_flags = ${arduino_base.build_flags} diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index a01fa586..6b443974 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -17,6 +17,10 @@ bool ArduinoSerialInterface::isConnected() const { return true; // no way of knowing, so assume yes } +bool ArduinoSerialInterface::isReadBusy() const { + return false; +} + bool ArduinoSerialInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index c4086353..3eed7671 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -28,6 +28,7 @@ public: bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index 23933fcb..818f7186 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -16,6 +16,7 @@ public: virtual bool isConnected() const = 0; virtual void loop() {}; + virtual bool isReadBusy() const = 0; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; virtual size_t checkRecvFrame(uint8_t dest[]) = 0; diff --git a/src/helpers/MultiSerialInterface.h b/src/helpers/MultiSerialInterface.h index f7742b24..7bb827c1 100644 --- a/src/helpers/MultiSerialInterface.h +++ b/src/helpers/MultiSerialInterface.h @@ -140,6 +140,23 @@ public: } } + bool isReadBusy() const override { + // not busy when disabled + if (!_enabled) { + return false; + } + + // check if any interface is busy + for (auto iface : _interfaces) { + if (iface.instance && iface.instance->isEnabled() && iface.instance->isReadBusy()) { + return true; + } + } + + // nothing busy + return false; + } + bool isWriteBusy() const override { // not busy when disabled if(!_enabled){ diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 6371cf33..1a8e3593 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -188,6 +188,10 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { #define BLE_WRITE_MIN_INTERVAL 60 +bool SerialBLEInterface::isReadBusy() const { + return uxQueueMessagesWaiting(recv_queue) > 0; // Kevin +} + bool SerialBLEInterface::isWriteBusy() const { return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? } diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 9fb6adba..3298011b 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -82,6 +82,7 @@ public: bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index 462e3ecc..bdecb1a9 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -39,6 +39,10 @@ size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) { return 0; } +bool SerialWifiInterface::isReadBusy() const { + return false; +} + bool SerialWifiInterface::isWriteBusy() const { return false; } diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h index 19291497..1ff1d83d 100644 --- a/src/helpers/esp32/SerialWifiInterface.h +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -52,6 +52,7 @@ public: bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 75a4e3b0..a846e744 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -401,6 +401,10 @@ bool SerialBLEInterface::isConnected() const { return _isDeviceConnected && Bluefruit.connected() > 0; } +bool SerialBLEInterface::isReadBusy() const { + return (recv_queue_len > 0); +} + bool SerialBLEInterface::isWriteBusy() const { return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); } diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index de103054..d3cc5055 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -66,6 +66,7 @@ public: void disable() override; bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; From be6f3599e2d3ed09a24650531cf5440c6e48b460 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:15:13 +0700 Subject: [PATCH 12/35] PowerSaving for room servers --- examples/simple_room_server/MyMesh.cpp | 5 +++++ examples/simple_room_server/MyMesh.h | 3 +++ examples/simple_room_server/main.cpp | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 546d094f..53e7d697 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1077,3 +1077,8 @@ void MyMesh::loop() { uptime_millis += now - last_millis; last_millis = now; } + +// To check if there is pending work +bool MyMesh::hasPendingWork() const { + return _mgr->getOutboundTotal() > 0; +} diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5cf949c6..0af82bdb 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -229,4 +229,7 @@ public: void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); void loop(); + + // To check if there is pending work + bool hasPendingWork() const; }; diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index d833fff3..804fc214 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -157,4 +157,14 @@ void loop() { #ifdef HAS_EXTERNAL_WATCHDOG external_watchdog.loop(); #endif + + if (the_mesh.getNodePrefs()->powersaving_enabled && !the_mesh.hasPendingWork()) { +#if defined(NRF52_PLATFORM) + board.sleep(0); // nrf ignores seconds param, sleeps whenever possible +#else + if (the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep + board.sleep(30); // Sleep. Wake up after a while or when receiving a LoRa packet + } +#endif + } } From 43882d868a7dcbbc20283910019a21bb46142ea3 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:25:44 +0700 Subject: [PATCH 13/35] Added battery measurement for Xiao S3 Wio --- variants/xiao_s3_wio/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 293a13c1..9d4372d0 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -8,6 +8,7 @@ build_flags = ${esp32_base.build_flags} -I variants/xiao_s3_wio -UENV_INCLUDE_GPS -D SEEED_XIAO_S3 + -D PIN_VBAT_READ=1 ; D0 -D P_LORA_DIO_1=39 -D P_LORA_NSS=41 -D P_LORA_RESET=42 ; RADIOLIB_NC @@ -136,7 +137,6 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 - -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} From 8002d1e2ecf332f3528352cac5bff6ea49dff481 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:26:18 +0700 Subject: [PATCH 14/35] Disabled BLE_DEBUG_LOGGING for M6 --- variants/thinknode_m6/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/thinknode_m6/platformio.ini b/variants/thinknode_m6/platformio.ini index ad7e6902..4ed0848d 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -82,7 +82,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 +; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 ; -D MESH_PACKET_LOGGING=1 From 4cb67d9054909f2652522359a1b97d1d7e9c0ed5 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:34:50 +0700 Subject: [PATCH 15/35] GPS PowerSaving, outpath, sensor, reboot.interval, board_build.ldscript --- examples/simple_repeater/MyMesh.cpp | 160 ++++++++++++++- examples/simple_repeater/MyMesh.h | 10 +- examples/simple_repeater/UITask.cpp | 13 +- examples/simple_repeater/UITask.h | 4 +- examples/simple_repeater/main.cpp | 29 ++- src/helpers/ClientACL.h | 3 +- src/helpers/CommonCLI.cpp | 110 +++++++++-- src/helpers/CommonCLI.h | 2 + src/helpers/SensorManager.h | 3 +- .../sensors/EnvironmentSensorManager.cpp | 183 +++++++++++++----- .../sensors/EnvironmentSensorManager.h | 1 + src/helpers/sensors/LocationProvider.h | 24 +++ .../sensors/MicroNMEALocationProvider.h | 13 +- variants/heltec_mesh_solar/target.cpp | 74 ++++++- variants/heltec_mesh_solar/target.h | 1 + variants/heltec_tracker/target.cpp | 73 ++++++- variants/heltec_tracker/target.h | 1 + variants/rak3401/platformio.ini | 7 +- variants/rak3401/variant.h | 4 +- variants/rak4631/platformio.ini | 10 +- variants/rak4631/variant.h | 3 + variants/t1000-e/target.cpp | 84 +++++++- variants/t1000-e/target.h | 1 + 23 files changed, 694 insertions(+), 119 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7d0179f3..475e1dbf 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -689,7 +689,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { - if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT + // Outpath PR + if (mesh::Packet::isValidPathLen(client->out_path_len)) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); @@ -722,10 +723,12 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *ack = createAck(ack_hash); if (ack) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); - } else { + // Outpath PR + if (mesh::Packet::isValidPathLen(client->out_path_len)) { sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); + } else { + // Outpath PR + sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); } } } @@ -736,7 +739,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (is_retry) { *reply = 0; } else { - handleCommand(sender_timestamp, command, reply); + // Outpath PR + handleCommand(sender_timestamp, client, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -750,10 +754,12 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); - } else { + // Outpath PR + if (mesh::Packet::isValidPathLen(client->out_path_len)) { sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); + } else { + // Outpath PR + sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } } } @@ -772,8 +778,11 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); auto client = acl.getClientByIdx(i); + // Outpath PR // store a copy of path, for sendDirect() - client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + if (client->out_path_len != OUT_PATH_FORCE_FLOOD) { + client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); + } client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); @@ -1193,7 +1202,110 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } -void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { +// Outpath PR +static char* trimSpaces(char* s) { + while (*s == ' ') s++; + char* end = s + strlen(s); + while (end > s && end[-1] == ' ') end--; + *end = 0; + return s; +} + +static bool parsePathCommand(char* raw, uint8_t* out_path, uint8_t& out_path_len, const char*& err) { + if (raw == NULL || out_path == NULL) { + err = "Err - bad params"; + return false; + } + + char* spec = trimSpaces(raw); + if (*spec == 0) { + err = "Err - missing path"; + return false; + } + if (strcmp(spec, "clear") == 0 || strcmp(spec, "-") == 0 || strcmp(spec, "none") == 0) { + out_path_len = OUT_PATH_UNKNOWN; + return true; + } + if (strcmp(spec, "flood") == 0) { + out_path_len = OUT_PATH_FORCE_FLOOD; + return true; + } + if (strcmp(spec, "direct") == 0) { + out_path_len = 0; + return true; + } + + uint8_t hash_size = 0; + uint8_t hop_count = 0; + char* token = spec; + while (token && *token) { + char* comma = strchr(token, ','); + if (comma) *comma = 0; + token = trimSpaces(token); + + int hex_len = strlen(token); + if (!(hex_len == 2 || hex_len == 4 || hex_len == 6)) { + err = "Err - bad params"; + return false; + } + + uint8_t hop_hash_size = (uint8_t)(hex_len / 2); + if (hash_size == 0) { + hash_size = hop_hash_size; + } else if (hash_size != hop_hash_size) { + err = "Err - bad params"; + return false; + } + + if (hop_count >= 63 || (hop_count + 1) * hash_size > MAX_PATH_SIZE) { + err = "Err - bad params"; + return false; + } + if (!mesh::Utils::fromHex(&out_path[hop_count * hash_size], hash_size, token)) { + err = "Err - bad hex"; + return false; + } + + hop_count++; + token = comma ? comma + 1 : NULL; + } + + if (hash_size == 0 || hop_count == 0) { + err = "Err - missing path"; + return false; + } + out_path_len = ((hash_size - 1) << 6) | (hop_count & 63); + return true; +} + +static void formatPathReply(const uint8_t* path, uint8_t path_len, char* out, size_t out_len) { + if (path_len == OUT_PATH_FORCE_FLOOD) { + snprintf(out, out_len, "> flood"); + return; + } + if (path_len == OUT_PATH_UNKNOWN) { + snprintf(out, out_len, "> unknown"); + return; + } + if (!mesh::Packet::isValidPathLen(path_len)) { + snprintf(out, out_len, "> invalid"); + return; + } + if ((path_len & 63) == 0) { + snprintf(out, out_len, "> direct"); + return; + } + + uint8_t hash_size = (path_len >> 6) + 1; + uint8_t hop_count = path_len & 63; + uint8_t byte_len = hop_count * hash_size; + char hex[(MAX_PATH_SIZE * 2) + 1]; + mesh::Utils::toHex(hex, path, byte_len); + snprintf(out, out_len, "> hs=%u hops=%u hex=%s", (uint32_t)hash_size, (uint32_t)hop_count, hex); +} + +// Outpath PR +void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *command, char *reply) { if (region_load_active) { if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation region_map = temp_map; // copy over the temp instance as new current map @@ -1270,6 +1382,34 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (strcmp(command, "get outpath") == 0 + || strcmp(command, "set outpath") == 0 + || strncmp(command, "set outpath ", 12) == 0) { + bool is_get = strncmp(command, "get ", 4) == 0; + if (sender == NULL) { + strcpy(reply, "Err - command needs remote client context"); + } else if (is_get) { + formatPathReply(sender->out_path, sender->out_path_len, reply, 160); + } else { + char* spec = command + 11; // length of "set outpath" + if (*spec == ' ') spec++; + + uint8_t path[MAX_PATH_SIZE]; + uint8_t path_len = OUT_PATH_UNKNOWN; + const char* err = NULL; + if (!parsePathCommand(spec, path, path_len, err)) { + strcpy(reply, err ? err : "Err - invalid path"); + } else { + if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { + memset(sender->out_path, 0, sizeof(sender->out_path)); + sender->out_path_len = path_len; + } else { + sender->out_path_len = mesh::Packet::copyPath(sender->out_path, path, path_len); + } + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + formatPathReply(sender->out_path, sender->out_path_len, reply, 160); + } + } } else if (memcmp(command, "discover.neighbors", 18) == 0) { const char* sub = command + 18; while (*sub == ' ') sub++; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb9..f1cff542 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -163,6 +163,13 @@ protected: #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { + // If powersaving on, apply powersaving to sensors + if (_prefs.powersaving_enabled) { + sensors.powersaving_enabled = true; + } else { + sensors.powersaving_enabled = false; + } + sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); } #endif @@ -227,7 +234,8 @@ public: void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + // Outpath PR + void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command, char* reply); void loop(); #if defined(WITH_BRIDGE) diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index e7225557..89c833fa 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -29,7 +29,8 @@ static const uint8_t meshcore_logo [] PROGMEM = { 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; -void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) { +// Added board for battery display +void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version, mesh::MainBoard* board) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; _started_at = millis(); @@ -106,6 +107,16 @@ void UITask::renderCurrScreen() { _display->setCursor(0, 30); sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); _display->print(tmp); + + // Battery + _display->setCursor(0, 40); + sprintf(tmp, "BAT: %.2fV", _board->getBattMilliVolts() / 1000.0); + _display->print(tmp); + + // PowerSaving + _display->setCursor(0, 50); + sprintf(tmp, "PowerSaving: %s", _node_prefs->powersaving_enabled? "ON" : "off"); + _display->print(tmp); } } diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index d8e3ce1d..93845fb6 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -16,7 +16,9 @@ class UITask { void renderCurrScreen(); public: UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; } - void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); + + // Added board for battery display + void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version, mesh::MainBoard* board); void loop(); }; \ No newline at end of file diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68..3a9c480a 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -102,12 +102,25 @@ void setup() { ethernet_command[0] = 0; #endif +#if ENV_INCLUDE_GPS == 1 + // Apply PowerSaving profile for GPS + if (sensors.getLocationProvider() != NULL) { + // Let CLI "gps on" call setSettingValue to enable the PowerSaving mode + // sensors.powersaving_enabled = true; + // sensors.getLocationProvider()->enablePowerSaving(true); + + // GPS on and off duration in seconds + sensors.getLocationProvider()->setPowerSavingProfile(600, 86400); // Max 10 minutes, 1 day + } +#endif + sensors.begin(); the_mesh.begin(fs); #ifdef DISPLAY_CLASS - ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); + // Added board for battery display + ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION, &board); #endif #ifdef ETHERNET_ENABLED @@ -145,10 +158,12 @@ void loop() { reply[0] = 0; #ifdef ETHERNET_ENABLED if (!ethernet_handle_command(command, reply)) { - the_mesh.handleCommand(0, command, reply); + // Outpath PR + the_mesh.handleCommand(0, NULL, command, reply); } #else - the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + // Outpath PR + the_mesh.handleCommand(0, NULL, command, reply); // NOTE: there is no sender_timestamp via serial! #endif if (reply[0]) { Serial.print(" -> "); Serial.println(reply); @@ -163,7 +178,8 @@ void loop() { char reply[160]; reply[0] = 0; if (!ethernet_handle_command(ethernet_command, reply)) { - the_mesh.handleCommand(0, ethernet_command, reply); + // Outpath PR + the_mesh.handleCommand(0, NULL, ethernet_command, reply); } ethernet_send_reply(reply); ethernet_command[0] = 0; @@ -204,4 +220,9 @@ void loop() { } #endif } + + if (the_mesh.getNodePrefs()->reboot_interval > 0 && + the_mesh.millisHasNowPassed(the_mesh.getNodePrefs()->reboot_interval * 3600000)) { + board.reboot(); + } } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f706..e0b8c542 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,7 +10,8 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 -#define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_FORCE_FLOOD 0xFE +#define OUT_PATH_UNKNOWN 0xFF struct ClientInfo { mesh::Identity id; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58..66130a7e 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,8 @@ #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" #include +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 @@ -90,7 +92,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.read(pad, 3); // 153 + file.read((uint8_t *)&_prefs->reboot_interval, sizeof(_prefs->reboot_interval)); // 153 + file.read(pad, 2); // 154 file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 @@ -126,6 +129,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy _prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14); _prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1); + _prefs->reboot_interval = constrain(_prefs->reboot_interval, 0, 255); _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); @@ -324,7 +328,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re if (_sensors->setSettingValue("gps", "1")) { _prefs->gps_enabled = 1; savePrefs(); - strcpy(reply, "ok"); + + if (_prefs->powersaving_enabled) { // Power Saving + strcpy(reply, "on (powersaving)"); + } else { // Normal mode + strcpy(reply, "ok"); + } } else { strcpy(reply, "gps toggle not found"); } @@ -340,7 +349,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re LocationProvider * l = _sensors->getLocationProvider(); if (l != NULL) { l->syncTime(); - strcpy(reply, "ok"); + strcpy(reply, "scheduled"); } else { strcpy(reply, "gps provider not found"); } @@ -386,13 +395,40 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re bool fix = l->isValid(); // has fix ? int sats = l->satellitesCount(); bool active = !strcmp(_sensors->getSettingByKey("gps"), "1"); - if (enabled) { - sprintf(reply, "on, %s, %s, %d sats", - active?"active":"deactivated", - fix?"fix":"no fix", - sats); - } else { - strcpy(reply, "off"); + + if (_prefs->powersaving_enabled && l->isPowerSavingEnabled()) { // GPS Power Saving + if (enabled) { + unsigned long mins = (l->getNextSleep() - millis()) / 60000UL; + sprintf(reply, "on (powersaving, sleep in %luh %lum), %s, %s, %d sats", + mins / 60UL, + mins % 60UL, + active ? "active" : "deactivated", + fix ? "fix" : "no fix", + sats); + } else { + unsigned long mins = (l->getNextWake() - millis()) / 60000UL; + sprintf(reply, "off (powersaving, wake in %luh %lum)", + mins / 60UL, + mins % 60UL); + } + + // "last sync" from GPS + DateTime dt = DateTime(l->getLastValidTimeSync()); + if (dt.unixtime() == 0) { + sprintf(reply + strlen(reply), ", last sync: none"); + } else { + sprintf(reply + strlen(reply), ", last sync: %02d:%02d - %d/%d/%d UTC", dt.hour(), dt.minute(), + dt.day(), dt.month(), dt.year()); + } + } else { // Normal mode + if (enabled) { + sprintf(reply, "on, %s, %s, %d sats", + active?"active":"deactivated", + fix?"fix":"no fix", + sats); + } else { + strcpy(reply, "off"); + } } } else { strcpy(reply, "Can't find GPS"); @@ -401,10 +437,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else if (memcmp(command, "powersaving on", 14) == 0) { #if defined(NRF52_PLATFORM) _prefs->powersaving_enabled = 1; + _sensors->powersaving_enabled = 1; savePrefs(); strcpy(reply, "on - Immediate effect"); #elif defined(ESP32) && !defined(WITH_BRIDGE) _prefs->powersaving_enabled = 1; + _sensors->powersaving_enabled = 1; savePrefs(); strcpy(reply, "on - After 2 minutes"); #elif defined(WITH_BRIDGE) @@ -414,6 +452,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re #endif } else if (memcmp(command, "powersaving off", 15) == 0) { _prefs->powersaving_enabled = 0; + _sensors->powersaving_enabled = 0; savePrefs(); strcpy(reply, "off"); } else if (memcmp(command, "powersaving", 11) == 0) { @@ -422,6 +461,36 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "off"); } + } else if (memcmp(command, "sensor", 6) == 0) { + // I2C +#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL) + sprintf(reply, "I2C Wire1: SDA=%s,SCL=%s\r\n", STR(ENV_PIN_SDA), STR(ENV_PIN_SCL)); +#elif defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) + sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_BOARD_SDA), STR(PIN_BOARD_SCL)); +#elif defined(PIN_WIRE_SDA) && defined(PIN_WIRE_SCL) + sprintf(reply, "I2C Wire: SDA=%s, SCL=%s\r\n", STR(PIN_WIRE_SDA), STR(PIN_WIRE_SCL)); +#else + sprintf(reply, "I2C GPIOs not defined\r\n"); +#endif + + // GPS +#if defined(PIN_GPS_RX) && defined(PIN_GPS_TX) + sprintf(reply + strlen(reply), "GPS Serial: RX=%s, TX=%s", STR(PIN_GPS_RX), STR(PIN_GPS_TX)); +#ifdef ENV_INCLUDE_GPS> 0 + sprintf(reply + strlen(reply), ". Configured"); +#else + sprintf(reply + strlen(reply), ". Not configured"); +#endif +#else + sprintf(reply + strlen(reply), "GPS Serial not defined"); +#endif + } else if (memcmp(command, "powerlog", 8) == 0) { + sprintf(reply, "Last reset reason: %s", _board->getResetReasonString(_board->getResetReason())); +#if defined(NRF52_PLATFORM) + sprintf(reply + strlen(reply), "\r\nLast shutdown reason: %s", + _board->getShutdownReasonString(_board->getShutdownReason())); + sprintf(reply + strlen(reply), "\r\nLast boot voltage: %u mV", _board->getBootVoltage()); +#endif } else if (memcmp(command, "log start", 9) == 0) { _callbacks->setLoggingOn(true); strcpy(reply, " logging on"); @@ -798,6 +867,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } } #endif + } else if (memcmp(config, "reboot.interval ", 16) == 0) { + int hours = _atoi(&config[16]); + if (hours == 0) { + _prefs->reboot_interval = 0; + savePrefs(); + strcpy(reply, "reboot.interval disabled"); + } else if (hours < 1 || 255 < hours) { + strcpy(reply, "Error: interval range is 1-255 hours"); + } else { + _prefs->reboot_interval = hours; + savePrefs(); + sprintf(reply, "OK - reboot.interval set to %d", _prefs->reboot_interval); + } } else { strcpy(reply, "unknown config: "); StrHelper::strncpy(&reply[16], config, 160-17); @@ -816,7 +898,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (memcmp(config, "int.thresh", 10) == 0) { sprintf(reply, "> %d", (uint32_t) _prefs->interference_threshold); } else if (memcmp(config, "cad", 3) == 0) { - sprintf(reply, "> %s", _prefs->cad_enabled ? "on" : "off"); + sprintf(reply, "> %s. # channel busy: %u", _prefs->cad_enabled ? "on" : "off", _board->n_cad_busy); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { sprintf(reply, "> %d", ((uint32_t) _prefs->agc_reset_interval) * 4); } else if (memcmp(config, "multi.acks", 10) == 0) { @@ -981,6 +1063,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep if (tmp == reply) { sprintf(reply, "No extra SF configured"); } + } else if (memcmp(config, "reboot.interval", 15) == 0) { + if (_prefs->reboot_interval == 0) { + strcpy(reply, "disabled"); + } else { + sprintf(reply, "> %d", (uint8_t)_prefs->reboot_interval); + } } else { sprintf(reply, "??: %s", config); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e..8b590b81 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -56,6 +56,7 @@ public: char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) // Power setting uint8_t powersaving_enabled = 0; // boolean + uint8_t reboot_interval = 0; // hours, 0-255 (default 0=disable) // Gps settings uint8_t gps_enabled = 0; uint32_t gps_interval = 0; // in seconds @@ -134,6 +135,7 @@ private: void structure() override { def("adc_mult", _parent->adc_multiplier); def("pwr_sav_en", _parent->powersaving_enabled); + def("pwr_reboot_int", _parent->reboot_interval); } public: PowerPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/src/helpers/SensorManager.h b/src/helpers/SensorManager.h index 89a174c2..2031bbc5 100644 --- a/src/helpers/SensorManager.h +++ b/src/helpers/SensorManager.h @@ -13,8 +13,9 @@ class SensorManager { public: double node_lat, node_lon; // modify these, if you want to affect Advert location double node_altitude; // altitude in meters + bool powersaving_enabled; // powersaving mode - SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; } + SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; powersaving_enabled = false; } virtual bool begin() { return false; } virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; } virtual void loop() { } diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index e2f0d33e..d490b2a9 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -15,6 +15,7 @@ #if ENV_INCLUDE_BME680_BSEC #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 +#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -40,6 +41,7 @@ static uint32_t bsec_last_save_ms = 0; #ifdef ENV_INCLUDE_BME680 #ifndef TELEM_BME680_ADDRESS #define TELEM_BME680_ADDRESS 0x76 +#define TELEM_BME680_ADDRESS_2 0x77 #endif #define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25) #include @@ -63,6 +65,7 @@ static Adafruit_AHTX0 AHTX0; #if ENV_INCLUDE_BME280 #ifndef TELEM_BME280_ADDRESS #define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address +#define TELEM_BME280_ADDRESS_2 0x77 #endif #define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -72,6 +75,7 @@ static Adafruit_BME280 BME280; #if ENV_INCLUDE_BMP280 #ifndef TELEM_BMP280_ADDRESS #define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address +#define TELEM_BMP280_ADDRESS_2 0x77 #endif #define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Atmospheric pressure at sea level #include @@ -165,7 +169,9 @@ static RAK12035_SoilMoisture RAK12035; #endif #ifdef RAK_WISBLOCK_GPS -static uint32_t gpsResetPin = 0; +// -1 = no enable pin; out-of-range values are no-ops in pinMode/digitalWrite, +// while 0 would be a real GPIO (P0.00 = LFXO crystal on nRF52) +static uint32_t gpsResetPin = -1; static bool i2cGPSFlag = false; static bool serialGPSFlag = false; #ifndef TELEM_RAK12500_ADDRESS @@ -181,6 +187,7 @@ class RAK12500LocationProvider : public LocationProvider { int _sats = 0; long _epoch = 0; bool _fix = false; + int _pin_en = -1; public: long getLatitude() override { return _lat; } long getLongitude() override { return _lng; } @@ -193,18 +200,25 @@ public: void begin() override { } void stop() override { } void loop() override { - if (ublox_GNSS.getGnssFixOk(8)) { - _fix = true; - _lat = ublox_GNSS.getLatitude(2) / 10; - _lng = ublox_GNSS.getLongitude(2) / 10; - _alt = ublox_GNSS.getAltitude(2); - _sats = ublox_GNSS.getSIV(2); - } else { - _fix = false; + static long next_gps_update = 0; + if ((int32_t)(millis() - next_gps_update) >= 0) { + if (ublox_GNSS.getGnssFixOk(8)) { + _fix = true; + _lat = ublox_GNSS.getLatitude(2) / 10; + _lng = ublox_GNSS.getLongitude(2) / 10; + _alt = ublox_GNSS.getAltitude(2); + _sats = ublox_GNSS.getSIV(2); + } else { + _fix = false; + } + _epoch = ublox_GNSS.getUnixEpoch(2); + + next_gps_update = millis() + 1000; } - _epoch = ublox_GNSS.getUnixEpoch(2); } bool isEnabled() override { return true; } + void setPinEn(int pin_en) override { _pin_en = pin_en; } + int getPinEn() override { return _pin_en; } }; static RAK12500LocationProvider RAK12500_provider; @@ -557,15 +571,27 @@ static const SensorDef SENSOR_TABLE[] = { #endif #ifdef ENV_INCLUDE_BME680 { TELEM_BME680_ADDRESS, "BME680", init_bme680, query_bme680 }, + #ifdef TELEM_BME680_ADDRESS_2 + { TELEM_BME680_ADDRESS_2, "BME680", init_bme680, query_bme680 }, + #endif #endif #if ENV_INCLUDE_BME680_BSEC { TELEM_BME680_ADDRESS, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + #ifdef TELEM_BME680_ADDRESS_2 + { TELEM_BME680_ADDRESS_2, "BME680+BSEC", init_bme680_bsec, query_bme680_bsec }, + #endif #endif #if ENV_INCLUDE_BME280 { TELEM_BME280_ADDRESS, "BME280", init_bme280, query_bme280 }, + #ifdef TELEM_BME280_ADDRESS_2 + { TELEM_BME280_ADDRESS_2, "BME280", init_bme280, query_bme280 }, + #endif #endif #if ENV_INCLUDE_BMP280 { TELEM_BMP280_ADDRESS, "BMP280", init_bmp280, query_bmp280 }, + #ifdef TELEM_BMP280_ADDRESS_2 + { TELEM_BMP280_ADDRESS_2, "BMP280", init_bmp280, query_bmp280 }, + #endif #endif #if ENV_INCLUDE_SHTC3 { 0x70, "SHTC3", init_shtc3, query_shtc3 }, @@ -714,8 +740,16 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val #if ENV_INCLUDE_GPS if (gps_detected && strcmp(name, "gps") == 0) { if (strcmp(value, "0") == 0) { + if (powersaving_enabled) { + _location->enablePowerSaving(false); + } + stop_gps(); } else { + if (powersaving_enabled) { + _location->enablePowerSaving(true); + } + start_gps(); } return true; @@ -774,24 +808,19 @@ void EnvironmentSensorManager::initBasicGPS() { // gps code for rak might be moved to MicroNMEALoactionProvider // or make a new location provider ... #ifdef RAK_WISBLOCK_GPS -void EnvironmentSensorManager::rakGPSInit(){ - +void EnvironmentSensorManager::rakGPSInit() { Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX); - #ifdef GPS_BAUD_RATE +#ifdef GPS_BAUD_RATE Serial1.begin(GPS_BAUD_RATE); - #else +#else Serial1.begin(9600); - #endif +#endif - //search for the correct IO standby pin depending on socket used - if(gpsIsAwake(WB_IO2)){ - } - else if(gpsIsAwake(WB_IO4)){ - } - else if(gpsIsAwake(WB_IO5)){ - } - else{ + // search for the correct IO standby pin depending on socket used + if (gpsIsAwake(WB_IO2)) { + _location->setPinEn(WB_IO2); // WB_IO2 is the power switch for all sensor and IO slots + } else { MESH_DEBUG_PRINTLN("No GPS found"); gps_active = false; gps_detected = false; @@ -799,13 +828,13 @@ void EnvironmentSensorManager::rakGPSInit(){ return; } - #ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented - //Now that GPS is found and set up, set to sleep for initial state +#ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented + // Now that GPS is found and set up, set to sleep for initial state stop_gps(); - #endif +#endif } -bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ +bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin) { #if defined(ETHERNET_ENABLED) && defined(RAK_BOARD) if (ioPin == WB_IO2) { @@ -813,17 +842,16 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ return false; } #endif - //set initial waking state - pinMode(ioPin,OUTPUT); - digitalWrite(ioPin,LOW); + pinMode(ioPin, OUTPUT); + digitalWrite(ioPin, LOW); delay(500); - digitalWrite(ioPin,HIGH); + digitalWrite(ioPin, HIGH); delay(500); - //Try to init RAK12500 on I2C - if (ublox_GNSS.begin(Wire) == true){ - MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i",ioPin); + // Try to init RAK12500 on I2C + if (ublox_GNSS.begin(Wire) == true) { + MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i", ioPin); ublox_GNSS.setI2COutput(COM_TYPE_UBX); ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GPS); ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GALILEO); @@ -841,13 +869,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ _location = &RAK12500_provider; return true; - } else if (Serial1.available()) { + } else if (Serial1.available()) { // RAK12501 (L76K) on UART MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on"); -#ifdef PIN_GPS_EN - if(PIN_GPS_EN){ - gpsResetPin = PIN_GPS_EN; - } -#endif + gpsResetPin = ioPin; serialGPSFlag = true; gps_active = true; gps_detected = true; @@ -862,11 +886,23 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){ void EnvironmentSensorManager::start_gps() { gps_active = true; - #ifdef RAK_WISBLOCK_GPS - pinMode(gpsResetPin, OUTPUT); - digitalWrite(gpsResetPin, HIGH); - return; - #endif + + if (powersaving_enabled && _location->isPowerSavingEnabled()) { + gps_wake = true; // gps_active is true + _location->syncTime(); // Clear GPS data and force sync time + _location->setNextSleep(); // Next time to off + } + +#ifdef RAK_WISBLOCK_GPS +#ifdef FORCE_GPS_ALIVE + // There is no UART command to enter power save for L76K + if (i2cGPSFlag) ublox_GNSS.powerSaveMode(false); +#else + pinMode(gpsResetPin, OUTPUT); + digitalWrite(gpsResetPin, HIGH); // WB_IO2 +#endif + return; +#endif _location->begin(); _location->reset(); @@ -877,10 +913,23 @@ void EnvironmentSensorManager::start_gps() { } void EnvironmentSensorManager::stop_gps() { - gps_active = false; + if (powersaving_enabled && _location->isPowerSavingEnabled()) { + gps_wake = false; // gps_active is unchanged (true) even the GPS sleep (e.g: off) + _location->stopTimeSync(); // Stop time sync + _location->setNextWake(); // Next time to on + } else { + gps_active = false; + gps_wake = false; // When GPS is off, wake is false to be sure + } + #ifdef RAK_WISBLOCK_GPS + #ifdef FORCE_GPS_ALIVE + // There is no UART command to enter power save for L76K + if(i2cGPSFlag) ublox_GNSS.powerSaveMode(false); + #else pinMode(gpsResetPin, OUTPUT); - digitalWrite(gpsResetPin, LOW); + digitalWrite(gpsResetPin, LOW); // WB_IO2 + #endif return; #endif @@ -896,13 +945,39 @@ void EnvironmentSensorManager::stop_gps() { void EnvironmentSensorManager::loop() { #if ENV_INCLUDE_GPS - static unsigned long next_gps_update = 0; - if (gps_active) { + static long next_gps_update = 0; + + // PowerSaving + if (powersaving_enabled) { + if (gps_detected && _location->isPowerSavingEnabled()) { + if (gps_wake && ((int32_t)(millis() - _location->getNextSleep()) >= 0 || + !_location->waitingTimeSync())) { // Time to off or GPS set + if ((int32_t)(millis() - _location->getNextSleep()) >= 0) { + POWERSAVING_DEBUG_PRINTLN("GPS wake timeout. Enter sleep"); + } + else if (!_location->waitingTimeSync()) { + POWERSAVING_DEBUG_PRINTLN("GPS set. Enter sleep early"); + } + + stop_gps(); + } else if (!gps_wake && ((int32_t)(millis() - _location->getNextWake()) >= 0)) { // Time to on + POWERSAVING_DEBUG_PRINTLN("GPS sleep timeout. Wakeup."); + + start_gps(); + } else if (!gps_wake && _location->waitingTimeSync()) { // On for "gps sync" + POWERSAVING_DEBUG_PRINTLN("CLI gps sync. Wakeup"); + + start_gps(); + } + } + } + + if ((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)) { _location->loop(); } - if ((long)(millis() - next_gps_update) > 0) { - if(gps_active){ + if ((int32_t)(millis() - next_gps_update) >= 0) { + if((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)){ #ifdef RAK_WISBLOCK_GPS if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) { node_lat = ((double)_location->getLatitude())/1000000.; @@ -920,8 +995,12 @@ void EnvironmentSensorManager::loop() { MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); } #endif + + // In powersaving mode, GPS is on and off. Only update data when GPS is on + if(powersaving_enabled) next_gps_update = millis() + (gps_update_interval_sec * 1000); } - next_gps_update = millis() + (gps_update_interval_sec * 1000); + + if(!powersaving_enabled) next_gps_update = millis() + (gps_update_interval_sec * 1000); } #endif #if ENV_INCLUDE_BME680_BSEC diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 29147c89..40c44e90 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -21,6 +21,7 @@ protected: bool gps_detected = false; bool gps_active = false; + bool gps_wake = false; // In PowerSaving, gps_active can have gps_wake true or false uint32_t gps_update_interval_sec = 1; #if ENV_INCLUDE_GPS diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652..c9204373 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -7,9 +7,31 @@ class LocationProvider { protected: bool _time_sync_needed = true; + bool powersaving_enabled = false; + unsigned long _wake_duration_secs = 86400; // Full day + unsigned long _sleep_duration_secs = 0; // No off + unsigned long _next_wake = 0; + unsigned long _next_sleep = 0; + unsigned long _last_valid_time_sync = 0; + public: virtual void syncTime() { _time_sync_needed = true; } virtual bool waitingTimeSync() { return _time_sync_needed; } + virtual void stopTimeSync() { _time_sync_needed = false; } + + virtual void setPowerSavingProfile(unsigned long wake_duration_secs, unsigned long sleep_duration_secs) { + _wake_duration_secs = wake_duration_secs; + _sleep_duration_secs = sleep_duration_secs; + } + + virtual void enablePowerSaving(bool enabled) { powersaving_enabled = enabled; _next_wake = 0; _next_sleep = 0; } + virtual bool isPowerSavingEnabled() { return powersaving_enabled; } + virtual void setNextWake() { _next_wake = millis() + _sleep_duration_secs * 1000UL; } + virtual unsigned long getNextWake() { return _next_wake; } + virtual void setNextSleep() { _next_sleep = millis() + _wake_duration_secs * 1000UL; } + virtual unsigned long getNextSleep() { return _next_sleep; } + virtual unsigned long getLastValidTimeSync() { return _last_valid_time_sync; } + virtual long getLatitude() = 0; virtual long getLongitude() = 0; virtual long getAltitude() = 0; @@ -22,4 +44,6 @@ public: virtual void stop() = 0; virtual void loop() = 0; virtual bool isEnabled() = 0; + virtual void setPinEn(int pin_en) = 0; + virtual int getPinEn() = 0; }; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index b66cb114..b7d8d14e 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -113,6 +113,14 @@ public : } } + void setPinEn(int pin_en) override { + _pin_en = pin_en; + } + + int getPinEn() override { + return _pin_en; + } + void syncTime() override { nmea.clear(); LocationProvider::syncTime(); } long getLatitude() override { return nmea.getLatitude(); } long getLongitude() override { return nmea.getLongitude(); } @@ -122,7 +130,7 @@ public : return alt; } long satellitesCount() override { return nmea.getNumSatellites(); } - bool isValid() override { return nmea.isValid(); } + bool isValid() override { return nmea.isValid() && nmea.getYear() < 2030; } // TODO: Pending better validation long getTimestamp() override { DateTime dt(nmea.getYear(), nmea.getMonth(),nmea.getDay(),nmea.getHour(),nmea.getMinute(),nmea.getSecond()); @@ -156,9 +164,10 @@ public : _clock->setCurrentTime(getTimestamp()); _time_sync_needed = false; _last_time_sync = millis(); + _last_valid_time_sync = _clock->getCurrentTime(); } } - if (isValid()) { + if (isValid() && satellitesCount() >= 5) { time_valid ++; } } diff --git a/variants/heltec_mesh_solar/target.cpp b/variants/heltec_mesh_solar/target.cpp index 94e2684c..28442334 100644 --- a/variants/heltec_mesh_solar/target.cpp +++ b/variants/heltec_mesh_solar/target.cpp @@ -32,13 +32,28 @@ mesh::LocalIdentity radio_new_identity() { void SolarSensorManager::start_gps() { if (!gps_active) { gps_active = true; + + if (powersaving_enabled && _location->isPowerSavingEnabled()) { + gps_wake = true; // gps_active is true + _location->syncTime(); // Clear GPS data and force sync time + _location->setNextSleep(); // Next time to off + } + _location->begin(); } } void SolarSensorManager::stop_gps() { if (gps_active) { - gps_active = false; + if (powersaving_enabled && _location->isPowerSavingEnabled()) { + gps_wake = false; // gps_active is unchanged (true) even the GPS sleep (e.g: off) + _location->stopTimeSync(); // Stop time sync + _location->setNextWake(); // Next time to on + } else { + gps_active = false; + gps_wake = false; // When GPS is off, wake is false to be sure + } + _location->stop(); } } @@ -68,16 +83,49 @@ bool SolarSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& void SolarSensorManager::loop() { static long next_gps_update = 0; - _location->loop(); + // PowerSaving + if (powersaving_enabled) { + if (gps_detected && _location->isPowerSavingEnabled()) { + if (gps_wake && ((int32_t)(millis() - _location->getNextSleep()) >= 0 || + !_location->waitingTimeSync())) { // Time to off or GPS set + if ((int32_t)(millis() - _location->getNextSleep()) >= 0) { + POWERSAVING_DEBUG_PRINTLN("GPS wake timeout. Enter sleep"); + } else if (!_location->waitingTimeSync()) { + POWERSAVING_DEBUG_PRINTLN("GPS set. Enter sleep early"); + } - if (millis() > next_gps_update) { - if (_location->isValid()) { - node_lat = ((double)_location->getLatitude())/1000000.; - node_lon = ((double)_location->getLongitude())/1000000.; - node_altitude = ((double)_location->getAltitude()) / 1000.0; - MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + stop_gps(); + } else if (!gps_wake && ((int32_t)(millis() - _location->getNextWake()) >= 0)) { // Time to on + POWERSAVING_DEBUG_PRINTLN("GPS sleep timeout. Wakeup."); + + start_gps(); + } else if (!gps_wake && _location->waitingTimeSync()) { // On for "gps sync" + POWERSAVING_DEBUG_PRINTLN("CLI gps sync. Wakeup"); + + start_gps(); + } } - next_gps_update = millis() + 1000; + } + + if ((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)) { + _location->loop(); + } + + if ((int32_t)(millis() - next_gps_update) >= 0) { + if ((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)) { + if (_location->isValid()) { + node_lat = ((double)_location->getLatitude()) / 1000000.; + node_lon = ((double)_location->getLongitude()) / 1000000.; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); + } + + // In powersaving mode, GPS is on and off. Only update data when GPS is on + if (powersaving_enabled) next_gps_update = millis() + 1000; + } + + if (!powersaving_enabled) next_gps_update = millis() + 1000; } } @@ -99,8 +147,16 @@ const char* SolarSensorManager::getSettingValue(int i) const { bool SolarSensorManager::setSettingValue(const char* name, const char* value) { if (gps_detected && strcmp(name, "gps") == 0) { if (strcmp(value, "0") == 0) { + if (powersaving_enabled) { + _location->enablePowerSaving(false); + } + stop_gps(); } else { + if (powersaving_enabled) { + _location->enablePowerSaving(true); + } + start_gps(); } return true; diff --git a/variants/heltec_mesh_solar/target.h b/variants/heltec_mesh_solar/target.h index 8e13a960..f80d2247 100644 --- a/variants/heltec_mesh_solar/target.h +++ b/variants/heltec_mesh_solar/target.h @@ -16,6 +16,7 @@ class SolarSensorManager : public SensorManager { bool gps_active = false; bool gps_detected = false; + bool gps_wake = false; // In PowerSaving, gps_active can have gps_wake true or false LocationProvider* _location; void start_gps(); diff --git a/variants/heltec_tracker/target.cpp b/variants/heltec_tracker/target.cpp index f32c41ff..9bc5ab7c 100644 --- a/variants/heltec_tracker/target.cpp +++ b/variants/heltec_tracker/target.cpp @@ -46,13 +46,28 @@ void HWTSensorManager::start_gps() { if (!gps_active) { _location->begin(); // Claims periph_power via RefCountedDigitalPin gps_active = true; + + if (powersaving_enabled && _location->isPowerSavingEnabled()) { + gps_wake = true; // gps_active is true + _location->syncTime(); // Clear GPS data and force sync time + _location->setNextSleep(); // Next time to off + } + Serial1.println("$CFGSYS,h35155*68"); // Configure GPS for all constellations } } void HWTSensorManager::stop_gps() { if (gps_active) { - gps_active = false; + if (powersaving_enabled && _location->isPowerSavingEnabled()) { + gps_wake = false; // gps_active is unchanged (true) even the GPS sleep (e.g: off) + _location->stopTimeSync(); // Stop time sync + _location->setNextWake(); // Next time to on + } else { + gps_active = false; + gps_wake = false; // When GPS is off, wake is false to be sure + } + _location->stop(); // Releases periph_power via RefCountedDigitalPin } } @@ -73,16 +88,48 @@ bool HWTSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& t void HWTSensorManager::loop() { static long next_gps_update = 0; - _location->loop(); + // PowerSaving + if (powersaving_enabled) { + if (_location->isPowerSavingEnabled()) { + if (gps_wake && ((int32_t)(millis() - _location->getNextSleep()) >= 0 || + !_location->waitingTimeSync())) { // Time to off or GPS set + if ((int32_t)(millis() - _location->getNextSleep()) >= 0) { + POWERSAVING_DEBUG_PRINTLN("GPS wake timeout. Enter sleep"); + } else if (!_location->waitingTimeSync()) { + POWERSAVING_DEBUG_PRINTLN("GPS set. Enter sleep early"); + } - if (millis() > next_gps_update) { - if (gps_active && _location->isValid()) { - node_lat = ((double)_location->getLatitude())/1000000.; - node_lon = ((double)_location->getLongitude())/1000000.; - node_altitude = ((double)_location->getAltitude()) / 1000.0; - MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + stop_gps(); + } else if (!gps_wake && ((int32_t)(millis() - _location->getNextWake()) >= 0)) { // Time to on + POWERSAVING_DEBUG_PRINTLN("GPS sleep timeout. Wakeup."); + + start_gps(); + } else if (!gps_wake && _location->waitingTimeSync()) { // On for "gps sync" + POWERSAVING_DEBUG_PRINTLN("CLI gps sync. Wakeup"); + + start_gps(); + } } - next_gps_update = millis() + 1000; + } + + if ((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)) { + _location->loop(); + } + + if ((int32_t)(millis() - next_gps_update) >= 0) { + if ((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)) { + if (_location->isValid()) { + node_lat = ((double)_location->getLatitude()) / 1000000.; + node_lon = ((double)_location->getLongitude()) / 1000000.; + node_altitude = ((double)_location->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); + } + + // In powersaving mode, GPS is on and off. Only update data when GPS is on + if (powersaving_enabled) next_gps_update = millis() + 1000; + } + + if (!powersaving_enabled) next_gps_update = millis() + 1000; } } @@ -100,8 +147,16 @@ const char* HWTSensorManager::getSettingValue(int i) const { bool HWTSensorManager::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps") == 0) { if (strcmp(value, "0") == 0) { + if (powersaving_enabled) { + _location->enablePowerSaving(false); + } + stop_gps(); } else { + if (powersaving_enabled) { + _location->enablePowerSaving(true); + } + start_gps(); } return true; diff --git a/variants/heltec_tracker/target.h b/variants/heltec_tracker/target.h index 2931eda8..0fe3d025 100644 --- a/variants/heltec_tracker/target.h +++ b/variants/heltec_tracker/target.h @@ -15,6 +15,7 @@ class HWTSensorManager : public SensorManager { bool gps_active = false; + bool gps_wake = false; // In PowerSaving, gps_active can have gps_wake true or false LocationProvider * _location; void start_gps(); diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 48e71922..a16c3e46 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -2,17 +2,22 @@ extends = nrf52_base board = rak3401 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 - -D NRF52_POWER_MANAGEMENT + -D RAK_BOARD +; -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM + -D FORCE_GPS_ALIVE ; on RAK3401, gpsResetPin ends up = WB_IO2 (kills 3V3_S rail) or + ; pin 4 (= SX126X_RESET, holds radio in reset). stop_gps() must not run. + -D PIN_GPS_EN=-1 ; -1 = no dedicated GPS enable pin (power is the shared 3V3_S rail), same as rak4631 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak3401> + diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 98827886..a2e32ae2 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -188,8 +188,8 @@ static const uint8_t AREF = PIN_AREF; // Power is on the controllable 3V3_S rail #define PIN_GPS_PPS (17) // Pulse per second input from the GPS -#define PIN_GPS_RX PIN_SERIAL1_RX -#define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX #define PIN_GPS_1PPS PIN_GPS_PPS #define GPS_BAUD_RATE 9600 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 0dfbf797..0ac5ec8b 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -2,6 +2,7 @@ extends = nrf52_base board = rak4631 board_check = true +board_build.ldscript = boards/nrf52840_s140_v6.ld extra_scripts = ${nrf52_base.extra_scripts} post:variants/rak4631/fix_bsec_lib.py build_flags = ${nrf52_base.build_flags} @@ -9,11 +10,10 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 - -D PIN_GPS_TX=PIN_SERIAL1_RX - -D PIN_GPS_RX=PIN_SERIAL1_TX +; -D FORCE_GPS_ALIVE // Do not off WB_IO2 if we want to keep other sensor / IO slots on -D PIN_GPS_EN=-1 -D PIN_OLED_RESET=-1 -D USE_SX1262 @@ -49,6 +49,7 @@ build_flags = -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; -D POWERSAVING_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + +<../examples/simple_repeater> @@ -220,10 +221,11 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 +; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; -D POWERSAVING_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + +<../examples/companion_radio/*.cpp> diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index 38cc8868..42ca3b0c 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -182,6 +182,9 @@ extern "C" #define EXTERNAL_FLASH_DEVICES IS25LP080D #define EXTERNAL_FLASH_USE_QSPI +#define PIN_GPS_RX PIN_SERIAL1_TX +#define PIN_GPS_TX PIN_SERIAL1_RX + #define PIN_GPS_1PPS 17 //GPS PPS pin #define GPS_BAUD_RATE 9600 #define GPS_ADDRESS 0x42 //i2c address for GPS diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index 42532827..99c9be29 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -81,6 +81,13 @@ mesh::LocalIdentity radio_new_identity() { void T1000SensorManager::start_gps() { gps_active = true; + + if (powersaving_enabled && _nmea->isPowerSavingEnabled()) { + gps_wake = true; // gps_active is true + _nmea->syncTime(); // Clear GPS data and force sync time + _nmea->setNextSleep(); // Next time to off + } + //_nmea->begin(); // this init sequence should be better // comes from seeed examples and deals with all gps pins @@ -104,7 +111,15 @@ void T1000SensorManager::start_gps() { } void T1000SensorManager::sleep_gps() { - gps_active = false; + if (powersaving_enabled && _nmea->isPowerSavingEnabled()) { + gps_wake = false; // gps_active is unchanged (true) even the GPS sleep (e.g: off) + _nmea->stopTimeSync(); // Stop time sync + _nmea->setNextWake(); // Next time to on + } else { + gps_active = false; + gps_wake = false; // When GPS is off, wake is false to be sure + } + digitalWrite(GPS_VRTC_EN, HIGH); digitalWrite(GPS_EN, LOW); digitalWrite(GPS_RESET, HIGH); @@ -116,7 +131,15 @@ void T1000SensorManager::sleep_gps() { } void T1000SensorManager::stop_gps() { - gps_active = false; + if (powersaving_enabled && _nmea->isPowerSavingEnabled()) { + gps_wake = false; // gps_active is unchanged (true) even the GPS sleep (e.g: off) + _nmea->stopTimeSync(); // Stop time sync + _nmea->setNextWake(); // Next time to on + } else { + gps_active = false; + gps_wake = false; // When GPS is off, wake is false to be sure + } + digitalWrite(GPS_VRTC_EN, LOW); digitalWrite(GPS_EN, LOW); digitalWrite(GPS_RESET, HIGH); @@ -149,16 +172,49 @@ bool T1000SensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& void T1000SensorManager::loop() { static long next_gps_update = 0; - _nmea->loop(); + // PowerSaving + if (powersaving_enabled) { + if (_nmea->isPowerSavingEnabled()) { + if (gps_wake && ((int32_t)(millis() - _nmea->getNextSleep()) >= 0 || + !_nmea->waitingTimeSync())) { // Time to off or GPS set + if ((int32_t)(millis() - _nmea->getNextSleep()) >= 0) { + POWERSAVING_DEBUG_PRINTLN("GPS wake timeout. Enter sleep"); + } else if (!_nmea->waitingTimeSync()) { + POWERSAVING_DEBUG_PRINTLN("GPS set. Enter sleep early"); + } - if (millis() > next_gps_update) { - if (gps_active && _nmea->isValid()) { - node_lat = ((double)_nmea->getLatitude())/1000000.; - node_lon = ((double)_nmea->getLongitude())/1000000.; - node_altitude = ((double)_nmea->getAltitude()) / 1000.0; - //Serial.printf("lat %f lon %f\r\n", _lat, _lon); + stop_gps(); + } else if (!gps_wake && ((int32_t)(millis() - _nmea->getNextWake()) >= 0)) { // Time to on + POWERSAVING_DEBUG_PRINTLN("GPS sleep timeout. Wakeup."); + + start_gps(); + } else if (!gps_wake && _nmea->waitingTimeSync()) { // On for "gps sync" + POWERSAVING_DEBUG_PRINTLN("CLI gps sync. Wakeup"); + + start_gps(); + } } - next_gps_update = millis() + 1000; + } + + if ((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)) { + _nmea->loop(); + } + + if ((int32_t)(millis() - next_gps_update) >= 0) { + if ((!powersaving_enabled && gps_active) || (powersaving_enabled && gps_wake)) { + if (_nmea->isValid()) { + node_lat = ((double)_nmea->getLatitude()) / 1000000.; + node_lon = ((double)_nmea->getLongitude()) / 1000000.; + MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon); + node_altitude = ((double)_nmea->getAltitude()) / 1000.0; + MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude); + } + + // In powersaving mode, GPS is on and off. Only update data when GPS is on + if (powersaving_enabled) next_gps_update = millis() + 1000; + } + + if (!powersaving_enabled) next_gps_update = millis() + 1000; } } @@ -176,8 +232,16 @@ const char* T1000SensorManager::getSettingValue(int i) const { bool T1000SensorManager::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps") == 0) { if (strcmp(value, "0") == 0) { + if (powersaving_enabled) { + _nmea->enablePowerSaving(false); + } + sleep_gps(); // sleep for faster fix ! } else { + if (powersaving_enabled) { + _nmea->enablePowerSaving(true); + } + start_gps(); } return true; diff --git a/variants/t1000-e/target.h b/variants/t1000-e/target.h index db003cc5..cd91ceec 100644 --- a/variants/t1000-e/target.h +++ b/variants/t1000-e/target.h @@ -14,6 +14,7 @@ class T1000SensorManager: public SensorManager { bool gps_active = false; + bool gps_wake = false; // In PowerSaving, gps_active can have gps_wake true or false LocationProvider * _nmea; void start_gps(); From a00fbbe252c1e1b48f6eca746403088dac9c17fa Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:40:01 +0700 Subject: [PATCH 16/35] Removed obsolete enterDeepSleep of TBbeam --- src/helpers/esp32/TBeamBoard.h | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index 98bd16bf..543226d6 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -86,7 +86,6 @@ #include #include "XPowersLib.h" #include "helpers/ESP32Board.h" -#include class TBeamBoard : public ESP32Board { XPowersLibInterface *PMU = NULL; @@ -131,29 +130,6 @@ public: } #endif - void enterDeepSleep(uint32_t secs, int pin_wake_btn) { - esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - - // Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep - rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY); - rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1); - - rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - - if (pin_wake_btn < 0) { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet - } else { - esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn - } - - if (secs > 0) { - esp_sleep_enable_timer_wakeup(secs * 1000000); - } - - // Finally set ESP32 into sleep - esp_deep_sleep_start(); // CPU halts here and never returns! -} - uint16_t getBattMilliVolts(){ return PMU->getBattVoltage(); } From 772dc8470816ee5da9ab553482adab04a90c737f Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:41:01 +0700 Subject: [PATCH 17/35] Time keep for NRF52 across resets --- src/helpers/ArduinoHelpers.cpp | 6 +++++ src/helpers/ArduinoHelpers.h | 42 ++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/helpers/ArduinoHelpers.cpp diff --git a/src/helpers/ArduinoHelpers.cpp b/src/helpers/ArduinoHelpers.cpp new file mode 100644 index 00000000..feb77a79 --- /dev/null +++ b/src/helpers/ArduinoHelpers.cpp @@ -0,0 +1,6 @@ +#include + +extern "C" { + __attribute__((section(".persistent_magic"))) uint32_t persistent_magic; + __attribute__((section(".persistent_data"))) uint32_t persistent_time; +} \ No newline at end of file diff --git a/src/helpers/ArduinoHelpers.h b/src/helpers/ArduinoHelpers.h index 97596daa..9b50b98c 100644 --- a/src/helpers/ArduinoHelpers.h +++ b/src/helpers/ArduinoHelpers.h @@ -3,19 +3,57 @@ #include #include +#ifdef NRF52_PLATFORM +#define CLOCK_MAGIC_NUM 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + +extern uint32_t persistent_magic; +extern uint32_t persistent_time; +#endif + class VolatileRTCClock : public mesh::RTCClock { uint32_t base_time; uint64_t accumulator; unsigned long prev_millis; + public: - VolatileRTCClock() { base_time = 1715770351; accumulator = 0; prev_millis = millis(); } // 15 May 2024, 8:50pm + VolatileRTCClock() { +#ifdef NRF52_PLATFORM + if (persistent_magic == CLOCK_MAGIC_NUM && persistent_time >= RTC_TIME_MIN) { + base_time = persistent_time; + } else { + base_time = RTC_TIME_MIN; + } +#else + base_time = 1715770351; +#endif + + accumulator = 0; + prev_millis = millis(); + } + uint32_t getCurrentTime() override { return base_time + accumulator/1000; } - void setCurrentTime(uint32_t time) override { base_time = time; accumulator = 0; prev_millis = millis(); } + + void setCurrentTime(uint32_t time) override { + base_time = time; + accumulator = 0; + prev_millis = millis(); + +#ifdef NRF52_PLATFORM + persistent_magic = CLOCK_MAGIC_NUM; + persistent_time = time; +#endif + } void tick() override { unsigned long now = millis(); accumulator += (now - prev_millis); prev_millis = now; + +#ifdef NRF52_PLATFORM + persistent_magic = CLOCK_MAGIC_NUM; + persistent_time = getCurrentTime(); +#endif } }; From 687bac55eb31f2cb127b6b966eb12507d73be4a7 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:42:39 +0700 Subject: [PATCH 18/35] Time keep for ESP32 accross reboots --- src/helpers/ESP32Board.h | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index d7eb5fee..511bd1fe 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -197,19 +197,30 @@ public: } }; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; +static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; +#define RTC_BACKUP_MAGIC 0xAA55CC33 +#define RTC_TIME_MIN 1772323200 // 1 Mar 2026 + class ESP32RTCClock : public mesh::RTCClock { public: ESP32RTCClock() { } void begin() { esp_reset_reason_t reason = esp_reset_reason(); - if (reason == ESP_RST_POWERON) { - // start with some date/time in the recent past - struct timeval tv; - tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm + if (reason == ESP_RST_DEEPSLEEP) { + return; // ESP-IDF preserves system time across deep sleep + } + // All other resets (power-on, crash, WDT, brownout) lose system time. + // Restore from RTC backup if valid, otherwise use hardcoded seed. + struct timeval tv; + if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) { + tv.tv_sec = _rtc_backup_time; + } else { + tv.tv_sec = RTC_TIME_MIN; + } tv.tv_usec = 0; settimeofday(&tv, NULL); } - } uint32_t getCurrentTime() override { time_t _now; time(&_now); @@ -220,6 +231,16 @@ public: tv.tv_sec = time; tv.tv_usec = 0; settimeofday(&tv, NULL); + _rtc_backup_time = time; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } + void tick() override { + time_t now; + time(&now); + if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) { + _rtc_backup_time = (uint32_t)now; + _rtc_backup_magic = RTC_BACKUP_MAGIC; + } } }; From 6e4e6f16a6426067cbf05a147ecfba7cded0201a Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:43:46 +0700 Subject: [PATCH 19/35] Prevented back-powering to save 2.8 mA for ST7735Display --- src/helpers/ui/ST7735Display.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 344f5b1d..95588d17 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -528,6 +528,15 @@ void ST7735Display::turnOff() { //digitalWrite(PIN_TFT_RST, LOW); // Now turn off the backlight digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE); + + // Prevent back-powering to save 2.8 mA + pinMode(PIN_TFT_CS, INPUT); + pinMode(PIN_TFT_DC, INPUT); + pinMode(PIN_TFT_SDA, INPUT); + pinMode(PIN_TFT_SCL, INPUT); + pinMode(PIN_TFT_RST, INPUT); + pinMode(PIN_TFT_LEDA_CTL, INPUT); + _isOn = false; if (_peripher_power) _peripher_power->release(); From 1e888cb31aa3c59a186238dbbf68ca71cdfed9c3 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 22:44:12 +0700 Subject: [PATCH 20/35] Added POWERSAVING_DEBUG --- src/MeshCore.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/MeshCore.h b/src/MeshCore.h index e67371ef..12f591e2 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -37,6 +37,15 @@ #define BRIDGE_DEBUG_PRINTLN(...) {} #endif +#if POWERSAVING_DEBUG && ARDUINO + #include + #define POWERSAVING_DEBUG_PRINT(F, ...) Serial.printf("POWERSAVING: " F, ##__VA_ARGS__) + #define POWERSAVING_DEBUG_PRINTLN(F, ...) Serial.printf("POWERSAVING: " F "\n", ##__VA_ARGS__) +#else + #define POWERSAVING_DEBUG_PRINT(...) {} + #define POWERSAVING_DEBUG_PRINTLN(...) {} +#endif + namespace mesh { #define BD_STARTUP_NORMAL 0 // getStartupReason() codes @@ -79,6 +88,8 @@ public: virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } + + inline static uint32_t n_cad_busy = 0; }; /** From d42f4240fff842dd9f5df0e753611127715073ef Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 23:24:50 +0700 Subject: [PATCH 21/35] Fixed PowerSaving for ESP32 USB companions --- examples/companion_radio/main.cpp | 2 ++ src/helpers/esp32/SerialBLEInterface.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 951f60ea..854aec62 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -296,9 +296,11 @@ void loop() { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible #elif defined(ESP32_PLATFORM) +#if defined(BLE_PIN_CODE) if (!bluetooth_interface.isReadBusy() && !bluetooth_interface.isWriteBusy()) { // BLE is not busy vTaskDelay(pdMS_TO_TICKS(10)); // attempt to sleep } +#endif #endif } diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 1a8e3593..2e1343f5 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -189,7 +189,7 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { #define BLE_WRITE_MIN_INTERVAL 60 bool SerialBLEInterface::isReadBusy() const { - return uxQueueMessagesWaiting(recv_queue) > 0; // Kevin + return uxQueueMessagesWaiting(recv_queue) > 0; } bool SerialBLEInterface::isWriteBusy() const { From 0e47bdf846ef23a7ca7b1601b855b08ee653aa38 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 23:25:35 +0700 Subject: [PATCH 22/35] Hardcoded femon, femoff for v4 and T096 --- variants/heltec_t096/T096Board.cpp | 4 ++++ variants/heltec_t096/platformio.ini | 12 ++++++++++-- variants/heltec_v4/HeltecV4Board.cpp | 4 ++++ variants/heltec_v4/platformio.ini | 24 ++++++++++++++++++++++-- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 78af529d..af048e38 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -122,6 +122,10 @@ const char* T096Board::getManufacturerName() const { } bool T096Board::setLoRaFemLnaEnabled(bool enable) { +#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) + enable = false; +#endif + if (!loRaFEMControl.isLnaCanControl()) { return false; } diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 02e88328..cb0a511f 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 - -D NRF52_POWER_MANAGEMENT +; -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 @@ -69,6 +69,7 @@ build_flags = -D MAX_NEIGHBOURS=50 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; -D POWERSAVING_DEBUG=1 [env:Heltec_t096_repeater_bridge_rs232] extends = Heltec_t096 @@ -120,7 +121,7 @@ build_src_filter = ${Heltec_t096.build_src_filter} lib_deps = ${Heltec_t096.lib_deps} -[env:Heltec_t096_companion_radio_ble] +[env:Heltec_t096_companion_radio_ble_femon] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 @@ -135,6 +136,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +; -D POWERSAVING_DEBUG=1 build_src_filter = ${Heltec_t096.build_src_filter} + +<../examples/companion_radio/*.cpp> @@ -143,6 +145,12 @@ lib_deps = ${Heltec_t096.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_t096_companion_radio_ble_femoff] +extends = env:Heltec_t096_companion_radio_ble_femon +build_flags = + ${env:Heltec_t096_companion_radio_ble_femon.build_flags} + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off + [env:Heltec_t096_companion_radio_usb] extends = Heltec_t096 board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 3f13f41f..ba3a7f1c 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -64,6 +64,10 @@ void HeltecV4Board::begin() { } bool HeltecV4Board::setLoRaFemLnaEnabled(bool enable) { +#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0) + enable = false; +#endif + if (!loRaFEMControl.isLnaCanControl()) { return false; } diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index d718c006..c2c48303 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -179,7 +179,7 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_companion_radio_usb] +[env:heltec_v4_companion_radio_usb_femon] extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} @@ -199,7 +199,13 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:heltec_v4_companion_radio_ble] +[env:heltec_v4_companion_radio_usb_femoff] +extends = env:heltec_v4_companion_radio_usb_femon +build_flags = + ${env:heltec_v4_companion_radio_usb_femon.build_flags} + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off + +[env:heltec_v4_companion_radio_ble_femon] extends = heltec_v4_oled build_flags = ${heltec_v4_oled.build_flags} @@ -223,6 +229,12 @@ lib_deps = ${heltec_v4_oled.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:heltec_v4_3_companion_radio_ble_femoff] +extends = env:heltec_v4_companion_radio_ble_femon +build_flags = + ${env:heltec_v4_companion_radio_ble_femon.build_flags} + -D RADIO_FEM_RXGAIN=0 ; undefined (default on), 1=on, 0=off + [env:heltec_v4_companion_radio_wifi] extends = heltec_v4_oled build_flags = @@ -388,6 +400,14 @@ lib_deps = ${heltec_v4_tft.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:heltec_v4_expansionkit_tft_companion_radio_ble] +extends = env:heltec_v4_tft_companion_radio_ble +platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 +build_flags = + ${env:heltec_v4_tft_companion_radio_ble.build_flags} + -D ENV_PIN_SDA=4 + -D ENV_PIN_SCL=3 + [env:heltec_v4_tft_companion_radio_wifi] extends = heltec_v4_tft build_flags = From 4bad6830b16921378fca9117f2e3e767d5e71877 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Fri, 14 Aug 2026 23:26:04 +0700 Subject: [PATCH 23/35] Build script for PowerSaving 17.1 --- build-iotthinks.sh | 185 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 build-iotthinks.sh diff --git a/build-iotthinks.sh b/build-iotthinks.sh new file mode 100644 index 00000000..1567e869 --- /dev/null +++ b/build-iotthinks.sh @@ -0,0 +1,185 @@ +# sh ./build-repeaters-iotthinks.sh +export FIRMWARE_VERSION="PowerSaving17.1" + +############# Repeaters ############# +# Commonly-used boards +## ESP32 - 18 boards +sh build.sh build-firmware \ +Heltec_ct62_repeater \ +Heltec_E290_repeater \ +Heltec_v3_repeater \ +heltec_v4_repeater \ +heltec_v4_r8_repeater \ +heltec_tracker_v2_repeater \ +Heltec_Wireless_Paper_repeater \ +Heltec_Wireless_Tracker_repeater \ +Heltec_WSL3_repeater \ +LilyGo_T3S3_sx1262_repeater \ +LilyGo_TBeam_1W_repeater \ +Station_G2_repeater \ +T_Beam_S3_Supreme_SX1262_repeater \ +Tbeam_SX1262_repeater \ +ThinkNode_M5_Repeater \ +Xiao_C3_repeater \ +Xiao_C6_repeater_ \ +Xiao_S3_repeater \ +Xiao_S3_WIO_repeater + +## NRF52 - 22 boards +sh build.sh build-firmware \ +GAT562_30S_Mesh_Kit_repeater \ +GAT562_Mesh_Tracker_Pro_repeater \ +Heltec_mesh_solar_repeater \ +Heltec_t096_repeater \ +Heltec_t114_repeater \ +Heltec_t1_repeater \ +ikoka_nano_nrf_22dbm_repeater \ +ikoka_nano_nrf_30dbm_repeater \ +ikoka_nano_nrf_33dbm_repeater \ +LilyGo_T-Echo_Card_repeater \ +LilyGo_T-Echo_repeater \ +ProMicro_repeater \ +RAK_3401_repeater \ +RAK_4631_repeater \ +RAK_WisMesh_Tag_repeater \ +SenseCap_Solar_repeater \ +t1000e_repeater \ +ThinkNode_M1_repeater \ +ThinkNode_M3_repeater \ +ThinkNode_M6_repeater \ +WioTrackerL1_repeater \ +Xiao_nrf52_repeater + +## ESP32, SX1276 - 3 boards +sh build.sh build-firmware \ +Heltec_v2_repeater \ +LilyGo_TLora_V2_1_1_6_repeater \ +Tbeam_SX1276_repeater + +############# Room Server ############# +# ESP32 - 8 boards +sh build.sh build-firmware \ +Heltec_v3_room_server \ +heltec_v4_room_server \ +heltec_v4_r8_room_server \ +heltec_tracker_v2_room_server \ +Heltec_Wireless_Paper_room_server \ +Heltec_WSL3_room_server \ +LilyGo_TBeam_1W_room_server \ +Xiao_S3_room_server + +# NRF52 - 7 boards +sh build.sh build-firmware \ +Heltec_t096_room_server \ +Heltec_t114_room_server \ +RAK_3401_room_server \ +RAK_4631_room_server \ +t1000e_room_server \ +WioTrackerL1_room_server \ +Xiao_nrf52_room_server + +############# Companions BLE ############# +# NRF52 - 17 boards +sh build.sh build-firmware \ +Heltec_t096_companion_radio_ble_femon \ +Heltec_t096_companion_radio_ble_femoff \ +Heltec_t1_companion_radio_ble \ +Heltec_t114_companion_radio_ble \ +LilyGo_T-Echo_Card_companion_radio_ble \ +LilyGo_T-Echo_companion_radio_ble \ +ProMicro_companion_radio_ble \ +RAK_3401_companion_radio_ble \ +RAK_4631_companion_radio_ble \ +RAK_WisMesh_Tag_companion_radio_ble \ +SenseCap_Solar_companion_radio_ble \ +t1000e_companion_radio_ble \ +ThinkNode_M1_companion_radio_ble \ +ThinkNode_M3_companion_radio_ble \ +ThinkNode_M6_companion_radio_ble \ +WioTrackerL1_companion_radio_ble \ +Xiao_nrf52_companion_radio_ble + +############# Companions BLE PS ############# +# ESP32 - 21 boards +sh build.sh build-firmware \ +Heltec_ct62_companion_radio_ble \ +heltec_tracker_v2_companion_radio_ble \ +Heltec_v2_companion_radio_ble \ +Heltec_v3_companion_radio_ble \ +heltec_v4_3_companion_radio_ble_femoff \ +heltec_v4_companion_radio_ble_femon \ +heltec_v4_expansionkit_tft_companion_radio_ble \ +heltec_v4_r8_companion_radio_ble \ +Heltec_Wireless_Paper_companion_radio_ble \ +Heltec_Wireless_Tracker_companion_radio_ble \ +Heltec_WSL3_companion_radio_ble \ +LilyGo_T3S3_sx1262_companion_radio_ble \ +LilyGo_TBeam_1W_companion_radio_ble \ +LilyGo_TLora_V2_1_1_6_companion_radio_ble \ +T_Beam_S3_Supreme_SX1262_companion_radio_ble \ +Tbeam_SX1262_companion_radio_ble \ +Tbeam_SX1276_companion_radio_ble \ +ThinkNode_M5_companion_radio_ble \ +Xiao_C3_companion_radio_ble \ +Xiao_S3_companion_radio_ble \ +Xiao_S3_WIO_companion_radio_ble + +############# Companions USB ############# +sh build.sh build-firmware \ +Heltec_t096_companion_radio_usb \ +heltec_v4_companion_radio_usb \ +heltec_v4_companion_radio_usb_femoff \ +heltec_v4_companion_radio_usb_femon + +############# Sample builds ############# +# 24 boards +sh build.sh build-firmware \ +Heltec_t096_companion_radio_ble_femon \ +Heltec_t096_companion_radio_ble_femoff \ +Heltec_t096_companion_radio_usb \ +Heltec_t096_repeater \ +Heltec_t114_companion_radio_ble \ +Heltec_t114_repeater \ +Heltec_v3_companion_radio_ble \ +Heltec_v3_repeater \ +heltec_v4_3_companion_radio_ble_femoff \ +heltec_v4_repeater \ +RAK_3401_companion_radio_ble \ +RAK_3401_repeater \ +RAK_4631_companion_radio_ble \ +RAK_4631_repeater \ +SenseCap_Solar_companion_radio_ble \ +SenseCap_Solar_repeater \ +WioTrackerL1_companion_radio_ble \ +WioTrackerL1_repeater \ +Xiao_C3_companion_radio_ble \ +Xiao_C3_repeater \ +Xiao_C6_companion_radio_ble_ \ +Xiao_C6_repeater_ \ +Xiao_nrf52_companion_radio_ble \ +Xiao_nrf52_repeater + +# 4 boards +sh build.sh build-firmware \ +heltec_v4_companion_radio_ble_femon \ +heltec_v4_companion_radio_usb \ +heltec_v4_companion_radio_usb_femoff \ +heltec_v4_companion_radio_usb_femon + +# Mini sample builds +sh build.sh build-firmware \ +Heltec_t096_companion_radio_ble_femon \ +Heltec_t096_companion_radio_usb \ +Heltec_t096_repeater \ +heltec_v4_3_companion_radio_ble_femoff \ +heltec_v4_repeater \ +RAK_3401_companion_radio_ble \ +RAK_3401_repeater \ +RAK_4631_companion_radio_ble \ +RAK_4631_repeater \ +Xiao_C3_companion_radio_ble \ +Xiao_C3_repeater \ +Xiao_C6_companion_radio_ble_ \ +Xiao_C6_repeater_ \ +Xiao_nrf52_companion_radio_ble \ +Xiao_nrf52_repeater \ No newline at end of file From 320ce563d1b563c89a0392e4bbe601f1a7517149 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 15 Aug 2026 13:13:25 +0700 Subject: [PATCH 24/35] BLE Powersaving for heltec_v4_expansionkit --- variants/heltec_v4/platformio.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index c2c48303..54d9a8cd 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -402,7 +402,6 @@ lib_deps = [env:heltec_v4_expansionkit_tft_companion_radio_ble] extends = env:heltec_v4_tft_companion_radio_ble -platform_packages = framework-arduinoespressif32 @ symlink://D:/esp32-2.0.17 build_flags = ${env:heltec_v4_tft_companion_radio_ble.build_flags} -D ENV_PIN_SDA=4 From e1758d2ece3030c3df7aacab59844baaf1d0bb53 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 15 Aug 2026 18:26:32 +0700 Subject: [PATCH 25/35] Fixed for T096 OLED to save 3.5mA and use dark theme --- src/helpers/ui/ST7735Display.cpp | 33 ++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 95588d17..ac0a3f08 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -417,16 +417,16 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { #define PIN_TFT_LEDA_CTL_ACTIVE HIGH #endif -// Color scheme -ColorVal UIColor::window_bkg = ST77XX_WHITE; -ColorVal UIColor::title_bkg = ST77XX_BLUE; -ColorVal UIColor::title_txt = ST77XX_WHITE; -ColorVal UIColor::primary_txt = ST77XX_BLACK; -ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray -ColorVal UIColor::warning_txt = ST77XX_ORANGE; -ColorVal UIColor::popup_bkg = ST77XX_CYAN; -ColorVal UIColor::popup_txt = ST77XX_BLACK; -ColorVal UIColor::corp_blue = 0x001A; +// Color scheme - Dark theme before v1.17 +ColorVal UIColor::window_bkg = ST77XX_BLACK; +ColorVal UIColor::title_bkg = ST77XX_BLACK; +ColorVal UIColor::title_txt = ST77XX_GREEN; +ColorVal UIColor::primary_txt = ST77XX_GREEN; +ColorVal UIColor::secondary_txt = ST77XX_YELLOW; +ColorVal UIColor::warning_txt = ST77XX_RED; +ColorVal UIColor::popup_bkg = ST77XX_BLACK; +ColorVal UIColor::popup_txt = ST77XX_WHITE; +ColorVal UIColor::corp_blue = ST77XX_BLUE; bool ST7735Display::begin() { if (!sprite) { @@ -512,6 +512,15 @@ void ST7735Display::_resetAndInit() { void ST7735Display::turnOn() { if (!_isOn) { if (_peripher_power) _peripher_power->claim(); + + // Restore GPIOs + pinMode(PIN_TFT_CS, OUTPUT); + pinMode(PIN_TFT_DC, OUTPUT); + pinMode(PIN_TFT_SDA, OUTPUT); + pinMode(PIN_TFT_SCL, OUTPUT); + pinMode(PIN_TFT_RST, OUTPUT); + pinMode(PIN_TFT_LEDA_CTL, OUTPUT); + _resetAndInit(); sendCommand(ST77XX_DISPON); @@ -525,11 +534,11 @@ void ST7735Display::turnOff() { if (_isOn) { sendCommand(ST77XX_DISPOFF); - //digitalWrite(PIN_TFT_RST, LOW); + // digitalWrite(PIN_TFT_RST, LOW); // Now turn off the backlight digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE); - // Prevent back-powering to save 2.8 mA + // Prevent back-powering to save 3.5mA (from 12.6 down to 9.1mA) pinMode(PIN_TFT_CS, INPUT); pinMode(PIN_TFT_DC, INPUT); pinMode(PIN_TFT_SDA, INPUT); From c0af1b5cb10e7a7f08d821305c6d18f2186e6ae3 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sat, 15 Aug 2026 18:29:27 +0700 Subject: [PATCH 26/35] Fixed to use shorter platform_packages for BLE PowerSaving --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index efcca795..97842603 100644 --- a/platformio.ini +++ b/platformio.ini @@ -60,7 +60,7 @@ extends = arduino_base platform = platformio/espressif32@6.11.0 ; PowerSaving Arduino Core 2.0.17 for low power ESP32 boards ; https://github.com/IoTThinks/powersaving-arduino-lib -platform_packages = platformio/framework-arduinoespressif32@https://github.com/IoTThinks/powersaving-arduino-lib/releases/download/2.0.17/esp32-2.0.17.zip +platform_packages = framework-arduinoespressif32 @ https://github.com/IoTThinks/powersaving-arduino-lib/releases/download/2.0.17/esp32-2.0.17.zip monitor_filters = esp32_exception_decoder extra_scripts = merge-bin.py build_flags = ${arduino_base.build_flags} From cb7c4b0fb6e7340178a79f09b09e54b3122094a1 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 16 Aug 2026 15:32:44 +0700 Subject: [PATCH 27/35] Fixed esp32c6_base to extend arduino_base --- platformio.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index a7bb2890..0763f64c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -76,8 +76,14 @@ lib_deps = ; esp32c6 uses arduino framework 3.x ; WARNING: experimental. May not work as stable as other platforms. [esp32c6_base] -extends = esp32_base +extends = arduino_base platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13-1/platform-espressif32.zip +monitor_filters = esp32_exception_decoder +extra_scripts = merge-bin.py +build_flags = + ${arduino_base.build_flags} + -D ESP32_PLATFORM +build_src_filter = ${arduino_base.build_src_filter} ; ----------------- NRF52 --------------------- From 54f292f40d7a245fbd9ca5dba68bb1f2a7531eaf Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 16 Aug 2026 16:07:03 +0700 Subject: [PATCH 28/35] Added uptime to companion screen --- examples/companion_radio/ui-new/UITask.cpp | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 051f3b31..bcc94d63 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -109,7 +109,13 @@ class HomeScreen : public UIScreen { bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; - + // Uptime tracking + // Stores the last 32-bit millis() value used to calculate elapsed time. + // uint32_t naturally handles the ~49.7-day millis() rollover. + uint32_t uptime_last_millis = millis(); + // 64-bit accumulated uptime in milliseconds. + // Allows uptime to continue beyond the 49.7-day millis() rollover. + uint64_t uptime_millis = 0; void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage #ifndef BATT_MIN_MILLIVOLTS @@ -131,6 +137,27 @@ class HomeScreen : public UIScreen { int iconY = 0; display.setColor(UIColor::title_txt); + // Uptime + uint32_t now = millis(); + uptime_millis += (uint32_t)(now - uptime_last_millis); + uptime_last_millis = now; + + uint64_t uptimeMinutes = uptime_millis / 60000ULL; + char uptime[16]; + if (uptimeMinutes < 60) { + snprintf(uptime, sizeof(uptime), "%lum", (unsigned long)uptimeMinutes); + } else if (uptimeMinutes < 1440) { + snprintf(uptime, sizeof(uptime), "%luh", (unsigned long)(uptimeMinutes / 60)); + } else { + snprintf(uptime, sizeof(uptime), "%lud", (unsigned long)(uptimeMinutes / 1440)); + } + + display.setTextSize(1); + int uptimeWidth = display.getTextWidth(uptime); + int spaceWidth = display.getTextWidth(" "); + display.setCursor(iconX - uptimeWidth - spaceWidth, iconY); // 1 extra space between uptime and battery icon + display.print(uptime); + // battery outline display.drawRect(iconX, iconY, iconWidth, iconHeight); From 1d36e0e019af93707f3273315b0cdaa850a07aa9 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Sun, 16 Aug 2026 18:45:42 +0700 Subject: [PATCH 29/35] Added missing ENABLE_USB_INTERFACE for Xiao S3 Wio --- variants/xiao_s3_wio/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 9d4372d0..9ec62a89 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -137,6 +137,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} From 54e5bb04e172da63897aff59c326ab88f616721f Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 17 Aug 2026 12:44:08 +0700 Subject: [PATCH 30/35] Fixed build script for usb companions --- build-iotthinks.sh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/build-iotthinks.sh b/build-iotthinks.sh index 1567e869..a6bdab33 100644 --- a/build-iotthinks.sh +++ b/build-iotthinks.sh @@ -127,16 +127,14 @@ Xiao_S3_WIO_companion_radio_ble ############# Companions USB ############# sh build.sh build-firmware \ Heltec_t096_companion_radio_usb \ -heltec_v4_companion_radio_usb \ heltec_v4_companion_radio_usb_femoff \ heltec_v4_companion_radio_usb_femon ############# Sample builds ############# -# 24 boards +# 23 boards sh build.sh build-firmware \ Heltec_t096_companion_radio_ble_femon \ Heltec_t096_companion_radio_ble_femoff \ -Heltec_t096_companion_radio_usb \ Heltec_t096_repeater \ Heltec_t114_companion_radio_ble \ Heltec_t114_repeater \ @@ -159,10 +157,9 @@ Xiao_C6_repeater_ \ Xiao_nrf52_companion_radio_ble \ Xiao_nrf52_repeater -# 4 boards +# 3 boards sh build.sh build-firmware \ -heltec_v4_companion_radio_ble_femon \ -heltec_v4_companion_radio_usb \ +Heltec_t096_companion_radio_usb \ heltec_v4_companion_radio_usb_femoff \ heltec_v4_companion_radio_usb_femon From a6ae765020f365c7825eb0ddabffcc4fef674af7 Mon Sep 17 00:00:00 2001 From: Simon Blandford Date: Mon, 17 Aug 2026 13:25:22 +0100 Subject: [PATCH 31/35] Applied GPS power saving profile with 10 min on, 1 day off cycle --- examples/simple_room_server/MyMesh.h | 7 +++++++ examples/simple_room_server/main.cpp | 12 ++++++++++++ examples/simple_sensor/SensorMesh.h | 7 +++++++ examples/simple_sensor/main.cpp | 12 ++++++++++++ 4 files changed, 38 insertions(+) diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 0af82bdb..62c31be2 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -168,6 +168,13 @@ protected: #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { + // If powersaving on, apply powersaving to sensors + if (_prefs.powersaving_enabled) { + sensors.powersaving_enabled = true; + } else { + sensors.powersaving_enabled = false; + } + sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); } #endif diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 804fc214..156c77ee 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -83,6 +83,18 @@ void setup() { ethernet_command[0] = 0; #endif +#if ENV_INCLUDE_GPS == 1 + // Apply PowerSaving profile for GPS + if (sensors.getLocationProvider() != NULL) { + // Let CLI "gps on" call setSettingValue to enable the PowerSaving mode + // sensors.powersaving_enabled = true; + // sensors.getLocationProvider()->enablePowerSaving(true); + + // GPS on and off duration in seconds + sensors.getLocationProvider()->setPowerSavingProfile(600, 86400); // Max 10 minutes, 1 day + } +#endif + sensors.begin(); the_mesh.begin(fs); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5c..7b9d7767 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -161,6 +161,13 @@ private: #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { + // If powersaving on, apply powersaving to sensors + if (_prefs.powersaving_enabled) { + sensors.powersaving_enabled = true; + } else { + sensors.powersaving_enabled = false; + } + sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0"); } #endif diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a..7585663a 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -106,6 +106,18 @@ void setup() { command[0] = 0; +#if ENV_INCLUDE_GPS == 1 + // Apply PowerSaving profile for GPS + if (sensors.getLocationProvider() != NULL) { + // Let CLI "gps on" call setSettingValue to enable the PowerSaving mode + // sensors.powersaving_enabled = true; + // sensors.getLocationProvider()->enablePowerSaving(true); + + // GPS on and off duration in seconds + sensors.getLocationProvider()->setPowerSavingProfile(600, 86400); // Max 10 minutes, 1 day + } +#endif + sensors.begin(); the_mesh.begin(fs); From 0965a6aad11208fc43b20831b612419dc75e3bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Doma=C5=84ski?= Date: Tue, 18 Aug 2026 02:16:37 +0200 Subject: [PATCH 32/35] fix(radio): recalibrate noise floor after AGC reset --- src/helpers/radiolib/RadioLibWrappers.cpp | 91 +++++++++++++++++------ src/helpers/radiolib/RadioLibWrappers.h | 2 + 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 0fdc19d4..96ed946b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -14,6 +14,8 @@ #define NF_CALIB_INTERVAL_MS 60000UL #define NF_CALIB_TIMEOUT_MS 5000UL #define NF_CALIB_SETTLE_MS 20UL +#define NF_CALIB_SAMPLE_INTERVAL_MS 1UL +#define NF_CALIB_MAX_SAMPLE_ATTEMPTS (NUM_NOISE_FLOOR_SAMPLES * 4U) static volatile uint8_t state = STATE_IDLE; @@ -86,15 +88,76 @@ void RadioLibWrapper::resetAGC() { if (_rx_ps_armed) stopReceiveDutyCycle(); doResetAGC(); - state = STATE_IDLE; // trigger a startReceive() + state = STATE_IDLE; - // Reset noise floor sampling so it reconverges from scratch. - // Without this, a stuck _noise_floor of -120 makes the sampling threshold - // too low (-106) to accept normal samples (~-105), self-reinforcing the - // stuck value even after the receiver has recovered. + // Recalibrate synchronously so callers never observe the temporary zero + // used to bypass a stale sampling threshold after an AGC reset. + const int16_t previous_noise_floor = _noise_floor; _noise_floor = 0; _num_floor_samples = 0; _floor_sample_sum = 0; + + _nf_calib_active = true; // force startReceiveMode() into continuous RX + bool packet_in_progress = false; + int16_t err = startReceiveMode(); + if (err == RADIOLIB_ERR_NONE) { + state = STATE_RX; + delay(NF_CALIB_SETTLE_MS); + + uint16_t sample_attempts = 0; + while (_num_floor_samples < NUM_NOISE_FLOOR_SAMPLES && + sample_attempts++ < NF_CALIB_MAX_SAMPLE_ATTEMPTS) { + if ((state & STATE_INT_READY) != 0) break; + if (isReceivingPacket()) { + packet_in_progress = true; + break; + } + sampleNoiseFloorOnce(); + if (_num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { + delay(NF_CALIB_SAMPLE_INTERVAL_MS); + } + } + } + + if (!publishNoiseFloor()) { + _noise_floor = previous_noise_floor; + _num_floor_samples = 0; + _floor_sample_sum = 0; + } + + _nf_calib_active = false; + _nf_last_calib = millis(); + + // A frame can start after resetAGC()'s initial idle check. Keep continuous + // RX alive until recvRaw() consumes it; restarting RX here would abort a + // frame that is between preamble detection and RX_DONE. + if (!packet_in_progress && (state & STATE_INT_READY) == 0) { + packet_in_progress = isReceivingPacket(); + } + if (!packet_in_progress) requestRestartRecv(); +} + +void RadioLibWrapper::sampleNoiseFloorOnce() { + int rssi = getCurrentRSSI(); + if (rssi < _noise_floor + SAMPLING_THRESHOLD) { + _num_floor_samples++; + _floor_sample_sum += rssi; + } +} + +bool RadioLibWrapper::publishNoiseFloor() { + if (_num_floor_samples < NUM_NOISE_FLOOR_SAMPLES || _floor_sample_sum == 0) return false; + + _noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES; + if (_noise_floor < -120) { + _noise_floor = -120; // clamp to lower bound of -120dBi + } + _floor_sample_sum = 0; + + #ifdef MESH_DEBUG_NOISE_FLOOR + MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); + #endif + return true; } // Clear the RX/idle state so the next loop calls startRecv() again, without @@ -139,23 +202,9 @@ void RadioLibWrapper::loop() { if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { if (!_rx_ps_armed && !(_nf_calib_active && (long)(millis() - _nf_sample_from) < 0) && !isReceivingPacket()) { - int rssi = getCurrentRSSI(); - if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD - _num_floor_samples++; - _floor_sample_sum += rssi; - } + sampleNoiseFloorOnce(); } - } else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) { - _noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES; - if (_noise_floor < -120) { - _noise_floor = -120; // clamp to lower bound of -120dBi - } - _floor_sample_sum = 0; - - #ifdef MESH_DEBUG_NOISE_FLOOR - MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); - #endif - + } else if (publishNoiseFloor()) { if (_nf_calib_active) endNoiseFloorCalib(millis()); } } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 0e2142f7..5fc37e6b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -44,6 +44,8 @@ protected: void startRecv(); void requestRestartRecv(); void prepareForRadioConfig(); + void sampleNoiseFloorOnce(); + bool publishNoiseFloor(); void noiseFloorCalibCheck(); void endNoiseFloorCalib(unsigned long now); int16_t startReceiveMode(); From be4b2769314c34d4e197f1c84982c2b62ffad78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Doma=C5=84ski?= Date: Tue, 18 Aug 2026 03:08:20 +0200 Subject: [PATCH 33/35] fix(radio): preserve RX during noise floor calibration --- src/helpers/radiolib/RadioLibWrappers.cpp | 28 +++++++++++++++-------- src/helpers/radiolib/RadioLibWrappers.h | 1 + 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 96ed946b..0691d09e 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -83,7 +83,7 @@ void RadioLibWrapper::doResetAGC() { void RadioLibWrapper::resetAGC() { // make sure we're not mid-receive or mid-transmit of a packet - if ((state & STATE_INT_READY) != 0 || isReceivingPacket() || state == STATE_TX_WAIT) return; + if (isPacketPendingOrReceiving() || state == STATE_TX_WAIT) return; if (_rx_ps_armed) stopReceiveDutyCycle(); @@ -107,8 +107,7 @@ void RadioLibWrapper::resetAGC() { uint16_t sample_attempts = 0; while (_num_floor_samples < NUM_NOISE_FLOOR_SAMPLES && sample_attempts++ < NF_CALIB_MAX_SAMPLE_ATTEMPTS) { - if ((state & STATE_INT_READY) != 0) break; - if (isReceivingPacket()) { + if (isPacketPendingOrReceiving()) { packet_in_progress = true; break; } @@ -131,9 +130,7 @@ void RadioLibWrapper::resetAGC() { // A frame can start after resetAGC()'s initial idle check. Keep continuous // RX alive until recvRaw() consumes it; restarting RX here would abort a // frame that is between preamble detection and RX_DONE. - if (!packet_in_progress && (state & STATE_INT_READY) == 0) { - packet_in_progress = isReceivingPacket(); - } + if (!packet_in_progress) packet_in_progress = isPacketPendingOrReceiving(); if (!packet_in_progress) requestRestartRecv(); } @@ -172,28 +169,35 @@ void RadioLibWrapper::requestRestartRecv() { interrupts(); } +bool RadioLibWrapper::isPacketPendingOrReceiving() { + return (state & STATE_INT_READY) != 0 || isReceivingPacket(); +} + void RadioLibWrapper::noiseFloorCalibCheck() { unsigned long now = millis(); if (_nf_calib_active) { if (!_rx_ps_enabled || (long)(now - _nf_calib_deadline) >= 0) { endNoiseFloorCalib(now); + } else if (_rx_ps_armed && !isPacketPendingOrReceiving()) { + // A packet may have delayed the RXPS-to-continuous-RX transition. + requestRestartRecv(); } } else if (_rx_ps_enabled && _rx_ps_armed && state == STATE_RX && (_nf_last_calib == 0 || now - _nf_last_calib >= NF_CALIB_INTERVAL_MS) && - !isReceivingPacket()) { + !isPacketPendingOrReceiving()) { _nf_calib_active = true; _nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS; _nf_sample_from = now + NF_CALIB_SETTLE_MS; _num_floor_samples = 0; _floor_sample_sum = 0; - requestRestartRecv(); + if (!isPacketPendingOrReceiving()) requestRestartRecv(); } } void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) { _nf_calib_active = false; _nf_last_calib = now; - requestRestartRecv(); + if (!isPacketPendingOrReceiving()) requestRestartRecv(); } void RadioLibWrapper::loop() { @@ -222,6 +226,12 @@ void RadioLibWrapper::startRecv() { } int16_t RadioLibWrapper::startReceiveMode() { + // Periodic calibration switches RXPS to continuous RX. Re-check at the + // hardware transition so a preamble that arrived after the scheduler's + // idle check is not aborted by stopReceiveDutyCycle(). + if (_nf_calib_active && _rx_ps_armed && isPacketPendingOrReceiving()) { + return RADIOLIB_ERR_NONE; + } if (_rx_ps_armed) stopReceiveDutyCycle(); if (!_rx_ps_enabled || _nf_calib_active) { _rx_ps_armed = false; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 5fc37e6b..107d59b1 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -43,6 +43,7 @@ protected: void idle(); void startRecv(); void requestRestartRecv(); + bool isPacketPendingOrReceiving(); void prepareForRadioConfig(); void sampleNoiseFloorOnce(); bool publishNoiseFloor(); From 307bc938b5e408350913b72ad77cd62c2f78e4ce Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Tue, 18 Aug 2026 09:29:12 +0700 Subject: [PATCH 34/35] Fixed RXPS for LR1110 --- src/helpers/radiolib/CustomLR1110.h | 23 +++-------- src/helpers/radiolib/CustomLR1110Wrapper.h | 4 +- src/helpers/radiolib/RXPowerSaving.h | 23 ----------- .../test_rx_power_saving.cpp | 38 ------------------- 4 files changed, 7 insertions(+), 81 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index d4fc31d3..a49eee83 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -71,6 +71,8 @@ class CustomLR1110 : public LR1110 { int16_t state = standby(RADIOLIB_LR11X0_STANDBY_RC); RADIOLIB_ASSERT(state); // Semtech requires the RC standby/RTC setup before SetRxDutyCycle. + state = configLfClock(RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED); + RADIOLIB_ASSERT(state); RadioModeConfig_t cfg = { .receive = { .timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF, @@ -79,23 +81,10 @@ class CustomLR1110 : public LR1110 { .len = 0, } }; - return runRxPowerSavingArmTransaction( - [this]() { - return configLfClock( - RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED); - }, - [this, &cfg]() { - return this->stageMode(RADIOLIB_RADIO_MODE_RX, &cfg); - }, - [this, rxPeriodRaw, sleepPeriodRaw]() { - return this->setRxDutyCycle( - rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX); - }, - [this]() { - // Preserve the arm error even if this best-effort rollback fails. - // A failed ConfigLfClock response may still mean the command ran. - restoreLfClock(); - }); + + state = stageMode(RADIOLIB_RADIO_MODE_RX, &cfg); + RADIOLIB_ASSERT(state); + return setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX); } int16_t setRxBoostedGainMode(bool en) { diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index d93ccca9..36faeae3 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -58,9 +58,7 @@ protected: } int16_t stopDutyCycleHardware() override { - int16_t standby_err = _radio->standby(); - int16_t lf_err = ((CustomLR1110 *)_radio)->restoreLfClock(); - return standby_err != RADIOLIB_ERR_NONE ? standby_err : lf_err; + return _radio->standby(); } public: diff --git a/src/helpers/radiolib/RXPowerSaving.h b/src/helpers/radiolib/RXPowerSaving.h index 473e5f29..7768c189 100644 --- a/src/helpers/radiolib/RXPowerSaving.h +++ b/src/helpers/radiolib/RXPowerSaving.h @@ -29,29 +29,6 @@ public: uint8_t consecutiveFailures() const { return _consecutive_failures; } }; -// Runs the part of a hardware arm sequence that starts by changing persistent -// radio configuration. Every failing step rolls that configuration back, while -// the original arm error remains the result returned to the caller. -template -int16_t runRxPowerSavingArmTransaction(Configure configure, Stage stage, - Arm arm, Rollback rollback) { - int16_t state = configure(); - if (state != 0) { - rollback(); - return state; - } - - state = stage(); - if (state != 0) { - rollback(); - return state; - } - - state = arm(); - if (state != 0) rollback(); - return state; -} - struct RxPowerSavingConfig { uint8_t enabled = 0; uint32_t rx_us = RX_POWERSAVING_DEFAULT_RX_US; diff --git a/test/test_rx_power_saving/test_rx_power_saving.cpp b/test/test_rx_power_saving/test_rx_power_saving.cpp index 3c769e0a..f699e3bd 100644 --- a/test/test_rx_power_saving/test_rx_power_saving.cpp +++ b/test/test_rx_power_saving/test_rx_power_saving.cpp @@ -89,44 +89,6 @@ TEST(RxPowerSaving, ClearingRetryStateGrantsThreeFreshAttempts) { EXPECT_EQ(retry.consecutiveFailures(), 0); } -TEST(RxPowerSaving, ArmTransactionRollsBackEveryFailureAndPreservesItsError) { - for (int failing_step = 0; failing_step < 3; failing_step++) { - int calls[3] = {0, 0, 0}; - int rollback_calls = 0; - auto step = [&](int index) -> int16_t { - calls[index]++; - return index == failing_step ? (int16_t)(-700 - index) : (int16_t)0; - }; - - int16_t result = runRxPowerSavingArmTransaction( - [&]() { return step(0); }, - [&]() { return step(1); }, - [&]() { return step(2); }, - [&]() { rollback_calls++; }); - - EXPECT_EQ(result, -700 - failing_step); - EXPECT_EQ(rollback_calls, 1); - for (int index = 0; index < 3; index++) { - EXPECT_EQ(calls[index], index <= failing_step ? 1 : 0); - } - } -} - -TEST(RxPowerSaving, ArmTransactionDoesNotRollBackSuccessfulArm) { - int step_calls = 0; - int rollback_calls = 0; - - int16_t result = runRxPowerSavingArmTransaction( - [&]() -> int16_t { step_calls++; return 0; }, - [&]() -> int16_t { step_calls++; return 0; }, - [&]() -> int16_t { step_calls++; return 0; }, - [&]() { rollback_calls++; }); - - EXPECT_EQ(result, 0); - EXPECT_EQ(step_calls, 3); - EXPECT_EQ(rollback_calls, 0); -} - TEST(RxPowerSaving, RejectsInvalidProfileInputs) { uint32_t rx_us = 0; uint32_t sleep_us = 0; From 167a32fbdc0d237a1508dd687102cac95529787c Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Tue, 18 Aug 2026 10:40:16 +0700 Subject: [PATCH 35/35] Fixed to show d h, h m and m for BLE companions --- examples/companion_radio/ui-new/UITask.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index bcc94d63..f842baa1 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -143,13 +143,17 @@ class HomeScreen : public UIScreen { uptime_last_millis = now; uint64_t uptimeMinutes = uptime_millis / 60000ULL; + + unsigned long days = uptimeMinutes / 1440; + unsigned long hours = (uptimeMinutes % 1440) / 60; + unsigned long minutes = uptimeMinutes % 60; char uptime[16]; - if (uptimeMinutes < 60) { - snprintf(uptime, sizeof(uptime), "%lum", (unsigned long)uptimeMinutes); - } else if (uptimeMinutes < 1440) { - snprintf(uptime, sizeof(uptime), "%luh", (unsigned long)(uptimeMinutes / 60)); + if (days > 0) { + snprintf(uptime, sizeof(uptime), "%lud %luh", days, hours); + } else if (hours > 0) { + snprintf(uptime, sizeof(uptime), "%luh %lum", hours, minutes); } else { - snprintf(uptime, sizeof(uptime), "%lud", (unsigned long)(uptimeMinutes / 1440)); + snprintf(uptime, sizeof(uptime), "%lum", minutes); } display.setTextSize(1);