ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#上位机与MCGS触摸屏TCP通信全指南:模式选型、组帧解析与排障

C#上位机与MCGS触摸屏TCP通信全指南:模式选型、组帧解析与排障 简介一套基于C#与MCGS昆仑通态的TCP通信范例代码面向工控上位机开发与组态软件联调场景解决C#程序如何通过网络读取MCGS内部数据的问题。示例基于VS2013编写演示了通信建立、数据请求与解析的完整流程适合具备一定C#基础、想熟悉MCGS TCP接口的工程师参考。整个压缩包共36个文件以9个C#源码文件为核心同时包含工程解决方案、配置文件、界面资源、可执行程序等辅助内容整体大小仅92KB结构精简便于直接打开工程查看调用关系和调试运行。目前该范例已有2504人学习下载代码覆盖通信关键步骤并提供可运行的示例工程和看板人机界面可帮助读者快速理解双方通信参数与代码实现的对应关系也能作为后续功能扩展或项目复用的基础模板。1. C#与MCGS用TCP通信先把“谁来连谁、谁问谁答”定下来C#上位机要和MCGS昆仑通态触摸屏走TCP通信第一步不是写代码而是先想清楚通信模式。触摸屏在系统里往往不只是显示界面它肚里装着PLC实时变量上位机盯上的是这些变量。而MCGS的TCP驱动通常给两条路要么C#当客户端按触摸屏的报文格式问一句答一句要么让触摸屏主动往C#监听端口里推数据。选错了后面代码全推倒重来。更麻烦的是MCGS不同产品线的组态环境不一样老TPC用McgsTpc新TPC用McgsPro同一个关键词在不同版本里菜单和驱动能力都不同。这一篇就把模式选型、C#侧代码骨架和真正耗时间的坑一次讲完。2. MCGS侧先打好地基TCP驱动模式、变量规划与跨网段设置动手写C#之前先把MCGS工程里的网络驱动配置好。常见做法是在设备窗口挂一个“通用TCP/IP父设备”下面再挂具体协议子设备。父设备负责决定连谁、谁监听、走哪个tcp端口号子设备决定报文怎么组织。父设备模式选错上位机写得再漂亮也白搭。2.1 先确认产品线和组态环境TPC老型号与McgsPro的差异昆仑通态触摸屏按产品线分老一批TPC型号通常在McgsTpc嵌入版组态软件里做工程新TPC、TPC-Com以及多数带“Pro”后缀的型号用McgsPro或新版McgsTpc。同一个“TCP/IP父设备”在不同环境里长得不一样McgsTpc里是“设备窗口 → 添加设备 → 网络设备 → 通用TCP/IP父设备”McgsPro里可能叫“TCP/IP通讯父设备”或在“采集设备”分类下。你拿到一台屏第一件事不是翻代码是把组态软件版本和触摸屏型号对上。父设备里需要确认三个东西本机IP或远端IP、端口号、工作模式。工作模式一般有“客户端”和“服务器服务端”两个选项。选“服务器”时触摸屏在本机监听一个端口C#上位机作为tcp连接主动找上门这是最常见的“上位机主连”场景选“客户端”时触摸屏会主动去连上位机开的监听端口程序适合触摸屏数量多、上位机需要统一接收的场景。端口号上不少MCGS TCP驱动模板默认填8888Modbus TCP则常是502具体以你工程里驱动实际显示为准别凭记忆硬填。2.2 变量规划把PLC变量映射成便于TCP读写的量MCGS里所有变量都放在实时数据库里C#通过TCP读写它们的本质是向MCGS驱动发请求由驱动去映射对应的PLC地址。变量规划直接影响后续代码复杂度。建议单独开一块区域用统一命名前缀比如SCADA_Level、SCADA_Flow、SCADA_Temp。这样做有实际好处C#端收到报文后可以直接按变量名后缀做字典映射不用频繁改解析代码MCGS侧做变量导出时也省事。同时要注意“原始值”和“工程值”的差别。触摸屏组态里一个液位变量可能来自PLC的4字节浮点也可能来自整数的量程转换。C#读到的数据到底是原始值还是工程值取决于MCGS侧变量设置和驱动协议。最容易踩的是浮点字节序MCGS不同版本对float的排列可能不同有的低字节在前有的高字节在前。建议在触摸屏组态里建立一个只读的“测试区”放几个已知数值的变量专门给上位机联调用。跨网段通讯也是现场常事。上位机在192.168.1.x触摸屏在192.168.2.x不少工程师先改IP发现还是不通就开始怀疑MCGS驱动。其实TCP/IP协议栈不负责跨网段路由触摸屏的“本机设置”里要正确填写网关地址三层交换机或路由器要有一条通往对方网段的静态路由。这个排障顺序很重要先ping通再谈tcp连接最后才谈报文解析。3. C#做TCP客户端读MCGS连接、组帧、解析与断线重连的实现模式定好了这一章给出一套可直接抄走的C# Socket客户端骨架。用的是System.Net.Sockets下的TcpClient不引第三方库。这套代码在.NET Framework 4.7.2和.NET 6/8下都能编译适合老工控机也适合新系统。3.1 TcpClient连接与超时参数设置工控现场最怕的不是连不上而是连不上时卡死。默认TcpClient.Connect在网络上没回应时可能阻塞很久必须自己做超时控制。常见做法是BeginConnect配合WaitOne短超时快速失败再交给上层重连逻辑。这里有个细节Connect成功不等于通信正常所以收发超时也要单独设置。using System.Net.Sockets; /// 建立TCP连接失败快速抛出不做重试 public TcpClient ConnectWithTimeout(string ip, int port, int timeoutMs 3000) { TcpClient client new TcpClient(); try { // BeginConnect WaitOne 实现可控制的连接超时 IAsyncResult ar client.BeginConnect(ip, port, null, null); if (!ar.AsyncWaitHandle.WaitOne(timeoutMs)) { client.Close(); throw new TimeoutException($连接 {ip}:{port} 超时请检查MCGS服务端是否监听); } client.EndConnect(ar); // 收发超时设为2000毫秒避免读半包后永久阻塞 client.SendTimeout 2000; client.ReceiveTimeout 2000; return client; } catch { client?.Close(); throw; } }这段代码解决了两个常见问题一是连接超时可控二是收发有上限。SendTimeout和ReceiveTimeout的取值要根据现场调PLC扫描周期快、并发量大的现场建议1500~2000毫秒经过多级交换机或无线桥接时网络延迟本身高超时放宽到3000~5000毫秒更合理否则会出现“明明通着却超时误报”的假故障。3.2 按报文帧格式组帧头部、长度、功能码、数据、校验MCGS TCP驱动的私有协议在不同组态版本里字节布局不完全一致。这里给一个通用组帧骨架用MemoryStream按“帧头 长度段 载荷 校验”拼装你只需要拿抓包结果替换头部的固定字节和校验算法。using System.IO; /// 按MCGS报文模板组帧 /// 注意不同MCGS驱动版本帧结构不同头部、长度段字节序和校验算法以实际抓包为准 public static byte[] BuildFrame(byte[] payload) { using MemoryStream ms new MemoryStream(); // 帧头常见为固定引导字节示例用 AA 55实际按驱动手册替换 ms.WriteByte(0xAA); ms.WriteByte(0x55); // 长度段2字节示例为低字节在前高字节在后 int len payload.Length; ms.WriteByte((byte)(len 0xFF)); ms.WriteByte((byte)((len 8) 0xFF)); // 载荷功能码 变量名 数量 等由具体协议决定 ms.Write(payload, 0, payload.Length); // 校验示例用异或累加很多驱动用CRC16需要替换成匹配的算法 byte xor 0; foreach (byte b in payload) { xor ^ b; } ms.WriteByte(xor); return ms.ToArray(); }把这个方法理解成“造帧车间”你只需要确定MCGS侧的报文格式把写帧头、长度、校验这三处替换掉剩下的发送和解析逻辑可以复用。特别注意长度段是包含功能码还是只包含数据多算一字节或少算一字节对端会认为校验失败直接丢帧。建议在MCGS侧加一个调试按钮C#端每发一帧后把原始十六进制打印出来肉眼对齐一次心里就踏实。3.3 发送请求并等待应答一次请求对应一次响应TCP是流式协议不存在“一次发送就是一次接收”的保证所以读应答不能简单地Read一次就完事。需要一个循环先读2字节帧头再读长度段知道载荷长度后按需读取拼成一帧再返回。这样才能扛住粘包和半包。public static byte[] ReadFullFrame(NetworkStream stream) { using MemoryStream ms new MemoryStream(); int b; // 第一步找帧头 while (true) { b stream.ReadByte(); if (b -1) throw new IOException(连接已关闭); ms.WriteByte((byte)b); // 示例帧头是 AA 55读到第二个头字节就跳出 if (ms.Length 2 ms.GetBuffer()[0] 0xAA ms.GetBuffer()[1] 0x55) break; // 如果连续读取没匹配帧头清空重来 if (ms.Length 2) ms.SetLength(0); } // 第二步读长度段2字节 byte[] lenBytes new byte[2]; stream.ReadExactly(lenBytes, 0, 2); int payloadLen lenBytes[0] | (lenBytes[1] 8); // 低字节在前 byte[] payload new byte[payloadLen]; stream.ReadExactly(payload, 0, payloadLen); // 第三步读校验字节 int checksum stream.ReadByte(); // 这里按实际协议规则校验 payload 与 checksum不匹配要丢掉整帧 return payload; }这段代码里的ReadExactly在.NET Core里直接可用在.NET Framework里需要自己写循环补读核心思想是一次读不够就继续读直到凑够字节为止。做完这些主流程就是连接 → 组帧 → 发送 → ReadFullFrame → 解析。解析结果用Dictionarystring, double存起来交给界面或数据库去消费。4. 反向通信让MCGS当TCP服务端主动推送C#只做监听与拆包很多项目里触摸屏数量不止一台每台屏还要周期性上报数据。这时让C#挨个去问逻辑繁琐不说还要管N份连接状态。更省事的办法是让MCGS侧当TCP客户端把屏上的变量周期性地往C#开的监听端口程序里推。C#那边只需要一个监听端口程序等着数据自己送上门。4.1 TcpListener开监听异步接受多个触摸屏连接用TcpListener开启监听接受连接后每个客户端单独开一个Task处理。这里不需要多线程并发争用主线程只负责Accept每个连接的任务各自读自己的数据。using System.Net; using System.Net.Sockets; public async Task StartMcgsListenerAsync(int port, CancellationToken ct) { TcpListener listener new TcpListener(IPAddress.Any, port); listener.Start(); Console.WriteLine($监听端口 {port} 已启动); while (!ct.IsCancellationRequested) { TcpClient client await listener.AcceptTcpClientAsync(ct); // 每个触摸屏连接单独处理避免一台屏掉线拖垮整个接收流程 _ Task.Run(() HandleClientAsync(client, ct), ct); } }端口选择上建议避开常用端口和MCGS其他驱动默认端口选9000~10000区间的空闲端口。防火墙里要放行该端口否则触摸屏侧显示连接成功C#这边却一直看不到数据。监听端口程序的核心价值不是接收而是把每条连接状态、最近一帧时间记录下来方便现场排查掉线问题。4.2 处理TCP粘包与半包缓冲区拆帧触摸屏按周期推数据C#接收端遇到最常见的问题是粘包两个数据帧连在一起被一次读出来。如果直接把读到的字节当一帧解析后面全错位。解决办法是定义一个TryExtractFrame函数把收到的字节先放进MemoryStream缓冲能拆出完整帧就返回拆不出来就等下一批数据。private readonly MemoryStream _pending new MemoryStream(); // 从缓冲流中尝试拆出一帧拆不出返回false private bool TryExtractFrame(byte[] incoming, int length, out byte[]? frame) { _pending.Write(incoming, 0, length); // 检查缓冲里是否已有完整帧头 if (_pending.Length 2) { frame null; return false; } byte[] buffer _pending.GetBuffer(); int startIndex -1; for (int i 0; i _pending.Length - 1; i) { if (buffer[i] 0xAA buffer[i 1] 0x55) // 帧头示例 { startIndex i; break; } } if (startIndex 0) { _pending.SetLength(0); frame null; return false; } // 丢弃帧头之前的脏数据 if (startIndex 0) { byte[] remain _pending.ToArray().Skip(startIndex).ToArray(); _pending.SetLength(0); _pending.Write(remain, 0, remain.Length); } // 至少要有 帧头2 长度2 才能判断整帧长度 if (_pending.Length 4) { frame null; return false; } int lenLow buffer[2]; int lenHigh buffer[3]; int payloadLen lenLow | (lenHigh 8); int totalLen 2 2 payloadLen 1; // 帧头长度载荷校验 if (_pending.Length totalLen) { frame null; return false; } // 半包继续收 byte[] fullFrame new byte[totalLen]; _pending.Read(fullFrame, 0, totalLen); frame fullFrame; return true; }拆帧的关键在于“不够就等够了再切”。每收到一段数据调一次TryExtractFrame返回true就处理一帧继续循环直到返回false为止。这个写法比“判断数据尾字节”可靠得多因为你没法保证触摸屏一次只发一帧更没法保证网络延迟不会把一帧切成两半。处理完的帧交解析函数按变量编号和值更新内存字典。这套反向推送模式还有一个优势触摸屏侧发生变量变化时组态里可以配置条件触发上送报警类数据能做到准实时。C#侧代码完全不用关心到底谁变化了只做被动接收。前提是MCGS组态工程的TCP客户端配置里把目标IP和端口填对并且采集周期设置在一个合理范围内比如500毫秒到1秒一次别小到让触摸屏驱动忙不过来。5. 参数设置与常见问题排查连不上、卡死、数据错位的5个根因不管是C#主动连接还是监听推送最终都会在调试现场碰到几类典型故障。下面这5条是我反复见过的每一条都按“现象 → 原因 → 解决”拆开写。5.1 端口、心跳和超时先别急着改代码按这个顺序查现象C#客户端连接报“目标计算机积极拒绝”或者触摸屏推送端显示已连接但C#收不到数。原因多数是三个层面之一。第一MCGS侧服务端没有正常启动监听父设备没保存或组态没下载到触摸屏屏上跑的其实还是旧工程。第二防火墙拦了端口Windows默认会拦外部连接。第三C#端口号和MCGS驱动配置不一致比如屏上配的8888代码里写的502。解决先做排除法。在触摸屏上用自带调试窗口或在线模拟确认驱动运行状态在上位机上用Test-NetConnection ip -Port 端口命令探测tcp连接是否通最后再把报文内容打出来看。顺序不要反很多人一上来就怀疑代码结果查半天是屏上的工程没下载对。5.2 连接正常但读几个帧就卡死收发线程互相等待现象C#能连上触摸屏读第一帧正常第二帧直接卡住程序假死。原因最常见的是同步Socket在单线程里收发发送一个请求后接收端没有在预期时间内返回下一帧Receive阻塞住整个界面线程。另一个常见原因是半包上次读到的数据没拼完下一次读到的是后半段导致解析错位请求永远等不到正确应答。解决把收发挪到后台线程界面只订阅结果。另外所有Read操作都要走完整帧读取逻辑也就是前面3.3节的ReadFullFrame不要用“读一次就当一帧”的省事写法。再加上ReceiveTimeout兜底超时后扔出异常由上层做重连而不是卡死。5.3 读到的数值和触摸屏上显示的对不上现象C#读回来的整数和触摸屏界面显示的变量值完全对不上或者float读出来是天文数字。原因要么是字节序问题MCGS里float变量在TCP报文里是高字节在前C#的BitConverter默认是低字节在前要么是读写地址映射错位MCGS侧变量列表里的索引和C#请求里的变量序号错了一行。还有一个很隐蔽的情况读的是原始值触摸屏界面显示的是工程值中间差了一个量程换算系数。解决写一段一次性解析测试代码连接后连续读已知变量逐一对照。float翻转字节序后打印再看是否是工程值换算问题。项目初期多花半小时调字节序能省掉试机现场一晚上的折腾。5.4 触摸屏和上位机不在同一网段ping都不通现象IP改了网线插了但C#始终连不上触摸屏ping也不通。原因跨网段通讯需要网关参与可触摸屏的网关设置是空的或者上位机本身开着多个网卡让路由走了错误网关。MCGS触摸屏系统设置里有本机IP、子网掩码、网关三项少填一项都跨不了网段。解决先在上位机cmd里用route print看看默认路由指向哪个网卡再核对触摸屏的网关填的是不是和现场路由器管理地址同一段。如果中间隔着三层交换机交换机上要有去往对方网段的静态路由。这类问题跟C#代码无关但最容易让人在代码里白找半天。5.5 触摸屏断电重启后上位机连不回来了现象现场工人把触摸屏断电重启之后C#程序再也连不上直到手动重启上位机软件。原因TCP连接是两端状态一端断掉后另一端不会立刻感知。C#的TcpClient对象还在但底层连接已经死了。发送数据时可能触发异常但如果程序一直只是“等数据”就永远等不到。解决建立心跳机制。C#侧每2~3秒检测一次连接状态通过发送一帧空读命令或心跳帧来确认连接是否还活着一旦失败立即销毁旧连接按指数退避重连。重连时要注意触摸屏重启后可能没有立刻准备好监听前几次连接失败是正常的所以要重试不能第一次失败就放弃。6. 把通信代码封装成上位机的“元器件”抓包验证和日志先行到最后这一步我习惯把整套通信逻辑封装成一个内部状态机式的组件而不是散落在窗体事件里。组件对外暴露Connect、Disconnect、读取变量、变量更新事件对内管理着连接状态、重连退避、收帧缓冲区和最近帧时间。好处是上位机页面代码永远干干净净出问题只查这一个类。重连退避是个值得做的细节连续失败时不要每200毫秒疯狂重连那会把触摸屏驱动拖垮。常见做法是记录失败次数按5秒、10秒、20秒、最大30秒递增成功一次后重置。判断连接健康的依据不应该是TcpClient.Connected属性它只反映Socket状态不代表对端还活着要定义一个旧数据保护阈值如果超过3个心跳周期没有收到任何有效帧自动判定掉线并触发重连。还有一个习惯我吃了亏才养成先抓包再写解析。新到一个MCGS工程不管对方给的协议文档写得多清楚先用抓包工具把触摸屏发出的原始帧抓下来对着十六进制逐字节研究。很多项目的协议文档和实际固件版本差了一两个版本长度段算法和帧头都可能不一样。抓包后把每一帧的用途、字段偏移、字节序记到一个固定表格里代码照着这个表写。这个习惯后来救过我很多次尤其是老TPC设备升级固件后报文悄悄变化的情况。日志也一定要在通信层做记录每一次连接成功、连接失败、帧超时、重连动作、未知帧头。日志级别分开平时只记连接状态和异常调试时打开完整帧记录。宁可日志多写几个字段不要等屏幕数据突然停下来才想起没有任何日志可查。这套组件做完C#与MCGS昆仑通态触摸屏的TCP通信就有了一个稳定、可替换、可测的底座后面加协议、加屏、加数据点只是配置层面的活儿。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进