39 · 设备通信:IoT 协议(BLE / MQTT / Modbus)
一句话定位:能源 App 不只是"读接口",还要和真实设备说话。本篇讲三条主流链路:蓝牙 BLE(近场)、MQTT(云端遥测)、Modbus(工业设备),并给出"断线重连、离线判定、指令下发"的通用框架。
开篇速览
| 问题 | 答案 |
|---|---|
| 前端对应物 | WebSocket 长连接(MQTT 类似)、navigator.bluetooth(BLE 类似)、串口调试 |
| 核心概念 | BLE 的 Service/Characteristic;MQTT 的 Topic/QoS/遗嘱;Modbus 的功能码/寄存器 |
| 能源场景 | 逆变器/储能设备离线上报、远程参数下发、近场配网 |
| 一句话红线 | IoT 链路先解决"断线、乱序、离线判定",再谈业务功能;没做重连机制的功能上线必现偶发失败 |
代码来源说明:HaierEnergy 当前
lib/未接入 BLE/MQTT/Modbus 协议层(设备通信走 HTTP 接口)。 本篇为能源域通用的能力篇,落地时结合具体硬件协议栈选型。
一、三条链路的选型
| 链路 | 距离 | 场景 | Flutter 常用库 | 前端类比 |
|---|---|---|---|---|
| BLE | 近场(≤10m) | 配网、近场调试、小设备控制 | flutter_blue_plus | Web Bluetooth API |
| MQTT | 远场(云端) | 设备遥测、状态订阅、指令下发 | mqtt_client | WebSocket + 发布订阅 |
| Modbus | 现场总线/内网 | 逆变器、电表等工业设备 | 自研/串口封装 | 无直接类比,像按寄存器地址读内存 |
二、BLE:近场配网与调试
2.1 核心模型
设备
└─ Service(服务,如电量 0x180F)
└─ Characteristic(特征,读/写/通知)
└─ Descriptor(描述符,CCCD 开启通知)2.2 最小流程
dart
// 1. 扫描
final results = await FlutterBluePlus.startScan(timeout: const Duration(seconds: 10));
// 2. 连接 + 发现服务
final device = await FlutterBluePlus.connect(remoteId);
await device.discoverServices();
// 3. 写指令
final char = services.characteristics.firstWhere((c) => c.uuid == targetUuid);
await char.write(bytes, withoutResponse: false);
// 4. 订阅通知
await char.setNotifyValue(true);
char.onValueReceived.listen((value) {
// 解析设备上报数据
});2.3 BLE 三大坑
- Android 权限分裂:
BLUETOOTH_SCAN/BLUETOOTH_CONNECT(12+)与旧权限并存,且扫描要动态定位权限(见 15 篇); - 连接状态变化:设备走远、系统回收,必须监听
onConnectionStateChanged并自动重连(带退避); - 并发指令排队:BLE 底层是串行的,多条写入要排队,不能并发发。
三、MQTT:云端遥测与指令
3.1 为什么用 MQTT 而不是 HTTP 轮询
设备状态是事件流,HTTP 轮询既费电又延迟高。MQTT 基于发布/订阅,服务端一推客户端就收到:
| 维度 | HTTP 轮询 | MQTT |
|---|---|---|
| 实时性 | 秒级~分钟级 | 毫秒级 |
| 电量/流量 | 高 | 低(长连接 + 压缩) |
| 离线感知 | 难 | 遗嘱消息(LWT) |
3.2 关键概念
| 概念 | 含义 | 本项目对应 |
|---|---|---|
| Topic | 消息主题,如 plant/{id}/status | 订阅电站状态 |
| QoS 0/1/2 | 最多一次/至少一次/恰好一次 | 遥测用 0~1,指令用 1 |
| 遗嘱 LWT | 断线时服务端代发的消息 | 设备离线判定 |
| retain | 保留最后一条消息 | 新客户端上线立即拿到最新状态 |
dart
final client = MqttClient('broker.example.com', 'client_$deviceId');
client.connectionMessage = MqttConnectMessage()
.withClientIdentifier('client_$deviceId')
.withWillQos(MqttQos.atLeastOnce)
.startClean();
await client.connect();
client.subscribe('plant/1001/status', MqttQos.atLeastOnce);
client.updates.listen((event) {
// 收到遥测,走事件总线更新 UI
});3.3 必须做的三件事
- 自动重连:断线按 1s→2s→4s 退避重连,并重订阅所有 Topic;
- 遗嘱 + retain:设备掉线由遗嘱标记离线,避免"假在线";
- 心跳:定期发 ping,防止运营商/网关掐连接。
四、Modbus:工业设备协议
Modbus 是"按寄存器地址读写"的工业老协议,常见变体:
| 变体 | 载体 | 场景 |
|---|---|---|
| Modbus RTU | 串口(RS-485) | 近距离电表/PLC |
| Modbus TCP | 以太网 | 网关/集中器 |
dart
// 伪代码:读保持寄存器(功能码 0x03)
final request = <int>[deviceId, 0x03, highByte(addr), lowByte(addr),
highByte(count), lowByte(count), crcHi, crcLo];
final response = await serial.writeAndRead(request);
final value = (response[3] << 8) | response[4]; // 大端解析要点:CRC 校验、大小端、寄存器地址映射表要单独维护;不同厂商设备寄存器地址语义不同,协议解析层要可配置。
五、通用通信框架(三条链路共用)
不管 BLE / MQTT / Modbus,通信层都建议统一抽象:
dart
abstract class DeviceChannel {
Future<bool> connect();
Stream<DeviceMessage> get messages; // 上行业数据
Future<void> send(DeviceCommand cmd); // 下行业指令
void reconnect(); // 带退避的重连
bool get isOnline;
}业务层只依赖 DeviceChannel,协议实现可替换。离线判定统一由"心跳超时 + 遗嘱/连接状态"给出,UI 层用同一个 isOnline。
六、能源场景落地清单
| 场景 | 链路 | 关键设计 |
|---|---|---|
| 逆变器遥测 | MQTT | QoS1 + retain + 遗嘱离线 |
| 近场配网 | BLE | 扫描 + 排队写入 + 状态监听 |
| 电表抄读 | Modbus TCP | 寄存器映射表 + CRC |
| 设备离线告警 | MQTT 遗嘱 | 服务端检测 + 推送(37 篇) |
动手练习
- 实现一个
MqttDeviceChannel骨架(连接、订阅、遗嘱、退避重连); - 用抽象
DeviceChannel把"离线判定"抽成统一接口,写 2 个实现(MQTT / Mock); - 设计一份 BLE 配网流程的时序图,标出 3 个失败点。
自检清单
- [ ] 能说清 BLE / MQTT / Modbus 三条链路的选型边界
- [ ] 会处理 BLE 的权限、连接状态、并发写入三个坑
- [ ] 知道 MQTT 的 QoS / 遗嘱 / retain 各自解决什么问题
- [ ] 能设计带退避重连的统一
DeviceChannel抽象 - [ ] 知道"离线判定"不能只靠 TCP 断开
常见误区
- "IoT 就一个长连接":近场 BLE、云端 MQTT、工业 Modbus 是三种完全不同的栈;
- "断线重连就是 while 循环":没有退避和重订阅的重连等于炸弹;
- "设备在线 = TCP 连着":要用心跳 + 遗嘱,TCP 断了不代表设备状态更新;
- "协议解析写死在页面里":寄存器/字段映射必须可配置,否则换厂商就重写。
相关文档
- 00 · 学习路线图 —— 阅读顺序
- 15 · 权限与设备能力 —— BLE 的 Android 权限分裂
- 37 · 后台任务与定时执行 —— 离线告警走服务端推送
- 06 · Dart 异步并发与进阶语法 —— Stream 处理设备消息
- 20 · 异常捕获与错误监控 —— 通信异常的可观测