MQTT 驱动功能设计
通用 MQTT 网关接入 · 发布订阅模型 · 网关自动发现与批量导入
MQTT 驱动是 EasyPro 平台用于接入 MQTT 协议网关的驱动。网关下挂在各类传感器、执行器,通过 MQTT 协议将采集数据上报到平台的 Broker,平台订阅相关 Topic 接收数据、发布 Topic 下发控制指令。
与 Modbus 的「主站主动轮询」截然不同,MQTT 是事件驱动模型——设备有数据变化时主动推送,平台被动接收,实时性高、网络开销小。
| 属性 | 值 | 说明 |
|---|---|---|
| 驱动名称 | mqtt | SPI 注册名 |
| 版本 | V1.0.0 | getDriverVersion() |
| 接入模式 | EMBEDDED | 内嵌于平台主应用 |
| 协议库 | Eclipse Paho MQTT v3 | Java 标准 MQTT 客户端 |
| 通信模型 | 发布/订阅(pub/sub) | 经 Broker 中转 |
| Web 接口 | 3 个 | 查网关 / 绑网关 / ZIP 导入 |
| 定时任务 | 1 个 | 每 10 秒检测实例存活 |
| 实例属性校验 | 无(空实现) | 配置由网关上报,非用户填写 |
MQTT 通信不直接发生在平台与设备之间,而是经 Broker(消息代理)中转。发布者发消息到某个 Topic,Broker 分发给所有订阅了该 Topic 的接收者。
(EMQX)"] P[" EasyPro 平台
MQTT 驱动"] G1 -->|"发布 /data/up/..."| B G2 -->|"发布 /data/up/..."| B B -->|"推送订阅消息"| P P -.->|"发布 /set/down (控制)"| B B -.->|"转发"| G1 style B fill:#e8f0fe,stroke:#007aff,stroke-width:2px,color:#1c1c1e style P fill:#f0f0f5,stroke:#5856d6,stroke-width:2px,color:#1c1c1e style G1 fill:#e8f8f0,stroke:#34c759,color:#1c1c1e style G2 fill:#e8f8f0,stroke:#34c759,color:#1c1c1e
MQTT 消息可指定 QoS(Quality of Service)等级,决定投递可靠性。本驱动对不同用途的消息采用不同 QoS:
| QoS | 保证 | 本驱动用途 |
|---|---|---|
0 至多一次 | 不保证送达,开销最小 | 心跳回复(高频,减负载) |
1 至少一次 | 保证送达,可能重复 | 下行控制(setDown)(必须送达) |
心跳回复用 QoS 0 是有意的——心跳消息量大且即使丢失下一次也会补上,用 QoS 0 可显著降低 Broker 与平台负载。
| Topic | 用途 |
|---|---|
/create/up/+ | 网关上线创建请求(首次接入) |
/ins/up/open/+ | 网关上报实例信息(设备类型/设备/点位) |
/heartbeat/up/+ | 网关心跳 |
/device/status/up/+ | 设备状态变更 |
/data/up/+ | 实时点位数据(核心) |
/history/data/up/+ | 历史数据补传 |
/reply/identifier/device/set/down | 设备对下行控制的回复 |
+ 是 MQTT 单层通配符,匹配任意网关 ID。
| Topic 模板 | 用途 | QoS |
|---|---|---|
/{0}/reply/create/up | 回复网关创建(建立连接标志) | — |
/{0}/reply/heartbeat/up | 回复心跳 | 0 |
/{0}/reply/ins/up/open | 回复实例信息(初始化结束标志) | — |
/{0}/identifier/set/down | 下行点位控制 | 1 |
{0} 替换为具体网关 ID(gatewayId)。
驱动只维护一个共享的 Paho MQTT 客户端连接到 Broker,订阅全部 7 个 Topic。每条入站消息由 MqttMessagePipLine 按 Topic 分发给对应的 handler,每个 handler 在独立虚拟线程上处理,互不阻塞。
messageArrived"] CB -->|"每条消息起虚拟线程"| PL["MqttMessagePipLine"] PL --> H1["DataUpHandler
实时数据"] PL --> H2["HeartbeatHandler
心跳/配置同步"] PL --> H3["DeviceStatusHandler
设备状态"] PL --> H4["HistoryDataUpHandler
历史数据"] PL --> H5["InitGatewayHandler
网关创建"] PL --> H6["InstanceInfoHandler
实例信息"] PL --> H7["ReplyIdentifierSetDown
下行回复"] style B fill:#e8f0fe,stroke:#007aff,stroke-width:2px,color:#1c1c1e style PL fill:#f0f0f5,stroke:#5856d6,stroke-width:2px,color:#1c1c1e
| 参数 | 值 | 说明 |
|---|---|---|
| cleanSession | false | 持久会话,断线重连后接收离线消息 |
| connectionTimeout | 30 秒 | 连接超时 |
| automaticReconnect | true | 自动重连 |
| keepAliveInterval | 20 秒 | 心跳间隔 |
| maxInflight | 20 | 同时飞行消息数 |
| clientId | 配置值 + 随机后缀 | 避免多实例 clientId 冲突 |
connectionLost 回调对重连做了30 秒一次的限流,避免 Broker 故障时疯狂重连雪崩。
网关接入平台有两条路径,最终都汇聚到同一套实例创建逻辑:
/create/up"] --> A2["进入
noProjectGatewayCache"] A2 --> A3["用户调
chooseGateway"] end subgraph 离线["路径 B:离线 ZIP 导入"] B1["用户上传 ZIP
(JSON 配置)"] --> B2["调
importDriverInstance"] end A3 --> C["chooseNoProjectGateway
创建驱动实例"] B2 --> D["MqttImportUtil
9 阶段管线解析"] C --> E["网关上报 /ins/up/open
→ 物化设备类型/设备/点位"] D --> E style A1 fill:#e8f8f0,stroke:#34c759,color:#1c1c1e style B1 fill:#fff4e8,stroke:#ff9500,color:#1c1c1e style E fill:#e8f0fe,stroke:#007aff,stroke-width:2px,color:#1c1c1e
离线 ZIP 导入(MqttImportUtil.importMqttProjectByZipFile)是一个严谨的流水线,支持产品合并(同 productCode 合并为一个设备类型)与并发控制(Semaphore 10):
| 阶段 | 动作 |
|---|---|
| 1. 校验 | 检查 file、driverId 非空,并发导入互斥 |
| 2. 解压 | 解压 ZIP 到临时目录 |
| 3. 解析 | 逐个解析 JSON 配置文件 |
| 4. 点位容量 | 检查点位数量是否超授权 |
| 5. 结构校验 | 批量校验产品/设备/点位结构合法性 |
| 6. 合并冲突 | 检查产品合并冲突 |
| 7. 开关校验 | switch-on 检查 |
| 8. 异步创建 | 异步创建/更新设备类型、设备、点位 |
| 9. 返回结果 | 返回成功/失败/冲突计数 |
网关每次心跳会带上自己的 dataVersion(配置版本号)。平台据此判断网关配置是否与平台一致,并通过 drq(data request)标志位要求网关上报最新配置:
drq_0=无需上报、drq_1=请求上报、drq_2=继续上报中。握手同步在 InstanceStorage.instanceIdMap 上加锁,避免与实例删除竞态。
平台下发点位控制(issuePointValueBatch)时,按网关分组,构建 InstanceSetDown 载荷发布到 /{gwId}/identifier/set/down(QoS 1):
{ "mId": "消息ID", "gwId": "网关ID", "da": [ // 数据数组 { "productCode": "产品编码", "devices": [{ "deviceCode": "设备编码", "identifiers": { "identifierCode[]": "value[]" } }] } ], "ps": { "scattered": "散点信息" } }
值会按数据类型归一化:normalizeValueByDataType 将 bool 转 0/1,int/double/text 做类型强转。散点(uniqueCode 以 scattered 开头)有专门处理。
存在 iot_driver_config 表(非实例属性),由 MqttConfig 加载,定义平台如何连接 Broker:
| 配置项 | 说明 |
|---|---|
url | Broker 连接地址(如 tcp://host:1883) |
clientId | 客户端 ID 前缀(自动追加随机后缀) |
username / password | Broker 认证凭证 |
emqApiUrl | EMQX HTTP API 地址(查网关 IP) |
emqAppId / emqAppSecret | EMQX API 认证 |
secret / expires | EMQX Token 密钥与过期时间 |
completionTimeout | 操作完成超时 |
实例属性由网关上报自动填充,而非用户手工填写(故 validateDriverInstanceAttr 为空实现):
| 属性名 | 来源 | 说明 |
|---|---|---|
ip | EMQX HTTP API 解析 | 网关客户端 IP,重连时刷新 |
model | 网关上报/导入文件 | 网关型号 |
version | 网关上报/导入文件 | 固件版本 |
sn | 网关上报/导入文件 | 序列号 |
gatewayId | 网关上报/导入文件 | 网关 ID(= 实例名) |
name | 网关上报 | 网关显示名 |
passWord | 网关上报/导入文件 | 接入密码 |
dataVersion | 网关上报 | 配置版本(驱动 drq 握手) |
mergeProduct | 用户/导入选择 | "1" 合并同名产品 / "0" 每网关独立 |
MQTT 驱动没有固定的点位属性表——点位由网关上报的产品/设备/标识结构自动创建,配置即网关的物模型。
| 映射路径 | 用途 | 参数 / 上传 |
|---|---|---|
queryGateway | 查询已连接但未绑定项目的网关列表 | 无参数;返回 GatewayLinkInfo[](含 IP) |
chooseGateway | 将网关绑定到项目并创建实例 | gatewayId, password, projectId,可选 mergeProduct=true |
importDriverInstance | 从 ZIP 批量导入网关项目 | ZIP 文件上传 + driverId,可选 mergeProduct |
调用路径前缀:POST /driver/api/mqtt/{mapping}。
| 任务名 | Cron | 频率 | 职责 |
|---|---|---|---|
MqttInstanceAliveTask | 0/10 * * * * ? | 每 10 秒 | 检测全部实例状态:客户端掉线则重连;按 MqttCache.isOnline 设置 RUNNING/OFFLINE/CLOSED 并推送状态 |
/create/up 进入 noProjectGatewayCache。可用 queryGateway 接口确认。/ins/up/open 后由 InstanceInfoHandler 物化。若网关在线则会很快完成;离线导入需等对应网关上线后触发配置同步(drq 握手)。/{gwId}/identifier/set/down。若网关离线,消息会暂存 Broker(cleanSession=false),网关重连后补投。检查 InstanceSetDown 载荷中 productCode/deviceCode/identifier 是否与网关物模型一致。mergeProduct="1",相同 productCode 的产品会合并为一个设备类型,跨网关共享。设 "0" 则每个网关独立建设备类型(名称带 gatewayId 后缀)。