3个工厂项目落地总结:C#分布式上位机系统,从单机工位到全厂级统一管控全实战

📅 发布时间:2026/7/4 23:39:37 👁️ 浏览次数:
3个工厂项目落地总结:C#分布式上位机系统,从单机工位到全厂级统一管控全实战
去年一整年我带着团队啃完了3个汽车零部件工厂的整线上位机改造项目从最开始单工位的单机版上位机到车间级分布式管控再到最终全厂级的统一调度平台前前后后改了5版架构踩了不下几十个致命坑——从通信超时、数据丢包到跨车间网络隔离、多系统数据同步再到MES对接、全链路权限管控算是把工业场景下分布式上位机的全链路落地逻辑摸了个透。很多同行一提到分布式系统第一反应就是互联网大厂的微服务、高并发架构直接照搬过来用在工业现场结果十有八九要翻车。工业场景的分布式上位机核心从来不是炫技而是稳定、容错、边缘自治——毕竟工厂里的产线停一分钟就是几万甚至几十万的损失互联网那套“服务降级、熔断”的逻辑在产线24小时不间断运行的需求面前根本不适用。这篇文章我就结合3个工厂项目的实战经验从架构设计、技术选型、核心代码实现到现场踩坑与优化完整拆解一套C#技术栈实现的、从单机工位到全厂级管控的分布式上位机系统所有内容都是产线验证过的干货没有空泛的理论拿来就能用。一、先搞懂为什么要放弃单机版做分布式上位机最开始这几个项目客户用的都是单工位单机版上位机——每个工位一台工控机装一套上位机软件直接和工位的PLC、视觉系统、传感器通信数据存在本地SQLite里。用了不到半年问题全暴露了数据孤岛严重车间主任要查当天的产量、设备OEE得挨个工位跑一遍手动抄数据下班汇总要花2个小时还经常对不上数系统升级维护难50个工位的产线软件改个bug、加个功能要工程师挨个工位跑一遍更新一次要大半天产线还不能停只能半夜加班权限管控混乱每个工位的上位机自己管用户权限操作工随便就能改工艺参数出了质量问题找不到责任人完全不符合IATF16949的追溯要求系统对接成本极高客户要上MES系统需要每个工位的上位机都开接口光对接调试就花了1个多月后期加个工位还要重新对接故障排查效率极低产线出了问题要挨个工位查日志、看数据经常错过最佳处理时间故障停机时间越拉越长。这些问题靠单机版上位机根本解决不了必须做分布式架构——把采集、控制、数据、管控分层解耦既能保证单工位的独立运行又能实现全厂级的数据统一和调度管控。二、核心设计原则工业分布式上位机的红线绝对不能碰在做第一版架构的时候我们踩了个致命的坑照搬互联网的微服务架构把工位的控制逻辑、逻辑判断全放到了服务端结果车间网络一波动工位直接停线客户当场就炸了。从那之后我们定了4条工业分布式上位机的核心设计原则后面所有的架构迭代都严格遵守再也没出过类似的问题边缘自治第一原则单工位的实时控制逻辑、急停处理、设备联动必须全部在边缘端工位工控机本地执行绝对不能依赖服务端。服务端只负责数据汇总、参数下发、非实时调度哪怕全厂网络断了每个工位都能正常生产不受任何影响数据不丢原则必须做断网续传机制网络中断时边缘端采集的所有数据都存在本地缓存网络恢复后自动补传绝对不能出现数据丢失、统计断层的情况容错优先原则任何一个节点、一个服务故障都不能影响整个系统的运行。单个工位上位机故障不影响其他工位单个车间服务故障不影响其他车间和全厂平台的运行网络适配原则工厂的网络环境极其复杂设备网、生产网、办公网三层隔离防火墙限制极多系统必须适配这种网络环境尽量少开端口优先用边缘端主动外联的方式避免在设备网开入站端口。三、整体架构设计从单机到全厂级的三层分布式架构基于上面的4条原则我们最终落地了一套三层分布式架构从边缘采集到车间级汇聚再到工厂级管控层层解耦既能满足单工位的实时控制需求又能实现全厂级的统一管控目前已经在5个工厂项目里稳定运行最长的已经不间断运行了11个月。整体架构分层【工厂级管控层】全厂统一管控平台、可视化大屏、MES/ERP对接、统一权限管理、报表中心 ↓↑企业内网/防火墙单向放行MQTT/gRPC通信 【车间级服务层】车间数据汇聚服务、MQTT Broker、配方管理服务、报警中心、历史数据存储 ↓↑生产网工业交换机MQTT/gRPC通信 【边缘采集层】工位上位机WPF、边缘采集服务、PLC/传感器/视觉通信、本地控制逻辑、本地缓存每层核心职责与C#技术选型工业场景的技术选型核心是稳定、兼容、易维护优先选微软生态的成熟技术避免用太新、太冷门的框架不然现场出了问题连排查的资料都找不到。架构分层核心职责C#技术栈选型边缘采集层单工位设备通信、实时数据采集、本地控制逻辑、人机交互、断网本地缓存、数据上报.NET 8 WPF工位人机界面、BackgroundService后台采集服务、MQTTnet消息通信、S7.NET/NModbusPLC通信、SQLite本地缓存、CommunityToolkit.MvvmMVVM框架车间级服务层车间内数据汇聚、消息转发、配方下发、报警汇总、历史数据存储、边缘端管理ASP.NET Core 8WebAPI/gRPC、MQTTnetMQTT Broker、Serilog统一日志、InfluxDB时序数据存储、Redis实时数据缓存、SQL Server关系型数据工厂级管控层全厂数据汇总、统一权限管控、跨车间调度、MES/ERP系统对接、可视化大屏、报表统计、全局报警管理ASP.NET Core 8后端服务、BlazorWeb管控端/大屏、Identity FrameworkRBAC权限、Quartz.NET定时任务、EFCoreORM框架这里重点提两个选型的原因为什么用MQTT做核心通信协议工厂里设备网和生产网是隔离的防火墙只允许边缘端主动外联不允许外网主动访问设备网。MQTT是客户端主动连接Broker只需要开一个1883加密用8883端口完全适配工厂的网络隔离要求而且协议轻量开销极小断连重连机制成熟完美适配工业网络波动的场景为什么用时序数据库InfluxDB一个车间50个工位每个工位每秒上报100个工艺参数一天就是4亿多条数据用SQL Server存历史数据查询报表直接卡崩。InfluxDB是专门为时序数据设计的针对工业采集的时间序列数据做了极致优化同样的数据量查询速度比SQL Server快上百倍还支持自动降采样完美解决工业大数据的存储和查询问题。四、核心模块C#代码实现拿来就能用的产线级代码整个系统的模块非常多这里我挑4个最核心、最容易踩坑的模块分享产线验证过的完整代码都是我们在项目里用了大半年的稳定性完全有保障。4.1 边缘采集服务边缘自治断网续传的核心实现边缘采集服务是整个系统的基石核心要实现两个功能一是不间断采集PLC数据本地执行控制逻辑保证边缘自治二是断网时本地缓存数据网络恢复后自动补传保证数据不丢。我们用.NET 8的BackgroundService实现后台托管服务配合SQLite做本地缓存代码如下/// summary/// 工位边缘采集托管服务24小时不间断运行/// /summarypublicclassEdgeCollectService:BackgroundService{privatereadonlyIPlcCommService_plcCommService;// PLC通信服务privatereadonlyIMqttClientService_mqttClientService;// MQTT通信服务privatereadonlyILocalCacheService_localCacheService;// 本地SQLite缓存服务privatereadonlyILoggerEdgeCollectService_logger;privatereadonlyint_collectInterval100;// 采集周期100msprivatebool_isNetworkConnected_mqttClientService.IsConnected;publicEdgeCollectService(IPlcCommServiceplcCommService,IMqttClientServicemqttClientService,ILocalCacheServicelocalCacheService,ILoggerEdgeCollectServicelogger){_plcCommServiceplcCommService;_mqttClientServicemqttClientService;_localCacheServicelocalCacheService;_loggerlogger;}protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){_logger.LogInformation(边缘采集服务启动工位编号{StationId},AppConfig.StationId);// 启动时先执行补传逻辑把上次断网未上传的数据补传await_mqttClientService.WaitForConnectionAsync(stoppingToken);awaitReUploadLocalCacheDataAsync(stoppingToken);// 主采集循环while(!stoppingToken.IsCancellationRequested){try{// 1. 核心本地采集PLC数据执行控制逻辑不依赖网络varcollectDataawait_plcCommService.CollectRealTimeDataAsync(stoppingToken);if(collectDatanull){_logger.LogWarning(PLC数据采集失败跳过本次循环);awaitTask.Delay(_collectInterval,stoppingToken);continue;}// 2. 数据写入WPF界面绑定的缓存实时刷新界面AppData.RealTimeData.UpdateData(collectData);// 3. 网络正常上报数据同时写入本地备份网络异常只写入本地缓存if(_isNetworkConnected){// 先上报实时数据varuploadResultawait_mqttClientService.PublishDataAsync(collectData,stoppingToken);if(!uploadResult){// 上报失败写入本地缓存后续补传await_localCacheService.WriteCacheDataAsync(collectData,stoppingToken);_logger.LogWarning(数据上报失败已写入本地缓存);}else{// 上报成功触发一次补传逻辑把之前缓存的数据传上去awaitReUploadLocalCacheDataAsync(stoppingToken);}}else{// 网络断开只写入本地缓存不影响本地采集和控制await_localCacheService.WriteCacheDataAsync(collectData,stoppingToken);_logger.LogDebug(网络断开数据已写入本地缓存);}}catch(Exceptionex){_logger.LogError(ex,采集循环执行异常);}finally{// 保证采集周期稳定不受单次循环执行时间影响awaitTask.Delay(_collectInterval,stoppingToken);}}_logger.LogInformation(边缘采集服务已停止);}/// summary/// 补传本地缓存的数据上报成功后删除本地记录/// /summaryprivateasyncTaskReUploadLocalCacheDataAsync(CancellationTokenstoppingToken){if(!_isNetworkConnected)return;try{// 每次取100条批量上报避免单次上报数据量过大varcacheListawait_localCacheService.GetTopCacheDataAsync(100,stoppingToken);if(cacheListnull||cacheList.Count0)return;varuploadResultawait_mqttClientService.BatchPublishDataAsync(cacheList,stoppingToken);if(uploadResult){// 上报成功删除已上传的缓存数据varuploadedIdscacheList.Select(xx.Id).ToList();await_localCacheService.DeleteCacheDataAsync(uploadedIds,stoppingToken);_logger.LogInformation(本地缓存数据补传成功数量{Count},uploadedIds.Count);}}catch(Exceptionex){_logger.LogError(ex,本地缓存数据补传异常);}}}这段代码的核心就是把本地采集控制和网络上报完全解耦哪怕网络完全断开采集和控制逻辑也能正常执行不会影响工位生产同时数据不会丢失网络恢复后自动补传完美符合工业场景的需求。4.2 MQTT通信模块边缘端与服务端的通信封装我们用MQTTnet这个成熟的开源库封装了边缘端的MQTT客户端核心实现自动重连、消息发布、订阅指令的功能适配工业网络波动的场景/// summary/// MQTT客户端服务封装适配工业网络波动场景/// /summarypublicclassMqttClientService:IMqttClientService{privatereadonlyIMqttClient_mqttClient;privatereadonlyMqttClientOptions_clientOptions;privatereadonlyILoggerMqttClientService_logger;privatereadonlyCancellationToken_stoppingToken;publicboolIsConnected_mqttClient.IsConnected;publicMqttClientService(ILoggerMqttClientServicelogger,IHostApplicationLifetimeappLifetime){_loggerlogger;_stoppingTokenappLifetime.ApplicationStopping;// 初始化MQTT客户端配置_clientOptionsnewMqttClientOptionsBuilder().WithTcpServer(AppConfig.MqttBrokerIp,AppConfig.MqttBrokerPort).WithCredentials(AppConfig.MqttUserName,AppConfig.MqttPassword).WithClientId($Station_{AppConfig.StationId}_{Guid.NewGuid():N})// 客户端ID唯一避免重复连接被踢.WithCleanSession(false)// 保留会话断连重连后能收到离线消息.WithKeepAlivePeriod(TimeSpan.FromSeconds(5))// 心跳包5秒.WithTimeout(TimeSpan.FromSeconds(3)).Build();_mqttClientnewMqttFactory().CreateMqttClient();// 注册事件_mqttClient.ConnectedAsyncOnConnectedAsync;_mqttClient.DisconnectedAsyncOnDisconnectedAsync;_mqttClient.ApplicationMessageReceivedAsyncOnMessageReceivedAsync;}/// summary/// 启动客户端连接/// /summarypublicasyncTaskStartAsync(CancellationTokencancellationToken){try{await_mqttClient.ConnectAsync(_clientOptions,cancellationToken);_logger.LogInformation(MQTT客户端连接成功Broker地址{Ip}:{Port},AppConfig.MqttBrokerIp,AppConfig.MqttBrokerPort);}catch(Exceptionex){_logger.LogError(ex,MQTT客户端连接失败);}}/// summary/// 等待连接成功超时时间30秒/// /summarypublicasyncTaskWaitForConnectionAsync(CancellationTokencancellationToken){varwaitCount0;while(!IsConnectedwaitCount30!cancellationToken.IsCancellationRequested){awaitTask.Delay(1000,cancellationToken);waitCount;}}/// summary/// 发布单条采集数据/// /summarypublicasyncTaskboolPublishDataAsync(CollectDataModeldata,CancellationTokencancellationToken){if(!IsConnected)returnfalse;try{varmessagenewMqttApplicationMessageBuilder().WithTopic($Factory/Workshop/{AppConfig.WorkshopId}/Station/{AppConfig.StationId}/Data).WithPayload(JsonSerializer.Serialize(data)).WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)// QoS1保证消息至少送达一次.WithRetainFlag(false).Build();varresultawait_mqttClient.PublishAsync(message,cancellationToken);returnresult.ReasonCodeMqttClientPublishReasonCode.Success;}catch(Exceptionex){_logger.LogError(ex,MQTT消息发布失败);returnfalse;}}/// summary/// 断连自动重连/// /summaryprivateasyncTaskOnDisconnectedAsync(MqttClientDisconnectedEventArgsargs){_logger.LogWarning(MQTT客户端断开连接原因{Reason},args.Reason);if(_stoppingToken.IsCancellationRequested)return;// 断连后自动重连间隔1秒无限重试while(!_stoppingToken.IsCancellationRequested){try{awaitTask.Delay(1000,_stoppingToken);varresultawait_mqttClient.ConnectAsync(_clientOptions,_stoppingToken);if(result.ResultCodeMqttClientConnectResultCode.Success){_logger.LogInformation(MQTT客户端重连成功);break;}}catch(Exceptionex){_logger.LogError(ex,MQTT客户端重连失败继续重试);}}}// 其他方法批量发布、订阅指令处理、连接成功事件等篇幅原因不全部展示}这里有两个关键细节是我们踩坑后优化的客户端ID必须唯一加上工位号和GUID避免多个工位用同一个客户端ID导致连接被Broker踢下线QoS等级用AtLeastOnce保证消息至少送达一次配合本地消息ID去重既不会丢数据也不会重复上报。4.3 全厂级分布式权限管控RBAC权限系统实现工厂级管控的核心就是统一的权限管理不能每个工位自己管自己的用户。我们基于ASP.NET Core Identity实现了一套RBAC基于角色的访问控制权限系统支持工位级、车间级、工厂级三级权限隔离完全适配工厂的组织架构。核心权限设计超管全厂所有权限系统配置、用户管理厂长全厂所有数据查看、报表统计、全局调度车间主任对应车间的所有数据查看、报警处理、报表统计工艺员配方管理、工艺参数下发、工艺数据查看设备工程师设备数据查看、故障处理、设备维护记录操作工对应工位的界面查看、设备启停操作无参数修改权限核心代码实现了权限校验过滤器所有API接口都能通过特性标注权限自动校验/// summary/// 自定义权限校验特性/// /summary[AttributeUsage(AttributeTargets.Method|AttributeTargets.Class)]publicclassPermissionAttribute:AuthorizeAttribute,IAsyncAuthorizationFilter{privatereadonlystring_permissionCode;privatereadonlyint_dataLevel;// 数据权限等级1-工位级 2-车间级 3-工厂级publicPermissionAttribute(stringpermissionCode,intdataLevel3){_permissionCodepermissionCode;_dataLeveldataLevel;}publicasyncTaskOnAuthorizationAsync(AuthorizationFilterContextcontext){varusercontext.HttpContext.User;if(!user.Identity.IsAuthenticated){context.ResultnewUnauthorizedResult();return;}// 获取用户服务varuserServicecontext.HttpContext.RequestServices.GetRequiredServiceIUserService();varuserIduser.FindFirstValue(ClaimTypes.NameIdentifier);// 1. 校验功能权限varhasPermissionawaituserService.CheckUserPermissionAsync(userId,_permissionCode);if(!hasPermission){context.ResultnewForbidResult();return;}// 2. 校验数据权限varuserDataLevelawaituserService.GetUserDataLevelAsync(userId);if(userDataLevel_dataLevel){context.ResultnewForbidResult();return;}// 3. 把用户的车间、工位权限写入HttpContext后续业务逻辑直接用varuserWorkshopsawaituserService.GetUserWorkshopIdsAsync(userId);context.HttpContext.Items[UserWorkshopIds]userWorkshops;}}// API接口使用示例[ApiController][Route(api/[controller])]publicclassWorkshopDataController:ControllerBase{/// summary/// 获取车间产量数据需要车间级以上数据权限/// /summary[HttpGet(output)][Permission(WorkshopData:View,dataLevel:2)]publicasyncTaskIActionResultGetWorkshopOutput([FromQuery]intworkshopId,[FromQuery]DateTimestartTime,[FromQuery]DateTimeendTime){// 校验用户是否有该车间的权限varuserWorkshopsHttpContext.Items[UserWorkshopIds]asListint;if(!userWorkshops.Contains(workshopId)){returnForbid();}// 业务逻辑vardataawait_workshopDataService.GetWorkshopOutputAsync(workshopId,startTime,endTime);returnOk(data);}}这套权限系统实现了功能权限和数据权限的双重管控用户在全厂任意一个终端登录都只能看到自己权限范围内的内容完全满足工厂的管理要求和合规要求。4.4 分布式报警中心全链路报警闭环处理工厂里的报警管理绝对不是简单的弹窗提示而是要实现边缘端本地报警、车间级汇总、工厂级全局推送、报警确认、处理记录、闭环追溯的全流程管理。核心实现逻辑边缘端采集到报警信号本地立即弹窗声光报警同时通过MQTT上报到车间服务车间服务收到报警根据报警等级推送给对应的负责人比如设备故障推给设备工程师质量报警推给工艺员工厂级平台实时展示所有车间的报警数据统计报警次数、故障停机时间生成报表报警处理完成后处理人填写处理记录系统闭环归档实现全流程追溯。五、项目落地踩过的那些致命坑每一个都导致过停线这部分是整篇文章最有价值的内容都是我们用停线的代价踩出来的坑每一个都能帮你避开项目里的致命问题。坑1把控制逻辑放到服务端网络波动直接停线这是我们最开始犯的最致命的错误把工位的工艺逻辑判断、设备联动控制放到了车间服务端边缘端只负责采集数据和执行指令。结果车间的工业交换机出了故障网络断了5分钟整个车间的工位全停了客户当场就发了火停线1小时损失了十几万。解决方案严格遵守边缘自治第一原则所有实时控制逻辑、设备联动、急停处理全部在边缘端本地执行服务端只负责非实时的参数下发、数据汇总、配方管理哪怕网络完全断开工位也能正常生产。坑2用SQL Server存时序数据报表查询直接卡崩最开始我们把所有的采集数据都存在SQL Server里一个车间50个工位一天就产生4亿多条数据客户要查一个月的产量报表直接查了10分钟都没出来数据库CPU直接拉满整个系统都卡了。解决方案用时序数据库InfluxDB存储历史采集数据针对高频采集数据做降采样原始100ms采集的数据保留7天1分钟级降采样数据最大值、最小值、平均值保留1年1小时级降采样数据保留3年优化后同样的报表查询响应时间从10分钟降到了200ms以内性能提升了3000倍。坑3全厂设备时间不同步数据统计完全对不上项目上线初期客户经常投诉两个车间的产量统计对不上同一个产品的生产时间在两个工位差了5分钟追溯的时候完全对不上。查了好久才发现各个工位的工控机、PLC、服务端的时间都不一样最多的差了十几分钟。解决方案在工厂机房搭建一台NTP时间服务器全厂所有的工控机、PLC、服务器、交换机全部统一同步这台NTP服务器的时间误差控制在100ms以内彻底解决了时间不同步的问题。坑4网络隔离导致的对接失败被IT部门打回3次工厂的网络是三层隔离的设备网工位PLC、工控机、生产网车间服务、办公网工厂平台、MES防火墙默认拒绝所有跨网访问只允许白名单端口。最开始我们用WebAPI做对接要在设备网开一堆入站端口被客户的IT部门连续打回3次说不符合网络安全规范。解决方案全部改用MQTT协议边缘端设备网主动连接生产网的MQTT Broker只需要在生产网开一个1883端口设备网不需要开任何入站端口完美适配了工厂的网络隔离要求一次性通过了IT部门的安全审核。坑5程序升级要挨个工位跑50个工位要一下午最开始的单机版程序升级要工程师带着U盘挨个工位跑一遍50个工位要一下午还只能半夜产线停了才能升级非常麻烦。解决方案做了自动升级功能在服务端搭建升级包管理服务边缘端上位机启动的时候自动检查服务端的最新版本有新版本就自动下载升级包静默安装升级不需要人工干预。后来我们还加了灰度升级功能先给2个测试工位升级没问题再全车间推送彻底解决了升级难的问题。六、最终落地效果这套分布式上位机系统我们在3个汽车零部件工厂落地后给客户带来的提升是肉眼可见的数据统计效率提升100倍原来车间主任每天下班要花2小时手动汇总数据现在实时报表自动生成点开就能看数据准确率100%故障处理时间缩短60%原来出了故障要挨个工位查问题现在报警实时推送给对应负责人平台直接展示故障点位和原因平均故障处理时间从40分钟降到了15分钟设备OEE提升15%通过全厂级的设备数据监控和分析优化了设备的换型时间和停机等待时间设备综合效率从原来的75%提升到了90%系统维护成本降低80%原来升级一次系统要大半天现在远程一键推送10分钟就能完成全车间升级不用再跑现场MES对接成本降低90%原来MES要和每个工位的上位机对接现在只需要对接工厂级平台的统一接口对接时间从1个多月降到了3天。结尾很多人一提到分布式上位机就觉得是很复杂、很高大上的东西其实不然。工业场景的分布式系统核心从来不是架构有多炫酷技术有多先进而是能不能经得住产线24小时不间断运行的考验能不能解决工厂的实际问题。这套系统我们现在已经用到了5个工厂项目里踩过的坑都填平了稳定性完全经得住考验。后面我会陆续更新这个系列的文章包括WPF工位上位机的完整工程、MQTT Broker的搭建与优化、InfluxDB时序数据的查询实战、Blazor工厂管控大屏的实现感兴趣的朋友可以关注一下。