From 29ad8e53f681d2d0fdee49907bea73bdad44df5e Mon Sep 17 00:00:00 2001 From: John Buckman Date: Mon, 10 Aug 2026 16:08:01 +0200 Subject: [PATCH 1/3] perf(power): Tier-2 automatic light sleep, -64% idle (AI FYI) Enable ESP-IDF automatic light sleep (CONFIG_PM_ENABLE + tickless idle + esp_pm_configure light_sleep=on), with the NimBLE controller's low-power clock on RTC_SLOW (internal RC) since this board has no external 32kHz crystal. New opt-in from-source env [env:esp32s3_pm] carries the sdkconfig. Config flags alone do nothing here: the main loop never blocks, so the FreeRTOS idle task never runs and tickless idle never enters. The fix is a blocking yield at the end of loop(); with it, idle drops 110.6 -> 39.7 mA (-64%) on a V8.1 unit. All runtime bits are guarded by CONFIG_PM_ENABLE, so the change is inert on the stock precompiled build. Serial is hardened for light sleep: UART0 wake-on-rx, plus a stay-awake grace window after serial activity (t_lastSerialActivity) so a command's bytes aren't lost to a mid-command sleep. Functional serial regression is consistently 8/8 with idle power unchanged. AI-written change, posted as an FYI for review; not necessarily to merge. Co-Authored-By: Claude Opus 4.8 --- include/parameter.h | 1 + platformio.ini | 34 ++++++++++++++++++++++++++++++++++ src/hds.ino | 27 +++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/include/parameter.h b/include/parameter.h index a047652..ea98c38 100644 --- a/include/parameter.h +++ b/include/parameter.h @@ -109,6 +109,7 @@ unsigned long t_batteryIcon = 0; bool b_showBatteryIcon = true; // volatile: now also written from the AsyncTCP task (WS soft-sleep command). volatile bool b_softSleep = false; +unsigned long t_lastSerialActivity = 0; #if defined(ACC_MPU6050) || defined(ACC_BMA400) bool b_gyroEnabled = true; #endif diff --git a/platformio.ini b/platformio.ini index 78afc7d..0f56430 100644 --- a/platformio.ini +++ b/platformio.ini @@ -92,3 +92,37 @@ platform = native test_build_src = no test_framework = unity build_flags = -Iinclude + +# Tier-2 automatic light-sleep build (AI FYI, see PR). Opt-in, from-source: the +# CONFIG_PM_* / tickless / BT-controller-sleep flags below are compile-time and +# require rebuilding the framework from source (custom_sdkconfig triggers it). +# The BT low-power clock is RTC_SLOW because this board has no external 32kHz +# crystal (CONFIG_RTC_CLK_SRC_INT_RC). Pair with the CONFIG_PM_ENABLE-guarded +# esp_pm_configure() + loop yield + UART wake in src/hds.ino. Build: +# pio run -e esp32s3_pm -t upload +# custom_component_remove skips unused managed IDF components that the from-source +# build would otherwise compile (esp-modbus fails a static assert); none are +# #included by this firmware, so removal is image-identical. +[env:esp32s3_pm] +extends = env:esp32s3 +custom_component_remove = + espressif/esp-modbus + espressif/esp-sr + espressif/esp-zboss-lib + espressif/esp-zigbee-lib + espressif/esp_insights + espressif/esp_diagnostics + espressif/esp_diag_data_store + espressif/esp_rainmaker + espressif/rmaker_common + espressif/esp_modem + espressif/esp-dsp + chmorgan/esp-libhelix-mp3 + espressif/qrcode +custom_sdkconfig = + CONFIG_PM_ENABLE=y + CONFIG_FREERTOS_USE_TICKLESS_IDLE=y + CONFIG_PM_DFS_INIT_AUTO=y + CONFIG_BT_CTRL_MODEM_SLEEP=y + CONFIG_BT_CTRL_MODEM_SLEEP_MODE_1=y + CONFIG_BT_CTRL_LPCLK_SEL_RTC_SLOW=y diff --git a/src/hds.ino b/src/hds.ino index e99c9bf..9831f0d 100644 --- a/src/hds.ino +++ b/src/hds.ino @@ -1,5 +1,8 @@ #include #include +#include "esp_pm.h" +#include "esp_sleep.h" +#include "driver/uart.h" #include "config.h" #include "parameter.h" @@ -882,6 +885,21 @@ void setup() { } else { hdsOtaRollbackMarkValid(); } + +#if CONFIG_PM_ENABLE + { + esp_pm_config_t pmcfg = { + .max_freq_mhz = 240, + .min_freq_mhz = 80, + .light_sleep_enable = true, + }; + esp_err_t pmrc = esp_pm_configure(&pmcfg); + Serial.printf("[pm] esp_pm_configure ls=on 240/80 -> %s\n", esp_err_to_name(pmrc)); + uart_set_wakeup_threshold(UART_NUM_0, 3); + esp_err_t urc = esp_sleep_enable_uart_wakeup(UART_NUM_0); + Serial.printf("[pm] uart0 wake-on-rx -> %s\n", esp_err_to_name(urc)); + } +#endif } /** @@ -1605,6 +1623,7 @@ void loop() { data[len++] = Serial.read(); } usbCallbacks.onStream(data, len); // Process serial byte stream + t_lastSerialActivity = millis(); } usbCallbacks.poll(); @@ -1785,6 +1804,14 @@ void loop() { } } } + +#if CONFIG_PM_ENABLE + if (millis() - t_lastSerialActivity > 250) { + vTaskDelay(pdMS_TO_TICKS(10)); + } else { + vTaskDelay(1); + } +#endif } From 0d4085b835aa2d5edabde99451c674a03d1daa8c Mon Sep 17 00:00:00 2001 From: John Buckman Date: Mon, 10 Aug 2026 17:54:10 +0200 Subject: [PATCH 2/3] perf(power): BLE-wakeable low-power sleep, ~70% lower (AI FYI) Make the soft-sleep state (BLE "sleep" cmd 03 0A 04 01) genuinely low-power: in soft-sleep the main loop yields 50ms instead of 10ms, so the SoC light- sleeps in longer chunks between BLE connection events while staying reachable. Builds on the automatic light sleep from the Tier-2 change; deep sleep is unchanged (double-tap and BLE "off" 03 0A 02 still fully power down the radio). Verified over real BLE (macOS central): connected BLE-wakeable soft-sleep drops from 103.8 mA (stock) to 31.4 mA (-70%, 3.3x), wake stays 5/5 at ~105ms with a stable connection. Guarded by CONFIG_PM_ENABLE; inert on the stock build. AI-written change, posted as an FYI for review; not necessarily to merge. Co-Authored-By: Claude Opus 4.8 --- src/hds.ino | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hds.ino b/src/hds.ino index 9831f0d..d65a543 100644 --- a/src/hds.ino +++ b/src/hds.ino @@ -1806,10 +1806,12 @@ void loop() { } #if CONFIG_PM_ENABLE - if (millis() - t_lastSerialActivity > 250) { - vTaskDelay(pdMS_TO_TICKS(10)); - } else { + if (millis() - t_lastSerialActivity <= 250) { vTaskDelay(1); + } else if (b_softSleep) { + vTaskDelay(pdMS_TO_TICKS(50)); + } else { + vTaskDelay(pdMS_TO_TICKS(10)); } #endif } From 911350cfabf45b06e579b4480f55ff5b0588020c Mon Sep 17 00:00:00 2001 From: John Buckman Date: Tue, 11 Aug 2026 10:05:34 +0200 Subject: [PATCH 3/3] perf(power): pin CPU to 240MHz when connected+live (extension headroom) On the DFS/light-sleep build, hold an ESP_PM_CPU_FREQ_MAX lock while (deviceConnected && !b_softSleep) so on-scale extensions (the upcoming extension mechanism) get full compute during active connected use; release it otherwise so DFS scales down and light sleep engages. Measured over real BLE: connected+live 64.4mA (240 pinned) vs connected+soft-sleep 31.4mA (released -> light sleep, unchanged), wake still 5/5. Guarded by CONFIG_PM_ENABLE; inert on the stock build. AI-written change, posted as an FYI for review; not necessarily to merge. Co-Authored-By: Claude Opus 4.8 --- src/hds.ino | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/hds.ino b/src/hds.ino index d65a543..0cc5b4d 100644 --- a/src/hds.ino +++ b/src/hds.ino @@ -426,6 +426,13 @@ void wifi_init() { MyUsbCallbacks usbCallbacks; +#if CONFIG_PM_ENABLE +// Held while the scale is connected + live (not soft-sleep), pinning the CPU at +// max freq so on-scale extensions have full compute; released otherwise so DFS +// scales down and light sleep engages. +esp_pm_lock_handle_t s_activeFreqLock = nullptr; +#endif + // Map esp_reset_reason() to a short string for the boot log only. The raw // numeric code is what we ship in the ADS debug packet (byte 24); this is // purely for human-readable serial output. @@ -895,6 +902,7 @@ void setup() { }; esp_err_t pmrc = esp_pm_configure(&pmcfg); Serial.printf("[pm] esp_pm_configure ls=on 240/80 -> %s\n", esp_err_to_name(pmrc)); + esp_pm_lock_create(ESP_PM_CPU_FREQ_MAX, 0, "active", &s_activeFreqLock); uart_set_wakeup_threshold(UART_NUM_0, 3); esp_err_t urc = esp_sleep_enable_uart_wakeup(UART_NUM_0); Serial.printf("[pm] uart0 wake-on-rx -> %s\n", esp_err_to_name(urc)); @@ -1588,6 +1596,19 @@ void loop() { // here on the loop task rather than racing peripheral drivers. processWsPendingCmds(); +#if CONFIG_PM_ENABLE + { + static bool prevActive = false; + bool active = deviceConnected && !b_softSleep; + if (active != prevActive && s_activeFreqLock != nullptr) { + if (active) esp_pm_lock_acquire(s_activeFreqLock); + else esp_pm_lock_release(s_activeFreqLock); + prevActive = active; + Serial.printf("[pm] cpu %s (connected+live=%d)\n", active ? "pinned 240" : "released->DFS", active); + } + } +#endif + if (b_powerOff){ shut_down_now_nobeep(); return;