一、前言
在实际标准设备项目中,MES 对接通常不是单一客户,单一接口,单一协议的形式。真正有挑战的部分,往往来自以下几个方面:
1. 不同客户的协议完全不同,既有 TCP/IP,也有 HTTP,还有本地文件落地方案。
2. 不同客户支持的能力并不一致,有的支持登录校验,有的只支持结果上传。
3. MES 配置、业务流程、设备动作如果写在一起,会导致系统耦合度很高。
4. 新客户接入时,如果没有统一架构,往往需要从头复制和改造,开发效率很低。
5. 新客户接入项目一旦增多,后期维护成本会迅速上升。
当前这套解决方案的目标非常明确:将 MES 能力从业务项目中独立出来,沉淀成一套标准化、模块化、可扩展、可快速嵌入的架构,让其既能服务当前项目,也能在未来新的客户接入中快速复用。
整个解决方案由两个核心项目组成:
1. MesUse:宿主项目,负责承载和演示。
2. StandardMes:MES 标准模块,负责协议、配置、页面、调用封装与扩展机制。
Github地址:https://github.com/2825077535/MesUse.git
二、整体架构定位
从职责划分上看,这套demo做了比较清晰的分层。
1. 宿主层:MesUse
宿主项目主要负责:应用启动,主窗口承载,示例按钮调用,接收并处理 MES 发出的设备控制请求
宿主层不直接负责 MES 协议细节,而是负责与具体业务或设备控制代码打通。
2. 标准能力层:StandardMes
标准模块主要负责: 定义 MES 统一接口,封装多种 MES 客户端实现,维护配置模型,提供标准配置界面,提供统一请求与响应对象,提供安全调用封装,提供与宿主之间的事件通信机制
也就是说,StandardMes 本质上是一个“可嵌入的 MES 功能包”,宿主项目只需要完成最少的集成工作,就能把整套能力接入进去。
三、Prism 在架构中的作用
在常见的WPF项目中,prism架构是最常用的,由于我现在也是在使用prism架构,所有本次项目默认按prism项目来适配的。如果不是prism架构,可以将MesApp作为单例入口即可。
1. 模块化加载
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<MesPluginModule>();
}
MesPluginModule 已模块的形式装载在项目中。这种方式的优势非常明显:宿主系统不需要直接管理大量 MES 细节,只需要装载模块即可。
2. 依赖注入
通过 Prism 的容器,MesApp、各类 View、ViewModel、Mes 客户端实现类都能够被自动解析。在 StandardMes 中,MesPluginModule 完成了核心类型注册:
public class MesPluginModule : IModule
public void RegisterTypes(IContainerRegistry containerRegistry)
3. 区域导航
<ContentControl prism:RegionManager.RegionName="MesViewRegion"/>整体配置页面的区域.
<ContentControl Grid.Row="0" prism:RegionManager.RegionName="{x:Static mesconst:MesConst.MesOptionsRegion}"/>作为客户UI的承载入口将配置页面与客户UI分离.
<ContentControl prism:RegionManager.RegionName="MesCustomerContentRegion"/>作为客户专属配置页面的区域
所以区域的挂载链路是:MesViewRegion作为整体配置页面的区域->MesOptionsRegion作为客户UI的承载入口将配置页面与客户UI分离->MesCustomerContentRegion作为客户专属配置页面的区域。
四、MES 架构的核心设计思想
通过 MesApp 统一入口,通过接口分层抽象能力,通过不同客户端实现隔离协议差异,通过 Prism 负责模块装配和页面组合。
1. MesApp:统一入口与工厂中心
MesApp 是整个 StandardMes 的核心门面对象,它负责:读取和保存配置, 持有当前激活的 MesClient,根据客户类型创建具体实现, 为上层提供统一调用入口
它内部维护了一个工厂字典:
private readonly Dictionary<MesEnum.MesCustomerEnum, Func<IMesClient>> _mesFactories = new();
RegisterMesFactory(MesEnum.MesCustomerEnum.LocalMes, () => _container.Resolve<LocalMes>());
当宿主调用 CreateMes() 时,会根据当前配置选择具体实现:
public async Task<bool> CreateMes()
{
var customer = MesEnum.GetEnumValueFromDescription<MesEnum.MesCustomerEnum>(StandardMesConfig.SelectCustomer);
if (_mesFactories.TryGetValue(customer, out var factory))
{
MesClient = factory();
}
else
{
MesClient = new DefaultMes();
}
return true;
}
将客户差异收敛在实现层,把创建逻辑集中在 MesApp 中管理。宿主只负责调用 MesApp,而不关心底层到底是 TCP、HTTP 还是本地文件。
2. 接口按能力拆分
在以前项目中,MES 接口会被定义成一个非常庞大的 IMesService,最终导致某些客户实现大量空方法或不支持方法。
当前架构把能力进行了拆分:IMesClient:基础客户端标识,IMesLoginService:登录能力,IMesResultService:结果上报与关闭能力,IMesBoardService:过板、取板、出板能力,IMesProgramService:程序相关能力,IMesDynamicService:动态扩展能力,IMesMachineStateService:设备状态能力,IMesAlarmService:报警能力
例如:
public interface IMesResultService
{
Task<MesResponse> ResultAsync(MesResultRequest request);
Task<MesResponse> CloseMesAsync(MesCloseRequest request);
}
这样设计的好处是:客户实现只需要实现自己真正支持的能力。不同协议的扩展边界清晰。上层代码更容易理解系统能力划分。新功能扩展时不容易牵动所有实现类。
这种做法非常适合实际中客户差异化明显的项目。
3. 请求与响应标准化
在 Contracts 目录中,架构为所有 MES 调用定义了统一的请求和响应模型。
请求模型包括:
- MesLoginRequest
- MesResultRequest
- MesBoardRequest
- MesRemovePcbRequest
- MesMachineStateRequest
- MesAlarmRequest
- MesProgramRequest
- MesDynamicRequest
响应模型包括:
- MesResponse
- MesResponse<TPayload>
- MesLoginResponse
标准响应对象定义如下:
public class MesResponse
{
public bool Success { get; set; }
public bool Enabled { get; set; } = true;
public string Code { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public bool IsTimeout { get; set; }
public string RawResponse { get; set; } = string.Empty;
}
统一模型最大的意义在于:
- 宿主代码不需要区分不同协议的返回格式。
- 上层业务可以统一处理成功、失败、超时、禁用和原始报文。
- 日志、重试、异常分析都具备统一入口。
五、安全调用封装:让业务层不关心底层细节
在这套架构中,宿主层不会直接把 MesClient 强转成某个接口再调用,而是通过扩展方法进行“安全调用”。
例如在 MainWindowViewModel 中,发送结果的代码是:
await _mesApp.ResultSafeAsync(request);
登录则是:
await _mesApp.MesLoginSafeAsync(new MesLoginRequest
{
LoginName = "TestName",
LoginPassword = "TestPassWord"
});
这些扩展方法统一定义在 MesSafeCallExtensions 中。它们负责做几件事:
1. 判断当前 MesClient 是否实现了目标接口。
2. 如果没有实现,返回统一的不可用响应。
3. 如果实现了,则发起真实调用。
4. 在调用过程中统一捕获异常、超时、取消和网络错误。
5. 把异常信息转换成标准 MesResponse。
核心逻辑如下:
private static async Task<TResult> SafeCallAsync<TService, TResult>(
TService service,
Func<TService, Task<TResult>> call,
Func<TResult> fallback)
where TService : class
where TResult : MesResponse
{
if (service == null)
{
return fallback();
}
try
{
return await call(service).ConfigureAwait(false);
}
catch (TimeoutException ex)
{
TResult response = fallback();
FillExceptionResponse(response, ServiceTimeoutCode, "MES调用超时。", ex, true);
return response;
}
}
六、多客户实现如何落地
当前架构内置了三种典型的 MES 客户端实现:
1. LocalMes
2. TCPIPMes
3. HttpPostMes
这三者分别代表了本地文件型、长连接型和 Web 接口型 MES 对接方式。
1. LocalMes:本地文件输出模式
LocalMes 只实现 IMesResultService,将结果数据写入本地文件夹。
2. TCPIPMes:Socket 长连接模式
TCPIPMes 负责通过 TCP/IP 与外部 MES 通信。
使用 ConcurrentDictionary<Guid, TaskCompletionSource<TCPIPMesAcceptData>> 来维护等待响应的请求任务。由于TCPIP是原生的通讯协议,在异步通讯时经常会出现请求和响应不匹配的情况,所以通过一个全局的字典来维护每个请求对应的 TaskCompletionSource,当收到响应时,根据请求ID找到对应的 TaskCompletionSource 并设置结果,这样就能保证请求和响应的正确匹配。
3. HttpPostMes:HTTP 接口模式
HttpPostMes 则适用于标准 Web MES 接口场景。它的流程相对直观:
1. 构造 JSON 报文。
2. 使用 HTTP Post 发送请求。
3. 解析服务器返回数据。
4. 转换成标准 MesResponse。
例如登录逻辑:
public async Task<MesLoginResponse> MesLoginAsync(MesLoginRequest request)
{
JObject pairs = new JObject
{
[nameof(Config.EquipmentID)] = Config.EquipmentID,
[nameof(Config.LineID)] = Config.LineID,
[nameof(request.LoginName)] = request.LoginName,
[nameof(request.LoginPassword)] = request.LoginPassword
};
MesResponse response = await SendAsync("MesLogin", pairs.ToString()).ConfigureAwait(false);
return new MesLoginResponse
{
Success = response.Success,
Enabled = response.Enabled,
Code = response.Code,
Message = response.Message,
RawResponse = response.RawResponse,
IsTimeout = response.IsTimeout,
CanEnter = response.Success,
ErrorMessage = response.Message
};
}
上层无论面对 TCP 还是 HTTP,实现的调用方式始终是统一的,而协议差异完全被隔离在内部实现中。
七、客户UI配置界面的模块化设计
这套架构不仅把通信层做了标准化,连配置界面也做了模块化。
1. MES 配置总入口
在 MesUse 的 ViewMes.xaml 中,定义了一个区域:
<ContentControl Grid.Row="0" prism:RegionManager.RegionName="MesOptionsRegion"/>
在 MesPluginModule 初始化时,将 MesAppView 注册到这个区域:
regionManager.RegisterViewWithRegion(MesConst.MesOptionsRegion, typeof(MesAppView));
也就是说,宿主只负责为 MES 配置区域留出位置,具体加载什么页面由模块自己决定。
2. 客户专属配置区域
MesAppView.xaml 中继续定义了一个内层区域:
<ContentControl prism:RegionManager.RegionName="MesCustomerContentRegion"/>
于是,整个配置界面形成了两层嵌套结构:
- 第一层:MES 总入口区域
- 第二层:客户专属配置区域
这种嵌套式 Region 设计非常适合复杂配置系统,因为不同客户通常拥有完全不同的配置项,但又需要统一挂在MesCustomerContentRegion入口下。项目只需要嵌入MesOptionsRegion的调度即可
3. 客户页面动态切换
在 MesStatic 中,维护了客户枚举到页面名称的映射:
internal static readonly Dictionary<MesEnum.MesCustomerEnum, string> _customerViewsMap =
new Dictionary<MesEnum.MesCustomerEnum, string>()
{
{ MesEnum.MesCustomerEnum.LocalMes , nameof(Views.LocalMesView) },
{ MesEnum.MesCustomerEnum.TCPIPMes , nameof(Views.OutPutMesView) },
{ MesEnum.MesCustomerEnum.HttpPostMes , nameof(Views.OutPutMesView) }
};
当用户切换客户后,系统会根据映射进行导航:
regionManager.RequestNavigate(MesConst.MesCustomerContentRegion, targetView, navigationParameters);
八、配置与 ViewModel 解耦
在配置页面这部分,架构并没有直接让页面对配置实体进行硬编码读写,而是引入了 ViewModel 层并配合 AutoMapper 做对象映射。主要是用于确认配置与取消配置的操作。
在 ConfigProfile 中定义了映射关系:
CreateMap<LocalMesViewModel, CustomerConfig.LocalMesJson>().ReverseMap();
CreateMap<OutPutMesViewModel, CustomerConfig.OutPutMesJson>().ReverseMap();
对于新客户页面,也可以直接复用这种模式:
1. 新增客户配置实体。
2. 新增客户配置页面与 ViewModel。
3. 增加 AutoMapper 映射。
4. 增加页面导航映射。
九、宿主与标准模块之间如何解耦协作
除了项目调用Mes模块之外,这套架构还允许Mes调用项目内容:MES 模块可以向宿主发起设备动作请求,而不是只做单向接口调用。在实际项目中,例如Mes状态需要控制信号灯的亮灭,Mes报警需要控制报警器的响起,客户一键转产需求等,这些都直接通过Mes交互来控制设备。
这一点主要依赖 Prism 的 EventAggregator 实现。
1. 标准模块发布事件
在 MesModel 中,封装了设备动作请求方法,例如切换程序、打印日志、设备启动、设备停止、设备报警等。其底层统一通过 MesMachineEvent 发布消息:
EventAggregator.GetEvent<MesMachineEvent>().Publish(
new MesControllerMachine(Name, Value, callback: (returnValue) =>
{
tcs.TrySetResult(returnValue);
}, TimeSpan.FromMilliseconds(MesStatic.SetMesMachineControllerTimeOut), timeoutCts.Token));
它不仅发布动作名称和参数,还带有回调、超时和取消令牌,这是一次跨模块的异步命令调用。
2. 宿主侧订阅事件
在 MainWindowViewModel 中,宿主通过 EventAggregator 订阅该事件:
_mesMachineEventToken = _eventAggregator.GetEvent<MesMachineEvent>()
.Subscribe(MesControllerMachine, ThreadOption.BackgroundThread);
宿主在回调中根据命令名称执行不同逻辑,例如:
switch (methodName)
{
case MesConst.MachineLog:
Console.WriteLine($"执行MachineLog:{valueStr}");
isSuccess = true;
break;
}
e.Callback?.Invoke(new MesControllerMachineReturn(isSuccess, errorMsg));
这代表着标准模块并不依赖任何具体设备控制代码,真正的设备动作始终由宿主项目决定。
十、整体调用流程说明
为了更直观地说明架构,下面按照实际代码路径梳理整个调用流程。
流程一:应用启动与模块装载
1. App 启动。
2. Prism 创建 MainWindow 作为 Shell。
3. 容器注册 MesApp 和导航页面。
4. 模块目录加载 MesPluginModule。
5. MesPluginModule 注册 MES 页面、ViewModel、客户端实现和映射组件。
6. MainWindow 加载后默认进入 View1。
这一流程完成的是:宿主应用初始化与标准模块装载。
流程二:进入 MES 配置界面
1. 用户点击 View1 中的“进入Mes配置”。
2. 系统导航到 ViewMes。
3. ViewMes 中的 MesOptionsRegion 加载 MesAppView。
4. MesAppView 再通过内部区域承载客户配置页面。
5. 根据当前客户配置,动态切换到 LocalMesView 或 OutPutMesView。
这一流程完成的是:MES UI 的嵌入与组合式展示。
流程三:创建当前客户的 MES 实例
当宿主点击“打开Mes”按钮时,会执行:
await _mesApp.CreateMes();
其内部会:
1. 读取当前配置中的客户类型。
2. 解析为 MesCustomerEnum。
3. 如果当前已有旧实例,则尝试关闭旧实例。
4. 从工厂字典中获取新客户对应的构造方法。
5. 创建新的 MesClient 并赋值给 MesApp。
这一流程完成的是:从配置驱动到具体实现实例化。
流程四:业务调用 MES 接口
以发送结果为例:
await _mesApp.ResultSafeAsync(request);
其内部调用链为:
1. 宿主 ViewModel 构造请求对象。
2. 通过 MesSafeCallExtensions 进入统一安全调用层。
3. 判断当前 MesClient 是否实现 IMesResultService。
4. 调用具体客户实现,例如 HttpPostMes.ResultAsync 或 TCPIPMes.ResultAsync。
5. 由 MesModel 统一构造结果报文。
6. 发送给真实 MES。
7. 返回标准 MesResponse。
这一流程完成的是:统一入口调用、协议适配与响应标准化。
流程五:MES 反向控制设备
当 StandardMes 需要通知宿主执行某些设备动作时,例如:
- 打印日志
- 报警
- 设备运行
- 设备停止
- 切换程序
则会通过 MesMachineEvent 发布命令,并等待宿主返回结果。宿主处理完成后,再通过回调把执行结果传回 StandardMes。
这一流程完成的是:MES 模块与设备控制系统之间的异步解耦协作。
十一、为什么这套架构适合快速嵌入项目
这篇文章的最重要的目标,是为什么这套架构适合作为一个标准能力快速嵌入到新的项目中。
主要原因有以下几点。
1. 宿主接入成本低
宿主只需要做少量工作:
- 引入 StandardMes 项目或程序集
- 在 App 中注册 MesApp
- 加载 MesPluginModule
- 提供一个区域承载配置页
- 订阅需要的设备事件
其余的配置页面、请求模型、调用封装、协议实现都已经在标准模块中准备好了。
2. 配置系统可直接复用
新项目不需要重新开发一套 MES 配置页面,因为当前架构已经把:
- 总入口页
- 客户选择页
- 客户专属配置页
完整拆分并可直接嵌入。
3. 业务调用方式统一
业务层面对不同客户时,调用方式完全一致,例如:
- MesLoginSafeAsync
- ResultSafeAsync
- CheckBoardSafeAsync
- OutBoardSafeAsync
- MachineStateSafeAsync
- AlarmInformationSafeAsync
这极大降低了业务开发复杂度。
4. 新客户接入边界清晰
如果未来接入新客户,通常只需要按固定步骤扩展:
1. 在 MesEnum 中新增客户枚举。
2. 新增一个客户实现类,如 XxxMes。
3. 实现需要支持的接口能力。
4. 在 MesPluginModule 中注册该实现。
5. 在 MesApp 中注册对应工厂。
6. 如果需要专属配置页面,则新增 View 和 ViewModel。
7. 在 MesStatic 中增加页面映射。
整个改动边界清晰、职责明确,不需要大面积修改宿主代码。
十二、总结
当前这套基于 Prism 的 MES 架构,本质上解决了两个核心问题:
1. 如何把 MES 做成可插拔、可嵌入、可复用的标准能力。
2. 如何在面对多客户差异化需求时,仍然保持清晰结构和快速交付能力。
在我自己实际的标准设备的项目接入Mes中。所遇到的客户的协议和要求可谓说是千奇百怪。这个整套架构也是我在数十个客户的接入经验基础上总结出来的。
主要还是针对嵌入现有项目,和新客户接入最快的完成内容的编码。

256

被折叠的 条评论
为什么被折叠?



