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 为什么是“网关+MQTT+Node.js”而不是传统SCADA
传统SCADA的架构是:PLC通过串口或以太网连到上位机,上位机装组态软件(如WinCC、组态王),组态软件负责画面、报警、历史存储。这套架构的问题在于:第一,组态软件授权贵,按点数收费,一个项目下来光软件就占了大半预算;第二,Web发布通常要额外买模块,而且界面老旧,手机适配差;第三,扩展性差,想接个新设备就得重新组态。
换成“网关+MQTT+Node.js”之后,成本结构完全变了。工业智能网关负责协议转换,把西门子S7、三菱MC、台达Modbus等协议统一转成MQTT;MQTT服务器负责消息中转,轻量、低带宽、支持断线重连;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对应40001,D100对应40101,M0对应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/temperature,QoS选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装Mosquitto:
yum 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.x:
curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - yum install nodejs -y node -v如果下载慢,可以先用nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc nvm install 22 nvm use 22怎么查看有没有安装Node.js?which 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接A,B接B,屏蔽层单端接地。
- 网关的网口通过网线连到交换机,交换机和服务器在同一个局域网。
- 服务器可以是一台工控机,也可以是一台云服务器(如果现场有公网出口)。
网络配置:PLC的Modbus站号设为1,波特率9600,8数据位,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主题 |
|---|---|---|---|---|
| 温度设定值 | 40101 | Float | 1000ms | szplant/workshop1/plc01/temp_set |
| 当前温度 | 40103 | Float | 1000ms | szplant/workshop1/plc01/temp_pv |
| 加热输出 | 40105 | Float | 1000ms | szplant/workshop1/plc01/heat_out |
这里地址40101对应D100,因为Modbus地址从40001开始,D0对应40001,D100对应40101。Float类型占两个寄存器,所以下一个点从40103开始。
配置完成后,用MQTT Explorer订阅szplant/#,应该能看到三条消息每秒刷新一次。如果看不到,检查网关的采集状态是否“在线”,以及PLC的RS485指示灯是否闪烁。
4.3 Node.js订阅端代码:从MQTT到WebSocket
Node.js端的核心代码分三块:连接MQTT、订阅主题、推送WebSocket。先初始化项目:
mkdir web-scada cd web-scada npm init -y npm install mqtt ws express然后写server.js:
const 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页面:实时曲线与数据卡片
前端页面用原生HTML+JavaScript+Chart.js,不依赖React或Vue,降低门槛。public/index.html:
<!DOCTYPE html> <html> <head> <title>Web SCADA</title> <script src="https://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 class="card"><div class="label">温度设定值</div><div class="value" id="temp_set">--</div></div> <div class="card"><div class="label">当前温度</div><div class="value" id="temp_pv">--</div></div> <div class="card"><div class="label">加热输出</div><div class="value" id="heat_out">--</div></div> </div> <div id="chart-container"><canvas id="tempChart"></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] Description=Web SCADA Node.js After=network.target [Service] ExecStart=/usr/bin/node /opt/web-scada/server.js Restart=always User=root [Install] WantedBy=multi-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"; }如果是HTTPS,WebSocket要用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-port=1883/tcp --permanent别忘了。
这套方案我从去年开始用在三个项目上,最长的已经稳定跑了8个月。中间换过一次网关(因为原来的不支持数据缓存),换过一次服务器(从Windows换到Linux),但架构没变。如果你也在找PLC上云的低成本方案,不妨从这套开始试。