说明:温度传感器、水浸绳、门磁、LoRa 网关------物联网设备大多说 MQTT。终端本身不直接说 MQTT,本文设计一个桥接器:订阅 MQTT 主题流,转换为终端触发调用。涵盖主题树设计、遗嘱消息做离线告警、QoS 选择与 retained 陷阱。代码基于 paho-mqtt 可直接运行,不放站外链接。
一、传感器世界的入口:为什么是 MQTT
本系列的协议矩阵(A1)覆盖了 IT、产线、网管与安防,但漏了增长最快的一类告警源:电池供电的物联网传感器 ------温湿度、水浸、门磁、烟感、LoRa/NB-IoT 网关。它们几乎都说同一种母语:MQTT。
MQTT 的三件事恰好是为传感器场景设计的:
- 发布/订阅解耦:传感器只管把读数发到"主题",不关心谁在听------一百个传感器与一台桥接器互不依赖;
- 极低开销:最小报文 2 字节,一节电池说几年;
- 遗嘱消息(LWT) :设备连接时预登记一句"遗言",异常掉线时由 Broker 代为发布------天然解决"传感器死了没人知道"(专栏可用性篇的安静失效,在传感器侧的标准解)。
终端侧不直接支持 MQTT,所以架构是桥接器模式:MQTT Broker → 桥接器(订阅+规则)→ 终端 HTTP/Modbus。桥接器与专栏海康篇的 Flask 转发器是同一个物种,只是耳朵换成了 MQTT。
text
[温湿度/水浸/门磁]──MQTT──┐
[LoRa 网关]──────MQTT──┤→ [Broker] → [桥接器] → HTTP/Modbus → [声光终端]
[HA / 平台]──────MQTT──┘ (EMQX/Mosquitto) 订阅·分级·防抖
二、主题树设计:命名即路由
MQTT 没有表结构,主题树就是你的数据模型。设计错了后面全是补丁,推荐按"位置/设备/事件"三级组织:
text
site/<点位>/sensor/<设备ID>/state 遥测:温度、湿度(周期上报)
site/<点位>/sensor/<设备ID>/alarm 事件:越限、触发(瞬时发布)
site/<点位>/sensor/<设备ID>/status 在线状态:LWT 遗嘱发布 offline 到这里
三条纪律:
- 点位代号与专栏分布式篇的编号规范统一 (
site/S03),桥接器按主题就能路由到通知组,不用拆 payload 再查表; - 遥测与事件分主题 :温度每 5 分钟上报走
state,越限瞬时告警走alarm------桥接器只订阅+/+/+/alarm与+/+/+/status,遥测洪流根本不进告警通道; - 通配符订阅 :
site/+/sensor/+/alarm一条订阅吃下全部点位的告警,新增传感器零配置。
三、桥接器实现:订阅、映射、防抖
python
# mqtt_bridge.py ------ MQTT 桥接器:订阅告警与遗嘱,转换为终端调用
import json
import threading
import time
import paho.mqtt.client as mqtt
import requests
BROKER, PORT = "192.168.10.5", 1883
TERMINAL = "http://192.168.0.66/api/api/user_msg/iot-{level}"
COOLDOWN = 60 # 同源告警冷却窗(专栏收敛篇)
_last: dict[str, float] = {}
_lock = threading.Lock()
PLACES = {"S03": "三号店", "W01": "一号仓"} # 点位表(专栏文案篇:位置表驱动)
EVENTS = {"water": ("水浸触发", "critical"),
"door_hold": ("门磁长开", "warning"),
"temp_high": ("温度越限", "warning")}
def on_alarm(client, userdata, msg):
"""site/<点位>/sensor/<id>/alarm → payload 为事件代号"""
parts = msg.topic.split("/")
site, dev = parts[1], parts[3]
name, level = EVENTS.get(msg.payload.decode(), (msg.payload.decode(), "warning"))
key = f"{site}/{dev}/{name}"
with _lock:
if time.monotonic() - _last.get(key, 0) < COOLDOWN:
return # 冷却窗内丢弃
_last[key] = time.monotonic()
requests.post(TERMINAL.format(level=level), timeout=3,
json={"place": PLACES.get(site, site), "event": name, "dev": dev})
def on_status(client, userdata, msg):
"""遗嘱主题:offline 即传感器失联 ------ 安静失效的标准捕获"""
if msg.payload.decode() == "offline":
parts = msg.topic.split("/")
requests.post(TERMINAL.format(level="critical"), timeout=3,
json={"place": PLACES.get(parts[1], parts[1]),
"event": "传感器离线", "dev": parts[3]})
def main() -> None:
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
c.on_message = lambda cl, u, m: (on_alarm(cl, u, m) if m.topic.endswith("/alarm")
else on_status(cl, u, m))
c.connect(BROKER, PORT)
c.subscribe("site/+/sensor/+/alarm", qos=1)
c.subscribe("site/+/sensor/+/status", qos=1)
c.loop_forever()
if __name__ == "__main__":
main()
四个工程点:QoS 1 用于告警主题(至少一次,宁可重复合并也不能丢------丢失由冷却窗兜住重复);遗嘱与 retained 配合 (传感器上线时发布 online 并 retained,桥接器重启后立即知道全部设备状态);冷却窗在桥接器侧 (专栏收敛篇的状态机完整版可平移至此);点位表外置(专栏文案篇的位置表驱动)。
四、遗嘱消息:传感器侧的看门狗
专栏可用性篇用心跳看门狗解决了"终端哑了没人知道",MQTT 的遗嘱消息是同一思想在传感器侧的协议级免费实现:
python
# 传感器侧(以模拟客户端示意):连接时登记遗嘱
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
c.will_set("site/S03/sensor/th01/status", "offline",
qos=1, retain=True) # 异常掉线时 Broker 代发
c.connect(BROKER, PORT)
c.publish("site/S03/sensor/th01/status", "online", qos=1, retain=True)
c.loop_forever()
三种掉线的归宿对比:
| 掉线方式 | TCP FIN | 遗嘱是否发布 |
|---|---|---|
| 正常断开(publish offline 后断开) | 正常 | 否(自己已说再见) |
| 断电 / 死机 / 电池耗尽 | 无 | 是,遗嘱立即代发 |
| 网络分区(半开连接) | 无 | keepalive 超时后代发 |
注意半开连接依赖 keepalive:传感器 keepalive 建议 60~120 秒------太短电池心疼,太长离线发现慢。
五、常见坑
- 订阅
#全部主题 → 遥测洪流灌进桥接器,告警被淹没在数据里,只订alarm与status; - QoS 0 发告警 → 尽最大努力等于可以不努力,告警主题至少 QoS 1;
- retained 的旧告警 →
alarm主题误用 retained,桥接器重启收三个月前的"旧遗嘱",retained 只用于status; - 共享订阅没配 → 多个桥接器实例各自播一遍,用 Broker 的共享订阅(
$share/bridge/site/+/sensor/+/alarm)或实例间选主; - 遗嘱主题写错层级 → 桥接器订的是
status,遗嘱发到了state,离线永远无声; - 明文 1883 出管理网 → MQTT 与管理面同罪(专栏安全篇),跨网段上 8883/TLS 或至少 ACL 限制;
- 桥接器单点 → 它挂了全部传感器失聪,按专栏可用性篇配看门狗 + 双实例共享订阅;
- payload 格式自由生长 → 每种传感器一个格式,桥接器变成格式博物馆,定义最小 JSON schema 并在新设备接入时适配。
六、验收清单
- 主题树按三级规范落地,新增传感器零桥接配置;
- 告警主题 QoS ≥ 1,冷却窗生效(同源 60 秒仅一条);
- 拔掉传感器电池:遗嘱 offline → 终端 critical 播报,时间 ≤ keepalive + 2 秒;
- Broker 重启后桥接器自动重连,retained 状态恢复正确;
- 遥测主题不进告警链路(压测 state 主题无告警误触发);
- 桥接器进程有守护与看门狗,双实例共享订阅验证只播一份。
七、小结
MQTT 桥接是协议矩阵的最后一块主要拼图:传感器的母语、遗嘱免费的看门狗、主题树即路由 。至此告警源谱系完整------IT 说 HTTP、产线说 Modbus、网管说 SNMP、安防说 ISAPI、旧设备说邮件、改不动的说原生 TCP、多点说说云中继,而物联网的一切,都说 MQTT。
当传感器数量从十只涨到一千只,桥接器会先撑不住------那时你需要的不是更大的桥,而是一条总线。那是下一篇的主题。