ARTICLE DETAIL

资讯详情

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

旁路协议转换桥:不动旧上位机,让老产线对接新TCP声光终端

旁路协议转换桥:不动旧上位机,让老产线对接新TCP声光终端 干产线自动化的这些年我最怕听到的一句话是“上位机源码找不到了你们别动它。”偏偏这次的项目就撞上了这个经典难题——车间要淘汰一款停产的老LED呼叫盒换成一台带声光和语音播报的新终端新终端只认TCP字节帧而老上位机走的是串口私有报文而且程序和协议文档一律没有。最后我用了“旁路协议转换桥”的思路把旧上位机原封不动仅仅在中间加了一个翻译层就把数据成功送进了声光语音终端的TCP字节帧协议里。这篇文章把整个改造过程完整拆开讲从方案选型、报文分析、串口监听到组帧、TCP重连、联调排错一步步写清楚希望能帮到正在和旧系统死磕的朋友。1. 项目背景一台“不能碰”的旧上位机1.1 旧上位机为什么动不得这台旧上位机是很多年前外包团队用C#写的跑在一台工控机上负责和12个工位的PLC通信再通过串口控制车间墙壁上的LED号码盒和蜂鸣器。工人按下工位上的呼叫按钮后PLC把信号送到上位机上位机再把一条固定报文通过RS232发出去老旧的LED盒点亮工位号并响铃。这套链路稳定跑了快十年车间对它非常依赖。问题出在后端设备上。LED号码盒停产了市面上替代品全是带TCP网口的声光语音终端支持自定义语音播报和RGB灯光控制。车间想趁这个机会把“只有声音和号码”的旧盒子升级成“语音喊话加灯光指示”的新终端。但这个上位机没有任何源码就算有源码重新编译、回归测试、停机切换的成本也极高车间根本不敢让它停。说白了这套系统已经成了一个“黑盒”黑盒外部接口能看黑盒内部逻辑谁也不敢动。这种场景在产线改造里太常见了。设备换新容易上位机不动才是硬约束。再加上旧报文格式只有抓包才能反推更没有现成文档所以整个改造的难点并不在“新终端怎么用”而在于“怎么在不碰旧上位机的前提下把它发出的旧报文变成新终端听得懂的TCP字节帧”。1.2 新终端想要什么声光语音终端其实不复杂。它本质就是一个可以联网的小盒子内置功放、喇叭和LED灯带通过网口接入局域网支持TCP协议。终端一般工作在服务器模式监听某个端口等客户端连上来后按照厂家私有字节帧格式下发命令设备就会播放指定语音、亮指定颜色的灯。我拿到的新终端协议是典型的自定义帧结构帧头固定为AA 55后面紧跟终端地址、命令字、语音编号、灯色、音量、播放次数等数据段末尾带CRC16校验最后以0D 0A收尾整体来看协议本身不复杂难点在于旧上位机那端没有任何文档报文只能靠抓包反推而且新终端要求的字段和旧上位机输出的字段完全对不上必须设计一套转换规则。1.3 改造目标与红线这个项目我给自己定了几条红线其实也是做类似改造时必须明确的边界第一旧上位机的程序文件和配置一律不动不允许任何重新编译、替换DLL、修改注册表之类的操作。第二原有的老LED盒子暂时保留作为后备手段。也就是说改造后新旧两套声光系统可以同时工作哪怕新链路出问题旧系统还能兜底。第三转换层必须独立部署、独立启停。它不能影响上位机和PLC之间的现有通信也不能给旧上位机增加额外延迟。基于这三条红线最终确认的改造目标就是一句话当旧上位机向原来的串口链路发送呼叫或报警报文时转换服务能实时捕获这些报文解析出工位号和命令类型再封装成新终端的TCP字节帧发送出去让新终端说出对应语音、亮出相应颜色的灯。2. 改造方案选型为什么是旁路协议转换2.1 三条路线的评估接到这种需求通常有三条路线可以走。第一条路线是直接改上位机源码或重新开发把新终端的TCP协议集成进去。这是最优解但前提是得有源码、有排期、有充足测试时间。本项目源码都找不到这条路线直接排除。第二条路线是在PLC侧做文章。因为呼叫信号是从PLC来的那是否可以在PLC程序里加一段逻辑让PLC直接同时驱动新终端听起来可行但PLC程序同样属于关键设备而且新终端是TCP协议老PLC没有网口或者程序加密根本不能改实施风险比上位机还大。再加上PLC修改同样涉及停机窗口和逻辑回归车间一样不接受。第三条路线是在旧上位机的串口通信链路上做一个“旁路协议转换桥”。原理很简单旧上位机发出的报文原本要走专用串口到老LED盒我们在中间插入一个监听点把报文复制一份出来由转换服务解析后翻译成新终端协议再通过TCP发出去。旧上位机完全感知不到中间多了一个“观察者”老LED盒也能继续收到原报文两边都不耽误。三条路线一对比答案已经很清楚只有第三条路线既不动旧系统又能满足新需求。2.2 旁路转换桥的落地形态旁路转换服务我选择做成一个C#的小程序部署在工控机上开机自启后台运行。它要做的事情可以拆成四步第一步监听串口。旧上位机原本用的串口号不能变所以通过虚拟串口软件创建一对互通的串口让上位机继续使用原串口号转换服务从虚拟串口的另一端读取数据。第二步解析旧报文。串口收到的是连续字节流需要按旧协议把一个个完整帧拆出来再解析出命令字和工位号。第三步构造新帧。把解析出的工位号、命令类型映射到新终端的语音编号和灯色定义按厂家协议拼出完整的TCP字节帧并计算CRC。第四步TCP发送。转换服务作为TCP客户端连接新终端保持长连接连接断了就自动重连确保不会因为一次网络抖动丢掉报警信息。这个方案还有一个好处整个转换服务是独立的进程和旧上位机零耦合。哪怕这进程挂了旧上位机到老LED盒的链路也照常工作系统不会因为改造而整体宕机。3. 协议解剖旧报文与新字节帧的映射关系3.1 旧上位机的串口报文开始动工之前第一件事是把旧报文抓明白。我通过虚拟串口监听抓了一大把报文反复核对后基本确定了旧上位机的报文格式AA 55 CMD 工位号 校验和 0A各部分含义如下AA 55固定帧头CMD命令字0A代表呼叫送料0B代表缺料报警0C代表解除呼叫工位号1到12对应车间12个工位校验和命令字与工位号之和的低8位例如0A 02 0C0A固定帧尾老LED盒收到的信号本质上就是“谁在叫、为什么叫”非常简单。按下呼叫按钮时上位机发送一次AA 55 0A 02 0C 0A老盒子就点亮2号工位并响铃解除时发送AA 55 0C 02 0E 0A老盒子复位。这个协议虽然简单但也代表了一类典型的老系统报文特征无长度字段靠固定帧头和帧尾对齐校验非常弱基本没有防错能力。解析的时候必须按固定长度来切帧不能依赖数据里的长度字段。3.2 声光语音终端的TCP字节帧新终端的协议则要规范得多。厂家文档给出的播放语音命令帧是12个字节偏移字段说明0帧头0xAA1帧头0x552终端地址0x013命令字0x10 播放语音0x12 停止播放4语音编号0表示不播放1~10对应预存语音5灯色控制0灭灯1绿2黄3红4黄闪5红闪6音量0~1007播放次数1~100表示循环8CRC高字节CRC16起始0xFFFF多项式0xA001覆盖0~7字节9CRC低字节10帧尾0x0D11帧尾0x0A终端通电后会在局域网内监听TCP端口5000等待控制端连接。连接建立后收到一条完整播放帧就开始动作播放完毕自动停止。如果想让终端停止播放并关灯就发送命令字为0x12的停止帧。要注意的一个坑是厂家文档里CRC字节顺序画的是“高字节在前”我一开始没注意直接按低字节在前发送结果终端完全不响应。后来把CRC字节交换位置才恢复正常。这种细节在联调阶段最容易卡壳后面会详细讲。3.3 映射规则的落表设计旧报文的信息是“命令字工位号”新终端的输入是“语音编号灯色”两者必须建立映射。我用了一个映射表放在配置文件里方便车间以后自己调整旧命令字含义新终端命令语音编号灯色0x0A工位呼叫送料0x10播放等于工位号黄色闪烁0x0B缺料报警0x10播放3号“请补料”红色闪烁0x0C解除呼叫0x12停止0不播灭灯这里有个关键设计普通呼叫的语音编号直接等于工位号所以2号工位呼叫会播放“2号工位请送料”5号工位呼叫会播放“5号工位请送料”。这个规则在一期需求里是最实用的因为12个工位语音文件都是预先录好的编号和工位号一一对应。缺料报警则固定使用3号语音不管哪个工位触发都播放“请补料”并亮红灯闪烁。映射表外置成配置后车间后来提出“把报警灯改成双色交替闪”“把某工位的提示音换成真人录音”等需求都只需要改配置文件或者换语音文件转换服务代码一行都不用动。这也是整个改造里性价比最高的设计之一。4. 实操实录从抓包到联调4.1 环境准备与工具清单整个改造用到的工具不算多但每一样都挺关键虚拟串口软件VSPD用于创建COM3和COM4的串口对。旧上位机原配置指向COM3转换服务监听COM4。串口调试助手用于模拟旧上位机发送报文或者直接观察串口数据。网络调试助手NetAssist用于模拟终端作为TCP服务器验证转换服务发出的字节帧内容。C#开发环境工控机上要是没有开发环境也可以直接把程序编译好拷过去装上.NET Framework 4.5运行库就行。工控机是一台老旧的Windows 7系统这一点也决定了转换服务不能用太新的跨平台方案用C#写个小程序最稳。开发机上编译时目标平台选x86因为老工控机上的.NET环境可能只装了32位运行库这个细节看起来小但很多人就是栽在这里。4.2 串口监听桥的实现串口旁路是整个方案里最微妙的一环。旧上位机独占COM3转换服务不能直接再去打开COM3否则会产生端口占用冲突。解决办法是用虚拟串口软件创建一对“内联串口”——COM3和COM4被绑定成一个管道数据从COM3进就会从COM4出反过来也一样。改造后的链路是旧上位机打开COM3并发送旧报文报文同时出现在虚拟串口COM4端转换服务打开COM4读取数据与此同时转换服务再把原始报文原样转发到物理串口COM5COM5连老LED盒。这样老LED盒的链路完全没变只是中间多了一个“复制转发”的动作。这里有个容易被忽略的问题虚拟串口对虽然透明但转换服务转发到COM5时会引入一个极小的延迟。实际测下来在9600波特率下每帧6字节转发耗时几乎可以忽略不计不会影响老LED盒的响应。但如果你在更高波特率或者更长的报文下做类似改造一定要实测一下转发延迟避免把老设备的响应时间拖长。4.3 报文解析与字节帧组包串口数据本质上是字节流不能保证一次Read刚好读完一帧。所以解析前必须做缓存和粘包处理。我整理了一个简单的解析器思路是先攒数据然后根据帧头AA 55找起点再按6字节固定长度切帧最后检查帧尾是不是0A。核心代码如下private readonly Listbyte _buffer new Listbyte(); private void OnSerialDataReceived(object sender, SerialDataReceivedEventArgs e) { int n _serial.BytesToRead; byte[] data new byte[n]; _serial.Read(data, 0, n); _buffer.AddRange(data); while (TryExtractFrame(out byte[] frame)) { ProcessOldFrame(frame); } } private bool TryExtractFrame(out byte[] frame) { frame null; if (_buffer.Count 6) return false; int head _buffer.FindIndex(b b 0xAA); while (head 0 head 1 _buffer.Count _buffer[head 1] ! 0x55) { head _buffer.FindIndex(head 1, b b 0xAA); } if (head 0 || head 6 _buffer.Count) { if (head 0) _buffer.RemoveRange(0, head); return false; } if (_buffer[head 5] ! 0x0A) { _buffer.RemoveAt(head); return false; } frame _buffer.GetRange(head, 6).ToArray(); _buffer.RemoveRange(0, head 6); return true; }解析到完整帧后ProcessOldFrame按映射表把它转换成终端的12字节帧。组帧的关键是CRC计算我用的就是标准Modbus CRC16算法public static ushort CalcCrc16(byte[] data, int offset, int len) { ushort crc 0xFFFF; for (int i offset; i offset len; i) { crc ^ data[i]; for (int j 0; j 8; j) { if ((crc 0x0001) ! 0) crc (ushort)((crc 1) ^ 0xA001); else crc 1; } } return crc; } public byte[] BuildVoiceFrame(byte voiceId, byte lampColor, byte volume, byte times) { byte[] frame new byte[12]; frame[0] 0xAA; frame[1] 0x55; frame[2] 0x01; // 终端地址 frame[3] 0x10; // 播放语音 frame[4] voiceId; frame[5] lampColor; frame[6] volume; frame[7] times; ushort crc CalcCrc16(frame, 0, 8); frame[8] (byte)(crc 8); // CRC高字节在前 frame[9] (byte)(crc 0xFF); frame[10] 0x0D; frame[11] 0x0A; return frame; }组帧只是手段真正的核心逻辑在ProcessOldFrame的映射处理上。代码先从旧帧取出命令字和工位号然后查映射表决定语音编号和灯色再调用BuildVoiceFrame生成12字节帧。整个链路从收到串口数据到发出TCP帧实际耗时基本都在1毫秒以内。4.4 TCP长连接与断线重连转换服务和新终端之间的通信我采用的是TCP长连接方式。工控机上电后转换服务就主动连接终端的IP和端口连接建立后保持住后续每一条播放命令直接在这个连接上发送。长连接的好处是响应快不用每次现连TCP而且能及时发现链路断开。但长连接必须处理断线重连。终端断电、网线松动、交换机重启等都会导致连接断开如果转换服务不自动重连那后续所有报警都会丢。重连逻辑我放在一个独立线程里每3秒检测一次连接状态断开了就重新连接private TcpClient _client; private NetworkStream _stream; private readonly object _sendLock new object(); public void ConnectLoop() { while (true) { try { if (_client null || !_client.Connected) { _client new TcpClient(); var result _client.ConnectAsync(_config.TerminalIp, _config.TerminalPort); if (result.Wait(TimeSpan.FromSeconds(3))) { _stream _client.GetStream(); Log(已连接终端 _config.TerminalIp : _config.TerminalPort); } } } catch (Exception ex) { Log(连接失败: ex.Message); _client?.Close(); _client null; } Thread.Sleep(3000); } } public bool SendToTerminal(byte[] frame) { if (_client null || _stream null || !_client.Connected) return false; lock (_sendLock) { try { _stream.Write(frame, 0, frame.Length); _stream.Flush(); return true; } catch { return false; } } }这里还要提一个细节TCP发送失败不一定等于连接已经断开尤其是写操作时可能当时没报错但数据其实没发出去。所以我除了依赖异常还在每次发送失败时把连接主动置为断开状态逼着重连线程尽快重建连接。另外如果报文被连续丢弃我会把最近一条未发送成功的离散信息记录下来联调时查日志会方便很多。4.5 三阶段联调步骤联调我分了三步走每一步的验证目的都很明确也可以说是避坑的关键。第一步是纯软件模拟。关闭旧上位机用串口调试助手打开COM4手动发送一条模拟报文AA 55 0A 02 0C 0A。同时用网络调试助手在电脑上创建一个TCP服务器监听5000端口。转换服务收到旧报文后应该马上把12字节的终端帧发到网络调试助手里。这一步主要验证组帧和CRC是否正确字节内容一眼就能看清。第二步是接真实终端。把网络调试助手的TCP服务器关掉转换服务改连真实终端IP。再次用串口调试助手发送模拟报文观察终端是否播放语音、灯色是否正确。这一步验证的是协议兼容性尤其是CRC字节序、终端地址这类问题只有在真机上才会暴露。第三步才是回到真实系统。把旧上位机恢复为打开COM3通过虚拟串口对进入转换服务。此时按下车间工位上的呼叫按钮PLC触发后旧上位机自动发送报文转换服务解析后推动新终端播报。同时我还确认了老LED盒也正常亮起。这一步通过后改造就算真正完成了。三个阶段的顺序不能乱。如果直接跳过第一步去连真实终端一旦终端没反应你根本判断不了是组帧错了、CRC错了、网络不通还是终端配置有问题。先用软件把帧内容验证清楚再接真机能把排错范围缩到最小。5. 常见问题与排查速查5.1 监听不到旧报文这个现象在联调初期很容易出现。常见原因有三个一是虚拟串口对没有配对成功COM3和COM4并不是真实互通的二是转换服务打开了COM4但旧上位机配置里打开的也是COM4而不是COM3两边抢同一个端口三是串口参数不一致旧上位机使用9600波特率而转换服务打开COM4时设置成了115200导致数据解析全是乱码。遇到这类问题我的排查习惯是先用串口调试助手分别打开COM3和COM4做一次自发自收测试。在COM3发一串数据看COM4能不能收到。能收到说明虚拟串口对没问题收不到就去设备管理器里删除串口对重新创建。串口参数方面新旧两个程序必须保持一致否则后面的解析工作根本无从谈起。5.2 报文解析总是丢帧或粘包串口转发的数据在应用层看来就是一段连续字节流一帧报文可能被拆成两次Read也可能一次Read里塞了两帧。如果解析逻辑只做“收一次数据就当成一帧来处理”必然会出现丢帧。解决办法就是我前面写的缓存加切帧逻辑所有收到的字节先追加进缓冲区然后循环去缓冲区里找帧头、切帧、校验帧尾把完整帧逐个处理掉。千万不能依赖“Read一次刚好6字节”这种侥幸。另外如果报文里频繁出现假帧头也就是数据域正好有0xAA也容易误切。老协议因为没有长度字段只能靠帧尾0A和校验和双重判断来增强容错。凡是校验失败的一律丢弃并记录日志方便定位。5.3 终端收到帧但没动作这种情况基本可以锁定在协议细节上。我踩过的坑有两个第一是CRC字节顺序反了。厂家文档里写“CRC高字节在前”我一开始没仔细看按Modbus传统的低字节在前发送结果终端毫无反应。第二是终端地址不匹配。终端配置软件里把设备地址设成了2但我组帧时一直用默认的0x01自然无法驱动终端。排查这类问题最好的工具就是网络调试助手。让转换服务连到网络调试助手把实际发出的12字节帧原样打出来然后逐字节和厂家协议文档比对。帧头对不对、地址对不对、命令字对不对、CRC计算范围对不对、字节顺序对不对一项项排除很快能定位到具体问题。千万不要直接改终端配置去“迁就”程序先把程序这边验证对了再说。5.4 如何让转换服务稳定可靠地长期运行转换服务如果只在调试时跑一跑那怎么都好说。但车间是7×24小时生产转换服务必须做到随工控机开机自启、异常退出能自动重启。我是把它做成了Windows服务用sc create命令注册设置开机自启。同时每次处理完一帧报文都会写日志日志文件按天滚动保留30天。这样一旦现场反馈“新终端没响”远程看一眼日志就知道是旧上位机没发报文还是映射表没匹配上还是TCP链路断了。还有一个容易被忽略的细节工控机的Windows防火墙可能拦截出站TCP连接。虽然默认情况下出站一般不拦但某些公司安全策略会做得比较死。如果连接失败除了检查IP和端口也要记得看一眼防火墙和终端所在网段是否有隔离。稳妥的做法是提前在工控机上把终端IP和端口加入白名单。排查问题做多了之后我自己养成了一个习惯日志一定要带上收发方向和时间戳。比如[14:30:12.345][RX串口] AA 55 0A 02 0C 0A、[14:30:12.346][TX-TCP] AA 55 01 10 02 04 60 03 3C 21 0D 0A。这样排查时一眼就能看出转换服务到底有没有收到旧报文、有没有发出新帧问题出在上游还是下游清清楚楚。这次改造做下来我心里最踏实的一点是旧上位机从头到尾没有改过一行代码老LED盒子也还在原位正常工作新终端只是在一旁“多听了一句”就把语音播报和灯光提示加进了原来的车间系统。后来车间陆续提了几次需求比如换语音文件、调整报警灯闪烁模式都只是改配置文件的事根本不碰程序。这种“能不动的系统坚决不动用旁路思维解决新旧系统对接”的思路我觉得比这次具体的技术实现更值得带走。如果你也正在面对一台老掉牙但是不能停的系统不妨先别急着改它想想能不能在边上架一座桥。
返回列表