简介:面向工业自动化控制与物联网通信场景,这份资源以CODESYS平台为基座,提供MQTT客户端库与Zigbee2MQTT集成方案,核心解决PLC设备与MQTT代理服务器之间双向数据高效可靠传输的问题,并支持多代理连接与JSON数据交换。资源包共38个文件、约6.6MB,以project工程和library库文件为主,包含从1.1.0到1.2.0系列多个MQTT库版本,同时配置png截图、md/txt说明、xml配置及PDF文档,既便于版本对比,也能按图文步骤完成库添加、动态内存分配、首次订阅发布等关键操作。随包示例工程覆盖Windows与Raspberry环境、TLS加密与非加密、接口示例的主题与负载、高负载压力测试等场景,另有CFC优势示例与集成说明,可帮助读者快速搭建测试环境,深入理解CODESYS环境下MQTT客户端库的调用方式、Zigbee2MQTT的对接流程以及多代理连接配置。目前已有88人学习,适合PLC工程师、物联网集成人员及自动化项目开发者,作为工业物联网联调与二次开发的直接参考。
1. 这个 zip 包解决的事:让 CODESYS PLC 直接连 MQTT,省掉网关那一层
车间里一台跑着 CODESYS 的 PLC,数据要上云、要收 Zigbee 传感器的环境量,还要同时和两个 MQTT 服务器说话——这种需求最近越来越多。这套方案的核心,是把一个 MQTT 客户端库装进 CODESYS 运行时,让 PLC 自己就是 MQTT 客户端,再由 Zigbee2MQTT 做传感器侧的桥接网关,两边用 JSON 格式的 topic 报文完成双向数据交换。相比加一个 DTU 网关或者写 Modbus 转 MQTT 的中间层,这个方案少了一台设备、少了协议转换的延迟,也少了“黑匣子”一样的排错过程。适合正在做设备联网、边缘数据采集的自动化工程师,也适合想绕开额外硬件、直接把 PLC 接进物联网平台的现场人员。
2. 为什么是“客户端库 + Zigbee2MQTT”:选型逻辑与双向数据链路
2.1 CODESYS 上接 MQTT 的三种常用做法,为什么选客户端库
把 CODESYS PLC 的数据送进 MQTT 生态,常见做法大致有三条路。
第一条,用硬件网关。像 DTU、边缘网关这类盒子,PLC 侧走 Modbus RTU/TCP 把寄存器读出来,网关侧再转成 MQTT 发布。这套方案成熟稳定,现场也最常见,但代价是每个站点多一台设备,多一层要维护的配置。Modbus 轮询本身有周期,网关的采集频率和 PLC 内部逻辑刷新不同步,数据到了云端往往是“第二手”的。
第二条,用 CODESYS 平台自带的 IoT 功能块。CODESYS 3.5 往后的版本里能够添加物联网相关的库,里面带 MQTT 客户端、HTTP 客户端这类功能块。好处是官方维护、和运行时集成度高,坏处是库的版本跟着 IDE 走,一些老项目用的 3.5.15 甚至更早的版本,不一定具备完整的 MQTT 功能。
第三条,就是标题里的做法:在 CODESYS 里导入一个现成的 MQTT 客户端库,自己写逻辑去调。这个方案把 PLC 变成真正的 MQTT 客户端,发布、订阅、遗嘱、多代理连接都由库内部处理,工程师只关心业务数据怎么填进报文。国内不少基于 CODESYS 二次开发的品牌,比如汇川的 CODESYS 平台、InoProShop,也都能用这种方式接入,选型时不用被厂商绑定。
从落地角度看,客户端库最大的价值在于“少一跳”。PLC 的变量状态直接进 MQTT 报文,没有中间层转发,时延可控,掉线重连、QoS 这些可靠性问题也能在 PLC 程序里统一处理。至于为什么集成 Zigbee2MQTT,是因为现场大量环境量传感器(温湿度、门磁、光照)走 Zigbee 协议最省事,而 Zigbee2MQTT 把这一整片设备翻译成 MQTT 主题,PLC 只需按主题订阅,不需要理解 Zigbee 的组网细节。
2.2 Zigbee2MQTT 在链路里的位置:Zigbee 网络和 MQTT 之间的桥
Zigbee2MQTT(简称 Z2M)是一个软件服务,它本身不产生业务数据,只做协议翻译。物理连接通常是这样的:一个 USB 协调器(常见的是基于 CC2530、CC2652 这类芯片的 Zigbee 网卡)插在工控机或树莓派上,上面跑着 zigbee2mqtt 服务,服务再连接到局域网里的 MQTT broker。Zigbee 设备(传感器、开关、插座)加入协调器的网络后,上报的状态会被 Z2M 翻译成 JSON 报文,发布到固定的主题上。
这条链路里,MQTT broker 是所有人的“中间人”。Zigbee2MQTT 是 broker 的一个客户端,CODESYS PLC 是另一个客户端。PLC 订阅zigbee2mqtt/#主题,就能收到所有 Zigbee 传感器的数据;PLC 向zigbee2mqtt/设备名/set发布指令,就能控制 Zigbee 开关、调节器。CODESYS 压根不需要关心 Zigbee 协议本身,只关心 JSON 里字段叫什么。
这里要特别说一下主题命名规则。Z2M 的默认主题前缀是zigbee2mqtt,每个设备在配置里有一个 friendly_name,比如temp_sensor_01。设备上报的消息发布到zigbee2mqtt/temp_sensor_01,设备状态(在线离线)发布到zigbee2mqtt/temp_sensor_01/availability,下行控制发布到zigbee2mqtt/temp_sensor_01/set。所以 PLC 侧订阅zigbee2mqtt/+/state还是订阅具体的设备主题,取决于你要收全部还是一台。
2.3 双向数据传输的 topic 设计:PLC 发布什么、订阅什么
能把 MQTT 跑通不算本事,topic 设计不好,后期维护才是真要命。尤其是多代理连接时,本地和云端两套主题如果不做隔离,两边数据一混,查问题能查到怀疑人生。
我一般会按“数据方向 + 设备身份”设计主题,而不是把业务语义堆在 topic 里。给出一套可以直接抄的约定:
| 方向 | Topic | Payload 内容 | QoS |
|---|---|---|---|
| PLC → Broker | factory/plc/line01/telemetry | 周期遥测:温度、压力、运行状态 | 0 |
| PLC → Broker | factory/plc/line01/event | 事件报警:故障码、停机原因 | 1 |
| PLC → Broker | factory/plc/line01/ack | 对云端指令的应答 | 1 |
| Broker → PLC | factory/plc/line01/cmd | 云端下发的控制指令 | 1 |
| Broker → PLC | zigbee2mqtt/+/state | Zigbee 传感器状态 | 0 |
遥测用 QoS 0 是合理的:周期数据丢了下一轮还有;事件和指令用 QoS 1,保证至少到达一次,这是“mqtt 怎么保证不丢失消息至少一次”的标准答案——QoS 1 就是 at least once。PLC 侧收到 QoS 1 可能重复,所以处理逻辑要幂等,这一点第 5 章会展开讲。
双向数据传输的落点在于:PLC 既作为数据源向外发,又作为消费者向内收。很多人只做了“PLC 发数据到 MQTT”这一半,忘了订阅指令通道。对于“双向数据传输”这个标题语义,遥测、事件、指令、应答四个通道缺一个都不完整。
3. 在 CODESYS 里把 MQTT 跑起来:导入库、多代理连接与发布订阅
3.1 导入库文件,先跑通一个最小连接
先说一句:不同 MQTT 库的 API 命名略有差异,但思路一致,下面代码以常见的 CODESYS MQTT 客户端库为例,方法名和结构体名以你手头库的说明书为准。
解压标题里的 zip 包,一般会得到库文件(.library后缀或工程格式的库工程)、示例工程和说明文档。在 CODESYS 3.5 的库管理器里,选择“安装库”,定位到.library文件安装,然后在工程里添加库引用,勾选 MQTT 客户端库和 JSON 处理库。这一步没做对,后面写代码时功能块会找不到。
先写一个最小连接程序。新建一个 POU,语言选 ST(结构化文本):
PROGRAM PRG_MQTT_Minimal VAR mqttClient : MQTT.Client; // 库提供的客户端实例 connParams : MQTT.ConnectionParams; // 连接参数结构体 bConnect : BOOL := FALSE; // 上升沿触发连接 bConnected : BOOL; // 连接状态反馈 eError : MQTT.ErrorCode; // 错误码 END_VAR// 连接参数赋值 connParams.Host := '192.168.1.50'; // broker 地址,别用域名先 connParams.Port := 1883; // 1883 明文,8883 需要 TLS connParams.ClientID := 'PLC_Line01'; // 同一 broker 下必须唯一 connParams.KeepAlive := 30; // 秒,低于 broker 的 keepalive 上限 connParams.CleanSession:= FALSE; // 离线期间保留订阅,重连不丢 connParams.Username := 'plc_user'; // 按需 connParams.Password := ''; // 建议用 broker 侧账号管理 // 调用连接方法,内部会完成 TCP 连接、MQTT 握手 mqttClient.Connect(connParams); bConnected := mqttClient.IsConnected(); eError := mqttClient.LastError();这段代码里两个参数最容易被忽略。一个是ClientID,同一 broker 下如果有两个客户端用了同一个 ID,后连接的那个会把先连接的踢下线,现场表现为 PLC 周期性掉线,这个问题在 5.5 节细说。另一个是CleanSession := FALSE,意思是 broker 为这个客户端保留离线期间的 QoS 1 消息和订阅关系,网络抖动重连后不会丢指令。代价是 broker 要维护会话状态,连接数多时占内存。
首次验证不要接复杂逻辑,直接把这个 POU 下载到 PLC,在线监视bConnected是否为 TRUE。如果连不上,优先查 broker 地址能不能 ping 通、1883 端口是否被防火墙拦了,以及 CODESYS 运行时的网络权限。这一步是后面所有工作的地基,地基不稳,后面全是玄学。
3.2 多代理连接怎么配:两个 broker、两套参数、两个 client ID
标题里明确写了“支持多代理连接”,这是这个库区别于玩具级方案的实质性功能。典型场景是:本地一个 broker 负责给 MES 系统供数,云端一个 broker 负责给远程运维平台供数,两边的数据内容有重叠也各有侧重——本地发全部遥测,云端只发设备和报警信息。
多代理连接在程序上的写法,就是创建多个MQTT.Client实例,每个实例一套独立的连接参数。实例之间互不影响,一个断了不能拖累另一个。
PROGRAM PRG_MQTT_MultiBroker VAR mqttLocal : MQTT.Client; // 本地 MES broker mqttCloud : MQTT.Client; // 云端远程运维 broker connLocal : MQTT.ConnectionParams; connCloud : MQTT.ConnectionParams; bLocOn : BOOL; bCldOn : BOOL; END_VAR// 本地 broker:IP 直连,低延迟 connLocal.Host := '192.168.1.50'; connLocal.Port := 1883; connLocal.ClientID := 'PLC_Line01_Local'; connLocal.CleanSession := TRUE; // 本地数据丢了不可惜 // 云端 broker:走域名 + TLS,可靠性优先 connCloud.Host := 'iot.xxxx.com'; connCloud.Port := 8883; connCloud.ClientID := 'PLC_Line01_Cloud'; connCloud.CleanSession := FALSE; connCloud.Username := 'line01'; connCloud.Password := '************'; // 分别建立连接,互不干扰 mqttLocal.Connect(connLocal); mqttCloud.Connect(connCloud); bLocOn := mqttLocal.IsConnected(); bCldOn := mqttCloud.IsConnected();这里有个经验:同一 PLC 连不同 broker,ClientID一定要带不同的后缀。曾经有同行两个连接都用了PLC_Line01,结果连云端时把本地连接顶掉了,MES 那边数据断断续续,排查了一下午。另外,云端 broker 走域名时,PLC 需要能解析 DNS,很多工业现场把 DNS 关了,导致域名连不上,IP 却正常,这一点排错时优先怀疑。
多代理的“断线重连”策略也要分开设。本地 broker 离得近,网络稳定,重连间隔可以短一些;云端链路经过公网,抖动大,重连间隔要拉长,避免频繁握手加重 broker 负担。很多库支持ReconnectInterval之类的参数,按 5 秒和 30 秒两个档分别设置比较合理。
3.3 发布与订阅消息:JSON payload 从哪来、收到后去哪
连接跑通后,进入业务正题:发布和订阅。先说发布,发布一个遥测报文,payload 用 JSON 字符串。CODESYS 库通常提供Publish方法,入参是 topic、payload、QoS、retain 标志。
PROGRAM PRG_MQTT_Publish VAR mqttClient : MQTT.Client; sTopic : STRING(80); sPayload : STRING(255); bPubDone : BOOL; fTemp : REAL := 23.5; nPressure : INT := 4120; sJsonBuffer : STRING(255); END_VAR// 拼 JSON:注意浮点数格式化,别让 PLC 输出 23.5000001 sTopic := 'factory/plc/line01/telemetry'; sPayload := CONCAT('{"temp":', REAL_TO_STRING(fTemp)); sPayload := CONCAT(sPayload, ','); sPayload := CONCAT(sPayload, '"pressure":'); sPayload := CONCAT(sPayload, INT_TO_STRING(nPressure)); sPayload := CONCAT(sPayload, ',"ts":'); sPayload := CONCAT(sPayload, UDINT_TO_STRING(GetTickCount())); // 用运行时长做时间戳 sPayload := CONCAT(sPayload, '}'); // QoS 0 周期遥测,retain 按 broker 需求 bPubDone := mqttClient.Publish(sTopic, sPayload, 0, FALSE);这段代码展示了两个坑。第一,CODESYS 的STRING默认长度是 255,拼接 JSON 时超过 255 会被截断,截断后的 JSON 必然解析失败。第二,REAL_TO_STRING转换浮点数时可能带出多余精度,应对办法是放大整数发送,比如温度乘以 100 变成整数 2350,接收侧再除 100,云端好处理,也绕开了浮点数格式化问题。
订阅侧的关键有两点:订阅什么主题,以及收到消息后怎么回调处理。CODESYS 的客户端库一般有两种模型:一种是周期轮询接收缓冲区,另一种是注册回调功能块。轮询模型写起来简单,适合收 Zigbee 传感器这类型低频数据:
PROGRAM PRG_MQTT_Subscribe VAR mqttClient : MQTT.Client; rxMsg : MQTT.ReceivedMessage; bGotMsg : BOOL; sTopic : STRING(80); sPayload : STRING(255); END_VAR// 启动时订阅一次,Zigbee2MQTT 所有传感器状态 IF NOT bSubscribed THEN mqttClient.Subscribe('zigbee2mqtt/+/state', 0); bSubscribed := TRUE; END_IF // 每次扫描周期检查是否有新消息 bGotMsg := mqttClient.GetMessage(rxMsg); IF bGotMsg THEN sTopic := rxMsg.Topic; sPayload := rxMsg.Payload; // 在这里做 JSON 解析,提取字段存入 PLC 变量 END_IF订阅的通配符zigbee2mqtt/+/state中,+匹配任意一级,也就是任意设备名。这样做的好处是新增 Zigbee 传感器时 PLC 程序不用改,坏处是收到报文后要自己根据sTopic区分是哪台设备。区分逻辑可以用FIND、MID这类字符串函数解析设备名,或者干脆在 JSON payload 里带上设备名,让 topic 只做路由、payload 做业务解析。
4. JSON 数据在 PLC 侧的生成与解析:不让字符串格式成为瓶颈
4.1 发布侧:拼 JSON 字符串的格式化与转义问题
上一章的发布示例已经展示了拼接 JSON 的基本方式,这里把话说透。CODESYS 的字符串处理能力远不如高级语言,没有sprintf、没有模板字符串,常见的做法就是CONCAT和类型转换函数硬拼。拼多了会发现三件事必须提前约定。
第一,字符串里的双引号。JSON 的 key 必须带双引号,但 CODESYS 的字符串本身也用双引号包围,所以要在拼接串里写'"key",左半部分是单引号包双引号,这是初学者最容易翻车的地方。第二,中文内容要确认编码。CODESYS 字符串内部是 UTF-8 还是 GBK 跟 IDE 区域设置有关,如果云端解析出乱码,先查这一层,别急着怀疑库。第三,特殊字符转义。内容里有换行、反斜杠或者双引号本身,需要在字符串里加反斜杠转义,CODESYS 里写起来非常痛苦,所以发布的内容我通常全部用 ASCII 范围内的英文字母和数字,中文信息放到云端对照表里维护。
另外,发布时建议固定小数位。CODESYS 没有现成的“保留两位小数”函数,但工程上可以这样做:ROUND(温度值 * 100.0) / 100.0,或者干脆用整数传输。整数传输是 PLC 和云端对接最省心的方式:temp_x100 : INT,发布时直接INT_TO_STRING(temp_x100),语义在云端字段描述里写清楚“温度 = 值 / 100”。这比在 PLC 里做格式化字符串可靠得多。
4.2 订阅侧:从 MQTT 报文里提取字段到 PLC 变量
订阅 Zigbee2MQTT 的报文,payload 长这样:
{"battery":87,"temperature":21.4,"humidity":56.2,"linkquality":120}CODESYS 这边拿到这个字符串,目标是把temperature和humidity解析成 REAL 变量。这里有两个路线。
路线一:如果导入的库自带 JSON 解析功能块,优先用库的。CODESYS 3.5 的扩展库里有 JSON 解析相关的功能块,能把 JSON 字符串解析成键值对集合,再按 key 取 value。这和我之前做过的“json 转换”逻辑一致,只是运行环境从脚本变成了 PLC。用法大致是:
// 伪代码示意,库不同则 API 名称不同 jsonParser.Init(sPayload); jsonParser.FindKey('temperature'); fTemp := jsonParser.GetRealValue('temperature');路线二:库没有 JSON 解析能力,就只能自己抠字符串。CODESYS 提供了FIND、MID、LEFT、DELETE这些字符串函数,可以写一个简单提取函数:先FIND定位"temperature"这个 key 在字符串中的位置,再从冒号后面截取数字段,最后用STRING_TO_REAL转成数值。代码如下:
FUNCTION F_ExtractJsonReal : REAL VAR CONSTANT strKey : STRING := '"temperature"'; END_VAR VAR_INPUT sJson : STRING(255); END_VAR VAR nPosKey : INT; nPosColon : INT; nPosEnd : INT; sNumber : STRING(20); END_VAR// 定位 key 的位置 nPosKey := FIND(sJson, strKey); IF nPosKey > 0 THEN // key 后面必须是冒号 nPosColon := FIND(sJson, ':'); // 从冒号后取字符直到逗号或右大括号 nPosEnd := FIND(sJson, ','); IF nPosEnd = 0 THEN nPosEnd := FIND(sJson, '}'); END_IF sNumber := MID(sJson, nPosEnd - nPosColon - 1, nPosColon + 1); F_ExtractJsonReal := STRING_TO_REAL(sNumber); END_IF这段代码能跑通,但依赖两个前提:JSON 字段顺序固定、数字后面紧跟逗号或右括号。Zigbee2MQTT 的报文里字段顺序是乱的,所以解析前要先用FIND定位到 key 的实际位置,而不是想当然以为temperature排第一个。这也是很多现场数据时有时无的原因——某些设备上报时字段顺序变了。
4.3 数据量、QoS 与发送频率怎么权衡
PLC 往 broker 发数据的频率是需要人为控制的。很多人图省事,在每个扫描周期里都发布一次,结果是 broker 被高频小报文淹没,网络稍一拥堵就丢包。CODESYS 的扫描周期一般是几毫秒到几十毫秒,按这个频率发 MQTT,任何一个 broker 都会叫苦。
工程上的做法是“聚合发送”。用定时器控制发布节奏:遥测数据 5 秒发一次,事件数据立即发。如果确实需要高频率,那就把多个变量合成一个 JSON 数组一次性发布,减少报文数量。比如:
{"line":"01","ts":123456,"items":[{"tag":"T001","val":23.5},{"tag":"P001","val":4.1}]}这个取舍背后是 MQTT 传输效率的问题:MQTT 的报文开销很小,但 broker 和客户端的处理开销是按“条”计的,而不是按“字节”计的。同样 100 个数据点,每点一条消息和一条消息塞 100 个数据点,性能差距是数量级的。所以这个“高效可靠”的标题里,高效体现在聚合、可靠体现在 QoS 和会话管理,两者侧重点完全不同。
另外,retain 标志也要审慎使用。retain 的消息会让 broker 保存最后一条,新订阅者一上来就能拿到最新状态。这对 Zigbee 传感器状态有好处——PLC 重启后马上能知道传感器当前值。但对遥测数据,不建议 retain,否则 broker 上积累一堆过期数据,新客户端订阅时瞬间收到几百条旧消息,反而造成误导。
5. CODESYS 与 Zigbee2MQTT 联调避坑:现象、原因、排查顺序
5.1 PLC 总是连不上 broker:八成不是库的问题
现象:bConnected一直为 FALSE,LastError报超时或拒绝连接,broker 日志里看不到任何客户端接入记录。
原因:第一是网络不通,PLC 和 broker 不在同一个网段,或者防火墙拦了 1883 端口。第二是 broker 配置了匿名访问关闭,但连接参数没带用户名密码。第三是用了主机名但 PLC 没有配置 DNS。这三种情况里,第一种最常见。
解决:先用笔记本连接测试,在同一个网络里用 MQTTX 或 mosquitto_pub 验证 broker 本身能接,再回到 PLC 侧排查。PLC 侧用 IP 直连,暂时不要用域名。CODESYS 运行时所在的设备(工控机或 PLC 内置 Linux)网络参数确认能访问 broker 的 IP。如果设备上能开命令行,用 ping 和 telnet 测试端口,比反复重启 PLC 高效得多。
5.2 JSON 解析报错或乱码:CODESYS 字符串的经典坑
现象:broker 上看到的报文内容是好的,但 PLC 解析出来是空的;或者云端收到的 JSON 里中文变成乱码,浮点数带出一长串小数。
原因:字符串长度截断是第一大原因。CODESYS 的STRING(255)一旦超过 255 字符就截断,截断后的 JSON 不完整,解析必然失败。第二大原因是编码不一致。CODESYS 工程里字符串用的是 UTF-8,但有些第三方库或 IDE 配置默认 GBK,双方一换就可能乱码。第三大原因是浮点数格式化,REAL_TO_STRING转换出的字符串可能包含尾数误差。
解决:所有 MQTT payload 的字符串变量统一用STRING(255)或更大长度,发布前用LEN检查长度。所有业务数据用整数传输,浮点扩 100 倍或 1000 倍。中文尽量不进入 MQTT 报文,字段名和设备名统一用英文小写加下划线,避免编码问题变成定时炸弹。
5.3 掉线重连后丢消息、收不到消息
现象:PLC 和 broker 之间的网络闪断十几秒,恢复后连接是恢复了,但断线期间下发的指令一个都没收到;或者重连后订阅关系全部丢失,要重启 PLC 才能恢复。
原因:这是CleanSession语义没搞清楚。如果建立连接时CleanSession := TRUE,broker 不会保留这个客户端的会话,断线期间的 QoS 1 消息全部丢弃,重连后订阅也要重新建立。很多 PLC 程序只在首次启动时调用了一次Subscribe,重连后没有重新订阅,导致连接恢复但消息进不来。
解决:把CleanSession设为FALSE,broker 会保留会话和订阅关系。另一个更稳妥的做法是:连接成功后无条件重新调用一次订阅,把订阅动作放进连接状态机的“已连接”分支里,保证每次重连都重新订阅。这也是“后悔药”——不依赖 broker 的会话保留,程序自己掌握主动权。
5.4 Zigbee2MQTT 侧数据“有去无回”:主题映射不一致
现象:broker 上能看到 Zigbee2MQTT 发布的传感器数据,但 PLC 订阅了zigbee2mqtt/#却收不到;或者 PLC 向某个设备主题发布了 set 指令,设备没有反应。
原因:最常见的是主题层级对不上。Z2M 配置里每个设备的 friendly_name 决定了它在 MQTT 里的主题名,如果你订阅的是zigbee2mqtt/+/state,但设备上报的主题是zigbee2mqtt/temp_sensor_01(没有/state层级),通配符就匹配不上。另一个原因是设备离线,Z2M 只转发在线设备的消息,设备掉线后下游自然收不到。
解决:先在 MQTTX 里订阅zigbee2mqtt/#,肉眼确认实际主题结构,再回 PLC 里改订阅表达式。set 指令发不出去时,先确认这个设备是否支持 set——很多传感器只上报,不支持下行控制;只有开关、执行器这类设备才响应 set。排查时不要凭文档猜,直接在 MQTTX 里手动发一条 set 看设备动不动,这一步能快速区分是 PLC 的问题还是 Zigbee 设备的问题。
5.5 多代理连接互踢:client ID 重复
现象:PLC 同时连接本地和云端 broker,运行一段时间后其中一个连接频繁掉线,重连成功后另一个又掉线,两个连接像在“抢”什么。
原因:两个连接用了相同的 ClientID,而 broker 检测到同 ID 的客户端重复接入,会把旧连接断开。本地的和云端的可能是两个不同的 broker,如果只用一套默认配置不改 ClientID,恰好两边都叫PLC_Line01,本地不会出事,但云端如果也接了别的设备,或者 PLC 程序重启后旧连接没断开,就会出现互踢。
解决:每个连接的 ClientID 带上代理标识,比如PLC_Line01_Local和PLC_Line01_Cloud。另外注意,同一个 broker 下如果有两台 PLC,ClientID 也不能重复,规范一点的命名是PLC_Line01_1、PLC_Line01_2,或者用 PLC 的 MAC 后几位做后缀。这个坑不难避,但掉线时很难想到,属于典型的“血泪经验”。
6. 用 MQTTX 做端到端验证:把每一条数据链路都摊开检查
6.1 验证清单与常用命令
联调完成不代表万事大吉,我习惯按清单走一遍端到端验证,工具用 MQTTX(跨平台图形化 MQTT 客户端)和mosquitto_sub命令行。
第一步,验证 PLC 发布的数据。在 PC 上订阅 PLC 的遥测主题:
mosquitto_sub -h 192.168.1.50 -t 'factory/plc/line01/#' -v能看到 JSON 报文持续刷出来,说明 PLC 到 broker 的发布链路正常。此时断掉 PLC 的网线,确认 broker 上不再刷新,再恢复网线,确认数据自动恢复——这一步检验的是自动重连能力。
第二步,验证 PLC 订阅和数据解析。在 MQTTX 里手动向factory/plc/line01/cmd发一条指令,看 PLC 有没有动作,同时订阅 PLC 的 ack 主题确认应答。然后是 Zigbee 侧:确认zigbee2mqtt/#下有数据流动,在 PLC 程序里给接收变量加断点或者强制监视,确认 JSON 解析成功。
第三步,验证多代理。两个 broker 的订阅检查要分别做,确认本地和云端各收到各的,没有串台。再分别断开一条链路,确认另一条不受影响。
6.2 一个值得保留的调试习惯
这个方向做久了,我的一个习惯是:在 PLC 程序里保留一个“调试发布”功能块,用一个BOOL变量手动触发,往一个独立 topic 发当前关键变量的 JSON 快照。排错时不需要改程序、重新编译、重新下载,只需要在可视化界面里把那个变量置 TRUE,就能拿到现场数据。
还有一个经验是关于 broker 的日志。不管用 Mosquitto 还是 EMQX,联调时把日志级别调到 debug,看到Client PLC_Line01 connected、SUBSCRIBE、PUBLISH这些记录,能快速定位问题出在连接层还是业务层。很多时候故障不在 PLC 代码里,而在网络和 broker 配置里,先把链路分段验证,再回来看程序,能少走很多弯路。
这套方案做下来,最大的收益是 PLC 真正融入了物联网体系,JSON 数据、多代理、Zigbee 设备都成了同一套技术栈里的东西,不再需要额外的协议转换盒子。希望帮到你。
本文还有配套的精品资源,点击获取