From 9dbd209d20d14c2a436596acc1aa6c1cf9c5cf43 Mon Sep 17 00:00:00 2001 From: sun qirui Date: Tue, 16 Sep 2025 14:07:18 +0800 Subject: [PATCH] update --- docs/.vitepress/theme/constrants/route.ts | 4 +- .../platform/MQTT\346\216\245\345\205\245.md" | 0 docs/xrobot/platform/OTA.md | 322 +++++-- docs/xrobot/platform/websocket.md | 243 ----- .../websocket\346\216\245\345\205\245.md" | 850 ++++++++++++++++++ 5 files changed, 1094 insertions(+), 325 deletions(-) rename docs/xrobot/platform/MQTT.md => "docs/xrobot/platform/MQTT\346\216\245\345\205\245.md" (100%) delete mode 100644 docs/xrobot/platform/websocket.md create mode 100644 "docs/xrobot/platform/websocket\346\216\245\345\205\245.md" diff --git a/docs/.vitepress/theme/constrants/route.ts b/docs/.vitepress/theme/constrants/route.ts index d35500a..62242c0 100644 --- a/docs/.vitepress/theme/constrants/route.ts +++ b/docs/.vitepress/theme/constrants/route.ts @@ -77,8 +77,8 @@ const items_xrobot_platform = [ collapsed: true, items: [ { text: "OTA 网关", link: "OTA" }, - { text: "WebSocket", link: "websocket" }, - { text: "MQTT", link: "MQTT" }, + { text: "WebSocket接入", link: "websocket接入" }, + { text: "MQTT接入", link: "MQTT接入" }, ].map((item) => apply_prefix(item, Chapters.xrobot_platform)), }, ]; diff --git a/docs/xrobot/platform/MQTT.md "b/docs/xrobot/platform/MQTT\346\216\245\345\205\245.md" similarity index 100% rename from docs/xrobot/platform/MQTT.md rename to "docs/xrobot/platform/MQTT\346\216\245\345\205\245.md" diff --git a/docs/xrobot/platform/OTA.md b/docs/xrobot/platform/OTA.md index a6da9b9..e03fc6f 100644 --- a/docs/xrobot/platform/OTA.md +++ b/docs/xrobot/platform/OTA.md @@ -2,92 +2,166 @@ title: OTA 协议(网关) --- -## OTA(Over-The-Air)更新介绍 +# OTA 协议文档 -OTA(Over-The-Air)更新是一种通过无线网络将软件更新直接推送到设备的技术。这种方式使得用户无需手动下载和安装更新,从而简化了更新过程。OTA 更新通常用于固件、应用程序和操作系统的更新,能够提高设备的安全性和性能。 +## 目录 -在本指南中,我们将介绍如何使用 OTA 更新小智设备的固件,包括更新的步骤、注意事项以及常见问题的解答。 +- [概述](#概述) +- [快速开始](#快速开始) +- [API 参考](#api-参考) +- [完整示例](#完整示例) +- [错误处理](#错误处理) +- [常见问题](#常见问题) -请确保在进行 OTA 更新之前,设备已连接到稳定的网络,并且电量充足,以避免更新过程中出现问题。 +## 概述 -通过OTA请求获取到小智设备的激活码和websocket地址后,再进行websocket通信。 +### 什么是 OTA -## OTA 地址 +OTA(Over-The-Air)更新是一种通过无线网络将软件更新直接推送到设备的技术。小智设备通过 OTA 协议可以: - +- 🔄 **自动获取固件更新**:检查并下载最新固件版本 +- 🔑 **设备激活**:获取设备激活码进行身份验证 +- 🌐 **服务器配置**:获取 WebSocket 和 MQTT 服务器连接信息 +- ⏰ **时间同步**:同步服务器时间到设备 -## OTA 协议描述 +### 工作流程 -### 请求方法 +```mermaid +graph LR + A[设备启动] --> B[发送 OTA 请求] + B --> C[服务器验证] + C --> D[返回配置信息] + D --> E[获取激活码] + E --> F[建立 WebSocket 连接] +``` + +### 基本信息 + +| 项目 | 值 | +|------|-----| +| **API 地址** | `https://xrobo.qiniuapi.com/v1/ota/` | +| **请求方法** | `POST` | +| **内容类型** | `application/json` | +| **认证方式** | 设备 ID + 客户端 ID | + +## 快速开始 + +### 前置条件 + +在进行 OTA 请求之前,请确保: + +- ✅ 设备已连接到稳定的网络 +- ✅ 设备电量充足 +- ✅ 已获取设备的 MAC 地址 +- ✅ 已生成客户端 UUID + +### 最小请求示例 + +```bash +curl -X POST https://xrobo.qiniuapi.com/v1/ota/ \ + -H "Content-Type: application/json" \ + -H "Device-Id: D4:06:06:B6:A9:FB" \ + -H "Client-Id: web_test_client" \ + -H "User-Agent: esp-box-3/1.5.6" \ + -H "Activation-Version: 2" \ + -d '{ + "application": { + "version": "1.0.0", + "elf_sha256": "1234567890abcdef1234567890abcdef1234567890abcdef" + }, + "board": { + "type": "esp-box-3", + "name": "esp-box-3", + "ssid": "MyWiFi", + "rssi": -45 + } + }' +``` + +## API 参考 -POST /api/ota/ +### 请求头参数 -### 请求头 +| 参数名 | 类型 | 必需 | 说明 | 示例 | +|--------|------|------|------|------| +| `Activation-Version` | string | ✅ | 激活版本,efuse 区有序列号为"2",无为"1" | `"2"` | +| `Device-Id` | string | ✅ | 设备唯一标识符(MAC 地址或伪 MAC) | `"D4:06:06:B6:A9:FB"` | +| `Client-Id` | string | ✅ | 客户端 UUID v4(重装后会变化) | `"web_test_client"` | +| `User-Agent` | string | ✅ | 客户端名称和版本号 | `"esp-box-3/1.5.6"` | +| `Accept-Language` | string | ❌ | 客户端当前语言 | `"zh-CN"` | -- Activation-Version: 激活版本(必需,设备芯片 efuse 区是否存储了有效的序列号,有则"2",无则"1") -- Device-Id: 设备的唯一标识符(必需,使用 MAC 地址或由硬件 ID 生成的伪 MAC 地址) -- Client-Id: 客户端的唯一标识符,由软件自动生成的 UUID v4(必需,擦除 FLASH 或重装后会变化) -- User-Agent: 客户端的名字和版本号(必需,例如 esp-box-3/1.5.6) -- Accept-Language: 客户端的当前语言(可选,例如 zh-CN) +### 请求体参数 +#### 必需参数 -### 请求体 -请求体应为 JSON 格式,包含以下字段: -- application: 包含设备当前固件版本信息的对象(必需) - - version: 当前固件版本号 - - elf_sha256: 用于校验固件文件完整性 Hash -- mac_address: MAC 地址(可选,与 HTTP header 里的 device-id 一致) -- uuid: ClientId(可选,与 HTTP header 里的 client-id 一致) -- chip_model_name: 设备的芯片型号,例如 esp32s3(可选) -- flash_size: 设备的闪存大小(可选) -- psram_size: 设备的 PSRAM 大小(可选) -- partition_table: 设备分区表,用于检查是否有足够的空间,用于下载固件(可选) -- board: 开发板类型与版本,以及所运行的环境(必需) - - type: 开发板类型 - - name: 开发板 SKU(与 user-agent 中的前面部分保持一致) - - ssid: 设备接入的 Wi-Fi 名字 - - rssi: 设备接入的 Wi-Fi 信号强度 +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `application` | object | 设备当前固件版本信息 | +| `application.version` | string | 当前固件版本号 | +| `application.elf_sha256` | string | 固件文件完整性校验 Hash | +| `board` | object | 开发板信息 | +| `board.type` | string | 开发板类型 | +| `board.name` | string | 开发板 SKU | +| `board.ssid` | string | Wi-Fi 网络名称 | +| `board.rssi` | number | Wi-Fi 信号强度 | -### 响应 +#### 可选参数 -#### 成功响应 -响应体为 JSON 格式,包含以下字段: -- activation: 设备需要激活 - - code: 激活码 - - message: 屏幕显示消息 -- mqtt: MQTT 协议服务器配置信息 -- websocket: WebSocket 协议服务器配置信息 -- server_time: 服务器时间信息(用于同步设备时间) - - timestamp: 当前时间戳 - - timezone: 服务器时区 - - timezone_offset: 服务器时区偏移量 -- firmware: 最新版本固件信息 - - version: 固件版本号 - - url: 固件下载链接(如果有更新) +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `mac_address` | string | MAC 地址(与 Device-Id 一致) | +| `uuid` | string | 客户端 ID(与 Client-Id 一致) | +| `chip_model_name` | string | 芯片型号(如 esp32s3) | +| `flash_size` | number | 闪存大小 | +| `psram_size` | number | PSRAM 大小 | +| `partition_table` | array | 设备分区表信息 | + +### 响应参数 + +#### 成功响应 (200 OK) + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `activation` | object | 设备激活信息 | +| `activation.code` | string | **激活码**(重要) | +| `activation.message` | string | 屏幕显示消息 | +| `websocket` | object | WebSocket 服务器配置 | +| `websocket.url` | string | WebSocket 连接地址 | +| `mqtt` | object | MQTT 服务器配置(可选) | +| `server_time` | object | 服务器时间信息 | +| `server_time.timestamp` | number | 当前时间戳 | +| `server_time.timezone` | string | 服务器时区 | +| `server_time.timezone_offset` | number | 时区偏移量 | +| `firmware` | object | 固件更新信息 | +| `firmware.version` | string | 最新固件版本 | +| `firmware.url` | string | 固件下载链接 | #### 错误响应 -- 400 Bad Request: 请求缺少必需的字段或字段无效 - error: 错误信息 -- 500 Internal Server Error: 服务器内部错误 - error: 错误信息 +| 状态码 | 说明 | 响应体 | +|--------|------|--------| +| 400 | 请求参数错误 | `{"error": "Device ID is required"}` | +| 500 | 服务器内部错误 | `{"error": "Failed to read device auto_update status"}` | - - +## 完整示例 -## 请求示例 -```Plain Text +### 请求示例 + +```http POST https://xrobo.qiniuapi.com/v1/ota/ Host: xrobo.qiniuapi.com -Accept-Language: zh-CN Content-Type: application/json +Accept-Language: zh-CN Device-Id: D4:06:06:B6:A9:FB Client-Id: web_test_client +User-Agent: esp-box-3/1.5.6 +Activation-Version: 2 ``` ```json { "version": 0, - "uuid": "", + "uuid": "web_test_client", "application": { "name": "xiaoling-web-test", "version": "1.0.0", @@ -95,28 +169,41 @@ Client-Id: web_test_client "idf_version": "4.4.3", "elf_sha256": "1234567890abcdef1234567890abcdef1234567890abcdef" }, - "ota": { "label": "xiaoling-web-test" }, + "ota": { + "label": "xiaoling-web-test" + }, "board": { "type": "xiaoling-web-test", "name": "xiaoling-web-test", - "ssid": "xxxxxx", - "rssi": 0, - "channel": 0, - "ip": "192.168.1.1", + "ssid": "MyWiFiNetwork", + "rssi": -45, + "channel": 6, + "ip": "192.168.1.100", "mac": "D4:06:06:B6:A9:FA" }, - "flash_size": 0, - "minimum_free_heap_size": 0, + "flash_size": 16777216, + "minimum_free_heap_size": 50000, "mac_address": "D4:06:06:B6:A9:FA", - "chip_model_name": "", - "chip_info": { "model": 0, "cores": 0, "revision": 0, "features": 0 }, + "chip_model_name": "esp32s3", + "chip_info": { + "model": 9, + "cores": 2, + "revision": 0, + "features": 32 + }, "partition_table": [ - { "label": "", "type": 0, "subtype": 0, "address": 0, "size": 0 } + { + "label": "nvs", + "type": 1, + "subtype": 2, + "address": 36864, + "size": 24576 + } ] } ``` -## 响应示例 +### 成功响应示例 ```json { @@ -126,39 +213,114 @@ Client-Id: web_test_client "timezone_offset": 480 }, "activation": { - "code": "608303", //【这就是我们需要的激活码】 + "code": "608303", // 🔑 重要:这是设备激活码 "message": "http://60.205.58.18:8002\n608303", "challenge": "D4:06:06:B6:A9:FA" }, "firmware": { "version": "1.0.0", - "url": "https://xrobo.qiniuapi.com/v1/ota/INVALID_FIRMWARE_FOR_TEST" + "url": "https://xrobo.qiniuapi.com/v1/ota/firmware_download_url" }, "websocket": { - "url": "ws://xrobo-io.qiniuapi.com/v1/ws/" + "url": "ws://xrobo-io.qiniuapi.com/v1/ws/" // 🌐 WebSocket 连接地址 } } ``` -## 错误响应 +## 错误处理 -```json -//缺少 device-id -HTTP/1.1 400 Bad Request -Content-Type: application/json +### 常见错误及解决方案 + +#### 400 Bad Request - 缺少设备 ID +```json { -"error": "Device ID is required" + "error": "Device ID is required" } +``` + +**解决方案**:检查请求头中是否包含 `Device-Id` 参数 -//无效的 OTA 请求 +#### 400 Bad Request - 无效的 OTA 请求 + +```json { -"error": "Invalid OTA request" + "error": "Invalid OTA request" } +``` -//服务器内部错误 +**解决方案**: +- 检查请求体 JSON 格式是否正确 +- 确认必需字段 `application` 和 `board` 是否存在 +- 验证字段类型是否匹配 + +#### 500 Internal Server Error - 服务器错误 + +```json { -"error": "Failed to read device auto_update status" + "error": "Failed to read device auto_update status" } +``` + +**解决方案**: +- 稍后重试请求 +- 检查网络连接 +- 联系技术支持 +### 错误处理最佳实践 + +```javascript +// 示例:JavaScript 错误处理 +async function performOTARequest(deviceData) { + try { + const response = await fetch('https://xrobo.qiniuapi.com/v1/ota/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Device-Id': deviceData.deviceId, + 'Client-Id': deviceData.clientId, + 'User-Agent': deviceData.userAgent, + 'Activation-Version': '2' + }, + body: JSON.stringify(deviceData.payload) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(`OTA 请求失败: ${errorData.error}`); + } + + const result = await response.json(); + + // 提取关键信息 + const activationCode = result.activation?.code; + const websocketUrl = result.websocket?.url; + + return { activationCode, websocketUrl, ...result }; + + } catch (error) { + console.error('OTA 请求错误:', error.message); + throw error; + } +} ``` + +## 常见问题 + +### Q: 激活码的有效期是多长? +A: 激活码通常有效期为 24 小时,建议获取后尽快使用。 + +### Q: 设备重启后需要重新获取激活码吗? +A: 是的,设备重启或重新连接网络后需要重新发起 OTA 请求获取新的激活码。 + +### Q: WebSocket 连接失败怎么办? +A: 请检查: +1. WebSocket URL 是否正确 +2. 网络连接是否稳定 +3. 防火墙是否阻止了 WebSocket 连接 + +### Q: 如何判断是否有固件更新? +A: 比较响应中的 `firmware.version` 与设备当前版本,如果不同且 `firmware.url` 存在,则有更新可用。 + +### Q: MAC 地址格式有什么要求? +A: MAC 地址应使用冒号分隔的格式,如:`D4:06:06:B6:A9:FB`,字母使用大写。 diff --git a/docs/xrobot/platform/websocket.md b/docs/xrobot/platform/websocket.md deleted file mode 100644 index 1a952e9..0000000 --- a/docs/xrobot/platform/websocket.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -title: WebSocket 协议 ---- - -## WebSocket 协议介绍 - -WebSocket 是一种网络通信协议,提供了全双工通信通道,允许客户端和服务器之间进行实时数据交换。与传统的 HTTP 请求-响应模型不同,WebSocket 允许在单个连接上进行双向通信,这使得它非常适合需要实时更新的应用场景,如在线聊天、实时游戏和金融交易等。 - -WebSocket 协议的主要特点包括: - -- **全双工通信**:客户端和服务器可以同时发送和接收消息,减少了延迟。 -- **持久连接**:一旦建立连接,客户端和服务器可以在不重新建立连接的情况下进行多次数据交换。 -- **低开销**:WebSocket 连接在建立后,数据传输的开销较小,适合频繁的数据交换。 - -在本指南中,我们将详细介绍 WebSocket 的地址、通信协议、会话流程以及如何在小智设备中实现 WebSocket 通信。 - -请确保在使用 WebSocket 协议时,设备已连接到稳定的网络,以确保数据传输的可靠性。 - -## 1. WebSocket 地址 - - - -## 2. WebSocket 通信协议 - -### 基本信息 - -- 协议版本: 1 -- 传输方式: WebSocket -- 音频格式: OPUS -- 音频参数: - - 采样率: 16000Hz - - 通道数: 1 - - 帧长: 60ms - -### 会话流程 - -1. 建立 WebSocket 连接 -2. 交换 hello 消息 -3. 开始语音交互: - - 发送开始监听 - - 发送音频数据 - - 接收识别结果 - - 接收 TTS 音频 -4. 结束会话时关闭连接 - -```mermaid -sequenceDiagram - participant Client - participant Server - - Client->>Server: 建立 WebSocket 连接 (wss://xrobo-io.qiniuapi.com/v1/ws/) - Note right of Client: 携带 headers: Authorization, Protocol-Version, Device-Id, Client-Id - Server-->>Client: 连接成功 - - Client->>Server: 发送 hello 消息 - Note right of Client: JSON: {type: "hello", version: 1, transport: "websocket", audio_params: {...}} - Server-->>Client: 响应 hello 消息 - Note left of Server: JSON: {type: "hello", transport: "websocket", audio_params: {...}} - - Client->>Server: 发送开始监听消息 - Note right of Client: JSON: {session_id: "", type: "listen", state: "start", mode: ""} - - Client->>Server: 发送 OPUS 音频数据 (二进制) - Server-->>Client: 返回语音识别结果 - Server-->>Client: 发送 TTS 音频数据 (OPUS) - Note left of Server: JSON: {type: "tts", state: "", text: "<文本>"} - - Client->>Server: 发送停止监听消息 - Note right of Client: JSON: {session_id: "", type: "listen", state: "stop"} - - Client->>Server: 关闭 WebSocket 连接 -``` - -### 连接建立 - -1. 客户端连接 WebSocket 服务器时需要携带以下 headers: - - ```Plain Text - Authorization: Bearer 【服务端如果开启 auth,则需要验证 token】 - Protocol-Version: 1 - Device-Id: <设备 MAC 地址> 【服务端验证,需要先服务端绑定智能体或者预注册,才能验证通过】 - Client-Id: <设备 UUID> - ``` - - 设备 MAC 地址和 UUID 都是设备唯一识别码。 - - 用户登录(login)服务获取到 token。 - - 请参考[小智固件接入-三、设备绑定](../guide/xiaozhi-firmware#三、设备绑定) - -2. 连接成功后,客户端发送 hello 消息: - - ```JSON - { - "type": "hello", - "version": 1, - "transport": "websocket", - "audio_params": { - "format": "opus", - "sample_rate": 16000, - "channels": 1, - "frame_duration": 60 - } - } - ``` - -3. 服务端响应 hello 消息: - - ```JSON - { - "type": "hello", - "transport": "websocket", - "audio_params": { - "format": "opus", - "sample_rate": 24000, - "channels": 1, - "frame_duration": 60 - } - } - ``` - - WebSocket 协议不返回 session_id,所以消息中的会话 ID 可设置为空。 - -### 消息类型 - -1. 语音识别相关消息 - - 开始监听 - - ```JSON - { - "session_id": "<会话 ID>", - "type": "listen", - "state": "start", - "mode": "<监听模式>" - } - ``` - - 监听模式: - - - "auto": 自动停止 - - "manual": 手动停止 - - "realtime": 持续监听 - auto 与 realtime 是服务器端 VAD 的两种工作模式,realtime 需要 AEC 支持。 - - 停止监听 - - ```JSON - { - "session_id": "<会话 ID>", - "type": "listen", - "state": "stop" - } - ``` - - 唤醒词检测 - - ```JSON - { - "session_id": "<会话 ID>", - "type": "listen", - "state": "detect", - "text": "<唤醒词>" - } - ``` - -2. 语音合成相关消息 - - 服务端发送的 TTS 状态消息: - - ```JSON - { - "type": "tts", - "state": "<状态>", - "text": "<文本内容>" // 仅在 sentence_start 时携带 - } - ``` - - 状态类型: - - - "start": 开始播放 - - "stop": 停止播放 - - "sentence_start": 新句子开始 - -3. 中止消息 - - ```JSON - { - "session_id": "<会话 ID>", - "type": "abort", - "reason": "wake_word_detected" // 可选 - } - ``` - -4. 情感状态消息 - 大语言模型会使用 1 个 token 来输出 emoji 来表示当前的心情,这个 emoji 不会被 TTS 朗读出来,但是会被以独立数据类型进行返回。 - 示例: - -```JSON - {"type":"llm", "text": "😊", "emotion": "smile"} -``` - -以下是常用的 emoji 列表。 - -1. 😶 - neutral -2. 🙂 - happy -3. 😆 - laughing -4. 😂 - funny -5. 😔 - sad -6. 😠 - angry -7. 😭 - crying -8. 😍 - loving -9. 😳 - embarrassed -10. 😲 - surprised -11. 😱 - shocked -12. 🤔 - thinking -13. 😉 - winking -14. 😎 - cool -15. 😌 - relaxed -16. 🤤 - delicious -17. 😘 - kissy -18. 😏 - confident -19. 😴 - sleepy -20. 😜 - silly -21. 🙄 - confused - -### 状态流程图 - -Manual 模式 - - -Auto 模式 - - -### 二进制数据传输 - -- 音频数据使用二进制帧传输 -- 客户端发送 OPUS 编码的音频数据 -- 服务端返回 OPUS 编码的 TTS 音频数据 - -### 错误处理 - -当发生网络错误时,客户端会收到错误消息并关闭连接。客户端需要实现重连机制。 diff --git "a/docs/xrobot/platform/websocket\346\216\245\345\205\245.md" "b/docs/xrobot/platform/websocket\346\216\245\345\205\245.md" new file mode 100644 index 0000000..2802317 --- /dev/null +++ "b/docs/xrobot/platform/websocket\346\216\245\345\205\245.md" @@ -0,0 +1,850 @@ +--- +title: WebSocket 协议 +--- + +# WebSocket 接入文档 + +## 目录 + +- [概述](#概述) +- [快速开始](#快速开始) +- [连接建立](#连接建立) +- [消息协议](#消息协议) +- [会话流程](#会话流程) +- [完整示例](#完整示例) +- [错误处理](#错误处理) +- [常见问题](#常见问题) + +## 概述 + +### 什么是 WebSocket 协议 + +WebSocket 是小智设备与服务器进行实时语音交互的核心协议。它提供了全双工通信通道,支持: + +- 🎤 **实时语音识别**:将设备音频流实时转换为文字 +- 🔊 **语音合成播放**:接收服务器TTS音频并播放 +- 💬 **智能对话**:与大语言模型进行自然语言交互 +- 😊 **情感表达**:获取AI的情感状态用于设备表情显示 + +### 核心特性 + +| 特性 | 说明 | +|------|------| +| **全双工通信** | 客户端和服务器可同时发送和接收消息 | +| **持久连接** | 单次连接支持多轮对话,减少连接开销 | +| **低延迟** | 实时音频流传输,延迟极低 | +| **多模式支持** | 支持自动、手动、实时三种监听模式 | + +### 基本信息 + +| 项目 | 值 | +|------|-----| +| **WebSocket 地址** | `wss://xrobo-io.qiniuapi.com/v1/ws/` | +| **协议版本** | `1` | +| **音频格式** | `OPUS` | +| **采样率** | `16000Hz` (上行) / `24000Hz` (下行) | +| **通道数** | `1` (单声道) | +| **帧长** | `60ms` | + +## 快速开始 + +### 前置条件 + +在建立WebSocket连接之前,请确保: + +- ✅ 设备已通过 [OTA协议](./OTA.md) 获取激活码 +- ✅ 设备已连接到稳定的网络 +- ✅ 已获取有效的访问令牌(如果启用认证) +- ✅ 设备支持OPUS音频编解码 + +### 最小连接示例 + +```javascript +// 1. 建立WebSocket连接 +const ws = new WebSocket('wss://xrobo-io.qiniuapi.com/v1/ws/', [], { + headers: { + 'Protocol-Version': '1', + 'Device-Id': 'D4:06:06:B6:A9:FB', + 'Client-Id': 'web_test_client', + 'Authorization': 'Bearer your_access_token' // 可选 + } +}); + +// 2. 发送hello消息 +ws.onopen = () => { + ws.send(JSON.stringify({ + type: "hello", + version: 1, + transport: "websocket", + audio_params: { + format: "opus", + sample_rate: 16000, + channels: 1, + frame_duration: 60 + } + })); +}; + +// 3. 处理服务器响应 +ws.onmessage = (event) => { + if (typeof event.data === 'string') { + const message = JSON.parse(event.data); + console.log('收到消息:', message); + } else { + console.log('收到音频数据:', event.data); + } +}; +``` + +## 连接建立 + +### 连接参数 + +建立WebSocket连接时需要在请求头中携带以下参数: + +| 参数名 | 类型 | 必需 | 说明 | 示例 | +|--------|------|------|------|------| +| `Protocol-Version` | string | ✅ | 协议版本号 | `"1"` | +| `Device-Id` | string | ✅ | 设备MAC地址(需预注册) | `"D4:06:06:B6:A9:FB"` | +| `Client-Id` | string | ✅ | 设备UUID | `"web_test_client"` | +| `Authorization` | string | ❌ | 访问令牌(如果启用认证) | `"Bearer token123"` | + +### Hello 握手 + +#### 客户端发送 + +```json +{ + "type": "hello", + "version": 1, + "transport": "websocket", + "audio_params": { + "format": "opus", + "sample_rate": 16000, + "channels": 1, + "frame_duration": 60 + } +} +``` + +#### 服务器响应 + +```json +{ + "type": "hello", + "transport": "websocket", + "audio_params": { + "format": "opus", + "sample_rate": 24000, // 服务器下行采样率 + "channels": 1, + "frame_duration": 60 + } +} +``` + +## 消息协议 + +### 消息类型概览 + +| 消息类型 | 发送方向 | 说明 | 数据格式 | 触发时机 | +|----------|----------|------|----------|----------| +| `hello` | 🔄 **双向** | 连接握手消息 | JSON | 连接建立后立即发送 | +| `listen` | 📤 **客户端 → 服务器** | 监听状态控制 | JSON | 开始/停止语音识别时 | +| `audio` | 🔄 **双向** | 音频数据流 | 二进制(OPUS) | 语音输入/TTS输出时 | +| `tts` | 📥 **服务器 → 客户端** | TTS播放状态 | JSON | 语音合成状态变化时 | +| `llm` | 📥 **服务器 → 客户端** | AI情感状态 | JSON | AI回复时的情感表达 | +| `abort` | 📤 **客户端 → 服务器** | 中止当前会话 | JSON | 需要打断对话时 | + +--- + +## 详细消息格式 + +### 1. Hello 握手消息 🔄 + +#### 客户端发送 (📤 Client → Server) + +**用途:** 建立连接后的协议握手,协商音频参数 + +```json +{ + "type": "hello", + "version": 1, + "transport": "websocket", + "audio_params": { + "format": "opus", // 音频编码格式 + "sample_rate": 16000, // 客户端上行采样率 + "channels": 1, // 声道数 + "frame_duration": 60 // 帧长度(ms) + } +} +``` + +#### 服务器响应 (📥 Server → Client) + +**用途:** 确认连接并返回服务器音频参数 + +```json +{ + "type": "hello", + "transport": "websocket", + "audio_params": { + "format": "opus", // 音频编码格式 + "sample_rate": 24000, // 服务器下行采样率 + "channels": 1, // 声道数 + "frame_duration": 60 // 帧长度(ms) + } +} +``` + +--- + +### 2. Listen 监听控制消息 📤 + +**发送方向:** 客户端 → 服务器 +**用途:** 控制语音识别的开始、停止和模式 + +#### 开始监听 + +```json +{ + "session_id": "session_12345", // 可选,会话标识 + "type": "listen", + "state": "start", + "mode": "auto" // 监听模式:auto | manual | realtime +} +``` + +#### 停止监听 + +```json +{ + "session_id": "session_12345", // 可选,会话标识 + "type": "listen", + "state": "stop" +} +``` + +#### 唤醒词检测 + +```json +{ + "session_id": "session_12345", // 可选,会话标识 + "type": "listen", + "state": "detect", + "text": "小智小智" // 检测到的唤醒词 +} +``` + +**监听模式说明:** + +| 模式 | 说明 | 停止方式 | 适用场景 | +|------|------|----------|----------| +| `auto` | 自动停止 | 服务器VAD检测到静音 | 一般对话场景 | +| `manual` | 手动停止 | 客户端发送stop消息 | 需要精确控制的场景 | +| `realtime` | 持续监听 | 需要AEC支持,实时处理 | 实时交互场景 | + +--- + +### 3. Audio 音频数据 🔄 + +**发送方向:** 双向 +**数据格式:** 二进制 OPUS 编码 +**用途:** 传输语音输入和TTS输出音频 + +#### 客户端发送音频 (📤 Client → Server) + +```javascript +// 发送OPUS编码的音频数据 +const audioData = new Uint8Array([...]); // OPUS编码的音频帧 +websocket.send(audioData); +``` + +**音频参数要求:** +- 编码格式:OPUS +- 采样率:16000Hz +- 声道数:1 (单声道) +- 帧长:60ms + +#### 服务器发送音频 (📥 Server → Client) + +```javascript +// 接收OPUS编码的TTS音频数据 +websocket.onmessage = (event) => { + if (event.data instanceof ArrayBuffer) { + // 这是TTS音频数据,可直接播放 + playAudio(event.data); + } +}; +``` + +**音频参数:** +- 编码格式:OPUS +- 采样率:24000Hz +- 声道数:1 (单声道) +- 帧长:60ms + +--- + +### 4. TTS 状态消息 📥 + +**发送方向:** 服务器 → 客户端 +**用途:** 通知客户端TTS播放状态变化 + +#### TTS 开始播放 + +```json +{ + "type": "tts", + "state": "start" +} +``` + +#### TTS 停止播放 + +```json +{ + "type": "tts", + "state": "stop" +} +``` + +#### TTS 新句子开始 + +```json +{ + "type": "tts", + "state": "sentence_start", + "text": "你好,我是灵矽小助手" // 当前播放的句子内容 +} +``` + +**TTS 状态说明:** + +| 状态值 | 说明 | 包含文本字段 | 触发时机 | +|--------|------|--------------|----------| +| `start` | 开始播放TTS | ❌ | TTS音频开始输出 | +| `stop` | 停止播放TTS | ❌ | TTS音频结束输出 | +| `sentence_start` | 新句子开始播放 | ✅ | 每个句子开始播放时 | + +--- + +### 5. LLM 情感状态消息 📥 + +**发送方向:** 服务器 → 客户端 +**用途:** 传递AI的情感状态,用于设备表情显示 + +```json +{ + "type": "llm", + "text": "😊", // 情感表情符号 + "emotion": "happy" // 情感类型标识 +} +``` + +**支持的情感类型:** + +| 表情 | 情感标识 | 中文描述 | 使用场景 | +|------|----------|----------|----------| +| 😶 | `neutral` | 中性 | 默认状态 | +| 🙂 | `happy` | 开心 | 积极回应 | +| 😆 | `laughing` | 大笑 | 幽默内容 | +| 😂 | `funny` | 搞笑 | 有趣对话 | +| 😔 | `sad` | 悲伤 | 同情安慰 | +| 😠 | `angry` | 生气 | 表达不满 | +| 😭 | `crying` | 哭泣 | 极度悲伤 | +| 😍 | `loving` | 喜爱 | 表达喜欢 | +| 😳 | `embarrassed` | 尴尬 | 尴尬场面 | +| 😲 | `surprised` | 惊讶 | 意外情况 | +| 😱 | `shocked` | 震惊 | 极度惊讶 | +| 🤔 | `thinking` | 思考 | 思考问题 | +| 😉 | `winking` | 眨眼 | 调皮暗示 | +| 😎 | `cool` | 酷 | 自信表现 | +| 😌 | `relaxed` | 放松 | 轻松状态 | +| 🤤 | `delicious` | 美味 | 食物相关 | +| 😘 | `kissy` | 亲吻 | 亲密表达 | +| 😏 | `confident` | 自信 | 自信回应 | +| 😴 | `sleepy` | 困倦 | 疲倦状态 | +| 😜 | `silly` | 调皮 | 顽皮表现 | +| 🙄 | `confused` | 困惑 | 不理解时 | + +--- + +### 6. Abort 中止消息 📤 + +**发送方向:** 客户端 → 服务器 +**用途:** 主动中止当前会话或对话 + +```json +{ + "session_id": "session_12345", // 可选,会话标识 + "type": "abort", + "reason": "wake_word_detected" // 可选,中止原因 +} +``` + +**常见中止原因:** + +| 原因标识 | 说明 | 使用场景 | +|----------|------|----------| +| `wake_word_detected` | 检测到新的唤醒词 | 用户重新唤醒设备 | +| `user_interrupt` | 用户主动打断 | 用户按键或手势打断 | +| `timeout` | 会话超时 | 长时间无交互 | +| `error` | 发生错误 | 系统异常需要重置 | +| `manual` | 手动中止 | 用户明确要求停止 | + +## 会话流程 + +### 完整交互时序图 + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant Server as 服务器 + + Note over Client,Server: 1. 建立连接 + Client->>Server: WebSocket连接 + Headers + Server-->>Client: 连接成功 + + Note over Client,Server: 2. 握手 + Client->>Server: hello消息 + Server-->>Client: hello响应 + + Note over Client,Server: 3. 语音交互 + Client->>Server: 开始监听 (listen/start) + Client->>Server: 音频数据流 (OPUS) + Server-->>Client: TTS状态 (tts/sentence_start) + Server-->>Client: 情感状态 (llm) + Server-->>Client: TTS音频数据 (OPUS) + Server-->>Client: TTS状态 (tts/stop) + Client->>Server: 停止监听 (listen/stop) + + Note over Client,Server: 4. 结束会话 + Client->>Server: 关闭连接 +``` + +### 监听模式状态流程 + +#### Manual 模式流程 +```mermaid +graph TD + A[发送 listen/start] --> B[发送音频数据] + B --> C[接收TTS响应] + C --> D[手动发送 listen/stop] + D --> E[结束本轮对话] + E --> F{继续对话?} + F -->|是| A + F -->|否| G[关闭连接] +``` + +#### Auto 模式流程 +```mermaid +graph TD + A[发送 listen/start] --> B[发送音频数据] + B --> C[服务器VAD检测静音] + C --> D[自动停止监听] + D --> E[接收TTS响应] + E --> F{继续对话?} + F -->|是| A + F -->|否| G[关闭连接] +``` + +### 音频数据传输 + +#### 音频格式要求 + +| 参数 | 客户端上行 | 服务器下行 | +|------|------------|------------| +| **编码格式** | OPUS | OPUS | +| **采样率** | 16000Hz | 24000Hz | +| **通道数** | 1 (单声道) | 1 (单声道) | +| **帧长** | 60ms | 60ms | +| **传输方式** | WebSocket二进制帧 | WebSocket二进制帧 | + +#### 音频数据流 + +```javascript +// 发送音频数据示例 +function sendAudioData(audioBuffer) { + // audioBuffer 应该是 OPUS 编码的音频数据 + if (ws.readyState === WebSocket.OPEN) { + ws.send(audioBuffer); // 直接发送二进制数据 + } +} + +// 接收音频数据示例 +ws.onmessage = (event) => { + if (event.data instanceof ArrayBuffer) { + // 这是 OPUS 编码的 TTS 音频数据 + playAudioData(event.data); + } else { + // 这是 JSON 消息 + const message = JSON.parse(event.data); + handleMessage(message); + } +}; +``` + +## 完整示例 + +### JavaScript 完整实现 + +```javascript +classLin xWebSocket { + constructor(deviceId, clientId, accessToken = null) { + this.deviceId = deviceId; + this.clientId = clientId; + this.accessToken = accessToken; + this.ws = null; + this.isConnected = false; + this.sessionId = null; + } + + // 建立连接 + async connect() { + const headers = { + 'Protocol-Version': '1', + 'Device-Id': this.deviceId, + 'Client-Id': this.clientId + }; + + if (this.accessToken) { + headers['Authorization'] = `Bearer ${this.accessToken}`; + } + + this.ws = new WebSocket('wss://xrobo-io.qiniuapi.com/v1/ws/', [], { + headers: headers + }); + + return new Promise((resolve, reject) => { + this.ws.onopen = () => { + console.log('WebSocket 连接已建立'); + this.sendHello(); + }; + + this.ws.onmessage = (event) => { + this.handleMessage(event); + }; + + this.ws.onclose = (event) => { + console.log('WebSocket 连接已关闭', event.code, event.reason); + this.isConnected = false; + }; + + this.ws.onerror = (error) => { + console.error('WebSocket 错误:', error); + reject(error); + }; + + // 等待 hello 响应 + this.onHelloReceived = resolve; + }); + } + + // 发送 hello 消息 + sendHello() { + const helloMessage = { + type: "hello", + version: 1, + transport: "websocket", + audio_params: { + format: "opus", + sample_rate: 16000, + channels: 1, + frame_duration: 60 + } + }; + + this.send(helloMessage); + } + + // 处理消息 + handleMessage(event) { + if (typeof event.data === 'string') { + const message = JSON.parse(event.data); + + switch (message.type) { + case 'hello': + console.log('收到 hello 响应:', message); + this.isConnected = true; + if (this.onHelloReceived) { + this.onHelloReceived(); + } + break; + + case 'tts': + console.log('TTS 状态:', message); + this.onTTSStatus && this.onTTSStatus(message); + break; + + case 'llm': + console.log('情感状态:', message); + this.onEmotion && this.onEmotion(message); + break; + + default: + console.log('未知消息类型:', message); + } + } else { + // 二进制音频数据 + console.log('收到音频数据:', event.data.byteLength, '字节'); + this.onAudioData && this.onAudioData(event.data); + } + } + + // 开始监听 + startListening(mode = 'auto') { + const message = { + session_id: this.sessionId || '', + type: 'listen', + state: 'start', + mode: mode + }; + + this.send(message); + } + + // 停止监听 + stopListening() { + const message = { + session_id: this.sessionId || '', + type: 'listen', + state: 'stop' + }; + + this.send(message); + } + + // 发送音频数据 + sendAudio(audioBuffer) { + if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(audioBuffer); + } + } + + // 中止会话 + abort(reason = null) { + const message = { + session_id: this.sessionId || '', + type: 'abort' + }; + + if (reason) { + message.reason = reason; + } + + this.send(message); + } + + // 发送 JSON 消息 + send(message) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(message)); + } + } + + // 关闭连接 + close() { + if (this.ws) { + this.ws.close(); + } + } +} + +// 使用示例 +const client = newLinxWebSocket( + 'D4:06:06:B6:A9:FB', // Device ID + 'web_test_client', // Client ID + 'your_access_token' // Access Token (可选) +); + +// 设置事件处理器 +client.onTTSStatus = (message) => { + console.log('TTS状态变化:', message.state, message.text); +}; + +client.onEmotion = (message) => { + console.log('AI情感:', message.emotion, message.text); + // 更新设备表情显示 + updateDeviceExpression(message.emotion); +}; + +client.onAudioData = (audioData) => { + // 播放接收到的TTS音频 + playTTSAudio(audioData); +}; + +// 连接并开始对话 +async function startConversation() { + try { + await client.connect(); + console.log('连接成功,开始监听...'); + client.startListening('auto'); + + // 模拟发送音频数据 + // const audioData = await recordAudio(); + // client.sendAudio(audioData); + + } catch (error) { + console.error('连接失败:', error); + } +} + +startConversation(); +``` + +## 错误处理 + +### 常见错误类型 + +| 错误类型 | 原因 | 解决方案 | +|----------|------|----------| +| **连接失败** | 网络问题、服务器不可达 | 检查网络连接,实现重连机制 | +| **认证失败** | Token无效或过期 | 刷新Token,重新获取访问权限 | +| **设备未注册** | Device-Id未在服务器注册 | 联系管理员注册设备 | +| **协议版本不匹配** | Protocol-Version不支持 | 更新客户端到支持的协议版本 | +| **音频格式错误** | 音频编码格式不正确 | 确保使用OPUS编码,参数正确 | + +### 重连机制实现 + +```javascript +class ReconnectingWebSocket extendsLinxWebSocket { + constructor(deviceId, clientId, accessToken = null) { + super(deviceId, clientId, accessToken); + this.maxReconnectAttempts = 5; + this.reconnectInterval = 1000; // 1秒 + this.reconnectAttempts = 0; + } + + async connect() { + try { + await super.connect(); + this.reconnectAttempts = 0; // 重置重连计数 + } catch (error) { + console.error('连接失败:', error); + this.scheduleReconnect(); + } + } + + scheduleReconnect() { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++; + console.log(`${this.reconnectInterval}ms 后进行第 ${this.reconnectAttempts} 次重连...`); + + setTimeout(() => { + this.connect(); + }, this.reconnectInterval); + + // 指数退避 + this.reconnectInterval = Math.min(this.reconnectInterval * 2, 30000); + } else { + console.error('达到最大重连次数,停止重连'); + } + } + + // 重写 onclose 处理 + setupEventHandlers() { + this.ws.onclose = (event) => { + console.log('连接关闭:', event.code, event.reason); + this.isConnected = false; + + // 如果不是正常关闭,尝试重连 + if (event.code !== 1000) { + this.scheduleReconnect(); + } + }; + } +} +``` + +### 错误处理最佳实践 + +```javascript +// 1. 网络状态监控 +window.addEventListener('online', () => { + console.log('网络已连接,尝试重连...'); + if (!client.isConnected) { + client.connect(); + } +}); + +window.addEventListener('offline', () => { + console.log('网络已断开'); +}); + +// 2. 心跳检测 +function startHeartbeat() { + setInterval(() => { + if (client.isConnected) { + // 发送心跳消息或检查连接状态 + if (client.ws.readyState !== WebSocket.OPEN) { + console.log('检测到连接异常,尝试重连...'); + client.connect(); + } + } + }, 30000); // 30秒检查一次 +} + +// 3. 音频错误处理 +function handleAudioError(error) { + console.error('音频处理错误:', error); + + switch (error.type) { + case 'encoding_error': + console.log('音频编码错误,检查OPUS编码器'); + break; + case 'playback_error': + console.log('音频播放错误,检查音频设备'); + break; + default: + console.log('未知音频错误'); + } +} +``` + +## 常见问题 + +### Q: WebSocket连接建立后立即断开怎么办? +A: 检查以下几点: +1. Device-Id是否已在服务器注册 +2. Protocol-Version是否正确 +3. 网络防火墙是否阻止WebSocket连接 +4. 如果启用认证,检查Authorization token是否有效 + +### Q: 音频数据发送后没有收到TTS响应? +A: 可能的原因: +1. 音频格式不正确(确保使用OPUS编码) +2. 音频数据损坏或为空 +3. 没有发送开始监听消息 +4. 服务器端语音识别失败 + +### Q: 如何判断当前监听状态? +A: 通过跟踪发送的listen消息状态: +- 发送`listen/start`后进入监听状态 +- 收到`tts/start`表示开始接收响应 +- 收到`tts/stop`表示响应结束 +- 发送`listen/stop`或自动停止后退出监听状态 + +### Q: 情感状态消息的作用是什么? +A: 情感状态消息用于: +1. 设备表情显示(LED、屏幕等) +2. 增强用户交互体验 +3. 提供AI当前"心情"的可视化反馈 + +### Q: 如何处理网络不稳定的情况? +A: 建议实现: +1. 自动重连机制(指数退避策略) +2. 网络状态监控 +3. 心跳检测 +4. 本地缓存机制 +5. 优雅降级(网络差时降低音频质量) + +### Q: 支持同时连接多个WebSocket吗? +A: 一个设备(Device-Id)同时只能建立一个WebSocket连接。新连接会导致旧连接被服务器主动断开。 + +### Q: 如何优化音频传输性能? +A: 优化建议: +1. 使用适当的OPUS编码参数 +2. 控制音频帧大小(推荐60ms) +3. 避免频繁的小数据包传输 +4. 实现音频缓冲机制 +5. 根据网络状况动态调整音频质量