feat(power): add battery monitoring and status overlays
This commit is contained in:
@@ -46,6 +46,7 @@ The following matrix is authoritative for all new and refactored cross-layer con
|
||||
| Connectivity semantic prompt IDs/events (connecting/success/fail/config) | `domain` | `components/domain/include/domain.h` | `platform` must not expose duplicated semantic enums in public facade; map semantics in adapters. |
|
||||
| Mini-program Wi-Fi provisioning semantics (TalkingQ BLE discovery/GATT schema, credential events, status/device-info payloads) | `domain` | `components/domain/include/domain.h` | `platform` exposes generic BLE GATT server primitives only; `control_plane` owns Wi-Fi connection policy and worker lifecycle. |
|
||||
| Hardware/runtime primitive descriptors (GPIO digital input config, low-level actuator primitives, storage identity/blob KV APIs, wall-clock sync API) | `platform` | `components/platform/include/platform.h` | Upper layers call through owner facade or owner-backed adapters; no semantic redefinition in non-owner public headers. |
|
||||
| Battery sensing and battery-state policy (`platform_battery_*`, `domain_battery_*`) | `domain` for SOC/level semantics; `platform` for ADC, charger GPIO, and load-quality primitives | `components/domain/include/domain.h` for semantic state; `components/platform/include/platform.h` for hardware samples | `control_plane` consumes domain status only; platform load tracking stays internal and upper layers must not infer SOC from raw ADC values. |
|
||||
| LAN HTTP transport contract (bounded request/response structs, handler delegate, server lifecycle/state) | `platform` | `components/platform/include/platform.h` | `domain` owns route semantics and maps them in implementation; upper layers must not expose platform HTTP types. |
|
||||
| Multimodal session transport contract exposed to orchestration (connected/disconnected/error/text/binary event family, audio uplink callback family) | `domain` | `components/domain/include/domain.h` | `platform` internal transport types stay internal; cross-layer public contract is declared once at `domain` facade. |
|
||||
| Device-auth persistence port (opaque auth key/state record slots and legacy migration targets) | `domain` | `components/domain/include/domain.h` | `control_plane` owns serialized auth record schemas and consumes typed auth slots through `domain`; `platform` remains an opaque blob KV owner only. |
|
||||
|
||||
@@ -68,7 +68,7 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void)
|
||||
{
|
||||
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_POWER_HOLD,
|
||||
"power_hold",
|
||||
"already_asserted_by_platform_core");
|
||||
"configured_or_disabled_by_platform_core");
|
||||
|
||||
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_STORAGE_PREPARE,
|
||||
"storage_prepare",
|
||||
@@ -139,6 +139,13 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void)
|
||||
return 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,
|
||||
"Early battery service start failed, continue boot: %s",
|
||||
esp_err_to_name(battery_err));
|
||||
}
|
||||
|
||||
#if CONFIG_TQ_FACTORY_CLI_ENABLE
|
||||
err = control_plane_factory_cli_start();
|
||||
if (err != ESP_OK) {
|
||||
|
||||
@@ -18,6 +18,7 @@ idf_component_register(
|
||||
"src/runtime/control_plane_runtime_workers.c"
|
||||
"src/runtime/control_plane_ota.c"
|
||||
"src/runtime/control_plane_factory_cli.c"
|
||||
"src/runtime/control_plane_battery_service.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
PRIV_INCLUDE_DIRS
|
||||
|
||||
@@ -88,6 +88,15 @@ typedef struct {
|
||||
|
||||
esp_err_t control_plane_init(void);
|
||||
|
||||
/**
|
||||
* @brief Start battery sensing and charge-status presentation.
|
||||
*
|
||||
* Safe to call repeatedly. This service is intentionally started before
|
||||
* connectivity bootstrap so battery and charger indications do not depend on
|
||||
* Wi-Fi, provisioning, or server readiness.
|
||||
*/
|
||||
esp_err_t control_plane_battery_service_start(void);
|
||||
|
||||
/**
|
||||
* @brief Prepare platform-backed persistent storage before runtime workers start.
|
||||
*
|
||||
|
||||
@@ -147,6 +147,8 @@ bool control_plane_voice_turn_is_idle(void);
|
||||
bool control_plane_ota_voice_services_allowed(void);
|
||||
bool control_plane_runtime_is_stopping(void);
|
||||
|
||||
esp_err_t control_plane_battery_service_stop(int timeout_ms);
|
||||
|
||||
esp_err_t control_plane_start_system_event_task(void);
|
||||
void control_plane_stop_system_event_task(void);
|
||||
esp_err_t control_plane_post_system_event(const control_plane_system_event_t *event, uint32_t timeout_ms);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,6 +7,7 @@ idf_component_register(
|
||||
"src/calibration/domain_calibration_service.c"
|
||||
"src/fortune/domain_fortune_store.c"
|
||||
"src/fortune/domain_user_config_server.c"
|
||||
"src/battery/domain_battery_service.c"
|
||||
"src/runtime/domain_runtime_adapters.c"
|
||||
"src/runtime/domain_ota.c"
|
||||
"src/runtime/domain_local_command.c"
|
||||
@@ -17,6 +18,8 @@ idf_component_register(
|
||||
"internal"
|
||||
PRIV_REQUIRES
|
||||
platform
|
||||
esp_timer
|
||||
freertos
|
||||
esp-sr
|
||||
log
|
||||
json
|
||||
|
||||
@@ -29,6 +29,47 @@ typedef struct {
|
||||
int settle_delay_ms;
|
||||
} domain_direction_calibration_t;
|
||||
|
||||
typedef enum {
|
||||
DOMAIN_BATTERY_CHARGE_UNKNOWN = 0,
|
||||
DOMAIN_BATTERY_CHARGE_NOT_CHARGING,
|
||||
DOMAIN_BATTERY_CHARGE_CHARGING,
|
||||
} domain_battery_charge_state_t;
|
||||
|
||||
typedef enum {
|
||||
DOMAIN_BATTERY_LEVEL_UNKNOWN = 0,
|
||||
DOMAIN_BATTERY_LEVEL_NORMAL,
|
||||
DOMAIN_BATTERY_LEVEL_LOW,
|
||||
DOMAIN_BATTERY_LEVEL_CRITICAL,
|
||||
} domain_battery_level_state_t;
|
||||
|
||||
typedef struct {
|
||||
int voltage_mv;
|
||||
int latest_adc_mv;
|
||||
int latest_sample_mv;
|
||||
uint8_t percentage;
|
||||
domain_battery_charge_state_t charge_state;
|
||||
domain_battery_level_state_t level_state;
|
||||
bool valid;
|
||||
bool latest_sample_valid;
|
||||
bool measurement_suppressed;
|
||||
bool high_load_active;
|
||||
bool cooldown_active;
|
||||
uint32_t consecutive_failures;
|
||||
int64_t last_valid_sample_time_ms;
|
||||
} domain_battery_status_t;
|
||||
|
||||
/** Initialize the battery measurement domain. Idempotent and thread-safe. */
|
||||
esp_err_t domain_battery_init(void);
|
||||
|
||||
/** Acquire and process one battery sample without applying product policy. */
|
||||
esp_err_t domain_battery_sample(void);
|
||||
|
||||
/** Copy the latest battery SSOT snapshot into `out_status`. */
|
||||
esp_err_t domain_battery_get_status(domain_battery_status_t *out_status);
|
||||
|
||||
/** Reserved policy hook; this release intentionally performs no shutdown. */
|
||||
esp_err_t domain_battery_prepare_shutdown(void);
|
||||
|
||||
typedef enum {
|
||||
DOMAIN_WIFI_PROMPT_WIFI_SUCCESS = 0,
|
||||
DOMAIN_WIFI_PROMPT_AFTER_WIFI_SUCCESS,
|
||||
@@ -560,6 +601,10 @@ void domain_runtime_start_lcd_render_loop(void);
|
||||
* values wait up to the bound before lower layers apply deterministic fallback.
|
||||
*/
|
||||
esp_err_t domain_runtime_stop_lcd_render_loop(int timeout_ms);
|
||||
|
||||
/** Map the domain battery snapshot to persistent LCD status overlays. */
|
||||
esp_err_t domain_runtime_update_battery_overlay(const domain_battery_status_t *status);
|
||||
|
||||
esp_err_t domain_runtime_request_expression(const domain_expression_request_t *request);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -11,6 +11,7 @@ set(platform_srcs
|
||||
"src/runtime/platform_multimodal_runtime.c"
|
||||
"src/runtime/platform_lcd_runtime.c"
|
||||
"src/platform_stepper_driver.c"
|
||||
"src/platform_battery_driver.c"
|
||||
"src/platform_digital_input_driver.c"
|
||||
"src/platform_io_effects.c"
|
||||
"src/lcd_direct.c"
|
||||
@@ -30,6 +31,7 @@ set(platform_priv_requires
|
||||
cxx
|
||||
driver
|
||||
esp_audio_codec
|
||||
esp_adc
|
||||
esp_codec_dev
|
||||
esp_driver_gpio
|
||||
esp_driver_i2c
|
||||
@@ -140,6 +142,8 @@ set(platform_lcd_png_assets
|
||||
icon_huo0
|
||||
icon_tu0
|
||||
icon_jin0
|
||||
charging
|
||||
lowbat
|
||||
qrcode
|
||||
)
|
||||
set(platform_lcd_png_asset_files)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -302,6 +302,33 @@ esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle,
|
||||
int target_delay_us,
|
||||
int timeout_ms);
|
||||
void platform_stepper_power_off(void);
|
||||
|
||||
typedef enum {
|
||||
PLATFORM_BATTERY_CHARGE_UNKNOWN = 0,
|
||||
PLATFORM_BATTERY_CHARGE_NOT_CHARGING,
|
||||
PLATFORM_BATTERY_CHARGE_CHARGING,
|
||||
} platform_battery_charge_state_t;
|
||||
|
||||
typedef struct {
|
||||
int adc_voltage_mv;
|
||||
int voltage_mv;
|
||||
platform_battery_charge_state_t charge_state;
|
||||
bool voltage_valid;
|
||||
bool high_load_active;
|
||||
bool cooldown_active;
|
||||
} platform_battery_sample_t;
|
||||
|
||||
/** Initialize the board battery ADC and TP4057 status input. Idempotent. */
|
||||
esp_err_t platform_battery_init(void);
|
||||
|
||||
/**
|
||||
* Read one trimmed-mean battery voltage sample and a load-quality snapshot.
|
||||
* A successful call can still return voltage_valid=false for an implausible
|
||||
* electrical reading; callers must not interpret such a sample as an empty
|
||||
* battery.
|
||||
*/
|
||||
esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample);
|
||||
|
||||
esp_err_t platform_configure_digital_input(const platform_digital_input_config_t *config);
|
||||
bool platform_is_digital_input_active(const platform_digital_input_config_t *config);
|
||||
|
||||
@@ -480,6 +507,19 @@ typedef struct {
|
||||
size_t step_count;
|
||||
} platform_lcd_presentation_request_t;
|
||||
|
||||
typedef struct {
|
||||
bool low_battery_visible;
|
||||
bool charging_visible;
|
||||
} platform_lcd_status_overlay_t;
|
||||
|
||||
/**
|
||||
* @brief Set persistent LCD status overlays composited over presentation frames.
|
||||
*
|
||||
* The state is copied synchronously and remains active until replaced. The
|
||||
* render task owns all PNG decoding and panel access.
|
||||
*/
|
||||
esp_err_t platform_lcd_set_status_overlay(const platform_lcd_status_overlay_t *overlay);
|
||||
|
||||
/**
|
||||
* @brief Request a LCD presentation playback.
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ extern "C" {
|
||||
esp_err_t lcd_direct_init_device(void);
|
||||
void lcd_direct_start_render_loop(void);
|
||||
esp_err_t lcd_direct_stop_render_loop(int timeout_ms);
|
||||
esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *overlay);
|
||||
esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request);
|
||||
esp_err_t lcd_direct_preheat_presentation(const platform_lcd_presentation_request_t *request);
|
||||
uint32_t lcd_direct_presentation_completion_count(void);
|
||||
|
||||
@@ -23,6 +23,8 @@ typedef struct {
|
||||
|
||||
#define LCD_DIRECT_NO_ICON 0xFFFFU
|
||||
#define LCD_DIRECT_DEFAULT_ICON_COUNT 5U
|
||||
#define LCD_DIRECT_ICON_CHARGING 5U
|
||||
#define LCD_DIRECT_ICON_LOW_BATTERY 6U
|
||||
|
||||
const lcd_direct_image_t *lcd_direct_default_image(void);
|
||||
const lcd_direct_image_t *lcd_direct_asset_frame(const char *asset_dir, uint8_t frame_index);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
PLATFORM_BATTERY_LOAD_AUDIO = 0,
|
||||
PLATFORM_BATTERY_LOAD_STEPPER,
|
||||
PLATFORM_BATTERY_LOAD_COUNT,
|
||||
} platform_battery_load_source_t;
|
||||
|
||||
void platform_battery_load_begin(platform_battery_load_source_t source);
|
||||
void platform_battery_load_end(platform_battery_load_source_t source);
|
||||
@@ -46,8 +46,15 @@
|
||||
#define LCD_DIRECT_PRESENTATION_WAIT_POLL_MS 20U
|
||||
#define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100U
|
||||
#define LCD_DIRECT_PREHEAT_WAIT_TIMEOUT_MS 5000U
|
||||
#define LCD_DIRECT_FRAME_CACHE_MAX 4U
|
||||
#define LCD_DIRECT_FRAME_CACHE_MAX 6U
|
||||
#define LCD_DIRECT_PATH_MAX 96U
|
||||
#define LCD_DIRECT_CHARGING_ICON_WIDTH 50U
|
||||
#define LCD_DIRECT_CHARGING_ICON_X (LCD_DIRECT_WIDTH - LCD_DIRECT_CHARGING_ICON_WIDTH - 8U)
|
||||
#define LCD_DIRECT_CHARGING_ICON_Y 8U
|
||||
#define LCD_DIRECT_LOW_BATTERY_ICON_WIDTH 232U
|
||||
#define LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT 36U
|
||||
#define LCD_DIRECT_LOW_BATTERY_ICON_X ((LCD_DIRECT_WIDTH - LCD_DIRECT_LOW_BATTERY_ICON_WIDTH) / 2U)
|
||||
#define LCD_DIRECT_LOW_BATTERY_ICON_Y (LCD_DIRECT_HEIGHT - LCD_DIRECT_LOW_BATTERY_ICON_HEIGHT - 8U)
|
||||
#define LCD_DIRECT_LAST_FRAME_EXTRA_HOLD_MS 200U
|
||||
#define LCD_DIRECT_BASIC_FACE_FRAME_HOLD_MS 100U
|
||||
#define LCD_DIRECT_BASIC_FACE_LAST_FRAME_HOLD_MS 100U
|
||||
@@ -135,6 +142,7 @@ static bool s_render_loop_running;
|
||||
static bool s_stop_requested;
|
||||
static bool s_pending_valid;
|
||||
static lcd_direct_request_t s_pending;
|
||||
static platform_lcd_status_overlay_t s_status_overlay;
|
||||
static bool s_preheat_pending_valid;
|
||||
static bool s_preheat_in_progress;
|
||||
static bool s_preheat_waiter_active;
|
||||
@@ -683,6 +691,38 @@ static void lcd_direct_release_prepared_icons(lcd_direct_prepared_icon_t *prepar
|
||||
}
|
||||
}
|
||||
|
||||
static size_t lcd_direct_append_status_icons(const lcd_direct_image_t *image,
|
||||
lcd_direct_icon_draw_t *icons,
|
||||
size_t icon_count,
|
||||
size_t icon_capacity)
|
||||
{
|
||||
if (image == NULL || image->path == NULL || icons == NULL ||
|
||||
icon_count >= icon_capacity || strstr(image->path, "/qrcode.png") != NULL) {
|
||||
return icon_count;
|
||||
}
|
||||
|
||||
platform_lcd_status_overlay_t overlay = {0};
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
overlay = s_status_overlay;
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
|
||||
if (overlay.low_battery_visible && icon_count < icon_capacity) {
|
||||
icons[icon_count++] = (lcd_direct_icon_draw_t) {
|
||||
.icon_id = LCD_DIRECT_ICON_LOW_BATTERY,
|
||||
.x = LCD_DIRECT_LOW_BATTERY_ICON_X,
|
||||
.y = LCD_DIRECT_LOW_BATTERY_ICON_Y,
|
||||
};
|
||||
}
|
||||
if (overlay.charging_visible && icon_count < icon_capacity) {
|
||||
icons[icon_count++] = (lcd_direct_icon_draw_t) {
|
||||
.icon_id = LCD_DIRECT_ICON_CHARGING,
|
||||
.x = LCD_DIRECT_CHARGING_ICON_X,
|
||||
.y = LCD_DIRECT_CHARGING_ICON_Y,
|
||||
};
|
||||
}
|
||||
return icon_count;
|
||||
}
|
||||
|
||||
static void lcd_direct_overlay_icons(uint16_t *pixel_buffer,
|
||||
uint16_t row_start,
|
||||
uint16_t row_count,
|
||||
@@ -766,9 +806,23 @@ static esp_err_t lcd_direct_draw_png_image(const lcd_direct_image_t *image,
|
||||
decoded->pinned = true;
|
||||
}
|
||||
|
||||
lcd_direct_icon_draw_t combined_icons[LCD_DIRECT_DEFAULT_ICON_COUNT] = {0};
|
||||
size_t combined_icon_count = icon_count;
|
||||
if (combined_icon_count > LCD_DIRECT_DEFAULT_ICON_COUNT) {
|
||||
combined_icon_count = LCD_DIRECT_DEFAULT_ICON_COUNT;
|
||||
}
|
||||
if (icons != NULL && combined_icon_count > 0U) {
|
||||
memcpy(combined_icons, icons, combined_icon_count * sizeof(combined_icons[0]));
|
||||
}
|
||||
combined_icon_count = lcd_direct_append_status_icons(
|
||||
image,
|
||||
combined_icons,
|
||||
combined_icon_count,
|
||||
LCD_DIRECT_DEFAULT_ICON_COUNT);
|
||||
|
||||
lcd_direct_prepared_icon_t prepared_icons[LCD_DIRECT_DEFAULT_ICON_COUNT] = {0};
|
||||
size_t prepared_icon_count = lcd_direct_prepare_icons(icons,
|
||||
icon_count,
|
||||
size_t prepared_icon_count = lcd_direct_prepare_icons(combined_icons,
|
||||
combined_icon_count,
|
||||
prepared_icons,
|
||||
sizeof(prepared_icons) / sizeof(prepared_icons[0]));
|
||||
esp_err_t err = ESP_OK;
|
||||
@@ -1424,6 +1478,33 @@ esp_err_t lcd_direct_stop_render_loop(int timeout_ms)
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
esp_err_t lcd_direct_set_status_overlay(const platform_lcd_status_overlay_t *overlay)
|
||||
{
|
||||
if (overlay == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
TaskHandle_t render_task = NULL;
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
changed = s_status_overlay.low_battery_visible != overlay->low_battery_visible ||
|
||||
s_status_overlay.charging_visible != overlay->charging_visible;
|
||||
s_status_overlay = *overlay;
|
||||
render_task = s_render_task;
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
|
||||
if (changed && render_task != NULL) {
|
||||
xTaskNotifyGive(render_task);
|
||||
}
|
||||
if (changed) {
|
||||
ESP_LOGI(TAG,
|
||||
"LCD battery overlays low=%d charging=%d",
|
||||
overlay->low_battery_visible,
|
||||
overlay->charging_visible);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t lcd_direct_request_presentation(const platform_lcd_presentation_request_t *request)
|
||||
{
|
||||
if (!s_initialized || request == NULL) {
|
||||
|
||||
@@ -217,6 +217,12 @@ static const lcd_direct_icon_t s_wuxing_icons[] = {
|
||||
{
|
||||
.path = LCD_ASSET_BASE_PATH "icon_jin0.png",
|
||||
},
|
||||
{
|
||||
.path = LCD_ASSET_BASE_PATH "charging.png",
|
||||
},
|
||||
{
|
||||
.path = LCD_ASSET_BASE_PATH "lowbat.png",
|
||||
},
|
||||
};
|
||||
|
||||
static const lcd_direct_image_t *lcd_direct_frame_from_array(const lcd_direct_image_t *frames,
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_adc/adc_cali.h"
|
||||
#include "esp_adc/adc_cali_scheme.h"
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "platform_battery_internal.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if CONFIG_TQ_BATTERY_ENABLE
|
||||
|
||||
#define BATTERY_ADC_SAMPLE_COUNT 16
|
||||
#define BATTERY_ADC_TRIM_COUNT 4
|
||||
#define BATTERY_VALID_MIN_MV 2500
|
||||
#define BATTERY_VALID_MAX_MV 4500
|
||||
#define BATTERY_ADC_SAMPLE_DELAY_US 200
|
||||
|
||||
static const char *TAG = "platform_battery";
|
||||
static adc_oneshot_unit_handle_t s_adc_handle;
|
||||
static adc_cali_handle_t s_cali_handle;
|
||||
static adc_channel_t s_adc_channel;
|
||||
static bool s_initialized;
|
||||
static StaticSemaphore_t s_adc_lock_buffer;
|
||||
static SemaphoreHandle_t s_adc_lock;
|
||||
static portMUX_TYPE s_adc_lock_init_guard = portMUX_INITIALIZER_UNLOCKED;
|
||||
static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static uint16_t s_load_ref_count[PLATFORM_BATTERY_LOAD_COUNT];
|
||||
static int64_t s_cooldown_until_us;
|
||||
|
||||
static bool lock_adc(void)
|
||||
{
|
||||
if (s_adc_lock == NULL) {
|
||||
portENTER_CRITICAL(&s_adc_lock_init_guard);
|
||||
if (s_adc_lock == NULL) {
|
||||
s_adc_lock = xSemaphoreCreateMutexStatic(&s_adc_lock_buffer);
|
||||
}
|
||||
portEXIT_CRITICAL(&s_adc_lock_init_guard);
|
||||
}
|
||||
return s_adc_lock != NULL &&
|
||||
xSemaphoreTake(s_adc_lock, pdMS_TO_TICKS(100)) == pdTRUE;
|
||||
}
|
||||
|
||||
static void unlock_adc(void)
|
||||
{
|
||||
(void)xSemaphoreGive(s_adc_lock);
|
||||
}
|
||||
|
||||
static uint64_t gpio_mask(int gpio_num)
|
||||
{
|
||||
return 1ULL << (unsigned)gpio_num;
|
||||
}
|
||||
|
||||
static void sort_ints(int *values, size_t count)
|
||||
{
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
int value = values[i];
|
||||
size_t j = i;
|
||||
while (j > 0U && values[j - 1U] > value) {
|
||||
values[j] = values[j - 1U];
|
||||
--j;
|
||||
}
|
||||
values[j] = value;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t create_calibration(adc_unit_t unit, adc_channel_t channel)
|
||||
{
|
||||
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
|
||||
{
|
||||
adc_cali_curve_fitting_config_t config = {
|
||||
.unit_id = unit,
|
||||
.chan = channel,
|
||||
.atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
esp_err_t err = adc_cali_create_scheme_curve_fitting(&config, &s_cali_handle);
|
||||
if (err == ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_LOGW(TAG, "Curve-fitting ADC calibration unavailable: %s", esp_err_to_name(err));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
|
||||
{
|
||||
adc_cali_line_fitting_config_t config = {
|
||||
.unit_id = unit,
|
||||
.atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
.default_vref = 0,
|
||||
};
|
||||
esp_err_t err = adc_cali_create_scheme_line_fitting(&config, &s_cali_handle);
|
||||
if (err == ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_LOGW(TAG, "Line-fitting ADC calibration unavailable: %s", esp_err_to_name(err));
|
||||
}
|
||||
#endif
|
||||
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t platform_battery_init(void)
|
||||
{
|
||||
if (!lock_adc()) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
if (s_initialized) {
|
||||
unlock_adc();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (CONFIG_TQ_BATTERY_CHARGE_GPIO >= GPIO_NUM_MAX) {
|
||||
unlock_adc();
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (CONFIG_TQ_BATTERY_CHARGE_GPIO >= 0) {
|
||||
gpio_config_t charge_config = {
|
||||
.pin_bit_mask = gpio_mask(CONFIG_TQ_BATTERY_CHARGE_GPIO),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
esp_err_t gpio_err = gpio_config(&charge_config);
|
||||
if (gpio_err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Configure TP4057 status GPIO failed: %s", esp_err_to_name(gpio_err));
|
||||
unlock_adc();
|
||||
return gpio_err;
|
||||
}
|
||||
}
|
||||
|
||||
adc_unit_t unit = ADC_UNIT_1;
|
||||
adc_channel_t channel = ADC_CHANNEL_0;
|
||||
esp_err_t err = adc_oneshot_io_to_channel(CONFIG_TQ_BATTERY_ADC_GPIO,
|
||||
&unit,
|
||||
&channel);
|
||||
if (err != ESP_OK || unit != ADC_UNIT_1) {
|
||||
ESP_LOGE(TAG,
|
||||
"Battery ADC GPIO%d is not a supported ADC1 input: %s",
|
||||
CONFIG_TQ_BATTERY_ADC_GPIO,
|
||||
esp_err_to_name(err));
|
||||
unlock_adc();
|
||||
return err != ESP_OK ? err : ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
adc_oneshot_unit_init_cfg_t unit_config = {
|
||||
.unit_id = unit,
|
||||
.ulp_mode = ADC_ULP_MODE_DISABLE,
|
||||
};
|
||||
err = adc_oneshot_new_unit(&unit_config, &s_adc_handle);
|
||||
if (err != ESP_OK) {
|
||||
unlock_adc();
|
||||
return err;
|
||||
}
|
||||
|
||||
adc_oneshot_chan_cfg_t channel_config = {
|
||||
.atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
err = adc_oneshot_config_channel(s_adc_handle, channel, &channel_config);
|
||||
if (err != ESP_OK) {
|
||||
(void)adc_oneshot_del_unit(s_adc_handle);
|
||||
s_adc_handle = NULL;
|
||||
unlock_adc();
|
||||
return err;
|
||||
}
|
||||
|
||||
err = create_calibration(unit, channel);
|
||||
if (err != ESP_OK) {
|
||||
(void)adc_oneshot_del_unit(s_adc_handle);
|
||||
s_adc_handle = NULL;
|
||||
unlock_adc();
|
||||
return err;
|
||||
}
|
||||
|
||||
s_adc_channel = channel;
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG,
|
||||
"Battery monitor ready ADC_GPIO=%d divider=%d/%d charge_GPIO=%d",
|
||||
CONFIG_TQ_BATTERY_ADC_GPIO,
|
||||
CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM,
|
||||
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM,
|
||||
CONFIG_TQ_BATTERY_CHARGE_GPIO);
|
||||
unlock_adc();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void snapshot_load_state(bool *out_active, bool *out_cooldown)
|
||||
{
|
||||
bool active = false;
|
||||
int64_t cooldown_until_us = 0;
|
||||
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
for (size_t i = 0; i < PLATFORM_BATTERY_LOAD_COUNT; ++i) {
|
||||
if (s_load_ref_count[i] > 0U) {
|
||||
active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cooldown_until_us = s_cooldown_until_us;
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
|
||||
*out_active = active;
|
||||
*out_cooldown = !active && esp_timer_get_time() < cooldown_until_us;
|
||||
}
|
||||
|
||||
esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample)
|
||||
{
|
||||
if (out_sample == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
memset(out_sample, 0, sizeof(*out_sample));
|
||||
|
||||
esp_err_t err = platform_battery_init();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
if (!lock_adc()) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
int raw = 0;
|
||||
for (size_t i = 0; i < 4U; ++i) {
|
||||
err = adc_oneshot_read(s_adc_handle, s_adc_channel, &raw);
|
||||
if (err != ESP_OK) {
|
||||
unlock_adc();
|
||||
return err;
|
||||
}
|
||||
esp_rom_delay_us(BATTERY_ADC_SAMPLE_DELAY_US);
|
||||
}
|
||||
|
||||
int samples_mv[BATTERY_ADC_SAMPLE_COUNT];
|
||||
for (size_t i = 0; i < BATTERY_ADC_SAMPLE_COUNT; ++i) {
|
||||
err = adc_oneshot_read(s_adc_handle, s_adc_channel, &raw);
|
||||
if (err != ESP_OK) {
|
||||
unlock_adc();
|
||||
return err;
|
||||
}
|
||||
err = adc_cali_raw_to_voltage(s_cali_handle, raw, &samples_mv[i]);
|
||||
if (err != ESP_OK) {
|
||||
unlock_adc();
|
||||
return err;
|
||||
}
|
||||
esp_rom_delay_us(BATTERY_ADC_SAMPLE_DELAY_US);
|
||||
}
|
||||
|
||||
sort_ints(samples_mv, BATTERY_ADC_SAMPLE_COUNT);
|
||||
int64_t sum_mv = 0;
|
||||
for (size_t i = BATTERY_ADC_TRIM_COUNT;
|
||||
i < BATTERY_ADC_SAMPLE_COUNT - BATTERY_ADC_TRIM_COUNT;
|
||||
++i) {
|
||||
sum_mv += samples_mv[i];
|
||||
}
|
||||
int adc_mv = (int)(sum_mv /
|
||||
(BATTERY_ADC_SAMPLE_COUNT - 2 * BATTERY_ADC_TRIM_COUNT));
|
||||
int64_t battery_mv = (int64_t)adc_mv *
|
||||
((int64_t)CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM +
|
||||
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM) /
|
||||
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM;
|
||||
battery_mv = battery_mv * CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM / 1000000LL +
|
||||
CONFIG_TQ_BATTERY_CALIBRATION_OFFSET_MV;
|
||||
|
||||
out_sample->adc_voltage_mv = adc_mv;
|
||||
out_sample->voltage_mv = battery_mv > INT_MAX ? INT_MAX : (int)battery_mv;
|
||||
out_sample->voltage_valid = out_sample->voltage_mv >= BATTERY_VALID_MIN_MV &&
|
||||
out_sample->voltage_mv <= BATTERY_VALID_MAX_MV;
|
||||
snapshot_load_state(&out_sample->high_load_active, &out_sample->cooldown_active);
|
||||
|
||||
if (CONFIG_TQ_BATTERY_CHARGE_GPIO < 0) {
|
||||
out_sample->charge_state = PLATFORM_BATTERY_CHARGE_UNKNOWN;
|
||||
} else {
|
||||
int level = gpio_get_level((gpio_num_t)CONFIG_TQ_BATTERY_CHARGE_GPIO);
|
||||
out_sample->charge_state = level == CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL
|
||||
? PLATFORM_BATTERY_CHARGE_CHARGING
|
||||
: PLATFORM_BATTERY_CHARGE_NOT_CHARGING;
|
||||
}
|
||||
unlock_adc();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void platform_battery_load_begin(platform_battery_load_source_t source)
|
||||
{
|
||||
if (source < 0 || source >= PLATFORM_BATTERY_LOAD_COUNT) {
|
||||
return;
|
||||
}
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
if (s_load_ref_count[source] < UINT16_MAX) {
|
||||
++s_load_ref_count[source];
|
||||
}
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
}
|
||||
|
||||
void platform_battery_load_end(platform_battery_load_source_t source)
|
||||
{
|
||||
if (source < 0 || source >= PLATFORM_BATTERY_LOAD_COUNT) {
|
||||
return;
|
||||
}
|
||||
int64_t cooldown_until_us = esp_timer_get_time() +
|
||||
(int64_t)CONFIG_TQ_BATTERY_LOAD_COOLDOWN_SEC * 1000000LL;
|
||||
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
if (s_load_ref_count[source] > 0U) {
|
||||
--s_load_ref_count[source];
|
||||
if (cooldown_until_us > s_cooldown_until_us) {
|
||||
s_cooldown_until_us = cooldown_until_us;
|
||||
}
|
||||
}
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
esp_err_t platform_battery_init(void)
|
||||
{
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t platform_battery_read_sample(platform_battery_sample_t *out_sample)
|
||||
{
|
||||
if (out_sample != NULL) {
|
||||
memset(out_sample, 0, sizeof(*out_sample));
|
||||
}
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
void platform_battery_load_begin(platform_battery_load_source_t source)
|
||||
{
|
||||
(void)source;
|
||||
}
|
||||
|
||||
void platform_battery_load_end(platform_battery_load_source_t source)
|
||||
{
|
||||
(void)source;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "platform_internal.h"
|
||||
#include "platform_battery_internal.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifndef CONFIG_TQ_STEPPER_ENABLE
|
||||
@@ -256,9 +257,16 @@ esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle,
|
||||
}
|
||||
}
|
||||
|
||||
return platform_stepper_driver_rotate_angle_with_accel(angle,
|
||||
if (angle == 0.0f) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
platform_battery_load_begin(PLATFORM_BATTERY_LOAD_STEPPER);
|
||||
esp_err_t err = platform_stepper_driver_rotate_angle_with_accel(angle,
|
||||
target_delay_us,
|
||||
timeout_ms);
|
||||
platform_battery_load_end(PLATFORM_BATTERY_LOAD_STEPPER);
|
||||
return err;
|
||||
}
|
||||
|
||||
void platform_stepper_power_off(void)
|
||||
|
||||
@@ -22,6 +22,11 @@ esp_err_t platform_stop_lcd_render_loop(int timeout_ms)
|
||||
return lcd_direct_stop_render_loop(timeout_ms);
|
||||
}
|
||||
|
||||
esp_err_t platform_lcd_set_status_overlay(const platform_lcd_status_overlay_t *overlay)
|
||||
{
|
||||
return lcd_direct_set_status_overlay(overlay);
|
||||
}
|
||||
|
||||
esp_err_t platform_lcd_request_presentation(const platform_lcd_presentation_request_t *request)
|
||||
{
|
||||
return lcd_direct_request_presentation(request);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "multimodal/audio_pipeline.h"
|
||||
#include "multimodal/transport_ws.h"
|
||||
#include "platform_battery_internal.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
|
||||
@@ -17,6 +18,38 @@ static platform_multimodal_transport_callbacks_t s_multimodal_transport_callback
|
||||
static portMUX_TYPE s_multimodal_transport_cb_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static platform_multimodal_downstream_playback_callbacks_t s_multimodal_downstream_playback_callbacks;
|
||||
static portMUX_TYPE s_multimodal_downstream_playback_cb_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static uint32_t s_battery_playback_generation;
|
||||
static bool s_battery_playback_active;
|
||||
|
||||
static void platform_battery_note_playback_event(
|
||||
multimodal_audio_downstream_playback_event_type_t type,
|
||||
uint32_t generation)
|
||||
{
|
||||
bool begin_load = false;
|
||||
bool end_load = false;
|
||||
|
||||
portENTER_CRITICAL(&s_multimodal_downstream_playback_cb_lock);
|
||||
if (type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STARTED) {
|
||||
if (!s_battery_playback_active) {
|
||||
s_battery_playback_active = true;
|
||||
begin_load = true;
|
||||
}
|
||||
s_battery_playback_generation = generation;
|
||||
} else if (s_battery_playback_active &&
|
||||
s_battery_playback_generation == generation) {
|
||||
s_battery_playback_active = false;
|
||||
s_battery_playback_generation = 0U;
|
||||
end_load = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_multimodal_downstream_playback_cb_lock);
|
||||
|
||||
if (begin_load) {
|
||||
platform_battery_load_begin(PLATFORM_BATTERY_LOAD_AUDIO);
|
||||
}
|
||||
if (end_load) {
|
||||
platform_battery_load_end(PLATFORM_BATTERY_LOAD_AUDIO);
|
||||
}
|
||||
}
|
||||
|
||||
static void snapshot_transport_callbacks(
|
||||
platform_multimodal_transport_callbacks_t *out_callbacks)
|
||||
@@ -228,6 +261,8 @@ static void platform_multimodal_downstream_playback_bridge(
|
||||
return;
|
||||
}
|
||||
|
||||
platform_battery_note_playback_event(event->type, event->playback_generation);
|
||||
|
||||
switch (event->type) {
|
||||
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STARTED:
|
||||
if (callbacks.on_started != NULL) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# ADR 0005: Battery Management Ownership
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The current board uses GPIO9 to measure a protected 600 mAh single-cell Li-ion
|
||||
battery through a 100 kOhm / 100 kOhm divider. GPIO10 receives the active-low
|
||||
TP4057 charge indication. Battery state is affected by ADC calibration, audio
|
||||
and stepper load transients, SOC estimation, threshold hysteresis, and runtime
|
||||
policy, so it cannot have duplicate state in several layers.
|
||||
|
||||
## Decision
|
||||
|
||||
- `platform` owns ADC1 setup, calibration, trimmed-mean sampling, divider
|
||||
conversion, TP4057 GPIO sampling, and internal audio/stepper load-quality
|
||||
tracking.
|
||||
- `domain` owns the single battery status snapshot, voltage filtering, SOC
|
||||
curve interpolation, implausible-jump confirmation, charge/discharge
|
||||
direction constraints, and low/critical hysteresis.
|
||||
- `control_plane` owns the periodic monitoring worker and reactions to semantic
|
||||
state transitions.
|
||||
- `domain` maps its battery snapshot to semantic LCD visibility, while
|
||||
`platform` owns transparent PNG composition and panel serialization.
|
||||
- The first release logs low and critical transitions but intentionally does
|
||||
not stop actuators, enter sleep, or cut power. Typed shutdown preparation
|
||||
remains a reserved, unsupported hook until hardware wake and shutdown policy
|
||||
are decided.
|
||||
- `app_composition` keeps using the aggregate control-plane runtime lifecycle;
|
||||
it does not acquire battery samples directly.
|
||||
|
||||
## Consequences
|
||||
|
||||
- GPIO9 must never be configured as the legacy power-hold output on this board.
|
||||
- High-load samples remain available for diagnostics but cannot update SOC or
|
||||
low-battery state. Normal processing resumes after the configured cooldown.
|
||||
- GPIO10 cannot distinguish a full battery from an unplugged charger because
|
||||
both produce a high level; the public state therefore reports only charging
|
||||
versus not charging.
|
||||
- The voltage-to-SOC curve is an initial approximation and requires bench
|
||||
calibration against measured battery voltage and a controlled discharge run.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Battery Management
|
||||
|
||||
## Hardware Profile
|
||||
|
||||
- Cell: protected single-cell Li-ion, 600 mAh, 3.7 V nominal, 4.2 V full.
|
||||
- Supported measurement range: 3.3 V to 4.2 V for SOC estimation.
|
||||
- GPIO9: ADC1 battery sense through R17=100 kOhm (VBAT to ADC) and R18=100
|
||||
kOhm (ADC to ground). `VBAT = VADC * 2`.
|
||||
- GPIO10: TP4057 active-low charge output with an input pull-up. A high level
|
||||
means either charge complete or charger absent.
|
||||
- Legacy power hold: disabled (`CONFIG_TQ_POWER_HOLD_GPIO=-1`).
|
||||
|
||||
## Processing Pipeline
|
||||
|
||||
Each hardware sample consists of 16 calibrated ADC readings. The platform
|
||||
driver sorts them, discards the lowest four and highest four, averages the
|
||||
remaining eight, and applies the divider ratio. Domain processing then applies:
|
||||
|
||||
1. A 2.5 V to 4.5 V plausibility check.
|
||||
2. Three-sample confirmation for voltage jumps greater than 300 mV.
|
||||
3. An exponential filter with 80% previous and 20% new voltage.
|
||||
4. Piecewise-linear interpolation over the Li-ion discharge curve.
|
||||
5. A two-percent maximum SOC change per accepted sample.
|
||||
6. Charge/discharge direction constraints.
|
||||
7. Three-sample confirmation and hysteresis for low and critical states.
|
||||
|
||||
The initial state thresholds are 20%/25% for low entry/recovery and 5%/10% for
|
||||
critical entry/recovery.
|
||||
|
||||
Board calibration can be adjusted without source changes using
|
||||
`CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM` and
|
||||
`CONFIG_TQ_BATTERY_CALIBRATION_OFFSET_MV` after comparing the log against a
|
||||
multimeter.
|
||||
|
||||
## High-Load Handling
|
||||
|
||||
Audio playback events and synchronous stepper rotations update one
|
||||
platform-owned load tracker. While a load is active and for five seconds after
|
||||
its terminal event, samples are marked suppressed. The monitor continues to
|
||||
poll once per second for diagnostics and charge state, but suppressed voltage
|
||||
does not update filtered voltage, SOC, or low-battery state.
|
||||
|
||||
Playback generations prevent a stale terminal event from ending suppression
|
||||
for a newer playback. Stepper cleanup executes after every synchronous return,
|
||||
including errors and timeouts.
|
||||
|
||||
## LCD Status Overlays
|
||||
|
||||
The direct LCD renderer composites the transparent `charging.png` asset in the
|
||||
top-right corner whenever GPIO10 reports active-low charging. It composites
|
||||
`lowbat.png` at the bottom center after the domain level enters low or critical
|
||||
state. Both overlays may be visible together, and QR-code presentations omit
|
||||
them to preserve scan reliability. Overlay state changes do not trigger any
|
||||
shutdown, sleep, prompt, or actuator policy.
|
||||
|
||||
## Deferred Policy
|
||||
|
||||
Critical status currently produces a state-transition log only. The reserved
|
||||
shutdown hook returns `ESP_ERR_NOT_SUPPORTED`; it does not stop audio or the
|
||||
stepper, enter deep sleep, or release board power. Implement wake sources and
|
||||
the product shutdown sequence before enabling any automatic critical action.
|
||||
|
||||
## Bench Calibration
|
||||
|
||||
After flashing, compare the `cp_battery` voltage log with a multimeter at no
|
||||
load near 4.2 V, 3.9 V, 3.6 V, and 3.3 V. Record audio and stepper voltage sag,
|
||||
then run a controlled discharge to tune the SOC curve and threshold timing.
|
||||
@@ -23,7 +23,7 @@ dark. This is accepted for this plan.
|
||||
## Startup Order
|
||||
|
||||
```text
|
||||
power hold
|
||||
power-hold configuration (disabled on the current battery-sensing board)
|
||||
-> audio init
|
||||
-> play boot sound with timeout
|
||||
-> audio deinit or idle release
|
||||
@@ -42,7 +42,7 @@ power hold
|
||||
|
||||
| Order | Stage | Wait policy | Failure policy | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Power hold GPIO | Wait | Continue only if board power remains stable | Configure the hold GPIO and assert the active level before any heavier work. |
|
||||
| 1 | Power-hold configuration | Wait | Continue only if board power remains stable | The current board sets the hold GPIO to `-1`; legacy boards may configure and assert a dedicated hold output. GPIO9 is reserved for battery ADC. |
|
||||
| 2 | Audio init | Wait with bounded timeout | Log and skip boot sound | Initializes the codec path needed for playback: I2C, I2S, codec, PA control, codec buffers, and decoder resources. |
|
||||
| 3 | Boot sound playback | Wait for completion or timeout | Stop playback and continue boot | The sound must not block startup indefinitely. Use a short product-defined timeout. |
|
||||
| 4 | Audio release | Wait for synchronous release | Continue if release reports a recoverable error | If Wi-Fi/TLS starts soon after, deinit or release idle audio resources to reduce DMA/internal heap pressure. |
|
||||
@@ -53,7 +53,7 @@ power hold
|
||||
| 9 | Turntable calibration | Product decision; default wait with timeout | Continue with degraded direction accuracy on timeout/failure | This is the mechanical SW1 homing/calibration process, not the stepper electrical init. |
|
||||
| 10 | Storage prepare | Wait | Stop features that need persistent state | Initializes NVS before Wi-Fi credentials, identity, auth, and fortune state are read or written. |
|
||||
| 11 | Wi-Fi bootstrap | Wait until STA connected or AP config mode starts | Enter config AP or keep retry policy active | Must not overlap with boot sound playback or heavy audio initialization. |
|
||||
| 12 | Runtime workers | Start and continue | Stop only failed dependent feature | Starts PTT, slot switch, Wi-Fi state monitor, model command worker, LCD runtime worker, and WebSocket wait worker as configured. |
|
||||
| 12 | Runtime workers | Start and continue | Stop only failed dependent feature | Starts battery monitoring, PTT, slot switch, Wi-Fi state monitor, model command worker, LCD runtime worker, and WebSocket wait worker as configured. |
|
||||
| 13 | Network services | Async after network readiness | Retry using existing policy | Includes multimodal WebSocket, LAN HTTP config server, SNTP, HTTPS fortune refresh, and auth flows. |
|
||||
|
||||
## Clarifications
|
||||
|
||||
@@ -15,7 +15,8 @@ the ESP32-S3 MCU perspective.
|
||||
| 6 | `CONFIG_TQ_SLOT_SWITCH_GPIO` | Input | Slot / key input | Rocker microswitch / original SW2 function | Uses the unconnected `TP_SCL` contact; active-low with internal pull-up |
|
||||
| 7 | `CONFIG_TQ_SCREEN_BACKLIGHT_GPIO` | Output | LCD | LCD adapter backlight control | Active-high by default |
|
||||
| 8 | `CONFIG_TQ_SCREEN_SPI_CLK_GPIO` | Output | LCD | ST7789P3 SPI clock / SCK | SPI2 host, 20 MHz default |
|
||||
| 9 | `CONFIG_TQ_POWER_HOLD_GPIO` | Output | Power | Board power hold | Active-high by default |
|
||||
| 9 | `CONFIG_TQ_BATTERY_ADC_GPIO` | Analog input | Battery | Battery voltage through 100 kOhm / 100 kOhm divider | ADC1 input; do not configure as a power-hold output |
|
||||
| 10 | `CONFIG_TQ_BATTERY_CHARGE_GPIO` | Input | Battery charger | TP4057 charge status | Active-low, internal pull-up enabled |
|
||||
| 11 | `CONFIG_MULTIMODAL_AUDIO_PA_GPIO` | Output | Audio amplifier | PA enable | Managed by codec GPIO interface |
|
||||
| 12 | `CONFIG_TQ_CALIBRATION_SWITCH_GPIO` | Input | Turntable calibration | Button 1 / SW1 direction calibration | Active-low, external and internal pull-up |
|
||||
| 13 | `CONFIG_MULTIMODAL_PTT_BUTTON_GPIO` | Input | Keys / multimodal | Button 2 / K2 push-to-talk | Active-low, external and internal pull-up |
|
||||
@@ -46,6 +47,7 @@ is installed.
|
||||
| `CONFIG_TQ_SLOT_SWITCH_PULLDOWN` | unset | SW2 uses pull-up only. |
|
||||
| `CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN` | unset | Calibration switch uses pull-up only. |
|
||||
| `CONFIG_MULTIMODAL_PTT_BUTTON_PULLDOWN` | unset | Button 2 uses pull-up only. |
|
||||
| `CONFIG_TQ_POWER_HOLD_GPIO` | `-1` | Legacy board power-hold output is disabled because GPIO9 is the battery ADC input. |
|
||||
|
||||
## Boot Hardware Test Profile
|
||||
|
||||
@@ -90,7 +92,6 @@ them to an active project feature.
|
||||
| --- | --- | --- |
|
||||
| GPIO0 / K1 | Switch to GND, commonly boot/download related | Not consumed by application code |
|
||||
| GPIO3 / `touch` | TTP223 touch output | Present on the board but not consumed by the current input policy |
|
||||
| GPIO10 / `Charge` | Charger status | No current firmware consumer found |
|
||||
| GPIO14 | Unassigned | Not used |
|
||||
| GPIO35 | Unassigned | Not used |
|
||||
| GPIO36 | Unassigned | Not used |
|
||||
@@ -107,6 +108,7 @@ them to an active project feature.
|
||||
- `docs/gpio-map.md`
|
||||
- `components/platform/src/lcd_direct.c`
|
||||
- `components/platform/src/platform_stepper_driver.c`
|
||||
- `components/platform/src/platform_battery_driver.c`
|
||||
- `components/platform/src/multimodal/audio_pipeline/audio_pipeline_codec_impl.c`
|
||||
- `components/control_plane/src/runtime/control_plane_key_input.c`
|
||||
- `components/platform/src/platform_console_driver.c`
|
||||
|
||||
+6
-5
@@ -1,8 +1,9 @@
|
||||
# ESP32-S3 GPIO Map
|
||||
|
||||
This document defines the reference GPIO assignments for the ESP32-S3 board
|
||||
from `SCH_招财猫0.3&商界0.2_2026-07-24.pdf`. Signal direction is described from
|
||||
the MCU's perspective.
|
||||
This document defines the active firmware GPIO assignments for the current
|
||||
battery-sensing ESP32-S3 board revision. Signal direction is described from the
|
||||
MCU's perspective. Older 2026-07-24 drawings assigned GPIO9 to a legacy power
|
||||
hold net; that assignment is not valid for the current board.
|
||||
|
||||
## GPIO Table
|
||||
|
||||
@@ -24,8 +25,8 @@ the MCU's perspective.
|
||||
| 14 | GPIO20 | ESP_D_P | Bidirectional | USB D+ | Type-C |
|
||||
| 15 | GPIO3 | touch | Input | TTP223 touch-button output | Present on the board but not consumed by the current input policy |
|
||||
| 16 | GPIO46 | UNASSIGNED | N/A | No connection assigned | Reserved; no signal assigned |
|
||||
| 17 | GPIO9 | Power | Output | Board power hold | Active high |
|
||||
| 18 | GPIO10 | Charge | Input | Charger status | Not consumed by current firmware |
|
||||
| 17 | GPIO9 | Power_t | Analog input | Battery voltage sense | R17=100 kOhm from VBAT and R18=100 kOhm to GND; ADC voltage is one half of VBAT |
|
||||
| 18 | GPIO10 | Charge | Input | TP4057 charger status | Active-low while charging; high means full or external power absent |
|
||||
| 19 | GPIO11 | PA_CTRL | Output | Audio power-amplifier enable | Amplifier control |
|
||||
| 20 | GPIO12 | SW1 | Input | Button 1 / turntable calibration switch | External 10 kOhm pull-up; active low |
|
||||
| 21 | GPIO13 | SW2 | Input | Button 2 / K2 push-to-talk input | External 10 kOhm pull-up; active low |
|
||||
|
||||
+86
-4
@@ -718,12 +718,12 @@ menu "Example Configuration"
|
||||
|
||||
config TQ_POWER_HOLD_GPIO
|
||||
int "Power hold GPIO (-1 to disable)"
|
||||
default 9
|
||||
default -1
|
||||
range -1 48
|
||||
help
|
||||
GPIO used to keep board power enabled after boot. The
|
||||
2026-07-24 board maps the Power net to IO9. Set to -1 only
|
||||
when no external power-hold circuit is connected.
|
||||
GPIO used to keep board power enabled after boot. The current
|
||||
battery-sensing board uses IO9 as an ADC input and therefore
|
||||
disables the legacy power-hold output by default.
|
||||
|
||||
config TQ_POWER_HOLD_ACTIVE_LEVEL
|
||||
int "Power hold active level"
|
||||
@@ -732,6 +732,88 @@ menu "Example Configuration"
|
||||
help
|
||||
Logic level that keeps the external power-hold circuit active.
|
||||
|
||||
config TQ_BATTERY_ENABLE
|
||||
bool "Enable battery monitoring"
|
||||
default y
|
||||
|
||||
config TQ_BATTERY_ADC_GPIO
|
||||
int "Battery voltage ADC GPIO"
|
||||
default 9
|
||||
range 0 48
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_CHARGE_GPIO
|
||||
int "TP4057 charge status GPIO (-1 to disable)"
|
||||
default 10
|
||||
range -1 48
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_CHARGE_ACTIVE_LEVEL
|
||||
int "TP4057 charging active level"
|
||||
default 0
|
||||
range 0 1
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_DIVIDER_HIGH_OHM
|
||||
int "Battery divider resistance from VBAT to ADC (ohm)"
|
||||
default 100000
|
||||
range 1 10000000
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_DIVIDER_LOW_OHM
|
||||
int "Battery divider resistance from ADC to ground (ohm)"
|
||||
default 100000
|
||||
range 1 10000000
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_CALIBRATION_GAIN_PPM
|
||||
int "Board battery voltage calibration gain (ppm)"
|
||||
default 1000000
|
||||
range 800000 1200000
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_CALIBRATION_OFFSET_MV
|
||||
int "Board battery voltage calibration offset (mV)"
|
||||
default 0
|
||||
range -500 500
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_SAMPLE_INTERVAL_SEC
|
||||
int "Battery monitoring interval (seconds)"
|
||||
default 10
|
||||
range 1 3600
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_LOAD_COOLDOWN_SEC
|
||||
int "Battery sampling cooldown after high load (seconds)"
|
||||
default 5
|
||||
range 0 60
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_LOW_ENTER_PERCENT
|
||||
int "Low battery entry threshold (percent)"
|
||||
default 20
|
||||
range 1 99
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_LOW_EXIT_PERCENT
|
||||
int "Low battery recovery threshold (percent)"
|
||||
default 25
|
||||
range 1 100
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_CRITICAL_ENTER_PERCENT
|
||||
int "Critical battery entry threshold (percent)"
|
||||
default 5
|
||||
range 0 99
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_CRITICAL_EXIT_PERCENT
|
||||
int "Critical battery recovery threshold (percent)"
|
||||
default 10
|
||||
range 1 100
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
endmenu
|
||||
|
||||
menu "Wi-Fi Provisioning"
|
||||
|
||||
+15
-1
@@ -301,6 +301,20 @@ CONFIG_TQ_CALIBRATION_SWITCH_PULLUP=y
|
||||
# CONFIG_TQ_CALIBRATION_SWITCH_PULLDOWN is not set
|
||||
# CONFIG_TQ_SCREEN_COLOR_AUTOTEST is not set
|
||||
CONFIG_TQ_SCREEN_COLOR_AUTOTEST_INTERVAL_MS=3000
|
||||
CONFIG_TQ_POWER_HOLD_GPIO=9
|
||||
CONFIG_TQ_POWER_HOLD_GPIO=-1
|
||||
CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL=1
|
||||
CONFIG_TQ_BATTERY_ENABLE=y
|
||||
CONFIG_TQ_BATTERY_ADC_GPIO=9
|
||||
CONFIG_TQ_BATTERY_CHARGE_GPIO=10
|
||||
CONFIG_TQ_BATTERY_CHARGE_ACTIVE_LEVEL=0
|
||||
CONFIG_TQ_BATTERY_DIVIDER_HIGH_OHM=100000
|
||||
CONFIG_TQ_BATTERY_DIVIDER_LOW_OHM=100000
|
||||
CONFIG_TQ_BATTERY_CALIBRATION_GAIN_PPM=1000000
|
||||
CONFIG_TQ_BATTERY_CALIBRATION_OFFSET_MV=0
|
||||
CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC=10
|
||||
CONFIG_TQ_BATTERY_LOAD_COOLDOWN_SEC=5
|
||||
CONFIG_TQ_BATTERY_LOW_ENTER_PERCENT=20
|
||||
CONFIG_TQ_BATTERY_LOW_EXIT_PERCENT=25
|
||||
CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT=5
|
||||
CONFIG_TQ_BATTERY_CRITICAL_EXIT_PERCENT=10
|
||||
CONFIG_MULTIMODAL_AUDIO_PA_GPIO=11
|
||||
|
||||
Reference in New Issue
Block a user