项目: TXW81x 可视对讲 UI
模块: 微信式语音与表情消息
阶段: UI、业务服务、UDP 与 Opus 链路整合
提交范围: e9a2fb52440a8e
分析基线: 2440a8e(2026-07-17)


1. 文档目标

本文给后续开发者建立模块级心智模型,重点说明以下问题:

  • 微信入口从哪里创建,页面由谁管理;
  • 按键事件如何转换为录音、发送、播放等业务动作;
  • UDP 收发线程、服务线程和音频线程如何协作;
  • Opus 在本项目中承担什么职责;
  • 哪些路径已经实现,哪些仍是预留能力;
  • 维护时需要优先关注哪些并发、队列和协议风险。

配套文档:

  • TXW81x微信消息UDP-v2协议开发文档.md:UDP v2 包格式与语音文件传输协议;
  • TXW81x微信语音Opus与WCA音频链路开发文档.md:Opus 参数、WCA 文件和录放音任务;
  • TXW81x微信UI控制层与业务服务开发文档.md:LVGL 页面、控制层、服务状态与消息队列。

2. 代码范围

层级 主要文件 职责
应用入口 sdk/app/ui/main_ui.c 调用 wechat_service_create_ui() 创建微信入口
UI 展示 sdk/lib/gui/intercom_ui/wechat_ui.c 创建聊天页面、气泡、表情面板和 30 ms 消息轮询定时器
输入控制 sdk/lib/gui/intercom_ui/wechat_ui_control.c 将 LVGL 键值转换为语音按下/松开、表情选择等动作
业务服务 sdk/lib/gui/intercom_ui/wechat_service.c 初始化资源、串行化业务事件、调度录音/播放、桥接 UI 与网络
音频适配 sdk/lib/gui/intercom_ui/wechat_app/wechat_audio.c 音频流、Opus 编解码、WCA 文件和异步作业状态机
UDP 传输 sdk/app/wechat_udp/wechat_udp.c UDP socket、协议封包、接收解析、文件分片发送
Opus 库 sdk/lib/opus/ Opus 1.5.1 编解码实现,固定点配置

sdk/app/intercom/intercom_opus.c 是项目原有的实时对讲链路。微信语音模块没有直接复用它的实时网络抖动缓冲和重传逻辑,而是复用了底层音频 stream 与 Opus API,形成“先录制为文件,再通过 UDP 发送”的异步语音消息链路。

基线版本的关键入口:

  • sdk/app/ui/main_ui.c:85:创建微信服务 UI;
  • sdk/lib/gui/intercom_ui/wechat_ui.c:818:通用 UI 创建接口;
  • sdk/lib/gui/intercom_ui/wechat_service.c:642:业务服务初始化;
  • sdk/app/wechat_udp/wechat_udp.c:365:UDP 初始化;
  • sdk/lib/gui/intercom_ui/wechat_app/wechat_audio.c:925:音频与 Opus 初始化。

3. 总体分层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
main_ui.c
|
v
wechat_ui.c <---- 30 ms 拉取 UI 消息 ---- wechat_service.c
| | |
| 按键动作 | +--> wechat_udp.c
v | |
wechat_ui_control.c | +--> 对端 UDP 5010
|
+--> wechat_audio.c
|
+--> sdk/lib/opus/
+--> 音频 stream / DAC
+--> FLASH:/OPUS/*.WCA

设计上的核心边界如下:

  1. UI 层不直接操作 socket、文件或 Opus。
  2. UDP 接收任务不操作 LVGL 对象,只回调服务层。
  3. 音频工作任务不操作 LVGL,只向服务队列投递完成事件。
  4. 服务任务负责把跨线程事件串行化,并保证录音和播放互斥。
  5. UI 通过独立 UI 队列获取可显示的消息,不直接读取服务内部状态。

4. 运行实体与资源

运行实体 创建位置 优先级 栈/队列 主要工作
LVGL 线程/定时器 GUI 框架 GUI 上下文 30 ms timer 页面操作、消息气泡渲染
wechat_service wechat_service_init() Normal 栈 2048 B;事件队列 16 业务状态编排
wechat_udp_tx wechat_udp_init() Normal 栈 1024 B;发送队列 16 异步发送表情
wechat_udp_rx wechat_udp_init() Normal 栈 1024 B 收包、校验、回调服务层
wechat_audio wechat_audio_init() Normal PSRAM 栈 30 KiB;作业队列 4 录音、编码、解码、播放

重要静态/动态资源:

  • UDP 上下文包含 1209 B 接收缓冲区和 1208 B 发送缓冲区;
  • 音频播放时从 PSRAM 申请 4 × 1024 B 节点数据区;
  • 音频任务栈从 PSRAM 申请 30 KiB;
  • Opus encoder、decoder 在服务打开时创建,在页面关闭时释放;
  • 语音文件统一保存在 FLASH:/OPUS

5. 初始化与退出顺序

5.1 打开页面

  1. main_ui.c 创建微信菜单入口。
  2. 用户短按入口,wechat_ui_open() 调用服务层 open 回调。
  3. wechat_service_init() 依次创建互斥锁、停止信号量、UI 队列和服务队列。
  4. 初始化 wechat_audio,创建 Opus encoder、decoder 和音频工作任务。
  5. 初始化 wechat_udp,绑定本地 UDP 端口 5010,创建收发任务。
  6. 根据 Wi-Fi 模式配置固定对端:
    • AP 模式:192.168.1.100:5010
    • STA 模式:192.168.1.1:5010
  7. 创建 wechat_service 任务。
  8. 服务初始化成功后,UI 才隐藏主页面并创建聊天页面、焦点组和消息 timer。

5.2 关闭页面

  1. 删除 UI 消息 timer,停用输入控制。
  2. 调用服务层 close 回调。
  3. 服务任务收到 STOP 消息后发出停止信号。
  4. 停止 UDP 收发任务。
  5. 中止并销毁音频任务,释放 Opus 和 PSRAM 资源。
  6. 清理未完成的接收文件、队列、锁和信号量。
  7. 删除聊天页面,恢复原 LVGL group 和主页面。

6. 本地语音消息链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
按住语音键
-> UI action: VOICE_PRESS
-> service queue: VOICE_START
-> 创建 record job_id
-> audio worker 打开 R_WECHAT_AUDIO
-> 每 160 个采样点编码一个 20 ms Opus 帧
-> 写入 FLASH:/OPUS/MSG<n>.WCA

松开语音键
-> UI action: VOICE_RELEASE
-> service queue: VOICE_STOP
-> audio worker 协作停止并回报 RECORD_FINISHED
-> service 读取真实录音结果
-> UDP BEGIN + DATA... + END 发送整个 WCA 文件
-> 发送成功后通知 UI 添加本地语音气泡

注意:UI 在松键时计算的秒数被放入 VOICE_STOP 事件,但当前服务逻辑只把它当作附带数据,并不用于最终发送。最终语音时长来自 WCA 文件的实际 Opus 帧数,即 wechat_audio_result_t.duration_sec

7. 远端语音消息链路

  1. UDP RX 任务收到 WECHAT_UDP_MSG_VOICE
  2. 服务回调按 BEGIN / DATA / END 子类型处理。
  3. BEGIN 创建 FLASH:/OPUS/RX<transfer_id>.TMP
  4. DATA 要求 offset 严格等于当前已接收长度,并边写文件边计算 CRC32。
  5. END 校验 transfer ID、文件长度和 CRC32。
  6. 校验通过后将临时文件重命名为 .WCA
  7. 服务任务把它转为待播放语音并通知 UI 添加远端气泡。
  8. 若当前没有录音或播放任务,立即创建播放 job。
  9. 音频任务逐帧 Opus 解码,将 PCM 节点送往 speaker stream。

录音优先级高于远端播放:用户在播放期间按下语音键时,服务先停止当前播放,待播放完成事件返回后再开始录音。

8. 表情消息链路

8.1 本地发送

  1. 用户打开表情面板并选择索引 0..7
  2. UI 调用 emoji_selected 回调。
  3. 服务把 SEND_EMOJI 投递到服务队列。
  4. UI 在投递成功后立即显示本地表情气泡。
  5. 服务任务调用 wechat_udp_send_emoji()
  6. UDP TX 任务异步发送一个 1 B payload 的表情包。

这里采用乐观 UI:本地气泡表示业务事件成功入队,不表示 UDP 已确认送达。

8.2 远端接收

  1. UDP RX 校验协议头和表情索引。
  2. 服务回调投递 REMOTE_EMOJI 事件。
  3. 服务任务写入 UI 队列。
  4. LVGL timer 拉取消息并显示远端表情气泡。

9. 当前功能边界

已实现:

  • 8 个固定表情的双向 UDP 消息;
  • 8 kHz、单声道、20 ms 帧的 Opus 语音录制;
  • WCA 文件保存、UDP 分片发送、CRC32 校验;
  • 远端语音自动播放;
  • 录音优先于播放的半双工业务调度;
  • UI 与业务服务解耦;
  • 语音、表情编译期开关。

尚未实现或仅预留:

  • 拍照按钮动作为空;
  • UDP 的 PHOTO_BEGIN、PHOTO_DATA、PHOTO_END 只有消息类型定义;
  • ACK 类型尚未形成确认与重传机制;
  • UI 语音气泡可点击但没有绑定重播事件;
  • 聊天记录未持久化,重开页面不会恢复历史消息;
  • 对端 IP 为固定配置,不支持发现、配对或动态切换。

10. 维护风险摘要

高优先级

  1. 音频完成回调使用非阻塞方式投递服务队列,且当前忽略投递失败。若 16 深度的服务队列已满,服务层可能收不到完成事件,导致 record_job_idplay_job_id 无法清零。
  2. UDP 协议没有 ACK、重传或乱序重组。任意一个语音 DATA 包丢失或乱序都会使接收端中止整个文件。

中优先级

  1. UDP RX 回调内直接执行 Flash 文件创建、写入、重命名和 CRC 计算。Flash 延迟过高时可能拖慢后续收包。
  2. 语音接收超时只在收到下一次 BEGIN 时检查。单次传输中途停止后,临时文件可能一直保持打开,直到新 BEGIN 或模块退出。
  3. UI 队列深度为 8,写入采用非阻塞方式;突发远端消息可能被丢弃。
  4. 接收端只校验来源 IP,没有校验来源 UDP 端口。

兼容性注意

  1. UDP 多字节字段使用大端序,跨平台可解析。
  2. WCA 文件头和每帧 packet_len 使用目标机本地字节序。当前文件以整体二进制形式传输并在同类设备上播放没有问题,但不应直接假定其跨端序兼容。
  3. Opus 库配置为 fixed-point,且启用了 C99 VLA;音频工作任务的大栈需求需要持续关注。

11. 建议的后续演进顺序

  1. 为服务队列完成事件增加可靠投递或兜底状态恢复。
  2. 给语音协议增加 ACK、超时清理和重传策略。
  3. 将 UDP RX 的文件写入移到专用接收任务或缓存队列。
  4. 为消息增加 message ID、发送状态和失败提示,替代完全乐观的 UI。
  5. 实现语音气泡重播与聊天记录索引。
  6. 再扩展照片分片协议,复用 transfer ID、offset、size、CRC 的通用文件传输模型。