diff --git a/include/pbl/services/battery/battery_charge_limit.h b/include/pbl/services/battery/battery_charge_limit.h new file mode 100644 index 000000000..0ba15f06d --- /dev/null +++ b/include/pbl/services/battery/battery_charge_limit.h @@ -0,0 +1,15 @@ +/* SPDX-FileCopyrightText: 2025 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include "pbl/services/battery/battery_state.h" + +// The battery charge limit service optionally stops charging at 80% and resumes at 77% +// to reduce battery degradation from sustained high charge levels. + +void battery_charge_limit_init(void); + +void battery_charge_limit_evaluate(PreciseBatteryChargeState state); + +bool battery_charge_limit_is_active(void); diff --git a/src/fw/services/battery/battery_charge_limit.c b/src/fw/services/battery/battery_charge_limit.c new file mode 100644 index 000000000..6f6cd96a8 --- /dev/null +++ b/src/fw/services/battery/battery_charge_limit.c @@ -0,0 +1,62 @@ +/* SPDX-FileCopyrightText: 2025 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "pbl/services/battery/battery_charge_limit.h" + +#include "drivers/battery.h" +#include "pbl/services/regular_timer.h" +#include "shell/prefs.h" +#include "system/logging.h" + +#define CHARGE_LIMIT_PCT 80 +#define PERIODIC_CHECK_INTERVAL_S 60 + +//////////////////////// +// State +T_STATIC bool s_limit_active; +static RegularTimerInfo s_periodic_timer; + +static void prv_periodic_timer_cb(void *data) { + BatteryChargeState charge = battery_get_charge_state(); + PreciseBatteryChargeState state = { + .pct = charge.charge_percent, + .is_plugged = charge.is_plugged, + .is_charging = charge.is_charging, + }; + battery_charge_limit_evaluate(state); +} + +void battery_charge_limit_init(void) { + s_periodic_timer.cb = prv_periodic_timer_cb; + regular_timer_add_multisecond_callback(&s_periodic_timer, PERIODIC_CHECK_INTERVAL_S); +} + +void battery_charge_limit_evaluate(PreciseBatteryChargeState state) { + if (!shell_prefs_get_charge_limit_enabled()) { + if (s_limit_active) { + battery_set_charge_enable(true); + s_limit_active = false; + PBL_LOG_INFO("Charge limit: disabled, re-enabling charging"); + } + return; + } + + if (!state.is_plugged) { + s_limit_active = false; + return; + } + + if (state.pct >= CHARGE_LIMIT_PCT && !s_limit_active) { + battery_set_charge_enable(false); + s_limit_active = true; + PBL_LOG_INFO("Charge limit: disabling charging at %d pct", state.pct); + } else if (state.pct < CHARGE_LIMIT_PCT && s_limit_active) { + battery_set_charge_enable(true); + s_limit_active = false; + PBL_LOG_INFO("Charge limit: resuming charging at %d pct", state.pct); + } +} + +bool battery_charge_limit_is_active(void) { + return s_limit_active; +} diff --git a/src/fw/services/battery/battery_monitor.c b/src/fw/services/battery/battery_monitor.c index 16b76fd38..59c77af04 100644 --- a/src/fw/services/battery/battery_monitor.c +++ b/src/fw/services/battery/battery_monitor.c @@ -4,6 +4,7 @@ #include "pbl/services/battery/battery_monitor.h" #include "board/board.h" +#include "pbl/services/battery/battery_charge_limit.h" #include "kernel/low_power.h" #include "kernel/util/standby.h" #include "pbl/services/firmware_update.h" @@ -208,6 +209,8 @@ void battery_monitor_handle_state_change_event(PreciseBatteryChargeState state) prv_log_battery_state(state); + battery_charge_limit_evaluate(state); + s_first_run = false; } @@ -219,6 +222,8 @@ void battery_monitor_init(void) { // Initialize driver interface battery_state_init(); + + battery_charge_limit_init(); } bool battery_monitor_critical_lockout(void) { diff --git a/src/fw/services/battery/wscript_build b/src/fw/services/battery/wscript_build index f6e541283..64c562480 100644 --- a/src/fw/services/battery/wscript_build +++ b/src/fw/services/battery/wscript_build @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 use = ['fw_includes', 'services_analytics'] -sources = ['battery_monitor.c'] +sources = ['battery_charge_limit.c', 'battery_monitor.c'] if bld.is_asterix() or bld.is_obelix() or bld.is_getafix(): use.append('nrf_fuel_gauge') diff --git a/src/fw/services/blob_db/settings_blob_db.c b/src/fw/services/blob_db/settings_blob_db.c index 3581340f8..3da459666 100644 --- a/src/fw/services/blob_db/settings_blob_db.c +++ b/src/fw/services/blob_db/settings_blob_db.c @@ -107,6 +107,9 @@ static const char *s_syncable_settings[] = { "menuScrollWrapAround", "menuScrollVibeBehavior", + // Battery preferences + "chargeLimitEnabled", + // Worker preferences "workerId", diff --git a/src/fw/shell/normal/prefs.c b/src/fw/shell/normal/prefs.c index fffd0f331..558267251 100644 --- a/src/fw/shell/normal/prefs.c +++ b/src/fw/shell/normal/prefs.c @@ -109,6 +109,9 @@ static uint32_t s_backlight_ambient_threshold = 0; // default set from board con #define PREF_KEY_STATIONARY "stationaryMode" static bool s_stationary_mode_enabled = true; +#define PREF_KEY_CHARGE_LIMIT_ENABLED "chargeLimitEnabled" +static bool s_charge_limit_enabled = false; + #define PREF_KEY_DEFAULT_WORKER "workerId" static Uuid s_default_worker = UUID_INVALID_INIT; @@ -430,6 +433,11 @@ static bool prv_set_s_stationary_mode_enabled(bool *enabled) { return true; } +static bool prv_set_s_charge_limit_enabled(bool *enabled) { + s_charge_limit_enabled = *enabled; + return true; +} + static bool prv_set_s_default_worker(Uuid *uuid) { s_default_worker = *uuid; return true; @@ -1233,6 +1241,14 @@ void shell_prefs_set_stationary_enabled(bool enabled) { prv_pref_set(PREF_KEY_STATIONARY, &enabled, sizeof(enabled)); } +bool shell_prefs_get_charge_limit_enabled(void) { + return s_charge_limit_enabled; +} + +void shell_prefs_set_charge_limit_enabled(bool enabled) { + prv_pref_set(PREF_KEY_CHARGE_LIMIT_ENABLED, &enabled, sizeof(enabled)); +} + AppInstallId worker_preferences_get_default_worker(void) { return app_install_get_id_for_uuid(&s_default_worker); } diff --git a/src/fw/shell/normal/prefs_values.h.inc b/src/fw/shell/normal/prefs_values.h.inc index 2594ba204..04bb4d22b 100644 --- a/src/fw/shell/normal/prefs_values.h.inc +++ b/src/fw/shell/normal/prefs_values.h.inc @@ -21,6 +21,7 @@ #endif PREFS_MACRO(PREF_KEY_BACKLIGHT_AMBIENT_THRESHOLD, s_backlight_ambient_threshold) PREFS_MACRO(PREF_KEY_STATIONARY, s_stationary_mode_enabled) + PREFS_MACRO(PREF_KEY_CHARGE_LIMIT_ENABLED, s_charge_limit_enabled) PREFS_MACRO(PREF_KEY_DEFAULT_WORKER, s_default_worker) PREFS_MACRO(PREF_KEY_TEXT_STYLE, s_text_style) PREFS_MACRO(PREF_KEY_LANG_ENGLISH, s_language_english) diff --git a/src/fw/shell/prefs.h b/src/fw/shell/prefs.h index 0bbeb1a44..e7fc959b4 100644 --- a/src/fw/shell/prefs.h +++ b/src/fw/shell/prefs.h @@ -188,6 +188,9 @@ bool display_orientation_is_left(void); void display_orientation_set_left(bool left); #endif +bool shell_prefs_get_charge_limit_enabled(void); +void shell_prefs_set_charge_limit_enabled(bool enabled); + GColor shell_prefs_get_theme_highlight_color(void); void shell_prefs_set_theme_highlight_color(GColor color); diff --git a/src/fw/shell/prf/stubs.c b/src/fw/shell/prf/stubs.c index 543e80e8b..20b4a0a19 100644 --- a/src/fw/shell/prf/stubs.c +++ b/src/fw/shell/prf/stubs.c @@ -156,6 +156,13 @@ void app_storage_get_file_name(char *name, size_t buf_length, AppInstallId app_i *name = 0; } +bool shell_prefs_get_charge_limit_enabled(void) { + return false; +} + +void shell_prefs_set_charge_limit_enabled(bool enabled) { +} + bool shell_prefs_get_clock_24h_style(void) { return true; } diff --git a/src/fw/shell/sdk/stubs.c b/src/fw/shell/sdk/stubs.c index 8f4a9dbd6..3dcba032f 100644 --- a/src/fw/shell/sdk/stubs.c +++ b/src/fw/shell/sdk/stubs.c @@ -101,6 +101,13 @@ bool shell_prefs_get_stationary_enabled(void) { return false; } +bool shell_prefs_get_charge_limit_enabled(void) { + return false; +} + +void shell_prefs_set_charge_limit_enabled(bool enabled) { +} + bool shell_prefs_get_language_english(void) { return true; }