ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于WPF的MES上位机产线执行系统架构设计与实践

基于WPF的MES上位机产线执行系统架构设计与实践 说到 MES 和上位机很多人的第一反应是去找一套现成的软件拿来用。我也走过这条路——翻遍各种开源仓库把不同的 MES 项目拉到本地跑起来试图直接部署到车间最后发现每条产线都有自己的脾气通用系统落地时反而是那层“上位机”最需要自己动手。这台运行在工控机上的 WPF 界面承载的才是产线执行系统的真实灵魂。这篇文章不是教你照着抄一套完整源码而是把一套基于 WPF 的 MES 上位机产线执行系统从选型、模块拆分、架构设计到现场部署的完整思考过程拆开讲。适合正在评估 MES 方案、准备用 C# 做上位机开发、或者已经维护着一套老 WinForm 系统想要重写升级的工程师。我会尽量说清楚每一步为什么这么做以及在现场吃过亏以后才懂的细节。1. 车间现场的旧系统之痛为什么坚持用 WPF 重写先说说我为什么要从 WinForm 迁移到 WPF。大多数工厂车间的工控机上跑的还是十年前写的 WinForm 程序界面逻辑和业务逻辑全揉在窗体代码里。一个新需求进来比如加一个工单打印按钮或者调整一下数据表格的列顺序就得把整个窗体的代码翻一遍改完还要担心会不会动到别的地方。维护成本高是一方面界面体验也确实跟不上现在的使用习惯——操作工年龄结构在变化新一代工人对触屏、响应速度、界面美观度是有要求的一套看起来像古董软件的系统日常使用阻力非常大。WPF 之所以值得坚持不是因为“新技术更酷”而是它的几个特性刚好命中产线软件的痛点。第一是数据绑定和 MVVM 模式。界面和业务逻辑分离之后View 层只负责显示和交互ViewModel 负责状态与命令。产线软件的需求变化极其频繁今天加一个字段明天改一个判定规则分离之后大部分改动只需要动 ViewModel 和后台逻辑窗体代码基本不动。这一点在长期迭代里省下的时间远比初期学习成本要高。第二是数据模板和样式系统。同样是显示一个产品状态WinForm 里可能就是一个文本框变色WPF 里可以通过样式和模板做得非常统一且灵活比如合格/不合格/待检三种状态用三种颜色块、加图标、加动画提示。换主题、做看板大屏适配改一套 ResourceDictionary 就能完成不用每个窗体单独调。第三是异步与 UI 响应。WPF 的 Dispatcher 机制配合 async/await处理串口、TCP、PLC 通讯这类耗时操作时界面不会卡死。产线上最忌讳的就是点一下按钮界面白屏三秒操作工第一反应就是“系统又坏了”。有人会问为什么不用 WinForm 继续修或者直接用 .NET MAUI我做过一个选型对比到目前为止在工业上位机这个场景WPF 依然是 Windows 工控环境下的最优解维度WinFormWPF.NET MAUI界面复杂度简单控件堆砌复杂界面吃力模板样式复杂界面可控跨平台优先Windows 表现一般数据绑定弱绑定代码繁琐强绑定数据驱动 UI绑定机制不错但生态偏新异步 UI 处理容易死锁和卡顿Dispatcher 机制成熟可用但工控类库支持少离线部署简单单文件/依赖打包均可运行时较大离线安装麻烦工业设备库兼容广泛与 WinForm 相当相对薄弱适合场景小型工具、维护老项目产线执行系统、看板、复杂交互跨平台移动端或轻量工具现场工控机有一个现实约束普遍是 Windows 10/11 离线环境不允许随便联网装依赖。MAUI 那套 runtime 和依赖管理在离线环境下部署起来比 WPF 麻烦得多。WinForm 呢简单项目够用但只要涉及多产线协同、多状态实时刷新、复杂看板WinForm 的界面架构就会让你改到怀疑人生。所以结论很明确在 Windows 工控机上做产线执行系统WPF 是当前综合成本最低、上限最高的选择。2. MES 上位机的功能地图产线执行系统真正要管的六件事很多人对“MES 上位机”的理解是“连接 PLC 读数据、显示在界面上”。这种理解太窄了。真正落到产线上的执行系统至少要拆成六个功能域每个功能域背后都有一堆细节缺一个都会在量产时出乱子。2.1 工单派发与物料绑定ERP 下达的生产工单到了产线层要在上位机里排产和派发。操作工登录系统后先看到自己工位今天要做的工单点击开工系统弹出物料绑定界面扫描物料批次号、确认物料代码与工单 BOM 是否一致。这一步看起来简单但它承担着防错的核心职责——物料绑定错误在汽车零部件、电子行业是重大质量事故上位机必须用代码拦住不能靠人眼核对。工单维度还要支持批次拆分。一个大工单拆成多个生产批次每个批次独立报工、独立判定、独立追溯。这个设计直接决定了后面质量报表和追溯链路的颗粒度。2.2 工艺参数下发与配方管理现代产线的设备普遍支持参数远程下发比如拧紧枪的扭矩和角度、压装机的压力曲线、老化测试的温度曲线都有一套配方参数。上位机的价值在于把这些配方按照产品型号、工单要求统一管理开机前自动校验配方版本版本不对禁止启动设备。配方管理最需要注意的是版本追溯。产线上经常出现“昨天这批产品用了哪个版本的参数”这种追责问题如果系统只存当前值不留历史版本到时候根本说不清楚。2.3 设备通讯与数据采集这一块是上位机与下位机通信的主战场。常见协议有 Modbus TCP、Modbus RTU、OPC UA以及各家 PLC 的私有协议。采集的数据点包括设备状态、当前产量、报警信息、关键工艺参数的实际值。采集频率要看场景设备状态和产量 500ms 到 1s 足够工艺曲线数据可能要 100ms 以下采集频率直接决定通讯架构选型——后者要考虑本地缓存和压缩存储不能全部实时入库。2.4 质量检验与返工返修检验环节包括进料检、过程检和终检。上位机要提供判定界面检验员录入检测数据系统自动对比公差范围给出结论。关键的是不合格品的处理流程——是直接报废、让步接收还是返工返修必须由系统引导操作工走流程而不是口头传递。汽车水冷板这类产品的 MES 返工返修模块我专门研究过做成什么样直接决定车间混乱程度返工返修不是打个标记就算完事要记录返工原因、返工工艺、返工次数、操作人员、每次返工后的验证结果超过最大允许返工次数还要强制触发报废评审流程。这个模块做得松现场就会用纸笔记录最后追溯时一片空白。2.5 全程追溯与报表每个产品序列号从上线到下线经过了哪些工位、用了哪些物料批次、当时的设备参数和检验数据全部串成一条完整履历。这块上有两种追溯方向都要做正查——按产品序列号查它的生产过程反查——从某个物料批次反查用到哪些产品上。汽车行业客户审厂必查这个做不出来体系建设都过不了。报表方面生产日报、一次合格率、直通率、设备 OEE、不良 Pareto 图都是产线管理的高频需求。报表数据最好独立查询库别在主业务库上跑复杂统计否则生产高峰期系统会卡。2.6 产线看板与异常响应看板分两种一种是车间墙上的大屏看板展示实时产量、设备状态、合格率另一种是工位终端上的异常响应看板。异常响应在精益生产里叫 Andon——操作工发现异常在触摸屏上一键呼叫系统通知班组长、工艺、设备工程师到现场处理处理结果录入系统闭环。这个小功能对现场管理帮助极大但很多 MES 产品忽略了它。做一个好的 Andon 模块比做十个报表更能让车间主任认可系统价值。3. 源码架构的核心设计MVVM 分层与实时通讯怎么落地功能想清楚了接下来是代码层面怎么组织。我见过很多 MES 上位机源码通病是三层架构都懒得做Program.cs 里写串口逻辑窗体的 button_Click 里连数据库。这种代码在产线软件这种业务逻辑复杂的场景里三个月后就没人敢动了。3.1 一种实践过的项目结构我习惯的解决方案结构大致是这样Solution.sln ├─ src/ │ ├─ App.sln 启动项目 // 入口、依赖注入、全局异常处理 │ ├─ Mike.Views // WPF 界面层纯 XAML 和 View 代码 │ ├─ Mike.ViewModels // 各界面 ViewModel、命令、状态机 │ ├─ Mike.Services // 业务服务工单、配方、质量、追溯 │ └─ Mike.Infrastructure // 基础设施数据库、日志、第三方通讯 │ ├─ Devices/ // 设备驱动PLC、串口、OPC UA │ ├─ Data/ // EF Core / SqlSugar 仓储实现 │ └─ Messaging/ // 内部消息总线、事件聚合这套结构的核心逻辑是依赖方向自外向内Views 引用 ViewModelsViewModels 引用 ServicesServices 引用 Infrastructure 的抽象接口Infrastructure 不反向依赖上层。更换具体设备驱动或数据库实现时上层代码不需要改动。说白了就是把变化隔离在底层工厂里最怕的就是“改一处牵全身”。依赖注入我用 Microsoft.Extensions.DependencyInjection不用手动 new 对象。窗口和 ViewModel 的创建交给容器管理界面导航也走容器。这样写测试和替换组件都方便。3.2 通讯层封装设备驱动不要裸写上位机和下位机通信是产线系统的生命线通讯代码最忌讳直接裸写在 ViewModel 里。我一般抽象出一个设备驱动接口public interface IDeviceDriver : IDisposable { string DeviceName { get; } DeviceStatus Status { get; } Taskbool ConnectAsync(CancellationToken token); Taskbool DisconnectAsync(); TaskDeviceReadResult ReadAsync(string address, int length, CancellationToken token); TaskDeviceWriteResult WriteAsync(string address, object value, CancellationToken token); event EventHandlerDeviceStatusChangedEventArgs StatusChanged; }Modbus TCP、Modbus RTU、OPC UA 各实现一套然后由 DeviceManager 统一管理连接生命周期。这样可以做到设备驱动热切换——今天这台设备用串口明天改造后换成 Ethernet配置改一下就行代码不用动。工控机上经常出现设备改地址、换规约这种事驱动抽象做得好能省太多事。这里特别说一下串口参数设置界面。很多人觉得串口设置就是几个 ComboBox 摆在那其实这里值得单独做一个封装把 SerialPortConfig 做成一个模型类包含端口号、波特率、数据位、停止位、校验位提供校验逻辑和“测试连接”按钮。产线维护人员不是程序员给他们一个参数能直观修改、能自检的界面可以减少大量现场支持电话。测试连接的实现也很直接打开串口发送一个心跳报文收到应答就提示成功否则提示失败原因。3.3 实时数据与 UI 的流动别让界面卡死设备数据到了上位机之后怎么流动到 UI 是关键。最反直觉的一句话是不要在界面线程里直接等待设备数据也不要在后台线程里直接操作 UI 控件。我采用的方案是数据经过一个实时刷新服务RealtimeDataService内部用 IObservable 做订阅分发。设备驱动把原始数据推到服务里服务按工位和设备分组ViewModel 订阅自己关心的数据流收到数据后再借助 SynchronizationContext 切换到 UI 线程更新绑定属性。这个机制下UI 线程永远只处理数据到达后的显示刷新不会因为等待设备而阻塞。高频数据还有一个信号频闪问题PLC 的产量信号 200ms 变化一次UI 如果每 200ms 刷新一次界面会闪得人眼发花。我在刷新服务里加了缓冲聚合——用 ConcurrentQueue 积攒数据每 500ms 统一推送一次既保证实时性又不抖屏。这个细节在现场体验上差异极大。ObservableCollection 需要注意一个铁律它只能由创建它的线程修改后台线程直接 Add 会抛异常。我通常在 ViewModel 里用 Application.Current.Dispatcher.Invoke 包裹集合更新或者干脆给 ViewModel 传一个 UI 调度器委托保持可测试性。3.4 第三方组件选型清单WPF 原生态控件能做东西但产线软件的界面需求比较多选好第三方库能省不少时间。我当前项目的选型参考组件用途选型理由HandyControl基础控件、窗口样式、主题工业风格界面搭建快内置消息提示和侧边栏FontAwesome.Sharp图标设备状态、菜单图标不用切图效率高LiveCharts2 或 ScottPlot数据曲线、趋势图轻量、性能尚可适合高频刷新SqlSugar / EF Core数据库 ORM按团队熟悉度SqlSugar 的上手门槛更低Newtonsoft.Json配置和报文序列化老牌稳定设备报文调试直接看 JSON 很方便组件的选择核心指标是社区活跃度、离线部署体积、与 WPF 的兼容性。只看文档漂亮而社区早就停更的库千万别用于产线环境。4. WPF 落地最容易踩坑的三个细节跨线程、DataGrid 与断线重连框架层面聊完了讲几个我实际开发时踩过的坑。这些坑不踩一遍光看文档是真发现不了。4.1 跨线程更新 UIInvoke 用多了一样卡跨线程更新 UI 最常见的做法就是在后台线程里调用 Dispatcher.Invoke 委托。比如下面这种Dispatcher.Invoke(() { TxtStatus.Text 设备已连接; });代码没问题但如果在高频数据回调里大量使用 Invoke而且是同步等待 UI 线程处理完才返回那么后台线程会被 UI 线程拖慢反过来 UI 又被排队的大量 Invoke 卡住形成相互等待现场表现就是界面越来越卡最后直接假死。我的解决思路是高频场景用 Dispatcher.BeginInvoke不等待回调执行完成。能用绑定的地方用绑定不要在代码里到处找控件赋值。绑定机制本身就做了线程切换和性能优化。数据采集线程和 UI 线程彻底隔离中间通过上节说的实时数据服务和缓冲队列解耦不要让设备线程知道 UI 控件的存在。async/await 里注意 ConfigureAwait(false) 的使用。工业代码里我默认加 false避免自动捕获 UI 同步上下文导致的不确定行为。但加了 false 之后访问 UI 控件前必须手动切换到 Dispatcher这个容易漏要统一封装一个方法。那年我在调试老化测试工位界面假死问题的时候最后排查出来就是设备驱动事件里同步调用了 Dispatcher.Invoke而且每 200ms 一次。改成 BeginInvoke 加缓冲推送之后问题当场消失。这个问题在局域网环境未必暴露连着无线网络或者现场网线质量差一点的时候网络抖动会把问题放大成“系统崩溃”排查难度相当高。4.2 DataGrid 大数据量渲染一行变两行、虚拟化与列冻结产线的生产记录表动辄几千上万行DataGrid 是 WPF 里最容易卡出问题的控件。第一次踩坑是渲染两千行数据花了三秒以上操作工看着进度转圈非常不耐烦。检查后发现问题是默认没开虚拟化——DataGrid 默认是启用的但若有行详细信息模板RowDetailsTemplate并且模板内含有较多内容时虚拟化会大打折扣。这里就要说“一行变两行显示”这个常见需求。很多产线界面希望主行显示产品序列号、状态、生产时间下一行再显示这个产品的工艺参数详情、检验数据。实现方式有两种DataGrid.RowDetailsTemplate点击行或选中行时展开第二行详情适合少量查看。自定义列模板把两个层面的信息拼接到一格里适合常显。我当时刚开始用了 RowDetailsTemplate做的时候发现虚拟化变差了数据滚动开始掉帧。后来换了一种方式不在行内展开而是把详情放到下方的一个独立 Panel选中某行时下方绑定该行数据。性能和体验反而更好。另外两点经验开启虚拟化后ColWidth 尽量别用 Auto用固定列宽或 Star 按比例分配Auto 模式下虚拟化机制会对每一列做反复测量性能急剧下降。列多的时候用 FrozenColumnCount 冻结前几列产品序列号、状态之类横向滚动时关键信息不掉出视野。操作工在触屏机上横向拖列表时这一条体验差异非常大。4.3 断线重连与数据补传最容易被新手忽略的一环产线环境比办公室恶劣得多机柜里电磁干扰、网线松动、PLC 偶尔重启、串口被误拔。断线这件事不是“万一”而是必然。一套产线执行系统如果断线后只能重启软件恢复那它离被抛弃不远了。我做的设备状态机分三个阶段在线、重连中、离线。通讯失败后驱动层自动进入重连中阶段按 1 秒、3 秒、5 秒、10 秒的退避间隔重试不是直接报错弹窗。重连期间产生的业务操作写入本地队列等连接恢复后自动补传。关键业务数据更需要本地冗余。我的做法是上位机程序启动时往本地 SQLite 写业务日志工单开工、完工、参数下发、质量判定这些关键事件先落盘再同步到中心数据库。SQLite 是文件型数据库单机冗余完全够用不怕断电也不需要单独部署数据库服务。那次调试某条产线的老旧 PLC 每到热天就会过热重启十分钟一断靠这套补传机制硬是没丢一条数据操作工都以为没有断过线。注意数据采集点频率很高的时候不建议把全部原始数据写进 SQLite 再同步。高频数据要按前面说的缓冲聚合方式本地只暂存最近一段时间的快照归档交给中心数据库的时序策略处理。否则 SQLite 文件会快速膨胀同步会变成灾难。5. 从单机版到产线级部署现场实施中容易被低估的问题一套上位机源码跑通了跟它能在产线稳定运行中间还隔着一堆“软件之外”的事。这些问题在花两个月写代码的时候几乎不会想到但到了上线那一天全部变成现实。5.1 数据库设计与历史数据策略产线执行系统的数据量跟普通管理软件完全不同。普通软件的订单表一天可能几百条而产线系统如果按 100ms 采集一个工艺参数点一台设备一小时就是 3.6 万条数据十条产线几十个采集点几天下来就是一个天文数字。数据库设计在第一周就要想清楚。我采用混合存储方案业务型数据工单、物料绑定、检验记录、返工记录存中心数据库 SQL Server定期备份。高频采集数据按天分表或分区保留 30 天可查明细超过 30 天只保留均值、最大值、最小值这类统计值原始数据归档到文件库查历史曲线时再按需恢复。历史数据这个事我踩过教训。第一次上线我没有做归档策略三个月后报表查询越来越慢最后花了两个周末做数据迁移。上线前就把归档策略定好后面能省很多事。5.2 权限模型与操作审计MES 上位机不是谁都能“随便点”的。工艺参数下发、返工放行、配方修改这些动作一旦出错造成的损失就不是一张报表的问题。权限模型要细到按钮级别操作工只能看和报工班组长可以发起返工工艺工程师才能修改工艺参数。系统里还要有完整的操作审计功能记录谁在什么时间、哪台终端改了哪个参数改前值是多少改后值是多少。很多车间之所以推行系统阻力大就是因为权限混乱——谁都能改出事了找不到责任人操作工自然抵触。权限和审计做扎实了反而是一种保护大家知道每一步都留痕会自觉规范操作。5.3 老设备与大屏适配现场最常见的终端组合是两种1024x768 的工位触屏一体机和 1920x1080 的车间看板大屏。WPF 的好处是布局用 Grid 按比例分配后不同分辨率下基本都能自适应。但有几个细节要注意触屏机上按钮最小尺寸做到 48 像素以上否则现场工人手指经常点不中。高 DPI 环境下字体渲染要设置 PerMonitorV2否则 Win10/11 不同缩放级别下界面会发虚。看板大屏的字体最小不要低于 24px车间里距离远、光线杂字小了没人看得清。老设备如果只跑 Windows 7.NET 版本选择要谨慎。现在还有不少工控机没升级系统选 .NET Framework 4.7.2 兼容性最佳如果确定全线上 Win10/11 以上可以用 .NET 6/8 配合单文件发布部署体验更好。5.4 版本升级与现场验证产线系统最难的不是开发是升级。产线不能停升级窗口只能在休息时间一般半小时到一个小时。第一次给客户升级时我用的还是“停软件、拷文件、再启动”的原始方式后来发现现场环境复杂覆盖错了 DLL 导致版本错乱回滚困难。后来我做了带版本的目录隔离方案C:\MES\versions\v1.2.3\ C:\MES\versions\v1.2.4\ C:\MES\app\ // 指向当前版本目录的启动器每次发布新版本就在 versions 下新建目录启动器负责加载最新版并保留上一个版本。一旦现场发现问题配置切换回退到上一版本即可不用重新部署安装包。这个方案成本极低但对产线运维的救急效果立竿见影。升级前还有一个容易被忽视的动作在小范围试点。先让一条线用新版本确认运行半小时无异常后再推到全线。千万别一口气全线上新版本现场有一些组合场景是办公室里完全模拟不出来的。最后分享一段真实经历。第一次整套系统上线后的第一周我的手机几乎每个夜班都会响总是同一个问题某台设备采集不到数据了。远程过去排查发现不是代码问题而是那台 PLC 前几天换过机柜IP 变了配置表里没有更新。后来我专门做了一页“设备配置核对”界面每次设备维护后由现场工程师确认配置状态再恢复生产这类问题才真正断根。这件事给我的触动很深MES 上位机的源码只是整个系统的起点配置管理和现场运维才是决定软件口碑的关键。写代码的时间往往只占项目周期的三分之一剩下三分之二都在处理现场那些让你意想不到却又真实存在的细节。如果你也在规划 WPF 产线执行系统我的建议是——在架构阶段就为运维预留好接口在模块设计阶段就跟现场操作工多聊几次把他们的操作习惯当成需求的一部分。做上几套系统之后你会体会到真正好用的产线执行系统都是在一个个夜里被现场电话教育出来的。
RELATED READING

延伸阅读

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