Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions include/pbl/services/battery/battery_charge_limit.h
Original file line number Diff line number Diff line change
@@ -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);
9 changes: 9 additions & 0 deletions src/fw/apps/system/settings/system.c
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ typedef enum {
SystemMenuItemInformation,
SystemMenuItemCertification,
SystemMenuItemStationaryToggle,
SystemMenuItemChargeLimitToggle,
SystemMenuItemDebugging,
SystemMenuItemShutDown,
SystemMenuItemFactoryReset,
Expand All @@ -164,6 +165,7 @@ static const char *s_item_titles[SystemMenuItem_Count] = {
[SystemMenuItemInformation] = i18n_noop("Information"),
[SystemMenuItemCertification] = i18n_noop("Certification"),
[SystemMenuItemStationaryToggle] = i18n_noop("Stand-By Mode"),
[SystemMenuItemChargeLimitToggle] = i18n_noop("Charge Limit (80%)"),
[SystemMenuItemDebugging] = i18n_noop("Debugging"),
[SystemMenuItemShutDown] = i18n_noop("Shut Down"),
[SystemMenuItemFactoryReset] = i18n_noop("Factory Reset"),
Expand Down Expand Up @@ -1404,6 +1406,10 @@ static void prv_draw_row_cb(SettingsCallbacks *context, GContext *ctx,
case SystemMenuItemStationaryToggle:
subtitle = stationary_get_enabled() ? i18n_get("On", data) : i18n_get("Off", data);
break;
case SystemMenuItemChargeLimitToggle:
subtitle = shell_prefs_get_charge_limit_enabled() ? i18n_get("On", data)
: i18n_get("Off", data);
break;
case SystemMenuItemShutDown:
case SystemMenuItemInformation:
case SystemMenuItemCertification:
Expand Down Expand Up @@ -1434,6 +1440,9 @@ static void prv_select_click_cb(SettingsCallbacks *context, uint16_t row) {
case SystemMenuItemStationaryToggle:
stationary_set_enabled(!stationary_get_enabled());
break;
case SystemMenuItemChargeLimitToggle:
shell_prefs_set_charge_limit_enabled(!shell_prefs_get_charge_limit_enabled());
break;
case SystemMenuItemShutDown:
launcher_task_add_callback(prv_shutdown_cb, 0);
break;
Expand Down
77 changes: 77 additions & 0 deletions src/fw/services/battery/battery_charge_limit.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* 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 "drivers/rtc.h"
#include "pbl/services/regular_timer.h"
#include "shell/prefs.h"
#include "system/logging.h"

#define CHARGE_LIMIT_PCT 80
#define CHARGE_RESUME_PCT 77
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.

While plugged in, if not charging, the watch will run from USB power, so the battery will not drain. I guess you can simply disable the charger and forget about any hysteresis.

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.

Made the changes, thanks.

#define MIN_TOGGLE_INTERVAL_S 60
#define PERIODIC_CHECK_INTERVAL_S 60

////////////////////////
// State
T_STATIC bool s_limit_active;
T_STATIC RtcTicks s_last_toggle_ticks;
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;
}

// Rate limit: don't toggle more than once per MIN_TOGGLE_INTERVAL_S
if (s_last_toggle_ticks != 0) {
RtcTicks now = rtc_get_ticks();
RtcTicks elapsed = (now - s_last_toggle_ticks) / RTC_TICKS_HZ;
if (elapsed < MIN_TOGGLE_INTERVAL_S) {
return;
}
}

if (state.pct >= CHARGE_LIMIT_PCT && !s_limit_active) {
battery_set_charge_enable(false);
s_limit_active = true;
s_last_toggle_ticks = rtc_get_ticks();
PBL_LOG_INFO("Charge limit: disabling charging at %d pct", state.pct);
} else if (state.pct <= CHARGE_RESUME_PCT && s_limit_active) {
battery_set_charge_enable(true);
s_limit_active = false;
s_last_toggle_ticks = rtc_get_ticks();
PBL_LOG_INFO("Charge limit: resuming charging at %d pct", state.pct);
}
}

bool battery_charge_limit_is_active(void) {
return s_limit_active;
}
5 changes: 5 additions & 0 deletions src/fw/services/battery/battery_monitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}

Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/fw/services/battery/wscript_build
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
16 changes: 16 additions & 0 deletions src/fw/shell/normal/prefs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/fw/shell/normal/prefs_values.h.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/fw/shell/prefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
7 changes: 7 additions & 0 deletions src/fw/shell/prf/stubs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
7 changes: 7 additions & 0 deletions src/fw/shell/sdk/stubs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading