工业现场的数据上云,最头疼的往往不是写代码,而是"最后一公里"的打通。PLC 里跑着稳定的梯形图逻辑,但要把这些寄存器值实时送到浏览器里画曲线、做报警,中间隔着协议转换、消息队列、后端服务好几道坎。我做过几个从零搭建的产线监控项目,踩过的坑从网关选型一直延伸到 Node.js 的事件循环阻塞,今天就把这套"PLC → 工业智能网关 → MQTT → Node.js → Web SCADA"的链路完整拆一遍。不管你是刚接触工业物联网的电气工程师,还是想切入工控领域的后端开发者,这套方案都能直接抄作业落地。
1. 为什么要在 PLC 和 Web SCADA 之间插入 MQTT 这一层
1.1 传统上位机方案的三个死结
很多人第一反应是让 Web 页面直接读 PLC,比如用 Modbus TCP 从浏览器发请求。这条路我试过,走不通。第一个死结是协议不匹配:浏览器原生只认 HTTP 和 WebSocket,而 PLC 开口就是 Modbus TCP、S7 协议、EtherNet/IP 这些工业协议,中间必须有人翻译。第二个死结是轮询效率:一个车间 20 台 PLC,每台 200 个点位,如果前端每秒轮询一次,就是 4000 次请求,PLC 的通信资源会被瞬间打满,严重的直接导致 PLC 扫描周期抖动,影响控制逻辑。第三个死结是网络拓扑:Web 服务器通常部署在办公网或云端,而 PLC 在车间内网,两者之间隔着防火墙和 NAT,直接互访几乎不可能。
MQTT 这一层恰好把这三个问题一次性解决。它采用发布/订阅模型,网关作为客户端把 PLC 数据发布到 Broker,Node.js 后端订阅自己关心的主题,前端再通过 WebSocket 从后端拿数据。整个链路是异步的、解耦的,PLC 不需要知道谁在消费它的数据,前端也不需要知道数据具体来自哪台设备。
1.2 发布订阅模型在工控场景的天然契合
工控数据有个特点:一变多、多对一、一对多同时存在。一个温度值可能同时被趋势图、报警系统、MES 系统、大屏看板消费;反过来,一个控制指令可能来自本地按钮、远程 HMI、手机 App 多个源头。如果用传统的请求/响应模型,每增加一个消费者就要改一次 PLC 程序或网关配置。而 MQTT 的 Topic 机制让生产者只管发,消费者按需订阅,新增一个看板只需要订阅一个新 Topic,对现有系统零侵入。
Topic 设计上我习惯用factory/{车间}/{设备类型}/{设备编号}/{点位}这种层级结构,比如factory/assembly/plc/line01/temperature。这样后端可以用通配符factory/assembly/plc/+/temperature一次性订阅所有产线的温度,也可以用factory/assembly/plc/line01/#订阅单台设备的全部数据。这种灵活性在后期扩展时价值巨大。
1.3 QoS 等级怎么选才不丢数据又不拖垮网络
MQTT 有三个 QoS 等级,工控场景选错了要么丢数据要么网络爆炸。QoS 0 是"最多一次",发出去就不管了,适合高频的实时曲线数据,丢一两个点对趋势判断没影响。QoS 1 是"至少一次",有确认重传机制,适合报警、状态变化这类不能丢的消息,但要注意它可能重复。QoS 2 是"恰好一次",握手四次,开销最大,一般只在计费、批次追溯这种绝对不能出错的场景用。
我的经验是:实时模拟量用 QoS 0,开关量和报警用 QoS 1,配方和批次数据用 QoS 2。曾经有个项目为了"保险"把所有数据都设成 QoS 2,结果 500 个点位每秒上报,Broker 的 CPU 直接飙到 90%,后来降到 QoS 0 才恢复正常。数据可靠性不是靠 QoS 堆出来的,而是靠合理的架构设计。
2. 工业智能网关的选型与配置实操
2.1 网关到底解决了什么问题
工业智能网关本质是一台协议翻译 + 边缘计算的小型工控机。它向下通过串口或网口连接 PLC,支持 Modbus、S7、三菱 MC、欧姆龙 FINS 等主流协议;向上通过 MQTT、HTTP、OPC UA 把数据送到云端或服务器。中间还能做数据过滤、单位换算、断线缓存、边缘报警这些事。
为什么不用工控机装个软件自己采?因为网关是无风扇、宽温、导轨安装的工业级设备,能扛住车间的粉尘、震动和电磁干扰,而且配置好之后基本不用维护。工控机在车间环境里跑,硬盘和风扇是最先坏的部件,我见过太多因为工控机死机导致数据中断的案例。
2.2 网关与 PLC 的物理连接和协议配置
以最常见的西门子 S7-1200 为例,网关和 PLC 通过网线连接,需要配置几个关键参数。PLC 侧要在 TIA Portal 里确认:IP 地址(比如 192.168.1.10)、子网掩码、机架号和槽号(S7-1200 通常是 0 和 1)。如果用的是 S7-300/400,还要注意 MPI 或 Profibus 转以太网的配置。
网关侧配置时,最容易踩的坑是端口号。S7 协议默认用 102 端口,但有些现场为了"安全"改过端口,这时候网关就连不上。另外,如果 PLC 开启了"仅允许 PUT/GET 通信"的选项没勾选,网关读取数据会直接被拒绝。这个选项在 TIA Portal 的 PLC 属性 → 保护 → 连接机制里,很多新手会漏掉。
对于汇川、信捷这类国产 PLC,Modbus TCP 是最通用的选择。配置时要注意寄存器地址的偏移:PLC 手册上写的 40001 对应 Modbus 地址 0,40002 对应 1,这个"减一"规则如果搞错,读出来的数据全是错位的。我一般会先用 Modbus Poll 这类工具单独测通,再接到网关上。
2.3 数据点表的设计与批量导入
网关配置最耗时的环节是点表录入。一台设备几百个点位,一个个手填会疯掉。我的做法是先在 Excel 里整理好点表,包含这几列:点位名称、PLC 地址、数据类型、单位、缩放系数、死区值。然后导出成 CSV,用网关自带的批量导入功能一次性导入。
死区值(Deadband)这个参数特别重要。比如温度值,如果每次变化 0.01 度都上报,一天能产生几十万条消息。设置死区为 0.5 度,只有变化超过这个值才上报,数据量能降 90% 以上,而且对监控趋势毫无影响。开关量则不需要死区,状态一变就报。
| 参数 | 说明 | 典型值 |
|---|---|---|
| 采集周期 | 网关轮询 PLC 的间隔 | 模拟量 1000ms,开关量 200ms |
| 死区值 | 变化超过此值才上报 | 温度 0.5,压力 0.01 |
| 缩放系数 | 原始值到工程值的换算 | 根据量程计算 |
| 数据类型 | 寄存器解析方式 | INT16、FLOAT32、BOOL |
2.4 断线缓存与边缘计算的价值
车间网络不是永远稳定的,交换机重启、网线松动都会导致网关和 Broker 断连。好的网关有断线缓存功能,把断连期间的数据存在本地 Flash 里,恢复后按时间顺序补发。这个功能在追溯场景里是刚需,否则数据出现空洞,事后分析根本没法做。
边缘计算则是把一些简单逻辑下沉到网关。比如温度超过 80 度直接触发本地报警输出,不用等云端判断再下发指令,响应时间从秒级降到毫秒级。我通常会把越限报警、变化率报警、设备联动这几类逻辑放在网关侧,云端只做展示和存储。
3. MQTT Broker 的搭建与主题规划
3.1 Broker 选型:EMQX、Mosquitto 还是自己写
Broker 是整套系统的消息中枢,选型要看规模。Mosquitto轻量,单机跑几千个连接没问题,适合小项目或测试环境,Windows 下解压就能用。EMQX功能全,支持集群、规则引擎、数据桥接,几万到百万级连接都能扛,生产环境我基本都用它。至于自己用 Node.js 写一个 Broker,除非是学习目的,否则不建议,MQTT 协议的会话管理、遗嘱消息、保留消息这些细节太多,造轮子性价比极低。
部署方式上,测试环境我直接在 Windows 上把 EMQX 的 zip 包解压,用emqx start启动。生产环境用 Docker 或直接装在 Linux 服务器上,配合 systemd 做开机自启。这里有个细节:MQTT 默认端口 1883 是明文传输,8883 是 TLS 加密。如果数据要过公网,必须上 8883 并配置证书,否则数据裸奔。
3.2 主题层级设计:从混乱到有序
Topic 设计是 MQTT 落地最容易埋雷的地方。我见过有人用data1、data2这种毫无意义的主题名,半年后自己都忘了哪个是哪个。规范的 Topic 应该像文件路径一样有层次,我推荐的结构是:
{企业}/{厂区}/{车间}/{设备类型}/{设备编号}/{点位类型}/{点位名}比如acme/shanghai/assembly/plc/line01/analog/temperature。这样设计的好处是权限控制粒度细,可以给不同角色分配不同层级的订阅权限。运维只能看acme/shanghai/#,而设备厂商只能访问自己设备的主题。
还要注意主题不要以/开头,虽然技术上允许,但会导致主题层级多一层空字符串,通配符匹配时容易出意外。另外,避免使用#和+作为主题名的一部分,这两个是通配符,用在发布主题里会造成混乱。
3.3 保留消息与遗嘱消息的实战用法
保留消息(Retained Message)是个被低估的功能。它让 Broker 记住某个主题的最后一条消息,新订阅者一上来就能立刻收到当前值,不用干等下一次上报。对于设备状态、当前温度这类"状态型"数据,我全部设为保留消息。这样前端页面刷新后,状态栏立刻就有值,体验好很多。
遗嘱消息(Last Will)则是设备掉线时的"遗言"。网关连接 Broker 时预先注册一条遗嘱,比如factory/line01/status内容为offline。一旦网关异常断开,Broker 自动发布这条消息,后端立刻知道设备离线了。配合保留消息,设备在线状态就能被准确追踪。这两个功能配合使用,基本能替代大部分心跳检测逻辑。
3.4 认证与权限:别让任何人都能发指令
生产环境的 Broker 绝对不能匿名访问。EMQX 支持用户名密码、JWT、客户端证书多种认证方式。小项目用用户名密码就够了,在 Broker 配置里建几个账号,网关用gateway账号,后端用backend账号,前端用frontend账号。
权限控制用 ACL(访问控制列表)实现。核心原则是最小权限:网关只能发布自己设备的主题,不能订阅;后端可以订阅所有主题并发布控制指令;前端只能订阅展示相关的主题,绝对不能有发布权限。我见过一个项目因为前端能发布主题,被人用浏览器控制台发了个停机指令,产线直接停了。这种事故一次就够记一辈子。
4. Node.js 后端服务的搭建与 MQTT 集成
4.1 Node.js 环境准备与版本选择
Node.js 在工控后端场景的优势是异步 IO 模型,处理大量并发 MQTT 消息时资源占用低。版本选择上,生产环境我建议用LTS 版本,比如 18.x 或 20.x,稳定性和生态兼容性最好。22.x 虽然新,但有些老库还没跟上,除非你确认所有依赖都兼容,否则别在生产环境冒险。
Windows 下安装直接去官网下载 msi 安装包,一路下一步即可。Linux 下推荐用 nvm 管理多版本,方便切换。安装完用node -v和npm -v验证。有个细节:CentOS 7.9 自带的 glibc 版本较老,装高版本 Node.js 可能报错,这时候要么升级系统,要么选 Node.js 16 这种对老系统友好的版本。
4.2 用 mqtt.js 订阅网关数据
Node.js 侧用mqtt这个库连接 Broker,它是目前最成熟的 MQTT 客户端实现。核心代码结构是这样的:
const mqtt = require('mqtt'); const client = mqtt.connect('mqtt://broker-ip:1883', { clientId: 'backend-service-' + Math.random().toString(16).slice(2, 8), username: 'backend', password: 'your-password', clean: false, // 保留会话,断线重连后能收到离线消息 reconnectPeriod: 3000, // 3秒重连一次 keepalive: 60 // 60秒心跳 }); client.on('connect', () => { console.log('Broker connected'); client.subscribe('factory/+/plc/+/analog/#', { qos: 1 }); client.subscribe('factory/+/plc/+/digital/#', { qos: 1 }); }); client.on('message', (topic, payload) => { const value = payload.toString(); // 解析 topic 提取设备信息,写入时序数据库 handleData(topic, value); });这里有几个关键点。clientId 必须唯一,如果两个服务用同一个 clientId,会互相踢下线,表现为连接反复断开。我习惯在 clientId 后面加随机后缀。clean 设为 false让 Broker 保留会话,服务重启后能收到断连期间的消息(QoS 1/2 的消息)。reconnectPeriod别设太短,否则 Broker 压力大,3 到 5 秒比较合适。
4.3 消息解析、批量写入与背压处理
网关发来的消息通常是 JSON 格式,包含时间戳、点位名、值、质量码。解析后要写入数据库,这里最容易出性能问题。如果每条消息都单独写一次数据库,每秒几千条消息会把数据库打爆。我的做法是攒批写入:用一个数组缓存消息,每 500 毫秒或攒够 1000 条就批量插入一次。
let buffer = []; let timer = null; function handleData(topic, value) { buffer.push({ topic, value, ts: Date.now() }); if (!timer) { timer = setTimeout(flush, 500); } if (buffer.length >= 1000) { flush(); } } async function flush() { clearTimeout(timer); timer = null; const batch = buffer; buffer = []; if (batch.length === 0) return; await db.insertBatch(batch); // 批量写入 }背压处理是另一个坑。如果数据库写入速度跟不上消息到达速度,内存会持续增长直到 OOM。解决办法是给 buffer 设上限,超过就丢弃最老的数据并记录告警。工控数据允许在极端情况下丢一些,但服务不能崩。
4.4 从 MQTT 到 WebSocket:给前端推数据
前端不能直接连 MQTT(浏览器原生不支持,虽然有 mqtt.js 的浏览器版,但把 Broker 暴露给前端不安全),所以后端要做一层 WebSocket 转发。用ws库起一个 WebSocket 服务,把 MQTT 收到的数据按订阅关系推给对应的前端连接。
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', (ws, req) => { ws.on('message', (msg) => { const { action, topics } = JSON.parse(msg); if (action === 'subscribe') { ws.topics = topics; // 记录该连接关心的主题 } }); }); // MQTT 收到消息后,遍历所有 WebSocket 连接,匹配主题后推送 function pushToClients(topic, value) { wss.clients.forEach((ws) => { if (ws.readyState === WebSocket.OPEN && matchTopics(ws.topics, topic)) { ws.send(JSON.stringify({ topic, value })); } }); }这里要注意主题匹配的效率。如果前端连接多、主题规则复杂,每次消息都遍历匹配会很慢。优化方法是把主题规则预处理成前缀树,或者用 Map 缓存匹配结果。
5. Web SCADA 前端的数据绑定与实时渲染
5.1 前端技术栈选择:Vue、React 还是原生
Web SCADA 前端本质是个实时数据可视化应用,技术栈选择看团队熟悉度。Vue 在国内工控圈用得多,生态里有 DataV、ECharts 这些现成的可视化库。React 配合 Ant Design 也很成熟。如果只是做个简单的看板,原生 JS 加 ECharts 就够了,不用上框架。
关键是要选一个支持高频更新的图表库。ECharts 的appendData和setOption配合notMerge: false能做到增量更新,比每次重绘整个图表性能好很多。如果数据点特别多(比如 10 万点以上的历史曲线),可以考虑 uPlot 或 Lightweight Charts 这类专为大数据量优化的库。
5.2 WebSocket 连接管理与断线重连
前端和 WebSocket 的连接不是一劳永逸的,网络抖动、服务重启都会断。必须实现自动重连 + 指数退避:第一次断开 1 秒后重连,失败就 2 秒、4 秒、8 秒,最多退到 30 秒。重连成功后要重新发送订阅请求,否则收不到数据。
let ws; let reconnectDelay = 1000; const maxDelay = 30000; function connect() { ws = new WebSocket('ws://backend:8080'); ws.onopen = () => { reconnectDelay = 1000; // 重置退避 ws.send(JSON.stringify({ action: 'subscribe', topics: myTopics })); }; ws.onclose = () => { setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 2, maxDelay); }; ws.onmessage = (e) => { const { topic, value } = JSON.parse(e.data); updateUI(topic, value); }; }还要处理页面隐藏时的连接。浏览器切到后台标签页时,定时器会被节流,WebSocket 也可能被挂起。可以在visibilitychange事件里做处理,页面隐藏时降低更新频率,显示时立即刷新一次。
5.3 实时曲线与报警的渲染策略
实时曲线最容易出的问题是渲染卡顿。如果每个数据点都触发一次重绘,每秒 60 帧根本扛不住。正确做法是数据缓冲 + 定时刷新:WebSocket 收到的数据先存到数组,用requestAnimationFrame或setInterval每 100 到 200 毫秒统一刷新一次图表。这样无论数据多快,渲染频率都是可控的。
报警渲染则要注意去抖动。一个温度值在阈值附近波动,可能一秒内触发几十次报警和恢复。前端要做延时确认:报警条件持续满足 3 秒才真正显示报警,恢复也要持续 3 秒才消除。这个延时值根据工艺特点调整,温度可以长一点,急停按钮必须立即响应。
5.4 历史数据查询与降采样
实时看当前值,历史看趋势。历史数据查询最大的问题是数据量:一个点位存一年,每秒一个点就是 3000 多万条。直接查出来渲染,浏览器直接卡死。解决办法是降采样:查询时按时间范围自动选择采样间隔,查一天用 1 分钟一个点,查一个月用 1 小时一个点。
降采样算法我常用LTTB(Largest Triangle Three Buckets),它能在保留曲线形状特征的前提下大幅减少点数。相比简单的等间隔抽样,LTTB 不会丢失尖峰和突变,对分析异常特别有用。后端查询时直接做降采样,只把结果返回给前端,能省大量带宽和渲染时间。
6. 联调排错:从网关到浏览器的完整链路验证
6.1 分段验证法:别一上来就端到端
整套链路涉及 PLC、网关、Broker、Node.js、前端五个环节,出问题时如果直接端到端查,根本不知道是哪一段断的。我的方法是分段验证,从下往上逐段确认。
第一步,用 Modbus Poll 或网关自带的调试工具,确认能读到 PLC 数据。这一步不通,后面全是白搭。第二步,用 MQTT Explorer 连上 Broker,看网关有没有在发布数据。MQTT Explorer 是个图形化客户端,能直观看到所有主题和消息,排查时特别好用。第三步,用 Node.js 写个最小订阅脚本,确认能收到消息。第四步,用浏览器开发者工具的 Network 面板看 WebSocket 有没有数据帧。这样一段段确认,问题定位快很多。
6.2 常见故障对照表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 网关连不上 PLC | IP 不通、端口错、协议不匹配 | ping 测试、确认端口号、核对协议类型 |
| 网关连不上 Broker | 账号密码错、防火墙拦截、端口不通 | telnet 测试端口、检查 ACL 配置 |
| 后端收不到消息 | 主题订阅不匹配、QoS 不兼容 | 用 MQTT Explorer 对比主题 |
| 前端无数据 | WebSocket 未连、订阅未发送 | 看浏览器控制台和 Network 面板 |
| 数据时有时无 | 网络抖动、死区设置过大 | 查网关日志、调小死区值 |
| 数据值不对 | 寄存器偏移错、数据类型错 | 用调试工具对比原始值 |
6.3 那些文档里不会写的坑
坑一:PLC 的字节序。Modbus 读 32 位浮点数时,有的 PLC 是高字在前,有的是低字在前,搞反了读出来的值完全是乱的。这个没有统一标准,只能查手册或实测。我一般先读一个已知值(比如设定温度),看解析出来对不对,不对就交换高低字。
坑二:网关的时间同步。网关本地时间如果和服务器差太多,数据时间戳就乱了,历史曲线会出现时间倒流。网关要配置 NTP 对时,指向内网的 NTP 服务器或公网时间源。
坑三:Node.js 的事件循环阻塞。如果在message回调里做同步的数据库操作或大量计算,会阻塞事件循环,导致 MQTT 心跳超时断连。所有耗时操作都要异步化,或者丢到 Worker 线程里处理。
坑四:Broker 的 max_queued_messages。QoS 1/2 的消息在客户端离线时会排队,如果队列满了,新消息会被丢弃。默认值通常够用,但如果你的服务经常长时间离线,要调大这个值,或者改用持久化会话。
6.4 压力测试与容量规划
上线前一定要做压力测试。用工具模拟大量设备同时上报,看 Broker 的 CPU、内存、连接数,看 Node.js 的消息处理延迟,看数据库的写入吞吐。我一般按实际点位数量的 3 倍来压测,留足余量。
容量规划的经验值:单台 EMQX 在 4 核 8G 的机器上,能稳定支撑 5 万个 MQTT 连接、每秒 10 万条消息。Node.js 单进程处理 MQTT 消息的瓶颈通常在数据库写入,用批量写入后,单进程每秒处理 2 到 3 万条没问题。超过这个量就要考虑多进程或消息队列削峰。
7. 从入门到落地的进阶路线
7.1 最小可行系统:先跑通再优化
新手最容易犯的错是一上来就追求完美架构,结果搭了两周还没看到数据。我的建议是先搭最小可行系统:一台 PLC(或用 Modbus Slave 模拟)、一个网关(或用 Node.js 模拟发布)、一个 Mosquitto、一个 Node.js 订阅脚本、一个简单的 HTML 页面。这条链路跑通可能只需要半天,但能让你对整个流程有直观认识。
跑通之后再逐步加功能:加数据库存历史、加 WebSocket 推前端、加报警逻辑、加权限控制。每加一个功能都验证一次,比一次性搭完再调试容易得多。
7.2 生产环境必须补上的几块拼图
最小系统跑通后,离生产还有距离。数据持久化要选时序数据库,InfluxDB 或 TDengine 都比 MySQL 适合存工控数据,写入快、压缩率高、支持降采样查询。服务高可用要做 Broker 集群和 Node.js 多实例,单点故障在产线监控里是不可接受的。监控告警要覆盖整套链路,Broker 的连接数、消息速率,Node.js 的内存、事件循环延迟,数据库的写入延迟,任何一环异常都要能及时发现。
安全加固也不能省。Broker 禁用匿名、启用 TLS、配置 ACL,Node.js 服务不要暴露公网,前端和后端之间加认证。工控系统的安全事件后果比互联网严重得多,一次非法指令可能造成设备损坏甚至人身伤害。
7.3 我踩过的三个印象最深的坑
第一个坑是网关固件版本。有次现场调试,网关死活连不上新买的 PLC,查了两天才发现是网关固件太老,不支持该 PLC 的新协议版本。升级固件后秒通。从此我养成了习惯:新项目先确认网关固件是不是最新,PLC 固件和网关固件的兼容性要提前查。
第二个坑是MQTT 主题大小写。MQTT 主题是大小写敏感的,网关发的是Factory/Line01/Temp,后端订阅的是factory/line01/temp,看起来一样,实际完全匹配不上。这种问题在 MQTT Explorer 里一眼就能看出来,但如果不熟悉,能查半天。统一命名规范很重要,我后来强制所有主题全小写。
第三个坑是Node.js 的内存泄漏。服务跑了一周后内存涨到 2G 然后崩了。排查发现是 WebSocket 连接断开后没有清理订阅关系,断开的连接对象一直被引用着。修复方法是在close事件里显式删除订阅记录。这类问题在开发环境跑几天看不出来,必须用--inspect配合 Chrome DevTools 做堆快照分析才能定位。
7.4 后续可以扩展的方向
这套架构跑通后,往上可以接规则引擎做复杂事件处理,比如"温度高且压力低且持续 10 秒"才触发报警,避免单一条件误报。可以接时序数据库的可视化工具如 Grafana,快速搭建运维看板。可以接MES 或 ERP 系统,把生产数据和订单、物料关联起来。
往下可以接边缘 AI,在网关上跑轻量模型做异常检测,比如通过振动数据预测设备故障。还可以做远程下发,从 Web 页面直接改 PLC 参数,但这块要特别谨慎,必须加二次确认和操作审计,防止误操作。
整套链路的核心思想是解耦:PLC 只管控制,网关只管采集和协议转换,Broker 只管消息路由,后端只管处理和存储,前端只管展示。每一层都可以独立替换和扩展,这才是工业物联网架构该有的样子。