Files
water/docs/mqtt-embedded-communication.md
yuhaiming 70e2356f22 fix(app): 修复 AppController 安全与查询问题
- 加强异常处理、类型安全和图片上传校验
- 优化设备相关查询,避免重复访问数据源
- 补充并记录并发测试与审查修复实施计划
2026-07-17 08:20:44 +08:00

341 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MQTT 与嵌入式通信协议
本文档说明后端服务与嵌入式设备之间的 MQTT Topic、通信方向、Payload 类型和 JSON 数据格式。
> **Topic 标识变更v5.6 起)**:除首次注册回复 `registerDeviceNo` 仍按 **MAC 地址** 下发外,其余 MQTT 下行命令 Topic 第一段统一使用**设备编号 deviceNo**。后端上行处理通过 `DeviceIdentityResolver` 兼容 MAC / 设备编号两种入参;下行命令请按 `deviceNo` 订阅 `subscriber/cmd` 或 `subscriber/schedule`。业务字段payload 中的 `deviceNo`、数据库主键、API 入参)仍是设备编号,未变更。
当前配置位置:
- 后端订阅配置:`water-admin/src/main/resources/application.yml`
- 后端上行消息分发:`water-modules/water-app/src/main/java/org/dromara/app/mqtt`
- 后端命令下发:`water-common/water-common-mqtt/src/main/java/org/dromara/mqtt/DeviceMqttCommandPublisher.java`
## 基础约定
| 项目 | 说明 |
| --- | --- |
| Broker | `mqtt.broker-url` |
| QoS | `mqtt.qos`,当前默认 `1` |
| 设备标识 | 首次注册 Topic 使用设备 **MAC**;注册完成后的上、下行业务 Topic 统一使用 `deviceNo`LWT 离线 Topic 兼容 MAC |
| Topic 变量 | 文档中的 `{deviceNo}` 指服务端分配的设备编号,`{mac}` 指设备 MAC 地址 |
| JSON 编码 | UTF-8 |
| 时间格式 | `yyyy-MM-dd HH:mm:ss` |
后端会订阅以下 Topic
```yaml
- /+/publish/finish/schedule
- /+/publish/register
- /+/publish/status
- /+/publish/power
- /+/publish/ack
- /+/publish/finish/key
- /+/publish/error
- /+/subscriber/cmd
```
## Topic 总览
| 功能名称 | 通信方向 | 订阅名称 / Topic | Payload 类型 | 后端处理 |
| --- | --- | --- | --- | --- |
| 排程任务完成上报 | 设备发布,后端订阅 | `/{deviceNo}/publish/finish/schedule` | JSON | 当前记录日志 |
| 设备注册 | 设备发布,后端订阅 | `/{mac}/publish/register` | JSON | 注册、标记上线并下发 deviceNo |
| 设备离线遗嘱 | 设备发布,后端订阅 | `/{deviceNo}/publish/status` | JSON | 收到 offline 后立即标记离线 |
| 电量及在线心跳 | 设备发布,后端订阅 | `/{deviceNo}/publish/power` | JSON | 更新设备电量并刷新 10 分钟在线心跳 |
| 命令应答 ACK | 设备发布,后端订阅 | `/{deviceNo}/publish/ack` | JSON 或纯文本 | 清理待确认命令 |
| 按键浇水完成上报 | 设备发布,后端订阅 | `/{deviceNo}/publish/finish/key` | JSON | 当前记录日志 |
| 硬件故障上报 | 设备发布,后端订阅 | `/{deviceNo}/publish/error` | JSON | 当前记录日志 |
| 下发命令 | 后端发布,设备订阅 | `/{deviceNo}/subscriber/cmd` | JSON | 设备执行命令 |
## 设备注册
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 设备注册 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{mac}/publish/register` |
| Payload 类型 | JSON |
后端以 Topic 中的设备标识作为解析入口payload 中的 `deviceNo` 即使传入也会被 Topic 覆盖。
示例:
```json
{
"deviceName": "一号浇水设备",
"powerLevel": "86",
"deviceEm": "WATER-EM-01",
"deviceSn": "SN202606100001",
"fwVer": "1.0.0",
"macAddress": "AA:BB:CC:DD:EE:FF"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| deviceName | string | 否 | 设备名称 |
| powerLevel | string | 否 | 电量 |
| deviceEm | string | 否 | 设备型号 |
| deviceSn | string | 否 | 设备序列号 |
| fwVer | string | 否 | 固件版本 |
| macAddress | string | 否 | MAC 地址 |
## 电量上报
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 电量上报 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/power` |
| Payload 类型 | JSON |
示例:
```json
{
"deviceName": "Waterer_01",
"powerLevel": "86",
"charging": 1
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| powerLevel | string | 是 | 电量值,后端当前按字符串保存 |
注册成功后后端会立即将设备标记为在线。之后收到包含 `powerLevel` 的有效消息,会把在线心跳有效期刷新为 600 秒。设备必须周期上报,即使电量没有变化也要发送,建议每 5 分钟一次;连续 10 分钟未收到电量或 ACK 刷新时将被判定为离线。ACK 仍保留现有的在线缓存刷新逻辑。
## 设备离线遗嘱
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 设备离线遗嘱 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/status`,注册前可使用 `/{mac}/publish/status` |
| Payload 类型 | JSON 或纯文本 |
推荐将以下消息配置为 MQTT LWT并设置 `retain=false`
```json
{
"status": "offline"
}
```
后端收到 `offline``0` 后立即标记设备离线。`online``1` 不会标记在线,设备上线通过注册成功或电量心跳确认。
兼容设备可发送 `{"deviceMac":"AA:BB:CC:DD:EE:FF","offline":"true"}`,服务端会使用 `deviceMac` 解析设备并标记离线。
## 命令应答 ACK
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 命令应答 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/ack` |
| Payload 类型 | JSON兼容纯文本 |
推荐 JSON
```json
{
"commandId": "9f4f1f2e8c6a4e1f8b7f3a1d2c0b9e11",
"status": "1",
"message": "receive"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| commandId | string | 是 | 后端下发命令时携带的命令 ID用于清理待 ACK 命令 |
| status | string | 否 | 执行状态,建议 `1` 成功,`0` 失败 |
| message | string | 否 | 设备返回消息 |
兼容格式:
```text
receive
```
说明:如果设备只返回纯文本,例如 `receive`,后端会查找该设备当前待确认命令。若只有一条 pending 命令,则按该命令完成 ACK若没有或有多条 pending 命令,只记录日志,不清理命令。嵌入式侧建议优先使用 JSON ACK并回传 `commandId`
## 下发命令
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 下发命令 |
| 通信方向 | 后端发布,设备订阅 |
| Topic | `/{deviceNo}/subscriber/cmd` |
| Payload 类型 | JSON |
后端下发手动开关设备命令示例:
```json
{
"deviceNo": "01",
"workStatus": "1",
"startTime": "2026-06-10 14:35:31",
"durationMin": 20,
"commandId": "9f4f1f2e8c6a4e1f8b7f3a1d2c0b9e11",
"commandType": "switchDevice"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| deviceNo | string | 是 | 设备编号 |
| workStatus | string | 是 | `0` 关闭浇水,`1` 开始浇水 |
| startTime | string | 否 | 开始时间,格式 `yyyy-MM-dd HH:mm:ss` |
| durationMin | number | 是 | 持续时间,单位分钟,必须大于 0 |
| commandId | string | 是 | 后端自动生成,设备 ACK 必须原样返回 |
| commandType | string | 是 | 命令类型,手动开关设备为 `switchDevice` |
设备收到命令后,应发布 ACK 到:
```text
/{deviceNo}/publish/ack
```
推荐 ACK
```json
{
"commandId": "9f4f1f2e8c6a4e1f8b7f3a1d2c0b9e11",
"status": "1",
"message": "receive"
}
```
## 设备编号下发
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 设备编号下发 |
| 通信方向 | 后端发布,设备订阅 |
| Topic | `/{deviceMac}/subscriber/cmd` |
| Payload 类型 | JSON |
说明:这是首次注册后的唯一例外,后端按设备 MAC 回传设备编号;其余下行命令都使用 `deviceNo` 作为 Topic 第一段。
## 排程任务完成上报
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 排程任务完成上报 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/finish/schedule` |
| Payload 类型 | JSON |
| 当前后端行为 | 写入浇水记录 |
JSON 示例:
```json
{
"deviceNo": "dcda0cfa289c",
"startWeek": 5,
"startTime": "2026-06-05 14:37",
"durationMin": 3,
"triggerType": "mqtt on"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| deviceNo | string | 否 | 设备编号;后端以 Topic 中的 `{deviceNo}` 为准,不一致时记录 warn |
| startWeek | number | 否 | 星期几,`1``7`;当前后端不入库 |
| startTime | string | 是 | 开始时间,支持 `yyyy-MM-dd HH:mm:ss``yyyy-MM-dd HH:mm` |
| endTime | string | 否 | 结束时间,支持 `yyyy-MM-dd HH:mm:ss``yyyy-MM-dd HH:mm` |
| durationMin | number | 否 | 预计持续时间,单位分钟;大于 `0` 时后端按 `startTime + durationMin` 计算预计结束时间 |
| triggerType | string | 否 | 设备触发来源;当前后端忽略该字段,排程完成记录固定入库为 `0` |
| scheduleId | number | 否 | 后端忽略 payload 中的该字段,按 `AppSchedulingDevice.deviceNo` 查询排程 ID查不到时写入 `0` |
后端先计算预计结束时间:`durationMin > 0` 时使用 `startTime + durationMin`,否则使用 payload 中的 `endTime`。实际入库的 `endTime` 取“消息接收时间”和“预计结束时间”中较早的时间,`durationMin``startTime` 到实际入库 `endTime` 重新计算。
## 按键浇水完成上报
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 按键浇水完成上报 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/finish/key` |
| Payload 类型 | JSON |
| 当前后端行为 | 写入浇水记录 |
JSON 示例:
```json
{
"deviceNo": "dcda0cfa289c",
"startWeek": 5,
"startTime": "2026-06-05 17:43",
"durationMin": 0,
"triggerType": "key"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| deviceNo | string | 否 | 设备编号;后端以 Topic 中的 `{deviceNo}` 为准,不一致时记录 warn |
| startWeek | number | 否 | 星期几,`1``7`;当前后端不入库 |
| startTime | string | 是 | 开始时间,支持 `yyyy-MM-dd HH:mm:ss``yyyy-MM-dd HH:mm` |
| endTime | string | 否 | 结束时间,支持 `yyyy-MM-dd HH:mm:ss``yyyy-MM-dd HH:mm` |
| durationMin | number | 否 | 预计持续时间,单位分钟;大于 `0` 时后端按 `startTime + durationMin` 计算预计结束时间 |
| triggerType | string | 否 | 设备触发来源;当前后端忽略该字段,按键完成记录固定入库为 `1` |
后端先计算预计结束时间:`durationMin > 0` 时使用 `startTime + durationMin`,否则使用 payload 中的 `endTime`。实际入库的 `endTime` 取“消息接收时间”和“预计结束时间”中较早的时间,`durationMin``startTime` 到实际入库 `endTime` 重新计算。按键完成记录的 `scheduleId` 固定为 `0`
## 硬件故障上报
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 硬件故障上报 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/error` |
| Payload 类型 | JSON |
| 当前后端行为 | 记录日志,暂未入库 |
建议 JSON
```json
{
"errorCode": "E001",
"errorType": "pump",
"message": "pump blocked",
"level": "error",
"time": "2026-06-10 10:00:00"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| errorCode | string | 建议 | 故障编码 |
| errorType | string | 否 | 故障类型 |
| message | string | 建议 | 故障描述 |
| level | string | 否 | 故障级别,例如 `warn``error` |
| time | string | 否 | 故障发生时间 |
## 嵌入式侧实现要点
1. 设备启动后使用 MAC 发布注册消息到 `/{mac}/publish/register`,取得服务端分配的 deviceNo。
2. MQTT 连接时配置离线遗嘱 `/{deviceNo}/publish/status`payload 为 `{"status":"offline"}``retain=false`
3. 设备至少每 5 分钟发布一次电量到 `/{deviceNo}/publish/power`,电量未变化也要上报。
4. 设备订阅自己的命令 Topic`/{deviceNo}/subscriber/cmd``/{deviceNo}/subscriber/schedule`
5. 设备收到命令后立即返回 ACK 到 `/{deviceNo}/publish/ack`,推荐返回 JSON 并携带 `commandId`
6. 排程或按键浇水完成后,分别发布到 `/{deviceNo}/publish/finish/schedule``/{deviceNo}/publish/finish/key`
7. 硬件异常时发布到 `/{deviceNo}/publish/error`