diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 9bb747e79..b082d4c3e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1985,3 +1985,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 95265a19a..c66c4518d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -158,6 +158,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..57cad75e7 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -99,6 +99,18 @@ 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 + +// Short-sleep cycle when phone is disconnected but BLE is enabled +const unsigned long DISCONNECT_SLEEP_TIMEOUT_MS = 60000; // 60s before short-sleep cycle +const unsigned long SHORT_SLEEP_SECS = 12; // sleep duration per cycle +const unsigned long RECONNECT_WINDOW_MS = 3000; // awake time for BLE advertising +unsigned long disconnectTime = 0; // when phone disconnected (0 = connected/N/A) +unsigned long lastSleepWake = 0; // when we last woke from short sleep (0 = not in cycle) + /* END GLOBAL OBJECTS */ void halt() { @@ -216,6 +228,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 +240,59 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + +#ifndef WIFI_SSID + // Track phone connection state for disconnect sleep + if (serial_interface.hasPendingConnection()) { + disconnectTime = 0; + lastSleepWake = 0; + } else if (serial_interface.isEnabled() && disconnectTime == 0) { + disconnectTime = millis(); + if (disconnectTime == 0) disconnectTime = 1; // avoid 0 sentinel collision + } + // Short-sleep cycle when BLE is enabled but phone is disconnected + if (serial_interface.isEnabled() && disconnectTime != 0 + && !the_mesh.getNodePrefs()->gps_enabled + && the_mesh.millisHasNowPassed(disconnectTime + DISCONNECT_SLEEP_TIMEOUT_MS) + && !the_mesh.hasPendingWork() + && (lastSleepWake == 0 || the_mesh.millisHasNowPassed(lastSleepWake + RECONNECT_WINDOW_MS))) { +#ifdef PIN_USER_BTN + board.enterLightSleep(SHORT_SLEEP_SECS, PIN_USER_BTN); +#else + board.enterLightSleep(SHORT_SLEEP_SECS); +#endif + // Restart BLE advertising after light sleep powers down the radio + serial_interface.disable(); + serial_interface.enable(); + lastSleepWake = millis(); + if (lastSleepWake == 0) lastSleepWake = 1; + } +#endif + + // 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 (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); +#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/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e60927654..7d9ff7263 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -14,6 +14,7 @@ class BaseSerialInterface { virtual bool isEnabled() const = 0; virtual bool isConnected() const = 0; + virtual bool hasPendingConnection() const { return isConnected(); } virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; 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/NRF52Board.h b/src/helpers/NRF52Board.h index 0332af078..db2bb5985 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -52,6 +52,7 @@ class NRF52Board : public mesh::MainBoard { virtual void reboot() override { NVIC_SystemReset(); } 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; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index eccfeca68..a599f8868 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -250,3 +250,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { bool SerialBLEInterface::isConnected() const { return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; } + +bool SerialBLEInterface::hasPendingConnection() const { + return pServer != NULL && pServer->getConnectedCount() > 0; +} diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd1..519adbb2f 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -75,6 +75,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool hasPendingConnection() const override; bool isWriteBusy() const override; size_t writeFrame(const uint8_t src[], size_t len) override; 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;