Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 4m28s
## 变更内容
### k8s/ingress.yaml
- 新增 /xiaozhi/v1/ 路径规则,将 WebSocket 流量路由到 hw-ws-svc:8888
- Traefik 最长前缀优先,/xiaozhi/v1/ 不影响 / 下的 Django 路由
### hw_service_go/k8s/service.yaml
- Service 类型由 LoadBalancer 改为 ClusterIP
- 移除阿里云 SLB 注解(通过 Traefik Ingress 统一暴露,不再需要独立公网 IP)
### hw_service_go/k8s/deployment.yaml
- 镜像地址改为 ${CI_REGISTRY_IMAGE}/hw-ws-service:latest 占位符
- CI/CD 部署时统一通过 sed 替换为华为云 SWR 实际地址
### hw_service_go/internal/server/server.go
- 新增 GET /xiaozhi/v1/healthz 接口,返回 {"status":"ok","active_connections":N}
- 用于部署后验证服务存活及当前连接数
### .gitea/workflows/deploy.yaml
- 新增 Build and Push HW WebSocket Service 步骤,构建并推送 hw_service_go 镜像
- 部署步骤新增 kubectl apply hw_service_go/k8s/deployment.yaml 和 service.yaml
- 新增 kubectl rollout restart deployment/hw-ws-service
### run.sh
- 本地同时启动 Django + hw_service_go 的开发脚本
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
12 KiB
12 KiB
hw_service_go 本地硬件通讯测试计划
目标:用浏览器模拟 ESP32 硬件,验证
hw_service_goWebSocket 服务能否正常接收指令、获取故事、推送 Opus 音频。
一、协议对比分析
1.1 小智(xiaozhi-server)vs 我们的服务
| 维度 | xiaozhi-server | hw_service_go(本服务) |
|---|---|---|
| WebSocket URL | ws://host:port/xiaozhi/v1/?device-id=&client-id= |
完全相同 |
| 连接参数 | device-id(MAC)、client-id(UUID) |
完全相同 |
| 握手消息 | 需要发送 hello JSON |
不需要,连上即用 |
| 触发指令 | listen(语音输入) |
只需发 {"type":"story"} |
| 音频方向 | 双向(硬件上传语音 + 服务下发 TTS) | 单向下行(服务→硬件,推 Opus) |
| Opus 编解码 | 需要编码(麦克风)+ 解码(播放) | 只需解码(浏览器只播放) |
| 认证 | token 参数 | 无需认证(仅 device-id 校验) |
| 消息复杂度 | hello/listen/stt/llm/tts/mcp | 只有 tts 系列 |
1.2 我们服务的完整消息流
浏览器(模拟硬件) hw_service_go Django
│ │ │
│── WS 连接 ──────────────────────────→│ │
│ ?device-id=AA:BB:CC:DD:EE:FF │ │
│ &client-id=test-001 │ │
│ │ │
│── {"type":"story"} ────────────────→ │ │
│ │── GET /api/v1/devices/ │
│ │ stories/?mac_address │
│ │ =AA:BB:CC:DD:EE:FF → │
│ │ │
│← {"type":"tts","state":"start"} ───── │ │
│ │← {title, audio_url} ── │
│ │ │
│ │── 下载 MP3 ──────────→ CDN
│ │← MP3 二进制流 ─────── │
│ │ │
│ │ ffmpeg 转码 PCM→Opus │
│ │ │
│← {"type":"tts","state":"sentence_start","text":"故事标题"} ─── │
│ │ │
│← [Opus帧1 二进制] ─────────────────── │ 60ms/帧,前3帧预缓冲 │
│← [Opus帧2 二进制] ─────────────────── │ │
│← [Opus帧3 二进制] ─────────────────── │ │
│← [Opus帧N 二进制] ─────────────────── │ 按时序流控发送 │
│ │ │
│← {"type":"tts","state":"stop"} ─────── │ │
│ │ │
1.3 Opus 音频参数(与小智完全一致)
| 参数 | 值 |
|---|---|
| 采样率 | 16000 Hz |
| 声道 | 1(单声道) |
| 帧时长 | 60ms |
| 每帧采样数 | 960 |
| 编码器 | libopus(WASM) |
二、前置条件检查
在开始测试之前,需要满足以下条件:
2.1 服务运行状态
- Django 后端运行在
http://localhost:8000 hw_service_go运行在ws://localhost:8888- 健康检查通过:
curl http://localhost:8888/healthz返回 200
2.2 Django 数据准备(关键!)
测试必须使用一个在 Django 数据库中真实存在的设备 MAC 地址。
Django API 查询逻辑(GET /api/v1/devices/stories/?mac_address=<MAC>):
- 根据 MAC 查找设备 → 找到设备绑定的用户 → 查找该用户的故事
- 任何一步缺失,服务返回
null,硬件不会播放任何内容
需要在 Django Admin 或 API 中准备:
- 注册一个设备,记下其 MAC 地址(格式:
AA:BB:CC:DD:EE:FF) - 该设备需已绑定用户(owner)
- 该用户名下有至少一个故事(有
audio_url字段)
快速验证:
curl "http://localhost:8000/api/v1/devices/stories/?mac_address=你的MAC"应返回{"code":0,"data":{"title":"...","audio_url":"..."}}
三、测试程序设计
3.1 技术选型
| 方案 | 优点 | 缺点 |
|---|---|---|
| 纯 HTML+JS(推荐) | 零依赖,直接浏览器打开,与小智方案一致 | - |
| Python 脚本 | 简单但无法播放音频 | 无法验证音频播放端到端 |
| Go 命令行 | 需额外音频库 | 环境搭建复杂 |
选择方案:纯 HTML+JS 单文件,复用小智项目的 libopus.js(WASM)做解码。
3.2 文件结构
hw_service_go/test/
├── PLAN.md ← 本文件
├── test.html ← 测试主页面(待实现)
└── libopus.js ← 复制自小智项目(Opus WASM 解码库)
libopus.js 来源:
/Users/maidong/Desktop/zyc/jikashe/xiaozhi-server/main/xiaozhi-server/test/libopus.js
3.3 测试页面功能
┌─────────────────────────────────────────────────────┐
│ hw_service_go 硬件通讯测试 │
├─────────────────────────────────────────────────────┤
│ 服务地址: [ws://localhost:8888/xiaozhi/v1/ ] │
│ device-id: [AA:BB:CC:DD:EE:FF ] │
│ client-id: [test-browser-001 ] [随机生成] │
├─────────────────────────────────────────────────────┤
│ [连接] [断开] 状态: ● 未连接 │
├─────────────────────────────────────────────────────┤
│ [▶ 触发故事播放] [⏹ 停止] │
├─────────────────────────────────────────────────────┤
│ 消息日志 [清空] │
│ ┌───────────────────────────────────────────────┐ │
│ │ [10:23:01] → 已连接 │ │
│ │ [10:23:02] → 发送: {"type":"story"} │ │
│ │ [10:23:02] ← 收到: {"type":"tts","state":"start"} │
│ │ [10:23:03] ← 收到: {"type":"tts","state":"sentence_start","text":"..."} │
│ │ [10:23:03] ← 收到: [Binary] Opus帧 #1 (38 bytes) │
│ │ [10:23:03] 🔊 开始播放... │ │
│ │ [10:23:15] ← 收到: {"type":"tts","state":"stop"} │
│ │ [10:23:15] 🔊 播放完毕 │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ 统计: 已收到 85 个Opus帧 | 约 5.1s 音频 │
└─────────────────────────────────────────────────────┘
3.4 核心实现逻辑
连接流程
const ws = new WebSocket(
`ws://localhost:8888/xiaozhi/v1/?device-id=${deviceId}&client-id=${clientId}`
);
ws.binaryType = 'arraybuffer';
触发故事
ws.send(JSON.stringify({ type: 'story' }));
接收消息处理
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
// 二进制:Opus 音频帧
const opusFrame = new Uint8Array(event.data);
const pcm = opusDecoder.decode(opusFrame); // Int16Array
schedulePlay(pcm); // 排队播放
} else {
// 文本:控制消息
const msg = JSON.parse(event.data);
handleTtsControl(msg); // 处理 start/sentence_start/stop
}
};
Opus 解码 + 播放(与小智方案完全一致)
- 使用
libopus.js(WASM)初始化解码器:16000Hz,单声道 - 解码:
Int16Array→Float32Array - 使用
AudioContext+AudioBufferSourceNode按时序排队播放 - 使用
BlockingQueue缓冲帧,避免播放卡顿
四、测试用例
Case 1:基础连接测试
- 输入正确的
device-id和client-id - 期望:WebSocket 连接建立成功,状态变为"已连接"
Case 2:故事触发测试
- 发送
{"type":"story"} - 期望:
- 收到
{"type":"tts","state":"start"} - 收到
{"type":"tts","state":"sentence_start","text":"<故事标题>"} - 陆续收到多个二进制 Opus 帧
- 最终收到
{"type":"tts","state":"stop"}
- 收到
Case 3:音频播放验证
- 期望:浏览器实际播放出故事音频,声音正常无杂音、无卡顿
Case 4:设备不存在测试
- 使用未注册的 MAC 地址
- 期望:发送故事指令后立即收到
{"type":"tts","state":"stop"}(服务侧找不到故事,直接结束)
Case 5:重复触发测试
- 播放过程中再次点击"触发故事"
- 期望:旧播放被打断,新故事从头开始(hw_service_go 的
StartPlayback会 close 旧 abortCh)
Case 6:断线重连测试
- 连接后断开,再重新连接
- 期望:可以正常重新发起故事请求
五、实现步骤
-
复制 libopus.js
cp /Users/maidong/Desktop/zyc/jikashe/xiaozhi-server/main/xiaozhi-server/test/libopus.js \ /Users/maidong/Desktop/zyc/qy_gitlab/rtc_backend/hw_service_go/test/ -
编写 test.html(单文件,嵌入所有 JS)
- 参考小智
StreamingContext.js和BlockingQueue.js的逻辑 - 去掉录音/编码部分(我们只需解码)
- 保留 Opus 解码 + AudioContext 播放部分
- 添加连接配置 UI 和消息日志面板
- 参考小智
-
浏览器打开测试
直接用浏览器打开 test.html(file:// 协议即可)注意:macOS Safari 对 WebSocket + file:// 可能有限制,建议用 Chrome
-
按测试用例逐项验证
六、已知限制与注意事项
| 问题 | 说明 |
|---|---|
| device-id 必须真实存在 | MAC 地址若未在 Django 数据库注册,服务会静默返回无故事 |
| ffmpeg 必须安装 | hw_service_go 的转码依赖系统 ffmpeg,需提前安装 |
| audio_url 必须可访问 | 故事的 MP3 链接需要能从本机下载(阿里云 OSS 等) |
| 浏览器 AudioContext 限制 | 需要用户交互(点击)后才能创建 AudioContext,不能自动播放 |
| WASM 加载 | libopus.js 较大(844KB),首次加载需要等待约 1-2 秒 |