--- /dev/null
+# 三代 IOT 设计文档(简化版)
+
+> 由 PDF 自动提取:`1.doc/K8_diaozhui/design/三代IOT设计文档 (简化版).pdf`(30 页)
+> 提取方式:PyMuPDF 文本导出;页眉页脚与版式可能有残留字符。
+
+## 目录(与固件版本相关)
+
+| 章节 | 指令 | 是否含 `version` |
+|------|------|------------------|
+| 设备 WebSocket 上报 | `device_info` | **是**(`data.version`) |
+| 设备 WebSocket 上报 | `post_heart_beat` | 否(仅 `battery`) |
+| 平台下发 | `get_device_state` | 触发设备回 `device_info` |
+| 小程序 WebSocket 推送 | `push_device_info_rest` | **是**(平台转发 `device_info` 结果) |
+| HTTP 查询 | `/ws-bus/query/device-state/{deviceId}` | 文档示例**无** `version` 字段 |
+
+---
+
+## Page 1
+
+三代IOT设计文档 (简化版)
+概述
+IOT物联网云平台:实现设备连接、消息上报。
+业务通讯架构
+IOT云平台
+Websocket基础通讯协议
+Json指令协议
+字段
+magic
+version
+type
+playload_le
+n
+payload
+## Page 2
+
+说明
+固定0xFFFE
+0x01
+参考类型说明
+载荷长度
+载荷
+大小
+2byte
+1byte
+1byte
+4byte
+N
+type类型说明
+类型值
+类型名称
+描述
+0x00
+设备登录
+废弃,通过url中携带bz-dt来登录
+0x01
+用户登录
+废弃,通过url中携带bz-ut来登录
+0x02
+心跳
+废弃,通过Json来心跳即可
+0x03
+json内容
+具体的内容,请参考协议内容,包括小程序与iot的,设备与iot的。
+数据流格式
+字段
+magic
+version
+type
+msgId
+flag
+playload_le
+n
+payload
+说明
+固定0xFFFE
+
+0x01
+
+参考类型说
+明
+
+消息ID
+
+标记0x00结
+束,0x01未
+结束
+载荷长度
+载荷
+大小
+2byte
+1byte
+1byte
+4byte
+1byte
+4byte
+N
+类型说明
+类型值
+类型名称
+描述
+0x04
+下行监听音频
+PCM/WAV/MP3流,由产品协商
+0x05
+上行监听音频
+PCM/WAV/MP3流,由产品协商
+0x06
+图片下行
+图片内容,格式由产品协商
+0x07
+图片上行
+图片内容,格式由产品协商
+0x08
+上行留言语音
+PCM/WAV/MP3流,由产品协商
+连接认证
+## Page 3
+
+在header上或者query上:设备携带token值,header上key是`authorization`。query上是:bz-
+ut/bz-dt,其中bz-ut是用户令牌,bz-dt是设备令牌。
+访问地址
+代码块
+测试环境设备连接地址:ws(s)://dmp-dev.raisound.com/iot-api/ws-hub/dev/v1?bz-dt=
+测试环境小程序连接地址:wss://dmp-dev.raisound.com/iot-api/ws-hub/user/v1?bz-ut
+生产环境设备连接地址:ws(s)://wsiot-nocdn.raisound.com/ws-hub/dev/v1?bz-dt=
+生产环境小程序连接地址:wss://wsiot-nocdn.raisound.com/ws-hub/user/v1?bz-ut=
+(CDN)生产环境设备连接地址:ws(s)://wsiot.raisound.com/ws-hub/dev/v1?bz-dt=
+(CDN)生产环境小程序连接地址:wss://wsiot.raisound.com/ws-hub/user/v1?bz-ut=
+query需要携带设备令牌key为:bz-dt,用户令牌key为:bz-ut
+Json指令数据结构
+传输的数据由json格式进行传输。文件/图片/音频等数据传到对象存储中,然后把连接以指令的方式传
+给设备,由设备根据对象地址进行获取使用。
+上报数据和下发数据的数据结构:
+代码块
+{
+ "cmd":"命令字符串",
+ "ts":1738283829
+ "msg":"消息说明",
+ "data":"JsonObject类型的数据对象"
+}
+•
+根据指令使用data里的数据
+•
+data里的数据是个JsonObject
+•
+设备ID会根据header上的token进行解析
+NLP与IOT的http协议内容
+通用指令接口✅
+请求路径
+/ws-bus/cmd/ai
+应用场景
+转发命令字符串给设备
+1
+2
+3
+4
+5
+6
+1
+2
+3
+4
+5
+6
+## Page 4
+
+请求方法
+POST
+请求参数
+
+•
+Query
+◦
+deviceId:设备ID
+◦
+deviceToken:设备token
+◦
+cmd:ai_cmd
+◦
+data:指令数据
+响应结果
+•
+下发失败
+代码块
+{
+ "success": false,
+ "msg": "设备不在线.",
+ "code": 50000,
+ "data": null
+}
+•
+下发成功
+代码块
+{
+ "success": true,
+ "msg": "下发成功.",
+ "code": 20000,
+ "data": null
+}
+
+设备接收指令
+代码块
+{
+ "cmd": "ai_cmd",
+ "msg": "AI指令:ai_cmd",
+ "data": "指令数据",
+ "ts": 1761153310392,
+ "targetDeviceId": "E405F8648007"
+}
+1
+2
+3
+4
+5
+6
+1
+2
+3
+4
+5
+6
+1
+2
+3
+4
+5
+6
+7
+设备-IOT的WebSocket内容
+## Page 5
+
+设备与iot云平台走的是websocket协议。
+心跳✓
+协议名称
+心跳
+应用场景
+心跳,不超过5分钟即可,响应的bind表示已绑定用户,unbind表示未绑定,unbind的情况下
+需要打开蓝牙,供app扫描绑定。
+指令
+post_heart_beat
+上报内容
+代码块
+{
+ "cmd":"post_heart_beat",
+ "ts":1738283829,
+ "msg":"心跳",
+ "data":{
+ "battery": 20
+ }
+}
+响应内容
+代码块
+{
+ "cmd":"post_rest",
+ "ts":1738283829,
+ "msg":"消息收到响应",
+ "data":{
+ "cmd":"post_heart_beat",
+ "rest":"bind/unbind",
+ "msg":"上报成功/格式错误"
+ }
+}
+1
+2
+3
+4
+5
+6
+7
+8
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+接收指令
+下发用户解绑✓
+协议名称
+用户解绑设备推送
+应用场景
+设备收到这个指令以后,广播蓝牙,让小程序可以搜索到设备进行绑定
+## Page 6
+
+指令
+unbind_dev
+下发内容
+
+代码块
+{
+ "cmd":"user_unbind_dev",
+ "ts":1738283829,
+ "msg":"解绑设备",
+ "data":null
+}
+响应内容
+
+代码块
+{
+ "cmd":"dispatch_rest",
+ "ts":1738283829,
+ "msg":"消息收到响应",
+ "data":{
+ "cmd":"user_unbind_dev",
+ "rest":"success|error",
+ "msg":"收到用户解绑设备指令"
+ }
+}
+1
+2
+3
+4
+5
+6
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+下发查找设备✓
+指令
+查找设备find_device
+指令说明
+收到该指令后,响铃 震动
+指令内容
+
+代码块
+{
+ "cmd": "find_device",
+ "msg": "查找设备",
+ "data": null,
+ "ts": 1759328001026,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+## Page 7
+
+下发音量修改✓
+指令
+音量修改switch_volume
+指令说明
+收到该指令后,对设备音量进行修改
+指令内容
+
+代码块
+{
+ "cmd":"switch_volume",
+ "data":{
+ "target":"+增加音量;-减少音量;[0,100]修改音量为特定百分比
+值"
+ }
+}
+1
+2
+3
+4
+5
+6
+下发开始声音克隆✓
+指令
+开始声音克隆start_voice_clone
+指令说明
+收到该指令后,开始声音克隆
+指令内容
+
+代码块
+{
+ "cmd": "start_voice_clone",
+ "msg": "开始声音克隆",
+ "data": null,
+ "ts": 1759329865731,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+下发获取获取设备状态✓
+指令
+get_device_state
+应用场景
+获取设备状态,状态会在推送中得到;调用时机,当UI需要电量、音量、wifi信号强弱时调
+用。
+请求内容
+代码块
+{
+1
+## Page 8
+
+ "cmd": "get_device_state",
+ "msg": "获取设备状态",
+ "data": null,
+ "ts": 1759330147313,
+ "targetDeviceId": "E405F8F71009"
+}
+2
+3
+4
+5
+6
+7
+下发清除配网✓
+指令
+wifi_clean_pair
+应用场景
+用户在app上点击清除配网,用于毛绒玩具,没有按键的设备;前提条件是已连接当前网络。
+请求内容
+代码块
+{
+ "cmd": "start_voice_clone",
+ "msg": "开始声音克隆",
+ "data": null,
+ "ts": 1759329865731,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+解绑
+指令
+device_unbind
+应用场景
+用户解绑设备时,下发这个指令,然后设备清除配网,清除本地数据,打开蓝牙广播。
+请求内容
+代码块
+{
+ "cmd":"device_unbind",
+ "data":null
+}
+1
+2
+3
+4
+socket通道上报
+当前服务状态上传✓
+(聆听中|播放中|空闲中|思考中)
+## Page 9
+
+指令
+chat_state
+指令说明
+当前对话状态的上传
+上报内容
+
+代码块
+{
+ "msg": "当前服务状态上传",
+ "data": {
+ "state": "listening|speaking|idle|thinking",
+ "deviceId": "E405F8F71009"
+ },
+ "cmd": "chat_state",
+ "ts": 1759979110
+}
+响应内容
+代码块
+{
+ "cmd": "post_rest",
+ "ts": 1759979110661,
+ "data": {
+ "cmd": "chat_state",
+ "rest": "success",
+ "msg": "收到服务状态上报结果."
+ }
+}
+1
+2
+3
+4
+5
+6
+7
+8
+9
+1
+2
+3
+4
+5
+6
+7
+8
+9
+上报对话文本(用于显示对话)✓
+指令
+chat_text
+指令说明
+对话内容上传
+上报内容
+
+代码块
+{
+ "msg": "上报对话文本",
+ "data": [
+ {
+ "deviceId": "E405F8F71009",
+ "type": "req", //req:请求说的话
+ "text": "今天天气如何?",
+ "createTime": 1759979306
+1
+2
+3
+4
+5
+6
+7
+8
+## Page 10
+
+ },
+ {
+ "deviceId": "E405F8F71009",
+ "type": "res", //res:响应的内容
+ "text": "今天暴雨,记得要带伞哦.?",
+ "createTime": 1759979306
+ }
+ ],
+ "cmd": "chat_text",
+ "ts": 1759979306
+}
+响应内容
+代码块
+{
+ "cmd": "post_rest",
+ "ts": 1759977562040,
+ "data": {
+ "cmd": "chat_text",
+ "rest": "success",
+ "msg": "收到对话文本结果."
+ }
+}
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+1
+2
+3
+4
+5
+6
+7
+8
+9
+上报设备信息(电量,Wi-Fi信号强度)✓
+指令
+device_info
+指令说明
+设备信息上报
+上报内容
+
+代码块
+{
+ "msg": "上报设备信息",
+ "data": {
+ "deviceId": "E405F8F71009",
+ "battery": "电池,百分比值,比如说30,25",
+ "wifiRssi": "wifi强弱:0,1,2,3,4,5。转成这几个梯度",
+ "volume": "百分比数值",
+ "bargein": "打断模式:0/1",
+ "currentRoleName": "当前音色的name",
+ "charge": "0表示未充电,1表示充电中",
+ "version": "版本号"
+ },
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+## Page 11
+
+ "cmd": "device_info",
+ "ts": 1759979415
+}
+响应内容
+代码块
+{
+ "cmd": "post_rest",
+ "ts": 1759975798004,
+ "data": {
+ "cmd": "device_info",
+ "rest": "success",
+ "msg": "收到设备信息结果."
+ }
+}
+13
+14
+15
+1
+2
+3
+4
+5
+6
+7
+8
+9
+获取智能体ID(主动上报)✓
+指令
+get_bot
+指令说明
+获取智能体配置,会在set_bot指令响应
+上报内容
+
+代码块
+{
+ "msg": "获取智能体ID",
+ "data": null,
+ "cmd": "get_bot",
+ "ts": 1759979520
+}
+响应内容
+代码块
+{
+ "cmd": "set_bot",
+ "msg": "简单设备信息",
+ "data": {
+ "agent": {
+ "name": "孙悟空",
+ "speakerId": "zh_male_sunwukong_mars_bigtts",
+ "id": "sunwukong",
+ "speakerType": "default"
+ },
+1
+2
+3
+4
+5
+6
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+## Page 12
+
+ "speaker": {
+ "name": "猴哥",
+ "language": "中文",
+ "id": "zh_male_sunwukong_mars_bigtts",
+ "type": "default"
+ },
+ "bargein": null,
+ "memoryId": null,
+ "longTermMemory": null
+ },
+ "ts": 1759974598622,
+ "targetDeviceId": "E405F8F71009"
+}
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+小程序与IOT的http协议内容
+小程序与iot云平台:指令下发,数据回显,走的是Https协议。
+参考文档:http://118.145.159.222:30700/doc.html
+下发指令
+通用接口✓
+请求路径
+/ws-bus/cmd/{deviceId}
+请求方法
+POST
+请求参数
+•
+path
+◦
+deviceId 设备Id
+•
+Query
+◦
+cmd:指令内容
+◦
+data:下发数据
+响应结果
+代码块
+{
+ "success": true,
+ "msg": "下发成功.",
+ "code": 20000,
+ "data": null
+}
+1
+2
+3
+4
+5
+6
+## Page 13
+
+查找设备✓
+指令
+查找设备find_device
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:find_device
+◦
+data:
+应用场景
+app向设备下发此指令,设备收到后响铃震动,用于找到设备所在的位置
+设备接收指令
+内容
+代码块
+{
+ "cmd": "find_device",
+ "msg": "查找设备",
+ "data": null,
+ "ts": 1759328001026,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+音色切换✓
+指令
+音色切换switch_accent
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:switch_accent
+◦
+data:
+应用场景
+修改音色,通过此接口切换设备TTS播报的音色
+设备接收指令
+内容
+
+代码块
+{
+ "cmd": "switch_accent",
+ "msg": "音色切换",
+ "data": {
+ "target": "1"
+1
+2
+3
+4
+5
+## Page 14
+
+ },
+ "ts": 1759328048259,
+ "targetDeviceId": "E405F8F71009"
+}
+6
+7
+8
+9
+设置智能体✓
+指令
+设置智能体set_bot
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:set_bot
+◦
+data: {"botId": "7462617881076351002", "botName": "小量博士"}
+应用场景
+app向设备下发智能体配置,该配置会被后台保存,设备开机时向后台获取上次配置
+设备接收指令
+内容
+代码块
+{
+ "cmd": "set_bot",
+ "msg": "设置智能体",
+ "data": {
+ "bot_name": "小量博士",
+ "bot_id": "7462617881076351002"
+ },
+ "ts": 1759331069327,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+开始声音克隆✓
+指令
+开始声音克隆start_voice_clone
+## Page 15
+
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:start_voice_clone
+◦
+data:
+应用场景
+声音克隆,音色复刻
+设备接收指令
+内容
+代码块
+{
+ "cmd": "start_voice_clone",
+ "msg": "开始声音克隆",
+ "data": null,
+ "ts": 1759329865731,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+清除配网✓
+指令
+清除配网 wifi_clean_pair
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:wifi_clean_pair
+◦
+data:
+应用场景
+用户在app上点击清除配网,用于毛绒玩具,没有按键的设备;前提条件是已连接当前网络。
+设备接收指令
+内容
+代码块
+{
+ "cmd": "wifi_clean_pair",
+ "msg": "清除配网",
+ "data": null,
+ "ts": 1759329910033,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+## Page 16
+
+获取设备状态✓
+指令
+get_device_state
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:get_device_state
+◦
+data:
+应用场景
+获取设备状态,状态会在推送中得到;调用时机,当UI需要电量、音量、wifi信号强弱时调
+用。
+设备接收指令
+内容
+代码块
+{
+ "cmd": "get_device_state",
+ "msg": "获取设备状态",
+ "data": null,
+ "ts": 1759330147313,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+更新设备✓
+指令
+让设备重新获取配置信息renew_device
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:renew_device
+◦
+data:
+应用场景
+app向设备下发此指令,设备收到后提示“更新配置中,设备将自动重启”,设备从dmp接口
+获取设备的配置信息,并重新发起对话
+设备接收指令
+内容
+
+代码块
+{
+ "cmd": "renew_device",
+ "msg": "更新设备",
+1
+2
+3
+## Page 17
+
+ "data": null,
+ "ts": 1759330208898,
+ "targetDeviceId": "E405F8F71009"
+}
+4
+5
+6
+7
+更新配置角色-dmp平台的角色✓
+指令
+通知设备更新了角色配置 setup_role
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:setup_role
+◦
+data:
+应用场景
+app向设备下发此指令,设备收到后提示“更新配置中”,并重新发起对话
+设备接收指令
+内容
+
+代码块
+{
+ "cmd": "setup_role",
+ "msg": "更新配置角色",
+ "data": null,
+ "ts": 1759330265367,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+设置打断模式✓
+指令
+set_break_off
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:set_break_off
+◦
+data:1
+应用场景
+用户在app上选择打断模式。
+## Page 18
+
+设备接收指令
+内容
+代码块
+{
+ "cmd": "set_break_off",
+ "msg": "设置打断模式",
+ "data": {
+ "target": "1:按键打断;2:插话打断"
+ },
+ "ts": 1759330363089,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+8
+9
+设置亮度✓
+指令
+亮度修改switch_brightness
+请求示例
+•
+path
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:switch_brightness
+◦
+data:50
+指令说明
+用户在app上滑动亮度调节。
+设备接收指令
+内容
+
+代码块
+{
+ "cmd": "switch_brightness",
+ "msg": "设置亮度",
+ "data": {
+ "target": "[0,100]修改亮度为特定百分比值"
+ },
+ "ts": 1759330840342,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+8
+9
+设置音量✓
+指令
+设置音量修改switch_volume
+请求示例
+•
+path
+## Page 19
+
+◦
+deviceId:E405F8F71009
+•
+Query
+◦
+cmd:switch_volume
+◦
+data:+20
+指令说明
+收到该指令后,对设备音量进行修改
+设备接收指令
+内容
+
+代码块
+{
+ "cmd": "switch_volume",
+ "msg": "音量修改",
+ "data": {
+ "target":"+增加音量;-减少音量;[0,100]修改音量为特定百分比
+值"
+ },
+ "ts": 1759331616977,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+8
+9
+查询接口
+获取设备的在线状态✓
+接口名称
+获取设备的在线状态
+业务说明
+小程序APP在打开首页的时候,先检查设备的在线状态。如果不在线,显示配网按钮
+请求路径
+/ws-bus/query/device-state/{deviceId}
+请求方法
+GET
+请求参数
+•
+path
+◦
+deviceId 设备Id
+响应结果
+
+•
+设备不在线
+代码块
+{
+ "success": true,
+ "msg": "获取设备状态成功.",
+ "code": 20000,
+ "data": {
+1
+2
+3
+4
+5
+## Page 20
+
+ "id": "68d4e5acbe41c865bbf8055d",
+ "deviceId": "E405F8F71009",
+ "battery": 1,
+ "mediaState": "stop",
+ "mediaTitle": null,
+ "deviceState": "offline",
+ "volume": 50,
+ "mediaCover": null,
+ "createTime": 1758782892683,
+ "updateTime": 1758782892683
+ }
+}
+•
+无权限获取
+代码块
+{
+ "timestamp": "2025-10-01 21:36",
+ "status": 403,
+ "error": "Forbidden",
+ "path": "/query/device-state/E405F8F71001"
+}
+•
+ 设备在线
+代码块
+{
+ "success": true,
+ "msg": "获取设备状态成功.",
+ "code": 20000,
+ "data": {
+ "id": "68d4e5acbe41c865bbf8055d",
+ "deviceId": "E405F8F71009",
+ "battery": 1,
+ "mediaState": "stop",
+ "mediaTitle": null,
+ "deviceState": "online",
+ "volume": 50,
+ "mediaCover": null,
+ "createTime": 1758782892683,
+ "updateTime": 1758782892683
+ }
+}
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+1
+2
+3
+4
+5
+6
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+## Page 21
+
+IOT与小程序的websocket协议
+iot平台推送数据给小程序,走的是websocket协议,传输的内容参考以下格式:
+心跳✓
+协议名称
+心跳
+应用场景
+心跳,不超过1分钟即可
+指令
+post_heart_beat
+上传内容
+代码块
+{
+ "cmd":"post_heart_beat",
+ "ts":1738283829,
+ "msg":"心跳",
+ "data":null
+}
+响应内容
+代码块
+{
+ "cmd":"post_rest",
+ "ts":1738283829,
+ "msg":"消息收到响应",
+ "data":{
+ "cmd":"post_heart_beat",
+ "rest":"success|error",
+ "msg":"上报成功/格式错误"
+ }
+}
+1
+2
+3
+4
+5
+6
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+接收指令
+接收设备状态(上线/下线)变化✓
+协议名称
+设备状态(上线/下线)变化
+应用场景
+## Page 22
+
+当设备连上iot的websocket,通知用户设备已上线;当设备断开iot的websocket时,通知用户和
+设备已下线
+指令
+push_dev_state
+设备上线通
+知
+
+代码块
+{
+ "cmd": "push_dev_state",
+ "msg": null,
+ "data": {
+ "id": "68d4e5acbe41c865bbf8055d",
+ "deviceId": "E405F8F71009",
+ "battery": 1,
+ "mediaState": "stop",
+ "mediaTitle": null,
+ "deviceState": "online",
+ "volume": 50,
+ "mediaCover": null,
+ "createTime": 1758782892683,
+ "updateTime": 1758782892683
+ },
+ "ts": 1759324958781,
+ "targetDeviceId": "E405F8F71009"
+}
+设备下线通
+知
+
+代码块
+{
+ "cmd": "push_dev_state",
+ "msg": null,
+ "data": {
+ "id": "68d4e5acbe41c865bbf8055d",
+ "deviceId": "E405F8F71009",
+ "battery": 1,
+ "mediaState": "stop",
+ "mediaTitle": null,
+ "deviceState": "offline",
+ "volume": 50,
+ "mediaCover": null,
+ "createTime": 1758782892683,
+ "updateTime": 1758782892683
+ },
+ "ts": 1759325118496,
+ "targetDeviceId": "E405F8F71009"
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+## Page 23
+
+}
+18
+接收当前服务状态✓
+(聆听中|播放中|空闲中|思考中)
+指令
+push_chat_state_rest
+应用场景
+设备对话中,把对话的状态上报给后台,后台把状态推送给app
+请求内容
+代码块
+{
+ "cmd": "push_chat_state_rest",
+ "msg": "收到设备服务状态上报结果.",
+ "data": {
+ "deviceId": "E405F8F71009",
+ "state": "listening"
+ },
+ "ts": 1759979110670,
+ "targetDeviceId": "E405F8F71009"
+}
+
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+接收对话文本(用于显示对话)✓
+指令
+push_chat_text_rest
+指令说明
+当前对话状态的上传
+上报内容
+
+代码块
+{
+ "cmd": "push_chat_text_rest",
+ "msg": "收到对话文本结果.",
+ "data": [
+ {
+ "deviceId": "E405F8F71009",
+1
+2
+3
+4
+5
+6
+## Page 24
+
+ "type": "req",
+ "text": "今天天气如何?",
+ "createTime": 1759979306
+ },
+ {
+ "deviceId": "E405F8F71009",
+ "type": "res",
+ "text": "今天暴雨,记得要带伞哦.?",
+ "createTime": 1759979306
+ }
+ ],
+ "ts": 1759979306740,
+ "targetDeviceId": "E405F8F71009"
+}
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+接收设备信息(电量,Wi-Fi信号强度)✓
+指令
+push_device_info_rest
+指令说明
+设备信息上报
+上报内容
+
+代码块
+{
+ "cmd": "push_device_info_rest",
+ "msg": "收到设备信息结果.",
+ "data": {
+ "deviceId": "E405F8F71009",
+ "battery": "电池,百分比值,比如说30,25",
+ "wifiRssi": "wifi强弱:0,1,2,3,4,5。转成这几个梯度",
+ "volume": "百分比数值",
+ "bargein": "打断模式:0/1",
+ "currentRoleName": "当前音色的name",
+ "charge": "0表示未充电,1表示充电中",
+ "version": "版本号"
+ },
+ "ts": 1759979416209,
+ "targetDeviceId": "E405F8F71009"
+}
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+SDK
+## Page 25
+
+go版本
+代码块
+package test
+
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "fmt"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+
+ ".go_vvvdevice/common"
+ ".go_vvvdevice/log"
+ ".go_vvvdevice/modules"
+ "github.com/gorilla/websocket"
+)
+
+var wg sync.WaitGroup
+
+func wsClient() *websocket.Conn {
+ ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
+ defer cancel()
+
+ urlStr := fmt.Sprintf("wss://dmp-dev.raisound.com/iot-api/ws-hub/dev/v1?bz-
+dt=%s", "954668a315d61dd4cacacb01b255387bca")
+ header := http.Header{
+ "User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64)
+AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"},
+ }
+
+ fmt.Println(urlStr)
+ ws, _, wsErr := websocket.DefaultDialer.DialContext(ctx, urlStr, header)
+ if wsErr != nil {
+ log.Warn(fmt.Sprintf("Dial failed: %s", wsErr.Error()))
+ }
+ return ws
+}
+
+// Json数据
+func fmtJsonMsg(jsonStr string) []byte {
+ log.Debug(fmt.Sprintf("拼接数据: %s", jsonStr))
+ // 创建一个字节缓冲区
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+## Page 26
+
+ buf := new(bytes.Buffer)
+ // 写入头部
+ buf.Write([]byte{0xFF, 0xFE})
+ // 写入版本
+ buf.WriteByte(0x01)
+ // 写入类型
+ buf.WriteByte(0x03)
+ // 将 JSON 字符串转换为字节数组
+ jsonBytes := []byte(jsonStr)
+ // 计算载荷长度
+ jsonLen := len(jsonBytes)
+ jsonLenBytes := make([]byte, 4)
+ // 载荷长度转大端序(高位在前)
+ binary.BigEndian.PutUint32(jsonLenBytes, uint32(jsonLen))
+ // 写入长度
+ buf.Write(jsonLenBytes)
+ // 写入 JSON 数据
+ buf.Write(jsonBytes)
+ return buf.Bytes()
+}
+
+func Run() {
+ wg.Add(1)
+
+ // 创建WebSocket连接
+ client := wsClient()
+ defer func() {
+ _ = client.Close()
+ os.Exit(0)
+ }()
+
+ heartTicker := time.NewTicker(3 * time.Second)
+ // 心跳协程
+ go func() {
+ for {
+ select {
+ case <-heartTicker.C:
+ heartData :=
+fmtJsonMsg(fmt.Sprintf(`{"cmd":"post_heart_beat","ts":%d,"msg":"心跳","data":
+{"battery":%d}}`, time.Now().Unix(), common.RandInt(1, 101)))
+ log.Debug(fmt.Sprintf("Write Message: %v", heartData))
+ _, wErr := modules.SendMsg(client, heartData,
+websocket.BinaryMessage)
+ if wErr != nil {
+ log.Error(fmt.Sprintf("SendMsg Error: %s", wErr))
+ reStart()
+ }
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+## Page 27
+
+ }
+ }
+ }()
+
+ // 接收协程
+ go func() {
+ for {
+ // 接收服务器返回的信息
+ _, result, readErr := client.ReadMessage()
+ log.Debug(fmt.Sprintf("服务器返回: %v\n字符串内容: %s", result,
+string(result)))
+ if readErr != nil {
+ log.Error(fmt.Sprintf("ReadMessage Error: %s", readErr.Error()))
+ reStart()
+ }
+ }
+ }()
+ wg.Wait()
+}
+
+func reStart() {
+ wg.Done()
+ time.Sleep(300 * time.Millisecond)
+ Run()
+}
+java版本
+代码块
+package com.dobell.iot;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.WebSocket;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.time.Instant;
+import java.util.Random;
+import java.util.concurrent.*;
+
+public class WsClient {
+
+ private static final String URL =
+ "wss://dmp-dev.raisound.com/iot-api/ws-hub/dev/v1?bz-
+dt=954668a315d61dd4cacacb01b255387bca";
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+## Page 28
+
+
+ private static WebSocket webSocket;
+ private static ScheduledExecutorService scheduler;
+ private static final Random RANDOM = new Random();
+
+ /** ===== main 启动入口 ===== */
+ public static void main(String[] args) {
+ start();
+ }
+
+ /** 启动逻辑(等价 Go 的 Run) */
+ private static void start() {
+ try {
+ scheduler = Executors.newScheduledThreadPool(2);
+ connect();
+ startHeartbeat();
+
+ // 阻塞主线程,防止 main 退出
+ Thread.currentThread().join();
+ } catch (Exception e) {
+ e.printStackTrace();
+ restart();
+ }
+ }
+
+ /** 建立 WebSocket 连接 */
+ private static void connect() {
+ HttpClient client = HttpClient.newHttpClient();
+
+ webSocket = client.newWebSocketBuilder()
+ .connectTimeout(java.time.Duration.ofSeconds(7))
+ .header("User-Agent",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64)
+AppleWebKit/537.36 " +
+ "(KHTML, like Gecko) Chrome/138.0.0.0
+Safari/537.36")
+ .buildAsync(URI.create(URL), new WsListener())
+ .join();
+
+ System.out.println("WebSocket connected");
+ }
+
+ /** 心跳任务(3 秒一次) */
+ private static void startHeartbeat() {
+ scheduler.scheduleAtFixedRate(() -> {
+ try {
+ long ts = Instant.now().getEpochSecond();
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+## Page 29
+
+ int battery = RANDOM.nextInt(100) + 1;
+
+ String json = String.format(
+ "{\"cmd\":\"post_heart_beat\",\"ts\":%d,\"msg\":\"心跳
+\",\"data\":{\"battery\":%d}}",
+ ts, battery
+ );
+
+ ByteBuffer data = buildBinaryMsg(json);
+ webSocket.sendBinary(data, true).join();
+
+ System.out.println("Send heartbeat: " + json);
+ } catch (Exception e) {
+ e.printStackTrace();
+ restart();
+ }
+ }, 0, 3, TimeUnit.SECONDS);
+ }
+
+ /** 二进制协议封包(完全等价 Go fmtJsonMsg) */
+ private static ByteBuffer buildBinaryMsg(String json) {
+ byte[] jsonBytes = json.getBytes();
+ ByteBuffer buffer = ByteBuffer.allocate(2 + 1 + 1 + 4 +
+jsonBytes.length);
+ buffer.order(ByteOrder.BIG_ENDIAN);
+
+ buffer.put((byte) 0xFF);
+ buffer.put((byte) 0xFE);
+ buffer.put((byte) 0x01); // version
+ buffer.put((byte) 0x03); // type
+ buffer.putInt(jsonBytes.length);
+ buffer.put(jsonBytes);
+ buffer.flip();
+
+ return buffer;
+ }
+
+ /** 重连(等价 Go reStart) */
+ private static synchronized void restart() {
+ try {
+ System.err.println("Restarting...");
+ if (scheduler != null) {
+ scheduler.shutdownNow();
+ }
+ if (webSocket != null) {
+ webSocket.abort();
+ }
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
+101
+102
+103
+104
+105
+## Page 30
+
+ Thread.sleep(300);
+ start();
+ } catch (Exception ignored) {
+ }
+ }
+
+ /** WebSocket 回调 */
+ static class WsListener implements WebSocket.Listener {
+
+ @Override
+ public CompletionStage<?> onBinary(WebSocket ws,
+ ByteBuffer data,
+ boolean last) {
+ byte[] bytes = new byte[data.remaining()];
+ data.get(bytes);
+ System.out.println("Server(binary): " + new String(bytes));
+ return WebSocket.Listener.super.onBinary(ws, data, last);
+ }
+
+ @Override
+ public CompletionStage<?> onText(WebSocket ws,
+ CharSequence data,
+ boolean last) {
+ System.out.println("Server(text): " + data);
+ return WebSocket.Listener.super.onText(ws, data, last);
+ }
+
+ @Override
+ public void onError(WebSocket ws, Throwable error) {
+ System.err.println("WebSocket error: " + error.getMessage());
+ restart();
+ }
+
+ @Override
+ public CompletionStage<?> onClose(WebSocket ws,
+ int statusCode,
+ String reason) {
+ System.err.println("WebSocket closed: " + statusCode + " " +
+reason);
+ restart();
+ return WebSocket.Listener.super.onClose(ws, statusCode, reason);
+ }
+ }
+}
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148