diff --git a/components/control_plane/src/runtime/control_plane_light_standby.c b/components/control_plane/src/runtime/control_plane_light_standby.c index 815ae4f..d7e3939 100644 --- a/components/control_plane/src/runtime/control_plane_light_standby.c +++ b/components/control_plane/src/runtime/control_plane_light_standby.c @@ -41,6 +41,14 @@ #define CONFIG_TQ_LIGHT_STANDBY_BACKLIGHT_PERCENT 10 #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 #define CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT 0 #endif @@ -317,6 +325,44 @@ static esp_err_t control_plane_light_standby_apply_entry_policy(void) 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) { 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, "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 restore_required = false; const char *completion_reason = "hardware_policy"; + uint64_t completion_ms = control_plane_light_standby_now_ms(); portENTER_CRITICAL(&s_standby_lock); if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING && !s_standby.stop_requested && s_standby.last_activity_ms == entry_activity_ms && + policy_applied && policy_err == ESP_OK) { 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++; entered = true; } else if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING) { s_standby.state = CONTROL_PLANE_LIGHT_STANDBY_STATE_WAKING; s_standby.transition_count++; - restore_required = true; + restore_required = policy_applied; 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) { completion_reason = "stop_requested"; + } else if (entry_current && !runtime_eligible) { + completion_reason = "runtime_ineligible"; } else { 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.standby_since_ms = 0U; if (policy_err != ESP_OK) { - s_standby.last_activity_ms = now_ms; + s_standby.last_activity_ms = completion_ms; } s_standby.transition_count++; activated = true; @@ -631,8 +691,7 @@ esp_err_t control_plane_light_standby_record_activity( s_standby.last_activity = activity; s_standby.last_activity_ms = now_ms; task = s_standby.task; - if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY || - s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_ENTERING) { + if (s_standby.state == CONTROL_PLANE_LIGHT_STANDBY_STATE_STANDBY) { wake_required = true; } portEXIT_CRITICAL(&s_standby_lock); diff --git a/components/platform/internal/multimodal/audio_pipeline.h b/components/platform/internal/multimodal/audio_pipeline.h index 229fe54..1fa691a 100644 --- a/components/platform/internal/multimodal/audio_pipeline.h +++ b/components/platform/internal/multimodal/audio_pipeline.h @@ -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_begin_downstream_response(uint32_t *out_playback_generation); 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_register_downstream_playback_callback( multimodal_audio_downstream_playback_callback_t cb, diff --git a/components/platform/src/multimodal/audio_pipeline.c b/components/platform/src/multimodal/audio_pipeline.c index 83caba1..cf79852 100644 --- a/components/platform/src/multimodal/audio_pipeline.c +++ b/components/platform/src/multimodal/audio_pipeline.c @@ -25,6 +25,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" #include "freertos/queue.h" +#include "freertos/semphr.h" #include "freertos/task.h" #include "soc/soc_caps.h" #include "sdkconfig.h" @@ -356,6 +357,16 @@ typedef struct { static multimodal_audio_ctx_t s_audio; static multimodal_audio_downstream_playback_callback_t s_downstream_playback_cb; 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 void *s_pcm_observer_ctx; static uint32_t s_pcm_observer_in_flight; diff --git a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c index 1225732..3b4622d 100644 --- a/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c +++ b/components/platform/src/multimodal/audio_pipeline/audio_pipeline_runtime_impl.c @@ -1212,10 +1212,29 @@ esp_err_t multimodal_audio_init(void) ESP_LOGW(TAG, "Codec path disabled by configuration"); return ESP_ERR_NOT_SUPPORTED; #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) { 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(); 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, 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; void *cb_ctx = NULL; snapshot_downstream_playback_callback(&cb, &cb_ctx); @@ -2974,7 +3013,19 @@ esp_err_t multimodal_audio_begin_downstream_response( } TickType_t begin_tick = xTaskGetTickCount(); + bool notify_interrupted_waiter = false; 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(); ctx->downstream_response_active = true; 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_response_activity_tick = begin_tick; 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; s_downstream_queued_packet_total = 0U; @@ -3055,3 +3110,72 @@ esp_err_t multimodal_audio_finish_downstream_response(void) } 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; +} diff --git a/components/platform/src/platform_behavior_stubs.c b/components/platform/src/platform_behavior_stubs.c index db0bd76..56c866f 100644 --- a/components/platform/src/platform_behavior_stubs.c +++ b/components/platform/src/platform_behavior_stubs.c @@ -38,7 +38,7 @@ #define VOICE_PROMPT_FRAME_HEADER_BYTES 4U #define VOICE_PROMPT_MAX_OPUS_PACKET_BYTES 2048U #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 *VOICE_TAG = "voice_prompt"; @@ -308,11 +308,20 @@ esp_err_t voice_prompt_play_file(const char *path) } if (err == ESP_OK) { - uint32_t drain_ms = packet_count * VOICE_PROMPT_OPUS_FRAME_MS + - VOICE_PROMPT_DRAIN_MARGIN_MS; - vTaskDelay(pdMS_TO_TICKS(drain_ms)); + uint64_t timeout_ms = + (uint64_t)packet_count * VOICE_PROMPT_OPUS_FRAME_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, - "Prompt queued: %s packets=%u bytes=%u", + "Prompt completed: %s packets=%u bytes=%u", resolved_path, (unsigned)packet_count, (unsigned)total_bytes); diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 6c36066..5728860 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -356,6 +356,21 @@ menu "Example Configuration" Backlight brightness while the LCD render loop continues. 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 bool "Disable speaker output in light standby" default y diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 9b4fa25..9946604 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -54,6 +54,8 @@ CONFIG_TQ_LIGHT_STANDBY_ENABLE=y CONFIG_TQ_LIGHT_STANDBY_IDLE_TIMEOUT_MS=300000 CONFIG_TQ_LIGHT_STANDBY_DIM_BACKLIGHT=y 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_RELEASE_MOTOR_HOLD=y CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y