feat(power): add battery monitoring and status overlays
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "domain.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "platform.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if CONFIG_TQ_BATTERY_ENABLE
|
||||
|
||||
#if CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT <= CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT
|
||||
#error "Battery low recovery threshold must exceed the entry threshold"
|
||||
#endif
|
||||
|
||||
#if CONFIG_TQ_BATTERY_CRITICAL_EXIT_PERCENT <= CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT
|
||||
#error "Battery critical recovery threshold must exceed the entry threshold"
|
||||
#endif
|
||||
|
||||
#if CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT >= CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT
|
||||
#error "Battery critical entry threshold must be below the low entry threshold"
|
||||
#endif
|
||||
|
||||
#define BATTERY_STATE_CONFIRM_SAMPLES 3U
|
||||
#define BATTERY_MAX_ACCEPTED_STEP_MV 300
|
||||
#define BATTERY_JUMP_MATCH_WINDOW_MV 100
|
||||
#define BATTERY_FILTER_OLD_WEIGHT 8
|
||||
#define BATTERY_FILTER_NEW_WEIGHT 2
|
||||
|
||||
typedef struct {
|
||||
int voltage_mv;
|
||||
uint8_t percentage;
|
||||
} battery_curve_point_t;
|
||||
|
||||
static const battery_curve_point_t s_discharge_curve[] = {
|
||||
{3300, 0},
|
||||
{3400, 2},
|
||||
{3600, 8},
|
||||
{3700, 18},
|
||||
{3800, 35},
|
||||
{3900, 55},
|
||||
{4000, 75},
|
||||
{4100, 90},
|
||||
{4200, 100},
|
||||
};
|
||||
|
||||
static StaticSemaphore_t s_lock_buffer;
|
||||
static SemaphoreHandle_t s_lock;
|
||||
static portMUX_TYPE s_lock_init_guard = portMUX_INITIALIZER_UNLOCKED;
|
||||
static domain_battery_status_t s_status;
|
||||
static bool s_initialized;
|
||||
static bool s_filter_initialized;
|
||||
static int s_filtered_mv;
|
||||
static int s_jump_candidate_mv;
|
||||
static uint8_t s_jump_candidate_count;
|
||||
static uint8_t s_low_count;
|
||||
static uint8_t s_critical_count;
|
||||
|
||||
static bool ensure_lock(void)
|
||||
{
|
||||
if (s_lock != NULL) {
|
||||
return true;
|
||||
}
|
||||
portENTER_CRITICAL(&s_lock_init_guard);
|
||||
if (s_lock == NULL) {
|
||||
s_lock = xSemaphoreCreateMutexStatic(&s_lock_buffer);
|
||||
}
|
||||
portEXIT_CRITICAL(&s_lock_init_guard);
|
||||
return s_lock != NULL;
|
||||
}
|
||||
|
||||
static bool lock_state(void)
|
||||
{
|
||||
return ensure_lock() && xSemaphoreTake(s_lock, pdMS_TO_TICKS(100)) == pdTRUE;
|
||||
}
|
||||
|
||||
static void unlock_state(void)
|
||||
{
|
||||
(void)xSemaphoreGive(s_lock);
|
||||
}
|
||||
|
||||
static domain_battery_charge_state_t map_charge_state(
|
||||
platform_battery_charge_state_t state)
|
||||
{
|
||||
switch (state) {
|
||||
case PLATFORM_BATTERY_CHARGE_CHARGING:
|
||||
return DOMAIN_BATTERY_CHARGE_CHARGING;
|
||||
case PLATFORM_BATTERY_CHARGE_NOT_CHARGING:
|
||||
return DOMAIN_BATTERY_CHARGE_NOT_CHARGING;
|
||||
default:
|
||||
return DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t voltage_to_percentage(int voltage_mv)
|
||||
{
|
||||
if (voltage_mv <= s_discharge_curve[0].voltage_mv) {
|
||||
return s_discharge_curve[0].percentage;
|
||||
}
|
||||
|
||||
const size_t count = sizeof(s_discharge_curve) / sizeof(s_discharge_curve[0]);
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
if (voltage_mv <= s_discharge_curve[i].voltage_mv) {
|
||||
int lower_mv = s_discharge_curve[i - 1U].voltage_mv;
|
||||
int upper_mv = s_discharge_curve[i].voltage_mv;
|
||||
int lower_pct = s_discharge_curve[i - 1U].percentage;
|
||||
int upper_pct = s_discharge_curve[i].percentage;
|
||||
return (uint8_t)(lower_pct +
|
||||
(voltage_mv - lower_mv) * (upper_pct - lower_pct) /
|
||||
(upper_mv - lower_mv));
|
||||
}
|
||||
}
|
||||
return 100U;
|
||||
}
|
||||
|
||||
static bool accept_voltage_step(int voltage_mv)
|
||||
{
|
||||
if (!s_filter_initialized || abs(voltage_mv - s_filtered_mv) <= BATTERY_MAX_ACCEPTED_STEP_MV) {
|
||||
s_jump_candidate_count = 0U;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (s_jump_candidate_count == 0U ||
|
||||
abs(voltage_mv - s_jump_candidate_mv) > BATTERY_JUMP_MATCH_WINDOW_MV) {
|
||||
s_jump_candidate_mv = voltage_mv;
|
||||
s_jump_candidate_count = 1U;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s_jump_candidate_count < BATTERY_STATE_CONFIRM_SAMPLES) {
|
||||
++s_jump_candidate_count;
|
||||
}
|
||||
if (s_jump_candidate_count < BATTERY_STATE_CONFIRM_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
s_jump_candidate_count = 0U;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void update_level_state(uint8_t percentage,
|
||||
domain_battery_charge_state_t charge_state)
|
||||
{
|
||||
if (charge_state == DOMAIN_BATTERY_CHARGE_CHARGING &&
|
||||
s_status.level_state == DOMAIN_BATTERY_LEVEL_CRITICAL) {
|
||||
s_status.level_state = DOMAIN_BATTERY_LEVEL_LOW;
|
||||
s_critical_count = 0U;
|
||||
}
|
||||
|
||||
switch (s_status.level_state) {
|
||||
case DOMAIN_BATTERY_LEVEL_UNKNOWN:
|
||||
case DOMAIN_BATTERY_LEVEL_NORMAL:
|
||||
s_status.level_state = DOMAIN_BATTERY_LEVEL_NORMAL;
|
||||
if (percentage <= CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT) {
|
||||
if (++s_low_count >= BATTERY_STATE_CONFIRM_SAMPLES) {
|
||||
s_status.level_state = DOMAIN_BATTERY_LEVEL_LOW;
|
||||
s_low_count = 0U;
|
||||
}
|
||||
} else {
|
||||
s_low_count = 0U;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOMAIN_BATTERY_LEVEL_LOW:
|
||||
if (percentage >= CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT) {
|
||||
s_status.level_state = DOMAIN_BATTERY_LEVEL_NORMAL;
|
||||
s_critical_count = 0U;
|
||||
} else if (charge_state == DOMAIN_BATTERY_CHARGE_CHARGING) {
|
||||
s_critical_count = 0U;
|
||||
} else if (percentage <= CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT) {
|
||||
if (++s_critical_count >= BATTERY_STATE_CONFIRM_SAMPLES) {
|
||||
s_status.level_state = DOMAIN_BATTERY_LEVEL_CRITICAL;
|
||||
s_critical_count = 0U;
|
||||
}
|
||||
} else {
|
||||
s_critical_count = 0U;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOMAIN_BATTERY_LEVEL_CRITICAL:
|
||||
if (charge_state == DOMAIN_BATTERY_CHARGE_CHARGING ||
|
||||
percentage >= CONFIG_TQ_BATTERY_CRITICAL_EXIT_PERCENT) {
|
||||
s_status.level_state = percentage >= CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT
|
||||
? DOMAIN_BATTERY_LEVEL_NORMAL
|
||||
: DOMAIN_BATTERY_LEVEL_LOW;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t domain_battery_init(void)
|
||||
{
|
||||
if (!lock_state()) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
if (s_initialized) {
|
||||
unlock_state();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t err = platform_battery_init();
|
||||
if (err != ESP_OK) {
|
||||
unlock_state();
|
||||
return err;
|
||||
}
|
||||
|
||||
memset(&s_status, 0, sizeof(s_status));
|
||||
s_status.charge_state = DOMAIN_BATTERY_CHARGE_UNKNOWN;
|
||||
s_status.level_state = DOMAIN_BATTERY_LEVEL_UNKNOWN;
|
||||
s_initialized = true;
|
||||
unlock_state();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t domain_battery_sample(void)
|
||||
{
|
||||
esp_err_t err = domain_battery_init();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
platform_battery_sample_t sample = {0};
|
||||
err = platform_battery_read_sample(&sample);
|
||||
if (!lock_state()) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
s_status.latest_sample_valid = false;
|
||||
if (s_status.consecutive_failures < UINT32_MAX) {
|
||||
++s_status.consecutive_failures;
|
||||
}
|
||||
if (s_status.consecutive_failures >= BATTERY_STATE_CONFIRM_SAMPLES) {
|
||||
s_status.valid = false;
|
||||
}
|
||||
unlock_state();
|
||||
return err;
|
||||
}
|
||||
|
||||
s_status.latest_adc_mv = sample.adc_voltage_mv;
|
||||
s_status.latest_sample_mv = sample.voltage_mv;
|
||||
s_status.latest_sample_valid = sample.voltage_valid;
|
||||
s_status.charge_state = map_charge_state(sample.charge_state);
|
||||
s_status.high_load_active = sample.high_load_active;
|
||||
s_status.cooldown_active = sample.cooldown_active;
|
||||
s_status.measurement_suppressed = sample.high_load_active || sample.cooldown_active;
|
||||
if (s_status.measurement_suppressed) {
|
||||
unlock_state();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (!sample.voltage_valid) {
|
||||
if (s_status.consecutive_failures < UINT32_MAX) {
|
||||
++s_status.consecutive_failures;
|
||||
}
|
||||
if (s_status.consecutive_failures >= BATTERY_STATE_CONFIRM_SAMPLES) {
|
||||
s_status.valid = false;
|
||||
}
|
||||
unlock_state();
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
if (!accept_voltage_step(sample.voltage_mv)) {
|
||||
unlock_state();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (!s_filter_initialized) {
|
||||
s_filtered_mv = sample.voltage_mv;
|
||||
s_filter_initialized = true;
|
||||
} else {
|
||||
s_filtered_mv = (s_filtered_mv * BATTERY_FILTER_OLD_WEIGHT +
|
||||
sample.voltage_mv * BATTERY_FILTER_NEW_WEIGHT) /
|
||||
(BATTERY_FILTER_OLD_WEIGHT + BATTERY_FILTER_NEW_WEIGHT);
|
||||
}
|
||||
|
||||
uint8_t percentage = voltage_to_percentage(s_filtered_mv);
|
||||
if (s_status.valid) {
|
||||
if (s_status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING &&
|
||||
percentage < s_status.percentage) {
|
||||
percentage = s_status.percentage;
|
||||
} else if (s_status.charge_state != DOMAIN_BATTERY_CHARGE_CHARGING &&
|
||||
percentage > s_status.percentage) {
|
||||
percentage = s_status.percentage;
|
||||
}
|
||||
|
||||
if (percentage > s_status.percentage + 2U) {
|
||||
percentage = s_status.percentage + 2U;
|
||||
} else if (s_status.percentage > percentage + 2U) {
|
||||
percentage = s_status.percentage - 2U;
|
||||
}
|
||||
}
|
||||
|
||||
s_status.voltage_mv = s_filtered_mv;
|
||||
s_status.percentage = percentage;
|
||||
s_status.valid = true;
|
||||
s_status.measurement_suppressed = false;
|
||||
s_status.consecutive_failures = 0U;
|
||||
s_status.last_valid_sample_time_ms = esp_timer_get_time() / 1000;
|
||||
update_level_state(percentage, s_status.charge_state);
|
||||
unlock_state();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status)
|
||||
{
|
||||
if (out_status == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (!lock_state()) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
if (!s_initialized) {
|
||||
unlock_state();
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
*out_status = s_status;
|
||||
unlock_state();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t domain_battery_prepare_shutdown(void)
|
||||
{
|
||||
/* Reserved for a future board-specific sleep or power-cut sequence. */
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
esp_err_t domain_battery_init(void)
|
||||
{
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t domain_battery_sample(void)
|
||||
{
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status)
|
||||
{
|
||||
if (out_status != NULL) {
|
||||
memset(out_status, 0, sizeof(*out_status));
|
||||
}
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t domain_battery_prepare_shutdown(void)
|
||||
{
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1318,6 +1318,22 @@ esp_err_t domain_runtime_stop_lcd_render_loop(int timeout_ms)
|
||||
return platform_stop_lcd_render_loop(timeout_ms);
|
||||
}
|
||||
|
||||
esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *status)
|
||||
{
|
||||
if (status == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const platform_lcd_status_overlay_t overlay = {
|
||||
.low_battery_visible = status->valid &&
|
||||
(status->level_state == DOMAIN_BATTERY_LEVEL_LOW ||
|
||||
status->level_state == DOMAIN_BATTERY_LEVEL_CRITICAL),
|
||||
.charging_visible =
|
||||
status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING,
|
||||
};
|
||||
return platform_lcd_set_status_overlay(&overlay);
|
||||
}
|
||||
|
||||
static const domain_expression_catalog_entry_t *domain_find_expression_entry(domain_expression_id_t id)
|
||||
{
|
||||
for (size_t i = 0; i < sizeof(s_expression_catalog) / sizeof(s_expression_catalog[0]); ++i) {
|
||||
|
||||
Reference in New Issue
Block a user