物联网的母语:MQTT 桥接声光告警终端的接入设计

说明:温度传感器、水浸绳、门磁、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 到这里

三条纪律:

  1. 点位代号与专栏分布式篇的编号规范统一site/S03),桥接器按主题就能路由到通知组,不用拆 payload 再查表;
  2. 遥测与事件分主题 :温度每 5 分钟上报走 state,越限瞬时告警走 alarm------桥接器只订阅 +/+/+/alarm+/+/+/status遥测洪流根本不进告警通道
  3. 通配符订阅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 秒------太短电池心疼,太长离线发现慢。


五、常见坑

  1. 订阅 # 全部主题 → 遥测洪流灌进桥接器,告警被淹没在数据里,只订 alarmstatus
  2. QoS 0 发告警 → 尽最大努力等于可以不努力,告警主题至少 QoS 1;
  3. retained 的旧告警alarm 主题误用 retained,桥接器重启收三个月前的"旧遗嘱",retained 只用于 status
  4. 共享订阅没配 → 多个桥接器实例各自播一遍,用 Broker 的共享订阅($share/bridge/site/+/sensor/+/alarm)或实例间选主;
  5. 遗嘱主题写错层级 → 桥接器订的是 status,遗嘱发到了 state,离线永远无声;
  6. 明文 1883 出管理网 → MQTT 与管理面同罪(专栏安全篇),跨网段上 8883/TLS 或至少 ACL 限制;
  7. 桥接器单点 → 它挂了全部传感器失聪,按专栏可用性篇配看门狗 + 双实例共享订阅;
  8. payload 格式自由生长 → 每种传感器一个格式,桥接器变成格式博物馆,定义最小 JSON schema 并在新设备接入时适配。

六、验收清单

  • 主题树按三级规范落地,新增传感器零桥接配置;
  • 告警主题 QoS ≥ 1,冷却窗生效(同源 60 秒仅一条);
  • 拔掉传感器电池:遗嘱 offline → 终端 critical 播报,时间 ≤ keepalive + 2 秒;
  • Broker 重启后桥接器自动重连,retained 状态恢复正确;
  • 遥测主题不进告警链路(压测 state 主题无告警误触发);
  • 桥接器进程有守护与看门狗,双实例共享订阅验证只播一份。

七、小结

MQTT 桥接是协议矩阵的最后一块主要拼图:传感器的母语、遗嘱免费的看门狗、主题树即路由 。至此告警源谱系完整------IT 说 HTTP、产线说 Modbus、网管说 SNMP、安防说 ISAPI、旧设备说邮件、改不动的说原生 TCP、多点说说云中继,而物联网的一切,都说 MQTT

当传感器数量从十只涨到一千只,桥接器会先撑不住------那时你需要的不是更大的桥,而是一条总线。那是下一篇的主题。

相关推荐
DLYSB_15 小时前
告警总线:把 N 个告警源解耦成一条消息流的架构设计
报警灯
DLYSB_2 天前
喇叭往哪摆才喊得醒人:声光告警的现场声学工程
报警灯
DLYSB_4 天前
谁来监控监控本身:告警链路的可用性工程
报警灯
DLYSB_6 天前
Alertmanager 的告警怎么“喊“出来:Webhook 到声光语音终端的路由与抑制设计
报警灯
DLYSB_12 天前
混合云与工业边缘场景下“云原生告警与物理现场声光”的集成架构实践
云原生·架构·报警灯
DLYSB_16 天前
旧上位机不肯改怎么办:原生 TCP 字节帧接入声光语音终端的改造实录
网络·网络协议·tcp/ip·报警灯
DLYSB_17 天前
交换机不会 HTTP 怎么办:用 SNMP Get / Trap 把指标和中断打到博灵声光 TTS
数据库·报警灯
DLYSB_18 天前
监控回调字段对不上 send_msg?用博灵自定义 API 把 WebHook 映射成声光 TTS
http·报警灯
DLYSB_1 个月前
让 PLC“开口说话”:基于 Modbus TCP 的声光语音告警设计与 Python 调试
python·网络协议·tcp/ip·报警灯