feat(power): add controlled low-battery shutdown
This commit is contained in:
@@ -79,6 +79,14 @@ static esp_err_t app_composition_run_plan_a_boot_sequence(void)
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t battery_guard_err = control_plane_battery_apply_boot_guard();
|
||||
if (battery_guard_err != ESP_OK &&
|
||||
battery_guard_err != ESP_ERR_NOT_SUPPORTED) {
|
||||
ESP_LOGW(TAG,
|
||||
"Battery boot guard failed, continue boot: %s",
|
||||
esp_err_to_name(battery_guard_err));
|
||||
}
|
||||
|
||||
app_composition_log_stage(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT,
|
||||
"audio_init",
|
||||
"boot_sound_policy_begin");
|
||||
|
||||
@@ -130,6 +130,12 @@ esp_err_t control_plane_init(void);
|
||||
*/
|
||||
esp_err_t control_plane_battery_service_start(void);
|
||||
|
||||
/**
|
||||
* Re-enter deep sleep before normal boot when a low-battery wake has not
|
||||
* recovered and no charger is active.
|
||||
*/
|
||||
esp_err_t control_plane_battery_apply_boot_guard(void);
|
||||
|
||||
/**
|
||||
* @brief Prepare platform-backed persistent storage before runtime workers start.
|
||||
*
|
||||
|
||||
@@ -147,6 +147,7 @@ bool control_plane_voice_turn_is_idle(void);
|
||||
bool control_plane_ota_voice_services_allowed(void);
|
||||
bool control_plane_ota_business_interactions_allowed(void);
|
||||
bool control_plane_runtime_is_stopping(void);
|
||||
void control_plane_set_runtime_stop_requested(bool stop_requested);
|
||||
|
||||
esp_err_t control_plane_battery_service_stop(int timeout_ms);
|
||||
void control_plane_battery_service_set_light_standby(bool standby);
|
||||
|
||||
@@ -25,9 +25,31 @@
|
||||
#define BATTERY_FULL_CONFIRM_TIME_MS 5000
|
||||
#define BATTERY_FULL_CONFIRM_SAMPLES 3U
|
||||
#define BATTERY_FULL_MIN_VOLTAGE_MV 4050
|
||||
#define BATTERY_SHUTDOWN_CONFIRM_SAMPLES 3U
|
||||
#define BATTERY_EMERGENCY_CONFIRM_SAMPLES 2U
|
||||
#define BATTERY_SHUTDOWN_RETRY_MS 1000
|
||||
#define BATTERY_SHUTDOWN_TASK_STACK_BYTES 6144
|
||||
#define BATTERY_SHUTDOWN_TASK_PRIORITY 5
|
||||
#define BATTERY_SHUTDOWN_MOTOR_STOP_MS 2000
|
||||
#define BATTERY_LOW_PROMPT_FILE "/littlefs/audio_prompts/battery_low_20.opus"
|
||||
#define BATTERY_FULL_PROMPT_FILE "/littlefs/audio_prompts/charge_complete.opus"
|
||||
|
||||
#ifndef CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
#define CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE 0
|
||||
#endif
|
||||
#ifndef CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE
|
||||
#define CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE "/littlefs/audio_prompts/shutdown_power_empty.opus"
|
||||
#endif
|
||||
|
||||
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
#if CONFIG_TQ_BATTERY_EMERGENCY_VOLTAGE_MV >= CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV
|
||||
#error "Emergency battery voltage must be below the controlled shutdown threshold"
|
||||
#endif
|
||||
#if CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV <= CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV
|
||||
#error "Battery wake recovery voltage must exceed the shutdown threshold"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC
|
||||
#define CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC 300
|
||||
#endif
|
||||
@@ -38,6 +60,17 @@ static bool s_stop_requested;
|
||||
static bool s_starting;
|
||||
static bool s_charge_gpio_isr_registered;
|
||||
static bool s_light_standby;
|
||||
static TaskHandle_t s_shutdown_task;
|
||||
static uint8_t s_shutdown_confirmations;
|
||||
static uint8_t s_emergency_confirmations;
|
||||
|
||||
typedef enum {
|
||||
BATTERY_POWER_STATE_RUNNING = 0,
|
||||
BATTERY_POWER_STATE_SHUTDOWN_REQUESTED,
|
||||
BATTERY_POWER_STATE_SHUTTING_DOWN,
|
||||
} battery_power_state_t;
|
||||
|
||||
static battery_power_state_t s_power_state = BATTERY_POWER_STATE_RUNNING;
|
||||
static portMUX_TYPE s_battery_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
static void IRAM_ATTR battery_charge_gpio_isr(void *arg)
|
||||
@@ -145,14 +178,137 @@ static void battery_worker_finish(void)
|
||||
portEXIT_CRITICAL(&s_battery_lock);
|
||||
}
|
||||
|
||||
static void control_plane_battery_critical_hook(
|
||||
static void battery_shutdown_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
portENTER_CRITICAL(&s_battery_lock);
|
||||
s_power_state = BATTERY_POWER_STATE_SHUTTING_DOWN;
|
||||
portEXIT_CRITICAL(&s_battery_lock);
|
||||
|
||||
ESP_LOGW(TAG, "Controlled low-battery shutdown started");
|
||||
control_plane_set_runtime_stop_requested(true);
|
||||
(void)control_plane_local_wake_stop(BATTERY_SHUTDOWN_MOTOR_STOP_MS);
|
||||
(void)control_plane_stop_ota_service(BATTERY_SHUTDOWN_MOTOR_STOP_MS);
|
||||
(void)control_plane_stop_multimodal_client();
|
||||
control_plane_stop_key_input_task();
|
||||
control_plane_stop_system_event_task();
|
||||
|
||||
esp_err_t motor_err = domain_direction_release_motor_hold(
|
||||
BATTERY_SHUTDOWN_MOTOR_STOP_MS);
|
||||
if (motor_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Release stepper before shutdown failed: %s",
|
||||
esp_err_to_name(motor_err));
|
||||
}
|
||||
|
||||
(void)domain_runtime_multimodal_audio_stop_downstream_playback();
|
||||
for (;;) {
|
||||
esp_err_t audio_init_err = domain_runtime_multimodal_audio_init();
|
||||
esp_err_t prompt_err = audio_init_err == ESP_OK
|
||||
? domain_runtime_behavior_play_prompt_file(
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE)
|
||||
: audio_init_err;
|
||||
if (prompt_err == ESP_OK) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* The product requires the complete prompt before entering deep sleep. */
|
||||
ESP_LOGE(TAG,
|
||||
"Shutdown prompt did not complete; retrying: %s",
|
||||
esp_err_to_name(prompt_err));
|
||||
vTaskDelay(pdMS_TO_TICKS(BATTERY_SHUTDOWN_RETRY_MS));
|
||||
}
|
||||
|
||||
(void)domain_runtime_multimodal_audio_set_speaker_output_enabled(false);
|
||||
domain_runtime_multimodal_audio_deinit();
|
||||
(void)domain_runtime_set_lcd_backlight_brightness(0U);
|
||||
(void)domain_runtime_wifi_portal_stop_sta();
|
||||
|
||||
ESP_LOGW(TAG, "Shutdown prompt completed; entering deep sleep");
|
||||
esp_err_t sleep_err = domain_battery_prepare_shutdown();
|
||||
ESP_LOGE(TAG, "Enter deep sleep failed: %s", esp_err_to_name(sleep_err));
|
||||
|
||||
portENTER_CRITICAL(&s_battery_lock);
|
||||
s_power_state = BATTERY_POWER_STATE_RUNNING;
|
||||
s_shutdown_task = NULL;
|
||||
portEXIT_CRITICAL(&s_battery_lock);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void battery_consider_controlled_shutdown(
|
||||
const domain_battery_status_t *status)
|
||||
{
|
||||
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
if (status == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
portENTER_CRITICAL(&s_battery_lock);
|
||||
bool already_requested = s_power_state != BATTERY_POWER_STATE_RUNNING;
|
||||
portEXIT_CRITICAL(&s_battery_lock);
|
||||
if (already_requested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status->charge_state == DOMAIN_BATTERY_CHARGE_CHARGING) {
|
||||
s_shutdown_confirmations = 0U;
|
||||
s_emergency_confirmations = 0U;
|
||||
return;
|
||||
}
|
||||
|
||||
bool normal_sample = status->valid && !status->measurement_suppressed &&
|
||||
status->percentage <= CONFIG_TQ_BATTERY_CRITICAL_ENTER_PERCENT &&
|
||||
status->voltage_mv <= CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV;
|
||||
bool emergency_sample = status->latest_sample_valid &&
|
||||
status->latest_sample_mv <= CONFIG_TQ_BATTERY_EMERGENCY_VOLTAGE_MV;
|
||||
|
||||
if (normal_sample) {
|
||||
if (s_shutdown_confirmations < UINT8_MAX) {
|
||||
++s_shutdown_confirmations;
|
||||
}
|
||||
} else {
|
||||
s_shutdown_confirmations = 0U;
|
||||
}
|
||||
if (emergency_sample) {
|
||||
if (s_emergency_confirmations < UINT8_MAX) {
|
||||
++s_emergency_confirmations;
|
||||
}
|
||||
} else {
|
||||
s_emergency_confirmations = 0U;
|
||||
}
|
||||
|
||||
if (s_shutdown_confirmations < BATTERY_SHUTDOWN_CONFIRM_SAMPLES &&
|
||||
s_emergency_confirmations < BATTERY_EMERGENCY_CONFIRM_SAMPLES) {
|
||||
return;
|
||||
}
|
||||
|
||||
portENTER_CRITICAL(&s_battery_lock);
|
||||
if (s_power_state != BATTERY_POWER_STATE_RUNNING || s_shutdown_task != NULL) {
|
||||
portEXIT_CRITICAL(&s_battery_lock);
|
||||
return;
|
||||
}
|
||||
s_power_state = BATTERY_POWER_STATE_SHUTDOWN_REQUESTED;
|
||||
portEXIT_CRITICAL(&s_battery_lock);
|
||||
|
||||
TaskHandle_t task = NULL;
|
||||
BaseType_t created = xTaskCreate(battery_shutdown_task,
|
||||
"cp_bat_shutdown",
|
||||
BATTERY_SHUTDOWN_TASK_STACK_BYTES,
|
||||
NULL,
|
||||
BATTERY_SHUTDOWN_TASK_PRIORITY,
|
||||
&task);
|
||||
portENTER_CRITICAL(&s_battery_lock);
|
||||
if (created == pdPASS && task != NULL) {
|
||||
s_shutdown_task = task;
|
||||
} else {
|
||||
s_power_state = BATTERY_POWER_STATE_RUNNING;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_battery_lock);
|
||||
if (created != pdPASS || task == NULL) {
|
||||
ESP_LOGE(TAG, "Create controlled-shutdown task failed");
|
||||
}
|
||||
#else
|
||||
(void)status;
|
||||
/*
|
||||
* Reserved for a future product decision. Do not stop actuators, enter
|
||||
* sleep, or cut board power in this release.
|
||||
*/
|
||||
#endif
|
||||
}
|
||||
|
||||
static void control_plane_battery_low_hook(
|
||||
@@ -268,9 +424,6 @@ static void battery_worker(void *arg)
|
||||
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) {
|
||||
@@ -321,12 +474,15 @@ static void battery_worker(void *arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
battery_consider_controlled_shutdown(&status);
|
||||
}
|
||||
|
||||
uint32_t delay_ms;
|
||||
if (status_err == ESP_OK && status.measurement_suppressed) {
|
||||
delay_ms = BATTERY_SUPPRESSED_RETRY_MS;
|
||||
} else if (full_hint_candidate) {
|
||||
} else if (full_hint_candidate || s_shutdown_confirmations > 0U ||
|
||||
s_emergency_confirmations > 0U) {
|
||||
delay_ms = BATTERY_FULL_CONFIRM_RETRY_MS;
|
||||
} else if (battery_light_standby()) {
|
||||
delay_ms = (uint32_t)CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC * 1000U;
|
||||
@@ -347,6 +503,42 @@ static void battery_worker(void *arg)
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t control_plane_battery_apply_boot_guard(void)
|
||||
{
|
||||
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
if (!domain_battery_woke_from_deep_sleep()) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t sample_err = domain_battery_sample();
|
||||
if (sample_err != ESP_OK) {
|
||||
return sample_err;
|
||||
}
|
||||
domain_battery_status_t status = {0};
|
||||
esp_err_t status_err = domain_battery_get_status(&status);
|
||||
if (status_err != ESP_OK) {
|
||||
return status_err;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG,
|
||||
"Deep-sleep wake battery check voltage=%dmV charge=%s valid=%d",
|
||||
status.voltage_mv,
|
||||
charge_state_name(status.charge_state),
|
||||
status.valid);
|
||||
if (status.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING ||
|
||||
(status.valid &&
|
||||
status.voltage_mv >= CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV)) {
|
||||
domain_battery_complete_wakeup();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "Battery has not recovered; returning to deep sleep");
|
||||
return domain_battery_prepare_shutdown();
|
||||
#else
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
esp_err_t control_plane_battery_service_start(void)
|
||||
{
|
||||
portENTER_CRITICAL(&s_battery_lock);
|
||||
@@ -442,6 +634,11 @@ esp_err_t control_plane_battery_service_start(void)
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_err_t control_plane_battery_apply_boot_guard(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;
|
||||
|
||||
@@ -490,7 +490,7 @@ static bool control_plane_runtime_stop_requested(void)
|
||||
return control_plane_runtime_is_stopping();
|
||||
}
|
||||
|
||||
static void control_plane_set_runtime_stop_requested(bool stop_requested)
|
||||
void control_plane_set_runtime_stop_requested(bool stop_requested)
|
||||
{
|
||||
portENTER_CRITICAL(&s_runtime_lock);
|
||||
s_runtime_stop_requested = stop_requested;
|
||||
|
||||
@@ -67,9 +67,15 @@ 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. */
|
||||
/** Enter the configured board deep-sleep state; a successful call does not return. */
|
||||
esp_err_t domain_battery_prepare_shutdown(void);
|
||||
|
||||
/** Return true when the current boot resumed through a deep-sleep wake source. */
|
||||
bool domain_battery_woke_from_deep_sleep(void);
|
||||
|
||||
/** Release low-power GPIO holds after accepting a deep-sleep wake. */
|
||||
void domain_battery_complete_wakeup(void);
|
||||
|
||||
typedef enum {
|
||||
DOMAIN_WIFI_PROMPT_WIFI_SUCCESS = 0,
|
||||
DOMAIN_WIFI_PROMPT_AFTER_WIFI_SUCCESS,
|
||||
|
||||
@@ -40,6 +40,14 @@ typedef struct {
|
||||
uint8_t percentage;
|
||||
} battery_curve_point_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t version;
|
||||
uint8_t reason;
|
||||
uint8_t percentage;
|
||||
uint8_t reserved;
|
||||
int32_t voltage_mv;
|
||||
} battery_shutdown_record_t;
|
||||
|
||||
static const battery_curve_point_t s_discharge_curve[] = {
|
||||
{3300, 0},
|
||||
{3400, 2},
|
||||
@@ -329,8 +337,37 @@ esp_err_t domain_battery_get_status(domain_battery_status_t *out_status)
|
||||
|
||||
esp_err_t domain_battery_prepare_shutdown(void)
|
||||
{
|
||||
/* Reserved for a future board-specific sleep or power-cut sequence. */
|
||||
#if CONFIG_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
battery_shutdown_record_t record = {
|
||||
.version = 1U,
|
||||
.reason = 1U,
|
||||
};
|
||||
if (lock_state()) {
|
||||
record.percentage = s_status.percentage;
|
||||
record.voltage_mv = s_status.voltage_mv;
|
||||
unlock_state();
|
||||
}
|
||||
/* Best-effort bounded record; a storage failure must not block deep sleep. */
|
||||
(void)platform_storage_set_blob("battery",
|
||||
"last_shutdown",
|
||||
&record,
|
||||
sizeof(record));
|
||||
return platform_power_enter_deep_sleep(
|
||||
CONFIG_TQ_BATTERY_CHARGE_GPIO,
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO);
|
||||
#else
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool domain_battery_woke_from_deep_sleep(void)
|
||||
{
|
||||
return platform_power_woke_from_deep_sleep();
|
||||
}
|
||||
|
||||
void domain_battery_complete_wakeup(void)
|
||||
{
|
||||
platform_power_release_deep_sleep_holds();
|
||||
}
|
||||
|
||||
#else
|
||||
@@ -358,4 +395,13 @@ esp_err_t domain_battery_prepare_shutdown(void)
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
bool domain_battery_woke_from_deep_sleep(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void domain_battery_complete_wakeup(void)
|
||||
{
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -44,6 +44,7 @@ set(platform_priv_requires
|
||||
app_update
|
||||
esp_http_client
|
||||
esp_http_server
|
||||
esp_hw_support
|
||||
esp_lcd
|
||||
esp_netif
|
||||
esp_psram
|
||||
|
||||
@@ -310,6 +310,18 @@ esp_err_t platform_stepper_rotate_angle_with_accel_timeout(float angle,
|
||||
esp_err_t platform_stepper_release_hold(int timeout_ms);
|
||||
void platform_stepper_power_off(void);
|
||||
|
||||
/** Return true when this boot was caused by a configured deep-sleep wake source. */
|
||||
bool platform_power_woke_from_deep_sleep(void);
|
||||
|
||||
/** Release board-output holds after a deep-sleep wake is accepted. */
|
||||
void platform_power_release_deep_sleep_holds(void);
|
||||
|
||||
/**
|
||||
* Enter deep sleep after configuring active-low charger and optional button
|
||||
* wake GPIOs. A successful call does not return.
|
||||
*/
|
||||
esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio);
|
||||
|
||||
typedef enum {
|
||||
PLATFORM_BATTERY_CHARGE_UNKNOWN = 0,
|
||||
PLATFORM_BATTERY_CHARGE_NOT_CHARGING,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_app_desc.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "platform_internal.h"
|
||||
@@ -20,6 +21,18 @@
|
||||
#ifndef CONFIG_TQ_STEPPER_ENABLE
|
||||
#define CONFIG_TQ_STEPPER_ENABLE 1
|
||||
#endif
|
||||
#ifndef CONFIG_MULTIMODAL_AUDIO_PA_GPIO
|
||||
#define CONFIG_MULTIMODAL_AUDIO_PA_GPIO -1
|
||||
#endif
|
||||
#ifndef CONFIG_TQ_SCREEN_BACKLIGHT_GPIO
|
||||
#define CONFIG_TQ_SCREEN_BACKLIGHT_GPIO -1
|
||||
#endif
|
||||
#ifndef CONFIG_TQ_STEPPER_IN1_GPIO
|
||||
#define CONFIG_TQ_STEPPER_IN1_GPIO -1
|
||||
#define CONFIG_TQ_STEPPER_IN2_GPIO -1
|
||||
#define CONFIG_TQ_STEPPER_IN3_GPIO -1
|
||||
#define CONFIG_TQ_STEPPER_IN4_GPIO -1
|
||||
#endif
|
||||
|
||||
static const char *TAG = "platform_core";
|
||||
static bool s_initialized;
|
||||
@@ -35,6 +48,106 @@ static const char *const s_known_nvs_namespaces[] = {
|
||||
"fortune",
|
||||
};
|
||||
|
||||
bool platform_power_woke_from_deep_sleep(void)
|
||||
{
|
||||
return esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_UNDEFINED;
|
||||
}
|
||||
|
||||
static void platform_power_set_output_low_and_hold(int gpio)
|
||||
{
|
||||
if (gpio < 0 || gpio >= GPIO_NUM_MAX) {
|
||||
return;
|
||||
}
|
||||
(void)gpio_hold_dis((gpio_num_t)gpio);
|
||||
(void)gpio_set_direction((gpio_num_t)gpio, GPIO_MODE_OUTPUT);
|
||||
(void)gpio_set_level((gpio_num_t)gpio, 0);
|
||||
(void)gpio_hold_en((gpio_num_t)gpio);
|
||||
}
|
||||
|
||||
static void platform_power_set_board_outputs_held(bool held)
|
||||
{
|
||||
const int output_gpios[] = {
|
||||
CONFIG_MULTIMODAL_AUDIO_PA_GPIO,
|
||||
CONFIG_TQ_SCREEN_BACKLIGHT_GPIO,
|
||||
CONFIG_TQ_STEPPER_IN1_GPIO,
|
||||
CONFIG_TQ_STEPPER_IN2_GPIO,
|
||||
CONFIG_TQ_STEPPER_IN3_GPIO,
|
||||
CONFIG_TQ_STEPPER_IN4_GPIO,
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(output_gpios) / sizeof(output_gpios[0]); ++i) {
|
||||
int gpio = output_gpios[i];
|
||||
if (gpio < 0 || gpio >= GPIO_NUM_MAX) {
|
||||
continue;
|
||||
}
|
||||
if (held) {
|
||||
platform_power_set_output_low_and_hold(gpio);
|
||||
} else {
|
||||
(void)gpio_hold_dis((gpio_num_t)gpio);
|
||||
}
|
||||
}
|
||||
if (held) {
|
||||
gpio_deep_sleep_hold_en();
|
||||
} else {
|
||||
gpio_deep_sleep_hold_dis();
|
||||
}
|
||||
}
|
||||
|
||||
void platform_power_release_deep_sleep_holds(void)
|
||||
{
|
||||
platform_power_set_board_outputs_held(false);
|
||||
}
|
||||
|
||||
esp_err_t platform_power_enter_deep_sleep(int charger_gpio, int button_gpio)
|
||||
{
|
||||
uint64_t wake_mask = 0U;
|
||||
const int wake_gpios[] = {charger_gpio, button_gpio};
|
||||
|
||||
for (size_t i = 0; i < sizeof(wake_gpios) / sizeof(wake_gpios[0]); ++i) {
|
||||
int gpio = wake_gpios[i];
|
||||
if (gpio < 0) {
|
||||
continue;
|
||||
}
|
||||
if (gpio >= GPIO_NUM_MAX ||
|
||||
!esp_sleep_is_valid_wakeup_gpio((gpio_num_t)gpio)) {
|
||||
ESP_LOGE(TAG, "Invalid deep-sleep wake GPIO: %d", gpio);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
gpio_config_t input_config = {
|
||||
.pin_bit_mask = 1ULL << (uint32_t)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(&input_config);
|
||||
if (gpio_err != ESP_OK) {
|
||||
return gpio_err;
|
||||
}
|
||||
wake_mask |= 1ULL << (uint32_t)gpio;
|
||||
}
|
||||
|
||||
if (wake_mask == 0U) {
|
||||
ESP_LOGE(TAG, "Deep sleep requires at least one wake GPIO");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t wake_err = esp_sleep_enable_ext1_wakeup_io(
|
||||
wake_mask,
|
||||
ESP_EXT1_WAKEUP_ANY_LOW);
|
||||
if (wake_err != ESP_OK) {
|
||||
return wake_err;
|
||||
}
|
||||
|
||||
platform_power_set_board_outputs_held(true);
|
||||
|
||||
ESP_LOGW(TAG,
|
||||
"Entering deep sleep wake_mask=0x%llx",
|
||||
(unsigned long long)wake_mask);
|
||||
esp_deep_sleep_start();
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
static esp_err_t platform_gpio_isr_service_init(void)
|
||||
{
|
||||
if (s_gpio_isr_service_installed) {
|
||||
|
||||
@@ -50,15 +50,33 @@ 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.
|
||||
them to preserve scan reliability.
|
||||
|
||||
## Deferred Policy
|
||||
## Controlled Critical-Battery Shutdown
|
||||
|
||||
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.
|
||||
When controlled shutdown is enabled, an unplugged battery at or below both the
|
||||
configured critical percentage and 3450 mV must produce three valid samples.
|
||||
An emergency raw voltage at or below 3350 mV requires two samples and is not
|
||||
blocked by the high-load suppression window. Either path latches a single
|
||||
shutdown sequence.
|
||||
|
||||
The sequence stops interaction sources, OTA, multimodal transport, active
|
||||
playback, and the stepper before synchronously playing
|
||||
`shutdown_power_empty.opus`. The prompt is mandatory: a failed or incomplete
|
||||
playback is retried and deep sleep is entered only after playback completion.
|
||||
The codec/PA, LCD backlight, Wi-Fi, and stepper outputs are then disabled. PA,
|
||||
backlight, and stepper GPIOs are held low through deep sleep. A small NVS record
|
||||
stores the shutdown reason, voltage, and percentage on a best-effort basis.
|
||||
|
||||
GPIO10 is configured as the default active-low wake source. The optional
|
||||
active-low button wake GPIO defaults to `-1` (disabled). After deep-sleep wake,
|
||||
normal boot continues only while charging or when battery voltage is at least
|
||||
3550 mV. Otherwise the device immediately returns to deep sleep before audio,
|
||||
LCD, Wi-Fi, or server startup.
|
||||
|
||||
This is a low-power software shutdown, not physical board power removal,
|
||||
because the board power-hold GPIO is disabled. The fixed battery divider and
|
||||
any board-level always-on loads continue to draw current.
|
||||
|
||||
## Bench Calibration
|
||||
|
||||
|
||||
@@ -1039,6 +1039,40 @@ menu "Example Configuration"
|
||||
range 1 100
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
bool "Enable controlled shutdown at critically low battery"
|
||||
default y
|
||||
depends on TQ_BATTERY_ENABLE
|
||||
|
||||
config TQ_BATTERY_SHUTDOWN_VOLTAGE_MV
|
||||
int "Controlled shutdown voltage threshold (mV)"
|
||||
default 3450
|
||||
range 3000 4200
|
||||
depends on TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
|
||||
config TQ_BATTERY_EMERGENCY_VOLTAGE_MV
|
||||
int "Emergency shutdown voltage threshold (mV)"
|
||||
default 3350
|
||||
range 3000 4200
|
||||
depends on TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
|
||||
config TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV
|
||||
int "Minimum voltage after deep-sleep wake (mV)"
|
||||
default 3550
|
||||
range 3000 4200
|
||||
depends on TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
|
||||
config TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO
|
||||
int "Optional active-low deep-sleep wake button GPIO (-1 to disable)"
|
||||
default -1
|
||||
range -1 48
|
||||
depends on TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
|
||||
config TQ_BATTERY_SHUTDOWN_PROMPT_FILE
|
||||
string "Critical-battery shutdown prompt file"
|
||||
default "/littlefs/audio_prompts/shutdown_power_empty.opus"
|
||||
depends on TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE
|
||||
|
||||
endmenu
|
||||
|
||||
menu "Wi-Fi Provisioning"
|
||||
|
||||
@@ -341,4 +341,10 @@ 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_TQ_BATTERY_CONTROLLED_SHUTDOWN_ENABLE=y
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_VOLTAGE_MV=3450
|
||||
CONFIG_TQ_BATTERY_EMERGENCY_VOLTAGE_MV=3350
|
||||
CONFIG_TQ_BATTERY_WAKE_RECOVERY_VOLTAGE_MV=3550
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_WAKE_BUTTON_GPIO=-1
|
||||
CONFIG_TQ_BATTERY_SHUTDOWN_PROMPT_FILE="/littlefs/audio_prompts/shutdown_power_empty.opus"
|
||||
CONFIG_MULTIMODAL_AUDIO_PA_GPIO=11
|
||||
|
||||
Reference in New Issue
Block a user