ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PLC数据上云实战:网关+MQTT+Node.js构建Web SCADA监控

PLC数据上云实战:网关+MQTT+Node.js构建Web SCADA监控 1. 从车间到浏览器这套方案到底在解决什么问题车间里一台台达PLC跑了三年温度PID参数调了无数遍操作工还是得站在电柜前面盯着触摸屏。老板想在中控室的大屏上看实时数据还想用手机查历史曲线更想在下班后收到微信告警。传统SCADA动辄几万块一套组态软件授权按点数收费Web发布还要额外买模块。这套方案的核心思路很直接用一台工业智能网关把PLC的数据读出来通过MQTT协议发到自建的服务器上再用Node.js写一个Web SCADA页面浏览器打开就能看。说白了就是把“PLC—触摸屏—人眼”这条链路换成“PLC—网关—MQTT—Node.js—浏览器/手机”这条链路。前者是封闭的、本地的、昂贵的后者是开放的、远程的、几乎零成本的。适合谁看做非标自动化项目的电气工程师、想给老设备加远程监控的运维人员、做PLC毕业设计的学生以及任何想搞明白“工业数据怎么上云”的开发者。不需要你精通前端框架也不需要你懂Kubernetes只要会基本的PLC编程和一点点JavaScript就能跟着走完。我见过太多项目卡在“数据出不来”这一步。PLC有网口但协议不开放网关买回来了但MQTT配置一头雾水Node.js装好了但不知道怎么写订阅代码。这篇文章就是把这些坑一个个填平从硬件选型到代码落地从MQTT主题设计到Web页面刷新全部讲透。2. 整体架构设计与选型逻辑2.1 为什么是“网关MQTTNode.js”而不是传统SCADA传统SCADA的架构是PLC通过串口或以太网连到上位机上位机装组态软件如WinCC、组态王组态软件负责画面、报警、历史存储。这套架构的问题在于第一组态软件授权贵按点数收费一个项目下来光软件就占了大半预算第二Web发布通常要额外买模块而且界面老旧手机适配差第三扩展性差想接个新设备就得重新组态。换成“网关MQTTNode.js”之后成本结构完全变了。工业智能网关负责协议转换把西门子S7、三菱MC、台达Modbus等协议统一转成MQTTMQTT服务器负责消息中转轻量、低带宽、支持断线重连Node.js负责订阅消息并推送到Web页面。整套下来硬件成本可能只要几百块软件全开源。更重要的是MQTT的发布/订阅模型天然适合多客户端场景——中控室大屏、手机、平板可以同时订阅同一个主题互不干扰。注意不是所有场景都适合这套方案。如果项目要求毫秒级响应、硬实时控制还是老老实实用传统SCADA或PLC本地逻辑。这套方案定位是“监控级”刷新周期在1秒左右适合温度、液位、流量这类慢变量。2.2 工业智能网关的选型要点网关是整套方案的咽喉。选错了后面全是坑。我踩过的坑包括网关只支持Modbus RTU不支持Modbus TCP、MQTT主题不能自定义、断网后数据直接丢不缓存。选型时盯住这几个参数协议支持至少支持Modbus TCP/RTU、西门子S7、三菱MC。如果现场有台达PLC确认网关是否支持台达的Modbus地址映射。汇川PLC通常走Modbus TCP问题不大。MQTT版本支持3.1.1或5.0。3.1.1足够用5.0支持会话过期、消息过期等高级特性但服务端也要对应支持。数据缓存断网后能缓存至少1小时数据恢复后自动补传。这个功能在车间网络不稳定的场景下救命。配置方式Web配置界面最好串口配置太原始。有些网关还支持Node-RED灵活性更高。工作温度-20~70℃是基本要求电柜里夏天能到50℃以上。我实测过某款国产网关MQTT发布周期设成500ms时CPU占用率飙到80%后来改成1秒就稳了。所以选型时别只看参数表实际带载测试一下。2.3 MQTT主题设计别等设备多了再改MQTT主题是数据的“门牌号”。设计不好后期设备一多就乱套。推荐的结构是工厂名/车间名/设备名/数据类型比如szplant/workshop1/plc01/temperature szplant/workshop1/plc01/status szplant/workshop1/plc01/alarm这样设计的好处是可以用通配符订阅。比如Node.js端订阅szplant/workshop1//temperature就能拿到车间1所有设备的温度。如果一开始就用plc01_temp这种扁平结构后期想按车间筛选就难了。提示主题层级不要超过5层否则可读性下降。另外避免用中文和特殊字符虽然MQTT协议支持UTF-8但有些网关的配置界面会出问题。2.4 Node.js的角色定位Node.js在这套方案里干三件事第一作为MQTT客户端订阅网关发布的数据第二把数据存到内存或轻量数据库如SQLite、InfluxDB第三通过WebSocket推送给浏览器。为什么不用Python因为Node.js的事件驱动模型天然适合处理大量并发连接而且npm生态里有成熟的MQTT.js和Socket.io库。为什么不用Java因为Node.js启动快、代码量少一个200行的脚本就能跑起来。Node.js的版本选择上建议用22.x LTS。我在CentOS 7.9上装过Node.js 18遇到glibc版本不兼容的问题后来换了Node.js 22.12才解决。Windows上直接去官网下载安装包一路下一步就行。Linux上推荐用nvm管理版本避免权限问题。3. 核心细节解析与实操要点3.1 PLC侧准备以台达和西门子为例台达PLC下载程序通常用ISPSoft或WPLSoft。以DVP系列为例先确认PLC的通讯口设置如果是RS485波特率默认9600站号1。在ISPSoft里设置好通讯参数后点击“通讯设置”搜索设备找到后下载程序。注意台达PLC的Modbus地址映射D0对应40001D100对应40101M0对应00001。网关配置时要填对地址否则读上来全是0。西门子S7-200 SMART用STEP 7-Micro/WIN SMART软件。常见问题是“连接PLC后搜索找不到CPU”。我遇到过好几次原因通常是第一网段不对PLC默认IP是192.168.2.1电脑要改成同网段第二防火墙拦了第三网线插在了非管理型交换机上导致广播风暴。解决办法是手动添加IP地址在“通讯”对话框里直接输入PLC的IP跳过搜索步骤。如果还不行把电脑防火墙临时关掉试试。注意S7-200 SMART的Modbus TCP服务器功能需要额外配置。在“系统块”里启用Modbus TCP服务器设置端口502然后分配保持寄存器地址。这一步不做网关读不到数据。3.2 网关配置从Modbus到MQTT的映射网关配置的核心是“采集点表”和“MQTT发布点表”的对应关系。以某款支持Node-RED的网关为例配置流程如下在“采集”页面添加设备选择Modbus TCP填PLC的IP和端口502。添加采集点比如“温度”地址40001数据类型Float采集周期1000ms。在“MQTT”页面配置服务器地址、端口、用户名密码。添加发布点把“温度”映射到主题szplant/workshop1/plc01/temperatureQoS选1。启用数据缓存设置缓存条数10000。这里有个细节数据类型要选对。PLC里的温度值通常是32位浮点数占两个寄存器。如果网关里选成16位整数读上来就是乱码。另外字节序也要注意有些PLC是大端有些是小端网关里通常有“字节交换”选项试一下就知道。3.3 MQTT服务器搭建Windows和Linux两条路Windows上搭建MQTT服务器最简单的是用EMQX或Mosquitto。Mosquitto有Windows安装包下载后一路下一步然后在配置文件里加一行listener 1883和allow_anonymous true重启服务就行。EMQX功能更全有Web管理界面适合需要看连接状态和主题监控的场景。Linux上以CentOS 7.9为例用yum装Mosquittoyum install epel-release -y yum install mosquitto mosquitto-clients -y systemctl enable mosquitto systemctl start mosquitto如果要支持WebSocket浏览器直接连MQTT需要在配置文件里加listener 9001 protocol websockets麒麟V10 ARM环境下离线安装Mosquitto比较麻烦需要先下载ARM64的rpm包然后手动解决依赖。我试过用Docker跑EMQX一条命令搞定docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 18083:18083 emqx/emqx:latest提示生产环境一定要改默认密码关闭匿名访问。我见过一个项目因为MQTT服务器没设密码被外部扫描到后疯狂发垃圾消息把网关的缓存都撑爆了。3.4 Node.js环境安装避坑指南Windows上安装Node.js最简单去官网下载LTS版本的.msi文件双击安装勾选“Add to PATH”。装完后打开cmd输入node -v和npm -v能看到版本号就成功了。如果提示“不是内部或外部命令”说明PATH没配好手动把Node.js安装目录加到系统环境变量里。CentOS 7.9上安装Node.js 22.xcurl -fsSL https://rpm.nodesource.com/setup_22.x | bash - yum install nodejs -y node -v如果下载慢可以先用nvmcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc nvm install 22 nvm use 22怎么查看有没有安装Node.jswhich node和node -v两条命令就够了。如果显示“command not found”说明没装或者PATH不对。3.5 MQTT客户端工具调试必备调试MQTT离不开客户端工具。Windows上推荐MQTT Explorer图形界面能看主题树、消息历史、连接状态。下载后填服务器IP和端口点连接就能看到网关发布的所有主题。Linux上可以用mosquitto_sub命令行mosquitto_sub -h 192.168.1.100 -t szplant/# -v这条命令会订阅所有以szplant/开头的主题并打印主题名和消息内容。调试阶段用这个命令确认网关有没有正常发布数据比直接写Node.js代码快得多。4. 实操过程与核心环节实现4.1 从零搭建硬件连接与网络配置假设现场有一台台达DVP-12SE PLC一个工业智能网关一台运行Node.js的服务器。硬件连接如下PLC的RS485口通过屏蔽双绞线连到网关的RS485口。A接AB接B屏蔽层单端接地。网关的网口通过网线连到交换机交换机和服务器在同一个局域网。服务器可以是一台工控机也可以是一台云服务器如果现场有公网出口。网络配置PLC的Modbus站号设为1波特率96008数据位1停止位无校验。网关的RS485参数要和PLC完全一致。网关的IP设为192.168.1.50服务器IP设为192.168.1.100。在网关的Web界面里采集设备填PLC的站号和地址MQTT服务器填192.168.1.100:1883。注意RS485接线一定要用屏蔽双绞线且屏蔽层只能一端接地。我见过一个项目因为两端都接地形成地环流通讯时好时坏折腾了一整天。4.2 网关采集点表配置以温度PID为例温度PID控制是PLC里的经典场景。假设PLC里D100是温度设定值D101是当前温度D102是加热输出百分比。网关采集点表配置如下采集点名称Modbus地址数据类型采集周期MQTT主题温度设定值40101Float1000msszplant/workshop1/plc01/temp_set当前温度40103Float1000msszplant/workshop1/plc01/temp_pv加热输出40105Float1000msszplant/workshop1/plc01/heat_out这里地址40101对应D100因为Modbus地址从40001开始D0对应40001D100对应40101。Float类型占两个寄存器所以下一个点从40103开始。配置完成后用MQTT Explorer订阅szplant/#应该能看到三条消息每秒刷新一次。如果看不到检查网关的采集状态是否“在线”以及PLC的RS485指示灯是否闪烁。4.3 Node.js订阅端代码从MQTT到WebSocketNode.js端的核心代码分三块连接MQTT、订阅主题、推送WebSocket。先初始化项目mkdir web-scada cd web-scada npm init -y npm install mqtt ws express然后写server.jsconst mqtt require(mqtt); const WebSocket require(ws); const express require(express); const path require(path); const app express(); app.use(express.static(path.join(__dirname, public))); const server app.listen(3000, () { console.log(Web server running on port 3000); }); const wss new WebSocket.Server({ server }); const mqttClient mqtt.connect(mqtt://192.168.1.100:1883, { username: scada, password: your_password, clientId: node_scada_ Math.random().toString(16).substr(2, 8) }); mqttClient.on(connect, () { console.log(MQTT connected); mqttClient.subscribe(szplant/workshop1/plc01/#, { qos: 1 }); }); mqttClient.on(message, (topic, message) { const payload { topic: topic, value: message.toString(), timestamp: Date.now() }; wss.clients.forEach(client { if (client.readyState WebSocket.OPEN) { client.send(JSON.stringify(payload)); } }); }); mqttClient.on(error, (err) { console.error(MQTT error:, err); });这段代码做了三件事启动一个Express服务器托管静态页面创建一个WebSocket服务器连接MQTT并订阅主题。收到消息后把主题、值、时间戳打包成JSON广播给所有WebSocket客户端。提示clientId一定要随机生成否则多个Node.js实例用同一个ID会互相踢下线。我踩过这个坑调试时开了两个终端结果只有一个能收到消息。4.4 Web SCADA页面实时曲线与数据卡片前端页面用原生HTMLJavaScriptChart.js不依赖React或Vue降低门槛。public/index.html!DOCTYPE html html head titleWeb SCADA/title script srchttps://cdn.jsdelivr.net/npm/chart.js/script style body { font-family: Arial; margin: 20px; background: #1a1a2e; color: #eee; } .card { background: #16213e; border-radius: 8px; padding: 20px; margin: 10px; display: inline-block; min-width: 200px; } .value { font-size: 36px; font-weight: bold; color: #0f3460; } .label { font-size: 14px; color: #aaa; } #chart-container { width: 80%; margin: 20px auto; } /style /head body h1车间1 - PLC01 监控/h1 div div classcarddiv classlabel温度设定值/divdiv classvalue idtemp_set--/div/div div classcarddiv classlabel当前温度/divdiv classvalue idtemp_pv--/div/div div classcarddiv classlabel加热输出/divdiv classvalue idheat_out--/div/div /div div idchart-containercanvas idtempChart/canvas/div script const ws new WebSocket(ws:// location.host); const tempData { labels: [], datasets: [{ label: 当前温度, data: [], borderColor: #e94560, fill: false }] }; const ctx document.getElementById(tempChart).getContext(2d); const chart new Chart(ctx, { type: line, data: tempData, options: { responsive: true, animation: false } }); ws.onmessage (event) { const msg JSON.parse(event.data); const topicParts msg.topic.split(/); const key topicParts[topicParts.length - 1]; const el document.getElementById(key); if (el) el.textContent parseFloat(msg.value).toFixed(1); if (key temp_pv) { const now new Date().toLocaleTimeString(); tempData.labels.push(now); tempData.datasets[0].data.push(parseFloat(msg.value)); if (tempData.labels.length 60) { tempData.labels.shift(); tempData.datasets[0].data.shift(); } chart.update(); } }; /script /body /html这个页面做了两件事用卡片显示实时值用Chart.js画最近60秒的温度曲线。animation: false很重要否则每次更新图表都会闪一下看起来像卡顿。4.5 部署与开机自启Windows上用pm2管理Node.js进程npm install -g pm2 pm2 start server.js --name web-scada pm2 save pm2 startupLinux上同样用pm2或者写systemd服务[Unit] DescriptionWeb SCADA Node.js Afternetwork.target [Service] ExecStart/usr/bin/node /opt/web-scada/server.js Restartalways Userroot [Install] WantedBymulti-user.target保存为/etc/systemd/system/web-scada.service然后systemctl enable web-scada。5. 常见问题与排查技巧实录5.1 数据不上报从PLC到MQTT的逐段排查数据不上报是最常见的问题。我的排查顺序是先看PLC的通讯灯再看网关的采集状态再看MQTT服务器最后看Node.js日志。现象可能原因排查方法PLC通讯灯不闪RS485接线错误检查A/B线是否接反用万用表测电压网关采集状态离线站号或波特率不对确认PLC和网关的通讯参数一致MQTT Explorer看不到消息主题配置错误检查网关的MQTT发布点是否启用Node.js收不到消息订阅主题不匹配用mosquitto_sub测试订阅Web页面不刷新WebSocket连接失败检查浏览器控制台报错我遇到过一次网关采集正常但MQTT不发的情况最后发现是网关的MQTT客户端ID和另一个设备冲突了。改成随机ID后解决。5.2 温度PID波动大先查采集再查参数热词里有“plc温度pid波动温差大如何调节”这个问题在Web SCADA上看得最清楚。先看曲线如果曲线是锯齿状说明PID参数太激进如果曲线是正弦波说明积分时间太长如果曲线有规律跳变可能是采集周期和PID周期不同步。我的经验是先把采集周期设成PID周期的1/5到1/10。比如PID周期是100ms采集周期设成1秒就够了。然后看曲线调参数比例带先设大一点积分时间从大到小调微分先关掉。如果还波动检查热电偶的冷端补偿和滤波。注意Web SCADA的曲线有延迟不能用来做PID整定。整定还是在PLC编程软件里看实时曲线更准。5.3 Node.js内存泄漏WebSocket客户端没清理Node.js跑几天后内存暴涨通常是WebSocket客户端断开后没清理。wss.clients是一个Set客户端断开时会自动移除但如果客户端异常断开比如拔网线Set里会残留死连接。解决办法是加心跳检测setInterval(() { wss.clients.forEach(client { if (client.isAlive false) return client.terminate(); client.isAlive false; client.ping(); }); }, 30000); wss.on(connection, (ws) { ws.isAlive true; ws.on(pong, () { ws.isAlive true; }); });这段代码每30秒ping一次客户端没响应的直接terminate。加上之后内存稳定在50MB左右。5.4 跨域和防火墙浏览器连不上WebSocket浏览器打开页面后控制台报“WebSocket connection failed”。常见原因第一服务器防火墙没开3000端口第二Nginx反向代理没配置WebSocket升级头第三浏览器用了HTTPS但WebSocket是ws://。Nginx配置location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }如果是HTTPSWebSocket要用wss://并且证书要有效。5.5 网关断线重连数据缓存与补传车间网络不稳定网关断线是常态。好的网关会缓存数据恢复后补传。但补传时如果QoS设成0消息可能丢设成1消息会重复。我的做法是在Node.js端用timestamp去重收到重复时间戳的消息直接丢弃。另外补传的数据不要直接推给WebSocket否则曲线会突然跳变。可以先存到数据库Web页面查询历史时再合并。6. 进阶扩展与个人经验6.1 从单台PLC到多车间主题命名与权限隔离设备多了之后主题命名要升级。推荐企业名/工厂名/车间名/产线名/设备名/数据类型比如acme/sz/workshop1/line1/plc01/temp。MQTT服务器可以配置ACL让不同用户只能订阅自己车间的主题。EMQX的ACL规则{allow, {user, workshop1_user}, subscribe, [acme/sz/workshop1/#]}.这样即使密码泄露也影响不到其他车间。6.2 历史数据存储SQLite够用InfluxDB更好Node.js端可以用SQLite存历史数据零配置单文件。但数据量大了之后查询慢。InfluxDB是时序数据库适合存传感器数据压缩率高查询快。我用InfluxDB存了半年的温度数据总共不到100MB。写入代码const { InfluxDB, Point } require(influxdata/influxdb-client); const influx new InfluxDB({ url: http://localhost:8086, token: your_token }); const writeApi influx.getWriteApi(myorg, scada); mqttClient.on(message, (topic, message) { const point new Point(sensor) .tag(topic, topic) .floatField(value, parseFloat(message.toString())); writeApi.writePoint(point); });6.3 AI辅助PLC代码生成现状与边界热词里有“ai plc代码生成”我试过用大模型生成梯形图逻辑。结论是简单的启保停、定时器逻辑可以生成但复杂的PID、运动控制、通讯协议还是得手写。AI生成的代码可以作为参考但不能直接下载到PLC。另外博途PLC与模拟屏不兼容的问题通常是通讯协议不匹配和AI无关。6.4 个人踩坑记录那些文档里不会写的事第一网关的RS485口和PLC的RS485口之间不要接终端电阻除非距离超过100米。我接过一次通讯直接挂了。第二MQTT主题不要用#和作为普通字符它们是通配符。我见过一个项目主题里带了#结果订阅时匹配到一堆无关消息。第三Node.js的mqtt库默认QoS是0要显式设成1。我调试时发现消息偶尔丢查了半天才发现是QoS问题。第四Web页面的Chart.js数据点不要超过200个否则浏览器卡顿。我的做法是保留最近60个点历史数据用另一个页面查。第五CentOS 7.9的防火墙默认拦1883端口firewall-cmd --add-port1883/tcp --permanent别忘了。这套方案我从去年开始用在三个项目上最长的已经稳定跑了8个月。中间换过一次网关因为原来的不支持数据缓存换过一次服务器从Windows换到Linux但架构没变。如果你也在找PLC上云的低成本方案不妨从这套开始试。
返回列表