ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

旧上位机对接新终端:协议网关实现声光语音播报改造

旧上位机对接新终端:协议网关实现声光语音播报改造 旧系统被“焊死”是很多工控老厂都逃不过的命。七八年前甚至十几年前用VB6、VC6、或某个早已停产的组态软件写的上位机还在产线上24小时跑着报警逻辑、联锁条件、报表全在里面。现在甲方说“我们要上声光语音终端最好能把具体哪条线、哪个工位、温度高了这个事情直接喊出来”你问能不能改上位机佛系一点的回复“源码找不到了”实在一点的“厂商已经不在了”再实在一点的是“要改可以签免责协议出问题你担着”。所以这个项目真正的难题不是“怎么去写新代码”而是“怎么完全不碰旧系统却让它听话地把消息交给新终端”。我这次干的事情就是在旧上位机和声光语音终端之间塞了一层看不见的“翻译官”旧上位机该写数据库写数据库该往串口吐数据吐数据一概不碰新终端只认TCP字节帧那我就在中间做协议适配、状态映射、帧转发。整个过程走下来真正耗费时间的地方不是写那几千行代码而是把“旧系统到底用什么方式吐信号”这件事摸清楚以及把字节帧设计得让终端厂家的协议栈不会挑刺。这篇文章把这套改造的真实过程、字节帧设计思路、桥接程序的实现细节、以及现场踩过的坑全部记录下来给正要接手同类“旧系统对接新设备”项目的朋友做个参考。1. 整体改造思路不是拆旧楼而是加电梯1.1 先搞清楚旧上位机的“数据出口”有哪些旧上位机之所以让人不敢碰是因为它往往承担着产线联锁和报警的核心职责改一行配置、动一个变量都可能导致整个控制逻辑行为变化。所以我的第一原则是只观察不干涉。要做到这一点核心是找到旧上位机那些“只往外吐、不需要收”的数据出口。常见可撬动的出口有以下几类数据库落盘很多老上位机每小时或每条报警都会往本机或服务器的数据库里写一条记录SQL Server、Access、MySQL都有字段可能乱但时间戳和报警代码基本在串口输出老系统很多用串口接打印机或LED屏发送的都是明文报文通过485转232直接并接就能监听OPC/Modbus TCP Server稍微新一点的上位机软件自带OPC或Modbus TCP服务端订阅变量或轮询寄存器即可拿到实时状态文件导出/报表生成部分老系统会在指定目录定时生成TXT、CSV报表。这个项目里旧上位机是一套某国产组态软件写的电力监控系统它有一个Access数据库文件作为历史报警存储同时还有两路串口输出用来驱动走廊里的老式LED显示屏。甲方说新系统要能播报具体报警内容和工位信息并且要联动声光报警灯。我评估之后决定采用“数据库轮询为主、串口并接LEC显示内容解析为辅”的方案。原因很简单数据库里的字段完整度最高而且读库不会对运行中的组态软件有任何脉冲式干扰串口并接方式则作为冗余能拿到LED屏上正在滚动显示的那条内容两者互相校验防止数据库写入延迟导致漏报。1.2 为什么选定“中间协议网关”而不是改上位机或并联硬件IO甲方最开始跟我说“你们能不能直接把终端接在报警灯的继电器回路上”这确实是最省钱的办法一台声光语音终端本身就带开关量输入口给它一个干接点信号就能播报。但问题在于干接点只有“有/无”两种状态我们现场有十一条监测点每条监测点报警的内容不一样、播报语音不一样、联动灯色不一样如果全部用开关量输入要么做十一组继电器阵列要么就只能在终端上写满“1号温高、2号温高……”的固定音频后续扩展一条测点就得改硬件。所以干接点方案被否掉任性一次会有后患。剩下的路基本就是网络接入。我选原生TCP字节帧而不是Modbus TCP不是追求新潮而是因为这个品牌的声光语音终端对Modbus TCP的支持很“残废”寄存器表只开放了开关控制位语音播报内容不可以通过寄存器下发更新只能通过私有字节帧协议上传。既然终端的协议已经定死是私有TCP字节帧那中间网关把各类旧系统信号统一翻译成这种帧格式就是了。用TCP还有两个额外好处第一产线上本来就有工业交换机一条网线就能把终端接到网关所在工控机上不需要额外布485总线第二TCP协议有完整的连接状态管理、超时重传机制和串口收发相比至少不用纠结“终端把嗓子喊哑了没听到”这种字节丢失问题。1.3 改造方案的整体架构整个系统运行在一台Windows 10工控机下这是原来旧上位机的宿主为了不动产线拓扑我把桥接程序也装在这台机器上。架构大致是数据采集层定时轮询Access报警库同时监听串口并接过来的LED屏报文协议适配层把采集到的报警信息按测点、等级、内容聚类去抖去重复形成统一的结构化报警事件TCP推送层将报警事件按声光语音终端私有协议组帧通过TCP客户端连接主动推送给终端辅助维护模块本地日志、断线自动重连、看门狗、配置管理。听起来复杂实际拆开核心就是一张帧格式表和一两个线程。但往深了走每个环节都有很多值得展开的细节。2. 字节帧协议设计终端要的是什么2.1 原厂协议的帧结构拆解做设备接入第一步永远是拿原厂协议文档如果原厂连文档都不给那就只有一条路抓包。这个终端厂商还算配合给了一份《语音播报终端TCP协议V1.3》虽然是十年前的排版风格但关键信息都有。协议的核心是终端作为TCP Server默认监听7000端口上位机作为客户端连入一条完整的控制帧最长不超过256字节格式如下字节位置字段名长度说明0帧头2固定为0xAA 0x552帧总长1从帧头开始到校验位之前的字节总数3命令字10x01播报请求0x02状态查询0x03复位等4设备编号2标识哪一台终端大端0x0001~0xFFFF6数据区长度1设备编号之后到数据区结束的有效字节数7数据区N语音播报内容、音色、音量、灯色等7N校验1异或校验从帧头到校验前一字节全部参与异或这里有个细节值得提出来数据区里不是直接放中文文本而是放“语音ID”终端内部固化了一段段语音资源上位机只需要告诉它要播哪一条。这个设计在工业声光终端里很常见相当于把一套语音库存在终端里上位机只负责按编号调取。但甲方这次的需求里存在一条报警信息包含动态数据的场景比如“3号线冷水机温度87.5度”875这个数值是实时变化的。我联系原厂问了一下他们终端里有一个“组合播报”功能把语音资源ID 12设为“当前温度”ID 13设为“摄氏度”动态数值部分通过整型的数值ID来拼接比如在数据区里这样组织起始ID12表示播放“当前温度”中间字节表示数值变量终端内部会按整型或浮点方式读出来拼在语音里结束ID13表示播放“摄氏度”。这样既实现了动态内容播报又不需要在终端上重新合成音频。设计这套逻辑需要搞清楚字节序终端用的是大端如果按小端发过去数值会变成另一个数字语音就会报出完全错误的温度。2.2 私有字节帧与标准协议的取舍第一次做这种对接容易犯的毛病是“看到TCP就想到Modbus”。很多从PLC背景转过来的工程师一上来就问“支不支持Modbus TCP”甚至在设备不支持的时候还想自己写个Modbus映射层。这里我需要说一个判断标准如果新设备已经有了完整可靠的私有协议就不要为了“标准化”而在上面套一层Modbus。原因有三。第一私有协议往往更贴近设备特性比如这个终端的数据区里可以直接传组合播报序列Modbus TCP的保持寄存器规约就很难优雅地表达“先播报后数值再播报”这种时序性内容硬套还得做寄存器状态机复杂度会明显增加。第二Modbus TCP本身的MBAP报文头有6字节固定开销虽然不多但在这个语音终端只有几十条并发播报需求的场景里没意义。第三也是最重要的减少中间翻译层就减少了调试工作量直接抓包对照原厂文档一步到位。但有些情况下我会反过来建议直接用Modbus TCP如果声光语音终端不是主角而是整个SCADA系统里众多从站之一且DCS或上位机组态软件原生支持Modbus TCP驱动那么用Modbus接入反而是让自己从“私有协议开发”中解脱出来的最优解。总结就是选择什么协议取决于你在架构中扮演的角色以及你手上有多少时间去调试私有协议。2.3 心跳、重连与多帧交互声光语音终端这类设备因为现场经常有断电、急停、重启的情况它的TCP Server一旦检测到客户端断开通常会立即释放连接。而很多组态软件默认的TCP连接方式是“长连接一次一直挂着”一旦中断终端不会主动重连网关需要网关侧能感知断线同时能自动重连。在协议设计层面上我还额外给网关加了一个心跳帧。心跳帧数据区为空命令字为0x04每5秒发一次终端收到后不回应应用层ACK但这不重要因为TCP本身会返回ACK。心跳真正的价值是维持NAT或中间交换机的会话表不超时以及让终端侧的通信模块在空闲时保持活动状态。因为有些终端的底层固件如果长时间收不到任何字节会认为这是一个死连接然后默默把Socket回收了但TCP层未必能及时反映出来应用还在傻等。还有一个比较典型的场景是声光终端支持多台同时在线还是只支持单连接原厂文档写的是“最多支持4个客户端同时连接”但这里的客户端指的是它的调试软件、网页、手机APP这些通道协议帧里的设备编号才是区分终端的根。我在做部署方案时让网关按“一设备一连接”的方式去维护保证每台终端一个独立TCP连接避免多个终端混在一个连接里造成设备编号错乱。3. 桥接程序核心实现从数据到字节帧3.1 技术栈与部署方式选择桥接程序我用了C# .NET Framework 4.7.2原因是这台工控机上的旧上位机是32位程序系统环境非常保守我不想为了跑一个网关程序再去装更高版本的.NET运行时惹出兼容性问题。C#做这种轻量级协议适配确实很顺手TCPClient、串口、数据库访问都有成熟的类库部署成Windows服务可以做到开机自启、异常自动重启。语言选择上我评估过Go和Python。Go的编译产物确实是一个静态exe拷贝到工控机上就能跑不太依赖运行库非常适合这种老旧Windows环境Python则因为在现场总是缺一堆依赖还要处理编码问题除非是拿来做临时脚本或数据分析否则不建议作为常驻服务。我选C#的另外一个考虑是这台机器本身装了Visual Studio的一部分运行库旧上位机开发商的残留用C#开发的程序在部署时风险最小。如果现在的需求是快速原型验证我会推荐Python脚本配合pycharm远程调试毕竟脚本改起来快。但生产环境里一个“不要老是要人去重启”的稳定服务C#的Windows Service或Go的SVCPackage是更合理的选择。3.2 数据库轮询与报警“边沿检测”旧上位机往Access数据库写报警记录时每条记录有一个自增ID还有一个时间戳字段以及一个报警确认标志。我读取这个库时只需要记住上次读到的自增ID最大值然后每次轮询把大于这个ID的记录拉出来处理即可。这种方式最怕“脏数据”旧上位机在某些情况下会往历史表里回补数据或者因为写入事务冲突导致ID不是严格递增。为了应对脏数据我做两层防护。第一层拉取时增加时间过滤条件只处理三分钟之内的记录超过三分钟的历史回补即使漏掉也不影响实时性第二层对报警代码与测点号建立一个“状态机”同一个测点只有在“从无报警变为有报警”的状态跳变时才会触发播报如果持续报警且已经播报过一次则不再重复播报。这就是很多人容易忽略的“边沿触发还是电平触发”问题。新上位机开发时这个逻辑都是在PLC或组态脚本里做的现在走到网关层就必须要自己实现去抖。比如有一个液位报警因为现场液面波动三秒内有五次报警和复位交替如果不去抖语音终端就会连续播报五次。我给这个测点设了2秒去抖时间在2秒内不管来多少条报警记录只取第一次报警触发做播报其余做日志记录但不推送。3.3 关键代码TCP组帧与推送这是最常见的一段组帧逻辑集中说明组帧参数public byte[] BuildVoiceFrame(int deviceId, byte[] voiceData, byte volume 10) { int dataLen 2 voiceData.Length; // 设备编号2字节 语音数据 int totalLen 7 voiceData.Length; // 帧头2 总长1 命令1 设备2 数据区长度1 数据区N 校验1 byte[] frame new byte[totalLen]; frame[0] 0xAA; frame[1] 0x55; frame[2] (byte)totalLen; frame[3] 0x01; // 播报命令 frame[4] (byte)((deviceId 8) 0xFF); frame[5] (byte)(deviceId 0xFF); frame[6] (byte)dataLen; Buffer.BlockCopy(voiceData, 0, frame, 7, voiceData.Length); byte xor 0; for (int i 0; i totalLen - 1; i) { xor ^ frame[i]; } frame[totalLen - 1] xor; return frame; }对照帧格式表可以看到帧头、总长、命令字、设备编号、数据区长度、数据区、校验各就各位。异或校验在这里很关键有些新手上位机对接时不太重视校验但在这个场景里一个错误的字节如果正好把“语音ID”改了可能播报出完全错误的内容比不播报更危险所以校验必须严谨。推送端的核心逻辑用一个队列加一个TCP客户端完成。所有报警事件先进入ConcurrentQueue然后由后台发送线程统一出队并调用BuildVoiceFrame发送。这个设计的好处是数据库轮询线程、协议适配线程、TCPIO线程不会互相阻塞终端如果暂时断网报警事件就积压在队列里网络恢复后按顺序逐条补发。3.4 语音内容映射与配置管理声光语音终端的数据区里只能放语音ID和数值变量所以我把“报警代码—语音ID—测点名称—报警文本”做成一张映射表保存为一个JSON配置文件。这样做的好处是以后新增一个测点只需要修改JSON文件不需要重新编译网关程序。截一段配置示例{ AlarmMappings: [ { AlarmCode: ALM_TEMP_03, DeviceId: 1, VoiceId: 12, UnitVoiceId: 13, LoopName: 3号线冷水机, AlarmText: 当前温度过高 } ], TcpConfig: { ServerHost: 192.168.10.20, ServerPort: 7000, HeartbeatIntervalSec: 5, ReconnectIntervalSec: 3 } }注意这里AlarmCode的取值是读自旧上位机数据库的原始查报字段基本不需要我去猜它是啥含义直接按查报字段查字典即可。但如果旧上位机没有报警代码只有描述文本那就要用正则或模糊匹配来映射。我的经验是能用代码匹配就绝不用文本匹配一旦旧系统的中文描述里有两三个空格或者标点差异模糊匹配就是一场灾难。4. 现场调试与问题排查实录4.1 用抓包验证字节帧是否正确第一步永远是抓包验证帧格式。平时开发上位机时我经常用Wireshark的过滤功能直接看TCP载荷但这里还要提醒一个细节字符串过滤会对中文编码和0x00字节有干扰最好直接用“Follow TCP Stream”功能把原始字节流以hex dump方式拷出来然后手动对比帧格式表。我当时抓到的第一帧就发现一个问题终端要求数据区长度是“设备编号之后到数据区结束的有效字节数”但原厂文档这句话写得很含糊我一开始把设备编号的2字节也加了进去结果终端识别到数据区长度过长直接丢弃整帧。后来用SocketTool模拟终端一份一份地回放抓到的字节流再对照协议文档终于确定设备编号虽然放在数据区之前但它既不属于数据区也不影响数据区长度计算。这个文档歧义问题如果在没有网络调试助手的环境下排查非常容易卡住。建议所有做协议对接的工程师在正式联调前先用一个“TCP Server模拟器”把设备的协议栈虚拟出来把自己的组帧程序往死里测帧头错误、长度错误、校验错误、半包、粘包、乱序全测一遍。等模拟器跑稳了再去现场连真机能把现场调试时间压缩一大半。4.2 常见问题速查表以下是这次改造中实际遇到过的问题整理成速查表故障现象排查方向解决办法终端长时间不播报终端LED显示是否在闪烁/重启抓包看是否有数据发到终端端口检查TCP连接是否建立用SocketTool模拟终端验证组帧是否正确语音播报内容错乱字节序问题尤其数值变量将温度等数值字段改为大端发送参照终端文档确认播报内容正确但重复多遍报警状态未做边沿检测网关增加状态机同一个测点同一条报警只播报一次每隔十几秒连接断开终端固件空闲连接自动回收启用5秒心跳帧维持连接活跃终端重启后网关无法连接TCP重连退避策略不佳设置3秒重连间隔增加最大重连次数限制并记录告警日志中文内容乱码编码不一致终端语音播报使用的是语音ID不在数据区传中文文本如必须传文本确认终端要求UTF-8还是GBK数据库轮询时漏报自增ID回补/写入事务冲突增加时间窗过滤三分钟以内记录才处理声光报警灯不亮灯色指令与播报指令是否分开查看终端协议可能需要单独发一则灯色控制帧而不是混在语音帧里最典型的坑其实是第一条加第二条的组合终端那边看起来连接正常网关日志也显示发送成功但终端就是不动。最后发现是我把语音ID和数值ID发送的先后顺序搞反了。终端协议要求必须“先语音ID、再数值ID、再结束ID”的顺序我按照常规思维发成“数值ID、语音ID”于是终端解析的时候把数值当成语音ID去找语音资源找不到就跳过整个播报流程卡住。还有一个坑是Windows防火墙。这个工控机上装的是第三方安全软件它默认拦截了来自终端方向的入站连接。网关作为客户端主动连接终端时不受影响但终端状态查询命令需要网关监听一个临时端口等待终端响应而系统的防火墙把入站包全丢了。排查了半天最后在安全软件里给网关程序放行了全部端点问题立刻消失。这件事提醒我在做这类项目时做“双向连接测试”非常必要不要只测网关到终端的单向通信。4.3 上线后的稳定性保障报警推送这块最怕的是网关程序自己死了或者TCP连接不可用了但线程还挂在那边没有任何提示。我在上线前加了三层保障。第一层Windows服务失败自动重启。把网关注册成Windows Service设置“第一次失败重启、第二次失败重启、第三次之后仍然重启”这样即使程序异常崩溃也能自动恢复。第二层程序内部看门狗。开一个定时器每30秒检查一次网络连接状态和数据库轮询线程的存活标记。如果连续两次检查发现推送线程阻塞超过10秒就强制重建TCP连接、清空消息队列并写入诊断日志。第三层异常日志与“报警风控”。我特别加了“连续推送失败N次暂停推送”的逻辑如果网络长期断线事件队列会无限积压内存占用不断上涨等网络恢复后会突然爆发式补发几十条播报直接影响现场工人听感。所以我将队列长度限制为200条超过200条时丢弃最旧的事件同时把丢弃数量和最新条目的报警信息写入日志。生产环境的稳定运行有时候比“完整无缺”更重要现场只需要知道“最近一条重点报警是啥”。5. 改造效果与后续扩展方向整个改造周期从摸清旧上位机数据库结构到协议联调完成一共花了一周时间。旧上位机系统没有重启过组态软件一次都没被中断数据库查询都是只读操作对它的影响接近于零。而新的声光语音终端现在能准确播报哪条产线、哪个测点、具体温度数值并同步闪红灯和响警铃。甲方最满意的一点就是“你们没有动我们原来的系统”。这个改造方案接下来还有几个明确的扩展方向。第一这个协议网关目前只支持这一种声光终端但它内部的“采集适配—状态映射—协议推送”框架是通用的。接下来可以将同一套采集层对接多个厂家的语音终端、短信猫、微信推送服务只要在协议适配层新增一个“target”适配器即可。第二如果旧上位机连数据库都读不到但有OPC接口那网关可以再加一个OPC UA客户端用订阅模式实时取变量数据实时性比轮询数据库好不少。但需要注意OPC和工控组态软件的变量绑定关系很敏感一定要先确认旧上位机允许外部客户端连接。第三针对一些老工位没有网络环境的情况这款网关也可以改装成串口转发模式把字节帧打成串口数据包发到终端只要在推送层换一个“SerialTransport”逻辑大体不变。我个人的体会是工业改造项目的大部分阻力往往不在技术而在“改旧系统”这件事本身。技术实现并不难以最难的永远是在“尽量不去影响正在运行的系统”的约束下找到那个最薄弱但又最稳妥的借力点。第一次做现场调试时我依然会紧张但“用网关做翻译”这条路走通之后类似的场景已经成了我最常用的处理手法。最后分享一个经验无论设备厂商给你的协议文档看起来有多严谨拿到手之后先自己画一遍帧结构图并对着抓包工具逐字段核对。文档只告诉你“这个字节代表什么”不会告诉你“这个字节到底该由谁来计算”很多歧义只有在联调现场才会暴露。协议适配这种事多花半小时做模拟验证就能给现场省下半天时间。
返回列表