/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "services/common/battery/battery_monitor.h" #include "services/common/battery/battery_state.h" #include "services/common/battery/battery_curve.h" #include "clar.h" // Stubs //////////////////////////////////// #include "stubs_analytics.h" #include "stubs_logging.h" #include "stubs_passert.h" #include "stubs_prompt.h" #include "stubs_serial.h" #include "fake_new_timer.h" #include "fake_battery.h" #include "fake_system_task.h" #include "fake_rtc.h" #include "kernel/events.h" #include "system/logging.h" #include "system/reboot_reason.h" static bool s_entered_standby; static bool s_in_low_power; static bool s_error_window_shown; static bool s_warning_window_shown; static bool s_stop_mode_allowed; static PebbleEvent s_last_event_put; bool battery_is_usb_connected_raw(void) { return false; } void low_power_standby(void) { s_entered_standby = true; } void low_power_exit(void) { s_in_low_power = false; } void low_power_enter(void) { s_in_low_power = true; } bool low_power_is_active(void) { return s_in_low_power; } bool firmware_update_is_in_progress(void) { return false; } void battery_force_charge_enable(bool is_charging) { } bool stop_mode_is_allowed(void) { return s_stop_mode_allowed; } static void periodic_timer_trigger(int count) { TimerID timer_id = battery_state_get_periodic_timer_id(); cl_assert(timer_id != TIMER_INVALID_ID); cl_assert(stub_new_timer_is_scheduled(timer_id)); for (int i=0; itype == PEBBLE_BATTERY_STATE_CHANGE_EVENT) { battery_monitor_handle_state_change_event(event->battery_state.new_state); } else if (event->type == PEBBLE_BATTERY_CONNECTION_EVENT) { battery_state_handle_connection_event(event->battery_connection.is_connected); periodic_timer_trigger(1); } } // Setup //////////////////////////////////// void test_battery_monitor__initialize(void) { //g_pbl_log_enabled = true; //g_pbl_log_level = 255; s_entered_standby = false; s_error_window_shown = false; s_warning_window_shown = false; s_in_low_power = false; s_stop_mode_allowed = true; fake_rtc_init(0, 0); fake_rtc_auto_increment_ticks(0); } void test_battery_monitor__cleanup(void) { } // Tests //////////////////////////////////// int32_t battery_curve_lookup_percent_with_scaling_factor( int battery_mv, bool is_charging, uint32_t scaling_factor); void test_battery_monitor__scaled_reading(void) { int32_t scaling_factor = INT32_MAX / 100; int32_t prev_reading = 0; // run through a wide range of battery readings. Confirm as the mv increases, // the percentage reported increases. Use the largest scaling factor to check // for integer overflows for (int mv = 3000; mv < 5000; mv++) { int32_t res = battery_curve_lookup_percent_with_scaling_factor( mv, false, scaling_factor); cl_assert(prev_reading <= res); prev_reading = res; } // make sure that when we compute the largest possible (100% - 0%) and lowest possible // (0% - 100%) battery delta that we don't overflow the computation int32_t start_percent = battery_curve_lookup_percent_with_scaling_factor( 2000, false, scaling_factor); int32_t end_percent = battery_curve_lookup_percent_with_scaling_factor( 5000, false, scaling_factor); int32_t delta_percent = end_percent - start_percent; cl_assert(delta_percent > (INT32_MAX - 100)); delta_percent = start_percent - end_percent; cl_assert(delta_percent < (INT32_MIN + 100)); } // Check that the percentage reported is somewhat protected from transient voltage changes void test_battery_monitor__charge_fluctuate_voltage(void) { int high_percent = 70; int low_percent = 20; int high_mv = battery_curve_lookup_voltage_by_percent(high_percent, true); int low_mv = battery_curve_lookup_voltage_by_percent(low_percent, true); fake_battery_init(high_mv, true, true); battery_monitor_init(); periodic_timer_trigger(1); // For the first sample, it will be identical cl_assert_equal_i(battery_get_charge_state().charge_percent, high_percent); // ...and should stay that way periodic_timer_trigger(10); cl_assert_equal_i(battery_get_charge_state().charge_percent, high_percent); // Then, when the voltage drops, the percentage should begin to decline - but should not reach the low value yet fake_battery_set_millivolts(low_mv); periodic_timer_trigger(1); int delta = high_percent - battery_get_charge_state().charge_percent; cl_assert(delta >= 0); cl_assert(delta < high_percent - low_percent); // But, it should approach that value over time int last_delta = delta; while(battery_get_charge_state().charge_percent > low_percent) { periodic_timer_trigger(1); delta = high_percent - battery_get_charge_state().charge_percent; cl_assert(delta >= last_delta); last_delta = delta; } cl_assert_equal_i(battery_get_charge_state().charge_percent, low_percent); } void test_battery_monitor__connection_reset(void) { // Test for PBL-19951: Reset charge percent on reconnection events int percent = 10; int charge_mv = battery_curve_lookup_voltage_by_percent(percent, true); int discharge_mv = battery_curve_lookup_voltage_by_percent(percent, false); fake_battery_init(discharge_mv, false, false); battery_monitor_init(); periodic_timer_trigger(1); cl_assert_equal_i(battery_get_charge_state().charge_percent, percent); fake_battery_set_charging(true); fake_battery_set_millivolts(charge_mv); fake_battery_set_connected(true); cl_assert_equal_i(battery_get_charge_state().charge_percent, percent); fake_battery_set_charging(false); fake_battery_set_millivolts(discharge_mv); fake_battery_set_connected(false); cl_assert_equal_i(battery_get_charge_state().charge_percent, percent); } void test_battery_monitor__curve_adjustment_when_charge_complete(void) { int charge_mv = battery_curve_lookup_voltage_by_percent(0, true); int full_mv = battery_curve_lookup_voltage_by_percent(100, false); int charge_terminate_mv = battery_curve_lookup_voltage_by_percent(95, false); fake_battery_init(charge_mv, true, true); battery_monitor_init(); periodic_timer_trigger(1); fake_battery_set_millivolts(charge_terminate_mv); fake_battery_set_charging(false); periodic_timer_trigger(1); cl_assert_equal_i(battery_get_charge_state().charge_percent, 100); fake_battery_set_millivolts(full_mv); periodic_timer_trigger(1); cl_assert_equal_i(battery_get_charge_state().charge_percent, 100); } void test_battery_monitor__curve_doesnt_shift_too_far(void) { int charge_mv = battery_curve_lookup_voltage_by_percent(0, true); int charge_terminate_mv = battery_curve_lookup_voltage_by_percent(80, false); fake_battery_init(charge_mv, true, true); battery_monitor_init(); periodic_timer_trigger(1); fake_battery_set_millivolts(charge_terminate_mv); fake_battery_set_charging(false); periodic_timer_trigger(1); cl_assert_equal_i(battery_get_charge_state().charge_percent, 80); } /* good -> lpm lpm -> good good -> critical critical -> lpm lpm -> critical critical -> good */ typedef enum { PowerStateGood, PowerStateLowPower, PowerStateCritical, PowerStateStandby } PowerStateID; extern PowerStateID s_power_state; void test_battery_monitor__transitions(void) { int good_mv = battery_curve_lookup_voltage_by_percent(100, false); int low_mv = battery_curve_lookup_voltage_by_percent(3, false); int critical_mv = battery_curve_lookup_voltage_by_percent(0, false); fake_battery_init(good_mv, false, false); battery_monitor_init(); periodic_timer_trigger(1); cl_assert(!s_in_low_power && !battery_monitor_critical_lockout()); cl_assert_equal_i(s_power_state, PowerStateGood); // good -> lpm fake_battery_set_millivolts(low_mv); periodic_timer_trigger(10); cl_assert(s_in_low_power); cl_assert_equal_i(s_power_state, PowerStateLowPower); // lpm -> good fake_battery_set_charging(true); fake_battery_set_connected(true); periodic_timer_trigger(1); cl_assert(!s_in_low_power); cl_assert_equal_i(s_power_state, PowerStateGood); // good -> critical fake_battery_set_millivolts(critical_mv); fake_battery_set_charging(false); fake_battery_set_connected(false); periodic_timer_trigger(20); cl_assert(battery_monitor_critical_lockout()); cl_assert_equal_i(s_power_state, PowerStateCritical); // critical -> lpm (only possible if unstable) fake_battery_set_millivolts(low_mv); battery_state_force_update(); periodic_timer_trigger(1); cl_assert(s_in_low_power); cl_assert_equal_i(s_power_state, PowerStateLowPower); // lpm -> critical fake_battery_set_millivolts(critical_mv); periodic_timer_trigger(20); cl_assert(battery_monitor_critical_lockout()); cl_assert(s_in_low_power); cl_assert_equal_i(s_power_state, PowerStateCritical); // critical -> good fake_battery_set_charging(true); fake_battery_set_connected(true); fake_battery_set_millivolts(good_mv); periodic_timer_trigger(20); cl_assert(!battery_monitor_critical_lockout()); cl_assert(!s_in_low_power); cl_assert_equal_i(s_power_state, PowerStateGood); } void test_battery_monitor__low_first_run(void) { int low_mv = battery_curve_lookup_voltage_by_percent(3, false); fake_battery_init(low_mv, false, false); battery_monitor_init(); periodic_timer_trigger(1); cl_assert(battery_monitor_critical_lockout()); cl_assert_equal_i(standby_timer_get_timeout(), 2000); standby_timer_trigger(1); cl_assert(s_entered_standby); } void test_battery_monitor__critical(void) { int good_mv = battery_curve_lookup_voltage_by_percent(10, false); int critical_mv = battery_curve_lookup_voltage_by_percent(0, false); fake_battery_init(good_mv, false, false); battery_monitor_init(); periodic_timer_trigger(1); cl_assert(!battery_monitor_critical_lockout()); fake_battery_set_millivolts(critical_mv); periodic_timer_trigger(25); cl_assert(battery_monitor_critical_lockout()); cl_assert_equal_i(standby_timer_get_timeout(), 30000); standby_timer_trigger(1); cl_assert(s_entered_standby); } void test_battery_monitor__critical_plugged_in(void) { int good_mv = battery_curve_lookup_voltage_by_percent(10, false); int critical_mv = battery_curve_lookup_voltage_by_percent(0, false); fake_battery_init(good_mv, false, false); battery_monitor_init(); periodic_timer_trigger(1); cl_assert(!battery_monitor_critical_lockout()); fake_battery_set_millivolts(critical_mv); periodic_timer_trigger(25); cl_assert(battery_monitor_critical_lockout()); cl_assert_equal_i(standby_timer_get_timeout(), 30000); fake_battery_set_charging(true); fake_battery_set_connected(true); periodic_timer_trigger(1); standby_timer_trigger(1); cl_assert(!s_entered_standby); } void test_battery_monitor__increase_discharging(void) { int low_mv = battery_curve_lookup_voltage_by_percent(50, false); int high_mv = battery_curve_lookup_voltage_by_percent(100, false); int lower_mv = battery_curve_lookup_voltage_by_percent(20, false); fake_battery_init(low_mv, false, false); fake_rtc_auto_increment_ticks(50000); battery_monitor_init(); periodic_timer_trigger(5); cl_assert_equal_i(battery_get_charge_state().charge_percent, 50); // Should be stable by now // Shouldn't update percent (actually, shouldn't even send events.) PBL_LOG(LOG_LEVEL_DEBUG, "Shouldn't be any updates"); PBL_LOG(LOG_LEVEL_DEBUG, "▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼"); fake_battery_set_millivolts(high_mv); periodic_timer_trigger(20); cl_assert_equal_i(battery_get_charge_state().charge_percent, 50); PBL_LOG(LOG_LEVEL_DEBUG, "▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲"); // Should still update if it goes lower fake_battery_set_millivolts(lower_mv); periodic_timer_trigger(20); cl_assert_equal_i(battery_get_charge_state().charge_percent, 20); } void test_battery_monitor__stop_mode_disabled(void) { int start_mv = battery_curve_lookup_voltage_by_percent(50, false); int end_mv = battery_curve_lookup_voltage_by_percent(20, false); fake_battery_init(start_mv, false, false); // Start off with a nice battery level battery_monitor_init(); periodic_timer_trigger(1); cl_assert_equal_i(battery_get_charge_state().charge_percent, 50); // Pretend vibe activated or something like that. // - The reported mV goes down and stop mode is disabled // It should skip 5 times (MAX_SAMPLE_SKIPS) before updating. fake_battery_set_millivolts(end_mv); s_stop_mode_allowed = false; periodic_timer_trigger(5); cl_assert_equal_i(battery_get_charge_state().charge_percent, 50); // After 5 skips, we should update. periodic_timer_trigger(1); cl_assert(battery_get_charge_state().charge_percent < 50); } void test_battery_monitor__connection_states(void) { int charge_mv = battery_curve_lookup_voltage_by_percent(60, true); int okay_mv = battery_curve_lookup_voltage_by_percent(5, false); int discharge_mv = battery_curve_lookup_voltage_by_percent(3, false); // Begin in LPM, unplugged and discharging. fake_battery_init(okay_mv, false, false); battery_monitor_init(); periodic_timer_trigger(1); fake_battery_set_millivolts(discharge_mv); periodic_timer_trigger(1); cl_assert(s_in_low_power); // If we somehow begin charging, ignore it. fake_battery_set_charging(true); periodic_timer_trigger(1); cl_assert(s_in_low_power); // If we're charging and connected, reset the filter fake_battery_set_millivolts(charge_mv); fake_battery_set_connected(true); periodic_timer_trigger(1); cl_assert(!s_in_low_power && battery_get_charge_state().charge_percent == 60); // Discharging but connected - The charge should update so 60% is 100% fake_battery_set_charging(false); periodic_timer_trigger(1); cl_assert_equal_i(battery_get_charge_state().charge_percent, 100); } void test_battery_monitor__battery_get_charge_state(void) { // range through all discrete percentages and verify that battery_get_charge_state() // returns sane values BatteryChargeState result; uint8_t last_charge_percent = 0; uint8_t last_discharge_percent = 100; for (uint32_t charge_percent = 0; charge_percent <= 100; ++charge_percent) { int charge_mv = battery_curve_lookup_voltage_by_percent(charge_percent, true); int discharge_mv = battery_curve_lookup_voltage_by_percent(100 - charge_percent, false); // // test as if the battery is plugged and charging // bool charging = charge_percent < 100; fake_battery_init(charge_mv, true, charging); battery_monitor_init(); periodic_timer_trigger(1); result = battery_get_charge_state(); // due to fudge factors we merely check that the percentage is in range and // that it is monotonically increasing cl_assert((result.charge_percent >= 0) && (result.charge_percent <= 100)); cl_assert(result.charge_percent >= last_charge_percent); cl_assert(result.is_charging == charging); cl_assert(result.is_plugged); last_charge_percent = result.charge_percent; // // test as if the battery is unplugged and discharging // fake_battery_init(discharge_mv, false, false); battery_monitor_init(); periodic_timer_trigger(1); result = battery_get_charge_state(); // due to fudge factors we merely check that the percentage is in range and // that it is monotonically decreasing cl_assert((result.charge_percent >= 0) && (result.charge_percent <= 100)); cl_assert(result.charge_percent <= last_discharge_percent); cl_assert(!result.is_charging); cl_assert(!result.is_plugged); last_discharge_percent = result.charge_percent; } }