☰
C#上位机对接MES:从PLC数据采集到HTTP上报的完整实现
2026/10/16 11:23:16 网站建设 项目流程

简介:面向C#开发人员与制造业信息化实施人员,聚焦上位机与MES系统的数据对接场景。资源围绕C#+MES+PLC整套交互链路,给出基于OPC UA协议读取PLC设备状态、通过HTTP/SOAP/RESTful调用MES接口的完整实现方式,同时涉及HTTPS与数字证书保障传输安全、JSON/XML序列化做数据格式转换,以及异步编程与消息队列提升系统稳定性等技术点。包内含WebService服务端工程与HTTP POST客户端程序,覆盖从数据采集、接口调用到结果回传的闭环示例,便于直接拆解学习。资源共77个文件,以C#源码cs、项目配置config、可执行文件exe、动态库dll及说明文档txt等为主,压缩包大小约121KB,结构上区分客户端与服务端可对照研究。目前已有6100余人学习下载,适合正在做设备数据采集、工厂信息化改造或MES接口集成的中高级开发者参考。

1. 一个C# MES对接项目的真实价值:把产线数据从手动录入变成自动上报

做产线信息化的开发基本都碰到过这样的场景:某电池厂的PACK工位,操作员每做完一件产品,都要在电脑上打开Excel登记工时、物料批次、设备参数,下班前再由班组长把这些数据一条条敲进MES系统。月底对账的时候,系统里的数字和车间实际产量永远对不上。这个项目的出现就是来解决这类问题的——用C#写一个上位机数据通道,直接对接MES系统的HTTP接口,把设备采集到的生产数据自动上报,人只需要在异常时候介入。它适合两类人:一类是给制造企业做产线数字化、需要快速打通设备与MES的C#开发;另一类是刚接手工厂IT项目、想知道上位机到底怎么和MES握手的新手。整个方案不依赖第三方重型中间件,一套WinForm加HttpClient加本地缓存就能跑起来。

2. 技术选型与整体架构:为什么是C# + HTTP + SQLite,而不是别的组合

2.1 MES对接的主流方式:接口优先于数据库直连

对接MES,很多刚入行的开发者第一反应是“直接连MES的数据库表,往里插数据”。这个方案在演示阶段看起来很直接,但放到生产环境就是灾难:MES的库表结构属于业务核心,厂商不会开放写权限;就算开放了,一旦MES升级改表结构,上位机代码直接报废。所以现在主流制造企业的MES对接都走接口——MES侧暴露WebAPI,上位机通过HTTP POST推送JSON数据。

本项目正是走这种接口优先的路线。上位机侧的技术栈选型非常克制:界面用WinForm,通信用HttpClient,本地暂存用SQLite,PLC采集通过ModbusTCP。选WinForm而不是WPF,是因为工位机配置普遍不高,WinForm启动快、内存占用小,在Windows 7工位机上也能流畅跑;选SQLite而不是MySQL,是因为本地缓存不需要网络服务,一个dll文件就能嵌入,断电也不容易丢数据。

2.2 解决方案目录结构:每个项目文件干什么用的

整个解决方案按功能边界拆成了四个工程,从入口到数据链路一目了然:

MesBridge.sln ├─ MesBridge.App // WinForm主程序,负责界面和启动调度 │ ├─ MainForm.cs // 主窗口:设备状态监控、手动补报按钮 │ └─ Program.cs // 程序入口,初始化配置和日志 ├─ MesBridge.Core // 核心类库:对接逻辑全在这里 │ ├─ Configuration/AppConfig.cs // JSON配置读取,强类型映射 │ ├─ Plc/ModbusTcpClient.cs // PLC采集客户端,封装Modbus协议读写 │ ├─ Mes/MesApiClient.cs // MES接口客户端,封装HTTP上报 │ ├─ Queue/UploadQueue.cs // 本地待上报队列(SQLite持久化) │ ├─ Models/ReportData.cs // 上报数据模型 │ └─ Services/CollectService.cs // 采集调度服务:PLC->队列->MES ├─ MesBridge.Simulator // 模拟器:模拟PLC和MES,用于本机调试 │ ├─ SimulatePlc.cs // 虚拟PLC,定时产生随机产量数据 │ └─ SimulateMes.cs // 虚拟MES,接收JSON并返回确认 └─ MesBridge.DbMigrations // 数据库初始化脚本,建表SQL └─ create_tables.sql

MesBridge.App只做两件事:显示状态、拉起后台线程。真正的业务逻辑全部在MesBridge.Core里,这样以后想换成Windows服务或控制台运行,直接换壳就行。Simulator工程是我习惯保留的——生产环境不能随便连真PLC调试,有个模拟器在手,开发阶段就能把整条链路跑通。

2.3 数据流设计:从设备寄存器到MES数据库的四步跳跃

数据从车间设备到MES系统,中间经历四个环节,每个环节都有明确的职责划分:

PLC寄存器 -> Modbus采集线程 -> SQLite本地队列 -> HTTP上报 -> MES接口 -> MES数据库

第一步,采集线程每2秒读一次PLC的保持寄存器,拿到产量计数、当前温度、设备状态等原始值。第二步,原始值经过单位换算和格式化,封装成ReportData对象,先写入SQLite队列——这一步非常重要,写本地队列而不是直接发HTTP,是为了应对网络抖动:MES接口偶尔超时是常态,如果直接阻塞采集线程,PLC数据会把内存撑爆。第三步,独立的上报线程从队列里取出数据,POST到MES接口。第四步,MES返回确认报文后,上位机把该条数据标记为“已上报”,并从待上报队列删除。

这套设计的好处是解耦了采集和上报两个节奏:采集是高频操作(2秒一次),上报是低频操作(5秒一次批量合并)。即使MES系统维护停机两小时,数据也不会丢——全部积压在SQLite里,MES恢复后自动补报。

3. 核心模块拆解:从PLC采集到MES确认的完整链路

3.1 配置中心:AppConfig与appsettings.json

工厂环境里每台工位机连的PLC地址、MES接口地址都不一样,把配置硬编码在代码里是给自己挖坑。我一般把配置全部放到appsettings.json,程序启动时加载成强类型对象:

public class AppConfig { public PlcConfig Plc { get; set; } public MesConfig Mes { get; set; } public int CollectIntervalSeconds { get; set; } public int UploadBatchSize { get; set; } } public class PlcConfig { public string Ip { get; set; } // PLC的IP地址,如 192.168.1.10 public int Port { get; set; } // ModbusTCP端口,默认502 public int StartAddress { get; set; } // 起始寄存器地址,如0 public int ReadCount { get; set; } // 读取寄存器个数,如10 } public class MesConfig { public string BaseUrl { get; set; } // MES接口根地址 public string UploadPath { get; set; } // 上报接口路径,如 /api/production/report public string Token { get; set; } // 接口鉴权Token } // Program.cs 启动时加载 var config = JsonSerializer.Deserialize<AppConfig>( File.ReadAllText("appsettings.json") );

逻辑说明:这个配置类的设计原则是“一个工位一份配置”。CollectIntervalSeconds控制采集频率,UploadBatchSize控制每批上报条数。参数说明:StartAddress是PLC寄存器的起始地址,不同PLC厂家地址映射不同,西门子通常从0开始,三菱可能从4096开始,换设备时只需要改这个数字,不用改代码。Token是MES侧的鉴权凭证,一般由MES厂商在后台生成,有效期可能是一年也可能是半年,过期后只需要替换配置重启程序,不用重新编译。

3.2 PLC采集:ModbusTcpClient封装

PLC通信是本项目里最容易翻车的部分,尤其是字节序和寄存器地址偏移。这里用ModbusTCP协议读取保持寄存器:

public class ModbusTcpClient : IDisposable { private TcpClient _tcpClient; private NetworkStream _stream; public async Task ConnectAsync(string ip, int port) { _tcpClient = new TcpClient(); await _tcpClient.ConnectAsync(ip, port); _stream = _tcpClient.GetStream(); } public async Task<ushort[]> ReadHoldingRegistersAsync(int startAddress, int count) { // ModbusTCP报文格式: 事务ID(2字节) + 协议ID(2字节) + 长度(2字节) + 单元ID(1字节) + 功能码(1字节) + 起始地址(2字节) + 寄存器数量(2字节) byte[] request = new byte[12]; request[0] = 0x00; request[1] = 0x01; // 事务ID,每次请求递增 request[2] = 0x00; request[3] = 0x00; // 协议ID,固定为0 request[4] = 0x00; request[5] = 0x06; // 剩余字节数 request[6] = 0xFF; // 单元ID,默认255 request[7] = 0x03; // 功能码:读取保持寄存器 request[8] = (byte)(startAddress >> 8); request[9] = (byte)(startAddress & 0xFF); request[10] = (byte)(count >> 8); request[11] = (byte)(count & 0xFF); await _stream.WriteAsync(request, 0, request.Length); // 响应头: 事务ID(2) + 协议ID(2) + 长度(2) + 单元ID(1) + 功能码(1) + 字节数(1) byte[] header = new byte[9]; await ReadExactlyAsync(header, 9); int byteCount = header[8]; byte[] data = new byte[byteCount]; await ReadExactlyAsync(data, byteCount); ushort[] result = new ushort[count]; for (int i = 0; i < count; i++) { // 注意:Modbus是大端字节序,高字节在前 result[i] = (ushort)((data[i * 2] << 8) | data[i * 2 + 1]); } return result; } }

逻辑说明:ModbusTCP是最常用的PLC通信协议,报文格式固定,核心是把请求按字节拼装、把响应的字节流解析成ushort数组。代码里的request[7] = 0x03是“读保持寄存器”的功能码,保持寄存器存的是设备运行数据。参数说明:事务ID(第0-1字节)每次请求应该递增,用来匹配请求和响应,否则并发请求时响应会错乱;单元ID默认255,但对某些PLC(如台达)可能要求设为0或1,遇到通信超时先查这个。

采集线程拿到的是原始ushort值,实际业务值可能是负数(PLC用补码存储)、可能是小数点(实际值需要在程序里除以10或100)。我一般在采集后加一个换算层:

public ReportData ConvertToReport(ushort[] rawValues) { return new ReportData { DeviceId = _config.Plc.DeviceId, TotalCount = rawValues[0], // 总产量,直接整数 Temperature = rawValues[1] / 10.0, // 温度,寄存器值除以10得到实际温度 ElapsedSeconds = rawValues[2], // 运行时长 IsRunning = rawValues[3] == 1, // 运行状态,1运行0停机 CollectTime = DateTime.Now }; }

参数说明:DeviceId是这台上位机对应的MES设备编号,MES靠这个字段区分数据来源,一定要和MES侧的设备档案一致,否则数据会进错产线。

3.3 MES上报客户端:HttpClient的封装与重试策略

上报MES接口是整条链路上最容易出问题的环节,超时、返回非200、报文格式不对都会导致数据积压。我在MesApiClient里做了三层防护:超时控制、重试、报文日志:

public class MesApiClient { private readonly HttpClient _httpClient; public MesApiClient(MesConfig config) { var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }; _httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(10) }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.Token}"); _httpClient.DefaultRequestHeaders.Add("X-Device-Id", config.DeviceId); } public async Task<MesReportResult> ReportAsync(ReportData data) { var json = JsonSerializer.Serialize(data); var content = new StringContent(json, Encoding.UTF8, "application/json"); // 最多重试3次,每次间隔2秒 for (int attempt = 1; attempt <= 3; attempt++) { try { var response = await _httpClient.PostAsync(_config.UploadPath, content); var responseBody = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { // 即使HTTP 200,MES也可能返回业务失败,需要解析业务码 var result = JsonSerializer.Deserialize<MesResponse>(responseBody); if (result.Code == "0000") { return MesReportResult.Success(result.MesId); } return MesReportResult.BusinessFailed(result.Message); } Log.Warn($"上报失败 HTTP {(int)response.StatusCode} 第{attempt}次"); } catch (TaskCanceledException) { Log.Warn($"上报超时 第{attempt}次"); } catch (HttpRequestException ex) { Log.Warn($"网络异常 {ex.Message} 第{attempt}次"); } await Task.Delay(2000); } return MesReportResult.Failed(); } }

逻辑说明:ReportAsync是上报的核心方法,做了三层防护。第一层是HTTP超时控制,10秒内MES没响应就当作一次失败;第二层是重试机制,最多3次,避免网络抖动导致单条数据误判;第三层是业务码校验,MES系统返回HTTP 200但Code非“0000”的情况很常见——比如工单已关闭、参数校验不过,必须把这种“假成功”识别出来。参数说明:ServerCertificateCustomValidationCallback是给测试环境用的,MES测试环境的HTTPS证书通常不是正规CA签发的,不加上这个回调HttpClient会直接抛证书异常;上生产环境前建议移除这一行,避免安全隐患。

3.4 本地队列:SQLite作为断网保险丝

上报失败的数据不能丢,也不能全堆内存里。我这边用SQLite做持久化队列,表结构非常简单:

CREATE TABLE upload_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, data_json TEXT NOT NULL, -- 上报数据的JSON快照 status INTEGER DEFAULT 0, -- 0待上报 1上报中 2已上报 3失败 retry_count INTEGER DEFAULT 0, -- 已重试次数 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_queue_status ON upload_queue(status);

写队列和扫队列的操作分散在两个线程里,写线程只负责INSERT,上报线程只负责SELECT status=0的数据,两者通过status字段协作而不是用内存队列,这样即使程序崩溃重启,数据仍然在SQLite里。

public class UploadQueue { private readonly string _connectionString; public async Task EnqueueAsync(ReportData data) { var json = JsonSerializer.Serialize(data); using var conn = new SqliteConnection(_connectionString); await conn.OpenAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = "INSERT INTO upload_queue (device_id, data_json) VALUES ($deviceId, $json)"; cmd.Parameters.AddWithValue("$deviceId", data.DeviceId); cmd.Parameters.AddWithValue("$json", json); await cmd.ExecuteNonQueryAsync(); } public async Task<List<UploadQueueItem>> GetPendingItemsAsync(int batchSize) { using var conn = new SqliteConnection(_connectionString); await conn.OpenAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT id, data_json FROM upload_queue WHERE status = 0 LIMIT $batchSize"; cmd.Parameters.AddWithValue("$batchSize", batchSize); // ... 执行并映射对象 } public async Task MarkStatusAsync(long id, int status) { using var conn = new SqliteConnection(_connectionString); await conn.OpenAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = "UPDATE upload_queue SET status = $status, update_time = CURRENT_TIMESTAMP WHERE id = $id"; cmd.Parameters.AddWithValue("$status", status); cmd.Parameters.AddWithValue("$id", id); await cmd.ExecuteNonQueryAsync(); } }

逻辑说明:EnqueueAsync是唯一的数据入口,不管上报成功失败,所有采集数据先入队。GetPendingItemsAsync按batchSize拉取待上报数据,MarkStatusAsync在MES确认后把状态改成已上报。参数说明:retry_count字段记录重试次数,我一般设置超过5次就丢弃并写日志——工厂场景里,一条数据重试5次还失败,说明要么MES接口彻底挂了,要么数据本身有问题,再重试也是浪费资源。

3.5 后台调度:采集线程与上报线程的配合

整个程序的心脏是CollectService,它负责调度两个线程:采集线程按固定间隔读PLC,上报线程按固定间隔扫队列:

public class CollectService { private readonly AppConfig _config; private readonly ModbusTcpClient _plcClient; private readonly UploadQueue _queue; private readonly MesApiClient _mesClient; private CancellationTokenSource _cts; public async Task StartAsync() { _cts = new CancellationTokenSource(); var token = _cts.Token; await _plcClient.ConnectAsync(_config.Plc.Ip, _config.Plc.Port); // 采集线程:每2秒读一次寄存器 var collectTask = Task.Run(async () => { while (!token.IsCancellationRequested) { try { var raw = await _plcClient.ReadHoldingRegistersAsync( _config.Plc.StartAddress, _config.Plc.ReadCount); var reportData = ConvertToReport(raw); await _queue.EnqueueAsync(reportData); } catch (Exception ex) { Log.Error(ex, "PLC采集异常,尝试重新连接"); await _plcClient.ConnectAsync(_config.Plc.Ip, _config.Plc.Port); } await Task.Delay(_config.CollectIntervalSeconds * 1000, token); } }, token); // 上报线程:每5秒批量上报一次 var uploadTask = Task.Run(async () => { while (!token.IsCancellationRequested) { try { var items = await _queue.GetPendingItemsAsync(_config.UploadBatchSize); foreach (var item in items) { var data = JsonSerializer.Deserialize<ReportData>(item.DataJson); var result = await _mesClient.ReportAsync(data); if (result.Success) await _queue.MarkStatusAsync(item.Id, 2); else await _queue.MarkStatusAsync(item.Id, 0); } } catch (Exception ex) { Log.Error(ex, "上报线程异常"); } await Task.Delay(5000, token); } }, token); await Task.WhenAll(collectTask, uploadTask); } }

逻辑说明:两个线程通过SQLite队列解耦,采集线程不管上报是否成功,只管往队列里写,这样PLC采集不会因为MES接口慢而被阻塞。Task.Run创建两个后台任务,配合CancellationToken实现优雅停止——程序关闭时先停止采集,把队列里剩余数据上报完再退出。参数说明:_config.CollectIntervalSeconds和_config.UploadBatchSize这两个参数直接影响系统行为,采集频率太高会增加PLC通信压力,建议2~5秒之间;上报批量太大单次HTTP报文会过大,建议50条以内。

4. 数据模型与接口约定:对接前先把这些字段扯清楚

4.1 MES上报接口的报文设计

工厂对接MES最磨人的不是写代码,而是跟MES厂商对字段。本项目里定义的ReportData模型,在动手前就跟MES方对齐了字段含义和类型:

{ "deviceId": "PACK_LINE_03", "workOrderId": "WO20240521001", "productCode": "BATTERY-48V20AH", "totalCount": 128, "goodCount": 126, "defectCount": 2, "temperature": 36.5, "isRunning": true, "collectTime": "2024-05-21 14:30:22" }

字段说明表:

字段名类型说明必填
deviceIdstring设备/工位编号,MES侧必须存在是
workOrderIdstring当前生产工单号,由MES下发给工位是
productCodestring产品编码,决定工艺路线是
totalCountint累计总产量,MES用来校验产量是
goodCountint良品数,从PLC的OK计数器读取是
defectCountint不良品数,从PLC的NG计数器读取是
temperaturefloat当前温度,寄存器值除以10否
isRunningbool设备运行状态是
collectTimedatetime数据采集时间是

workOrderId是MES对接里最关键的字段,MES的所有产量统计都挂在工单下面。实际做法是:MES系统通过接口下发了当前工单号,上位机在收到新工单号时更新配置,上报数据时带上当前工单号。这块涉及MES侧主动下发,我在后面的改造章节里会提。

4.2 本地库表结构:除了队列还需要什么

除了upload_queue,本地库还需要几张表来支撑业务:

-- 工单信息表:记录MES下发的当前工单 CREATE TABLE work_order ( id INTEGER PRIMARY KEY, work_order_id TEXT NOT NULL, product_code TEXT, plan_count INTEGER, start_time DATETIME, end_time DATETIME, status INTEGER DEFAULT 0 -- 0进行中 1已结束 ); -- 上报历史表:留存已上报数据,方便对账 CREATE TABLE upload_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, data_json TEXT NOT NULL, mes_id TEXT, -- MES返回的确认编号 upload_time DATETIME DEFAULT CURRENT_TIMESTAMP );

upload_history和upload_queue是两种不同用途的表:队列表是“待办事项”,历史表是“存档记录”。上报成功的数据从队列移到历史表,月底跟MES对账时可以直接查历史表,不用去翻MES的数据库。

4.3 数据状态机:一条数据从生到死的完整流转

设计队列时我把上报状态定义成4个值,整个状态流转是一个典型的“待办-处理中-终态”模型:

0 待上报 -> 1 上报中 -> 2 已上报(终态) |-> 3 失败(超过最大重试次数,人工介入)

多线程并发时,两个上报线程同时取到同一条数据的概率存在,所以取队列的SQL要加FOR UPDATE或者用事务隔离。更简单的方式是单上报线程,本项目直接用单线程,彻底避免并发冲突。采集线程是唯一的写入方,上报线程是唯一的读取方,两个线程操作不同的字段,中间靠status字段衔接,不需要额外的锁机制——这是SQLite队列比内存队列更稳的原因。

5. 运行部署与避坑排查:从本机调试到工位机稳定运行

5.1 本机调试三步走:没有PLC也能跑通全链路

生产环境不能随便连PLC调试,模拟器工程就是干这个的。调试步骤固定三步,我每次搭新项目都是这个套路:

第一步,启动MesBridge.Simulator里的SimulateMes——这是一个简单的ASP.NET Core WebApi,监听本地5000端口,收到POST请求后返回{"Code":"0000","MesId":"MES20240521001"};第二步,启动SimulatePlc——虚拟一个ModbusTCP服务端,监听502端口,每100毫秒自增一次产量寄存器,欺骗客户端“PLC在正常运转”;第三步,启动MesBridge.App,配置appsettings.json里的IP指向127.0.0.1,观察主界面是否能看到产量在上涨、队列积压在减少。

如果三步走完数据链路是通的,再把IP换成真实PLC和MES地址。这一步能规避80%的对接问题,因为通信链路逻辑在模拟环境已经验证过,真机排查范围就缩小到“地址对不对”“字段名对不对”。

5.2 部署到工位机:命令行启动和Windows服务两种姿势

调试完成后部署到车间工位机,通常有两种方式,按工位机的使用场景选:

# 方式一:开机启动文件夹(推荐,操作简单) # 把发布目录扔到 shell:startup 启动文件夹 "C:\Users\Operator\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup" # 方式二:注册为Windows服务(适合无人值守工位) sc create MesBridgeService binPath= "C:\MesBridge\MesBridge.App.exe --service" start= auto

方式一适合有人值守的工位——操作员每天开机能自动拉起程序,关闭窗口时确认一下队列数据已清空;方式二适合无人值守的自动化产线,程序崩溃后Windows服务能自动重启,操作员不会去碰它。工位机上Windows系统如果开启了自动更新,建议把时间调整到非生产时段,否则半夜系统重启后服务没有自启,第二天产线数据就是断档的。

5.3 避坑排查:对接过程中最常踩的四个坑

坑一:MES返回200就判定成功,结果数据全部对不上账。现象是上位机日志里全是“上报成功”,月底MES那边却说少了一百多条数据。原因是MES的HTTP 200只是“网关收到了”,业务层面可能校验失败返回了业务错误码,但上位机没解析Code字段直接当作成功。解决方法是严格校验报文里的业务码,非“0000”一律走失败重试逻辑,同时把MES返回的原始Body完整记录到日志里备查。

坑二:PLC寄存器地址偏移一位,产量数据错乱。现象是产量偶尔正常偶尔夸张,比如实际128件读出来是1024件。原因是Modbus地址的0和1在不同PLC里有不同含义,有的PLC把0作为起始地址,有的从1开始,代码里StartAddress少加1就全错位了。解决方法是先用PLC编程软件在线监视寄存器地址,确认实际地址值,再填到配置里——我血泪经验,不要凭记忆写地址,一定要在线确认。

坑三:温度单位不一致,MES那边显示36.5度,车间实际温度是365度。现象是数据整体放大10倍或缩小10倍。原因是PLC侧为了省寄存器空间把小数放大了10倍存储,上位机没做除法换算直接上传。解决方法和换算逻辑绑在一起,在ConvertToReport方法里对每个寄存器的业务含义和缩放比例单独定义,而不是统一处理。

坑四:工位机断电,SQLite队列文件损坏。现象是重启后程序报database disk image is malformed,积压的数据全废了。原因是SQLite在写入中突然断电,WAL日志没有落盘。解决方法是打开SQLite的WAL模式,并设置synchronous=NORMAL,这样断电后自动从WAL恢复;同时配一个定时任务,每10分钟执行一次VACUUM,能极大降低损坏概率。

6. 二次开发与改造思路:从固定对接变成灵活的数据通道

6.1 从硬编码字段到可视化映射配置

当前实现里ConvertToReport是把寄存器数组按固定下标映射成业务字段,换一个设备型号就要重新编译发布。改造思路是引入一份“映射配置”,用JSON描述“寄存器地址到业务字段”的对应关系:

{ "fieldMappings": [ { "fieldName": "totalCount", "registerIndex": 0, "scale": 1, "dataType": "int" }, { "fieldName": "temperature", "registerIndex": 1, "scale": 0.1, "dataType": "float" }, { "fieldName": "isRunning", "registerIndex": 2, "scale": 1, "dataType": "bool", "trueValue": 1 } ], "mesFields": { "deviceId": "PACK_LINE_03", "workOrderId": "{currentWorkOrder}" } }

改造后,换设备只需要新增一套映射配置,代码里加一个“字段反射赋值”的方法,根据配置的fieldName从寄存器取原始值,按scale缩放后,反射赋值到ReportData对应属性。这样就把“代码写死”变成“数据驱动”,设备接入的时间从改代码重新发布的两小时,缩短到改配置重启的十分钟。

6.2 从单向上报到双向交互:处理MES的工单下发

当前实现是上位机单向往MES推数据,但实际生产里MES要往工位下发工单——产品编码、计划数量、工艺参数。改造时在MesApiClient里加一个PollWorkOrderAsync方法,每30秒拉一次当前工单:

public async Task<WorkOrder> GetCurrentWorkOrderAsync() { var response = await _httpClient.GetAsync("/api/workorder/current"); var body = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<WorkOrderResponse>(body).Data; }

拿到新工单号时,更新work_order表并把currentWorkOrder切换成新的。这样上报数据时自动带上当前工单号,操作员不需要手动切换,MES那边对账也更准确。我一般还会加一个“工单切换确认”弹窗:工单号变化时提示操作员确认,防止MES侧下发错误导致数据挂错工单。

6.3 单实例到多设备:一台工位机管理多台PLC

如果一条产线上有多台设备连同一台上位机,改造方向是把CollectService改成支持多采集源配置:

public class PlcDeviceConfig { public string DeviceId { get; set; } public PlcConfig Plc { get; set; } public List<FieldMappingConfig> Mappings { get; set; } }

在CollectService里对每个PlcDeviceConfig开一个采集任务,共享同一个UploadQueue队列,上报时按DeviceId区分数据来源。这只需要把原来的“单PLC支持”扩成“配置数组”,核心链路不用动,工作量不大但能覆盖产线的常见形态。

对接MES这条路说难不难,说简单也容易翻车——我现在每接一个新项目,第一件事不是写代码,而是先跟MES方把字段表和报文示例对齐,然后搭模拟器把链路跑通,再接真机。从那以后我每个对接项目都强制走一遍“配置确认-模拟联调-真机验证”的流程,省下来的返工时间远比多花的一晚上调试时间值。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询