22 KiB
网络异常语音反馈与业务快速失败改造说明
1. 提交概况
提交:ceb07804254a257b17db291c8198567f5d265c04
标题:feat(control-plane): add network outage voice feedback
日期:2026-08-25
本次提交为设备增加网络异常语音反馈,并调整断网时的对话和云端抽签行为。改造后的核心原则是:
- 被动断网时只提示一次,避免网络抖动造成连续播报。
- 用户在网络不可用时发起对话或抽签,立即得到语音反馈,不再无提示等待。
- Wi-Fi 恢复不等于业务恢复,只有服务器连接恢复后才播放业务恢复提示。
- 保留后台自动重连、Wi-Fi 多凭据遍历以及空闲 WebSocket 唤醒机制。
2. 问题原因
2.1 Wi-Fi 断开后只有视觉反馈
原有运行时能够检测 Wi-Fi STA 断开,并在断开持续超过 3 秒后显示 Wi-Fi 断开表情、停止旧多模态客户端并启动重连。
但该路径没有接入提示音。用户没有看屏幕时,无法知道设备已经断网以及设备正在自动修复网络。
2.2 离线对话会进入无效等待
原有对话入口会先进入聆听状态,再等待 Wi-Fi 和 WebSocket 可用。在点击对话模式下,整个捕获窗口约为 8 秒;如果网络始终不可用,操作最终结束,但没有明确的“网络不可用”语音反馈。
用户感知是:设备似乎开始聆听,但没有回复,也不知道失败原因。
2.3 离线抽签会排队后静默失败
原有云端抽签请求可以在服务器未连接时先写入待处理状态,然后等待多模态会话建立。准备阶段最多等待约 10 秒,超时后清除请求。
如果设备已经明确断网,继续排队没有实际价值,而且请求被清除时没有用户可感知的反馈。
2.4 Wi-Fi 断开和服务器断开没有统一反馈状态
项目中存在以下不同状态:
- Wi-Fi 与路由器断开;
- Wi-Fi 已关联,但公网、DNS、HTTPS 或服务器不可用;
- WebSocket 空闲超时后主动关闭;
- WebSocket 异常断开;
- Wi-Fi 已恢复,但服务器尚未完成重新鉴权;
- Wi-Fi 与服务器均已恢复。
原有代码分别处理这些状态,但没有统一记录“是否发生过故障、是否已经播报、是否允许播放恢复提示”。因此直接在多个回调中增加提示音,容易出现重复播报、恢复音过早播放或把正常节能关闭误判为故障。
2.5 提示音播放是同步阻塞操作
本地 Opus 提示音接口会读取文件、提交音频帧并等待播放完成。如果直接在 WebSocket 回调中播放,会阻塞网络事件处理线程,扩大断线期间的并发风险。
因此服务器回调只能记录状态,提示音需要由现有运行时任务在安全上下文中消费。
3. 解决方式
3.1 增加统一网络反馈状态机
新增控制层网络反馈模块,集中维护网络服务状态、待播放提示、故障是否已经播报、提示冷却时间和服务器断开时间。
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 network_feedback_runtime_t s_feedback;
static portMUX_TYPE s_feedback_lock = portMUX_INITIALIZER_UNLOCKED;
状态由自旋锁保护,可以安全地在 Wi-Fi 监控任务、按键任务和多模态回调之间共享。模块没有增加新的后台任务,而是复用现有 Wi-Fi 状态监控任务定期处理待播放提示。
3.2 复用现有音频资源
本次没有新增录音文件,直接复用 LittleFS 中已有的五个 Opus 资源:
#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"
对应关系如下:
| 场景 | 提示音资源 |
|---|---|
| Wi-Fi 持续断开 | network_interrupted_repairing.opus |
| 用户在 Wi-Fi 断开时操作 | network_unstable.opus |
| 服务器持续断开 | server_abnormal_interrupted.opus |
| 用户在服务器断开时操作 | server_repair_wait_reopen.opus |
| 网络与服务器均恢复 | signal_restored_business_resume.opus |
3.3 Wi-Fi 持续断开时播放一次提示
Wi-Fi 原有 3 秒断开保护期保持不变。只有断开持续超过保护期、准备停止旧业务客户端时,才将反馈状态标记为 WIFI_DOWN。
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();
}
标记 Wi-Fi 断开时,只在本次故障尚未播报的情况下生成一个待播放提示:
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);
}
Wi-Fi 断开表情仍可按原逻辑刷新,但提示音不会随表情刷新重复播放。
3.4 服务器异常保留 3 秒自恢复时间
WebSocket 连接成功、断开和错误事件接入统一反馈状态:
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();
(void)control_plane_schedule_websocket_wait_task();
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();
(void)control_plane_schedule_websocket_wait_task();
break;
}
服务器断开不会立即播报,而是记录断开时间。超过 3 秒仍未恢复时,才安排服务器异常提示:
#define CONTROL_PLANE_SERVER_DISCONNECT_GRACE_MS 3000U
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;
}
这样可以过滤短暂 WebSocket 抖动,避免服务器在几秒内自行恢复时仍播放异常提示。
3.5 对话和抽签统一执行网络可用性检查
新增两类需要网络的用户操作:
typedef enum {
CONTROL_PLANE_NETWORK_ACTION_CONVERSATION = 0,
CONTROL_PLANE_NETWORK_ACTION_CLOUD_CHOUQIAN,
} control_plane_network_action_t;
对话入口在显示聆听表情和创建语音会话之前检查网络:
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;
}
云端抽签入口在写入待处理请求之前检查网络:
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;
}
统一检查函数根据故障层级选择提示音,并决定触发 Wi-Fi 重连还是服务器重连:
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;
}
if (!wifi_connected) {
prompt_path = CONTROL_PLANE_WIFI_ACTION_BLOCKED_PROMPT_FILE;
} else {
prompt_path = CONTROL_PLANE_SERVER_ACTION_BLOCKED_PROMPT_FILE;
}
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;
本次操作会明确结束,不会继续排队。用户需要在网络恢复后重新发起对话或抽签。
3.6 用户操作可加速 Wi-Fi 重连
新增一个受锁保护的立即重连请求标志。离线操作提示播放完成后,唤醒 Wi-Fi 状态任务:
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();
}
Wi-Fi 状态任务消费该标志,并允许本轮跳过剩余保护期或退避等待:
bool reconnect_now_requested =
control_plane_consume_wifi_reconnect_now_request();
bool disconnect_grace_elapsed =
reconnect_now_requested ||
ptt_foreground_disconnect ||
disconnected_expression_active ||
(now_tick - disconnect_started_tick) >= disconnect_grace_ticks;
bool reconnect_due = ptt_foreground ||
reconnect_now_requested ||
!reconnect_attempted ||
(now_tick - last_reconnect_attempt_tick) >= reconnect_delay_ticks;
这里的“立即”表示立即唤醒并允许开始下一轮连接,不代表 Wi-Fi 可以瞬间恢复。扫描、认证、关联和 DHCP 仍受原有 20 秒连接周期约束。
3.7 增加 5 秒提示冷却和播放互斥
所有网络故障提示共享 5 秒冷却时间,并通过 prompt_in_progress 防止并发播放:
#define CONTROL_PLANE_NETWORK_PROMPT_COOLDOWN_MS 5000U
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;
}
用户连续点击对话和抽签时,第一次操作可以播放提示,冷却期内后续操作仍会被快速拒绝和触发恢复,但不会重复播音。
3.8 只有业务真正恢复后才播放恢复提示
Wi-Fi 获得 IP 后,只把状态从 WIFI_DOWN 转换为 SERVER_DOWN,不会立刻播放恢复音:
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);
}
只有 WebSocket 再次连接成功,并且设备此前确实完成过服务器连接、当前故障也已经对用户播报,才安排业务恢复提示:
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);
}
开机首次联网不会播放“业务恢复”,仍使用原有服务器连接成功提示。
3.9 保留空闲 WebSocket 唤醒行为
设备会在多模态连接空闲约 30 秒后主动关闭 WebSocket以节省资源。这属于正常状态,不是服务器故障。
网络检查只在状态机已经确认 WIFI_DOWN 或 SERVER_DOWN 时快速拒绝操作。如果 Wi-Fi 已连接、WebSocket 只是正常空闲关闭,则允许原有对话流程继续,并由首次操作唤醒服务器连接:
if (wifi_connected && !confirmed_outage) {
return ESP_OK;
}
该判断避免把正常节能行为误判为服务器异常。
3.10 OTA 和 BLE 配网期间抑制被动网络提示
网络反馈服务在 OTA 不允许语音服务或小程序 BLE 配网运行时直接返回:
if (!control_plane_ota_voice_services_allowed() ||
control_plane_is_mini_program_provisioning_running()) {
return;
}
这样可以避免网络提示音打断系统升级、配网引导和相应的专用提示音。
4. 解决后的完整体验
4.1 Wi-Fi 真正断开
Wi-Fi 链路断开
-> 等待原有 3 秒保护期
-> 3 秒内恢复:不显示断网状态,不播放提示
-> 超过 3 秒:停止旧业务连接并显示断网表情
-> 播放一次“网络中断,正在修复”
-> 遍历可见的已保存 Wi-Fi 并自动重连
4.2 用户在 Wi-Fi 断开时操作
用户发起对话或云端抽签
-> 播放“网络不稳定”提示,5 秒内不重复播放
-> 不进入聆听或抽签排队状态
-> 本次操作立即结束
-> 唤醒 Wi-Fi 状态任务并加速下一轮重连
4.3 Wi-Fi 正常但服务器异常
WebSocket 断开或服务器连接失败
-> 记录服务器异常并重新鉴权
-> 3 秒内恢复:不播放异常提示
-> 超过 3 秒:播放一次服务器异常提示
-> 用户继续操作:播放服务器修复提示并结束本次操作
-> 立即安排新的服务器连接
4.4 网络恢复
Wi-Fi 获得 IP
-> 恢复局域网状态,但暂不播放业务恢复提示
-> 重新鉴权并建立 WebSocket
-> WebSocket 连接成功
-> 若本次故障此前已经播报,则播放一次“信号恢复,业务恢复”
-> 用户可以重新发起对话或抽签
5. 解决成果
5.1 用户体验成果
- 持续断网不再只有屏幕表情,用户可以通过语音获知设备正在修复网络。
- 离线对话不再先进入假聆听状态后静默结束。
- 离线云端抽签不再排队约 10 秒后静默失败。
- Wi-Fi 故障和服务器故障使用不同语音,用户能区分问题层级。
- 连续点击不会重复播音,避免提示音轰炸。
- Wi-Fi 恢复后不会提前宣布业务恢复,恢复提示以 WebSocket 真正可用为准。
- 正常的空闲 WebSocket 关闭仍能由首次业务操作唤醒,不受快速失败策略影响。
5.2 工程成果
- 网络反馈策略集中在控制层单一模块中,没有把状态分散到按键、Wi-Fi 和 WebSocket 实现。
- 多任务共享状态由自旋锁保护。
- WebSocket 回调只更新状态,不执行同步音频播放。
- 没有新增长期运行任务、定时器或跨层依赖。
- 没有新增音频资产,直接复用已打包资源。
- 原有 Wi-Fi 多凭据、20 秒连接周期、指数退避、BLE 重新配网和服务器鉴权机制保持不变。
5.3 构建验证
使用 ESP-IDF 5.5.4 完成完整固件构建:
Project build complete
TQ_Cat_Code.bin binary size 0x217d50 bytes
Smallest app partition: 0x380000 bytes
Free space: 0x1682b0 bytes(40%)
构建阶段确认:
- 新增网络反馈模块编译通过;
- 固件链接通过;
- 五个提示音均进入 LittleFS 镜像;
- 应用分区容量检查通过;
- Markdown 格式和空白错误检查通过。
6. 后续影响与限制
6.1 提示音会推迟重连开始时间
提示音接口会同步等待播放完成。Wi-Fi 被动断开和用户主动离线操作路径会先完成提示播放,再继续或加速重连。
这样可以避免提示音与多模态会话销毁、音频初始化同时运行,但相应重连会比完全静默时晚一个提示音时长开始。该延迟不计入 Wi-Fi 单轮 20 秒连接预算。
6.2 离线操作不会自动补执行
断网时发起的对话和云端抽签会立即结束,不会在网络恢复后自动执行。
这是有意设计:自动补执行可能在几十秒后突然触发已经失去上下文的抽签;对话音频也无法在长时间断网后可靠重放。恢复后需要用户重新操作。
6.3 所有网络故障提示共享冷却时间
Wi-Fi 异常、服务器异常、离线对话和离线抽签共用 5 秒故障提示冷却。冷却期内状态处理和重连仍会执行,但不会播放第二条提示。
因此,如果 Wi-Fi 故障刚播报后立即转变为服务器故障,服务器提示可能被冷却时间抑制。这是为了优先保证设备安静和避免连续播报。
6.4 恢复提示有严格前置条件
只有同时满足以下条件才会播放业务恢复提示:
- 设备本次运行中曾成功连接过服务器。
- 当前故障已经对用户播放过异常提示。
- WebSocket 已重新连接成功。
如果故障很短、异常提示被 OTA 或 BLE 配网抑制,恢复后不会额外播放恢复音。这可以避免用户只听到“恢复”却不知道之前发生过什么。
6.5 服务器异常识别依赖现有事件
服务器故障主要由 WebSocket 断开、WebSocket 错误或多模态客户端启动失败进行标记。如果某种网络异常没有触发这些事件,也没有产生现有 HTTPS 网络错误,则可能延迟进入 SERVER_DOWN 状态。
后续可以结合心跳超时、DNS 错误分类和 HTTPS 错误统计进一步增强服务器可用性判定,但不建议仅凭 Wi-Fi 已关联状态判断业务可用。
6.6 尚需真机验证音频与时序
当前已经完成编译、链接、资源打包和静态检查,但仍需要真机覆盖以下场景:
- 路由器断电超过 3 秒,确认断网提示只播放一次。
- Wi-Fi 在 3 秒内恢复,确认不播放断网提示。
- 断网时连续点击对话和抽签,确认立即结束且 5 秒内不重复播音。
- Wi-Fi 恢复但 WebSocket 尚未恢复,确认不提前播放业务恢复提示。
- WebSocket 短暂断开少于 3 秒,确认不播放服务器异常提示。
- WebSocket 持续断开超过 3 秒,确认播放一次服务器异常提示。
- 服务器异常时发起业务操作,确认播放服务器修复提示并重新鉴权。
- WebSocket 最终恢复,确认业务恢复提示只播放一次。
- 空闲 WebSocket 主动关闭后发起对话,确认正常唤醒连接而不是被拒绝。
- OTA 和 BLE 配网过程中制造网络变化,确认不会插入普通网络提示音。
- 提示音播放过程中恢复 Wi-Fi,确认不会出现音频冲突或重复恢复播报。
6.7 固件容量变化
新增的是状态管理和接线代码,没有新增音频文件。相比前一版构建,应用二进制由 0x217430 增长到 0x217d50,增加 0x920 字节,即 2336 字节。
当前最小应用分区仍有 0x1682b0 字节可用,约占分区的 40%,不存在近期容量风险。
7. 结论
本次提交解决了网络异常状态下“用户不知道设备发生了什么”和“网络业务静默等待后失败”两个主要体验问题。
设备现在能够区分 Wi-Fi 断开与服务器断开,在合适的时机播放一次提示,并在用户主动操作时快速反馈和触发恢复。恢复提示以服务器业务连接真正可用为准,同时保留正常的空闲 WebSocket 唤醒、Wi-Fi 多凭据重连、OTA 和 BLE 配网行为。
后续主要风险不在编译或容量,而在真实网络抖动、音频播放时长和多种恢复时序组合。应通过上述真机测试确认提示音频率、恢复延迟和用户感知是否符合产品预期。