Skip to content

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_plusWeb Bluetooth API
MQTT远场(云端)设备遥测、状态订阅、指令下发mqtt_clientWebSocket + 发布订阅
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 必须做的三件事 ​

  1. 自动重连:断线按 1s→2s→4s 退避重连,并重订阅所有 Topic;
  2. 遗嘱 + retain:设备掉线由遗嘱标记离线,避免"假在线";
  3. 心跳:定期发 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。


六、能源场景落地清单 ​

场景链路关键设计
逆变器遥测MQTTQoS1 + retain + 遗嘱离线
近场配网BLE扫描 + 排队写入 + 状态监听
电表抄读Modbus TCP寄存器映射表 + CRC
设备离线告警MQTT 遗嘱服务端检测 + 推送(37 篇)

动手练习 ​

  1. 实现一个 MqttDeviceChannel 骨架(连接、订阅、遗嘱、退避重连);
  2. 用抽象 DeviceChannel 把"离线判定"抽成统一接口,写 2 个实现(MQTT / Mock);
  3. 设计一份 BLE 配网流程的时序图,标出 3 个失败点。

自检清单 ​

  • [ ] 能说清 BLE / MQTT / Modbus 三条链路的选型边界
  • [ ] 会处理 BLE 的权限、连接状态、并发写入三个坑
  • [ ] 知道 MQTT 的 QoS / 遗嘱 / retain 各自解决什么问题
  • [ ] 能设计带退避重连的统一 DeviceChannel 抽象
  • [ ] 知道"离线判定"不能只靠 TCP 断开

常见误区 ​

  • "IoT 就一个长连接":近场 BLE、云端 MQTT、工业 Modbus 是三种完全不同的栈;
  • "断线重连就是 while 循环":没有退避和重订阅的重连等于炸弹;
  • "设备在线 = TCP 连着":要用心跳 + 遗嘱,TCP 断了不代表设备状态更新;
  • "协议解析写死在页面里":寄存器/字段映射必须可配置,否则换厂商就重写。

相关文档 ​