feat(power): add battery monitoring and status overlays

This commit is contained in:
chenjintao
2026-08-18 11:53:14 +08:00
parent 609fe978f1
commit 8f8254b397
31 changed files with 1655 additions and 22 deletions
@@ -0,0 +1,426 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "control_plane_internal.h"
#include "domain.h"
#include "driver/gpio.h"
#include "esp_attr.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#if CONFIG_TQ_BATTERY_ENABLE
#define BATTERY_WORKER_STACK_BYTES 4096
#define BATTERY_WORKER_PRIORITY 2
#define BATTERY_SUPPRESSED_RETRY_MS 1000
#define BATTERY_STOP_POLL_MS 10
#define BATTERY_CHARGE_DEBOUNCE_MS 30
#define BATTERY_FULL_CONFIRM_RETRY_MS 1000
#define BATTERY_FULL_CONFIRM_TIME_MS 5000
#define BATTERY_FULL_CONFIRM_SAMPLES 3U
#define BATTERY_FULL_MIN_VOLTAGE_MV 4050
#define BATTERY_LOW_PROMPT_FILE "/littlefs/audio_prompts/battery_low_20.opus"
#define BATTERY_FULL_PROMPT_FILE "/littlefs/audio_prompts/charge_complete.opus"
static const char *TAG = "cp_battery";
static TaskHandle_t s_battery_task;
static bool s_stop_requested;
static bool s_starting;
static bool s_charge_gpio_isr_registered;
static portMUX_TYPE s_battery_lock = portMUX_INITIALIZER_UNLOCKED;
static void IRAM_ATTR battery_charge_gpio_isr(void *arg)
{
(void)arg;
TaskHandle_t task = s_battery_task;
if (task == NULL) {
return;
}
BaseType_t higher_priority_task_woken = pdFALSE;
vTaskNotifyGiveFromISR(task, &higher_priority_task_woken);
if (higher_priority_task_woken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
static esp_err_t battery_charge_interrupt_start(void)
{
if (CONFIG_TQ_BATTERY_CHARGE_GPIO < 0) {
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t err = gpio_install_isr_service(0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
return err;
}
err = gpio_set_intr_type((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
GPIO_INTR_ANYEDGE);
if (err != ESP_OK) {
return err;
}
err = gpio_isr_handler_add((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
battery_charge_gpio_isr,
NULL);
if (err != ESP_OK) {
(void)gpio_set_intr_type((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
GPIO_INTR_DISABLE);
return err;
}
s_charge_gpio_isr_registered = true;
return ESP_OK;
}
static void battery_charge_interrupt_stop(void)
{
if (!s_charge_gpio_isr_registered || CONFIG_TQ_BATTERY_CHARGE_GPIO < 0) {
return;
}
(void)gpio_intr_disable((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO);
(void)gpio_isr_handler_remove((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO);
(void)gpio_set_intr_type((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO,
GPIO_INTR_DISABLE);
s_charge_gpio_isr_registered = false;
}
static const char *charge_state_name(domain_battery_charge_state_t state)
{
switch (state) {
case DOMAIN_BATTERY_CHARGE_CHARGING:
return "charging";
case DOMAIN_BATTERY_CHARGE_NOT_CHARGING:
return "not_charging";
default:
return "unknown";
}
}
static const char *level_state_name(domain_battery_level_state_t state)
{
switch (state) {
case DOMAIN_BATTERY_LEVEL_NORMAL:
return "normal";
case DOMAIN_BATTERY_LEVEL_LOW:
return "low";
case DOMAIN_BATTERY_LEVEL_CRITICAL:
return "critical";
default:
return "unknown";
}
}
static bool battery_stop_requested(void)
{
bool requested;
portENTER_CRITICAL(&s_battery_lock);
requested = s_stop_requested;
portEXIT_CRITICAL(&s_battery_lock);
return requested;
}
static void battery_worker_finish(void)
{
portENTER_CRITICAL(&s_battery_lock);
s_battery_task = NULL;
s_stop_requested = false;
s_starting = false;
portEXIT_CRITICAL(&s_battery_lock);
}
static void control_plane_battery_critical_hook(
const domain_battery_status_t *status)
{
(void)status;
/*
* Reserved for a future product decision. Do not stop actuators, enter
* sleep, or cut board power in this release.
*/
}
static void control_plane_battery_low_hook(
const domain_battery_status_t *status)
{
if (status == NULL) {
return;
}
ESP_LOGI(TAG,
"Low-battery prompt start level=%u%% file=%s",
(unsigned)status->percentage,
BATTERY_LOW_PROMPT_FILE);
esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file(
BATTERY_LOW_PROMPT_FILE);
if (prompt_err != ESP_OK) {
ESP_LOGW(TAG,
"Low-battery prompt failed: %s",
esp_err_to_name(prompt_err));
}
}
static void control_plane_battery_full_hint_hook(
const domain_battery_status_t *status)
{
if (status == NULL) {
return;
}
ESP_LOGI(TAG,
"Inferred charge-complete prompt start voltage=%dmV file=%s",
status->voltage_mv,
BATTERY_FULL_PROMPT_FILE);
esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file(
BATTERY_FULL_PROMPT_FILE);
if (prompt_err != ESP_OK) {
ESP_LOGW(TAG,
"Charge-complete prompt failed: %s",
esp_err_to_name(prompt_err));
}
}
static void battery_worker(void *arg)
{
(void)arg;
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
domain_battery_level_state_t previous_level = DOMAIN_BATTERY_LEVEL_UNKNOWN;
domain_battery_charge_state_t previous_charge = DOMAIN_BATTERY_CHARGE_UNKNOWN;
bool full_hint_candidate = false;
TickType_t full_hint_started_at = 0;
uint8_t full_hint_confirmations = 0U;
esp_err_t init_err = domain_battery_init();
if (init_err != ESP_OK) {
ESP_LOGE(TAG, "Battery service initialization failed: %s", esp_err_to_name(init_err));
battery_worker_finish();
vTaskDelete(NULL);
return;
}
esp_err_t interrupt_err = battery_charge_interrupt_start();
if (interrupt_err == ESP_OK) {
ESP_LOGI(TAG,
"TP4057 charge-state interrupt ready gpio=%d debounce=%dms",
CONFIG_TQ_BATTERY_CHARGE_GPIO,
BATTERY_CHARGE_DEBOUNCE_MS);
} else if (interrupt_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG,
"TP4057 charge-state interrupt unavailable; using polling: %s",
esp_err_to_name(interrupt_err));
}
ESP_LOGI(TAG, "Battery monitoring worker started");
while (!battery_stop_requested()) {
esp_err_t sample_err = domain_battery_sample();
domain_battery_status_t status = {0};
esp_err_t status_err = domain_battery_get_status(&status);
if (sample_err != ESP_OK) {
ESP_LOGW(TAG, "Battery sample rejected: %s", esp_err_to_name(sample_err));
}
if (status_err == ESP_OK) {
esp_err_t overlay_err = domain_runtime_update_battery_overlay(&status);
if (overlay_err != ESP_OK && overlay_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGD(TAG, "Battery LCD overlay update skipped: %s", esp_err_to_name(overlay_err));
}
if (status.valid && !status.measurement_suppressed) {
ESP_LOGI(TAG,
"Battery adc=%dmV voltage=%dmV level=%u%% charge=%s state=%s",
status.latest_adc_mv,
status.voltage_mv,
(unsigned)status.percentage,
charge_state_name(status.charge_state),
level_state_name(status.level_state));
} else if (status.measurement_suppressed) {
ESP_LOGD(TAG,
"Battery SOC update suppressed adc=%dmV raw=%dmV valid=%d load=%d cooldown=%d",
status.latest_adc_mv,
status.latest_sample_mv,
status.latest_sample_valid,
status.high_load_active,
status.cooldown_active);
}
if (status.level_state != previous_level) {
ESP_LOGW(TAG,
"Battery level state changed: %s -> %s",
level_state_name(previous_level),
level_state_name(status.level_state));
if (status.level_state == DOMAIN_BATTERY_LEVEL_LOW &&
(previous_level == DOMAIN_BATTERY_LEVEL_NORMAL ||
previous_level == DOMAIN_BATTERY_LEVEL_UNKNOWN)) {
control_plane_battery_low_hook(&status);
}
if (status.level_state == DOMAIN_BATTERY_LEVEL_CRITICAL) {
control_plane_battery_critical_hook(&status);
}
previous_level = status.level_state;
}
if (status.charge_state != previous_charge) {
ESP_LOGI(TAG,
"Battery charge state changed: %s -> %s",
charge_state_name(previous_charge),
charge_state_name(status.charge_state));
if (previous_charge == DOMAIN_BATTERY_CHARGE_CHARGING &&
status.charge_state == DOMAIN_BATTERY_CHARGE_NOT_CHARGING) {
/*
* TP4057 exposes the same high level for charge termination
* and charger removal. Voltage can only make this a useful
* UI/audio hint, not a rigorous full-charge measurement.
* Never use this inference for safety, shutdown, or power
* switching decisions.
*/
full_hint_candidate = true;
full_hint_started_at = xTaskGetTickCount();
full_hint_confirmations = 0U;
} else if (status.charge_state != DOMAIN_BATTERY_CHARGE_NOT_CHARGING) {
full_hint_candidate = false;
full_hint_confirmations = 0U;
}
previous_charge = status.charge_state;
}
if (full_hint_candidate && status.valid &&
!status.measurement_suppressed) {
if (status.charge_state != DOMAIN_BATTERY_CHARGE_NOT_CHARGING ||
status.voltage_mv < BATTERY_FULL_MIN_VOLTAGE_MV) {
ESP_LOGI(TAG,
"Charge-complete inference cancelled charge=%s voltage=%dmV",
charge_state_name(status.charge_state),
status.voltage_mv);
full_hint_candidate = false;
full_hint_confirmations = 0U;
} else {
if (full_hint_confirmations < UINT8_MAX) {
++full_hint_confirmations;
}
TickType_t elapsed_ticks =
xTaskGetTickCount() - full_hint_started_at;
if (full_hint_confirmations >= BATTERY_FULL_CONFIRM_SAMPLES &&
elapsed_ticks >= pdMS_TO_TICKS(BATTERY_FULL_CONFIRM_TIME_MS)) {
control_plane_battery_full_hint_hook(&status);
full_hint_candidate = false;
full_hint_confirmations = 0U;
}
}
}
}
uint32_t delay_ms;
if (status_err == ESP_OK && status.measurement_suppressed) {
delay_ms = BATTERY_SUPPRESSED_RETRY_MS;
} else if (full_hint_candidate) {
delay_ms = BATTERY_FULL_CONFIRM_RETRY_MS;
} else {
delay_ms = (uint32_t)CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC * 1000U;
}
uint32_t notifications = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms));
if (notifications > 0U && !battery_stop_requested()) {
vTaskDelay(pdMS_TO_TICKS(BATTERY_CHARGE_DEBOUNCE_MS));
/* Collapse contact bounce into one fresh read of the final level. */
(void)ulTaskNotifyTake(pdTRUE, 0U);
}
}
battery_charge_interrupt_stop();
ESP_LOGI(TAG, "Battery monitoring worker stopped");
battery_worker_finish();
vTaskDelete(NULL);
}
esp_err_t control_plane_battery_service_start(void)
{
portENTER_CRITICAL(&s_battery_lock);
if (s_battery_task != NULL || s_starting) {
portEXIT_CRITICAL(&s_battery_lock);
return ESP_OK;
}
s_stop_requested = false;
s_starting = true;
portEXIT_CRITICAL(&s_battery_lock);
TaskHandle_t task = NULL;
BaseType_t created = xTaskCreate(battery_worker,
"cp_battery",
BATTERY_WORKER_STACK_BYTES,
NULL,
BATTERY_WORKER_PRIORITY,
&task);
if (created != pdPASS || task == NULL) {
portENTER_CRITICAL(&s_battery_lock);
s_starting = false;
portEXIT_CRITICAL(&s_battery_lock);
return ESP_ERR_NO_MEM;
}
portENTER_CRITICAL(&s_battery_lock);
s_battery_task = task;
s_starting = false;
portEXIT_CRITICAL(&s_battery_lock);
xTaskNotifyGive(task);
return ESP_OK;
}
esp_err_t control_plane_battery_service_stop(int timeout_ms)
{
if (timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
TaskHandle_t task;
portENTER_CRITICAL(&s_battery_lock);
task = s_battery_task;
s_stop_requested = task != NULL;
portEXIT_CRITICAL(&s_battery_lock);
if (task == NULL) {
return ESP_OK;
}
xTaskNotifyGive(task);
int waited_ms = 0;
while (waited_ms < timeout_ms) {
portENTER_CRITICAL(&s_battery_lock);
task = s_battery_task;
portEXIT_CRITICAL(&s_battery_lock);
if (task == NULL) {
return ESP_OK;
}
vTaskDelay(pdMS_TO_TICKS(BATTERY_STOP_POLL_MS));
waited_ms += BATTERY_STOP_POLL_MS;
}
portENTER_CRITICAL(&s_battery_lock);
task = s_battery_task;
s_battery_task = NULL;
s_stop_requested = false;
portEXIT_CRITICAL(&s_battery_lock);
if (task != NULL) {
ESP_LOGW(TAG, "Battery worker stop timeout; forcing teardown");
vTaskDelete(task);
battery_charge_interrupt_stop();
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
#else
esp_err_t control_plane_battery_service_start(void)
{
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t control_plane_battery_service_stop(int timeout_ms)
{
return timeout_ms < 0 ? ESP_ERR_INVALID_ARG : ESP_OK;
}
#endif
@@ -2264,6 +2264,11 @@ esp_err_t control_plane_start_runtime_workers(void)
return queue_err;
}
esp_err_t battery_err = control_plane_battery_service_start();
if (battery_err != ESP_OK && battery_err != ESP_ERR_NOT_SUPPORTED) {
ESP_LOGW(TAG, "Battery service start failed: %s", esp_err_to_name(battery_err));
}
if (control_plane_get_task_handle(&s_model_cmd_task) == NULL) {
esp_err_t task_err = control_plane_create_task_psram_first(control_plane_model_command_task,
"cp_model_cmd",
@@ -2412,6 +2417,7 @@ void control_plane_stop_event_observer_runtime(void)
void control_plane_stop_runtime_workers(void)
{
control_plane_set_runtime_stop_requested(true);
(void)control_plane_battery_service_stop(CONTROL_PLANE_TASK_STOP_WAIT_MS);
(void)control_plane_stop_ota_service(CONFIG_TQ_LOCAL_WAKE_STOP_WAIT_MS);
(void)control_plane_stop_mini_program_provisioning(
CONTROL_PLANE_TASK_STOP_WAIT_MS);