SCADA-Web-化架构-前端渲染层如何与数据采集层解耦

0 阅读10分钟

这两年做工业可视化项目,一个很高频的坑是:前端一开始看起来只是画个组态页面,结果做着做着,浏览器里塞进了半个采集系统。

页面组件直接订 OPC UA 节点,图表自己管 WebSocket 重连,告警面板偷偷写一套点位转换逻辑,最后整个系统变成一锅“能跑但不敢动”的工业大杂烩。

如果你也经历过下面这些场景,那这篇大概率对你有用:

  • 前端代码里到处是 ns=2;s=Channel1.Device1.TagA
  • 新接一个 Modbus 设备,前端要跟着改字段映射
  • 同一份点位在大屏、趋势图、告警列表里被重复处理三次
  • 协议层一波抖动,页面动画、图表和告警一起抽风

我的结论很直接:SCADA Web 化真正难的,不是把页面搬进浏览器,而是把“渲染层”和“采集层”切干净。

这篇就聊聊我现在更推荐的一种做法:让前端只认统一语义数据,不直接感知底层工业协议。

1. 先说结论:前端不要直接面向协议开发

传统工业项目里,OPC UA、Modbus TCP、BACnet、私有 TCP 协议各玩各的,桌面 SCADA 因为跟驱动、运行环境绑得更近,很多耦合还能忍。

但一旦到了 Web 架构,问题会被迅速放大:

  1. 浏览器天然不适合直接对接复杂工业协议
  2. 前端发布频繁、组件繁多,不应该承载协议适配职责
  3. 一个页面往往同时服务大屏、PC、平板甚至移动端,协议细节越早暴露,越难复用

所以比较稳的思路是分四层:

image.png

推荐的职责边界

  • 设备层:PLC、传感器、仪表、网关
  • 采集与网关层:负责协议驱动、点位采集、清洗、补时戳、质量码、边缘缓存
  • 消息与数据层:负责统一事件模型、消息分发、时序存储、权限/API
  • Web 前端层:负责渲染、交互、视图状态、页面编排

这里最关键的一刀是:

前端消费的是“TagUpdate / AlarmEvent / TrendPoint”这类统一业务对象,而不是 OPC UA NodeId、Modbus 寄存器地址或某个驱动 SDK 返回结构。

这件事听起来像废话,但真正落地时,很多项目都死在“先快点跑起来”这一步。

2. 为什么前端一旦碰协议细节,后面几乎必炸

2.1 设备接入变化会不断传染到 UI

假设一开始接的是 OPC UA,前端订阅的是:

subscribe('ns=3;s=Line1.Pump1.Speed')

后面现场改造,把数据接入改成 MQTT 网关转发,你原来的页面逻辑就会开始变形:

  • 订阅地址要改
  • 数据结构要改
  • 质量码来源要改
  • 重连逻辑要改
  • 历史查询接口也可能要改

这就说明:你的前端没有服务于“业务对象”,而是服务于“接入方式”。

在工业项目里,接入方式恰恰是最容易变化的一层。

2.2 一个项目里,实时流和历史流通常不是一回事

实时画面看的是秒级甚至 100ms 级更新;趋势分析查的可能是时序库聚合结果;告警面板又来自规则引擎。

如果前端自己把这些数据源揉在一起,就会出现:

  • 图表组件绑定实时流时很好用,但切换历史模式要重写一半
  • 组态组件依赖实时值结构,结果无法复用在回放界面
  • 告警面板和实时状态展示的“设备在线/离线”定义不一致

这时真正缺的不是更多组件,而是统一的数据语义层

2.3 前端会被迫替后端补业务规则

工业数据不是“有值就行”,通常还带这些信息:

  • 时间戳 ts
  • 数据质量 quality
  • 来源 source
  • 单位 unit
  • 工程量转换规则
  • 告警阈值和滞回

如果这些规则散在前端组件里,最后一定会形成一个局面:

后端说自己只是转发,前端说自己只是展示,但业务规则已经神不知鬼不觉地住进了十几个页面。

3. 更稳的做法:建立统一 Tag 总线

我现在做这类项目,基本都会在“采集层”和“前端层”之间加一层统一 Tag 总线。它未必真的是某个独立产品,但在架构上必须存在。

image.png

它干的事情很朴素:

  • 把 OPC UA、Modbus、MQTT、HTTP 等不同来源统一抽象成同一种更新事件
  • 给每条数据补统一字段:tagId / value / quality / ts / source
  • 屏蔽底层协议差异
  • 提供统一的订阅、查询、回放、告警联动能力

一个最小可用的数据对象,大概长这样:

type TagUpdate = {
  tagId: string;
  value: number | string | boolean;
  quality: 'good' | 'bad' | 'stale';
  ts: number;
  source: 'opcua' | 'mqtt' | 'simulator';
};

前端状态层只接这个,不接别的。

对应的代码组织也会清爽很多:

image.png

再给一个更贴近业务代码的例子:

class TagBus {
  private listeners = new Map<string, Set<(u: TagUpdate) => void>>();
​
  subscribe(tagId: string, handler: (u: TagUpdate) => void) {
    if (!this.listeners.has(tagId)) {
      this.listeners.set(tagId, new Set());
    }
    this.listeners.get(tagId)!.add(handler);
    return () => this.listeners.get(tagId)?.delete(handler);
  }
​
  publish(update: TagUpdate) {
    this.listeners.get(update.tagId)?.forEach(fn => fn(update));
  }
}

这样页面组件只需要:

useTagValue('line1.pump1.speed')

而不是:

useOpcUaNode('ns=3;s=Line1.Pump1.Speed')

别小看这一层抽象。它决定了你的前端是在做“业务界面”,还是在做“浏览器里的轻量驱动中心”。

4. OPC UA、MQTT、Sparkplug B 应该怎么放位置

这次检索资料时,我专门看了几类公开信息:

  • Web SCADA / HMI 开源项目 FUXA 的能力介绍,能看到它支持 Modbus、OPC UA、MQTT 等协议,并以 Web 方式完成工程化设计
  • 多篇 OPC UA 与 MQTT Sparkplug B 的对比资料,核心共识基本一致:OPC UA 强在工业互操作语义和标准化,MQTT/Sparkplug 强在轻量发布订阅和跨网络分发

把它们放到架构里理解,会很清楚:

OPC UA 更适合靠近采集层

它的优势是:

  • 对工业对象建模更完整
  • 数据类型、层级、语义更规范
  • 在设备接入和工业系统互联场景里更自然

但如果你让前端直接吃 OPC UA 结构,问题也很明显:

  • NodeId 天然不友好
  • 浏览器侧安全和连接控制复杂
  • 对页面开发者不够“语义化”

所以 OPC UA 更适合作为采集层或边缘网关的输入协议

MQTT / Sparkplug B 更适合作为分发层

它的优势是:

  • 发布/订阅模型天然适合多终端消费
  • 跨网络、跨服务广播实时状态更轻便
  • 更适合做 WebSocket 之前的一层消息总线

但也别神化它。MQTT 本身不是银弹,如果没有统一 Topic 约定、设备生命周期管理和语义规范,最后也会演变成“字符串地狱”。

所以我的偏好是:

  • 设备接入:优先 OPC UA / Modbus / 原生驱动
  • 统一分发:消息总线用 MQTT 或内部事件流
  • 浏览器消费:通常再包装成 WebSocket / SSE / API

也就是说,前端大多数时候更应该面对:

  • wss://.../realtime
  • /api/trends/query
  • /api/alarms

而不是直接面对 PLC 协议。

5. 前端真正该关心的,是状态组织而不是协议接入

一个靠谱的 SCADA Web 前端,我认为核心工作有四块:

5.1 统一状态仓库

实时值、历史值、告警态、设备在线态,都要落到统一状态模型里,而不是每个组件自己拉一份。

最少也得做到:

  • 实时更新单点分发
  • 同一 tag 多组件共享
  • 脏数据和过期数据可识别
  • 断线重连后支持增量恢复

5.2 视图层和数据层解耦

组态编辑器、趋势图、告警列表、设备卡片,应该通过统一 selector 取数。

比如:

const speed = selectTagValue(state, 'line1.pump1.speed');
const speedQuality = selectTagQuality(state, 'line1.pump1.speed');

而不是组件内部自己解析消息包。

5.3 让“质量码”成为一等公民

很多项目只展示 value,不展示 quality,现场一抖动页面还在一本正经地显示“正常运行”,这就很离谱。

工业前端一定要把下面这些能力内建进去:

  • good / bad / stale 可视化
  • 最后更新时间显示
  • 失联态样式降级
  • 关键控制项禁止在坏质量下操作

5.4 让回放和实时走同一套渲染协议

这是我很推荐但经常被忽视的一点。

如果实时数据和历史回放使用同一种“组件输入协议”,你就会得到两个巨大好处:

  1. 趋势分析、事件回放、事故复盘能复用同一套视图
  2. 测试和演示不必总连真实设备

比如不管是真实流还是回放流,最后都向前端发 TagUpdate[],那画面层根本不需要知道自己是在“在线监控”还是“历史回放”。

6. 一个我更推荐的 SCADA Web 化落地模板

如果你现在正要搭一套 Web SCADA / Web 组态系统,我会建议按下面这个骨架来:

后端 / 边缘侧

  • 协议适配器:OPC UA、Modbus TCP、MQTT、HTTP
  • 点位中心:维护 tagId、单位、阈值、映射关系
  • 实时分发:MQTT / 内部事件总线 / WebSocket Gateway
  • 历史存储:时序库
  • 告警引擎:规则、去抖、确认、恢复
  • 权限系统:页面、设备、点位、操作权限分层

前端侧

  • Runtime SDK:只暴露 subscribeTag / queryTrend / queryAlarm
  • 状态管理:统一 tag store / alarm store / session store
  • 渲染引擎:组态画布、图表、列表、弹窗、联动
  • 编辑器:只配置业务 tag,不配置协议细节

一个常见接口设计大概是:

interface ScadaRuntime {
  subscribeTags(tagIds: string[], cb: (updates: TagUpdate[]) => void): () => void;
  queryTrend(tagId: string, range: [number, number]): Promise<TrendPoint[]>;
  queryAlarms(condition: AlarmQuery): Promise<AlarmRecord[]>;
}

注意这里暴露的是业务能力接口,不是协议能力接口。

7. 那低代码/组态平台为什么经常能做得更稳

这也是我最近越来越明确的一个判断:

组态平台的价值,不只是“拖拽更快”,而是它天然逼着你做中间层抽象。

因为一旦要支撑:

  • 多页面复用
  • 多设备接入
  • 模板化组件
  • 大屏、PC、移动端共用
  • 实时 + 历史 + 回放共存

你就不可能让每个页面都直接写协议代码。

这也是为什么很多成熟可视化/组态产品,最后都会把“点位绑定”“事件模型”“运行时 SDK”“渲染引擎”拆得比较清楚。真正难的不是画布,而是抽象边界

8. 最后一句人话总结

SCADA Web 化不是把传统监控画面塞进浏览器,而是重新定义一遍系统边界。

如果前端直接绑定 OPC UA NodeId、直接理解 Modbus 地址、直接处理设备驱动异常,那它看起来像是更灵活,实际上是在提前透支整个系统的可维护性。

更稳的方式是:

  • 让采集层负责协议和设备差异
  • 让消息层负责统一事件分发
  • 让前端只关心业务语义和视图状态

一句话概括就是:

浏览器应该渲染工业系统,而不是兼职扮演工业驱动。

如果你正在做 Web 组态、SCADA Web 化或者工业物联网前端,这一刀越早切,后面越轻松。