前言:在现代智能制造中,上位机(PC 端软件)往往充当着“承上启下”的关键角色。一方面,它需要与底层 PLC/控制器进行实时数据交互;另一方面,它需要响应来自MES(制造执行系统)的生产指令、配方下发或参数配置。
一、为什么选择 EmbedIO?
为了实现跨系统的高效通信,使用HTTP/RESTful API是目前最主流、兼容性最好的方案。今天,我们就来探讨如何利用开源的EmbedIO库,在 C# 上位机中快速搭建一个轻量级 Web 服务端,以接收 MES 指令并操作设备。
在 C# 生态中,虽然有 ASP.NET Core 这样强大的框架,但对于桌面端上位机软件而言,ASP.NET Core 有时显得过于臃肿且配置繁琐。EmbedIO的优势在于:
- 极致轻量:依赖少,启动速度极快。
- 易于集成:直接集成在 Windows Forms 或 WPF 应用程序中,无需额外安装 IIS 或复杂的 Web 环境。
- 简单易用:基于中间件(Middleware)架构,逻辑清晰。
二、客户MES需求流程
上位机作为中转,获取MES推送的数据,再写命令给PLC,设备实现弹窗提醒。
下面描述下需设备侧协作的部分:
- 在08:30 ~10:00 20:30~22:00两个时间段,MES系统每10分钟给未点检设备推送一次消息设备,根据点检状态进行弹窗或者锁机
- 返回未点检,机台弹窗或者锁机(弹窗内容建议附带推送内容中的设备编码,信息来源,点检状态描述)
- 推送失败,包括任何处理逻辑的错误,机台无弹窗;尤其注意异常情况要求不能影响生产
- 操作人员可能不会及时点掉弹窗,要求同一个时刻同一个系统(来源)仅保留一个弹窗,若当前已有弹窗,则下次弹窗时,先将上次的弹窗隐藏掉
- 机台需要兼容弹窗或者锁机两种情况,需要根据是否锁机标签位(lockFlag)决定是否锁机
三、核心实现步骤
1. 安装 NuGet 包
在您的项目中,打开 NuGet 包管理器,搜索并安装以下包:
Install-Package EmbedIO2.创建 HTTP 服务端
我们需要编写一个后台服务类,用于初始化服务器并绑定路由。
using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; private void StartWebServer() { string url = DataHelper.ReadByVariableName("MaterialCallTaskResUrl"); cts = new CancellationTokenSource(); Task.Run(() => { server = new WebServer(o => o .WithUrlPrefix(url) .WithMode(HttpListenerMode.EmbedIO)) .WithWebApi("/", m => m.WithController(typeof(ApiController))); server.RunAsync(cts.Token); }, cts.Token); DataHelper.WriteUseLog("系统登录", $"系统提示:web服务已开启,监听地址:{url}"); // 通过CancellationToken发出取消信号 //cts?.Cancel(); //server?.Dispose(); // 释放资源 //server = null; //MessageBox.Show("服务器已停止。"); }相关阅读:C# 代码片段(一)。
3.处理 MES 指令(Controller)
通过继承WebApiController,我们可以定义处理逻辑,并将解析后的数据下发给设备。
public class ApiController : WebApiController { /// <summary> /// 服务接收点检接口 /// </summary> /// <returns></returns> [Route(HttpVerbs.Post, "/equip/spotcheck/push")] public async Task spotCheckPush() { var param = new spotCheckPush(); Response<object> response; string strResult = ""; bool result = true; try { // 读取请求体 using (var reader = new StreamReader(HttpContext.Request.InputStream)) { strResult = await reader.ReadToEndAsync(); param = JsonConvert.DeserializeObject<spotCheckPush>(strResult); } #endregion if (param != null || param.equipCode != GlobalValues.deviceNo) { if (param.spotCheckStatus == "0") { // 根据 lockFlag 区分参数、日志文本与任务名称 bool isLock = param.lockFlag == "0"; float writeValue = isLock ? 2f : 1f; string actionText = isLock ? "锁机" : "弹窗"; string taskText = isLock ? "锁机" : "弹窗"; // 执行写入 var res = DataHelper.WriteByVariableName("MES_DJ[1]", writeValue);//弹窗、锁机提醒 DataHelper.WriteByVariableName("GST_MES_XTLX", param.source);//写数据来源 if (!res) result = false; // 记录日志 DataHelper.WriteUseLog("数据交互", $"{DateTime.Now:yyyy-MM-dd HH:mm:ss:fff}-->上位机收到MES {actionText} 命令,执行 {taskText} 任务!"); } else { //写复机命令 var res = DataHelper.WriteByVariableName("MES_DJ[2]", 1f);//复机提醒 result = true; } #region 写入结果判断 if (result) { response = new Response<object> { code = "200", traceID = DateTime.Now.ToString("yyyyMMddHHmmssfff"), errorMsg = "", message = "操作成功", }; } else { response = new Response<object> { code = "500", traceID = DateTime.Now.ToString("yyyyMMddHHmmssfff"), errorMsg = "参数修改不成功", message = "操作成功", }; } #endregion } else { response = new Response<object>() { code = "500", traceID = DateTime.Now.ToString("yyyyMMddHHmmssfff"), errorMsg = "参数解析错误或设备编号不匹配", message = "操作成功", }; } catch (Exception ex) { #region 异常处理 response = new Response<object> { code = "500", traceID = DateTime.Now.ToString("yyyyMMddHHmmssfff"), errorMsg = "参数不全/参数错误", message = "操作成功", }; #endregion } await HttpContext.SendDataAsync(response); } }接口类:
public class spotCheckPush : IModel { /// <summary> /// 设备编号 /// </summary> public string equipCode { get; set; } /// <summary> /// 点检状态 0-未点检 弹窗 1-已点检 /// </summary> public string spotCheckStatus { get; set; } /// <summary> /// 推送方的本地时间 /// </summary> public string requestTime { get; set; } /// <summary> /// 点检状态描述 /// </summary> public string spotCheckStatusDesc { get; set; } /// <summary> /// 来源 /// </summary> public string source { get; set; } /// <summary> /// 是否锁机 0-锁机 1-弹窗 /// </summary> public string lockFlag { get; set; } }4.关键流程:从指令到设备
当 MES 系统发起请求时,整体通信流程如下:
- POST 请求:MES 系统通过
http://[上位机IP]:[端口]/api/send-params发送 JSON 数据。 - 数据解析:EmbedIO 接收请求,自动将 JSON 反序列化为 C# 对象。
- 设备交互:上位机内部调用 PLC 驱动接口(如
S7.Net、Modbus TCP等),将参数写入设备寄存器。 - 状态反馈:上位机将操作结果(成功/失败)返回给 MES,闭环生产流程。
四、进阶建议
安全性加固:在生产环境中,务必加入API Key验证或JWT Token鉴权,防止恶意非法指令干扰设备。
异常处理:上位机必须具备完善的全局异常捕获,确保即使指令解析失败,也不会导致上位机软件崩溃。
日志记录:记录每一次 MES 指令的请求原始数据(Payload)和响应时间,以便事后追溯生产异常。
总结
通过 EmbedIO,我们可以将上位机软件轻松扩展为支持 HTTP 协议的“智能终端”。这种方案不仅满足了与 MES 系统解耦的需求,还大大降低了上位机软件与外部系统集成的难度。
如果您正在为上位机开发接入 MES 的功能烦恼,EmbedIO 绝对是一个值得尝试的轻量级利器!