mqtt对接嵌入式

This commit is contained in:
yuhaiming
2026-06-11 10:07:03 +08:00
parent 1ab8c28c36
commit c7f9df980a
56 changed files with 2698 additions and 905 deletions

View File

@@ -0,0 +1,306 @@
# MQTT 与嵌入式通信协议
本文档说明后端服务与嵌入式设备之间的 MQTT Topic、通信方向、Payload 类型和 JSON 数据格式。
当前配置位置:
- 后端订阅配置:`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 第一段 `{deviceNo}` |
| Topic 变量 | 文档中的 `{deviceNo}` 替换为真实设备编号,例如 `/01/publish/register` |
| JSON 编码 | UTF-8 |
| 时间格式 | `yyyy-MM-dd HH:mm:ss` |
后端会订阅以下 Topic
```yaml
- /+/publish/finish/schedule
- /+/publish/register
- /+/publish/power
- /+/publish/ack
- /+/publish/finish/key
- /+/publish/error
- /+/subscriber/cmd
```
## Topic 总览
| 功能名称 | 通信方向 | 订阅名称 / Topic | Payload 类型 | 后端处理 |
| --- | --- | --- | --- | --- |
| 排程任务完成上报 | 设备发布,后端订阅 | `/{deviceNo}/publish/finish/schedule` | JSON | 当前记录日志 |
| 设备注册 | 设备发布,后端订阅 | `/{deviceNo}/publish/register` | JSON | 注册或更新设备 |
| 电量上报 | 设备发布,后端订阅 | `/{deviceNo}/publish/power` | JSON | 更新设备电量 |
| 命令应答 ACK | 设备发布,后端订阅 | `/{deviceNo}/publish/ack` | JSON 或纯文本 | 清理待确认命令 |
| 按键浇水完成上报 | 设备发布,后端订阅 | `/{deviceNo}/publish/finish/key` | JSON | 当前记录日志 |
| 硬件故障上报 | 设备发布,后端订阅 | `/{deviceNo}/publish/error` | JSON | 当前记录日志 |
| 下发命令 | 后端发布,设备订阅 | `/{deviceNo}/subscriber/cmd` | JSON | 设备执行命令 |
## 设备注册
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 设备注册 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/register` |
| Payload 类型 | JSON |
后端以 Topic 中的 `{deviceNo}` 作为设备编号payload 中的 `deviceNo` 即使传入也会被 Topic 覆盖。
示例:
```json
{
"deviceName": "一号浇水设备",
"status": "1",
"workStatus": "0",
"powerLevel": "86",
"wifiName": "office-wifi",
"deviceEm": "WATER-EM-01",
"deviceSn": "SN202606100001",
"fwVer": "1.0.0",
"macAddress": "AA:BB:CC:DD:EE:FF",
"bindToken": "123456"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| deviceName | string | 否 | 设备名称 |
| status | string | 否 | 设备状态:`1` 在线,`0` 离线,`2` 到期,`3` 故障;为空时后端默认 `2` |
| workStatus | string | 否 | 工作状态:`0` 休息,`1` 工作 |
| powerLevel | string | 否 | 电量 |
| wifiName | string | 否 | WiFi 名称 |
| deviceEm | string | 否 | 设备型号 |
| deviceSn | string | 否 | 设备序列号 |
| fwVer | string | 否 | 固件版本 |
| macAddress | string | 否 | MAC 地址 |
| bindToken | string | 否 | 设备绑定令牌 |
## 电量上报
| 项目 | 内容 |
| --- | --- |
| 功能名称 | 电量上报 |
| 通信方向 | 设备发布,后端订阅 |
| Topic | `/{deviceNo}/publish/power` |
| Payload 类型 | JSON |
示例:
```json
{
"powerLevel": "86"
}
```
字段说明:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| powerLevel | string | 是 | 电量值,后端当前按字符串保存 |
## 命令应答 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 | `/{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. 设备启动后发布注册消息到 `/{deviceNo}/publish/register`
2. 设备定时或电量变化时发布电量到 `/{deviceNo}/publish/power`
3. 设备订阅自己的命令 Topic`/{deviceNo}/subscriber/cmd`
4. 设备收到命令后立即返回 ACK 到 `/{deviceNo}/publish/ack`,推荐返回 JSON 并携带 `commandId`
5. 排程或按键浇水完成后,分别发布到 `/{deviceNo}/publish/finish/schedule``/{deviceNo}/publish/finish/key`
6. 硬件异常时发布到 `/{deviceNo}/publish/error`

View File

@@ -0,0 +1,199 @@
# Water 项目架构图
本文档按当前项目代码结构整理重点包含后端模块、智能灌溉业务、MQTT 设备通信、ACK 应答与重发链路。
## 整体模块
```mermaid
flowchart TB
APP["移动端 / 管理端 App"]
DEVICE["灌溉设备"]
BROKER["MQTT Broker<br/>TLS: ssl://service.reinkun.com:8883"]
subgraph BACKEND["water 后端服务"]
ADMIN["water-admin<br/>Spring Boot 启动入口"]
APPMOD["water-modules/water-app<br/>设备、排程、浇水记录、MQTT业务分发"]
SYSTEM["water-modules/water-system<br/>用户、角色、租户、权限"]
COMMON["water-common<br/>通用能力"]
MQTT["water-common-mqtt<br/>MQTT连接、TLS、订阅、发布、ACK重发"]
REDISCOMMON["water-common-redis<br/>Redis 工具与 Redisson"]
DBACCESS["water-common-mybatis<br/>MyBatis Plus / 数据权限"]
end
MYSQL[("MySQL<br/>业务数据")]
REDIS[("Redis<br/>缓存、设备状态、待ACK命令")]
APP -->|"HTTP REST"| ADMIN
ADMIN --> APPMOD
ADMIN --> SYSTEM
ADMIN --> COMMON
APPMOD --> DBACCESS
SYSTEM --> DBACCESS
DBACCESS --> MYSQL
APPMOD --> REDISCOMMON
MQTT --> REDISCOMMON
REDISCOMMON --> REDIS
MQTT <-->|"MQTT over TLS"| BROKER
DEVICE <-->|"MQTT over TLS"| BROKER
MQTT --> APPMOD
```
## 后端分层
```mermaid
flowchart LR
HTTP["HTTP 请求<br/>/app/v1/**"]
CONTROLLER["AppController<br/>设备、排程、日志、用户接口"]
SERVICE["Service 层<br/>IAppDeviceService / IAppScheduleService / IAppWateringLogService"]
MAPPER["Mapper 层<br/>MyBatis Plus Mapper"]
DB[("MySQL")]
HTTP --> CONTROLLER
CONTROLLER --> SERVICE
SERVICE --> MAPPER
MAPPER --> DB
CONTROLLER --> CMDPUB["IDeviceCommandPublisher<br/>设备命令发布接口"]
CMDPUB --> MQTTIMPL["DeviceMqttCommandPublisher<br/>MQTT命令实现"]
```
## MQTT 高并发消费链路
```mermaid
flowchart TB
DEVICE1["设备 A"]
DEVICE2["设备 B"]
DEVICEN["设备 N"]
BROKER["MQTT Broker"]
CLIENT["MqttAsyncClient<br/>单个后端订阅客户端"]
QUEUE["有界内存队列<br/>queue-capacity: 20000"]
CONSUMERS["mqtt-consumer-*<br/>consumer-count: 16<br/>batch-size: 100"]
DISPATCHER["MqttMessageDispatcher<br/>topic 路由"]
DATA["DeviceDataHandler<br/>/water/{deviceNo}/data"]
STATUS["DeviceStatusHandler<br/>/water/{deviceNo}/status"]
ERROR["ErromesHandler<br/>/water/{deviceNo}/erromes"]
ACK["IDeviceCommandAckHandler<br/>/water/{deviceNo}/ack"]
DEVICE1 --> BROKER
DEVICE2 --> BROKER
DEVICEN --> BROKER
BROKER --> CLIENT
CLIENT -->|"快速入队"| QUEUE
QUEUE -->|"批量拉取"| CONSUMERS
CONSUMERS --> DISPATCHER
DISPATCHER --> DATA
DISPATCHER --> STATUS
DISPATCHER --> ERROR
DISPATCHER --> ACK
```
## 设备命令 ACK 与重发
```mermaid
sequenceDiagram
participant App as 移动端 App
participant API as AppController
participant Pub as DeviceMqttCommandPublisher
participant Redis as Redis
participant MQTT as MqttClientManager
participant Broker as MQTT Broker
participant Dev as 设备
participant Ack as MqttCommandAckService
participant Retry as MqttCommandRetryTask
App->>API: switchDevice(workStatus, durationMin)
API->>Pub: send(DeviceCommand)
Pub->>Redis: 保存 pending commandId
Pub->>MQTT: publish /water/{deviceNo}/command
MQTT->>Broker: 下发命令
Broker->>Dev: 命令到达设备
alt 设备正常回复
Dev->>Broker: publish /water/{deviceNo}/ack
Broker->>MQTT: ACK 消息
MQTT->>Ack: handleAck(deviceNo, payload)
Ack->>Redis: 删除 pending保存 ack 结果
else 设备断网或未回复
Retry->>Redis: 扫描 pending 命令
Retry->>Redis: 检查设备状态缓存
alt 设备在线且未超过重试次数
Retry->>MQTT: 重新 publish 命令
MQTT->>Broker: 重新下发
else 设备离线
Retry->>Redis: 延后 nextRetryAt
else 超过最大重试次数
Retry->>Redis: 删除 pending
end
end
```
## Redis Key 规划
```mermaid
flowchart TB
REDIS[("Redis")]
STATUS["mqtt:device:status:{deviceNo}<br/>设备最新在线状态<br/>TTL: 300s"]
PENDING["mqtt:command:pending:{commandId}<br/>待ACK命令详情<br/>TTL: 86400s"]
PENDING_IDS["mqtt:command:pending:ids<br/>待ACK commandId 集合"]
ACK["mqtt:command:ack:{commandId}<br/>设备ACK结果<br/>TTL: 86400s"]
REDIS --> STATUS
REDIS --> PENDING
REDIS --> PENDING_IDS
REDIS --> ACK
```
## 核心 Topic 约定
```mermaid
flowchart LR
DEVICE["设备"]
SERVER["后端"]
DEVICE -->|"/water/{deviceNo}/data"| SERVER
DEVICE -->|"/water/{deviceNo}/status"| SERVER
DEVICE -->|"/water/{deviceNo}/erromes"| SERVER
DEVICE -->|"/water/{deviceNo}/ack"| SERVER
SERVER -->|"/water/{deviceNo}/command"| DEVICE
```
设备 ACK 示例:
```json
{
"commandId": "后端下发的commandId",
"status": "success",
"message": "ok"
}
```
## 高并发关注点
```mermaid
flowchart TB
A["上千设备并发连接"] --> B["MQTT Broker 承载连接数和 TLS 握手"]
B --> C["后端单/少量订阅客户端消费通配 topic"]
C --> D["有界队列吸收突发流量"]
D --> E["批量消费降低线程调度成本"]
E --> F["状态写 Redis避免心跳打数据库"]
F --> G["数据/告警后续建议批量落库"]
```
当前已完成的关键优化:
- MQTT 使用 `MqttAsyncClient`
- MQTT 连接支持 TLS
- MQTT 回调只入队,不直接执行业务
- 消息按批次消费
- 设备状态写 Redis 缓存
- 设备命令支持 ACK
- 未 ACK 命令支持离线延后和超时重发
- 日志异步队列已扩大
后续建议:
- 设备数据和告警数据落库改为批量写入
- 对设备命令增加业务状态表,便于前端查询命令执行状态
- MQTT Broker 使用集群或至少做连接数、会话数、消息速率监控
- 生产环境 TLS 不建议开启 `skip-verify`