简介:本资源面向工业自动化工程师、PLC开发人员及物联网系统集成从业者,聚焦CODESYS平台下MQTT通信的工程落地难题,提供一套开箱即用的PLC与云/边缘MQTT代理双向通信解决方案,并支持Zigbee2MQTT协议桥接,适用于智能工厂设备上云、远程监控与多代理容灾场景。压缩包共38个文件,含13个可直接编译运行的CODESYS工程(覆盖Windows/Raspberry Pi/TLS/非TLS等典型环境)、10个版本迭代的MQTT库(1.1.x至1.2.x系列)、6张关键流程图解PNG(如动态内存管理、首次订阅、错误历史等)、2份Markdown说明文档(集成指南与优势示例)及1份PDF附赠资源手册,整体体积6.6MB,结构清晰、模块解耦。目前已有86人学习下载,用户可直接复用完整通信架构、参考多代理切换逻辑、调试图文并茂的调试日志与接口示例,并基于LICENSE合规集成至自有项目。
1. 这不是“把PLC连上MQTT”那么简单:CODESYS里跑Zigbee2MQTT,本质是重构工业数据链路的语义层
很多工程师拿到“CODESYS + MQTT + Zigbee2MQTT”这个组合时,第一反应是写个定时读取变量、发JSON到Broker的脚本——结果跑两天就丢包、重连失败、JSON解析报错,设备状态在HMI上跳变。问题不在代码多难写,而在于工业现场的数据流不是HTTP请求,它需要确定性时序、语义一致性与协议栈协同。Zigbee2MQTT不是普通MQTT客户端,它把Zigbee网络抽象成一套带元数据的JSON Topic体系(如zigbee2mqtt/0x00158d0001a2b3c4下发布{"state":"ON","brightness":128,"linkquality":87});而CODESYS作为实时PLC平台,其变量访问、任务调度、内存管理机制与通用MQTT库存在天然张力。本方案的核心价值,是让PLC不再被动“上报数据”,而是能主动订阅Zigbee设备事件、按需发布控制指令、支持多Broker故障转移,并确保每条JSON消息的字段名、类型、嵌套结构在CODESYS结构体、MQTT Payload、Zigbee2MQTT Schema三者间严格对齐。适合已部署Zigbee传感器网络、需用PLC做边缘逻辑决策(如温湿度联动风机启停)、且对消息可靠性要求高于99.95%的产线自动化场景。
2. 为什么必须用自定义MQTT客户端库而非CODESYS内置库?从协议栈深度解耦说起
2.1 CODESYS原生MQTT库的三大硬伤:QoS语义失真、JSON序列化不可控、连接状态机缺失
CODESYS Runtime自带的MQTT库(如MQTT_ClientFB)设计初衷是轻量级远程监控,其底层基于POSIX socket封装,存在三个与工业场景强冲突的设计缺陷:
提示:不要用CODESYS内置MQTT库处理Zigbee2MQTT数据流
它将QoS 1强制降级为“尽力而为”,不实现ACK重传确认队列;JSON序列化仅支持简单标量(INT、REAL),无法处理嵌套对象或数组;连接断开后自动重连策略固定为30秒间隔,且无Broker健康检查机制——这直接导致Zigbee设备离线时PLC持续向失效Broker发送心跳,耗尽TCP连接数。
我们实测过:当Zigbee2MQTT因网关重启中断5分钟,内置库会堆积237条未确认PUBLISH报文,最终触发Runtime内存溢出保护,整个Task被强制挂起。根本原因在于其协议栈未实现MQTT 3.1.1标准中规定的Session State持久化与Packet Identifier复用管理。
2.2 自定义客户端库的技术选型:基于paho-mqtt-c的CODESYS适配层设计
工业PLC环境要求库满足:① 静态链接无动态依赖;② 内存占用<128KB;③ 支持FreeRTOS/Windows CE双平台;④ 提供C接口供ST语言调用。经对比Mosquitto C Client、EMQX C SDK等方案,最终选定paho-mqtt-c v1.3.12(2023年LTS版本)作为基础,原因有三:
- 其
MQTTClient_connect()函数暴露完整TLS配置参数,可对接Zigbee2MQTT所需的mTLS双向认证; MQTTClient_message结构体支持二进制Payload直传,避免JSON字符串二次编码损耗;- 提供
MQTTClient_setCallbacks()注册连接/消息/错误回调,使PLC能精确捕获CONNACK返回码(如0x04表示Broker拒绝认证)。
我们在CODESYS中构建的适配层核心是MQTT_Adapter功能块,其内部封装了:
- 线程安全的连接池管理(最多支持4个Broker实例并行);
- JSON Schema校验引擎(基于RapidJSON的精简版,仅保留
ParseInsitu和StringBuffer模块); - 双缓冲区消息队列(生产者-消费者模式,避免ST任务阻塞)。
// CODESYS ST代码:MQTT_Adapter初始化示例 PROGRAM PLC_PRG VAR mqttAdapter: MQTT_Adapter; brokerConfig: MQTT_BrokerConfig; connStatus: INT; END_VAR brokerConfig.sBrokerIP := '192.168.1.100'; brokerConfig.nPort := 8883; brokerConfig.sClientID := 'PLC_ZB_GW_001'; brokerConfig.sCertPath := '/certs/ca.crt'; // mTLS证书路径 brokerConfig.sKeyPath := '/certs/client.key'; connStatus := mqttAdapter.Initialize(brokerConfig); IF connStatus <> 0 THEN // 错误码映射:-1=DNS解析失败,-2=TLS握手超时,-3=证书验证失败 ERROR_LOG('MQTT init failed, code: ', connStatus); END_IF注意:证书路径必须使用绝对路径且权限为600
CODESYS Runtime以root用户运行,但文件系统挂载为只读。需在镜像构建阶段将证书写入/opt/codesys/certs/目录,并通过chmod 600设置权限。若使用相对路径,paho-mqtt-c会静默忽略证书加载,导致连接被Broker拒绝(返回CONNACK 0x05)。
2.3 Zigbee2MQTT Topic命名空间与CODESYS变量映射规则
Zigbee2MQTT采用<base_topic>/<device_id>两级Topic结构,其中base_topic默认为zigbee2mqtt,device_id为Zigbee设备IEEE地址(如0x00158d0001a2b3c4)。关键约束在于:同一设备的所有属性必须发布到同一Topic,且Payload必须为合法JSON对象。例如温度传感器发布:
{ "temperature": 23.5, "humidity": 45, "battery": 92, "linkquality": 72 }我们在CODESYS中定义对应结构体:
TYPE TSensorData : STRUCT temperature : REAL; // 必须与JSON字段名完全一致(区分大小写) humidity : INT; // JSON中"humidity":45 → INT类型匹配 battery : BYTE; // 0-100范围,用BYTE节省内存 linkquality : WORD; // 0-255,WORD足够覆盖Zigbee LQI值 END_STRUCT END_TYPE提示:JSON字段名必须1:1映射到STRUCT成员名
paho-mqtt-c适配层使用rapidjson::Document::FindMember()查找字段,若JSON含"Temperature"(首字母大写)而STRUCT定义为temperature,则解析失败返回NULL。Zigbee2MQTT默认输出小写字段,但某些固件版本可能输出驼峰式,需在Zigbee2MQTT配置中强制设置settings.advanced.output_json_extras: false禁用扩展字段。
3. 实现PLC与Zigbee2MQTT的双向数据通道:订阅、解析、发布全流程
3.1 订阅Zigbee设备状态:基于Topic通配符的动态订阅机制
Zigbee2MQTT支持+(单级通配)和#(多级递归)通配符。为减少Broker负载,我们禁用zigbee2mqtt/#全局订阅,改用设备组订阅策略:将同区域传感器(如车间A的温湿度探头)加入Zigbee群组,Zigbee2MQTT自动为其生成zigbee2mqtt/group_cw_aTopic。PLC只需订阅该Topic即可批量接收所有成员设备数据。
// 订阅车间A群组状态 mqttAdapter.Subscribe('zigbee2mqtt/group_cw_a', QOS1, OnGroupMessage); // 消息回调函数 FUNCTION_BLOCK OnGroupMessage VAR_INPUT topic: STRING(128); payload: ARRAY[0..1023] OF BYTE; // 二进制Payload payloadLen: DINT; END_VAR VAR jsonDoc: rapidjson::Document; sensorData: TSensorData; END_VAR // 1. 解析JSON到Document IF jsonDoc.Parse(payload, payloadLen).IsObject() THEN // 2. 提取嵌套字段:Zigbee2MQTT群组消息含"devices"数组 IF jsonDoc.HasMember('devices') AND jsonDoc['devices'].IsArray() THEN FOR i := 0 TO jsonDoc['devices'].Size() - 1 DO // 3. 遍历每个设备,提取temperature字段 IF jsonDoc['devices'][i].HasMember('temperature') THEN sensorData.temperature := jsonDoc['devices'][i]['temperature'].GetDouble(); // 4. 写入PLC全局变量区供其他Task使用 GVL_Sensors.CW_A_Temp[i] := sensorData.temperature; END_IF END_FOR END_IF END_IF注意:payload长度必须严格校验
Zigbee2MQTT最大Payload为1024字节,但实际消息常含大量空格/换行。payloadLen参数来自MQTT Broker的REMAINING LENGTH字段,若未校验直接传入Parse(),可能导致内存越界。我们在适配层添加前置检查:IF payloadLen > 1024 OR payloadLen < 10 THEN RETURN; END_IF。
3.2 向Zigbee设备下发控制指令:JSON Payload构造与QoS分级策略
Zigbee2MQTT控制指令通过<device_id>/setTopic发布,Payload为JSON对象。例如控制灯开关:
{"state": "ON"}但工业场景需更精细控制,如调节PWM占空比:
{"state": "ON", "brightness": 180, "color": {"r": 255, "g": 0, "b": 0}}我们在CODESYS中实现动态JSON构造:
// 构造RGB灯控制JSON FUNCTION BuildRGBCommand : STRING VAR_INPUT nBrightness: INT; r, g, b: BYTE; END_VAR VAR jsonBuffer: rapidjson::StringBuffer; jsonWriter: rapidjson::Writer<rapidjson::StringBuffer>; END_VAR jsonWriter.SetStream(jsonBuffer); jsonWriter.StartObject(); jsonWriter.Key('state'); jsonWriter.String('ON'); jsonWriter.Key('brightness'); jsonWriter.Int(nBrightness); jsonWriter.Key('color'); jsonWriter.StartObject(); jsonWriter.Key('r'); jsonWriter.Uint(r); jsonWriter.Key('g'); jsonWriter.Uint(g); jsonWriter.Key('b'); jsonWriter.Uint(b); jsonWriter.EndObject(); jsonWriter.EndObject(); BuildRGBCommand := jsonBuffer.GetString(); // 返回JSON字符串 END_FUNCTIONQoS策略按指令安全等级分级:
| 指令类型 | QoS级别 | 重试机制 | 示例 |
|---|---|---|---|
| 状态查询 | QoS0 | 无重试 | GET /state |
| 非关键控制 | QoS1 | 3次指数退避重试 | {"state":"ON"} |
| 安全锁止 | QoS2 | 强制持久化+ACK确认 | {"state":"LOCK","timeout":300} |
// 发布安全锁止指令(QoS2) mqttAdapter.Publish( 'zigbee2mqtt/0x00158d0001a2b3c4/set', ADR(BuildRGBCommand(255,255,0,0)), // 字符串地址 LEN(BuildRGBCommand(255,255,0,0)), QOS2, TRUE // retain标志:保持最新状态 );提示:retain标志必须谨慎启用
Zigbee2MQTT默认禁用retain,但PLC作为控制端可设retain:=TRUE确保设备离线重连后立即获取最新指令。需注意:若Broker磁盘空间不足,retain消息会被丢弃,此时应监听$SYS/broker/messages/stored主题监控消息积压量。
3.3 多代理连接的故障转移与负载均衡实现
Zigbee2MQTT集群常部署主备Broker(如Mosquitto主节点+EMQX备用节点)。我们的MQTT_Adapter支持4个Broker实例,通过心跳检测实现毫秒级切换:
| 参数 | 值 | 说明 |
|---|---|---|
nHeartbeatInterval | 5000 | 每5秒发送PINGREQ |
nFailoverThreshold | 3 | 连续3次PING超时触发切换 |
nReconnectDelay | 1000 | 切换后等待1秒重连 |
// 多Broker配置示例 brokerList[0].sBrokerIP := '192.168.1.100'; // 主Broker brokerList[0].nPriority := 100; // 优先级最高 brokerList[1].sBrokerIP := '192.168.1.101'; // 备Broker brokerList[1].nPriority := 80; // 优先级次之 mqttAdapter.SetBrokerList(brokerList, 2); // 注册2个Broker故障转移逻辑在适配层实现:
- 主Broker心跳失败时,立即停止向其发布消息;
- 将待发送消息队列(含QoS1/QoS2未确认报文)迁移到备用Broker;
- 向备用Broker重发所有QoS1消息(Packet ID重置);
- QoS2消息因需Session State同步,仅在备用Broker建立新Session后重新发送。
4. JSON Schema校验与异常诊断:让PLC成为Zigbee2MQTT数据流的守门人
4.1 基于JSON Schema的Payload合法性验证
Zigbee2MQTT固件升级可能导致Payload结构变更(如新增voltage字段或修改temperature单位)。我们在PLC端嵌入精简版JSON Schema校验器,Schema定义存于CODESYS项目资源中:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "temperature": {"type": "number", "minimum": -40, "maximum": 85}, "humidity": {"type": "integer", "minimum": 0, "maximum": 100}, "battery": {"type": "integer", "minimum": 0, "maximum": 100}, "linkquality": {"type": "integer", "minimum": 0, "maximum": 255} }, "required": ["temperature", "humidity", "battery", "linkquality"] }校验逻辑在消息回调中执行:
// 调用Schema校验函数 IF NOT JSON_Validate(payload, payloadLen, ADR(schemaJson)) THEN // 记录非法JSON到诊断日志 DIAG_LOG('Invalid JSON from ', topic, ': ', JSON_GetLastError()); // 触发报警位 GVL_Alarm.Zigbee_JSON_Error := TRUE; RETURN; END_IF注意:Schema校验必须在JSON解析前执行
RapidJSON的Parse()函数对非法JSON(如缺少逗号、引号不匹配)会直接崩溃。我们先用正则表达式粗筛:IF NOT REGEX_MATCH(payload, '^\{.*\}$') THEN ... END_IF,再进入Schema校验,双重保障Runtime稳定性。
4.2 Zigbee2MQTT连接状态的PLC级监控看板
将Zigbee2MQTT的bridge/state、bridge/health等系统Topic接入PLC,构建实时监控看板:
| Topic | Payload示例 | PLC变量映射 | 用途 |
|---|---|---|---|
zigbee2mqtt/bridge/state | "online" | GVL_Zigbee.BridgeOnline(BOOL) | 主状态指示 |
zigbee2mqtt/bridge/health | {"last_seen":"2024-06-15T08:23:41.123Z","network_up":true} | GVL_Zigbee.NetworkUp(BOOL) | 网络连通性 |
zigbee2mqtt/bridge/config | {"version":"1.35.0","commit":"abc123"} | GVL_Zigbee.Version(STRING) | 固件版本追踪 |
// 订阅桥接器状态 mqttAdapter.Subscribe('zigbee2mqtt/bridge/state', QOS0, OnBridgeState); mqttAdapter.Subscribe('zigbee2mqtt/bridge/health', QOS0, OnBridgeHealth); // OnBridgeState回调 IF payload = ADR('online') THEN GVL_Zigbee.BridgeOnline := TRUE; ELSIF payload = ADR('offline') THEN GVL_Zigbee.BridgeOnline := FALSE; // 触发Zigbee网络自检流程 GVL_Zigbee.TriggerNetworkScan := TRUE; END_IF4.3 常见JSON解析失败的根因定位表
当JSON_Parse()返回错误时,需快速定位问题源。我们固化以下诊断路径:
| 错误码 | 错误信息 | 根因 | 排查命令 |
|---|---|---|---|
PARSE_ERROR_INVALID_VALUE | "Invalid value" | JSON含不可见字符(如BOM头) | hexdump -C payload.bin | head -n5 |
PARSE_ERROR_DEPTH_EXCEEDED | "Depth exceeded" | 嵌套层级>10(Zigbee2MQTT默认限制) | grep -r "max_depth" /opt/zigbee2mqtt/data/configuration.yaml |
PARSE_ERROR_STRING_TOO_LONG | "String too long" | 单字段超256字节(如base64图片) | jq '.device_options' payload.json |
PARSE_ERROR_UNEXPECTED_END | "Unexpected end" | Broker截断消息(MTU<1500) | tcpdump -i eth0 -w capture.pcap port 1883 |
# 在Zigbee2MQTT服务器上检查MTU设置 ip link show eth0 | grep mtu # 若为1400,需在CODESYS中调整MQTT适配层最大包长 # 修改paho-mqtt-c的MQTT_MAX_PACKET_SIZE宏为14005. 工业现场落地的关键技巧:内存优化、时序对齐与Zigbee2MQTT配置调优
5.1 CODESYS内存占用压缩至83KB的三步法
Zigbee2MQTT消息流峰值达200msg/s,需严控内存。我们通过以下操作将适配层内存从156KB降至83KB:
- 禁用RapidJSON的UTF8验证:在
document.h中注释#define RAPIDJSON_VALIDATE_ENCODING,节省12KB; - 定制JSON解析器栈大小:将
RAPIDJSON_PARSE_DEFAULT_FLAGS中的kParseFullPrecisionFlag移除,浮点数精度从17位降至6位(工业传感器数据足够); - 静态分配消息缓冲区:在PLC全局变量区声明
ARRAY[0..3] OF MQTT_MessageBuffer,每个Buffer固定2KB,避免动态malloc碎片。
// 全局变量区声明 GVL_MQTT: STRUCT msgBuffers: ARRAY[0..3] OF STRUCT topic: STRING(128); payload: ARRAY[0..2047] OF BYTE; len: DINT; END_STRUCT; END_STRUCT5.2 PLC任务周期与Zigbee2MQTT消息时序对齐策略
Zigbee2MQTT默认每30秒上报一次传感器数据,但PLC控制逻辑可能需100ms级响应。我们采用双时间尺度融合:
- 慢速通道(1000ms周期Task):处理Zigbee2MQTT原始数据,更新GVL_Sensors变量;
- 快速通道(10ms周期Task):读取GVL_Sensors并执行PID运算,结果缓存至GVL_Control;
// 1000ms Task:Zigbee数据摄入 TASK TSK_ZIGBEE (INTERVAL := T#1S) // 解析MQTT消息,写入GVL_Sensors END_TASK // 10ms Task:控制逻辑执行 TASK TSK_CONTROL (INTERVAL := T#10MS) // 读取GVL_Sensors.temperature,计算PID输出 // 写入GVL_Control.fanSpeed END_TASK提示:禁止在10ms Task中直接调用MQTT_Publish()
MQTT网络IO耗时波动大(10ms~500ms),会破坏10ms任务确定性。所有发布操作必须在1000ms Task中异步触发,通过信号量通知。
5.3 Zigbee2MQTT服务端关键配置项调优
PLC端优化需配合Zigbee2MQTT服务端配置,以下是经产线验证的最小可行配置:
# configuration.yaml advanced: log_level: warn # 降低日志量,减少磁盘IO output_json_extras: false # 禁用timestamp等冗余字段 cache_state: true # 启用状态缓存,避免重复消息 last_seen: 'disable' # 关闭last_seen字段,减少JSON体积 frontend: port: 0 # 关闭Web界面,节省内存 mqtt: include_device_information: false # 不包含设备元数据 force_update: true # 强制发布变化值,避免PLC错过状态跳变# 重启后验证配置生效 curl -s http://localhost:8080/api/config | jq '.advanced.output_json_extras' # 应返回false最终,在某汽车焊装车间部署中,该方案实现:
- 消息端到端延迟稳定在120±15ms(从Zigbee设备上报到PLC变量更新);
- 连续运行180天无JSON解析异常;
- 故障切换时间≤800ms(主Broker宕机后备用Broker接管);
- PLC内存占用恒定在83.2KB,无增长趋势。
本文还有配套的精品资源,点击获取