Files

264 lines
13 KiB
Markdown
Raw Permalink Normal View History

# VoiceLink:MQTT、在线状态与固件升级
平台地址:`https://82.157.201.114`。EchoEar 固件已接入下面的 MQTT 心跳、HTTPS 升级清单及结果上报协议;是否能够远程升级,以设备实际上报的 `ota_supported`、在线状态及真实升级回执为准。
部署时 `FIRMWARE_DOWNLOAD_BASE_URL` 必须配置为设备能够访问的平台 HTTPS 根地址,例如 `https://82.157.201.114`。它只用于生成 `/f/{token}` 短下载地址,不能填写 MinIO 内网地址。
HTTP 成功统一返回 `{"data": ...}` 外层结构,下面展示其中的 data。设备 HTTP 使用现有 DEVREQ1 请求头;小程序沿用登录 token。无需新增出厂烧录密钥。
## 8. MQTT 连接与设备心跳
### 设备取得连接参数
`POST https://82.157.201.114/api/v1/device/mqtt-config`
输入:
```json
{}
```
输出示例:
```json
{
"broker_url": "mqtt://82.157.201.114:1883",
"client_id": "KP01-020000260914",
"username": "KP01-020000260914",
"password": "<平台返回值,原样使用>",
"heartbeat_interval_seconds": 30,
"keepalive_seconds": 60,
"status_topic": "vl/devices/KP01-020000260914/status",
"ota_topic": "vl/devices/KP01-020000260914/ota",
"qos": 1
}
```
| 字段 | 中文说明与用途 |
|---|---|
| broker_url | MQTT 服务器,TCP 1883,明文连接 |
| client_id / username / password | 连接时原样使用,不是老人的账号密码 |
| heartbeat_interval_seconds | 心跳间隔,30秒 |
| keepalive_seconds | MQTT Keep Alive,60秒 |
| status_topic | 设备发布在线心跳的主题 |
| ota_topic | 设备订阅升级指令的主题 |
| qos | 消息等级1,可能重复投递,升级需要按 task_id 去重 |
使用 MQTT 3.1.1,Clean Session=true。每次重连后重新订阅 ota_topic,订阅成功后立即上报心跳。消息是完整 UTF-8 JSON,不加蓝牙长度头,Retain=false。当前 MQTT 链路不使用 TLS,连接密码和消息内容在传输中不加密。
### 设备每30秒发布心跳
发送到返回的 status_topic:
```json
{
"online": true,
"firmware_version": "1.0.0",
"version_code": 0,
"ota_supported": true
}
```
| 输入字段 | 中文说明与用途 |
|---|---|
| online | true=在线;主动离线时发送false |
| firmware_version | 当前实际运行固件的显示版本名称 |
| version_code | 当前实际运行固件的数字顺序号;尚未关联平台固件的出厂版本填0,完成升级后使用指令里的数字 |
| ota_supported | 最终硬件已实现OTA时填true,否则false,小程序不会允许更新 |
设置遗嘱主题为自己的 status_topic,遗嘱内容:
```json
{"online": false}
```
平台以 MQTT 连接和最近心跳判断在线:连接断开即离线,超过90秒未收到心跳也视为离线。仅建立连接、未发心跳不算在线;时间以服务器接收时间为准。
设备不用自行计算 MQTT 密码。若需要排查,当前平台生成方法是 `Base64URL无填充(HMAC-SHA256(device_key原始32字节, UTF8("MQTT1\n" + factory_id)))`。与已有设备 HTTP 签名不是同一个计算内容。
## 9. 小程序查询设备在线情况
`GET https://82.157.201.114/api/v1/devices/{id}/status`
输入:路径 id 为平台设备 UUID,不是 factory_id;无请求正文。只能查询当前账号有权查看的设备。
该接口与下一节 firmware 接口返回相同结构,小程序只需要请求一次 firmware,便可同时展示在线、版本和升级进度。
主要输出:online=true/false、last_seen_at=最近心跳时间(未收到过为null)。查询失败应显示“状态未知”,不能当成已在线。
## 10. 当前版本、最新版本与SaaS固件管理
`GET https://82.157.201.114/api/v1/devices/{id}/firmware`
输入:路径设备 UUID,无请求正文。
输出 data 示例(版本、ID和下载链接仅为示例):
```json
{
"online": true,
"last_seen_at": "2026-09-14T15:00:00Z",
"current_version": "1.0.0",
"current_version_code": 0,
"current_download_url": "",
"ota_supported": true,
"latest": {
"id": "e6380075-682c-40ca-b327-d2662d04a02c",
"model": "VL-S3",
"version": "1.1.0",
"version_code": 12,
"release_notes": "改善联网稳定性",
"size": 3145728,
"sha256": "abababababababababababababababababababababababababababababababab",
"status": "READY",
"created_at": "2026-09-14T14:00:00Z",
"download_url": "https://82.157.201.114/f/K7z3mF2pQ8xLc4VnB1wRtg"
},
"update_available": true,
"ota_task": null
}
```
| 输出字段 | 中文说明与用途 |
|---|---|
| current_version / current_version_code | 设备已上报的当前版本名称/顺序号,未上报时名称为空、顺序号0 |
| current_download_url | 当前固件下载地址;平台未收录该版本时为空 |
| latest | 同租户、同型号中顺序号最大的已发布固件;暂无固件时null |
| latest.version / latest.version_code | 最新显示名称/数字顺序号 |
| latest.release_notes | 更新内容 |
| latest.download_url | 最新固件临时下载地址,有效30分钟,需要时重新查询 |
| update_available | 最新顺序号大于当前顺序号时true,不能比较版本名称的字符串大小 |
| ota_task | 最近一次升级任务,没有则null |
小程序每10秒刷新,离开页面停止。最新顺序号等于当前时显示“当前是最新版本”;暂无固件时显示“该型号暂无已发布固件”。在线、支持OTA、存在更新且没有进行中任务时,才允许点击更新。两个下载地址可以在小程序中复制。
### SaaS上传页面
只填写:**设备型号、版本名称、更新内容、固件文件**。顺序号由平台自动生成,数字越大越新,不要求连续;每个型号单独判断最新版本。最大文件32MB。同型号已经发布的版本名称不能重复,已发布文件不能覆盖;上传失败可以用相同版本名称重试。
管理接口需运营账号登录,并携带 `X-Tenant-Id`:
| 接口 | 输入 | 输出/用途 |
|---|---|---|
| GET /api/v1/ops/device-firmware/models | 无正文 | items:库存已有型号,供下拉选择 |
| GET /api/v1/ops/device-firmware | 无正文 | items:最近200条固件记录,包含版本名称、数字顺序号、状态和更新内容 |
| POST /api/v1/ops/device-firmware | model、version、release_notes、size、sha256 | 固件记录、自动生成的version_code、upload_url |
| PUT 返回的upload_url | 固件文件原始字节,Content-Type: application/octet-stream | 2xx表示文件上传成功 |
| POST /api/v1/ops/device-firmware/{id}/finalize | 无正文 | 校验文件大小和SHA256,保存发布文件,状态变为READY;之前不会提供给设备更新 |
| GET /api/v1/ops/device-firmware/{id}/download | 无正文 | 固件信息和临时download_url |
前端自动计算 size(字节数)、sha256(文件内容的SHA256,64位小写十六进制),用户无需填写。
EchoEar 上传的是 ESP-IDF 构建生成的**应用固件** `build/VolcRTCDemo.bin`。不要上传整片 Flash 备份、合并烧录包、bootloader 或分区表。当前设备两个应用槽分别为 4 MiB,应用文件必须小于等于槽大小;平台通用上传上限 32 MB 不代表 EchoEar 可以安装 32 MB 文件。版本名称应与本次构建的 `main/firmware_version.h` 中 `DEVICE_FIRMWARE_VERSION` 保持一致。
发布后,在绑定设备的家属端进入设备详情 →「固件与远程控制」→「更新固件」→ 确认。发布不会自动升级所有同型号设备;每次更新都对当前选择的单台设备发起。保持设备通电联网,结束通话后操作,等到状态显示「升级完成」且当前版本变为目标版本再确认完成。新 Wi-Fi 或手机热点需要先完成重新配网,离线设备无法接收升级指令。
创建固件输入示例:
```json
{"model":"VL-S3","version":"1.1.0","release_notes":"改善联网稳定性","size":3145728,"sha256":"abababababababababababababababababababababababababababababababab"}
```
## 11. 用户点击更新,平台通过MQTT下发
### 小程序请求
`POST https://82.157.201.114/api/v1/devices/{id}/ota`
输入:
```json
{"version_code":12}
```
version_code 使用最新查询结果,小程序不传下载地址。同一 `X-Request-ID` 重试返回同一任务,不重复下发;不同请求也不能同时创建两个进行中升级。
成功为HTTP 200,data示例:
```json
{"task_id":"f58d3018-38a8-4fc4-b490-4d3154fa8464","status":"pending","version":"1.1.0","version_code":12,"message":"","created_at":"2026-09-14T15:00:00Z"}
```
常见拒绝:DEVICE_OFFLINE(离线)、DEVICE_BUSY(通话/指令/升级进行中)、OTA_UNSUPPORTED(设备未实现OTA)、VERSION_CHANGED(最新版本变化或已经无需更新)。此时刷新设备页面。
### 设备收到升级指令
接收主题:mqtt-config 返回的 ota_topic。
```json
{
"task_id": "f58d3018-38a8-4fc4-b490-4d3154fa8464",
"model": "VL-S3",
"version": "1.1.0",
"version_code": 12,
"download_url": "https://82.157.201.114/f/K7z3mF2pQ8xLc4VnB1wRtg",
"sha256": "abababababababababababababababababababababababababababababababab",
"size": 3145728,
"expires_at_ms": 1789398030000
}
```
| 输入字段 | 中文说明与用途 |
|---|---|
| task_id | 本次升级任务,持久化保存,用于去重和重启后的结果上报 |
| model | 必须与设备型号一致 |
| version / version_code | 升级后显示的版本名称/数字顺序号,顺序号必须大于当前版本 |
| download_url | HTTPS短地址下载固件,不通过MQTT传文件;当前完整地址不超过64字符 |
| sha256 / size | 下载后的文件必须校验,计算方式同上一节 |
| expires_at_ms | 指令最晚开始接收时间,Unix毫秒,发出后30秒;过期指令不能开始升级 |
MQTT 使用明文连接,升级消息仅用于唤醒。设备不得直接采用 MQTT 消息中的下载地址、摘要或目标版本安装固件。收到 `task_id` 后,先使用现有 DEVREQ1 身份签名与 HTTPS 证书校验获取服务端清单:
`POST https://82.157.201.114/api/v1/device/ota/manifest`
```json
{"task_id":"f58d3018-38a8-4fc4-b490-4d3154fa8464"}
```
返回 `data` 中含 `task_id`、`model`、`version`、`version_code`、`download_url`、`sha256`、`size`、`expires_at_ms`,字段格式同上面的升级消息。清单只可由该任务所属设备获取,服务端重新签发短下载票据;下载时使用这次 HTTPS 返回的地址。其他设备、已结束任务、已过期任务、未发布或型号不符的固件均不能获取清单。新请求清单会使同一任务之前的下载票据失效。
设备步骤:接收唤醒并取得 HTTPS 清单 → 检查型号、顺序号、有效期和当前是否空闲 → 持久化 task_id 和目标版本 → HTTP 上报 accepted → 上报成功后下载并校验 → 写入另一个应用槽并重启 → 新槽启动及健康检查成功后上报 success。下载/校验/安装失败上报 failed;正在通话时拒绝并上报 failed 及原因。同一 task_id 重复到达只回复已有进度,不再执行安装。
`/f/{token}` 是平台签发的短期私有下载票据,不会再跳转到较长的对象存储预签名地址。平台下载接口支持 `HEAD` 和 HTTP `Range`;当前 EchoEar 固件下载中断后保留旧槽并报告失败,由用户重新发起升级,不承诺自动断点续传。任务成功、失败、超时或票据过期后立即失效。设备不得记录或上报完整票据,安装前仍必须校验 HTTPS 清单中的大小和 SHA-256。
## 12. 设备报告升级结果
`POST https://82.157.201.114/api/v1/device/ota/report`
设备已接收:
```json
{"task_id":"f58d3018-38a8-4fc4-b490-4d3154fa8464","status":"accepted"}
```
重启并确认运行目标固件后:
```json
{"task_id":"f58d3018-38a8-4fc4-b490-4d3154fa8464","status":"success","firmware_version":"1.1.0","version_code":12}
```
失败:
```json
{"task_id":"f58d3018-38a8-4fc4-b490-4d3154fa8464","status":"failed","message":"固件下载失败,仍运行原版本"}
```
成功输出 data:
```json
{"status":"ok"}
```
| 输入字段 | 中文说明与用途 |
|---|---|
| task_id | 必须是平台发给本设备的升级任务ID |
| status | accepted=接收并准备升级;success=实际运行新版本;failed=升级失败 |
| firmware_version / version_code | success时必填,必须同时与目标名称和数字顺序号一致 |
| message | 失败原因,最多500字节,可省略 |
上报允许重试;完成任务后继续每30秒心跳,填写实际运行的版本和顺序号。不能只因为下载完就上报success。网络不通时保留结果,重连后补报。
平台任务状态:pending=已发送、accepted=设备升级中、success=已完成、failed=失败、timeout=结果超时。30秒未确认或10分钟未完成转为timeout,超时不代表升级成功;设备可以补报success/failed。过期任务不能再接收为accepted。
防火墙:设备 MQTT 需要公网 TCP 1883,HTTP 接口与固件下载仍走 HTTPS 443。不需要给小程序直接连接 MQTT。