feat(control-plane): add network outage voice feedback

This commit is contained in:
chenjintao
2026-08-25 09:30:55 +08:00
parent ae27d5bbaa
commit ceb0780425
6 changed files with 857 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@ idf_component_register(
"src/runtime/control_plane_fortune_service.c"
"src/runtime/control_plane_system_events.c"
"src/runtime/control_plane_light_standby.c"
"src/runtime/control_plane_network_feedback.c"
"src/runtime/control_plane_key_input.c"
"src/runtime/control_plane_reset_service.c"
"src/runtime/control_plane_remote_log_service.c"
@@ -96,6 +96,11 @@ typedef enum {
CONTROL_PLANE_TRANSPORT_LOSS_PROTOCOL_ERROR,
} control_plane_transport_loss_reason_t;
typedef enum {
CONTROL_PLANE_NETWORK_ACTION_CONVERSATION = 0,
CONTROL_PLANE_NETWORK_ACTION_CLOUD_CHOUQIAN,
} control_plane_network_action_t;
esp_err_t control_plane_multimodal_start(void);
esp_err_t control_plane_multimodal_stop(void);
bool control_plane_multimodal_is_connected(void);
@@ -149,6 +154,16 @@ 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);
void control_plane_network_feedback_reset(void);
void control_plane_network_feedback_mark_wifi_down(void);
void control_plane_network_feedback_mark_wifi_recovered(void);
void control_plane_network_feedback_mark_server_down(void);
void control_plane_network_feedback_mark_service_ready(void);
void control_plane_network_feedback_service(void);
esp_err_t control_plane_network_feedback_require_ready(
control_plane_network_action_t action);
void control_plane_request_wifi_reconnect_now(void);
esp_err_t control_plane_battery_service_stop(int timeout_ms);
void control_plane_battery_service_set_light_standby(bool standby);
void control_plane_runtime_diagnostics_set_light_standby(bool standby);
@@ -690,6 +690,19 @@ esp_err_t control_plane_request_cloud_chouqian(void)
return ESP_ERR_INVALID_STATE;
}
portENTER_CRITICAL(&s_multimodal_runtime_lock);
bool request_pending = s_cloud_chouqian_pending;
portEXIT_CRITICAL(&s_multimodal_runtime_lock);
if (request_pending) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t network_err = control_plane_network_feedback_require_ready(
CONTROL_PLANE_NETWORK_ACTION_CLOUD_CHOUQIAN);
if (network_err != ESP_OK) {
return network_err;
}
uint32_t requested_ms = control_plane_now_ms();
portENTER_CRITICAL(&s_multimodal_runtime_lock);
@@ -895,11 +908,13 @@ static void control_plane_multimodal_event_bridge(control_plane_multimodal_event
switch (event) {
case CONTROL_PLANE_MULTIMODAL_EVENT_TRANSPORT_CONNECTED: {
control_plane_network_feedback_mark_service_ready();
control_plane_schedule_server_success_prompt();
break;
}
case CONTROL_PLANE_MULTIMODAL_EVENT_TRANSPORT_DISCONNECTED:
control_plane_network_feedback_mark_server_down();
control_plane_clear_downstream_stall_recovery();
control_plane_clear_cloud_chouqian_pending("transport disconnected");
control_plane_multimodal_stop_uplink();
@@ -908,6 +923,7 @@ static void control_plane_multimodal_event_bridge(control_plane_multimodal_event
break;
case CONTROL_PLANE_MULTIMODAL_EVENT_TRANSPORT_ERROR:
control_plane_network_feedback_mark_server_down();
control_plane_clear_downstream_stall_recovery();
control_plane_clear_cloud_chouqian_pending("transport error");
control_plane_multimodal_stop_uplink();
@@ -0,0 +1,315 @@
/*
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "control_plane_internal.h"
#include <stdbool.h>
#include <stdint.h>
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#define CONTROL_PLANE_SERVER_DISCONNECT_GRACE_MS 3000U
#define CONTROL_PLANE_NETWORK_PROMPT_COOLDOWN_MS 5000U
#define CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE \
"/littlefs/audio_prompts/network_interrupted_repairing.opus"
#define CONTROL_PLANE_WIFI_ACTION_BLOCKED_PROMPT_FILE \
"/littlefs/audio_prompts/network_unstable.opus"
#define CONTROL_PLANE_SERVER_INTERRUPTED_PROMPT_FILE \
"/littlefs/audio_prompts/server_abnormal_interrupted.opus"
#define CONTROL_PLANE_SERVER_ACTION_BLOCKED_PROMPT_FILE \
"/littlefs/audio_prompts/server_repair_wait_reopen.opus"
#define CONTROL_PLANE_NETWORK_RESTORED_PROMPT_FILE \
"/littlefs/audio_prompts/signal_restored_business_resume.opus"
typedef enum {
NETWORK_FEEDBACK_STATE_BOOTING = 0,
NETWORK_FEEDBACK_STATE_READY,
NETWORK_FEEDBACK_STATE_WIFI_DOWN,
NETWORK_FEEDBACK_STATE_SERVER_DOWN,
} network_feedback_state_t;
typedef enum {
NETWORK_FEEDBACK_PROMPT_NONE = 0,
NETWORK_FEEDBACK_PROMPT_WIFI_INTERRUPTED,
NETWORK_FEEDBACK_PROMPT_SERVER_INTERRUPTED,
NETWORK_FEEDBACK_PROMPT_RESTORED,
} network_feedback_prompt_t;
typedef struct {
network_feedback_state_t state;
network_feedback_prompt_t pending_prompt;
bool service_ever_ready;
bool outage_active;
bool outage_announced;
bool passive_prompt_attempted;
bool prompt_in_progress;
int64_t server_down_since_us;
int64_t last_fault_prompt_us;
} network_feedback_runtime_t;
static const char *TAG = "cp_network_feedback";
static network_feedback_runtime_t s_feedback;
static portMUX_TYPE s_feedback_lock = portMUX_INITIALIZER_UNLOCKED;
static int64_t control_plane_network_feedback_now_us(void)
{
return esp_timer_get_time();
}
static const char *control_plane_network_action_name(control_plane_network_action_t action)
{
switch (action) {
case CONTROL_PLANE_NETWORK_ACTION_CONVERSATION:
return "conversation";
case CONTROL_PLANE_NETWORK_ACTION_CLOUD_CHOUQIAN:
return "cloud_chouqian";
default:
return "unknown";
}
}
static const char *control_plane_network_prompt_path(network_feedback_prompt_t prompt)
{
switch (prompt) {
case NETWORK_FEEDBACK_PROMPT_WIFI_INTERRUPTED:
return CONTROL_PLANE_WIFI_INTERRUPTED_PROMPT_FILE;
case NETWORK_FEEDBACK_PROMPT_SERVER_INTERRUPTED:
return CONTROL_PLANE_SERVER_INTERRUPTED_PROMPT_FILE;
case NETWORK_FEEDBACK_PROMPT_RESTORED:
return CONTROL_PLANE_NETWORK_RESTORED_PROMPT_FILE;
default:
return NULL;
}
}
static void control_plane_network_feedback_begin_outage_locked(void)
{
if (s_feedback.outage_active) {
return;
}
s_feedback.outage_active = true;
s_feedback.outage_announced = false;
s_feedback.passive_prompt_attempted = false;
s_feedback.pending_prompt = NETWORK_FEEDBACK_PROMPT_NONE;
}
static bool control_plane_network_feedback_begin_prompt_locked(int64_t now_us)
{
if (s_feedback.prompt_in_progress) {
return false;
}
if (s_feedback.last_fault_prompt_us > 0 &&
now_us - s_feedback.last_fault_prompt_us <
(int64_t)CONTROL_PLANE_NETWORK_PROMPT_COOLDOWN_MS * 1000LL) {
return false;
}
s_feedback.prompt_in_progress = true;
s_feedback.last_fault_prompt_us = now_us;
s_feedback.outage_announced = true;
return true;
}
static void control_plane_network_feedback_end_prompt(void)
{
portENTER_CRITICAL(&s_feedback_lock);
s_feedback.prompt_in_progress = false;
portEXIT_CRITICAL(&s_feedback_lock);
}
static esp_err_t control_plane_network_feedback_play(const char *path,
const char *reason)
{
ESP_LOGI(TAG, "Play network prompt: reason=%s path=%s", reason, path);
esp_err_t err = domain_runtime_behavior_play_prompt_file(path);
if (err != ESP_OK) {
ESP_LOGW(TAG,
"Network prompt failed: reason=%s err=%s",
reason,
esp_err_to_name(err));
}
control_plane_network_feedback_end_prompt();
return err;
}
void control_plane_network_feedback_reset(void)
{
portENTER_CRITICAL(&s_feedback_lock);
s_feedback = (network_feedback_runtime_t){
.state = NETWORK_FEEDBACK_STATE_BOOTING,
};
portEXIT_CRITICAL(&s_feedback_lock);
}
void control_plane_network_feedback_mark_wifi_down(void)
{
portENTER_CRITICAL(&s_feedback_lock);
control_plane_network_feedback_begin_outage_locked();
s_feedback.state = NETWORK_FEEDBACK_STATE_WIFI_DOWN;
s_feedback.server_down_since_us = 0;
if (!s_feedback.outage_announced) {
s_feedback.pending_prompt = NETWORK_FEEDBACK_PROMPT_WIFI_INTERRUPTED;
s_feedback.passive_prompt_attempted = true;
}
portEXIT_CRITICAL(&s_feedback_lock);
}
void control_plane_network_feedback_mark_wifi_recovered(void)
{
int64_t now_us = control_plane_network_feedback_now_us();
portENTER_CRITICAL(&s_feedback_lock);
if (s_feedback.state == NETWORK_FEEDBACK_STATE_WIFI_DOWN) {
s_feedback.state = NETWORK_FEEDBACK_STATE_SERVER_DOWN;
s_feedback.server_down_since_us = now_us;
s_feedback.pending_prompt = NETWORK_FEEDBACK_PROMPT_NONE;
}
portEXIT_CRITICAL(&s_feedback_lock);
}
void control_plane_network_feedback_mark_server_down(void)
{
int64_t now_us = control_plane_network_feedback_now_us();
portENTER_CRITICAL(&s_feedback_lock);
if (s_feedback.state != NETWORK_FEEDBACK_STATE_WIFI_DOWN) {
if (s_feedback.service_ever_ready) {
control_plane_network_feedback_begin_outage_locked();
}
s_feedback.state = NETWORK_FEEDBACK_STATE_SERVER_DOWN;
if (s_feedback.server_down_since_us == 0) {
s_feedback.server_down_since_us = now_us;
}
}
portEXIT_CRITICAL(&s_feedback_lock);
}
void control_plane_network_feedback_mark_service_ready(void)
{
portENTER_CRITICAL(&s_feedback_lock);
bool should_announce_recovery = s_feedback.service_ever_ready &&
s_feedback.outage_active &&
s_feedback.outage_announced;
s_feedback.service_ever_ready = true;
s_feedback.state = NETWORK_FEEDBACK_STATE_READY;
s_feedback.outage_active = false;
s_feedback.passive_prompt_attempted = false;
s_feedback.server_down_since_us = 0;
s_feedback.pending_prompt = should_announce_recovery ?
NETWORK_FEEDBACK_PROMPT_RESTORED :
NETWORK_FEEDBACK_PROMPT_NONE;
portEXIT_CRITICAL(&s_feedback_lock);
}
void control_plane_network_feedback_service(void)
{
if (!control_plane_ota_voice_services_allowed() ||
control_plane_is_mini_program_provisioning_running()) {
return;
}
int64_t now_us = control_plane_network_feedback_now_us();
network_feedback_prompt_t prompt = NETWORK_FEEDBACK_PROMPT_NONE;
portENTER_CRITICAL(&s_feedback_lock);
if (s_feedback.state == NETWORK_FEEDBACK_STATE_SERVER_DOWN &&
s_feedback.service_ever_ready &&
s_feedback.outage_active &&
!s_feedback.outage_announced &&
!s_feedback.passive_prompt_attempted &&
s_feedback.server_down_since_us > 0 &&
now_us - s_feedback.server_down_since_us >=
(int64_t)CONTROL_PLANE_SERVER_DISCONNECT_GRACE_MS * 1000LL) {
s_feedback.pending_prompt = NETWORK_FEEDBACK_PROMPT_SERVER_INTERRUPTED;
s_feedback.passive_prompt_attempted = true;
}
bool fault_prompt_cooling_down =
s_feedback.pending_prompt != NETWORK_FEEDBACK_PROMPT_RESTORED &&
s_feedback.last_fault_prompt_us > 0 &&
now_us - s_feedback.last_fault_prompt_us <
(int64_t)CONTROL_PLANE_NETWORK_PROMPT_COOLDOWN_MS * 1000LL;
if (s_feedback.pending_prompt != NETWORK_FEEDBACK_PROMPT_NONE &&
!s_feedback.prompt_in_progress &&
!fault_prompt_cooling_down) {
prompt = s_feedback.pending_prompt;
s_feedback.pending_prompt = NETWORK_FEEDBACK_PROMPT_NONE;
s_feedback.prompt_in_progress = true;
if (prompt == NETWORK_FEEDBACK_PROMPT_RESTORED) {
s_feedback.outage_announced = false;
} else {
s_feedback.last_fault_prompt_us = now_us;
s_feedback.outage_announced = true;
}
}
portEXIT_CRITICAL(&s_feedback_lock);
const char *path = control_plane_network_prompt_path(prompt);
if (path != NULL) {
(void)control_plane_network_feedback_play(path, "state_change");
}
}
esp_err_t control_plane_network_feedback_require_ready(
control_plane_network_action_t action)
{
bool wifi_connected = control_plane_is_sta_connected();
bool server_connected = wifi_connected && control_plane_multimodal_is_connected();
if (server_connected) {
return ESP_OK;
}
portENTER_CRITICAL(&s_feedback_lock);
bool confirmed_outage = s_feedback.state == NETWORK_FEEDBACK_STATE_WIFI_DOWN ||
s_feedback.state == NETWORK_FEEDBACK_STATE_SERVER_DOWN;
portEXIT_CRITICAL(&s_feedback_lock);
if (wifi_connected && !confirmed_outage) {
return ESP_OK;
}
int64_t now_us = control_plane_network_feedback_now_us();
bool should_play = false;
const char *prompt_path = NULL;
portENTER_CRITICAL(&s_feedback_lock);
control_plane_network_feedback_begin_outage_locked();
if (!wifi_connected) {
s_feedback.state = NETWORK_FEEDBACK_STATE_WIFI_DOWN;
s_feedback.server_down_since_us = 0;
prompt_path = CONTROL_PLANE_WIFI_ACTION_BLOCKED_PROMPT_FILE;
} else {
s_feedback.state = NETWORK_FEEDBACK_STATE_SERVER_DOWN;
if (s_feedback.server_down_since_us == 0) {
s_feedback.server_down_since_us = now_us;
}
prompt_path = CONTROL_PLANE_SERVER_ACTION_BLOCKED_PROMPT_FILE;
}
s_feedback.pending_prompt = NETWORK_FEEDBACK_PROMPT_NONE;
s_feedback.passive_prompt_attempted = true;
should_play = control_plane_network_feedback_begin_prompt_locked(now_us);
portEXIT_CRITICAL(&s_feedback_lock);
ESP_LOGI(TAG,
"Reject network action: action=%s wifi=%d server=%d prompt=%d",
control_plane_network_action_name(action),
wifi_connected ? 1 : 0,
server_connected ? 1 : 0,
should_play ? 1 : 0);
if (should_play) {
(void)control_plane_network_feedback_play(prompt_path,
control_plane_network_action_name(action));
}
if (wifi_connected) {
(void)control_plane_schedule_websocket_wait_task();
} else {
control_plane_request_wifi_reconnect_now();
}
return ESP_ERR_INVALID_STATE;
}
@@ -246,6 +246,7 @@ static QueueHandle_t s_model_cmd_queue;
static bool s_model_cmd_queue_with_caps;
static TaskHandle_t s_model_cmd_task;
static bool s_ws_start_requested;
static bool s_wifi_reconnect_now_requested;
#if CONFIG_TQ_NETWORK_AUTO_REASSOCIATION_ENABLE
static uint32_t s_multimodal_network_failure_count;
static bool s_wifi_reassociation_requested;
@@ -594,6 +595,26 @@ static void control_plane_notify_wifi_state_task(void)
}
}
void control_plane_request_wifi_reconnect_now(void)
{
portENTER_CRITICAL(&s_runtime_lock);
if (!s_runtime_stop_requested) {
s_wifi_reconnect_now_requested = true;
}
portEXIT_CRITICAL(&s_runtime_lock);
control_plane_notify_wifi_state_task();
}
static bool control_plane_consume_wifi_reconnect_now_request(void)
{
bool requested = false;
portENTER_CRITICAL(&s_runtime_lock);
requested = s_wifi_reconnect_now_requested;
s_wifi_reconnect_now_requested = false;
portEXIT_CRITICAL(&s_runtime_lock);
return requested;
}
static void control_plane_request_ptt_expression(
domain_expression_id_t id,
domain_expression_trigger_t trigger)
@@ -700,6 +721,13 @@ static void control_plane_set_ptt_active_with_trigger(
}
control_plane_local_wake_pause_for_foreground();
if (control_plane_network_feedback_require_ready(
CONTROL_PLANE_NETWORK_ACTION_CONVERSATION) != ESP_OK) {
ESP_LOGI(TAG, "PTT activation rejected while network service is unavailable");
control_plane_local_wake_resume_after_foreground();
return;
}
ptt_state_t old_state = PTT_STATE_IDLE;
ptt_state_t new_state = PTT_STATE_IDLE;
uint32_t generation = 0;
@@ -1705,6 +1733,7 @@ static bool control_plane_try_start_multimodal_from_state_machine(TickType_t *la
#if CONFIG_TQ_NETWORK_AUTO_REASSOCIATION_ENABLE
control_plane_record_multimodal_network_result(err);
#endif
control_plane_network_feedback_mark_server_down();
ESP_LOGW(TAG, "Multimodal client start failed: %s", esp_err_to_name(err));
return false;
}
@@ -2082,6 +2111,8 @@ static void control_plane_wifi_state_task(void *arg)
bool connected = control_plane_is_sta_connected();
TickType_t now_tick = xTaskGetTickCount();
bool reconnect_now_requested =
control_plane_consume_wifi_reconnect_now_request();
#if CONFIG_TQ_MEMORY_MONITOR_ENABLE
control_plane_memory_monitor_sample(now_tick, false);
@@ -2099,6 +2130,7 @@ static void control_plane_wifi_state_task(void *arg)
bool ptt_foreground_disconnect = control_plane_get_ptt_active();
bool disconnect_grace_elapsed =
reconnect_now_requested ||
ptt_foreground_disconnect ||
disconnected_expression_active ||
(now_tick - disconnect_started_tick) >= disconnect_grace_ticks;
@@ -2108,8 +2140,10 @@ static void control_plane_wifi_state_task(void *arg)
if (!disconnected_expression_active) {
ESP_LOGI(TAG, "Wi-Fi STA disconnect persisted; stopping multimodal client");
control_plane_network_feedback_mark_wifi_down();
(void)control_plane_stop_multimodal_client();
control_plane_stop_user_config_server();
control_plane_network_feedback_service();
}
bool refresh_due = disconnected_expression_active &&
@@ -2139,6 +2173,7 @@ static void control_plane_wifi_state_task(void *arg)
ptt_wifi_foreground_attempts <
CONTROL_PLANE_PTT_FOREGROUND_RECONNECT_MAX_ATTEMPTS;
bool reconnect_due = ptt_foreground ||
reconnect_now_requested ||
!reconnect_attempted ||
(now_tick - last_reconnect_attempt_tick) >= reconnect_delay_ticks;
bool provisioning_running =
@@ -2226,6 +2261,7 @@ static void control_plane_wifi_state_task(void *arg)
if (connected && (!was_connected || disconnected_expression_active)) {
ESP_LOGI(TAG, "Wi-Fi STA connected; restoring idle expression");
control_plane_network_feedback_mark_wifi_recovered();
control_plane_start_wall_clock_sync();
control_plane_request_wifi_state_expression(DOMAIN_EXPRESSION_IDLE,
DOMAIN_EXPRESSION_TRIGGER_WIFI_RECONNECTED,
@@ -2280,6 +2316,8 @@ static void control_plane_wifi_state_task(void *arg)
}
}
control_plane_network_feedback_service();
was_connected = connected;
}
@@ -2575,6 +2613,10 @@ esp_err_t control_plane_start_lcd_render_worker(void)
esp_err_t control_plane_start_runtime_workers(void)
{
control_plane_set_runtime_stop_requested(false);
control_plane_network_feedback_reset();
portENTER_CRITICAL(&s_runtime_lock);
s_wifi_reconnect_now_requested = false;
portEXIT_CRITICAL(&s_runtime_lock);
#if CONFIG_TQ_MEMORY_MONITOR_ENABLE
memset(&s_memory_monitor, 0, sizeof(s_memory_monitor));
@@ -2813,6 +2855,7 @@ void control_plane_stop_runtime_workers(void)
control_plane_delete_model_cmd_queue(queue, queue_with_caps);
(void)control_plane_stop_multimodal_client();
control_plane_network_feedback_reset();
control_plane_set_runtime_stop_requested(false);
}