Skip to content
Hack Your WorldSoftware · Infrastructure · Home automation

Analysis

M5StickC Plus Battery Life: Testing ESP-IDF Light Sleep

For an idle Bluetooth M5StickC Plus, supported ESP-IDF power management matters more than simply lowering CPU frequency or forcing manual sleep. The project reports an idle estimate of about 12 hours with coordinated light sleep, compared with roughly 2.3 hours before the power work. That estimate is not a completed discharge test.

M5StickC Plus running the Hardware Buddy firmware on a desk
Project photograph from the companion firmware repository.

The code is public in my claude-desktop-buddy fork. The device is an M5StickC Plus running the screen-side half of my Buddy Bridge setup. It shows activity from coding sessions and can approve or deny an opted-in request from two physical buttons.

That sounds like a light workload: a 135-by-240 display, a Bluetooth Low Energy connection, and an animation that mostly sleeps. The first battery result made it clear that “mostly” was doing too much work.

In this article

The display was off, but the processor and radio were not

The firmware already dimmed and shut off the screen when it was idle. It also reduced animation, display, and motion-sensor work. None of those changes gave the device the kind of runtime I expected from a desk companion.

The original low-power pass lowered the screen-off CPU clock, throttled the idle loop, sampled the IMU less often, and stopped advertising after a Bluetooth client connected. It also increased the stale-data window so the slower idle heartbeat would not make the firmware declare the bridge dead.

Those are sensible reductions, but BLE still has timing requirements. The radio must wake for connection events, and the processor cannot be pushed below a clock floor that quietly disables that path.

Forty megahertz made the sleeping device unreachable

I tried dropping the ESP32 core to 40 MHz while the screen was off. The battery number improved, but the device stopped behaving like a remote. Buddy Bridge could connect, then the link would disappear roughly 70 seconds into screen-off. Re-advertising also vanished, so the bridge could not find the stick again until I woke it by hand.

The problem was not the approval logic. The firmware retained an 80 MHz CPU floor after that failure. Espressif documents 80 MHz as the ESP32 APB maximum, not an open-ended minimum: Bluetooth holds that frequency lock while active and can release it during modem-sleep intervals. The older board library had happened to keep that clock domain alive. Moving to M5Unified exposed the too-low floor.

I raised the screen-off clock back to 80 MHz and kept the crystal domain powered for Bluetooth. That restored reconnection and prompt delivery. It also removed an attractive but invalid power-saving number: battery life does not count if the device misses the event it exists to show.

A manual light-sleep loop looked like a brownout

The next attempt called esp_light_sleep_start() directly during idle gaps. About nine seconds after the screen turned off, the ESP32-PICO-D4 rebooted. The first suspicion was a brownout because the failure appeared only in the low-power path.

A debug build made the reset reason visible over serial and on the boot splash. It was an RTC watchdog reset, not a brownout. The manual sleep loop conflicted with the watchdog and the crystal arrangement needed by BLE on this chip revision.

I tried the obvious watchdog controls before abandoning the path. Disabling, feeding, and changing the RTC watchdog did not make the loop stable. A custom bootloader with watchdog changes did not fix it either. The manual call was the wrong level of control.

The power-manager build stopped the immediate resets

The working design moves the firmware to Arduino built as an ESP-IDF component. That enables CONFIG_PM_ENABLE and tickless idle, then configures automatic light sleep with esp_pm_configure().

The project records more than 100 seconds of screen-off testing with no reboot, GIF characters intact, and prompts waking into the approval screen. That shows the revised build avoided the immediate failure in that check. It does not establish how much time the CPU spent asleep.

There is a configuration detail to verify before attributing the saving to light sleep. The ESP-IDF 4.4.7 ESP32 guide says Bluetooth retains a no-light-sleep lock unless its low-power clock uses an external 32 kHz crystal. The project describes a main-crystal configuration. Setting light_sleep_enable=true permits sleep when locks allow it; it does not prove sleep occurred. I have not captured the generated build configuration, runtime power locks, or sleep residency from this device to reconcile that difference. The short stability check remains useful, but the sleep mechanism needs verification.

Correction, October 10: Enabling automatic sleep is not proof that it occurred; the Bluetooth power-lock caveat below remains unverified on this hardware. I previously described about 12 hours as measured battery runtime. The original pull request calls it an idle estimate and explicitly leaves a full-discharge curve and hours-long reconnect testing for later. The recorded hardware check covered more than 100 seconds with the screen off and BLE connected, without a reboot. That supports the immediate stability fix, not a twelve-hour endurance claim.

Bluetooth still needed an active and an idle personality

The project attributes the larger improvement to its power-manager build. The BLE schedule also changed, so the estimate alone cannot separate their contributions. A desk display does not need the same radio responsiveness while its screen is dark as it does while a permission prompt is waiting.

The firmware now asks for a relaxed connection while idle and a tight connection after wake-up. The idle request uses a 30–60 ms interval, slave latency of eight, and a ten-second supervision timeout. Waking requests 7.5–22.5 ms with no slave latency and a four-second supervision timeout.

That split keeps the radio out of unnecessary idle events without forcing every prompt through a sleepy schedule. The first BLE-only tuning pass moved runtime from roughly 2.3 to 3.5 hours. In the power-manager build, idle latency was reduced from twelve to eight to favor connection reliability.

The battery page had to measure the whole session

An instantaneous current reading is misleading when I open the battery page: the screen has just turned on, the processor is active, and the radio may be busy. That number describes inspection, not idle life.

The M5StickC Plus has an AXP192 power-management chip with a hardware coulomb counter. The firmware snapshots that counter when USB power is removed and integrates the discharge over the battery session. The device page can then show elapsed battery time, average draw, and projected time remaining.

The newer M5StickS3 is supported by the same codebase, but its M5PM1 does not expose the same current-sense and coulomb-counter path. On that board the battery page falls back to a voltage estimate. The firmware keeps that limitation visible instead of inventing precision.

There are two build paths for a reason

The normal PlatformIO environments use the precompiled Arduino framework. They are easier to build and currently cover the M5StickC Plus, M5StickS3, bench, and debug targets in GitHub Actions. The article originally checked a CI run that passed all four builds at commit 8b7eb0f.

The approximately 12-hour path is the separate ESP-IDF environment. It builds Arduino and LittleFS from source, pulls an ESP-IDF component, and takes longer. The repository’s CI file deliberately excludes that network-dependent build; the project history records a local build, a short hardware check, and an idle-runtime estimate. I did not rerun the PlatformIO build or perform a full battery discharge test for this article.

The published 0.1.0 release also predates the 12-hour work. Its prebuilt M5StickC Plus images remain useful, but downloading that release does not install the current power-management path. As checked on October 10, 2026, v0.1.0 is still the latest published release. To reproduce the experiment, start with the linked power-management revision and its IDF environment, rather than assuming a newer main-branch checkout is identical.

What I would buy for a new build

I would not hunt down an old M5StickC Plus just to chase the reported idle estimate. M5Stack now marks the M5StickC Plus2 as end-of-life and points new buyers to the M5StickS3. The S3 is the current retail board, with a 250 mAh battery, native USB-C, 8 MB of flash, and 8 MB of PSRAM.

The firmware builds for the S3 and the project history records a hardware check, but its power hardware is different. It has no AXP192 coulomb counter, and I have not completed a comparable discharge test on the S3. I would buy the S3 for a new desk display, not assume that the old board’s runtime estimate transfers unchanged.

Use the right image when updating an already-paired stick

The release has a merged image for a blank device and an app-only image for an update. They are not interchangeable.

# Blank device: bootloader, partitions, and app
esptool.py --chip esp32 write_flash 0x0 claude-desktop-buddy-<version>-merged.bin

# Existing paired device: application only
esptool.py --chip esp32 write_flash 0x10000 claude-desktop-buddy-<version>-app.bin

The merged image starts at 0x0 and overwrites NVS at 0x9000. That removes the BLE bond and saved settings. If the computer still holds the old bond, the new connection can drop about one second after connecting because the two sides no longer share the same keys.

For a routine update, the app image at 0x10000 preserves NVS. If I intentionally use the merged image, I also remove the old Bluetooth device from Windows before pairing again. The problem is stale identity, not weak signal.

What I would keep from this work

The revised build avoided the immediate reset and produced a better runtime estimate. Before calling that a light-sleep result, I still need evidence that the CPU entered sleep with Bluetooth connected, followed by a full discharge test.

The debugging order mattered just as much. I made the reset reason visible before changing more hardware settings. I rejected a 40 MHz result when the radio stopped working. I separated an instantaneous current display from a whole-session measurement. I kept a simpler build path when the best runtime required a more complicated toolchain.

Before treating it as an all-day device, I would record a full discharge with the firmware revision, battery condition, screen settings, prompt activity, and reconnect count. A longer estimate is encouraging; missed prompts or unexpected resets would still make the build unsuitable.

Disclosure: I maintain this fork and Buddy Bridge. Both descend from Anthropic’s Hardware Buddy example; the projects are unofficial.