☰
WEB SCADA系统搭建实战:协议选型、实时链路与部署排查
2026/10/14 10:34:14 网站建设 项目流程

简介:文档系统讲解了Web SCADA技术的体系架构与应用优势,面向工业自动化、远程监控与控制系统设计人员,旨在帮助读者理解第四代SCADA如何通过互联网技术实现数据开放与远程管理。资源包仅含1个doc文件,大小4.54MB,以文字说明为主体,内容结构清晰,便于快速查阅。当前已有309人学习,具备一定的参考价值。文档以Action Web SCADA系统为主线,详细介绍了WebViewI/O、WVC16等硬件模块:WebViewI/O支持多样信号输入并具备智能电源功能,WVC16则内置Web服务器,可独立完成邮件报警等任务;同时梳理了OPC/WebOPC软件基础,解释了如何通过普通浏览器远程访问过程数据。并归纳了相比传统SCADA系统的八项优势,包括远程访问、多样化报警、仪器虚拟化、协同工作等。此外,还讨论了脚本语言扩展和智能仪表独立工作等进阶话题,例如通过脚本实现复杂控制逻辑,对企业管控一体化建设与技术选型具有实践参考意义。

1. WEB SCADA 是什么:为什么浏览器里的监控画面总是先跑通后翻车

进厂维护过三次以上 SCADA 的人,基本都有同一个体会:传统组态软件的画面做得再漂亮,最后都卡在“只能在那台工控机上打开”这件事上。WEB SCADA 就是把监控系统搬进浏览器的路子——现场操作员用任意一台能上网的机器,打开网页就能看流程画面、操作阀门、查报警。这个标题里的 WEBscada系统.doc,与其说是一份软件,不如说是一张图纸:把系统怎么选型、点位怎么定义、链路怎么搭、部署在哪里全写在文档里。真正拉开项目差距的,不是前端画布画得多炫,而是采集链路稳不稳、点位表维护得好不好。我见过只花一周就把画面做出来的系统,上线后却被数据跳变和断线问题折腾了两个月。这篇就是照着这条链路,把协议选型、最小实现、现场排错和部署边界一次讲透。

2. 先选对路子:WEB SCADA 的通信协议、实时数据架构和前端渲染三件套

WEB SCADA 不是一个具体软件,是一套组合方案。开始写代码之前,有三件事必须定下来:采集端跟设备说什么语言、数据从现场到浏览器走什么通道、画面在浏览器里用什么技术画。这三件事定了,后面所有开发都是在填空;这三件事没定,写着写着就会推倒重来。

2.1 先看设备端:Modbus TCP、OPC UA 与 MQTT 怎么选

采集是 SCADA 的根。WEB SCADA 跟普通 Web 项目最大的差别在于:数据不是人填的,是设备给的。设备那头支持什么协议,基本决定了你这套系统的地基长什么样。

Modbus TCP 是绝大多数存量 PLC、仪表、电表的默认选项,报文简单,按功能码读线圈、离散输入、保持寄存器、输入寄存器四类数据。上手成本最低,用一个串口服务器加几条指令就能拉通。缺点是协议本身没有加密、没有会话概念,也不支持主动订阅,只能轮询。现场如果全是国产 PLC 和通用仪表,Modbus TCP 通常是最稳妥的起点。组态王、中控 SCADA 这些传统组态软件早年就是靠 Modbus 起家的,你换到 WEB SCADA 时点位表基本能平移过来,省掉大量重新对点的时间。

OPC UA 适合设备异构、品牌混杂的现场。它自带数据模型、历史上下文和安全加密,西门子、罗克韦尔、施耐德的中大型控制器基本都原生支持。代价是联调成本高,证书、端点、命名空间三样东西能让新手卡两三天。如果你是在组态王类环境里先做原型验证,可以用博途的 PLCSIM 起一个虚拟 S7-1200,配一个 Modbus TCP 仿真从站,不用等硬件到场就能把整条采集链路先跑通,这一步能省下大量现场调试时间。

MQTT 是分布式采集场景的首选,适合光伏、水务站房、分散的泵站这类“设备在现场、监控在远端”的拓扑。现场边缘网关采集 Modbus 数据后转成 MQTT 报文上云,broker 集中管理,断网时网关本地缓存,恢复后补传。国产 SCADA 技术栈这几年也大量往这个方向走,因为它的运维模型比传统中心轮询更适合无人值守。

选择逻辑可以压缩成一张表:

场景推荐协议理由
存量 PLC/仪表,点位几十到几百Modbus TCP通用性最好,联调最快
异构设备,需要安全和数据建模OPC UA模型统一,语义清晰
分散站房,需要断网缓存MQTT边缘网关接入,broker 中心化
混合场景Modbus 采集 + MQTT 上行采集层稳定,传输层解耦

2.2 实时链路:轮询、WebSocket 与消息队列的取舍

设备协议定了,接下来是数据通道。最常见的错误做法是让浏览器直接定时请求后端接口拉数据,也就是 HTTP 轮询。点位少、刷新要求不高的场景能跑,点位超过两三百,每个点一秒刷一次,后端要扛每秒几百次请求,数据库和网络先撑不住。而且报警这类事件型数据用轮询来拿,延迟完全不可控。

WEB SCADA 实时链路现在的常规做法是三层结构:采集引擎负责和设备对话,把点位值、质量戳、时间戳统一成内部格式;消息中间件负责把变化后的数据广播出去;后端通过 WebSocket 推给浏览器。采集引擎就是俗称的 scada engine,它只干四件事:按点位表轮询、变化检测、断线重连、把数据交给消息层。它不该关心画面怎么画,也不该关心报警怎么展示。

消息中间件这块,常见选择是 EMQX 这类 MQTT broker,或者 Redis Streams。小系统用 Redis 就够了,点位上千或者要跨机房同步再上 EMQX。后端订阅消息,然后通过 WebSocket 推给前端。注意一个原则:WebSocket 只推变化值,不推全量快照。全量快照在页面首次打开时走一次 HTTP 请求拉取,之后靠增量更新,这样带宽和前端渲染压力都小很多。

为什么不建议浏览器直接连 MQTT broker?一是跨域和安全不好控制,二是鉴权逻辑会摊到每个客户端上,三是页面里的业务逻辑也需要后端参与。让后端统一做数据出口,前端只认 WebSocket 这一种协议,排查问题的时候路径最清晰。

2.3 前端画布与组态:WebGL、SVG 和 canvas 的边界

画面渲染是 WEB SCADA 最直观的部分,但也是选型最容易拍脑袋的部分。组态画面需要的不是普通网页动效,而是拖动、缩放、闪烁、变色、管道流动、报警高亮这些工业图元能力。

点位规模在五百以下的小系统,用 SVG 加 DOM 操作最稳。浏览器兼容性好,图元属性可以像普通 DOM 一样绑定事件,点位更新直接改元素的属性就能刷新,开发效率最高。现场操作员用的机器千奇百怪,很多是老旧工控机加核显,SVG 在这类环境里翻车的概率最低。

点位超过两千,或者有大屏可视化需求,就要认真考虑 WebGL。Three.js 这类库能把几千个图元放进一个 canvas 里统一渲染,帧率稳定,还能做 3D 效果。代价是开发复杂度上来一截,图元拾取、缩放适配、事件分发全靠自己实现,一个点击命中问题就能折腾半天。中间地带用 canvas 2D 自己封装图元库,灵活但重复造轮子。

我的建议是:画面按“层级”隔离。核心监控画面用 SVG 保证稳定,大屏展示画面单独用 WebGL 渲染,中间用统一的数据接口对接。这样就算 WebGL 在某些机器上起不来,核心监控功能也不受影响,最多是展示画面看不了。这一条设计能避免很多现场尴尬。

3. 最小可用系统搭建:WebSocket 采集-推送架构从零到跑通

选型定了就可以动手了。下面这套最小系统用 Python 做采集和后端,前端用原生 JavaScript 订阅并刷新页面,覆盖了“设备到浏览器”的完整链路。架构不复杂,但每个环节都是生产级的思路。

3.1 数据模型先行:点位表、设备表与报警等级

WEB SCADA 的一切业务都围绕点位表展开,它决定了每个数据从设备哪个寄存器来、怎么解析、变化多大才认为有效。我一般会把点位信息直接做成数据库表,而不是写死在代码里,这样新增设备不用重新发布。

CREATE TABLE device ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, protocol ENUM('modbus_tcp', 'opcua', 'mqtt') NOT NULL, host VARCHAR(64) NOT NULL, port INT NOT NULL, unit_id INT DEFAULT 1, poll_interval INT DEFAULT 1000 COMMENT '轮询周期毫秒', enabled TINYINT DEFAULT 1 ); CREATE TABLE tag ( id INT PRIMARY KEY AUTO_INCREMENT, device_id INT NOT NULL, name VARCHAR(64) NOT NULL, address INT NOT NULL COMMENT '寄存器地址', register_type ENUM('coil', 'discrete', 'holding', 'input') NOT NULL, data_type ENUM('bool', 'int16', 'uint16', 'int32', 'float', 'string') NOT NULL, byte_order ENUM('ABCD', 'CDAB', 'BADC', 'DCBA') DEFAULT 'ABCD' COMMENT '字节序', scale_factor FLOAT DEFAULT 1.0 COMMENT '缩放系数', offset_value FLOAT DEFAULT 0.0 COMMENT '偏移量', deadband FLOAT DEFAULT 0.0 COMMENT '变化死区', alarm_level TINYINT DEFAULT 0 COMMENT '0无 1提示 2重要 3紧急', history_enabled TINYINT DEFAULT 1, unit VARCHAR(16) DEFAULT '', description VARCHAR(255) DEFAULT '' );

这段建表语句里有三个字段最容易被忽略。byte_order 是字节序,不同厂家的 PLC 存放 32 位数据时字节排列方式不一样,同一个地址读出来可能是对的也可能是反的,必须逐点配置。scale_factor 和 offset_value 处理传感器量程换算,比如 4-20mA 信号转成 0-100kPa 就要做线性变换。deadband 是变化死区,液位在小范围内波动时,没有死区就会导致每个扫描周期都产生一条变化记录,数据链路和数据库全被打满。

这个表要当作系统的主数据来维护。每新增一个设备,先在表里把点位配好,再用后面要讲到的采集服务启动时打印解析结果做验证。别指望靠代码里的 if-else 处理点位差异,那样点位一多代码就没法维护了。

3.2 采集端:Modbus TCP 轮询加变化检测的 Python 服务

采集端用 Python 的 pymodbus 库实现,按设备聚合连续寄存器批量读取,然后做变化检测后发布到 Redis Streams。

import asyncio import redis from pymodbus.client import AsyncModbusTcpClient from pymodbus.constants import Endian from pymodbus.payload import BinaryPayloadDecoder import json r = redis.Redis(host='localhost', port=6379, decode_responses=True) def decode_tag(decoder, data_type, byte_order): """按点位表配置解析寄存器值""" if data_type == 'int16': return decoder.decode_16bit_int() elif data_type == 'uint16': return decoder.decode_16bit_uint() elif data_type == 'int32': return decoder.decode_32bit_int() elif data_type == 'float': return decoder.decode_32bit_float() return None async def poll_device(device, tags): """按设备批量轮询,tags 已按寄存器地址排序""" client = AsyncModbusTcpClient(device['host'], port=device['port']) if not await client.connect(): print(f"设备 {device['name']} 连接失败") return # 按寄存器类型分段批量读取,避免逐点请求 ranges = group_continuous_addresses([t['address'] for t in tags]) for start, end in ranges: count = end - start + 1 if tags[0]['register_type'] in ('holding', 'input'): rr = await client.read_registers(start, count, slave=device['unit_id']) else: rr = await client.read_coils(start, count, slave=device['unit_id']) if rr.isError(): print(f"读取失败: 设备={device['name']} 起始地址={start}") continue # 解析出每个点位并逐点做变化检测 for tag in tags: addr_offset = tag['address'] - start decoder = BinaryPayloadDecoder.fromRegisters( rr.registers[addr_offset:addr_offset + regs_needed(tag['data_type'])], byteorder=Endian.BIG, wordorder=Endian.LITTLE ) raw_value = decode_tag(decoder, tag['data_type'], tag['byte_order']) scaled_value = raw_value * tag['scale_factor'] + tag['offset_value'] if abs(scaled_value - last_values.get(tag['id'])) >= tag['deadband']: last_values[tag['id']] = scaled_value publish_change(tag, scaled_value, quality='good') client.close() def publish_change(tag, value, quality): """只发布变化数据,带时间戳和质量戳""" payload = { 'tag_id': tag['id'], 'tag_name': tag['name'], 'value': value, 'unit': tag['unit'], 'quality': quality, 'ts': time.time() } r.xadd('scada:realtime', payload) async def main_loop(): while True: devices = load_devices() for device in devices: tags = load_tags(device['id']) await poll_device(device, tags) await asyncio.sleep(1) if __name__ == '__main__': asyncio.run(main_loop())

这段代码里有三个设计要点。按设备分组后,再把连续地址合并成区间批量读取,一个 16 点模拟量站原本要发 16 次请求,合并后可能只要 3 次,这对 PLC 的 CPU 负载是天壤之别。变化检测放在采集端而不是后端,死区以内的抖动直接被拦截,进入消息层的数据量能降一个数量级。发布的数据带 quality 字段,设备断线时不是不发值,而是发一个质量戳为 bad 的占位消息,前端拿到后把图元置为灰色,而不是停留在最后一个正常值上。

3.3 推送端与前端订阅:从 WebSocket 到画布绑定

后端用 FastAPI 提供 WebSocket 接口,把 Redis Streams 里的变化数据实时推给浏览器。

from fastapi import FastAPI, WebSocket import redis import asyncio import json app = FastAPI() r = redis.Redis(host='localhost', port=6379, decode_responses=True) connected_clients = set() @app.websocket("/ws/realtime") async def realtime_endpoint(websocket: WebSocket): await websocket.accept() connected_clients.add(websocket) try: while True: # 等待前端发心跳,保持连接活跃 await websocket.receive_text() except Exception: pass finally: connected_clients.discard(websocket) async def broadcast_loop(): """监听 Redis Streams,把新数据推给所有在线客户端""" last_id = '0' while True: entries = r.xread({'scada:realtime': last_id}, block=1000, count=100) for stream, messages in entries: for msg_id, data in messages: last_id = msg_id payload = json.dumps(data) for client in list(connected_clients): try: await client.send_text(payload) except Exception: connected_clients.discard(client) await asyncio.sleep(0.01)

前端这边只需要维护一个 WebSocket 连接,收到数据后按 tag_id 找到页面上对应的图元更新。

let ws = null; let reconnectTimer = null; function connectWebSocket() { ws = new WebSocket(`ws://${location.host}/ws/realtime`); ws.onopen = () => { // 心跳:每 30 秒发一次,保持连接不被中间设备断开 heartbeatTimer = setInterval(() => ws.send('ping'), 30000); }; ws.onmessage = (evt) => { const data = JSON.parse(evt.data); // 按点位 ID 找到 DOM 元素,只更新变化的图元 const el = document.querySelector(`[data-tag-id="${data.tag_id}"]`); if (el) { el.textContent = formatValue(data.value, data.unit); el.classList.toggle('quality-bad', data.quality !== 'good'); } }; ws.onclose = () => { clearInterval(heartbeatTimer); // 指数退避重连,避免断网时疯狂请求 reconnectTimer = setTimeout(connectWebSocket, 3000); }; } connectWebSocket();

前端通过 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

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

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

立即咨询