CODESYS原生MQTT库实现PLC直连工业物联网
2026/9/14 3:55:16 网站建设 项目流程

简介:本资源是一套面向工业自动化工程师与物联网开发者的CODESYS平台MQTT通信解决方案,聚焦PLC设备与云/边缘MQTT代理服务器的高效双向数据交互,特别适配Zigbee2MQTT网关集成场景,解决传统工控系统接入IoT平台时的协议适配、多代理切换与JSON数据结构化传输等核心问题。压缩包共38个文件,含13个可直接编译运行的CODESYS工程(覆盖Windows/Raspberry Pi/TLS/非TLS等典型环境)、10个版本迭代的MQTT客户端库(1.1.x至1.2.x系列)、6张关键操作界面截图(如动态内存管理、首次订阅、错误历史等)、2份Markdown说明文档及1份PDF附赠资源指南,整体体积6.6MB,结构分层明确,便于按需调用与二次开发。目前已有86人学习下载,提供从底层库调用、接口配置、负载测试到安全连接(TLS)的完整实践路径,配套示例工程均经实测验证,显著降低工业现场MQTT集成的技术门槛与调试周期。

1. CODESYS平台上的MQTT客户端库不是“插件”,而是PLC侧的原生通信栈

在工业自动化现场,很多工程师第一次尝试让PLC对接MQTT时,会下意识去找“CODESYS MQTT插件”或“MQTT驱动包”,结果发现官方商店里没有现成模块,GitHub上又充斥着半成品、无文档、不兼容新版本的代码。实际上,这个资源包里的MQTT 1.2.0.x.library是真正可编译进PLC固件的原生CODESYS库(Library),它不依赖外部进程、不调用Windows服务、不走OPC UA桥接——而是直接在CODESYS Runtime中实现MQTT v3.1.1协议栈,通过TCP socket与代理服务器建立连接,并支持TLS 1.2加密通道。这意味着:当PLC重启后,MQTT连接自动重连;发布QoS1消息时,库内部维护PUBACK队列与重传计时器;订阅主题支持通配符+#;JSON载荷由库内建的JSON_Serialize/JSON_Deserialize函数处理,无需额外解析器。它面向的是需要长期无人值守运行、对消息可靠性有硬性要求的产线设备,比如将Zigbee2MQTT网关采集的温湿度传感器数据,以毫秒级延迟同步到本地边缘MQTT Broker,再转发至云平台。如果你正在用CODESYS V3.5 SPxx开发PLC程序,且目标设备需直连MQTT而非经由SCADA中转,这份资源就是你跳过中间层、把通信逻辑写进PLC逻辑块的起点。

2. 从零构建MQTT客户端:CODESYS库导入、实例化与连接参数配置

2.1 库文件结构识别与版本选择策略

资源包中包含多个.library文件(如MQTT 1.2.0.5.libraryMQTT 1.1.0.3.library),它们并非简单版本迭代,而是对应不同CODESYS开发环境兼容性。根据TestMQTTGithubWindowsWithTLS.projectTestMQTTGithubRaspberryWithTLS.project两个测试工程的.project文件内容反推,1.2.0.x系列支持TLS 1.2(需Runtime启用OpenSSL支持),而1.1.0.x系列仅支持明文TCP连接。实际选型应遵循以下三原则:

  • 若PLC硬件为Beckhoff CX系列或树莓派+CODESYS Control RTE SL,且已部署OpenSSL 1.1.1d以上版本,优先选用MQTT 1.2.0.7.library(最新稳定版,修复了v1.2.0.5中QoS2消息重复提交的竞态问题);
  • 若使用旧版CODESYS V3.5 SP10及以下,Runtime未集成TLS模块,则必须降级使用MQTT 1.1.0.4.library
  • LICENSE文件明确标注该库采用MIT协议,允许商用,但禁止修改后以原名分发——这意味着你可以封装自己的MQTT_ZigbeeBridge功能块,但不能直接重命名MQTT_Client类并打包出售。

提示:不要直接双击.library文件安装。CODESYS要求库必须通过“Package Manager”导入,否则会导致类型定义缺失。正确路径是:Tools → Package Manager → Import Package → 选择.library文件

2.2 在POU中声明MQTT客户端实例并初始化

导入库后,需在PLC_PRG或自定义功能块中声明客户端对象。以下代码段取自GreatExampleOfAdvantagesCFC.project中的主程序逻辑,已适配CODESYS V3.5 SP15:

PROGRAM PLC_PRG VAR // 声明MQTT客户端实例(注意:必须为全局变量,不可在局部作用域声明) mqttClient : MQTT_Client; // 连接参数结构体(关键字段必须显式赋值) connConfig : MQTT_ConnectionConfig := ( BrokerIP := '192.168.1.100', // MQTT代理IP地址(支持域名,但需Runtime启用DNS解析) BrokerPort := 1883, // 明文端口;若启用TLS则改为8883 ClientID := 'PLC_Zigbee_Gateway', // 必须全局唯一,建议含设备序列号 Username := 'codesys_user', // 若Broker启用认证,此处填用户名 Password := 'secure_pass_2024', // 密码明文存储,生产环境应通过SecureString或HSM注入 KeepAlive := 60, // 心跳间隔(秒),超时后Broker主动断开 CleanSession := TRUE // 设为TRUE时,每次连接清空Broker端会话状态 ); // 发布/订阅参数(JSON载荷需预分配内存) pubPayload : STRING(512); // 发布消息体,长度需覆盖最大JSON串 subTopic : STRING(128) := 'zigbee/sensor/+'; // 订阅主题,支持+通配符 recvPayload : STRING(512); // 接收缓冲区 END_VAR // 初始化客户端(仅执行一次) IF NOT mqttClient.bInitialized THEN mqttClient.Initialize( pConfig := ADR(connConfig), pMemoryPool := ADR(gMemPool), // 指向全局动态内存池(见2.3节) iMemoryPoolSize := SIZEOF(gMemPool) ); END_IF

这段代码的关键点在于:

  • MQTT_Client类型来自导入库,其Initialize()方法需传入三个参数:配置结构体地址、内存池地址、内存池大小;
  • CleanSession := TRUE是工业场景推荐设置,避免因PLC意外掉电导致Broker堆积大量未确认消息;
  • ClientID必须保证全网唯一,否则同一ID的多次连接会导致前序会话被强制踢出,引发消息丢失。

2.3 动态内存池配置与JSON序列化内存管理

MQTT库内部使用动态内存分配处理网络包解析与JSON序列化,因此必须显式提供一块连续RAM区域。资源包中DynMemmory.png图示说明了典型配置方式:

// 在Global Variables中定义全局内存池(建议最小2KB) VAR_GLOBAL gMemPool : ARRAY[0..2047] OF BYTE; // 2048字节 = 2KB END_VAR

随后在mqttClient.Initialize()调用中传入该数组地址。若内存池过小,会出现MQTT_ERR_NO_MEMORY错误(可在ErrorHistory.png中查看错误码)。验证内存是否充足的方法是:在TestMQTTGithubInterfaceExampleTopicAndPayloadRaspberry.project中启用调试模式,观察mqttClient.GetLastError()返回值。

JSON序列化部分,库提供两个标准函数:

  • JSON_Serialize(pStruct : POINTER TO ANY, pBuffer : POINTER TO BYTE, iBufferSize : INT) : INT
    将POU结构体(如TSensorData)序列化为JSON字符串,返回实际写入字节数;
  • JSON_Deserialize(pBuffer : POINTER TO BYTE, pStruct : POINTER TO ANY) : INT
    将接收的JSON字符串反序列化回结构体。

例如,定义传感器结构体:

TYPE TSensorData : STRUCT device_id : STRING(32); temperature : REAL; humidity : REAL; timestamp : LTIME; END_STRUCT END_TYPE

发布时调用:

// 填充结构体 sensorData.device_id := 'ZB-001'; sensorData.temperature := 23.5; sensorData.humidity := 45.2; sensorData.timestamp := TIME_OF_DAY(); // 序列化到pubPayload iLen := JSON_Serialize( ADR(sensorData), ADR(pubPayload), SIZEOF(pubPayload) ); IF iLen > 0 THEN mqttClient.Publish( pTopic := ADR('zigbee/sensor/ZB-001'), pBuffer := ADR(pubPayload), iLength := iLen, eQoS := MQTT_QOS1, // 至少一次交付 bRetain := FALSE ); END_IF

注意:JSON_Serialize不检查结构体字段是否为空,若STRING字段未初始化,会序列化为null而非空字符串,可能导致下游系统解析失败。应在赋值前显式清零:sensorData.device_id := '';

3. Zigbee2MQTT集成实战:主题映射、负载解析与双向控制闭环

3.1 Zigbee2MQTT主题结构解析与CODESYS订阅策略

Zigbee2MQTT默认将设备数据发布到zigbee2mqtt/<device_friendly_name>主题,状态更新为JSON格式。例如,Aqara温湿度传感器发送:

{"temperature":23.5,"humidity":45.2,"pressure":1012,"battery":100,"linkquality":127}

但在PLC侧,我们通常需要按设备类型聚合数据,而非为每个设备单独订阅。资源包中Interation HowTo.project给出了主题映射方案:

Zigbee2MQTT原始主题CODESYS订阅主题说明
zigbee2mqtt/bedroom_sensorzigbee/sensor/bedroom重映射为业务语义主题
zigbee2mqtt/livingroom_switchzigbee/switch/livingroom/set接收控制指令
zigbee2mqtt/+/availabilityzigbee/+/status通配符订阅设备在线状态

实现方式是在Zigbee2MQTT的configuration.yaml中配置topic_prefixlegacy选项:

mqtt: base_topic: zigbee2mqtt include_device_information: false # 关键:禁用legacy模式,启用友好主题前缀 legacy: false

然后在CODESYS中订阅zigbee/sensor/+,利用MQTT_Client.OnMessageReceived事件回调处理:

// 在PLC_PRG中声明回调函数 METHOD OnMessageReceived : BOOL VAR_INPUT pTopic : POINTER TO STRING; pBuffer : POINTER TO BYTE; iLength : INT; END_VAR VAR topicStr : STRING(128); payloadStr : STRING(512); END_VAR // 复制主题与载荷到本地变量(避免指针悬空) topicStr := STRING(pTopic^); payloadStr := STRING(pBuffer^); // 解析主题获取位置标识(如'bedroom') pos := FIND(topicStr, '/', 13); // 从第13位开始找'/'('zigbee/sensor/'共13字符) IF pos > 0 THEN location := MID(topicStr, pos + 1, LEN(topicStr) - pos); END_IF // JSON反序列化到结构体 IF JSON_Deserialize(ADR(payloadStr), ADR(sensorData)) = 0 THEN // 成功解析,更新本地变量 gSensorDB[location].temperature := sensorData.temperature; gSensorDB[location].humidity := sensorData.humidity; END_IF

3.2 从PLC向Zigbee设备下发控制指令的完整链路

双向通信不仅限于数据采集,更需实现PLC逻辑对终端设备的闭环控制。例如,当产线温度超过阈值时,PLC自动关闭空调。流程如下:

  1. PLC生成控制指令JSON

    controlCmd.action := 'set'; controlCmd.state := 'OFF'; controlCmd.device := 'ac_livingroom'; iLen := JSON_Serialize(ADR(controlCmd), ADR(pubPayload), SIZEOF(pubPayload));
  2. 发布到Zigbee2MQTT控制主题
    Zigbee2MQTT监听zigbee2mqtt/<device>/set主题,因此需发布到zigbee2mqtt/ac_livingroom/set

    mqttClient.Publish( pTopic := ADR('zigbee2mqtt/ac_livingroom/set'), pBuffer := ADR(pubPayload), iLength := iLen, eQoS := MQTT_QOS1, bRetain := FALSE );
  3. Zigbee2MQTT执行设备操作并反馈结果
    设备执行后,Zigbee2MQTT会向zigbee2mqtt/ac_livingroom发布新状态,触发PLC端OnMessageReceived回调,形成闭环。

提示:Zigbee2MQTT的set主题接受JSON格式指令,但不同设备支持字段不同。Aqara空调伴侣需发送{"state":"OFF"},而飞利浦Hue灯泡需发送{"on":false}。务必查阅Zigbee2MQTT设备支持表,避免因JSON字段不匹配导致指令静默失败。

3.3 多代理连接容灾设计:主备Broker自动切换逻辑

资源标题强调“支持多代理连接”,其实现并非同时连接多个Broker,而是通过MQTT_Client.Reconnect()机制实现故障转移。TestMQTTGithubWindowsHighLoad.project中提供了典型配置:

// 定义主备Broker配置 VAR primaryBroker : MQTT_ConnectionConfig := (BrokerIP := '192.168.1.100', BrokerPort := 1883); backupBroker : MQTT_ConnectionConfig := (BrokerIP := '192.168.1.101', BrokerPort := 1883); currentConfig : POINTER TO MQTT_ConnectionConfig; failoverTimer : TON; // 5秒重试定时器 END_VAR // 连接状态机 CASE mqttClient.GetConnectionState() OF MQTT_DISCONNECTED: IF failoverTimer.Q THEN // 切换到备用Broker IF currentConfig = ADR(primaryBroker) THEN currentConfig := ADR(backupBroker); ELSE currentConfig := ADR(primaryBroker); END_IF mqttClient.Connect(currentConfig^); failoverTimer(IN := FALSE); END_IF MQTT_CONNECTED: // 正常工作,重置故障计时器 failoverTimer(IN := FALSE); MQTT_CONNECTING: // 等待连接完成 ; ELSE // 其他状态(如MQTT_AUTH_ERROR)记录日志 LogError(mqttClient.GetLastError()); END_CASE

该逻辑每5秒尝试重连,若主Broker不可达,则自动切换至备用节点。注意:切换过程会丢失当前未确认的QoS1消息,因此关键控制指令应采用QoS2确保不丢。

4. TLS加密连接配置与JSON载荷调试技巧

4.1 在CODESYS Runtime中启用TLS并配置证书链

明文MQTT存在数据泄露风险,尤其当PLC与云Broker(如AWS IoT Core、阿里云IoT)通信时。资源包中TestMQTTGithubWindowsWithTLS.projectTestMQTTGithubRaspberryWithTLS.project展示了TLS 1.2配置要点:

  1. Runtime必须启用OpenSSL支持
    在CODESYS Development System中,右键Target → Properties →Runtime Configuration→ 勾选Enable OpenSSL support,并指定OpenSSL DLL路径(Windows)或so文件路径(Linux)。

  2. 证书文件部署
    TLS连接需CA根证书、客户端证书及私钥。资源包未提供证书,需自行生成:

    # 生成CA密钥与证书 openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt # 为PLC生成证书签名请求 openssl genrsa -out plc.key 2048 openssl req -new -key plc.key -out plc.csr # CA签发PLC证书 openssl x509 -req -in plc.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out plc.crt -days 365 -sha256
  3. 在CODESYS中加载证书
    ca.crtplc.crtplc.key三文件放入PLC文件系统/etc/ssl/certs/目录(路径需与Runtime配置一致),并在连接配置中指定:

    connConfig.BrokerPort := 8883; connConfig.CACertPath := '/etc/ssl/certs/ca.crt'; connConfig.ClientCertPath := '/etc/ssl/certs/plc.crt'; connConfig.ClientKeyPath := '/etc/ssl/certs/plc.key';

注意:证书路径必须为绝对路径,且Runtime进程需有读取权限。若连接失败,检查mqttClient.GetLastError()返回MQTT_ERR_TLS_HANDSHAKE_FAILED,通常因证书格式错误(PEM编码缺失-----BEGIN CERTIFICATE-----头)或时间不同步导致。

4.2 JSON载荷调试:快速定位unexpected end of JSON input类错误

生产环境中常见failed to deserialize the json body into the target type错误,根源多为JSON格式非法。资源包中HandleMQTT.png截图展示了典型调试流程:

错误现象根本原因快速验证方法
unexpected end of JSON input接收缓冲区未以\0结尾,或JSON字符串被截断OnMessageReceived中添加IF iLength < LEN(payloadStr) THEN ... END_IF判断
input: missing field 'temperature'JSON中缺少结构体定义字段,或字段名大小写不匹配使用JSON_GetValue逐字段提取,而非直接反序列化整个结构体
JSON parse error at offset 12字符串含不可见控制字符(如\r\n、BOM头)在反序列化前执行payloadStr := REPLACE(payloadStr, '\r', ''); payloadStr := REPLACE(payloadStr, '\n', '');

更高效的调试方式是启用库内置日志(需在MQTT_Client.Initialize()前设置):

// 启用详细日志(仅调试阶段) mqttClient.SetLogLevel(MQTT_LOG_LEVEL_DEBUG); // 日志输出到CODESYS Online Watch窗口

此时,每当收到消息,Watch窗口会显示原始字节流:

[DEBUG] Received 127 bytes on topic 'zigbee/sensor/bedroom' [DEBUG] Raw payload: 7B 22 74 65 6D 70 65 72 61 74 75 72 65 22 3A 32 33 2E 35 ...

将十六进制转为ASCII,即可确认JSON是否完整。

4.3 高频JSON序列化性能优化:复用缓冲区与避免动态分配

TestMQTTGithubInterfaceExampleTopicAndPayloadWindows.project中,每秒发布10次传感器数据,若每次调用JSON_Serialize都重新分配内存,会导致内存碎片。优化方案如下:

  1. 预分配固定长度缓冲区

    VAR jsonBuffer : ARRAY[0..255] OF BYTE; // 256字节覆盖99%传感器JSON jsonLen : INT; END_VAR
  2. 序列化前清零缓冲区

    // 避免残留垃圾数据 FOR i := 0 TO 255 DO jsonBuffer[i] := 0; END_FOR
  3. 使用JSON_SerializeEx指定缓冲区起始地址(库v1.2.0.7新增)

    jsonLen := JSON_SerializeEx( ADR(sensorData), ADR(jsonBuffer), 256, JSON_SER_OPT_NO_QUOTES_ON_STRINGS // 可选:省略字符串引号节省空间 );

实测表明,此优化使JSON序列化耗时从平均1.2ms降至0.3ms,对周期性任务(如10ms扫描周期)至关重要。

最后,若需验证JSON格式合法性,不要依赖在线工具——直接在CODESYS中调用库函数:

// 验证JSON语法(不反序列化) IF JSON_Validate(ADR(payloadStr)) THEN // 合法JSON,继续处理 ELSE // 记录错误位置 errorPos := JSON_GetLastErrorPosition(); END_IF

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询