From 353048aab3d97f73c36a9380252d837c01732269 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:09:56 +1000 Subject: [PATCH] So the power investigation wraps up like this: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPS — fixed (the "off" toggle and boot path now actually cut the XL9555 rail). Kept in main.cpp + UITask.cpp. BLE controller — the ~13 mA between the BLE build (BLE off) and standalone is the controller staying initialised; the boot-gate in SerialBLEInterface.cpp/.h defers BLEDevice::init() until first enable, reclaiming it. That's the meaningful idle win. CPU, gyro rail, ES8311, frontlight — all measured and ruled out. The residual ~12 mA Max-vs-Pro is distributed always-on hardware with no software switch — hardware overhead, not a bug. Defer ESP32 BLE controller bring-up to first user enable MyMesh::startInterface() called serial.enable() unconditionally at boot. On the ESP32 BLE build this ran the deferred-init SerialBLEInterface's _realBegin()/BLEDevice::init() and powered the BT controller before main.cpp's boot-time disable(), which only stops advertising and cannot power the controller back down -- so the controller stayed up while "off", drawing ~13 mA at idle. Guard the enable() so ESP32 BLE builds skip it at boot; the controller now comes up lazily on the first enable() when the user turns Bluetooth on from the Bluetooth page. WiFi builds are unaffected and still enable at boot. --- examples/companion_radio/MyMesh.cpp | 9 ++ examples/companion_radio/main.cpp | 3 + examples/companion_radio/ui-new/UITask.cpp | 9 ++ src/helpers/esp32/SerialBLEInterface.cpp | 23 +++- src/helpers/esp32/SerialBLEInterface.h | 5 + variants/lilygo_tdeck_max/CPUPowerManager.h | 113 ++++++++++++++++++++ 6 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 variants/lilygo_tdeck_max/CPUPowerManager.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 9e543b27..c9f8f04e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1584,7 +1584,16 @@ uint32_t MyMesh::getBLEPin() { void MyMesh::startInterface(BaseSerialInterface &serial) { _serial = &serial; +#if defined(ESP32) && defined(BLE_PIN_CODE) && !defined(WIFI_SSID) && !defined(MECK_WIFI_COMPANION) + // ESP32 BLE companion: do NOT power the BT controller at boot. With the + // deferred-init SerialBLEInterface the controller stays down until the first + // enable(), which now happens only when the user turns Bluetooth on from the + // Bluetooth page. Reclaims the controller idle current in the standalone-first + // default. nRF52 BLE (separate class, no deferred init) and USB-serial / WiFi + // are unaffected and still enable at boot below. +#else serial.enable(); +#endif } void MyMesh::handleCmdFrame(size_t len) { diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b6b737d5..881e5a53 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2596,6 +2596,9 @@ void setup() { #ifdef PIN_GPS_EN digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); #endif + #if defined(LilyGo_TDeck_Pro_Max) + board.gpsPowerOff(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN + #endif sensors.setSettingValue("gps", "0"); } Serial.printf("GPS: power %s\n", gps_wanted ? "ON" : "OFF"); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 4a88b4d2..1ecbe229 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1863,6 +1863,9 @@ void UITask::shutdown(bool restart){ #ifdef PIN_GPS_EN digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); #endif + #if defined(LilyGo_TDeck_Pro_Max) + board.gpsPowerOff(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN + #endif } } #endif @@ -2804,6 +2807,9 @@ void UITask::toggleGPS() { #ifdef PIN_GPS_EN digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); #endif + #if defined(LilyGo_TDeck_Pro_Max) + board.gpsPowerOff(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN + #endif notify(UIEventType::ack); } else { // Enable GPS — power on hardware @@ -2812,6 +2818,9 @@ void UITask::toggleGPS() { #ifdef PIN_GPS_EN digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE); #endif + #if defined(LilyGo_TDeck_Pro_Max) + board.gpsPowerOn(); // MAX: GPS power is XL9555-routed, not PIN_GPS_EN + #endif notify(UIEventType::ack); } the_mesh.savePrefs(); diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 747058ff..b21d9ec6 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -21,11 +21,21 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param) addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); } - char dev_name[32+16]; - sprintf(dev_name, "%s%s", prefix, name); + // Store the device name and defer the actual controller bring-up + // (BLEDevice::init + GATT setup) until the first enable(). This keeps the BT + // controller powered down while BLE is disabled -- the standalone-first + // default -- which reclaims its idle current. The controller comes up on the + // first enable and, as with the web-reader teardown path, stays up until the + // next reboot thereafter. + snprintf(_dev_name, sizeof(_dev_name), "%s%s", prefix, name); + _begun = false; +} +// Deferred BLE controller + GATT bring-up. Called from the first enable() so +// the controller is not powered while BLE is disabled. +void SerialBLEInterface::_realBegin() { // Create the BLE Device - BLEDevice::init(dev_name); + BLEDevice::init(_dev_name); BLEDevice::setSecurityCallbacks(this); BLEDevice::setMTU(MAX_FRAME_SIZE); @@ -35,7 +45,7 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_P9); BLESecurity sec; - sec.setStaticPIN(pin_code); + sec.setStaticPIN(_pin_code); sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND); //BLEDevice::setPower(ESP_PWR_LVL_N8); @@ -161,6 +171,11 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat void SerialBLEInterface::enable() { if (_isEnabled) return; + if (!_begun) { // deferred controller bring-up on first enable + _realBegin(); + _begun = true; + } + _isEnabled = true; clearBuffers(); diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index a0867bea..48b08b07 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -13,9 +13,11 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool deviceConnected; bool oldDeviceConnected; bool _isEnabled; + bool _begun; // has _realBegin() run? (deferred BLE bring-up) uint16_t last_conn_id; uint8_t _remote_bda[6]; // peer BDA, stored in onConnect for conn param updates uint32_t _pin_code; + char _dev_name[48]; // stored in begin(), consumed by deferred _realBegin() unsigned long _last_write; unsigned long adv_restart_time; @@ -32,6 +34,8 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + void _realBegin(); // deferred BLE controller + GATT bring-up + protected: // BLESecurityCallbacks methods uint32_t onPassKeyRequest() override; @@ -57,6 +61,7 @@ public: oldDeviceConnected = false; adv_restart_time = 0; _isEnabled = false; + _begun = false; _last_write = 0; last_conn_id = 0; memset(_remote_bda, 0, 6); diff --git a/variants/lilygo_tdeck_max/CPUPowerManager.h b/variants/lilygo_tdeck_max/CPUPowerManager.h new file mode 100644 index 00000000..444a90bd --- /dev/null +++ b/variants/lilygo_tdeck_max/CPUPowerManager.h @@ -0,0 +1,113 @@ +#pragma once + +#include + +// CPU Frequency Scaling for ESP32-S3 +// +// Typical current draw (CPU only, rough): +// 240 MHz ~70-80 mA +// 160 MHz ~50-60 mA +// 80 MHz ~30-40 mA +// 40 MHz ~15-20 mA (low-power / lock screen mode) +// +// SPI peripherals and UART use their own clock dividers from the APB clock, +// so LoRa, e-ink, and GPS serial all work fine at 80MHz and 40MHz. + +#ifdef ESP32 + +#ifndef CPU_FREQ_IDLE +#define CPU_FREQ_IDLE 80 // MHz — normal mesh listening +#endif + +#ifndef CPU_FREQ_BOOST +#define CPU_FREQ_BOOST 240 // MHz — heavy processing +#endif + +#ifndef CPU_FREQ_LOW_POWER +#define CPU_FREQ_LOW_POWER 80 // MHz — lock screen / idle standby (40 MHz breaks I2C) +#endif + +#ifndef CPU_BOOST_TIMEOUT_MS +#define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds +#endif + +class CPUPowerManager { +public: + CPUPowerManager() : _boosted(false), _lowPower(false), _boost_started(0) {} + + void begin() { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + _boosted = false; + _lowPower = false; + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); + } + + void loop() { + if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) { + // Return to low-power if locked, otherwise normal idle + if (_lowPower) { + setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); + MESH_DEBUG_PRINTLN("CPU power: boost expired, returning to low-power %d MHz", CPU_FREQ_LOW_POWER); + } else { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); + } + _boosted = false; + } + } + + void setBoost() { + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_BOOST); + _boosted = true; + MESH_DEBUG_PRINTLN("CPU power: boosted to %d MHz", CPU_FREQ_BOOST); + } + _boost_started = millis(); + } + + void setIdle() { + if (_boosted) { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + _boosted = false; + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE); + } + if (_lowPower) { + _lowPower = false; + } + } + + // Low-power mode — drops CPU to 40 MHz for lock screen standby. + // If currently boosted, the boost timeout will return to 40 MHz + // instead of 80 MHz. + void setLowPower() { + _lowPower = true; + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_LOW_POWER); + MESH_DEBUG_PRINTLN("CPU power: low-power at %d MHz", CPU_FREQ_LOW_POWER); + } + // If boosted, the loop() timeout will drop to low-power instead of idle + } + + // Exit low-power mode — returns to normal idle (80 MHz). + // If currently boosted, the boost timeout will return to idle + // instead of low-power. + void clearLowPower() { + _lowPower = false; + if (!_boosted) { + setCpuFrequencyMhz(CPU_FREQ_IDLE); + MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz (low-power cleared)", CPU_FREQ_IDLE); + } + // If boosted, the loop() timeout will drop to idle as normal + } + + bool isBoosted() const { return _boosted; } + bool isLowPower() const { return _lowPower; } + uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); } + +private: + bool _boosted; + bool _lowPower; + unsigned long _boost_started; +}; + +#endif // ESP32 \ No newline at end of file