尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

老上位机TCP字节帧对接声光报警终端:工业现场最小侵入改造实录

老上位机TCP字节帧对接声光报警终端:工业现场最小侵入改造实录 上个月我接到一个挺棘手的活儿一条跑了九年的装配线上甲方要求新增一套声光语音报警终端把每个工位的完工、不良、堵料状态实时播报出来。按常规思路这活儿得动产线上那台老上位机可它偏偏是设备厂商配套的闭源程序不说源码连配置界面都只有几个只读页面旧上位机根本不肯改。厂家倒是愿意出手报价一台新上位机加通讯对接小二十万还得停机拆装。最后我把思路落在了老上位机的原生TCP能力上——它程序封闭但系统层还开着网络端口有远程数据发布之类的功能。只要在它的配置页里定义一个字节帧格式把现场状态按帧发出来外面套一个轻量网关就能把数据翻译成声光语音终端听得懂的指令。这篇文章就是这次改造的完整记录从帧格式设计到现场踩坑全部复盘。1. 为什么非要动这条产线的上位机一次“不能改代码”的改造请求1.1 现场的真实处境一台运行多年的闭源上位机这套设备是2015年前后投产的装配线工控机用的是当时很常见的 Windows 7 嵌入式版本上位机软件是设备厂商用 VC 写的监控程序负责采集 PLC 数据、显示工位节拍、记录产量和不良品数。软件本身没有任何插件机制没有对外开放的 API连配置文件都做成了加密的只读页面。但现场的情况很“微妙”这台机器虽然老却特别稳。连续运行七八年除了换过一块硬盘基本没出过事。产线的节拍、工艺参数、人员操作习惯都围着它转。甲方设备科长原话是“你敢动它我就敢动你”。所以他们一开始的诉求非常明确不要动上位机程序不要影响产线运行但新买的声光语音报警终端必须能响、能闪、能播报。这个需求放到日常项目里很容易被理解成“加一个短信网关”或者“接一块I/O板”但真正做进去才发现难点不在于声光终端怎么接而在于怎么把上位机脑子里的“现场状态”安全地取出来。1.2 需求拆解不只是响铃而是要“按工况播报”甲方采购的是三层式声光语音终端红黄绿三色灯自带语音模块可以预录几十条语音也支持外接喇叭。听起来功能很全但供应商只给了RS485通信指令集剩下的联动逻辑一概不管。真正要实现的场景我列了一下每工位完成一个节拍语音播报“1号工位完成”绿灯闪一次不蜂鸣出现不良品语音播报“1号工位不良请处理”红灯常亮蜂鸣慢速前工序堆料堵塞语音播报“前工序堆料请及时处理”黄灯快闪蜂鸣快速设备故障停机语音播报“设备异常请检查”红灯快闪蜂鸣急促。这些事件对实时性要求并不算苛刻2秒以内能触发就算合格。真正苛刻的是准确性和可维护性不能把A工位的信息播到B工位不能因为一两帧误码就乱报警出问题以后要能查日志、能回放。搞清楚需求以后我反而松了口气——这活儿的难点不在“能不能实现”而在“怎么用最小的代价实现”。1.3 供应商方案 vs 自己改造的成本对比厂商给的方案是整体更换上位机软件把声光终端联动做进去。报价单里写着“上位机软件改造联调费”加起来接近二十万还要求停产两天做切换和验证。这条线一天产能值多少钱甲方比我们清楚所以这个方案基本被否掉了。我们这边给的方案是利用旧上位机自带的TCP数据发布功能配置一套私有字节帧每100ms向新增的协议网关推送状态字网关负责解析、映射再通过RS485下发给声光语音终端。整个新增部分就一个巴掌大的ARM小主机加一块RS485板卡硬件成本几百块开发加现场调试一个多星期搞定。后来这个方案能落地核心原因就一条没有对旧上位机做任何代码级改动只改了几个配置项。即使后面出了问题把配置改回去、网关断电产线立刻恢复原样。2. 选型终点为什么只有原生TCP字节帧这条路能走通2.1 常见对接方式比了一圈接手这个需求以后我先按老经验把常见的工控对接方式过了一遍。每种方式看着都有道理但放到这个现场都各有各的别扭。对接方式对旧上位机的要求侵入性实时性本场景结论OPC UA/DA上位机需支持OPC Server高基本要装软件好闭源系统装不了放弃Modbus TCP上位机需支持Modbus从站中配置复杂好老软件不支持放弃数据库中间表上位机需把状态写入数据库极高完全没有可能一般闭源软件不会自己写库放弃原生TCP自定义字节帧上位机有任意TCP数据发布能力低仅配置好最终采用我特意把这几种方式列成表是想说一个容易被忽视的点很多老上位机虽然界面封死了但它的网络通信功能往往还活着。尤其是设备厂商为了远程维护方便十有八九在系统里留了一个“数据上报”或“远程监控”的开关只是现场没人用过。2.2 为什么选原生TCP兼容性与实时性的平衡这台老上位机的组态软件里藏着一个叫“透明数据发送”的页面。它可以配置目标IP和端口把软件内部变量按指定字节偏移填进一帧数据里然后周期或变化触发地向目标端口发送。说白了它就是个裸socket发送端不管你外面怎么解析它只负责把字节流发出去。这个功能简直是量身定做的接入点。Modbus TCP、OPC这些协议依赖双方有共同的语义层而原生TCP字节帧把语义层彻底拿掉了——上位机只负责把状态字节塞进帧里网关按约定解析两边各干各的。这种“低语义”的对接方式恰恰是对付闭源系统的利器。实时性方面100ms一个周期TCP加上局域网延迟整个链路到声光终端动作基本在300ms以内完全满足现场“2秒内触发”的要求。2.3 整体架构中间网关的定位和硬件选择最终的网络架构用文字描述就是旧上位机作为TCP客户端主动向网关发起连接网关是一个ARM小主机上面跑Python写的TCP服务端和协议转换程序网关通过RS485总线连接三台声光语音终端所有声光联动逻辑全部收敛在网关上上位机不感知终端的存在。为什么让上位机做客户端、网关做服务端这里有个现场经验旧上位机软件的透明数据发送功能通常只支持主动连接不支持被动监听而且工控机在产线局域网里有固定IP它对外的出站连接比入站连接更容易被防火墙放行。网关做成服务端IP和端口固定调试也顺手。硬件上我选了一款带金属外壳的ARM工业小主机双网口、带隔离额外插了一块USB转RS485模块。供电直接接现场24V没用什么昂贵的工业协议转换网关——因为帧是私有的成品网关也未必认识反而不如自己写程序灵活。3. 字节帧格式设计从一条指令到一套完整的消息协议3.1 帧结构帧头、长度、命令、数据、校验自己定义协议最容易犯的毛病就是一上来就堆功能。我这次刻意把帧格式压到最简单核心原则一句话能用单字节表达的绝不用双字节。最终的帧结构是这样一个表格字段长度字节含义示例值帧头2固定0xAA 0x55AA 55长度1命令字数据区校验的总字节数04命令字1消息类型01数据区N播报索引、灯色码、蜂鸣模式等08 05校验1异或校验取命令字与数据区所有字节的异或结果0C长度字段的设计有个小讲究它算的是“命令字数据区校验”的总长度而不是单纯数据区长度。这样解析端拿到长度以后可以直接算出整帧总长度等于2帧头 1长度 长度字段值不用再拐弯抹角地加加减减代码里少一个出错机会。3.2 命令字定义按业务场景设计命令字是整个协议的中枢我按照现场实际会发生的消息类型设计了四类命令字方向含义数据区说明0x01上位机→网关触发声光事件播报索引1字节 灯色码1字节 蜂鸣模式1字节0x02上位机→网关复位已触发事件事件ID 1字节0x03上位机→网关心跳序列号1字节0x04网关→上位机终端状态回读状态位1字节灯色码也做了统一归档0x00灭灯、0x01绿色、0x02黄色、0x03红色、0x04绿色闪烁、0x05红色闪烁、0x06黄色闪烁。蜂鸣模式则是0x00静音、0x01单次短鸣、0x02连续慢鸣、0x03连续快鸣。这里我想多说一句命令字和数据区的枚举定义不要光写在代码里整理成一张表放进项目文档现场调试时对照着抓包看效率完全不一样。3.3 校验和与字节序细节决定成败校验我选了单字节异或而不是CRC16。理由很现实帧短、局域网、偶发干扰异或校验拦截随机错误已经够用CRC16虽然更强但让老上位机自动追加校验的配置项不支持反而要多写一层脚本。做技术选型不只看“最好”更要看“最合适”。字节序方面我明确约定多字节字段一律大端在前。虽然当前命令字和数据区清一色单字节看不出大小端问题但后续要扩展16位的产量计数或者时间戳时这个约定能避免大坑。实际上后来在调试中就差点因为一个16位字段的字节顺序错乱出大事这个放到后面踩坑部分细说。格式定完以后我给网关侧和测试脚本同时落了代码协议才算正式生效。纯粹靠口头约定搞协议最后一定以互相看不懂收场。4. 两端代码落地上位机透明发布与网关拆包服务4.1 上位机侧配置透明数据发送不写一行代码前面说过老上位机软件里有“透明数据发送”功能把网关的IP和端口填进去以后关键就是配置帧结构。我在配置页面里做的事情大致是这些填写目标地址网关IP192.168.10.88端口9000选择发送方式按周期发送周期设100ms勾选“自定义帧头”帧头填入AA 55把需要上报的变量映射到数据区比如产线状态字映射到偏移3工位编号映射到偏移4选择“自动追加异或校验”软件会在帧尾自动算好校验字节。整个配置过程不到二十分钟没有编译一行代码。这才是这个方案最值钱的地方——对外宣称“不改上位机”实际也确实没动程序。你可能会问万一老上位机连透明数据发送都没有怎么办那这招确实不适用只能从PLC侧旁路取数但那就是另一个故事了。在正式让上位机发送之前我强烈建议先用一个简单的TCP测试工具从电脑上模拟发送端把整个链路打通再做真实对接。4.2 网关侧Python TCP服务端的拆包处理网关程序我用Python asyncio写的主要考虑到现场改逻辑方便不用重新编译。TCP服务端最大的技术难点不是收数据而是把连续的字节流按照帧边界切出来专业叫法叫“拆包”。TCP是流协议上层发的一帧数据可能分几次到达也可能几帧数据黏在一起到达。这里我贴一下实际使用的拆包逻辑import asyncio FRAME_HEAD bytes([0xAA, 0x55]) BUFFER_LIMIT 4096 class FrameParser: def __init__(self): self.buf bytearray() def feed(self, data: bytes): self.buf.extend(data) frames [] while True: idx self.buf.find(FRAME_HEAD) if idx -1: self.buf.clear() break if idx 0: # 帧头前出现脏数据直接丢掉 del self.buf[:idx] if len(self.buf) 3: break # 长度字节还没到齐 total 2 1 self.buf[2] if len(self.buf) total: if len(self.buf) BUFFER_LIMIT: # 超过缓冲区上限仍未凑齐帧做保护性清空 self.buf.clear() break frame bytes(self.buf[:total]) del self.buf[:total] if self.check(frame): frames.append(frame) else: # 校验失败丢一个字节继续找帧头 self.buf.pop(0) return frames staticmethod def check(frame: bytes) - bool: if len(frame) 5: return False length frame[2] if len(frame) ! 3 length: return False crc 0 for b in frame[3:-1]: crc ^ b return crc frame[-1]这个解析器有一个细节我特别说明一下校验失败时只弹出一个字节重新找帧头而不是直接把缓冲清空。因为实际网络环境里偶尔会混入一两个脏字节丢掉脏字节后后续帧还是完整的没必要一棍子打死。服务端的接收循环也不复杂class TcpGatewayServer: def __init__(self, host0.0.0.0, port9000): self.host host self.port port self.parser FrameParser() async def handle_client(self, reader, writer): peer writer.get_extra_info(peername) print(f[连接] {peer}) try: while True: data await reader.read(1024) if not data: break frames self.parser.feed(data) for frame in frames: await self.dispatch_frame(frame, writer) except (ConnectionResetError, asyncio.IncompleteReadError): pass finally: print(f[断开] {peer}) writer.close() await writer.wait_closed()收到完整帧以后按照命令字做分发。0x01就更新当前工位的声光状态0x02就复位0x03就回应一个0x04状态帧0x04在测试阶段用来验证终端回读。4.3 心跳与状态管理让链路“看得见”现场最怕的就是“上位机以为连上了网关早就断了”。我在上位机的透明发送配置里把心跳帧一并打开周期性发0x03。网关上维护一个字典记录每个客户端最近一次心跳时间超过3秒没收到就标记为离线。另外我还加了一个小功能网关上跑了一个数据记录列表把最近1000条解析出来的有效帧放到内存里通过一个简单的HTTP接口可以拉取。现场调试时不用每次都用抓包工具直接浏览器看最近数据就行。这个设计后面救了大命第三次翻车就靠它做的回溯。网关进程本身用systemd守护加了开机自启和崩溃重启。工控现场不需要花哨的集群和容器稳定重启、日志可查才是第一位的。5. 声光语音终端协议适配把字节帧翻译成能响能闪的指令5.1 常见声光语音终端的接口形态声光语音终端这块市面上产品接口大致分三类选型不同对接难度天差地别。接口形态优点缺点适用场景RS485 ASCII指令指令可读、容易调试需要自己管理从机地址多台终端、逻辑灵活Modbus RTU/TCP标准、PLC生态成熟寄存器映射依赖厂商文档走PLC现有总线干接点/IO触发不需要协议信息量少、无法回读最朴素的报警这次甲方采购的是RS485 ASCII指令型每条指令都是一行可读字符串比如ATPLAY08\r\n表示播报第08条语音ATLIGHTREDFLASH\r\n表示红灯闪烁ATBEEP3\r\n表示蜂鸣模式3。这种接口对协议转换最友好因为中间层不需要做二进制拼装直接字符串拼好发串口就行。5.2 映射表设计生产事件 → 播报内容、亮灯颜色、蜂鸣模式上位机通过字节帧只告诉我们“事件编号”真正决定声光终端怎么响的是网关里那张映射表。这张表我在项目文档里做成了表单格式事件编号现场含义触发条件语音索引灯色蜂鸣抑制规则0x011号工位完成节拍完成信号上升沿01绿色闪一下静音同事件2秒合并0x021号工位不良不良信号有效02红色常亮慢鸣直至人工复位0x032号工位完成节拍完成信号上升沿03绿色闪一下静音同事件2秒合并0x04前工序堆料堆料传感器有效04黄色快闪快鸣直至堵料消失0x05设备故障停机故障信号有效05红色快闪急促直至复位映射规则里有一个很容易忽略的点抑制。一条装配线一小时能完成几十个节拍如果每完成一拍就播一次“完成”车间里会吵到谁也听不清。所以我把“完成类”事件做成2秒内合并同一工位只播一次“故障类”事件则一直保持到人工复位因为它意味着真正需要人过去处理。网关里的实现其实就是一个事件分发函数根据frame里的命令字和数据区去查这张表拿到结果再拼字符串下发到串口。5.3 回读与自检终端状态怎么反馈给上位机RS485串口属于半双工网关下发指令以后要等一下终端的应答。大多数终端的应答很短比如OK\r\n或者ERR\r\n。我在网关里加了应答超时判断500ms内没收到应答就重发一次连续三次失败就判定终端离线同时在网关上把状态位置为异常再通过0x04状态帧上报给上位机。这一步下来整条链路的可观测性就比较完整了上位机能知道网关在线网关能知道终端在线终端能知道自己在播报什么。任何一个环节断了都能在五分钟内定位出来而不是到现场靠猜。6. 现场调试踩坑实录从连不上到误报的三次翻车6.1 第一次翻车防火墙拦掉了入站连接上位机透明发送配置好以后它的界面上显示“TCP连接失败”网关这边也一直没收到任何连接请求。第一反应是排查网络。顺着链路逐步定位我先在上位机上用ping测网关IP通了再用telnet 网关IP 9000测端口结果连接被拒绝。到这里基本可以断定是网关侧端口问题。查了一下这台ARM小主机的防火墙状态果然系统自带的防火墙默认拦了所有入站端口。解决办法很简单把9000端口放行顺手把SSH端口也一起放行了否则后面调试又得卡脖子firewall-cmd --zonepublic --add-port9000/tcp --permanent firewall-cmd --reload这个坑回头看实在不算高级但值得单独拿出来说现场很多看不见摸不着的“连不上”不要一上来就怀疑协议写错了先拿telnet把“通不通”和“对不对”这两件事分开验证。链路不通后面解析逻辑再完美也白搭。6.2 第二次翻车数据区错位语音播了对面工位的内容链路通以后我把上位机透明发送的真实数据抓回来对照协议分析发现一个诡异现象灯色是对的但语音内容张冠李戴1号工位的完成事件播出来的却是2号工位的语音。第一次遇到这现象我以为是网关映射表配错了。检查了一遍映射表没问题。接着怀疑终端语音索引烧录错了但也对不上。最后没办法用抓包工具把网关收到的原始字节流完整导出来一字节一字节对照发现上位机发来的帧里数据区整体偏移了一个字节。原因是老上位机软件的透明发送配置里变量映射起始偏移默认从1开始而我协议里约定的是从0开始。这个坑非常隐蔽因为灯色码是0x01和0x02这类小数字错位以后看起来依然是合法值直到语音索引特别接近才暴露出数据错位。后面我把所有字段的偏移核对表打了一份贴在工控机旁边倒逼配置页和协议文档严格一致。6.3 第三次翻车连续快报把终端“说”哑了排查链路最长上线后第三天车间反馈连续出现不良品的时候声光终端播报第一条后就卡住之后几条全部丢失网关侧显示“串口发送应答超时”上位机里看网络连接却一切正常。这个问题的排查链路最长也最值得记录。我先在网关上查了最近1000条帧回放确认上位机发到网关的帧一条不缺说明TCP链路和拆包逻辑没有问题。问题出在网关往终端下发的那一段。用串口监控工具挂在RS485总线上抓包发现终端在播报过程中收到了第二条播报指令它的串口应答正常返回OK但语音模块实际处于“忙”状态根本播不出来。翻终端厂商手册才明白这个型号的语音模块不支持打断式播报一条语音没播完新的播报指令即使返回OK也会被内部队列忽略甚至导致后续指令一直排队排到超时。解决思路分两层。第一层是我在网关的事件分发加了个“忙碌锁”同一台终端播报期间新的非紧急事件直接丢弃紧急故障事件可以抢占但要在抢占前先给终端发ATSTOP停止当前播报。第二层是加去抖逻辑2秒内同一事件只播一次。这个方案再上线以后连续不良、快速换线、短时间多事件叠加的情况都稳住了。说实话声光终端这类设备“能用”和“好用”的差距基本都体现在这种细碎的状态管理上。7. 这次改造教会我的事关于“以最小侵入解决现场问题”7.1 这套方案的适用边界做完这个项目很多人问我这套路子能不能复制。我的回答是要看条件。直接套用的前提有这么几个旧上位机软件有某种形式的TCP数据输出能力哪怕是远程维护用的透明通道也行产线网络相对干净不涉及跨大网段跨路由的复杂链路事件的数据量不大100ms一个周期每帧几字节到几十字节这种量级对任何网关都没压力甲方不排斥在中间加一个小设备也接受用开源脚本做协议解析。反过来如果上位机彻底没有任何网络出口或者现场是硬实时的运动控制事件要求毫秒级触发那这个方案就不合适趁早考虑从PLC侧直接取数或者换上位机。7.2 上线的回退保护与后续扩展这套方案在工程上的最大底气是“可回退”。网关是独立设备拔掉网线或者断电上位机的透明发送配置改回关闭系统就完全恢复原样。我在上线第一天特意做了断电演练网关断电5分钟产线照常生产只是声光终端不响而已不影响任何原有功能。扩展方面我在网关上还留了一个预留接口后续如果要接MES或者SCADA同一个网关可以再开一个JSON/HTTP接口把状态数据送出去帧协议不动只是把解析结果多一条分发路径。这种“一次改造后续不重写”的余地反而是甲方最看重的地方。最后分享一个实用小技巧是我在多次现场调试里养成的习惯网关里始终保留最近一段时间原始帧的循环记录不光是解析成功的数据连校验失败的脏帧也要留。很多网络层面的偶发问题只看解析结果根本看不出来把原始字节流回放一遍往往一眼就能定位。这次改造里的三次翻车两次都是靠原始帧回放才找到根因。做工业通讯改造手里一定要有能随时抓包、随时回放的底牌这比多写几行代码管用得多。
返回列表