☰
CODESYS集成MQTT客户端库:PLC设备上云与双向通信实战指南
2026/10/22 18:23:40 网站建设 项目流程

简介:这套基于CODESYS平台的MQTT客户端库,面向工业自动化与物联网开发者,解决PLC设备与MQTT代理间高效可靠双向通信的需求;同时包含Zigbee2MQTT集成方案,支持多代理连接与JSON数据交互,适用于智能产线、远程运维等场景。压缩包共38个文件,大小仅6.6MB,主体为13个project示例工程与10个library库文件(MQTT 1.1和1.2多个版本),另有PNG架构/操作示意图、TXT说明、Markdown集成文档和PDF教程,从库添加、消息订阅发布到TLS加密连接均有清晰演示。示例覆盖Windows与Raspberry平台,区分有无TLS环境,方便读者对照不同软硬件环境调试;结合文档中关于消息主题、负载格式和动态内存处理的讲解,可直接迁移到自有PLC项目中复用。目前已有89人浏览学习,适合正在评估或实施CODESYS联网方案的工程师快速入门并少走弯路。

1. 基于CODESYS的MQTT客户端库:给PLC配一条双向消息通道

一条装配线,既有工位温湿度盒子,又有AGV呼叫按钮和能源表,老板说把这些数据都接进PLC,做完还能远程下发参数。用Modbus去一台台轮询,点位表能改到崩溃;这时候再回头看基于CODESYS平台的MQTT客户端库,就明白它为什么成了设备上云方向上的主流做法。它是把PLC当成一个普通MQTT客户端,向代理服务器发布数据、订阅指令,实现双向传输,还支持同时挂多个代理,消息载荷统一走JSON,调试和扩展都比点位表舒服得多。这套方案适合两类人:一类是懂ST语言但没摸过MQTT的自动化工程师,另一类是被各种网关盒子来回倒腾通信格式、想收拢技术栈的人。读完你应该能判断它值不值得用,并且能直接照着搭一个最小闭环。

2. MQTT进PLC的选型逻辑:通信模型与CODESYS库的功能块划分

2.1 从轮询到订阅:MQTT在控制现场能带来什么

传统PLC上位通信,最常见的主从轮询模型:PLC当主站,一个周期里把仪表、网关、触摸屏轮一遍,谁的点位多了,轮询周期就拉长;断线重连后要把历史数据重新捞一遍,通信逻辑经常比控制逻辑还难维护。MQTT换了个思路,PLC不再是主站,而是代理服务器(broker)下的一个普通客户端,所有设备都往broker上发布、订阅,PLC和上位机、传感器盒子之间不需要直接认识。

这个模型带来的直接好处是解耦。新增一台设备,只要它往同一个代理上发主题,PLC订阅对应主题就能收到,不用改其他节点的通信配置。掉线重连也简单,客户端带着clientId重新连上,broker会按cleanSession配置决定是否补发离线消息,自动化场景里常用的做法是采集类数据允许丢,命令类用QoS1保证投递并做去重。

对PLC工程师来说,要把这个概念落到CODESYS里,关键理解三点:一是QoS等级的含义,QoS0最多一次、QoS1至少一次、QoS2恰好一次,现场最常用的是QoS0做高频采集、QoS1做命令下发;二是keepalive心跳,它决定broker多长时间认为你掉线;三是Last Will遗嘱消息,PLC异常断线时broker会替它发一条消息,让上位机立刻知道这台设备失联了。

2.2 CODESYS侧MQTT库的常见构成:连接、发布、订阅三组功能块

CODESYS里MQTT能力不是内嵌在标准库里的,而是以库的形式装进工程,常见做法是厂商或社区提供一组功能块,按职责拆成三组:连接管理、发布、订阅。我一般在库管理器里先确认这三组都在,再决定工程怎么分层。

// 连接功能块的典型实例化(功能块名以你所装库的文档为准) VAR fbMqttConn : MQTTClient_Connect; hClient : DWORD; // 连接句柄,多代理时用来区分 sBrokerUrl : STRING(120) := 'tcp://192.168.1.20:1883'; sClientId : STRING(40) := 'PLC_LineA'; bConnReq : BOOL := FALSE; bConnected : BOOL := FALSE; END_VAR // 需要建立连接时给 bConnReq 一个上升沿 fbMqttConn( bExecute := bConnReq, sBrokerUrl := sBrokerUrl, sClientID := sClientId, hClient => hClient, bConnected => bConnected);

关键参数有三个:sBrokerUrl的写法要带协议头,tcp://表示明文1883,ssl://表示加密443;sClientID在同一代理下必须唯一,重复会导致旧连接被踢掉线;hClient输出句柄是后续发布和订阅都要带的身份标识,多代理连接时尤其靠它区分是哪个链路收到的数据。

还需要说明一点,CODESYS的MQTT库调用多为触发式功能块,bExecute给上升沿执行一次,连接建立后靠keepalive维持。很多初用者会把这个功能块放在周期任务里每次执行,反而导致重复建连,正确做法是只在需要时给一次触发,连接状态用bConnected去判断。

2.3 选型判断:什么场景适合这套,什么场景还是走Modbus更省事

不是所有PLC通信都该换成MQTT。我一般按三个问题做判断:数据实时性要求是不是毫秒级?是不是同一网段内少数设备直连?控制回路本身是不是需要硬实时?伺服轴控、运动插补这类链路,老老实实走EtherCAT或PROFINET,别拿MQTT凑热闹,它本质是消息投递,不保证周期抖动。

该用MQTT的场景是数据采集、远程监控、跨网段传输、设备上云。PLC内做库的优势是省掉一个网关硬件,接线最少、成本最低;代价是通信占用了PLC自己的任务周期和内存。另一种常见做法是外置网关盒子,PLC只管Modbus给网关,网关做MQTT转发,好处是PLC负载小,坏处是多一个设备多一层排障。

我的建议很简单:改造老设备、PLC算力紧张,加网关;新项目、点位多且后续要加设备,直接在CODESYS里做MQTT客户端库,省一个中间环节。两者各是各的合理边界,别指望一个方案通吃。

3. 跑通最小闭环:连接、发布订阅与JSON载荷的落地配置

3.1 连接与断线重连:最小ST代码与连接参数说明

先从连接开始。上一章的代码能建连,但生产环境不能只建连,还要处理掉线重连。常见做法是把连接状态机化:未连接时等待、连接中避免重复触发、已连接时周期检查心跳状态。

CASE byConnState OF 0: // 空闲,等待条件满足后请求连接 IF bNeedConnect THEN bConnReq := TRUE; byConnState := 1; END_IF 1: // 连接中,等待功能块反馈 bConnReq := FALSE; IF fbMqttConn.bConnected THEN byConnState := 2; ELSIF fbMqttConn.bError THEN byConnState := 0; // 间隔一段时间再重试 END_IF 2: // 已连接,周期检查,异常则回到空闲 IF NOT fbMqttConn.bConnected THEN byConnState := 0; END_IF END_CASE

这段状态机的意义在于避免周期性重复触发连接功能块。很多库的bExecute是边沿触发,如果每个扫描周期都给TRUE,代理端会看到同一个clientId反复连接,表现为掉线、重连、再掉线。状态机里只有状态0才给bConnReq上升沿,其他状态保持FALSE。

连接参数里还有一个容易被忽略的项:keepalive。常见做法设30到60秒,比PLC扫描周期大一到两个数量级,目的是让broker在真正掉线时能通过心跳超时感知,而不是等TCP超时等上几分钟。如果PLC任务周期本身是10ms,却把keepalive设成5秒,代理侧稍微抖动就可能误判掉线。

3.2 上报方向:把PLC变量封装成JSON发布到主题

CODESYS的ST没有原生的JSON序列化,常见做法是用字符串拼接,把PLC变量转成字符串后拼出JSON文本。温度值如果原来是整数除以10的精度,先放大成整数再转字符串,避免浮点格式化出现意外的科学计数法。

// 上报当前设备状态到主题 plc/lineA/status sPayload := CONCAT( '{"temp":', DINT_TO_STRING(nTempInt), ',"run":', BOOL_TO_STRING(bRunning), ',"ts":', DINT_TO_STRING(udiTimestamp), '}'); fbMqttPub( bExecute := bPubReq, hClient := hClient, // 绑定到已建立的连接 sTopic := 'plc/lineA/status', sPayload := sPayload, uiQoS := 0, bRetain := FALSE);

发布功能块的参数逻辑:hClient填连接功能块输出的句柄,多代理时这个参数决定消息发给哪个broker;uiQoS为0时最快但可能丢包,适合周期性状态上报;bRetain为TRUE时broker会把最后一条消息存下来,新订阅者一上来就能拿到当前值,适合温湿度这种慢变量,不适合会误导控制逻辑的瞬时命令。

这里要提醒一个坑:CONCAT拼接的字符串超过CODESYS字符串变量最大长度时会静默截断,长JSON会莫名缺字段。我一般把sPayload声明成STRING(255)或更大,并且发送前用LEN检查长度,超过阈值就拆包或者改用轮询读取缓冲区。

3.3 下发方向:订阅主题并解析JSON回写PLC变量

订阅是MQTT客户端库里最体现差异的部分。CODESYS库的常见实现是:订阅功能块注册一个主题过滤器,之后收到的消息放进内部缓冲区或回调队列,用户在周期任务里轮询读取。回调里直接操作PLC变量不是主流做法,因为中断上下文里访问全局变量容易和数据一致性打架。

// 订阅下发命令主题,支持通配符 fbMqttSub( bExecute := TRUE, hClient := hClient, sTopicFilter := 'cmd/lineA/#', // # 匹配该层级所有下级主题 uiQoS := 0); // 在每个扫描周期轮询是否有新消息 bGotMsg := fbMqttSub.bMessageAvailable; IF bGotMsg THEN sRcvTopic := fbMqttSub.sMessageTopic; sRcvPayload := fbMqttSub.sMessagePayload; // 接下来做JSON解析,把目标值写入对应PLC变量 ... END_IF

订阅后解析JSON的策略有两种,一种是用CODESYS的JSON解析库,用路径表达式取键值;另一种是字符串查找截取。前者工程规范,后者临时排查快。我自己的习惯是:正式工程用解析库,因为它能处理键顺序变化和转义字符;调试阶段直接在监视窗口里用查找函数看字段,几秒钟能定位问题。

JSON解析库有一个常见约定:键名写带点号的路径temp.zone1会先找temp对象再找zone1键。可Zigbee2MQTT这类系统生成的键名常带连字符或点号,解析库的路径分隔符会踩坑,后面专门讲。

3.4 本地闭环验证的检查清单

跑通闭环后,别急着接真实设备,先在CODESYS仿真或者一台小PLC上做验证,我的检查顺序如下。

第一,broker连通性:用PC上的MQTT客户端工具订阅#,看PLC发布的消息能不能收到,这是最快的一层验证。第二,连接稳定性:让PLC反复断电重启,观察broker上是否出现clientId冲突,旧连接有没有被正常释放。第三,JSON完整度:订阅端收到的JSON逐字段和PLC原始变量对比,重点检查字符串截断、大小写、时间戳类型。

第四,命令下发回环:从订阅端发一条JSON命令,PLC收到后改变输出,再反过来发布一条状态,确认双向链路都通。这个回环验证做完,最小闭环就稳了,然后再去接多代理和Zigbee2MQTT。

4. 多代理连接:同机同时对接多个Broker的路由与状态管理

4.1 多代理场景辨析与连接资源分配

单代理方案覆盖不了所有现场。我遇到最典型的是:本地边缘代理负责采集产线实时状态,给MES看;云端代理负责远程运维和报表,给总部看。同一个PLC必须同时和两个代理维持连接,数据还不能发串。

多代理不是把连接功能块复制一份那么简单,连接句柄、clientId、心跳、遗嘱、重连状态都必须按代理维度分开管理。常见做法是用结构体数组描述代理连接,每个代理一个元素,索引号就是路由ID。

TYPE ST_MqttProxy : STRUCT sBrokerUrl : STRING(120); sClientID : STRING(40); hClient : DWORD; // 连接句柄,建连成功后由库回填 byState : BYTE; // 0空闲 1连接中 2已连接 bError : BOOL; byErrCode : BYTE; END_STRUCT END_TYPE VAR arrProxy : ARRAY [1..2] OF ST_MqttProxy; END_VAR

用数组管理的好处是重连逻辑可以统一写成FOR循环,新增代理只是加一个数组元素和连接参数。这里要特别注意:clientId在同一个broker内唯一,但不同broker之间可以相同,所以同一个PLC在两个代理上用同一个ID没问题,只要不是连到同一个broker就行。

4.2 clientId、Last Will与重连状态机怎么处理

多代理下最容易翻车的是重连逻辑。两个代理一个断线,重连代码如果不区分句柄,很可能把还活着的那个连接也触发一遍重复建连。所以重连状态机必须和数组下标绑定。

FOR i := 1 TO 2 DO CASE arrProxy[i].byState OF 0: IF bNeedReconnect[i] THEN fbMqttConn[i](bExecute := TRUE, sBrokerUrl := arrProxy[i].sBrokerUrl, sClientID := arrProxy[i].sClientID, hClient => arrProxy[i].hClient, bConnected => arrProxy[i].bConnected); arrProxy[i].byState := 1; END_IF 1: fbMqttConn[i](bExecute := FALSE); IF arrProxy[i].bConnected THEN arrProxy[i].byState := 2; ELSIF fbMqttConn[i].bError THEN arrProxy[i].byState := 0; // 稍后重试 END_IF 2: IF NOT arrProxy[i].bConnected THEN arrProxy[i].byState := 0; END_IF END_CASE END_FOR

每个代理独立走一遍状态机,互不干扰。另外还要给每个代理单独配Last Will遗嘱,内容至少包含代理代号和PLC状态,这样broker能替PLC上报"这台设备从这里掉了",上位机和云端都能第一时间感知是哪个区域失联。

4.3 消息路由与话题规划:避免跨代理串扰

多代理最头疼的不是连不上,而是消息发错地方。比如本地MES不该看到云端的远程升级指令,云端又不该采集本地的高频过程数据。话题规划要按方向切干净。

我一般按主题前缀划分:本地上报全部走local/plc/...,云端上报走cloud/plc/...;下发命令本地走cmd/local/...,云端走cmd/cloud/...。主题过滤器和发布调用各绑定各的句柄,订阅时更要注意,不要图省事用#把两个代理的消息全收到一个缓冲区里。

如果要跨代理转发,比如本地MES需要把一条数据同时发到云端,常见做法不是让PLC发两次,而是PLC发布到本地broker,由broker侧的路由规则镜像到云端broker。PLC端只保证自己发布到正确主题,跨代理转发尽量交给broker或网关做,别让PLC任务承担太多消息搬运。

4.4 多代理ST代码框架

这里给一个发布方向的路由选择框架,体现"同一份PLC变量,按代理分别构建载荷"。

CASE iTargetProxy OF 1: // 本地代理,发布高频状态 sPayload := BuildJson_Local(); // 自建函数:拼接本地点位JSON fbMqttPub(bExecute := TRUE, hClient := arrProxy[1].hClient, sTopic := 'local/plc/lineA/status', sPayload := sPayload, uiQoS := 0); 2: // 云端代理,发布低频汇总 sPayload := BuildJson_Cloud(); // 自建函数:拼接云端汇总JSON fbMqttPub(bExecute := TRUE, hClient := arrProxy[2].hClient, sTopic := 'cloud/plc/lineA/summary', sPayload := sPayload, uiQoS := 1); END_CASE

这个框架把"发什么数据""发到哪个代理""用什么QoS"三件事分开,改路由只改CASE分支,不动数据拼装逻辑。实际项目里我还会加一个上电自检:两个代理的连接状态都健康后,才开放命令下发,防止PLC带着半连接状态执行来自云端的误指令。

5. Zigbee2MQTT集成联调与常见问题排查:从主题映射到数据落地

5.1 Zigbee2MQTT的通信链路:设备状态怎么变成JSON推给PLC

Zigbee2MQTT做的事情是把Zigbee无线设备桥接到MQTT。它连接Zigbee协调器,订阅协调器上报的无线数据,再转换成JSON发布到MQTT代理。对PLC来说,Zigbee设备完全透明,PLC只面对主题和JSON。

链路是:Zigbee传感器 → Zigbee2MQTT → MQTT代理 → PLC的MQTT订阅 → 解析JSON → 回写变量。典型主题是zigbee2mqtt/<设备友好名>,比如一个叫temp_sensor_01的设备,它上报的消息长这样:

{ "temperature": 25.3, "battery": 89, "linkquality": 55, "last_seen": "2025-01-15T10:24:00" }

PLC要拿到温度,就订阅zigbee2mqtt/temp_sensor_01,解析出temperature字段。另外Zigbee2MQTT还维护一个zigbee2mqtt/bridge/devices主题,用于查询设备列表和状态,联调时很有用,可以确认设备到底有没有正常上报。

5.2 排查1:CODESYS侧收不到Zigbee2MQTT消息

现象:Zigbee2MQTT运行正常,用PC端MQTT工具能收到温度,可PLC程序里bMessageAvailable始终为FALSE。

原因:多半是订阅的clientId或连接状态没对上,或者PLC订阅的broker地址不是Zigbee2MQTT写入的那个代理。Zigbee2MQTT配置里写的是mqtt://某IP:1883,PLC连的却是另一个代理,两边的broker不是同一个,自然收不到。

解决:先用PC工具连PLC所连的那个代理,订阅#确认Zigbee2MQTT有没有把消息发到这个代理上。确认后,把PLC的sTopicFilter从zigbee2mqtt/#开始,逐级收窄,看哪一级断了。还有一个常见细节:订阅功能块要在连接成功后执行,很多库在未连接时调用订阅会静默失败,要在状态机的已连接状态下发订阅请求。

5.3 排查2:JSON键名不兼容导致解析失败

现象:消息明明收到了,但JSON解析库取不到temperature的值,或者取出来是空字符串。

原因:Zigbee2MQTT的JSON键名本身没有特殊字符,问题常出在嵌套对象上,比如有些设备上报{"illuminance": {"lux": 320}},用路径表达式取illuminance.lux时,解析库把点号当分隔符,可设备上报的键里如果真带点号,就会被误拆。

解决:先看原始载荷,别盲猜键路径。用PC订阅工具抓一条完整JSON,再逐级用解析库的取键方法验证。CODESYS字符串查找法在这里反而快,直接FIND(sRcvPayload, '"temperature"')定位后截取数值段,联调期间临时用,稳定后再固化到正式封装里。

5.4 排查3:无线设备掉线但PLC不知道

现象:Zigbee温湿度传感器电池耗尽或被挪走,PLC里温度还停留在最后值,控制系统拿着一个假温度在跑。

原因:Zigbee设备是无线低功耗,休眠或离网后不会主动通知MQTT,PLC没有收到新消息,但也不可能凭空知道设备已经失联。

解决:把Zigbee2MQTT的availability机制用起来。Zigbee2MQTT默认会发布zigbee2mqtt/<设备名>/availability,值为online或offline。PLC订阅这个主题,在解析逻辑里把offline直接映射成设备故障变量,并在HMI上亮黄色提醒。再配合last_seen字段做超时判断,比如超过10分钟没有温度上报就置位"传感器超时"标志,比只信last value安全得多。

5.5 排查4:MQTT任务拖慢扫描周期

现象:接上Zigbee2MQTT后,PLC任务周期从10ms变成50ms,甚至出现任务超时报警。

原因:常见的错误做法是把JSON解析和字符串拼接直接放在高频任务里,每个扫描周期都执行大量字符串查找操作,而ST的字符串函数本身就比较重,消息多的时候直接把任务周期拉爆。

解决:把MQTT多收多解析的活放到独立任务或低优先级任务里,高频控制任务里只放一个变量翻转标志,等收到完整消息后再联动。如果CPU实在紧张,把JSON解析库换成字符串查找法,解析耗时能降不少。另一个常见做法是消息节流:Zigbee2MQTT里对温度这种慢变量设置上报最小间隔,比如5分钟一条,避免设备频繁上报压垮PLC。

6. 收尾验证:抓包、离线缓存与重连参数的进阶调试

6.1 用抓包确认协议层问题

应用层排查找不到原因时,别在代码里瞎试,先抓包看协议。用Wireshark抓broker所在网卡,过滤条件mqtt || tcp.port==1883,重点看三件事:连接报文里的clientId对不对、CONNACK返回码是什么、掉线前有没有发DISCONNECT。

有一次设备不断闪断,代码层怎么查都正常,抓包发现PLC在反复CONNECT,原来broker配置了最大连接数限制,新连接踢掉了旧连接。这种问题看代码永远看不出来,只有抓包能看到完整的连接时序。抓包习惯了之后,MQTT调试效率比纯看日志高很多。

6.2 离线缓存与重连参数调优

如果担心PLC短暂掉线导致关键数据丢失,有两招。第一招是broker侧用RETAIN消息,PLC发布时bRetain := TRUE,broker会保留最新值,重连后新订阅者立刻拿到当前值,适合状态类数据。第二招是PLC侧缓存,把断电或断网期间要补发的数据放进环形缓冲区,重连后按顺序补发,适合能源计量这类需要连续性的数据。

重连参数上,注意三个数:keepalive设30到60秒,重连间隔从5秒开始按指数退避,最大不超过60秒,避免broker被打爆;cleanSession通常设FALSE,让broker保留订阅关系和队列,但一定要搭配遗嘱消息,否则broker堆积大量僵尸会话。把这三组数按现场网络情况调一遍,比盲目改代码管用得多。

我最早做这套的时候,把所有消息都设成QoS1,结果累积计数在broker重投时翻倍,后来给自己立了规矩:采集用QoS0,命令用QoS1并在载荷里带消息序号做去重。这个习惯一直保留到现在。希望帮到你。

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

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

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

立即咨询