ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

自研轻量级SCADA系统全栈实践:通信、实时库、HMI组态与报警引擎

自研轻量级SCADA系统全栈实践:通信、实时库、HMI组态与报警引擎 前两年接了一条农产品加工产线的数据采集项目客户现场用的上位机是一套老牌商用组态软件。功能确实全但每年的授权费相当可观点位一超就要再买授权通信协议是个黑盒想接自己的算法模块根本无从下手。当时我就下决心与其被商用方案绑死不如自己动手做一套真正够用的轻量级监控系统。于是就有了SuperSCADA这个项目配套的人机界面叫SuperHMI。后来这套技术底板继续演化又拆出了面向标准化交付的TopSCADA和面向轻量组态场景的TopHMI两个产品形态。说实话自研SCADA这件事在工业自动化圈子里争议挺大。多数人觉得“为什么不直接用现成的”但真到了项目现场你才会明白商用方案那些看不见的成本有多高。这篇文章就把我在这套系统上踩过、填过、沉淀下来的东西完整写一遍从通信采集、实时库、历史存储到前端组态引擎、报警引擎再到上线压测的坑。内容偏实操适合正在考虑自研监控平台、或者对SCADA内部机制感兴趣的工程师参考。1. 为什么要自研一套轻量级SCADA商用方案的痛点与我的取舍1.1 商用组态软件用起来到底哪里难受很多年长的工程师习惯了WinCC、组态王、iFix这类软件觉得它们成熟、稳定、有问题有人兜底。我承认大厂的SCADA平台上限很高特别是做大型DCS系统、几千上万点位的场景它们有完整生态。但中小型项目里商用方案的问题非常具体。首先是授权费用。商用SCADA普遍按点位收费几百个点位的项目授权费就是一笔不小的开销加上冗余备份、Web发布、历史报表这些模块通常还要单独付费。其次是实施效率这类软件配置一个画面往往要在编辑器里反复点鼠标图元绑定、动画关联、脚本事件一套流程下来并不比写代码快。最后是跨平台和二次开发的问题很多老牌软件的运行环境锁定Windows想要在Linux服务器上部署就很别扭想做业务集成、想接自己的算法、想和MES/ERP做深度数据交互走官方SDK的话又是一轮学习成本。开源SCADA类项目也存在问题。大多数开源项目的核心开发者只有一两个人文档不全协议栈老旧系统架构十多年没什么大变化。真正出问题时只能自己啃源码那和自研有什么区别。1.2 技术选型全栈用哪些东西搭起来的SuperSCADA的技术栈我基本上是按“部署简单、资源占用低、二次开发顺手”这三个原则选的。后端用的Go。理由很直接编译出来就是一个静态二进制扔到工控机上直接跑不需要装运行时内存占用比Java系低一大截而且goroutine模型天然适合处理大量设备连接和并发轮询。C性能确实更好但开发效率、第三方生态、团队招人都有点跟不上。数据这块分成实时库和历史存储两层。实时值保存在进程内存中用哈希表加读写锁实现保证毫秒级访问历史数据先写内存环形缓冲再批量落盘到SQLite。后来点位增加、报表需求变多之后我在新版本里加了PostgreSQL和TDengine的适配接口满足不同现场的需求。前端HMI选择了Vue 3加TypeScript画面渲染用了SVG和Canvas组合的方案。通信用WebSocket做实时推送HTTP REST做配置管理和历史查询。Mobile端其实就是同一个Web应用只是做了响应式适配方便现场人员在平板上看画面和确认报警。1.3 Super、Top两条产品线的定位差异这套系统我取了四个名称SuperSCADA、SuperHMI、TopSCADA、TopHMI。很多人第一次看到会懵觉得是不是做了两套重复的系统。实际上不是这里有一条明确的产品线逻辑。SuperSCADA是整个平台的后端核心代号包含采集、存储、报警、接口等全部服务端能力。SuperHMI是配套的Web组态与运行界面负责画面编辑、数据可视化、操作控制。两者合在一起是一套完整的自研SCADA解决方案。TopSCADA和TopHMI是在Super这套底座上做的两种交付形态。TopSCADA强调标准化把轮询、存储、报警等能力固化成模板适合代理集成商按标准模式快速交付。TopHMI则主打轻量组态不要求使用者写任何脚本用预置的模板和组件库组合出监控画面适合产线局部改造、单机设备监控这类轻应用场景。一句话总结Super是技术内核Top是面向市场的封装。内核只有一套但交付路径可以按项目档次灵活选择。2. 通信采集层Modbus这关过不了其他都是扯淡2.1 为什么通信层决定了SCADA的天花板SCADA系统做的是“现场设备数据上来、控制指令下去”这件事通信采集层就是数据的水龙头。水龙头没接好后面实时库再快、HMI画面再炫全部白搭。工业现场协议种类非常多。通用一点的有Modbus RTU/TCP、OPC UA、MQTT、S7comm、PROFINET行业专用的还有DLT645电表、CJ/T188水表、BACnet楼宇等。SuperSCADA第一版先实现了Modbus RTU/TCP和MQTT后续按项目需要逐步加了OPC UA和S7comm。选择Modbus优先没有任何悬念它是工业自动化领域事实上的通用语言。PLC、仪表、变频器、智能电表几乎每个设备都会提供Modbus接口只要把Modbus吃透系统就能覆盖大多数采集需求。2.2 驱动管理器通道、设备、点位三层模型这套采集架构我把模型设计成了三层通道Channel、设备Device、点位Tag。通道代表一条物理或逻辑链路。一个Modbus TCP通道对应一个IP端口一个Modbus RTU通道对应一个串口和波特率参数。通道的职责是管理连接生命周期、超时、重试和并发限制。设备挂在通道下Modbus里就是从站地址Slave ID。一个通道下面可以挂多个设备只要它们的地址参数配置正确。点位是最终的数据项有寄存器地址、数据量类型、读写属性、缩放系数这些属性标签。这样三层模型的好处是配置直观和现场物理拓扑完全对应。排查问题的时候看一眼配置就知道数据链路走的是哪条通道、哪个设备不用猜。2.3 轮询调度与合并读减少报文就是减少故障刚开始做采集驱动时我用的是最简单的顺序轮询每个点位依次发送读取请求。点位数量少的时候没什么感觉但点位一多比如一台设备有两三百个寄存器要采集按5秒周期轮询报文数量就变得非常大现场总线负荷高不说还容易触发设备通信超时。后来我改了轮询调度算法现在是按“连续读区间合并”来规划报文。也就是说一台设备的点位表里那些地址连续的寄存器会合并成一次“批量读”请求。比如某个从站的保持寄存器从0x0000到0x0050整段需要采集我就直接用0x03功能码发送起始地址0x0000、长度0x0051的请求一次报文把整段都读回来再在本地按点位表拆分。合并之后报文数量大幅下降。以前一台200点位的设备需要发出200次请求合并后大概只需要30到40次整体轮询速度提升非常明显。需要注意的一点是有的老PLC对单次请求的寄存器数量有限制比如Modbus标准建议单次不超过125个寄存器我在驱动里做了可配置的最大合并长度默认120遇到奇怪的设备可以调小。代码层面轮询调度的核心逻辑大概是这样type PollSchedule struct { Device *Device ReadPlans []ReadPlan Interval time.Duration lastPoll time.Time inProgress bool } type ReadPlan struct { FuncCode byte StartAddr uint16 Quantity uint16 Tags []*Tag // 映射回点位 } func (s *PollSchedule) BuildPlans() { // 将点位按功能码分组再按地址排序 // 遍历排序后的点位将连续地址区间合并为一个ReadPlan // 限制每个ReadPlan的Quantity不超过MaxReadQuantity }每个通道有独立的goroutine循环调度器根据各设备的PollInterval决定何时发送请求响应回来后通过回调把数据分发给实时库。这样不同通道之间天然并发不会因为某一台设备慢而拖垮整个系统。2.4 数据解析的几个大坑字节序、缩放、无符号数通信协议里最容易出错的地方不在通信而在数据解析。Modbus返回的报文是原始字节流怎么解释成直观的工程值是每个SCADA工程师都绕不过去的坎。首先是字节序Byte Order。Modbus协议规定寄存器的高字节在前big-endian但不同PLC在组合32位浮点数或32位整数时寄存器之间的顺序差别很大。西门子PLC和施耐德PLC处理同样的浮点数在Modbus上呈现的寄存器高低字顺序可能是反的。也就是说同样读4个字节有的设备是按“字1高字节、字1低字节、字2高字节、字2低字节”排列有的则是“字2高字节、字2低字节、字1高字节、字1低字节”。我在点位属性里加了字节序配置项支持ABCD、BADC、CDAB、DCBA四种排列模式遇到任何品牌的设备都能适配。其次是缩放系数。很多仪表内部是整数存储工程值是毫安、摄氏度还是MPa需要乘一个系数。比如一个压力变送器的量程是0到1.6MPa对应数据0到16000那么缩放系数就是0.0001。这个也是点位属性里必须有的配置项。再有就是无符号数和溢出。有些电表的功率值会超过32767如果按有符号数来解析读出来就变成负数了。我统一规定所有Modbus点位默认按无符号读需要符号解析时显式配置。为了把这些解析逻辑做干净我在驱动层定义了一个Transform管道原始字节流进入后依次经过字节序转换、类型转换Int16/UInt16/Int32/UInt32/Float32、缩放计算最终形成标准化数值交给上层。每个环节都是独立函数方便单测。3. 实时数据库与历史存储纯内存环形缓冲和时序落盘的结构设计3.1 实时库为什么不能用MySQL第一次做SCADA的人容易犯一个错误把所有采集数据直接写关系型数据库用SELECT查最新值来刷页面。理论上这是可行的但一旦点位数量上千、采集周期到秒级MySQL这类通用关系型库的读写瓶颈立刻暴露而且HMI页面要拿实时值走SQL链路延迟非常高画面一卡就是几百毫秒。SCADA的实时数据访问有几个特点高频写入、按TagKey随机读取、几乎不做事务操作。这正好是内存哈希表最擅长的场景。SuperSCADA的实时库结构很简单核心就是一张大哈希表。为了减少锁竞争我没有用一个大锁保护整张表而是采用了分段锁的思路把哈希表按Key哈希值分成256个桶每个桶有自己的读写锁。写值的时候只锁对应的桶其他桶完全不受影响。实测下来在10000点位的规模下单机写入吞吐轻松到十万级每秒HMI页面读取周期值几乎零延迟。3.2 实时值的状态管理质量戳和时间戳实时库里除了数值本身应用程序还常关心这个值“可不可信”。现场经常遇到设备断线、点位超时、传感器异常这些情况如果系统把这些异常值照单全收HMI上就会出现离谱的数据和误报警。为此我为每个点位增加了Quality状态概念值分为Good、Bad、Uncertain、Stale四种。通信层成功更新点位值时标记Good轮询超时且超过重试次数后标记Bad设备返回的数据超过了组态时设置的高低量程则标记Uncertain系统重启后、首轮采集尚未完成时历史点位标记为Stale。HMI画面上的颜色着色、趋势曲线的阴影区域、报警引擎能否触发全部参考Quality状态。这样数据是真是假界面上一眼就能看出来。时间戳归通信层管每次数据包到达时采集驱动打上服务器接收时间。这里特别注意时间戳用的是服务器本地时间不是设备侧时间因为很多现场设备根本不校时设备时钟慢十几分钟的事情很常见。历史数据的时间轴我统一以服务器时间为准。3.3 历史存储环形缓冲、批量写、聚合降采样历史数据存储设计的目标是“写得快、占得小、查得动”。点位实时值从采集层到实时库之后并不会立刻落盘。每个点位前面有一个环形缓冲Ring Buffer默认容量是120个秒级快照。历史归档线程每5秒扫描一次把缓冲里的数据批量写入存储层。这样设计的一个好处是当现场网络短暂断开又恢复时断网期间的数据不会立刻丢失缓冲能暂存一部分恢复后再补写。批量写入是性能关键。一条条INSERT的数据库性能非常差但改成事务批量提交后几千条记录一次提交IO效率能提高一两个数量级。存储选型上SQLite在处理十万到百万级的历史记录上表现完全够用文件单机部署还方便备份。更大的数据处理我后面接了TDengine列式存储加时序压缩查询性能比SQLite强很多。降采样这块也值得提一下。趋势曲线要显示一年内的历史数据时原始秒级数据量太大直接把全部记录拉到浏览器肯定卡死。我的做法是查询接口支持降采样参数按天、小时、分钟自动聚合返回MAX、MIN、AVG值前端曲线用聚合值绘图浏览器负载小曲线轮廓也完整。历史表的核心结构大致是CREATE TABLE IF NOT EXISTS history_data ( tag_key TEXT NOT NULL, ts INTEGER NOT NULL, value REAL NOT NULL, quality INTEGER NOT NULL, PRIMARY KEY (tag_key, ts) ); CREATE TABLE IF NOT EXISTS agg_hour ( tag_key TEXT NOT NULL, bucket INTEGER NOT NULL, avg_value REAL NOT NULL, min_value REAL NOT NULL, max_value REAL NOT NULL, sample_count INTEGER NOT NULL, PRIMARY KEY (tag_key, bucket) );实际查询时按时间范围和tag_key走索引性能没问题。我用一台树莓派4B做过测试模拟3000点、5秒周期上报连续跑48小时SQLite文件增长约400MB查询一周的曲线数据响应时间在2秒以内。这个数字在中小型项目里完全可以接受。3.4 重启恢复从文件回放历史与实时库预热SCADA系统最怕的就是服务器重启一旦内存里的实时值全没了HMI画面上所有数据变成空白或Stale要等下一轮采集周期才能恢复这个恢复过程看起来非常业余。我在实时库里做了快照机制每30秒将当前所有点位的最新值、质量戳、时间戳异步写到内存映射文件中。系统启动时先加载这个快照把点位预热到上一轮的状态再启动采集驱动。这样HMI页面一打开看到的不是空白数据而是几秒前的现场值采集恢复后数值自然刷新。这个细节在现场演示效果很加分客户看到“上位机重启了数据还在”信任感完全不一样。4. SuperHMI组态引擎为什么我没用现成Web组态而是自己画了一套渲染层4.1 市面上的Web组态和自研之间的账怎么算都不亏做HMI前端之前我认真考察过市面上的Web组态产品。一类是和商用SCADA绑定的Web发布模块功能强但只能配合自己的平台用而且价格不低。另一类是国产的通用Web组态框架比如乐吾乐这类拖拽式编辑做得确实好但问题是它们的核心套件授权费用对几个中小项目来说还是偏高并且二次开发和私有化部署受制于框架本身的API边界。SuperHMI自研渲染引擎的出发点是“所有渲染行为都可控”。点位刷新、动画绑定、事件脚本、报警闪烁这些核心交互我在原生Canvas和SVG层面自己实现不依赖某个框架封装的黑盒。前期开发周期确实长了点但后续迭代的自由度非常大新项目加需求时不会被上游框架卡住。4.2 SVG和Canvas的分工静态基元用SVG动态大数据用CanvasSuperHMI在渲染方案上没有二选一而是让SVG和Canvas各司其职。画面里的阀门、泵、管道、储罐、仪表盘这类静态设备图元用SVG绘制。SVG本身就是DOM节点天然支持CSS样式、事件绑定和动画而且矢量缩放不丢细节。像阀门的开到位绿、关到位红这种状态切换直接用样式绑定就能实现开发效率很高。工业现场的画面不会像游戏界面那样极端复杂几百个SVG图元在浏览器里渲染毫无压力。实时趋势曲线、历史曲线、大数据量表盘这类需要频繁重绘的动态元素用Canvas来画。Canvas没有DOM节点一帧就是一次绘制处理几千个数据点的曲线刷新非常丝滑。SuperHMI里的趋势图组件是纯Canvas实现的支持多曲线叠加、游标取值、缩放区间、量程自动匹配实际体验下来流畅度比SVG方案高一个档次。为了管理两类元素的混合渲染我设计了一套统一的图元接口每个图元都向上层暴露render、update、hitTest三个方法。SVG图元的update内部就是更新DOM属性Canvas图元的update则把更新请求加入重绘画布列表。这样组态编辑器拖一个图元到画布上时引擎根据图元类型自动决定走哪套渲染路径使用者完全无感知。4.3 数据订阅与动画刷新WebSocket推送加帧合并HMI页面刷新的设计是整个前端的关键。实时数据如果走HTTP轮询几百毫秒间隔请求服务器页面上几千个点位的情况下浏览器和服务器都会很吃力。我用的是WebSocket长连接加增量推送。SuperHMI有一个订阅管理器保存当前页面所有活动点位集合并建立“点位 → 订阅者”的映射。采集驱动每轮更新点位后实时库会把变动点位的时间戳标记为脏Dirty。服务器端推送器每秒扫描一次脏点位表把新增变动的数据压缩成一条消息推送给对应客户端。推送频率固定为一秒一次也就是说客户端页面最多一秒刷新一次数据。这里有个优化细节每次画面切换时订阅管理器会计算当前画面涉及的点位集合只向服务器注册这些点位离开画面就自动取消订阅。这样切到大画面时页面不会收到一堆用不上的点位推送内存和CPU占用都稳得住。动画这块处理上我做得比较克制。阀门切换状态的变色动画用了CSS transition来做过渡时间设置200毫秒太短看不见效果太长显得拖沓。流量流动、液位波动这类连续动画则用requestAnimationFrame驱动Canvas绘制。对于数字跳动的画面我不做滚动数字特效直接一刀切更新数值工业画面贵在清晰准确动画特效只服务于状态表达不纯粹为了炫。4.4 组态编辑器拖拽、图元属性、事件脚本的最小闭环SuperHMI的编辑器和运行器是同一个渲染内核。打开组态编辑器画布左边是图元面板右边是属性面板底部是事件脚本编辑器。图元面板把常用设备图元分成基础、电气、工艺、仪表、管线五类全部是SVG模板拖拽就能放到画布上。每个图元的属性面板包含位置尺寸、填充颜色、可见性、旋转角度这些基础样式最关键的是数据绑定配置。给一个阀门图元绑定点位之后点位值变化时图元会根据配置的映射规则变化样式比如数值大于0.5时阀门开到位变绿。事件脚本用的不是JavaScript而是我设计的一套轻量级公式语言HMIScript支持简单的IF条件、算术表达式、字符串拼接和价值比较。这样做的原因是安全完全开放JavaScript会让组态工程像自由发挥的程序很容易写出不可控的代码限制成一门小型DSL后表达力对监控场景足够安全性却大幅提升。TopHMI形态其实是基于这套编辑器做的模板化封装。我在编辑器里内置了一套预置工程模板用户只需要替换点位表和背景图画面上绑定的点位自动映射不需要从头搭画面半小时就能交付一个设备监控页面。很多集成商就是冲着这个模板化能力选的TopHMI项目进场快了很多。5. 报警与事件引擎分级、去抖、确认流一条都不能少5.1 Basic报警流程里的两个管理重心报警是SCADA系统最“重”的功能之一它直接关系现场安全设计草率会有很大的隐患。报警管理有两个核心命题一是避免误漏报二是建立闭环流程。先说误漏报。现场经常遇到这种情况一个液位信号在阈值边界上抖动采集周期内数值来回穿越阈值就会产生一连串的报警产生、恢复消息。我在报警引擎里做了去抖Debounce机制报警条件成立后必须连续持续设定的时间默认2秒可配置才正式触发报警同样恢复条件也要持续一段时间才宣布恢复。这个去抖时间设置要谨慎太长会延迟真实报警太短则过滤不掉抖动我一般建议设置在采集周期的2到3倍。报警处理还常涉及一个“滤波”概念通常和去抖一起用。滤波是先判断数值的死区Deadband对变化小于死区的波动不产生事件比如温度在49.8℃和50.2℃之间波动死区设置0.5℃就能把这种频繁切换抑制掉。滤波和去抖配合现场的误报率能降低很显著。5.2 报警分级、多状态流转与确认报警分级我参考了国际电工委员会的惯例划分为四个等级Critical、Major、Minor、Warning。Critical对应设备停机和人员安全风险必须立即处理Major对应影响生产的故障需要尽快响应Minor对应需要关注的异常可以在计划内处理Warning对应提示性信息。每一个等级配置不同的颜色、声音、短信通知策略。报警状态是一个有限状态机核心状态包括Active未经确认、Acknowledged已确认、Recovered已恢复、Cleared已关闭。当报警条件第一次满足时状态置为Active系统推送报警通知操作员在HMI上确认后状态变为Acknowledged确认动作记录操作员ID和时间条件恢复后状态变为Recovered但事件仍然保留在报警列表中供追溯明确处理后关闭记录状态为Cleared。这里有个细节已恢复但未确认的报警不能直接消失必须保留在报警列表中。现场很多安全规范要求操作员必须对每一个报警逐一确认哪怕这一刻报警已经恢复了也要确认一下“我看到了我知道发生了这件事”。SuperSCADA的报警列表在这方面严格按流程来。报警通知这一块我在服务端做了通知器接口。默认支持钉钉机器人、企业微信机器人、Webhook和邮件。Critical级别报警触发时还会直接调用短信网关接口把报警内容推给值班工程师和车间负责人。通知频次做了限流同一个报警点10分钟内不会重复推送超过3次防止半夜被同一报警轰炸。5.3 报警风暴的抑制机制报警风暴是工业现场的经典灾难场景。电网闪断、通信电缆被挖断这类事件会导致大量设备同时离线瞬间产生几百条甚至上千条报警。如果直接把所有报警全部推送操作员后台会被信息淹没真正的关键报警反而被刷掉了。SuperSCADA的报警引擎里加了两层抑制机制。第一层是关联抑制配置报警关联关系后当一个父级设备处于离线状态时子设备引发的报警自动降级为影子报警不推送通知只在“隐藏报警”查询里可见。第二层是限流抑制当系统检测到每分钟报警数超过设定阈值比如120条自动进入风暴模式后续报警只写入存储推送改为汇总合并告知等到报警速率回落后再恢复正常模式。这个抑制设计在电网闪断场景下非常有效。闪断那几秒钟几百台仪表的通信全部中断但系统不会刷屏几百条“设备离线”而是推一条“检测到多处设备离线已进入报警风暴抑制模式”汇总之后陆续恢复时也只推汇总信息。现场管理人员实战反馈说有了这个功能值班压力小多了至少能看清发生了什么。5.4 报警事件表设计与追溯能力报警记录和趋势曲线、操作日志是SCADA三个最重要的审计数据。报警事件表的数据模型我在设计时单独拆了出来和普通历史数据表分开因为报警数据有明显的电视结构特征产生、确认、恢复每次状态变化都有独立时间戳查询模式也不同。报警事件表核心字段包括点位Key、报警等级、报警内容、触发值、限值、产生时间、确认操作员、确认时间、恢复时间、恢复值、状态。版本设计确认后我补充了两张辅助表报警配置表和报警确认记录表。报警配置表存储了每个点位的报警上下限、死区、去抖时间、等级、是否启用确认记录表则存操作审计信息。有了这套数据结构追溯一个报警的完整生命周期非常轻松。从第一次超限到操作员确认再到现场恢复正常全过程可查而且操作员是谁、何时点的确认、当时触发值是多少都清清楚楚。这在应对客户审计、事故复盘时是不可或缺的底牌。6. 上线压测与踩坑实录断线重连风暴、页面卡死、时间戳错乱6.1 现场网络闪断引发的重连风暴SuperSCADA上线后遇到的第一个大坑就是断线重连风暴。当时现场有200多台设备连着同一台工业交换机某天施工队的电焊机意外把市电搞跳闸了交换机断电200台设备同时离线。等交换机恢复后我的采集程序里每个设备的goroutine都在专心重连200个连接同时撞向设备和Modbus TCP端口结果导致那台老PLC的通信模块卡死部分设备恢复了又被踢下线反复了很长时间才稳定下来。这个问题的根源是重试策略太简单。打了个补丁后我实行了指数退避加抖动设备连接失败后第1次重试等待500毫秒第2次1秒第3次2秒依次递增到上限30秒并在这基础上加入随机抖动避免所有设备退避步调一致。同时我把同一个通道下的设备重连请求做了串行化一台设备重连过程中同通道其他设备的连接请求排队等待。改完之后再遇到交换机断电恢复整个平台可以在几十秒内安静地恢复状态不再出现设备踢来踢去的情况。6.2 Modbus TCP的粘包、半包和处理时序开发Modbus TCP驱动时报文解析也踩了很经典的坑。TCP是字节流协议不是按报文边界传输的应用层自己需要处理粘包一个TCP包包含多帧响应和半包一帧响应被拆分成多个TCP包。这个问题的标准解法是用状态机解析在套接字读取循环里维护一个接收缓冲区先解析MBAP报文头根据报文头里的长度字段来确定本帧报文的总字节数再判断缓冲区内数据是否达到这个长度。数据不足就继续等待数据超出就截取一帧剩余数据继续参与下一帧解析。func (c *ModbusClient) readFrame() ([]byte, error) { header : make([]byte, 6) if _, err : io.ReadFull(c.conn, header); err ! nil { return nil, err } // header[4], header[5] 是长度字段高、低字节 length : int(header[4])8 | int(header[5]) frame : make([]byte, 6length-1) // 继续读取剩余字节 if _, err : io.ReadFull(c.conn, frame[6:]); err ! nil { return nil, err } copy(frame[:6], header) return frame, nil }事务ID的匹配也是一个容易出问题的点。SuperSCADA的Modbus TCP客户端支持并发请求也就是同一设备可以同时发出多个读请求响应返回后根据事务ID匹配对应的请求。最初我图省事把事务ID固定为0导致并发请求时响应错乱。后来实现的是一张自增ID表每个请求分配唯一ID收到响应后查表匹配。事务ID达到上限时自动翻转同时哈希表清理超时未响应的旧请求。6.3 32位浮点的字节序差点让整套数据报废我遇到过规格很小的坑一批温度变送器用Modbus读回来的数值有的通道显示正常有的通道数值大得离谱。排查了很久最后定位到是字节序配置问题。这批变送器的Modbus寄存器排序方式不同于我先前接触的设备。我的驱动默认按大端方式解析而它们的32位浮点实际是按小端存放的。修正方法其实很简单点位上配置字节序改为小端后数据立刻恢复正常。这里我想强调任何Modbus设备的点位拿到手后都先用Modbus Poll这类调试工具读一下原始寄存器值手动验证字节序再配置进系统。摸清设备的寄存器分布、字节序和数据格式比写好代码本身更花时间。不要嫌烦这一步省了后续在数据解析上耗的时间会成倍返还。6.4 前端页面越用越卡的元凶还有一次现场反馈说HMI页面运行半小时后开始变卡刷新页面能好一阵但过一会儿又不行了。这种问题十有八九是前端内存泄漏。排查发现泄漏点出在WebSocket消息处理里。我的订阅管理器每次收到推送消息后会构建一个点位更新对象通知图元刷新但有些图元销毁时没有注销自己的监听回调时间一长订阅管理器里积累了大量僵尸回调每帧推送数据都要遍历一遍浏览器内存不断膨胀。修复方式有两块。第一块是规范生命周期管理所有图元销毁时强制调用unsubscribe方法必须在绑定时登记一个释放函数防止回调泄漏。第二块是给推送器加了增量快照机制同一秒内同一个点位有两轮更新只推最后一次值不推历史中间值。加上这个改动之后现场页面连续跑了好多天内存都很稳定。6.5 时钟不同步导致的历史数据时间轴错乱最后一个坑来自现场工控机的系统时钟。某天客户说趋势曲线的数据点“跑到未来去了”打开数据库一看最近几个小时的历史表时间戳全部有问题有的记录时间戳比实际时间大了好几个小时。原因是现场那台工控机上跑着其他软件调整了系统时间而我的采集程序打的是本地时间戳系统时钟一调新数据按错误时钟写入旧数据就被覆盖了。调整时间把时钟同步拨正后数据才恢复正常。这个教训让我做了一个决定SuperSCADA服务端启动时如果检测到系统时间和上一次运行记录的时间差超过30秒比如系统重启或者被动调整时间会明确记录一条系统事件日志并校验所有点位是否进入Stale状态。如果后台配置了NTP服务器服务端还会定期触发系统校时。历史时间戳的产出逻辑不变始终以服务器当前时间为准但启动时检测时间跳变可以及时发现问题至少工程师能知道时间被修改过而不是在数据错乱之后才意识到。写在最后SuperSCADA和SuperHMI从最开始的一个采集小工具慢慢长成一整套轻量级SCADA平台中途又拆出了TopSCADA、TopHMI两条产品线。我把这段经历完整分享出来核心是想说明一件事在工业自动化这个领域稳定可靠的系统不是靠买某个万能软件堆出来的而是靠对通信协议、数据结构、前端渲染、报警逻辑每一个细节的较真和打磨。回头再看那个“要不要自研SCADA”的问题我的答案依然是肯定的。商用方案自有它的价值和场景但当你真正掌握整套系统的每个环节能根据现场需求灵活调整时那种主动权是任何现成软件都给不了的。后续我还会继续迭代这个系统把边缘计算、预测性维护这些方向一步步加进去。
返回列表