From a7e92f62acd9a0bb56879bb9b65b118bf886ae9a Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 8 Jan 2026 17:02:58 +0100 Subject: [PATCH 1/3] Save some more power when BLE/WiFi is disabled on the companion radio --- examples/companion_radio/MyMesh.cpp | 5 ++++ examples/companion_radio/MyMesh.h | 1 + examples/companion_radio/main.cpp | 35 +++++++++++++++++++++++ src/helpers/ESP32Board.h | 12 ++++++-- src/helpers/esp32/SerialWifiInterface.cpp | 22 +++++++++++++- src/helpers/esp32/SerialWifiInterface.h | 4 +++ 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c96f7e017..48a4ce3f4 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2046,3 +2046,8 @@ bool MyMesh::advert() { return false; } } + +// Check if there is pending work (packets to send) +bool MyMesh::hasPendingWork() const { + return _mgr->getOutboundCount(0xFFFFFFFF) > 0; +} diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 87e6cf338..ad766af1e 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -161,6 +161,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { public: void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } + bool hasPendingWork() const; private: void writeOKFrame(); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index eff9efca4..0674ddea8 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -99,6 +99,11 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store #endif ); +// Power saving timing variables +unsigned long lastActive = 0; // Last time there was activity +unsigned long nextSleepInSecs = 120; // Wait 2 minutes before first sleep +const unsigned long WORK_TIME_SECS = 5; // Stay awake 5 seconds after wake/activity + /* END GLOBAL OBJECTS */ void halt() { @@ -216,6 +221,9 @@ void setup() { #ifdef DISPLAY_CLASS ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved #endif + + // Initialize power saving timer + lastActive = millis(); } void loop() { @@ -225,4 +233,31 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + + // Power saving when BLE/WiFi is disabled + // Don't sleep if GPS is enabled - it needs continuous operation to maintain fix + // Note: Disabling BLE/WiFi via UI actually turns off the radio to save power + if (!serial_interface.isEnabled() && !the_mesh.getNodePrefs()->gps_enabled) { + // Check for pending work and update activity timer + if (the_mesh.hasPendingWork()) { + lastActive = millis(); + if (nextSleepInSecs < 10) { + nextSleepInSecs += 5; // Extend work time by 5s if still busy + } + } + + // Only sleep if enough time has passed since last activity + if (millis() >= lastActive + (nextSleepInSecs * 1000)) { +#ifdef PIN_USER_BTN + // Sleep for 30 minutes, wake on LoRa packet, timer, or button press + board.enterLightSleep(1800, PIN_USER_BTN); +#else + // Sleep for 30 minutes, wake on LoRa packet or timer + board.enterLightSleep(1800); +#endif + // Just woke up - reset timers + lastActive = millis(); + nextSleepInSecs = WORK_TIME_SECS; // Stay awake for 5s after wake + } + } } diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index bade3e898..c7c63b691 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -9,6 +9,7 @@ #include #include #include "driver/rtc_io.h" +#include "driver/gpio.h" class ESP32Board : public mesh::MainBoard { protected: @@ -56,11 +57,18 @@ class ESP32Board : public mesh::MainBoard { return raw / 4; } - void enterLightSleep(uint32_t secs) { + void enterLightSleep(uint32_t secs, int pin_wake_btn = -1) { #if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(P_LORA_DIO_1) // Supported ESP32 variants if (rtc_gpio_is_valid_gpio((gpio_num_t)P_LORA_DIO_1)) { // Only enter sleep mode if P_LORA_DIO_1 is RTC pin esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); - esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // To wake up when receiving a LoRa packet + + esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // Wake on LoRa packet + + // Wake on button press (active-LOW: pin is HIGH when idle, LOW when pressed) + if (pin_wake_btn >= 0) { + gpio_wakeup_enable((gpio_num_t)pin_wake_btn, GPIO_INTR_LOW_LEVEL); + esp_sleep_enable_gpio_wakeup(); + } if (secs > 0) { esp_sleep_enable_timer_wakeup(secs * 1000000); // To wake up every hour to do periodically jobs diff --git a/src/helpers/esp32/SerialWifiInterface.cpp b/src/helpers/esp32/SerialWifiInterface.cpp index 462e3ecc3..f4ebf5d69 100644 --- a/src/helpers/esp32/SerialWifiInterface.cpp +++ b/src/helpers/esp32/SerialWifiInterface.cpp @@ -4,18 +4,38 @@ void SerialWifiInterface::begin(int port) { // wifi setup is handled outside of this class, only starts the server server.begin(port); + + // Store WiFi credentials for re-enable +#ifdef WIFI_SSID + _ssid = WIFI_SSID; + _password = WIFI_PWD; + _isEnabled = true; // WiFi starts enabled +#else + _ssid = nullptr; + _password = nullptr; +#endif } // ---------- public methods -void SerialWifiInterface::enable() { +void SerialWifiInterface::enable() { if (_isEnabled) return; _isEnabled = true; clearBuffers(); + + // Re-enable WiFi with stored credentials + if (_ssid != nullptr && _password != nullptr) { + WiFi.mode(WIFI_STA); + WiFi.begin(_ssid, _password); + } } void SerialWifiInterface::disable() { _isEnabled = false; + + // Actually turn off WiFi to save power + WiFi.disconnect(true); // Disconnect and clear config + WiFi.mode(WIFI_OFF); // Turn off WiFi radio } size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) { diff --git a/src/helpers/esp32/SerialWifiInterface.h b/src/helpers/esp32/SerialWifiInterface.h index 19291497f..f900d18bc 100644 --- a/src/helpers/esp32/SerialWifiInterface.h +++ b/src/helpers/esp32/SerialWifiInterface.h @@ -8,6 +8,8 @@ class SerialWifiInterface : public BaseSerialInterface { bool _isEnabled; unsigned long _last_write; unsigned long adv_restart_time; + const char* _ssid; + const char* _password; WiFiServer server; WiFiClient client; @@ -39,6 +41,8 @@ class SerialWifiInterface : public BaseSerialInterface { deviceConnected = false; _isEnabled = false; _last_write = 0; + _ssid = nullptr; + _password = nullptr; send_queue_len = recv_queue_len = 0; received_frame_header.type = 0; received_frame_header.length = 0; From 0a09f738e08fe1ab65ea45b3d12574c13dae5fd9 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 11 Feb 2026 03:55:02 +0100 Subject: [PATCH 2/3] Fix millis() overflow in companion powersave sleep timing Use millisHasNowPassed() (2's complement safe) instead of direct comparison, consistent with the repeater's sleep timing logic. Co-Authored-By: Wessel --- examples/companion_radio/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 0674ddea8..0dc5f1224 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -247,7 +247,7 @@ void loop() { } // Only sleep if enough time has passed since last activity - if (millis() >= lastActive + (nextSleepInSecs * 1000)) { + if (the_mesh.millisHasNowPassed(lastActive + (nextSleepInSecs * 1000))) { #ifdef PIN_USER_BTN // Sleep for 30 minutes, wake on LoRa packet, timer, or button press board.enterLightSleep(1800, PIN_USER_BTN); From c089637c92434077011cf5c8bebe4d01b4c6a184 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 16 Feb 2026 09:09:00 +0100 Subject: [PATCH 3/3] Fix sleep nRF52 --- src/helpers/NRF52Board.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 96f67dc95..f64e878cd 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -53,6 +53,7 @@ class NRF52Board : public mesh::MainBoard { virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual void sleep(uint32_t secs) override; + void enterLightSleep(uint32_t secs, int pin_wake_btn = -1) { sleep(secs); } #ifdef NRF52_POWER_MANAGEMENT bool isExternalPowered() override;