feat(control_plane): play prompt before light standby

This commit is contained in:
chenjintao
2026-08-21 15:13:23 +08:00
parent bba99a09ec
commit cb203a74f4
7 changed files with 235 additions and 12 deletions
@@ -41,6 +41,14 @@
#define CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT 10 #define CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT 10
#endif #endif
#ifndef CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE
#define CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE 0
#endif
#ifndef CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE
#define CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE "standby_5min.opus"
#endif
#ifndef CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT #ifndef CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT
#define CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT 0 #define CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT 0
#endif #endif
@@ -317,6 +325,44 @@ static esp_err_t control_plane_light_standby_apply_entry_policy(void)
return ESP_OK; return ESP_OK;
} }
static esp_err_t control_plane_light_standby_play_entry_prompt(void)
{
#if CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE
TickType_t started_tick = xTaskGetTickCount();
ESP_LOGI(TAG,
"Entry prompt started file=%s",
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE);
esp_err_t prompt_err = domain_runtime_behavior_play_prompt_file(
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE);
uint32_t elapsed_ms = (uint32_t)pdTICKS_TO_MS(
xTaskGetTickCount() - started_tick);
if (prompt_err != ESP_OK) {
ESP_LOGE(TAG,
"Entry prompt failed elapsed_ms=%u err=%s",
(unsigned)elapsed_ms,
esp_err_to_name(prompt_err));
return prompt_err;
}
ESP_LOGI(TAG,
"Entry prompt completed elapsed_ms=%u",
(unsigned)elapsed_ms);
#endif
return ESP_OK;
}
static bool control_plane_light_standby_entry_is_current(
uint64_t entry_activity_ms)
{
bool current;
portENTER_CRITICAL(&s_standby_lock);
current = s_standby.running &&
!s_standby.stop_requested &&
s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING &&
s_standby.last_activity_ms == entry_activity_ms;
portEXIT_CRITICAL(&s_standby_lock);
return current;
}
static void control_plane_light_standby_enter(uint64_t now_ms) static void control_plane_light_standby_enter(uint64_t now_ms)
{ {
if (!control_plane_light_standby_lock_transition()) { if (!control_plane_light_standby_lock_transition()) {
@@ -349,27 +395,41 @@ static void control_plane_light_standby_enter(uint64_t now_ms)
CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING, CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING,
"idle_timeout"); "idle_timeout");
esp_err_t policy_err = control_plane_light_standby_apply_entry_policy(); esp_err_t policy_err = control_plane_light_standby_play_entry_prompt();
bool policy_applied = false;
bool entry_current = policy_err == ESP_OK &&
control_plane_light_standby_entry_is_current(entry_activity_ms);
bool runtime_eligible = entry_current &&
control_plane_light_standby_runtime_is_eligible();
if (runtime_eligible) {
policy_applied = true;
policy_err = control_plane_light_standby_apply_entry_policy();
}
bool entered = false; bool entered = false;
bool restore_required = false; bool restore_required = false;
const char *completion_reason = "hardware_policy"; const char *completion_reason = "hardware_policy";
uint64_t completion_ms = control_plane_light_standby_now_ms();
portENTER_CRITICAL(&s_standby_lock); portENTER_CRITICAL(&s_standby_lock);
if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING && if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING &&
!s_standby.stop_requested && !s_standby.stop_requested &&
s_standby.last_activity_ms == entry_activity_ms && s_standby.last_activity_ms == entry_activity_ms &&
policy_applied &&
policy_err == ESP_OK) { policy_err == ESP_OK) {
s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY; s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY;
s_standby.standby_since_ms = now_ms; s_standby.standby_since_ms = completion_ms;
s_standby.transition_count++; s_standby.transition_count++;
entered = true; entered = true;
} else if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING) { } else if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING) {
s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING; s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING;
s_standby.transition_count++; s_standby.transition_count++;
restore_required = true; restore_required = policy_applied;
if (policy_err != ESP_OK) { if (policy_err != ESP_OK) {
completion_reason = "hardware_policy_failed"; completion_reason = policy_applied ?
"hardware_policy_failed" : "entry_prompt_failed";
} else if (s_standby.stop_requested) { } else if (s_standby.stop_requested) {
completion_reason = "stop_requested"; completion_reason = "stop_requested";
} else if (entry_current && !runtime_eligible) {
completion_reason = "runtime_ineligible";
} else { } else {
completion_reason = "activity_during_entry"; completion_reason = "activity_during_entry";
} }
@@ -399,7 +459,7 @@ static void control_plane_light_standby_enter(uint64_t now_ms)
s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE; s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_ACTIVE;
s_standby.standby_since_ms = 0U; s_standby.standby_since_ms = 0U;
if (policy_err != ESP_OK) { if (policy_err != ESP_OK) {
s_standby.last_activity_ms = now_ms; s_standby.last_activity_ms = completion_ms;
} }
s_standby.transition_count++; s_standby.transition_count++;
activated = true; activated = true;
@@ -631,8 +691,7 @@ esp_err_t control_plane_light_standby_record_activity(
s_standby.last_activity = activity; s_standby.last_activity = activity;
s_standby.last_activity_ms = now_ms; s_standby.last_activity_ms = now_ms;
task = s_standby.task; task = s_standby.task;
if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY || if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY) {
s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING) {
wake_required = true; wake_required = true;
} }
portEXIT_CRITICAL(&s_standby_lock); portEXIT_CRITICAL(&s_standby_lock);
@@ -51,6 +51,9 @@ esp_err_t multimodal_audio_unregister_pcm_observer(
esp_err_t multimodal_audio_handle_downstream_opus(const uint8_t *data, size_t len); esp_err_t multimodal_audio_handle_downstream_opus(const uint8_t *data, size_t len);
esp_err_t multimodal_audio_begin_downstream_response(uint32_t *out_playback_generation); esp_err_t multimodal_audio_begin_downstream_response(uint32_t *out_playback_generation);
esp_err_t multimodal_audio_finish_downstream_response(void); esp_err_t multimodal_audio_finish_downstream_response(void);
esp_err_t multimodal_audio_wait_downstream_playback_complete(
uint32_t playback_generation,
int timeout_ms);
esp_err_t multimodal_audio_stop_downstream_playback(void); esp_err_t multimodal_audio_stop_downstream_playback(void);
esp_err_t multimodal_audio_register_downstream_playback_callback( esp_err_t multimodal_audio_register_downstream_playback_callback(
multimodal_audio_downstream_playback_callback_t cb, multimodal_audio_downstream_playback_callback_t cb,
@@ -25,6 +25,7 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h" #include "freertos/idf_additions.h"
#include "freertos/queue.h" #include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "soc/soc_caps.h" #include "soc/soc_caps.h"
#include "sdkconfig.h" #include "sdkconfig.h"
@@ -356,6 +357,16 @@ typedef struct {
static multimodal_audio_ctx_t s_audio; static multimodal_audio_ctx_t s_audio;
static multimodal_audio_downstream_playback_callback_t s_downstream_playback_cb; static multimodal_audio_downstream_playback_callback_t s_downstream_playback_cb;
static void *s_downstream_playback_cb_ctx; static void *s_downstream_playback_cb_ctx;
static SemaphoreHandle_t s_downstream_playback_completion_sem;
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
static StaticSemaphore_t s_downstream_playback_completion_sem_storage;
#endif
static bool s_downstream_playback_wait_registered;
static uint32_t s_downstream_playback_wait_generation;
static bool s_downstream_last_terminal_valid;
static uint32_t s_downstream_last_terminal_generation;
static multimodal_audio_downstream_playback_event_type_t
s_downstream_last_terminal_type;
static multimodal_audio_pcm_observer_t s_pcm_observer; static multimodal_audio_pcm_observer_t s_pcm_observer;
static void *s_pcm_observer_ctx; static void *s_pcm_observer_ctx;
static uint32_t s_pcm_observer_in_flight; static uint32_t s_pcm_observer_in_flight;
@@ -1212,10 +1212,29 @@ esp_err_t multimodal_audio_init(void)
ESP_LOGW(TAG, "Codec path disabled by configuration"); ESP_LOGW(TAG, "Codec path disabled by configuration");
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
#else #else
if (s_downstream_playback_completion_sem == NULL) {
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
s_downstream_playback_completion_sem = xSemaphoreCreateBinaryStatic(
&s_downstream_playback_completion_sem_storage);
#else
s_downstream_playback_completion_sem = xSemaphoreCreateBinary();
#endif
if (s_downstream_playback_completion_sem == NULL) {
return ESP_ERR_NO_MEM;
}
}
if (s_audio.initialized) { if (s_audio.initialized) {
return ESP_OK; return ESP_OK;
} }
(void)xSemaphoreTake(s_downstream_playback_completion_sem, 0U);
portENTER_CRITICAL(&s_audio_runtime_lock);
s_downstream_playback_wait_registered = false;
s_downstream_playback_wait_generation = 0U;
s_downstream_last_terminal_valid = false;
s_downstream_last_terminal_generation = 0U;
portEXIT_CRITICAL(&s_audio_runtime_lock);
int retry_count = get_audio_init_retry_count(); int retry_count = get_audio_init_retry_count();
esp_err_t last_err = ESP_FAIL; esp_err_t last_err = ESP_FAIL;
@@ -1677,6 +1696,26 @@ static void dispatch_downstream_playback_event(
multimodal_audio_downstream_playback_event_type_t type, multimodal_audio_downstream_playback_event_type_t type,
uint32_t playback_generation) uint32_t playback_generation)
{ {
bool terminal = type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_COMPLETED ||
type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED ||
type == MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STALLED;
bool notify_waiter = false;
if (terminal) {
portENTER_CRITICAL(&s_audio_runtime_lock);
s_downstream_last_terminal_valid = true;
s_downstream_last_terminal_generation = playback_generation;
s_downstream_last_terminal_type = type;
if (s_downstream_playback_wait_registered &&
s_downstream_playback_wait_generation == playback_generation) {
s_downstream_playback_wait_registered = false;
notify_waiter = true;
}
portEXIT_CRITICAL(&s_audio_runtime_lock);
if (notify_waiter && s_downstream_playback_completion_sem != NULL) {
(void)xSemaphoreGive(s_downstream_playback_completion_sem);
}
}
multimodal_audio_downstream_playback_callback_t cb = NULL; multimodal_audio_downstream_playback_callback_t cb = NULL;
void *cb_ctx = NULL; void *cb_ctx = NULL;
snapshot_downstream_playback_callback(&cb, &cb_ctx); snapshot_downstream_playback_callback(&cb, &cb_ctx);
@@ -2974,7 +3013,19 @@ esp_err_t multimodal_audio_begin_downstream_response(
} }
TickType_t begin_tick = xTaskGetTickCount(); TickType_t begin_tick = xTaskGetTickCount();
bool notify_interrupted_waiter = false;
portENTER_CRITICAL(&s_audio_runtime_lock); portENTER_CRITICAL(&s_audio_runtime_lock);
if (s_downstream_playback_wait_registered &&
s_downstream_playback_wait_generation ==
ctx->downstream_playback_generation) {
s_downstream_last_terminal_valid = true;
s_downstream_last_terminal_generation =
ctx->downstream_playback_generation;
s_downstream_last_terminal_type =
MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED;
s_downstream_playback_wait_registered = false;
notify_interrupted_waiter = true;
}
uint32_t playback_generation = advance_downstream_playback_generation_locked(); uint32_t playback_generation = advance_downstream_playback_generation_locked();
ctx->downstream_response_active = true; ctx->downstream_response_active = true;
ctx->downstream_response_end_received = false; ctx->downstream_response_end_received = false;
@@ -2983,6 +3034,10 @@ esp_err_t multimodal_audio_begin_downstream_response(
ctx->downstream_last_audio_tick = begin_tick; ctx->downstream_last_audio_tick = begin_tick;
ctx->downstream_last_response_activity_tick = begin_tick; ctx->downstream_last_response_activity_tick = begin_tick;
portEXIT_CRITICAL(&s_audio_runtime_lock); portEXIT_CRITICAL(&s_audio_runtime_lock);
if (notify_interrupted_waiter &&
s_downstream_playback_completion_sem != NULL) {
(void)xSemaphoreGive(s_downstream_playback_completion_sem);
}
*out_playback_generation = playback_generation; *out_playback_generation = playback_generation;
s_downstream_queued_packet_total = 0U; s_downstream_queued_packet_total = 0U;
@@ -3055,3 +3110,72 @@ esp_err_t multimodal_audio_finish_downstream_response(void)
} }
return ESP_OK; return ESP_OK;
} }
static esp_err_t downstream_terminal_type_to_error(
multimodal_audio_downstream_playback_event_type_t type)
{
switch (type) {
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_COMPLETED:
return ESP_OK;
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STALLED:
return ESP_ERR_TIMEOUT;
case MULTIMODAL_AUDIO_DOWNSTREAM_PLAYBACK_STOPPED:
default:
return ESP_ERR_INVALID_STATE;
}
}
esp_err_t multimodal_audio_wait_downstream_playback_complete(
uint32_t playback_generation,
int timeout_ms)
{
if (playback_generation == 0U || timeout_ms < 0) {
return ESP_ERR_INVALID_ARG;
}
if (s_downstream_playback_completion_sem == NULL) {
return ESP_ERR_INVALID_STATE;
}
/* Remove a signal left by a waiter that timed out at a terminal-event race. */
(void)xSemaphoreTake(s_downstream_playback_completion_sem, 0U);
esp_err_t result = ESP_ERR_TIMEOUT;
portENTER_CRITICAL(&s_audio_runtime_lock);
if (s_downstream_last_terminal_valid &&
s_downstream_last_terminal_generation == playback_generation) {
result = downstream_terminal_type_to_error(
s_downstream_last_terminal_type);
portEXIT_CRITICAL(&s_audio_runtime_lock);
return result;
}
if (s_audio.downstream_playback_generation != playback_generation) {
portEXIT_CRITICAL(&s_audio_runtime_lock);
return ESP_ERR_INVALID_STATE;
}
if (s_downstream_playback_wait_registered) {
portEXIT_CRITICAL(&s_audio_runtime_lock);
return ESP_ERR_INVALID_STATE;
}
s_downstream_playback_wait_registered = true;
s_downstream_playback_wait_generation = playback_generation;
portEXIT_CRITICAL(&s_audio_runtime_lock);
TickType_t wait_ticks = pdMS_TO_TICKS((uint32_t)timeout_ms);
if (timeout_ms > 0 && wait_ticks == 0U) {
wait_ticks = 1U;
}
(void)xSemaphoreTake(s_downstream_playback_completion_sem, wait_ticks);
portENTER_CRITICAL(&s_audio_runtime_lock);
if (s_downstream_last_terminal_valid &&
s_downstream_last_terminal_generation == playback_generation) {
result = downstream_terminal_type_to_error(
s_downstream_last_terminal_type);
}
if (s_downstream_playback_wait_registered &&
s_downstream_playback_wait_generation == playback_generation) {
s_downstream_playback_wait_registered = false;
}
portEXIT_CRITICAL(&s_audio_runtime_lock);
return result;
}
@@ -38,7 +38,7 @@
#define VOICE_PROMPT_FRAME_HEADER_BYTES 4U #define VOICE_PROMPT_FRAME_HEADER_BYTES 4U
#define VOICE_PROMPT_MAX_OPUS_PACKET_BYTES 2048U #define VOICE_PROMPT_MAX_OPUS_PACKET_BYTES 2048U
#define VOICE_PROMPT_OPUS_FRAME_MS 60U #define VOICE_PROMPT_OPUS_FRAME_MS 60U
#define VOICE_PROMPT_DRAIN_MARGIN_MS 120U #define VOICE_PROMPT_COMPLETION_TIMEOUT_MARGIN_MS 3000U
static const char *TAG = "power_manager"; static const char *TAG = "power_manager";
static const char *VOICE_TAG = "voice_prompt"; static const char *VOICE_TAG = "voice_prompt";
@@ -308,11 +308,20 @@ esp_err_t voice_prompt_play_file(const char *path)
} }
if (err == ESP_OK) { if (err == ESP_OK) {
uint32_t drain_ms = packet_count * VOICE_PROMPT_OPUS_FRAME_MS + uint64_t timeout_ms =
VOICE_PROMPT_DRAIN_MARGIN_MS; (uint64_t)packet_count * VOICE_PROMPT_OPUS_FRAME_MS +
vTaskDelay(pdMS_TO_TICKS(drain_ms)); VOICE_PROMPT_COMPLETION_TIMEOUT_MARGIN_MS;
if (timeout_ms > INT32_MAX) {
timeout_ms = INT32_MAX;
}
err = multimodal_audio_wait_downstream_playback_complete(
playback_generation,
(int)timeout_ms);
}
if (err == ESP_OK) {
ESP_LOGI(VOICE_TAG, ESP_LOGI(VOICE_TAG,
"Prompt queued: %s packets=%u bytes=%u", "Prompt completed: %s packets=%u bytes=%u",
resolved_path, resolved_path,
(unsigned)packet_count, (unsigned)packet_count,
(unsigned)total_bytes); (unsigned)total_bytes);
+15
View File
@@ -356,6 +356,21 @@ menu "Example Configuration"
Backlight brightness while the LCD render loop continues. Backlight brightness while the LCD render loop continues.
The minimum remains visible so the base expression persists. The minimum remains visible so the base expression persists.
config TQ_LIGHT_STANDBY_PROMPT_ENABLE
bool "Play a prompt before entering light standby"
default y
depends on TQ_LIGHT_STANDBY_ENABLE
help
Play the configured prompt to completion before applying
standby policies such as disabling the speaker amplifier.
config TQ_LIGHT_STANDBY_PROMPT_FILE
string "Light-standby entry prompt file"
default "standby_5min.opus"
depends on TQ_LIGHT_STANDBY_PROMPT_ENABLE
help
Prompt file in the LittleFS audio_prompts directory.
config TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT config TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT
bool "Disable speaker output in light standby" bool "Disable speaker output in light standby"
default y default y
+2
View File
@@ -54,6 +54,8 @@ CONFIG_TQ_LIGHT_STANDBY_ENABLE=y
CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS=300000 CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS=300000
CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT=y CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT=y
CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10 CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT=10
CONFIG_TQ_LIGHT_STANDBY_PROMPT_ENABLE=y
CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="standby_5min.opus"
CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y
CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y
CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y