OpenHarmony 小鸿 AI 开发实战 11:WS63 Agent 与 WebSocket 协议状态机

发布时间:2026/7/28 20:06:53
OpenHarmony 小鸿 AI 开发实战 11:WS63 Agent 与 WebSocket 协议状态机 语音设备的 WebSocket 连接成功并不等于一次对话已经可以开始。WS63 还要知道当前是在配网、连接、唤醒、监听还是播放要把按键、唤醒词、VAD、服务器 JSON、下行音频和断线事件串到同一条可推理的流程中也要避免网络线程、Agent 任务和 UART 任务同时操作连接对象。本文依据小鸿 AI 当前 OpenHarmonymini系统、LiteOS-M 与 WS63 的真实 C 源码整理。代码里的 Agent 有 7 个业务状态Mongoose WebSocket 建连后还要完成 client hello / server hello音频通道才算真正打开。本轮重新执行了后端协议冒烟测试但没有声称完成新的实机弱网、路由器切换或公网长连接稳定性测试。Agent 状态机解决的是业务顺序不只是界面文字agent_state.h定义了IDLE、WIFI_CONFIGURING、CONNECTING、WAKEUP、LISTENING、SPEAKING和FATAL_ERROR。这些名字既决定屏幕提示也约束是否允许建立会话、发送上行音频、接收 TTS、响应按键或进入配网。typedef enum { AGENT_STATE_IDLE 0, AGENT_STATE_WIFI_CONFIGURING, AGENT_STATE_CONNECTING, AGENT_STATE_WAKEUP, AGENT_STATE_LISTENING, AGENT_STATE_SPEAKING, AGENT_STATE_FATAL_ERROR, AGENT_STATE_MAX, } agent_state_t;状态与事件必须分开。状态回答“设备此刻处于什么阶段”事件回答“刚刚发生了什么”。当前事件包括 Wi-Fi 连接变化、短按切换对话、长按配网、唤醒词、TTS 开始/停止、音频通道关闭、发送音频以及 CI1302 的 VAD 段结束。UART 回调、网络回调和按键处理不直接随意改状态而是投递事件给 Agent 任务处理。这样做的价值是把并发输入收敛到一处唤醒词和短按可能同时出现TTS stop 和 WebSocket close 也可能前后紧挨如果每个回调都直接开关音频、刷新界面、修改状态很快就会出现重复 close、错过 stop 或连接对象被并发访问的问题。合法状态转换是第一道防线set_state()不是简单赋值。它先检查旧状态到新状态是否被允许例如LISTENING只能转到SPEAKING或IDLESPEAKING可以回到LISTENING或IDLE已经进入FATAL_ERROR后不再接受普通转换。非法转换会被拒绝而不是把系统推入一个看似有枚举值、实际资源状态不一致的组合。case AGENT_STATE_LISTENING: valid ( new_state AGENT_STATE_SPEAKING || new_state AGENT_STATE_IDLE); break; case AGENT_STATE_SPEAKING: valid ( new_state AGENT_STATE_LISTENING || new_state AGENT_STATE_IDLE); break;这也解释了一个容易写错的逻辑设备已经在LISTENING时再次收到唤醒事件不能先跳回WAKEUP因为状态表不允许LISTENING → WAKEUP。当前实现选择重置必要标志并再次发送监听命令保持状态图闭合。OTA 配置把服务器地址与固件解耦Agent 不应把 WebSocket 地址写死在业务文件里。当前 OTA 模块解析服务器响应中的 WebSocket URL、访问 token 和协议版本再交给协议层。这样后端迁移端口、切换域名或调整协议版本时不需要为每台设备重新改一份 Agent 代码。文章只展示字段关系不展示任何真实 token{ websocket: { url: ws://server.example/hajimi/ws, token: ..., version: 1 } }地址可配置不等于配置永远可信。设备仍需要限制字符串长度、检查空值并在连接失败时回到明确状态。认证值属于部署秘密应该由服务端环境与设备配置链管理不应出现在公开仓库的文章、截图或日志中。TCP 连上以后还要完成两层握手mg_open_audio_channel()的实际顺序是创建 WebSocket 连接并注入请求头等待MG_EV_WS_OPEN发送 client hello再等待 server hello。只有服务器 hello 被正确解析后audio_channel_opened才设为 true并通知 Agent。HTTP Upgrade 请求头包括Authorization配置存在时、Protocol-Version、Device-Id和Client-Id。随后 client hello 描述 transport 与音频参数return snprintf(buf, bufsize, {\type\:\hello\, \version\:%d, \features\:{\mcp\:true}, \transport\:\websocket\, \audio_params\:{ \format\:\%s\, \sample_rate\:16000, \channels\:1, \frame_duration\:40 } }, version, MG_WS_UPLINK_AUDIO_FORMAT_STR);当前握手等待按 20 ms 一个 step 轮询step 数是 200即 server hello 阶段上限约 4 秒。连接失败后 Agent 层有受限重试而不是无限紧循环。代码还在真正连接前等待 120 ms给前一轮 close 和网络状态收敛留出时间。WebSocket 已连接与音频通道已打开不是同一件事Mongoose 回调收到MG_EV_WS_OPEN时只设置ws_connected。如果没有 server helloserver_hello_received仍为 falseAgent 不能把这个连接当作可用语音通道。最终的is_audio_channel_opened同时检查两个条件return ctx-audio_channel_opened ctx-ws_connected;这种双条件很重要。网络层可能已经 Upgrade 成功但服务端因为版本、鉴权或 hello 格式不匹配而不接受会话。如果此时 CI1302 立即灌入 Opus设备会消耗 UART 和队列资源服务器却没有建立对应 session。把“链路可达”和“协议就绪”分开能让失败日志更准确。服务器断开时同样要区分“握手还没成功”和“会话曾经打开”。当前 close 分支只在会话确实建立过时通知 Agent 音频通道关闭避免握手失败路径和阻塞中的 open 函数同时投递重复关闭事件。协议版本决定二进制音频帧怎样拆包当前接收路径支持三种线格式。版本 1 把整个 WebSocket binary message 当作裸 Opus版本 2 使用 16 字节头包含版本、类型、时间戳和 payload size版本 3 使用 4 字节紧凑头。解析出的 payload 最终统一放入protocol_audio_packet_t上层 Agent 不需要重复理解三套线格式。v1: [ Opus payload ] v2: [ version/type/reserved/timestamp/size | payload ] v3: [ type/flags/size | payload ]当 v2 或 v3 的头长、声明长度与实际 frame 不一致时当前代码会回退为裸 Opus而不是静默丢包。这是兼容策略不是可以忽略协议校验的理由。线上排障仍应记录版本、消息长度与服务端日志避免格式错误被长期掩盖。监听、等待回答和播放之间有严格的先后关系进入新会话时Agent 根据voice_interrupt设置选择 auto、manual 或 realtime 监听模式先进入CONNECTING打开音频通道随后依次进入WAKEUP和LISTENING。WAKEUP的进入动作负责上报 detectLISTENING的进入动作负责发送 start listening。CI1302 上报0x0106后Agent 不会抢在最后一段 Opus 之前发送listen/stop。它先标记 pending等上行 staging 与发送队列排空再发 stop之后保持LISTENING状态但进入“等待回答”子阶段拒绝新的普通上行。服务器发送 TTS start 后才转入SPEAKING。TTS stop 也不是收到 JSON 就立刻回待机。下行音频可能仍在播放队列中当前实现设置 finish pending等待播放排空并经过稳定窗口再关闭通道、回到IDLE。这能避免最后几个音频帧被状态切换提前截断。Mongoose API 由锁保护背压从 4 KiB 开始拒绝Mongoose 管理器由独立 poll 任务推进。活动连接使用 5 ms sleep无活动连接时使用 30 ms避免既让实时音频等待太久又让空闲任务一直占用 CPU。发送、close、检查 send buffer 等 Mongoose API 操作经过同一把锁减少 Agent 任务和 poll 任务并发触碰连接结构的风险。#define MG_WS_AUDIO_SEND_BACKLOG_LIMIT 4096U #define MG_WS_POLL_ACTIVE_SLEEP_MS (5U) #define MG_WS_POLL_IDLE_SLEEP_MS (30U) busy ( ctx-conn-send.len MG_WS_AUDIO_SEND_BACKLOG_LIMIT);达到 4096 字节阈值后拒绝继续塞音频是一种有界失败。它不会让 RAM 随网络变慢无限增长但也意味着弱网下可能丢语音帧。因此验证时不能只看“WebSocket 没断”还要看 backlog 告警、上行帧计数、VAD stop 是否最终发送以及服务器 ASR 是否收到完整语句。源码注释冲突必须按执行代码判定mongoose_protocol.c顶部保留了一段旧注释称 CI1302 的0x0105是 PCM但同一文件当前默认MG_WS_UPLINK_AUDIO_FORMAT_STR为opusCI1302 数据处理调用的也是 Opus uplink bridge后端按 Opus 解码。文章不能把过期注释当作事实也不能悄悄忽略它。本篇采用的结论是当前执行代码链为 Opus旧 PCM 注释属于需要清理的维护债。判断依据是命令处理、函数调用、hello 格式宏和后端解码四处互相印证。若未来协议真的切回 PCM就必须同时修改 payload 产生端、hello 声明、服务端解码和测试而不是只改一个字符串。本轮验证了什么还没有验证什么本轮对源文件重新建立 SHA-256 快照并执行protocol_smoke.py。测试输出包含protocol_headersok、asr_to_llmok、voice_reconnectok也检查了模拟 TTS 下行包。它证明当前 Python 协议测试中的请求头、会话与语音链路约定仍能跑通。但这是本地确定性冒烟不是设备端运行证据。真实验收仍需要在 WS63 上看见MG_EV_WS_OPEN、server hello 和 audio channel opened对着 CI1302 说完整问题确认 Opus 帧、VAD stop、ASR 文本与 TTS 播放顺序拔网线或切换 AP检查 close、重试与回待机限速制造 backlog确认内存稳定且下一轮会话能够恢复。对 OpenHarmonymini LiteOS-M 的小设备来说可靠的语音交互不靠一个“连接成功”日志而靠状态、事件、协议握手、音频顺序和有界资源共同成立。下一篇将转到 Python asyncio 服务端逐段对应 OTA、WebSocket、Session、ASR、LLM 和 TTS 如何接住这里发出的消息。