资讯动态

C#上位机与MES系统对接:PLC数据采集、HTTP上报与断线重传实战解析

发布时间:2026/10/16 9:42:08 来源:尧图企业网站定制
简介这是一份面向C#上位机开发与MES系统集成场景的实操资源以客户端和服务端两个工程演示设备数据采集与上传流程。工程基于OPC UA协议与PLC通信在客户端中实现HTTP请求发送在服务端使用Web服务接口暴露方法构成从设备取数到MES入库的完整链路同时包含窗体界面、配置文件、动态链接库与可执行程序便于直接运行或二次改造。压缩包共77个文件其中C#源文件19个以C#源码、Web服务文件、配置文件和依赖库为主整体仅121KB目录按客户端与Web服务端拆分适合需要快速上手MES接口对接、了解上位机数据通道的开发者。当前已有6127人学习下载。除完整示例外还涵盖数据格式转换、异步编程、异常处理与安全传输等知识点可直接作为从PLC采集到MES上报的一体化参考模板。1. 从车间一线的实际痛点说起做上位机开发的这些年我至少接手过三个和 MES 系统对接的项目说的直白一点MES 就是工厂的大脑上位机就是你工位上的那双眼睛和手。它既要随时盯着 PLC可编程逻辑控制器的实时状态又得把产量、设备状态、质量数据上报给 MES还得乖乖执行 MES 下发的生产工单。这个 c# 与 MES 对接传输数据的项目解决的就是车间里最常见的那个困惑怎么让 PLC 到上位机到 MES 这条链路稳定跑起来不丢数、不卡壳。很多刚接触工控的 C# 开发者容易把这事想成单纯调接口真正落地时你会发现串口读写、网络通讯、数据库中间表、HTTP 接口、断线重传这些全都混杂在一起。这篇笔记不是科普 C# 有多强而是把一套可以直接抄的上位机与 MES 对接方案从架构设计到核心代码逐层拆开。这套方案适合谁你已经能在 Visual Studio 里写 WinForms 或 WPF 应用了现在要把设备数据送上 MES或者被 MES 反控设备这套代码里的通讯管理、指令轮询、断线补偿思路可以直接复刻到你的项目里。2. 整体架构与选型不是只有一种 MES 对接方式2.1 数据链路的四层结构整个对接链条里数据从车间最底层的传感器到管理层的 MES 数据库中间至少要穿过四层。最下面一层是设备层包括 PLC、扫码枪、工业传感器它们通过串口、以太网或者现场总线对外暴露数据。再往上是采集层也就是你写的上位机它把 PLC 的数据读上来做解析、缓存、必要的计算之后格式化上报。第三层是传输层体现为 MES 对外提供的接口服务可能是 HTTP API、Web Service网络服务也可能是直接共享数据库的中间表。到了最上面一层就是 MES 的核心业务系统它对数据进行业务处理维护生产工单、质量追溯和物料批次台账。在真实的车间环境里PLC 和上位机的通讯方式服从于设备硬件最常见的是两种Modbus TCP 和 S7 协议。前者通用性强几乎各种品牌 PLC 都支持 Modbus 寄存器读写西门子 Smart 系列用的很多后者主要针对西门子 S7-1200/1500 系列协议内部经过封装用 Sharp7 这类库可以直接读写 DB 块、M 区这些地址。而 C# 上位机对 MES 的通讯就更开放了可以做 RESTful API 上报也可以轮询中间表实际选型取决于你现场那套 MES 是采购的还是自研的以及它的开放度。2.2 选择接口方式的判断标准我快速说说我对三种主流对接方式的判断。第一种是 MES 提供 REST API这是最干净的方式C# 端用 HttpClientHTTP 客户端类库发 JSON 数据包即可MES 返回成功失败结果问题是 MES 厂商的接口文档有时含糊参数名给你个workOrderCode半中半英字段对应关系要反复确认。第二种是中间数据库表方案MES 开放一个 SQL Server 实例给你几张约定好的表你的上位机按特定格式往里插记录MES 后台服务自行读取这种方式调试最直观SQL 查询一看就知道数据到没到但最怕数据库连接串有防火墙拦截或账号权限不到位。第三种是 Web Service 的 SOAP 接口老项目偏多C# 用 WCF 或直接添加服务引用就能调维护成本稍高。这个项目我最终的落地方案是双通道并联实时状态下采用 PLC 数据实时采集加 MES 主动上报的模式同时也保留中间表作为数据备份通道这样即便 MES 接口临时宕机数据也不会堵在生产现场。实际反控指令例如工序跳步、暂停放行我采用了 MES 下发指令到中间表、上位机轮询执行的方案因为指令类数据频率低但可靠性要求极高中间表方式可以留痕、可追溯车间工艺人员也容易排查问题。2.3 工程目录结构与分层思想项目不大但我仍然建议把解决方案按职责拆成四个工程而不是一股脑写在一个窗体里后面二开你就知道香了。呈现给操作员的界面层可以是一个 WinForms 工程承载工单信息展示、设备状态监控和手动操作按钮。核心逻辑层是一个类库解决 PLC 通讯管理和报文解析的问题。往上再挂一个业务服务层负责数据格式化、MES 接口调用的封装内部有断线重传和日志机制。单独拆分一个实体模型工程把 PLC 点位、上报报文、指令对象定义成强类型类省得在代码里到处传字典。按这个分层整理下来视觉界面和业务逻辑彻底解耦。后面更换 PLC 品牌时只需要换掉采集层的通讯库界面和上报服务完全不用动这正是工控项目后续维护最担心的事情的解决方案。3. 核心模块拆解从 PLC 采集到 MES 上报的完整链路3.1 上位机连接 PLC建立实时数据通道这个项目里我用的是开源库 HslCommunication 来做 PLC 通讯它对主流 PLC 的支持很全面西门子、三菱、欧姆龙都能通过统一的读写接口操作。使用前从 NuGet 安装包在代码里只需要传入 PLC 的 IP 和端口就能建立连接。下面是连接西门子 S7-1200 的初始化代码// 引用 HslCommunication 命名空间 using HslCommunication; using HslCommunication.Profinet.Siemens; // 创建 S7 协议通讯对象PLC 固定 IP 为 192.168.1.10 SiemensS7Net plc new SiemensS7Net(SiemensPLCS.S1200, 192.168.1.10); plc.ConnectTimeOut 2000; // 连接超时 2 秒 plc.ReadWriteTimeOut 3000; // 读写超时 3 秒 // 建立连接S7 协议一般在首次读写时自动握手此处可以省略 OperateResult connectResult plc.ConnectServer(); if (!connectResult.IsSuccess) { // 连接失败记录日志并触发界面告警 Logger.Error(PLC 连接失败 connectResult.Message); }这段代码的逻辑并不复杂实例化一个西门子 S7 客户端指定 PLC 型号和地址然后调用连接方法。关键的参数是超时时间的设定在车间现场网络状况不比办公网偶尔会因交换机端口拥塞导致连接假死设置连接超时和读写超时以免界面线程卡死。你还需要注意一个细节S7 通讯建立的机制是西门子私有的 TPKT 协议封装握手动作在首次读写时底层会自动完成所以这里调用的 ConnectServer 方法更多是验证网络通断和 PLC 是否空闲。PLC 连接成功以后接下来就要设计点位表了。点位表是现场工程最容易翻车的地方同一台设备的转速可能既有 4-20 毫安模拟量信号又有变频器反馈的 Modbus 寄存器值稍不留神地址就重叠了。我的做法是先把点位在项目里定义成枚举或者常量表然后再写采集逻辑这样后续维护点位时只需要改一处。下面是一个点位定义和读取的示例// 点位常量定义地址按 PLC 程序中的 DB 块来 public static class PlcAddresses { // 设备状态字1 运行2 待机3 故障4 离线 public const string DevStatus DB100.DBD0; // 当前主轴转速实数4 字节 public const string SpindleSpeed DB100.DBD4; // 当前产品计数整数4 字节 public const string TotalCount DB100.DBD8; // 设备急停信号位 public const string EStopSignal DB100.DBX12.0; } // 读取 PLC 数据的核心方法 public async TaskDeviceData ReadPlcDataAsync() { DeviceData data new DeviceData(); // 异步读取多个地址避免阻塞 UI 线程 OperateResultfloat speedResult await plc.ReadFloatAsync(PlcAddresses.SpindleSpeed); if (speedResult.IsSuccess) { data.SpindleSpeed speedResult.Content; } else { Logger.Warn(主轴转速读取失败 speedResult.Message); } // 读取设备状态字按约定解析为枚举 OperateResultint statusResult await plc.ReadInt32Async(PlcAddresses.DevStatus); if (statusResult.IsSuccess) { data.DevStatus (DeviceStatus)statusResult.Content; } // 急停信号读到且为 True 时立即触发告警 OperateResultbool eStopResult await plc.ReadBoolAsync(PlcAddresses.EStopSignal); if (eStopResult.IsSuccess eStopResult.Content) { data.EStop true; AlarmManager.Trigger(急停被触发请立即检查设备); } return data; }每台设备的 PLC 程序各有各的编排逻辑点位地址不能照搬你要按照现场 PLC 工程师提供的变量表来配。读取失败时不要一再重试先记录日志并标记该点位异常在下一次采集周期中再进行恢复检查。我一般会在界面上画一个通讯状态指示灯PLC 通讯异常超过三次就变红提醒操作员检查网线和 PLC 运行开关状态。3.2 数据上行HTTP 上报 MES 系统的实现上位机采集到数据之后下一步就是把数据提交给 MES。项目中使用 HttpClient 调用 MES 的 REST API 来做数据上行这里要设计好数据格式和鉴权逻辑。我自定义了一个统一的报文实体类用一个生产数据上报方法来完成序列化、发送、响应解析的完整流程。看代码// 上报数据的实体对象 public class MesReportData { public string EquipmentCode { get; set; } // 设备编号 public string WorkOrderNo { get; set; } // 当前工单号 public int Status { get; set; } // 设备状态码 public float Speed { get; set; } // 当前转速值 public int Count { get; set; } // 已完成产量 public DateTime CollectTime { get; set; } // 采集时间戳 } // 上报服务类负责向 MES 提交数据 public class MesReportService { private readonly HttpClient _httpClient; public MesReportService(string baseUrl, string token) { _httpClient new HttpClient(); _httpClient.BaseAddress new Uri(baseUrl); // 在请求头中注入鉴权 token _httpClient.DefaultRequestHeaders.Authorization new System.Net.Http.Headers.AuthenticationHeaderValue(Bearer, token); _httpClient.Timeout TimeSpan.FromSeconds(5); } // 异步上报单条生产数据 public async Taskbool ReportProductionAsync(MesReportData report) { // 序列化为 JSON 字符串 string json JsonSerializer.Serialize(report); HttpContent content new StringContent(json, Encoding.UTF8, application/json); try { HttpResponseMessage resp await _httpClient.PostAsync(/api/mes/production/report, content); if (resp.IsSuccessStatusCode) { string respBody await resp.Content.ReadAsStringAsync(); // 解析响应通常 MES 会返回 code0 表示成功 MesResponse result JsonSerializer.DeserializeMesResponse(respBody); return result.Code 0; } else { Logger.Error($MES 上报失败HTTP 状态{(int)resp.StatusCode}); return false; } } catch (HttpRequestException ex) { Logger.Error(MES 接口网络异常 ex.Message); return false; } catch (TaskCanceledException ex) { Logger.Warn(MES 接口超时 ex.Message); return false; } } }这套上报服务里最容易忽略的有两个点其一是时间戳车间现场的工控机时钟未必和 MES 服务器时钟一致最好在设备启动时做一次校时并且上报时间统一用 DateTime.UtcNow 或者和 MES 约定的时区格式少踩产量算到了别的时间段这种莫名其妙的坑。其二是上报结果的失败处理网络偶发抖动超时在所难免我在这套实现里采用的是订阅模式上报失败后数据会先入队再由一个后台定时任务按指数退避算法重试发送。3.3 指令下行上位机如何接收 MES 的工单与放行指令MES 不仅接收数据还要向下发号施令。实际车间里最常见的 MES 反控需求有两个把某工单排产到某台设备上以及让工位放行 / 暂停当前批次。本项目用轮询中间表的方式来接收指令。这种设计的好处是即使 MES 服务本身不稳定只要数据库还开着指令就能留下来上位机不会因为错过一条瞬时推送而漏执行。下面是轮询指令表的核心逻辑// 从 SQL Server 中间表读取待执行指令 public async TaskListMesCommand FetchPendingCommandsAsync() { ListMesCommand commands new ListMesCommand(); string sql SELECT Id, CommandType, CommandContent, CreateTime FROM dbo.Mes_CommandQueue WHERE IsProcessed 0 AND ExpireTime GETDATE() ORDER BY Id ASC; // 先进先出按时间顺序处理 using (SqlConnection conn new SqlConnection(_connString)) { await conn.OpenAsync(); using (SqlCommand cmd new SqlCommand(sql, conn)) using (SqlDataReader reader await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { commands.Add(new MesCommand { Id reader.GetInt32(0), CommandType reader.GetString(1), CommandContent reader.GetString(2), CreateTime reader.GetDateTime(3) }); } } } return commands; } // 执行指令并反馈执行结果 public async Task ProcessCommandAsync(MesCommand cmd) { bool execSuccess false; string resultMsg string.Empty; switch (cmd.CommandType) { case START_WORK_ORDER: // 工单号解析后写入本地变量界面提示操作员确认 execSuccess await _deviceAdapter.StartWorkOrderAsync(cmd.CommandContent); resultMsg execSuccess ? 工单已启动 : 启动失败; break; case RELEASE_PRODUCT: // 放行指令通知 PLC 放行当前产品 execSuccess await _deviceAdapter.ReleaseProductAsync(); resultMsg execSuccess ? 产品已放行 : 放行失败; break; default: resultMsg 未知指令类型 cmd.CommandType; break; } // 回写执行结果到指令中间表让 MES 知道指令已被处理 string updateSql UPDATE dbo.Mes_CommandQueue SET IsProcessed 1, ProcessResult result, ProcessTime GETDATE() WHERE Id id; using (SqlConnection conn new SqlConnection(_connString)) { await conn.OpenAsync(); using (SqlCommand cmd new SqlCommand(updateSql, conn)) { cmd.Parameters.AddWithValue(result, resultMsg); cmd.Parameters.AddWithValue(id, cmd.Id); await cmd.ExecuteNonQueryAsync(); } } }轮询频率我一般设置在 3 到 5 秒一次太频繁会增加数据库压力太慢操作员会明显感觉到指令延迟。执行指令时用 switch 分支处理不同类型的命令在扩展新指令类型时只需要在 switch 里加一个 case不用动整体结构。仍有改进空间你可以在执行指令前加一个事务锁避免多线程同时拉取同一条指令导致重复执行经典的防重复处理方案就是使用 WITH (UPDLOCK, READPAST) 表提示。3.4 缓存与断线补偿保证数据不丢的核心细节MES 接口偶尔宕一下这事儿太常见了数据库重启、应用池回收、网络交换机半夜被误拔任何一种故障都可能让你正在上报的数据丢失。老设备数据丢了可以人工补录几块钱的损失不致命但关键工艺参数和首件检验数据丢了整个批次的质量追溯链就断了。我的处理方案是落盘缓存。所有上报的数据先写本地 SQLite 或者轻量级日志文件然后由后台线程按顺序向上发送只有收到 MES 成功响应才能标记为已发送。下面是失败重传的核心逻辑思路// 本地缓存待发送数据队列 public class ResendBufferService { private ConcurrentQueueMesReportData _bufferQueue new ConcurrentQueueMesReportData(); // 上报失败时入队 public void Enqueue(MesReportData data) { // 超过队列长度时丢弃最旧的数据并在界面提示 if (_bufferQueue.Count 2000) { Logger.Error(重传队列溢出数据被丢弃); return; } _bufferQueue.Enqueue(data); } // 后台任务定时重发 public async Task ResendLoopAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { if (_bufferQueue.TryDequeue(out MesReportData report)) { bool success await _reportService.ReportProductionAsync(report); if (!success) { // 重发失败再塞回队尾并且退避时间拉长 _bufferQueue.Enqueue(report); await Task.Delay(TimeSpan.FromSeconds(10), ct); continue; } } // 正常空转等待 await Task.Delay(TimeSpan.FromSeconds(2), ct); } } }缓存重传这套机制体感上你好像只是在处理失败但实际上它对 MES 侧的影响还在于顺序性。在质量追溯场景下MES 要求数据的时序必须精确不同批次的产品不能混着记录。所以缓存队列必须严格先进先出重传时也不能打乱顺序。4. 运行部署与常见问题排查4.1 环境配置与启动流程跑这套项目的硬件要求不高一台研华工控机就足够了配置 4GB 内存加一块 128G 固态硬盘系统装 Windows 10 IoT 版本或者 Windows Server 环境都行。工控机上需要安装 .NET 6/8 运行环境数据库可视化管理工具可有可无但建议装一个以便快速察看待发送指令表和上报日志表。部署时按三步走第一步调整防火墙放行规则PLC 通讯一般涉及 TCP 102 端口上位机连接 SQL Server 需要 1433 端口调 HTTP 接口则要确认目标端口的白发白名单第二步修改配置文件 appsettings.json把 PLC 的 IP、端口、点位表、MES 的接口地址、Token、SQL 连接串全部从代码中剥离到配置文件里方便现场实施人员维护第三步验证串口或网线物理连接使用 ping 命令测试上位机到 PLC 和 MES 服务器的网络连通性。4.2 避坑指南现场最容易翻车的五个问题现象一PLC 网线没问题但读取始终超时超时的原因是多方面的有时候 PLC 上有两个网口一个走调试口一个走设备联网口你连的是调试口那网络可能被博途软件占用着或者是 PLC 侧的连接机制设置了最大连接数有人在用软件在线监视占住了连接。解决的方法是关掉博途在线监视通道或者把上位机网线接到支持多连接的那个网口再不行就在 PLC 侧增加允许的连接资源数。真实现场很多所谓的通讯不稳定都是这种连接数被占用的冷知识。现象二MES 接口文档字段名写的是 orderId实际传输的却是 workOrderCode两者概念不一致字段对不上就会出现接收方一直报参数为空、无法插入业务表的问题。解决的方法是在对接阶段的第一天梳理出一份详细的字段映射表标明中文字段名、MES 侧英文名、上位机实体名、类型和长度双方开发各持一份任何字段变更先更新映射表避免后期互相扯皮。这件事我非常推荐在需求评审阶段就定下来黑纸白字省掉十次联调。现象三同一台设备的数据两条同时被上报到了 MES多了一条重复记录发生这种情况通常是因为你后台的重传机制和主线程上报任务在并发跑PLC 采集触发一次上报同时缓存重传队列里还躺着上一次失败的数据两边一交叠就重了。解决的方法是上报缓存的重传必须独占一个锁或者从设计上改为只有采集模块单线程负责上报后台任务只做补偿不入主流程。现象四PLC 的断电寄存器掉电保持区没有规划好断电后产量清零MES 累计数和现场对数对不上不同品牌 PLC 的 DB 块分为掉电保持和非保持两种属性上位机在定义采集地址之前务必找 PLC 程序工程师确认哪些 DB 块配置了掉电保持。产量这种关键累计值要放在掉电保持区否则一把停电让你一个班的产量直接回到解放前。如果 PLC 端不好改程序上位机可以每小时把累计数快照存到本地文件或数据库。现象五轮询指令中间表时出现锁等待或者死锁多人同时更新同一条指令表容易触发 SQL 阻塞。解决的方法是查询待执行指令时使用 UPDLOCK 提示并开启事务执行完成后立刻提交事务事务内部不要做耗时长的设备通讯操作先把指令状态改成执行中并记录时间戳再去做真实的设备动作。4.3 日志体系发现问题全靠它工控系统在车间 7 乘 24 小时地运行出了故障你人不在现场没有日志你真的不知道从哪里查起。项目里我引入了一个独立的日志模块按天滚动文件日志级别区分 INFO、WARN、ERROR 三类。向上记录的要点是PLC 连接断开、重连成功、数据读取成功失败、上报成功失败、指令入队出队、指令执行结果等。关键点在于日志里必须包含上下文信息比如某条上报日志要带工单号、设备号、报文原文否则后排查时你会发现只有报错信息根本不知道是哪一个工单的问题。另外我习惯把心跳日志单独出个开关默认关闭需要排查 MES 连接时打开可以每隔 1 分钟输出一条存活日志。这种日志配合保留最近 30 天的存储策略基本能应对车间现场 99% 的故障排查需求。5. 二次开发的改造方向与实战技巧5.1 新增一种 PLC 品牌怎么扩展这个项目的 PLC 通讯层我定义了一个抽象的 IDeviceAdapter 接口任何设备接入都实现这个接口的读写方法。你要接入新的 PLC 品牌时只需要新建一个类继承接口然后在工厂方法里根据配置文件返回对应实例。接口的定义大致是这样的public interface IDeviceAdapter : IDisposable { // 连接设备 Taskbool ConnectAsync(); // 断开连接 Task DisconnectAsync(); // 读取设备数据集合 TaskDeviceData ReadDataAsync(); // 下发控制指令 Taskbool WriteCommandAsync(DeviceCommand command); // 设备通讯健康检查 Taskbool HealthCheckAsync(); }新增品牌的时候不要改界面层和上报层界面只管绑定 DeviceData 类上报层只管接收 DeviceData 类。这样整个系统可以保证一个设备一套适配器的扩展模型。实际现场还经常出现一台工控机同时管两种设备品牌的情况此时只需要创建两个适配器实例分别放置在不同的后台任务线程中上报通道共用一套。5.2 上报频率和 MES 接口压力的平衡我刚做第一个项目时天真地设计了 2 秒上报一次的策略结果 MES 服务器直接在业务高峰期被打满。后来总结出一套经验法则离散制造设备比如机床、注塑机10 到 15 秒上报一次基础数据就足够了MES 关心的是生产进度和设备状态的宏观变化只有对于计量器具或者在线检测仪器每次测量结果才必须实时上报。另外一个技巧是数据聚合上位机在本地保存分钟级区间内的最大值、最小值、平均值每分钟上报一条。这个方法在能量监控类的设备上效果异常好又能节省 MES 的存储空间又保留了数据特征。我在这个项目的二次开发分支上就增加了这个功能代码改动量很小加一个 AggregationService 类处理区间统计即可。5.3 MES 响应时间过长时对 UI 的影响处理界面卡死是 WinForms 老项目的日常原因大多是同步调用 HTTP 接口放在了 UI 线程。改造时统一使用 async/await 模式同时接入一个全局的 CancellationToken 机制窗体关闭时主动取消所有在途请求。为了减少请求次数我还会在界面上加一个数据上报状态的摘要条显示最后上报时间以及当前待重传条数操作员看到重传条数积压多了会主动检查交换机或者网线你是生产管理方时这东西能帮你省很多电话。5.4 关于 MES 系统和设备数据的最后一个心得从那以后每次接新产线我都强制自己先画一遍从 PLC 地址到 MES 界面显示的数据字典再动代码。很多翻车现场都是因为没有在两端之间对齐数据口径产量到底是件数还是公斤、状态字段里的 1 到底代表运行还是待机。C# 写这种对接项目往往最后瓶颈不是语法不是算法而是和现场各方的沟通逻辑一个清晰的字段映射表比任何设计模式都好使。如果你要接手类似的 MES 对接项目我的建议是先把点位表和接口字段表梳理出来确认它们完全一致后再开始写采集和上报代码。把每一步的数据流转都做成可视化的日志你的系统离稳定可靠就不远了。希望这篇笔记对你有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑