Skip to content

fw/services/battery: add optional battery charge limit - #1841

Draft
adipascu wants to merge 4 commits into
coredevices:mainfrom
peblum:battery-charge-limit-upstream
Draft

adipascu wants to merge 4 commits into
coredevices:mainfrom
peblum:battery-charge-limit-upstream

Conversation

@adipascu

@adipascu adipascu commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Adds an optional battery charge limit that pauses charging at a configurable percentage to reduce long-term battery degradation. This picks up the idea from #1156 and reworks it per the review feedback there. Tested on a Pebble Time 2 together with the companion app change (coredevices/mobileapp#318).

Fixes #722

fw/services/battery: add optional battery charge limit

The limiter runs on every battery state change event. Once the battery reaches the configured limit while plugged in, charging pauses through the nPM1300 charger enable (BCHGENABLECLR) and re-enables below the limit, on unplug, or when the limit is turned off. Per @gmarull's feedback on the original PR there is no resume hysteresis. While plugged in, SYSREG supplies VSYS from VBUS and the battery is isolated (nPM1300 PS v1.1, 6.2.10), so the level parks at the limit. If a heavy load ever drains the battery through supplement mode, the drop below the limit re-enables charging on the next state change event.

The chargeLimitPct preference (0 disables, 50-95 accepted) is off by default and set from the phone through the settings blob db whitelist, per @ericmigi's feedback to keep new settings off the watch UI. Values are validated on sync and clamped at boot, since the boot load bypasses the validating setters. Preference changes force a battery state update so a new limit applies immediately, replacing the 60 s polling timer from the original patch. The limiter also stands down while charging is force-disabled from the debug prompt, so battery charge disable keeps full manual control.

Compared to the original patch this also fixes a nonexistent system/logging.h include that broke the build, and a path that left the PMIC charger permanently disabled after unplugging at the limit, because BCHGENABLECLR is persistent and nothing re-enables it on replug. test_battery_charge_limit covers the state machine.

Note

Known limitation: the limiter evaluates on the once-per-minute fuel gauge sample, so at the original 1C rapid charge rate the level can run about 2% past the configured limit before charging pauses (an 80% limit can park showing 81%). The follow-up #1844 tightens the sampling while plugged in to bound the overshoot to a fraction of a percent.

fw/services/battery: enable charger before emitting state events

The boot-time one-shot charger enable ran after the state change event was posted. The event handler runs on KernelMain, which preempts KernelBG, so booting plugged in above the limit would pause charging and then have the one-shot silently re-enable it for the session. The enable now happens before the event.

drivers/qemu: honor the argument in battery_set_charge_enable

The QEMU battery driver faked an unplug instead of tracking charger enablement, which made the limiter untestable in the emulator and permanently cleared the emulated USB connection.

fw/apps/settings: show charge limit in system information

Since the pref is phone-only, Settings > System > Information gains a read-only Charge Limit row (shows Off while disabled) so the active limit can be checked on the watch itself. Captured on a Pebble Time 2 with an 80% limit synced from the app:

Charge Limit row in System Information

Written with AI assistance (Claude), and running on my own Pebble Time 2 since the PR opened.


Aside: I am currently available for firmware contracting or full-time work. Contact: adrian@pascu.be.

@adipascu
adipascu force-pushed the battery-charge-limit-upstream branch 3 times, most recently from 4fee13f to 9a6b76e Compare August 6, 2026 04:25
@adipascu
adipascu marked this pull request as ready for review August 6, 2026 16:42
@adipascu
adipascu force-pushed the battery-charge-limit-upstream branch 2 times, most recently from 606c713 to 73a6eef Compare August 6, 2026 16:57
@adipascu
adipascu marked this pull request as draft August 6, 2026 17:08
@adipascu
adipascu marked this pull request as ready for review August 6, 2026 17:23
@adipascu
adipascu force-pushed the battery-charge-limit-upstream branch from 73a6eef to 9b3861b Compare August 6, 2026 17:36
@adipascu
adipascu force-pushed the battery-charge-limit-upstream branch from 9b3861b to e662449 Compare August 6, 2026 18:07
Comment thread include/pbl/services/comm_session/session_remote_version.h Outdated
@gmarull

gmarull commented Aug 10, 2026

Copy link
Copy Markdown
Member

Note that on partial charges we have a known fuel gauge algorithm accuracy problem, pending discussion with Nordic. So let's hold this until vendor clarifies if there's anything wrong on our side, or the fuel gauge lib (closed blob) has problems.

@adipascu

adipascu commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor Author

@gmarull I have been running this on my own Time 2 for a month: 14 days under the limiter with no CHARGE_STATE_COMPLETE, then a deliberate full charge, then another 17 days unanchored.

On whether the pack needs to reach 100 % periodically to keep the gauge calibrated: it does not. The first anchor, after 14 days without one, corrected reported SOC by 1.0 pp. In the 17 days since, with no anchor at all, the same resting voltage still maps to the same SOC within +0.17 pp (sd 0.38, n = 30 voltages, rested samples only).

Reported SOC follows both cell voltage and integrated current, on different timescales. Within a single 3.4-day discharge run, SOC keeps falling while voltage sits on one 5 mV step, by up to 3.8 pp, so current is being integrated. Over weeks the voltage term bounds the coulomb term, so the error does not accumulate without a full-charge reference.

Anything measured before #1852 fed the gauge inputs quantised 4x coarser than design.

The 0.5C charge current change is also in this PR, running on the same watch.

Charts and method: https://claude.ai/code/artifact/58d02ef7-d87a-4654-8d02-758ee1520da7
CSV, 3,678 samples over 31 days: https://github.com/peblum/Peblum/releases/download/pr-assets/battery-series-2026-08-08-to-09-07.csv

@adipascu
adipascu force-pushed the battery-charge-limit-upstream branch from ce80c4a to 11dd579 Compare August 28, 2026 19:19
@adipascu
adipascu force-pushed the battery-charge-limit-upstream branch from 320d9a4 to c02be2a Compare September 8, 2026 02:12
[SystemInformationItemHardware] = i18n_noop("Hardware"),
[SystemInformationItemSerial] = i18n_noop("Serial"),
[SystemInformationItemUptime] = i18n_noop("Uptime"),
[SystemInformationItemChargeLimit] = i18n_noop("Charge Limit"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems redundant, there's already a setting for that (even if on the phone only)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a read-only row in System > Information, not a setting. Still want it removed?

bool blob_db_version_support: 1;
bool settings_sync_support: 1; // Phone supports Settings BlobDB sync
bool weather_db_v4_support: 1; // Phone writes the v4 weather BlobDB record (rich forecast)
bool charge_limit_support: 1; // Watch supports the chargeLimitPct setting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nack, settings sync should handle that (if not already, @jplexer may be more familiar)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped. The app gates on firmware version instead, same as the HRM prefs.

Comment thread src/fw/board/boards/board_obelix.c Outdated
Comment on lines +541 to +542
// 190mA = 1C (rapid charge, max limit from datasheet)
.chg_current_ma = 190,
// 96mA = 0.5C standard charge (datasheet rapid-charge max is 190mA = 1C)
.chg_current_ma = 96,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nack, add new setting to choose between normal/fast charge instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to separate PR: #2052 and coredevices/mobileapp#419.

battery_set_charge_enable unconditionally cleared the emulated USB
connection, so pausing charging made the emulator report the charger
as unplugged and the connection could never be restored. Track a
separate charging-enabled flag and report charging only while it is
set.

Signed-off-by: Adrian Pascu <adrian@pascu.be>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
adipascu and others added 3 commits September 12, 2026 20:22
The one-shot charger enable after the first fuel gauge update ran
after the state change event was posted. The event handler runs on
KernelMain, which preempts KernelBG, so a consumer reacting to the
event by disabling the charger would be silently overridden when
KernelBG resumed. Enable the charger before posting the event.

Signed-off-by: Adrian Pascu <adrian@pascu.be>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a battery charge limit service that pauses charging once the
battery reaches a configurable percentage limit, reducing degradation
from sustained high charge levels. While plugged in the watch runs
from USB power, so the level holds without a resume threshold and
charging re-enables when the level is below the limit, the watch is
unplugged, or the limit is disabled. The limiter stands down while
charging is force-disabled from the debug prompt.

The chargeLimitPct preference (0 disables, 50-95 accepted, validated
on sync and at boot) is off by default, set from the phone via the
settings blob db, and applies immediately by forcing a battery state
update.

Based on an original patch by Shashvat Prabhu, reworked to a
configurable percentage with event-driven evaluation instead of a
60 s poll timer, with the nonexistent logging include and the
charging-stuck-off-after-unplug bugs fixed, and with unit tests
added.

Fixes coredevices#722

Co-authored-by: Shashvat Prabhu <shashvatprabhu2006@gmail.com>
Signed-off-by: Adrian Pascu <adrian@pascu.be>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Surface the configured battery charge limit as a read-only row in
Settings > System > Information, so the active limit can be checked
on the watch itself. Shows Off while the limit is disabled.

Signed-off-by: Adrian Pascu <adrian@pascu.be>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@adipascu
adipascu force-pushed the battery-charge-limit-upstream branch from d1a2167 to 6ab5ea9 Compare September 12, 2026 18:23
@adipascu
adipascu requested a review from gmarull September 12, 2026 19:42
@adipascu
adipascu marked this pull request as draft September 16, 2026 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DNM Do Not Merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to limit battery charge to X%

2 participants