前言
由于近期我做了好几个客户的接入工厂Mes系统的需求。但是每个客户的Mes都有不同程度的定制需求,原有的代码复用难度其实很大。所以打算将整个接入Mes系统的框架单独拿出来作为一个项目使用,同时因为不同的设备接入同一个Mes系统,所以代码的迁移规范同样非常重要。
1.需求分析
这部分的需求分析,主要来自于我在接入不同客户Mes系统时发现的一些问题和解决方案,同时也了解过工厂Mes系统供应商的朋友们。列举了一些比较实际的功能(主要是代码方面的)。
- 需要有同一个的接入方式,方便接入不同客户的Mes系统。
- 需要有全局的参数列表。
- 只允许存在一个Mes系统的入口。
- 保证数据统一性,在多线程访问,设备不同轨道运行时,数据需要做区分。
- 尽可能的减少后续的代码修改。
- UI上,不同的客户的选项卡要做区分,但是只显示一个选项卡。
- 所有关于Mes的操作需要独立于设备内容,方便在不同设备软件上迁移,设备软件只做传入数据的功能。
- 需要考虑由Mes控制设备的功能
2.设计项目内容
- 首先会有一个MesApp的入口,用来访问整个Mes系统,但是这个入口只能有一个,所有MesApp应当使用单例模式。
- 不同的客户需要继承于同一个接口,根据客户的名称信息去访问指定的客户类,所以MesApp应当具有工厂模式,通过工厂生产客户类。
- 根据全局参数列表,需要一个可通过MesApp访问的全局静态类Const,和枚举类
- 保证数据做好区分,但是又保证数据统一性,所以Mes需要全部公用一个数据类对象,并且可以在外部写入参数,并作为类对象进行传参
- UI上面仍然使用MVVM框架去实现,同时使用Visibility的binding的形式来控制在选项卡中显示UI(根据实际情况可以选择公用选项卡或者,多选项卡的形式)
- 考虑数据的通用性,所以数据应到以类对象的形式存在(类型为String的Name,类型为Object的Value)
- 需要独立的数据存储部分,通讯方式需要独立,
- 在MesApp中需要由一个队列跟软件的框架进行通讯,用来控制软件执行某些内容
3.代码内容
3.1MesApp入口
首先MesApp是整个项目的入口,除了数据结构类以外,全部数据都应该从MesApp的端口中进入。所以MesApp有一个单例的入口。其中包含,config配置文件,const在软件运行时的不用存储的变量,IMesSend接口与Mes交互的主要代码,Enum需要使用的枚举值。同时在进入Mes前,需要根据不同的客户去选择我们需要的使用的Mes内容,所以有一个创建mes的函数。同时还有保存配置和获取配置的部分
namespace Mes
{public class MesApp{#region 单例部分private static MesApp _instance = null;private static readonly object Lock = new object();private MesConst _Const = new MesConst();private IMesSend _Mes = null;private MesEnum _MesEnum = new MesEnum();private MyMesConfig _MesConfig = new MyMesConfig();private string EnvironmentAddress = Path.Combine(Environment.CurrentDirectory, "Config");private string ConfigPath = Path.Combine(Environment.CurrentDirectory, "Config\\MesConfig.txt");//mes接收控制软件的队列public Queue<MesProcessData> MesQueueAccept = new Queue<MesProcessData>();//mes接收控制软件结束后的反馈队列public Queue<MesProcessData> MesQueueSend = new Queue<MesProcessData>();public MyMesConfig MyMesConfig{get => _MesConfig;set => _MesConfig = value;}public MesEnum MesEnum{get => _MesEnum;}public MesConst Const{get => _Const;set => _Const = value;}public IMesSend Mes{get => _Mes;set => _Mes = value;}public static MesApp Instance{get{if (_instance == null){lock (Lock){if (_instance == null){_instance = new MesApp();}}}return _instance;}}#endregion/// <summary>/// 创建Mes对象/// </summary>public bool CreatMes(){try{MesEnum.MesCustomer customer = new MesEnum.MesCustomer();customer = MesEnum.GetEnumValueFromDescription<MesEnum.MesCustomer>(MesApp.Instance.MyMesConfig.SelectCustomer);if (!MesApp.Instance.MyMesConfig.IsEnableMes){customer = MesEnum.MesCustomer.None;}switch (customer){case MesEnum.MesCustomer.CustomerA:Mes = new CustomerA();break;default:Mes = new DefaultMes();break;}return true;}catch (Exception){return false;}}public bool SaveMesConfig(){// 检查 config 文件夹是否存在if (!Directory.Exists(EnvironmentAddress)){try{// 创建 config 文件夹Directory.CreateDirectory(EnvironmentAddress);string json = JsonConvert.SerializeObject(MesApp.Instance.MyMesConfig, Formatting.Indented);File.WriteAllText(ConfigPath, json);return true;}catch (Exception ex){MesLog.Error("配置参数序列化失败:" + ex.ToString());return false;}}else{string json = JsonConvert.SerializeObject(MesApp.Instance.MyMesConfig, Formatting.Indented);File.WriteAllText(ConfigPath, json);return true;}}public bool GetMesConfig(){if (Directory.Exists(EnvironmentAddress)){try{string json = File.ReadAllText(ConfigPath);MesApp.Instance.MyMesConfig = MesJson.DeserializeObject<MyMesConfig>(json);}catch (Exception ex){MesLog.Error("配置参数反序列化失败:" + ex.ToString());}}else{return false;}return true;}}
}
3.2IMesSend接口
IMesSend接口是项目主要的内容,在创建Mes时,会使用工厂模式,通过IMesSend接口去生产指定的客户类,客户类通常包含我们自有设备通常需要上传的函数方法。同时包含一个动态接口,因为在某些客户需要定制一些独特的功能,但是大部分客户都是没有的,可以使用这个Task Dynamic(MesDynamic dynamic);的接口。
using System.Threading.Tasks;namespace Mes
{public interface IMesSend{/// <summary>/// 用户登录/// </summary>/// <returns></returns>Task<MesProcessData> MesLogin(MesDynamic dynamic);/// <summary>/// 上报拿板情况/// </summary>Task<bool> RemovePCB(MesDynamic data);/// <summary>/// 上传工艺参数/// </summary>void ProcessParameters();/// <summary>/// 上传整板测试结果/// </summary>Task<bool> Result(MesDynamic dynamic);/// <summary>/// Mes启用/// </summary>/// <returns></returns>bool MesEnable();/// <summary>/// 上传报警信息,/// </summary>/// <param name="message"></param>/// <param name="Level">级别:1为警告黄灯,2为红灯报警</param>Task<bool> AlarmInformation(string message, int Level);/// <summary>/// 消除报警信息/// </summary>/// <param name="message"></param>Task<bool> CancelAlarmInformation(string message);/// <summary>/// 发送设备状态/// </summary>Task<bool> ProcessStop(MesEnum.MachineState on);/// <summary>/// 切换程序/// </summary>void SwitchPrograms();/// <summary>/// 过站检测/// </summary>/// <param name="BoardCode"></param>/// <returns></returns>Task<bool> CheckBoard(MesDynamic dynamic);/// <summary>/// 设备出板/// </summary>/// <returns></returns>Task<bool> OutBoard(MesDynamic dynamic);/// <summary>/// 关闭Mes/// </summary>void CloseMes();/// <summary>/// 动态接口,用于在特殊情况下调用的接口/// </summary>/// <param name="dynamic"></param>/// <returns></returns>Task<MesProcessData> Dynamic(MesDynamic dynamic);}
}
3.3通讯类
通讯类所需要做的内容并不是很多,需要有创建通讯的步骤,发送数据,监听端口,关闭通讯就可以了。
using JOJO.Mes.Log;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace JOJO.Mes.CommModel
{internal class MesHttp{public string MesUrlAddress { get; set; } = @"http:\\Send";public string AccessInterface { get; set; } = "";public string MesUrlAcceptAddress { get; set; } = @"http:\\Accept";public int MesUrlTimeOut { get; set; } = 5000;public bool UseToken { get; set; } = false;public string Token { get; set; } = "";private string url { get; set; } = "";public TimeSpan CtsTimeOut = TimeSpan.FromSeconds(10);HttpClient Client;HttpListener Listener;public Queue<string> GetHttpQueue = new Queue<string>();public Queue<string> ResponseHttpQueue = new Queue<string>();public bool CreatHttpClient(){Client = new HttpClient();Client.Timeout = TimeSpan.FromSeconds(MesUrlTimeOut);Listener = new HttpListener();Listener.Prefixes.Add(MesUrlAcceptAddress); // 监听的 URL 前缀Listener.Start();return true;}public async Task<string> MesUrlSendAndAccept(string obj){try{string dataOut = "";url = MesUrlAddress + "/" + AccessInterface;MesLog.Info("当前访问的URL地址:" + url);// 创建 HTTP 客户端实例if (UseToken){Client.DefaultRequestHeaders.Add("token", Token);}// 构造要发送的内容var content = new StringContent(obj, Encoding.UTF8, "application/json");MesLog.Info("MesUrl数据上传:" + obj);// 发送POST请求var response = await Client.PostAsync(url, content);// 确保响应成功if (response.IsSuccessStatusCode){dataOut = await response.Content.ReadAsStringAsync().ConfigureAwait(false);MesLog.Info("Mes数据接受:" + dataOut);}content = null;response = null;return dataOut;}catch (Exception ex){MesLog.Error("发送Http数据失败:" + ex.ToString());return null;}}public async void AcceptHttp(){while (true){HttpListenerContext context = await Listener.GetContextAsync();HttpListenerRequest request = context.Request;HttpListenerResponse response = context.Response;if (request.HttpMethod == "POST"){using (StreamReader reader = new StreamReader(request.InputStream, request.ContentEncoding)){string requestContent = await reader.ReadToEndAsync();GetHttpQueue.Enqueue(requestContent);string responseString = "";//等待内容响应try{Task ResponseTask = Task.Run(async () =>{while (true){if (ResponseHttpQueue.Count > 0){responseString = ResponseHttpQueue.Dequeue();break;}await Task.Delay(10);}}, new CancellationTokenSource(CtsTimeOut).Token);}catch (Exception ex){MesLog.Error("Http接收响应失败:" + ex.ToString());MesApp.Instance.Const.SetMachineLog("Http接收响应失败");return;}byte[] buffer = Encoding.UTF8.GetBytes(responseString);response.ContentLength64 = buffer.Length;using (Stream output = response.OutputStream){await output.WriteAsync(buffer, 0, buffer.Length);}}}else{response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;response.Close();}}}public void Close(){Client.Dispose();}}
}
using JOJO.Mes.Log;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;namespace JOJO.Mes.CommModel
{internal class MesSocket{private readonly byte[] buffer = new byte[1024 * 1024 * 100];public Socket listener;public Socket handler;public int Point = 8888;public string Address = "192.168.8.88";public Queue<string> SocketQueue = new Queue<string>();public bool CreatSocket(){listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);long address = new long();try{IPAddress ipAddress = IPAddress.Parse(Address);address = BitConverter.ToInt32(ipAddress.GetAddressBytes(), 0);IPEndPoint localEndPoint = new IPEndPoint(address, Point);listener.Bind(localEndPoint);listener.Listen(10);}catch (FormatException){return false;}MesLog.Info("Socket,等待客户端连接...");// 开始异步接受客户端连接listener.BeginAccept(AcceptCallback, listener);return true;}private void AcceptCallback(IAsyncResult ar){Socket listener = (Socket)ar.AsyncState;// 完成接受客户端连接handler = listener.EndAccept(ar);MesLog.Info($"连接Socket成功:" + handler.AddressFamily);// 开始异步接收数据handler.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, handler); 继续监听新的连接listener.BeginAccept(AcceptCallback, listener);}private void ReceiveCallback(IAsyncResult ar){Socket handler = (Socket)ar.AsyncState;try{int bytesRead = handler.EndReceive(ar);if (bytesRead > 0){byte[] data = new byte[bytesRead];Array.Copy(buffer, data, bytesRead);string message = System.Text.Encoding.UTF8.GetString(data);MesLog.Info($"接收Socket数据: {message}");SocketQueue.Enqueue(message);handler.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, handler);}}catch (SocketException e){MesLog.Warn($"接收Socket数据出错: {e.Message}");}finally{handler.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, handler);}}public async void SendObject(string SendString){try{if (!handler.Connected){MesApp.Instance.Const.SetMachineLog("Mes所在的Socket端口未连接,无法发送数据");return;}byte[] SendBytes = Encoding.UTF8.GetBytes(SendString);await Task.Run(() =>{// 通过Socket发送数据handler.Send(SendBytes, 0, SendBytes.Length, SocketFlags.None);});}catch (Exception ex){MesLog.Error("发送不带反馈的Socket数据失败:" + ex.ToString());}}public void Close(){try{handler.Shutdown(SocketShutdown.Both);//listener.Shutdown(SocketShutdown.Both);handler.Close();listener.Close();}catch (Exception){}}}
}
3.4日志类
using System;
using System.IO;
using System.Threading.Tasks;namespace Mes.Log
{internal static class MesLog{public enum LogLevel{Trace,Debug,Info,Warn,Error,Fatal}private static string logBasePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log", "Meslog");private static long maxFileSize = 5 * 1024 * 1024; // 5MBprivate static LogLevel minimumLevel = LogLevel.Trace;static MesLog(){if (!Directory.Exists(logBasePath)){Directory.CreateDirectory(logBasePath);}}private static string GetLogFilePath(){string date = DateTime.Now.ToString("yyyy-MM-dd");return Path.Combine(logBasePath, $"{date}.txt");}private static async Task AppendTextAsync(string text, string filePath){var fileOptions = FileOptions.Asynchronous;using (var fileStream = new FileStream(filePath,FileMode.Append, // 使用FileMode.Append以追加模式打开文件FileAccess.Write,FileShare.ReadWrite,bufferSize: 4096 * 10,fileOptions)){using (var streamWriter = new StreamWriter(fileStream)){// 异步写入文本到文件await streamWriter.WriteAsync(text);}}}public static async void Write(LogLevel level, string message){if (level < minimumLevel){return;}string timestamp = DateTime.Now.ToString("yyyy - MM - dd HH:mm:ss");string logMessage = $"{timestamp} [{level}]: {message}{Environment.NewLine}";string filePath = GetLogFilePath();if (File.Exists(filePath) && new FileInfo(filePath).Length >= maxFileSize){filePath = Path.Combine(logBasePath, $"{DateTime.Now.ToString("yyyyMMddHHmmss")}.txt");}await AppendTextAsync(logMessage, filePath);}public static void Trace(string message){Write(LogLevel.Trace, message);}public static void Debug(string message){
#if DEBUGWrite(LogLevel.Debug, message);
#endif}public static void Info(string message){Write(LogLevel.Info, message);}public static void Warn(string message){Write(LogLevel.Warn, message);}public static void Error(string message){Write(LogLevel.Error, message);}public static void Fatal(string message){Write(LogLevel.Fatal, message);}}
}
3.5:配置参数类(使用Json格式)
using System;
using System.Windows;namespace Mes.Config
{[Serializable]public class MyMesConfig{/// <summary>/// 是否需要Mes控制软件,不需要情况下,减少线程开辟/// </summary>public bool IsMesControMachine { get; set; } = false;/// <summary>/// 是否显示选择客户页面/// </summary>public string IsShowSelectCustomer { get; set; } = Visibility.Visible.ToString();/// <summary>/// 是否开启Mes/// </summary>public bool IsEnableMes { get; set; } = false;/// <summary>/// Mes超时时间/// </summary>public int MesTimeOut { get; set; } = 5000;public string EquipmentID { get; set; } = "SMT01";public string MesAddress { get; set; } = "192.168.1.1";/// <summary>/// 选择客户选项卡的ID/// </summary>public int SelectedTabIndex { get; set; } = 0;/// <summary>/// 是否显示客户页面/// </summary>public string IsShowCustomer { get; set; } = Visibility.Collapsed.ToString();/// <summary>/// 选择的客户名称/// </summary>public string SelectCustomer { get; set; } = "选择Mes客户";public CustomerConfig.CustomerA CustomerA { get; set; } = new CustomerConfig.CustomerA();public CustomerConfig.CustomerB CustomerB { get; set; } = new CustomerConfig.CustomerB();}
}
3.6,实际使用的用户类参考
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;namespace Mes.Client
{/// <summary>/// 客户A/// </summary>public class CustomerA : IMesSend{private readonly byte[] buffer = new byte[1024 * 1024 * 100];private MesSocket socket = new MesSocket();private static readonly object _lockObject = new object();private Dictionary<string, CustomerARec> _recDic = new Dictionary<string, CustomerARec>();private Dictionary<string, CustomerARec> RecDic{get => _recDic;set{lock (_lockObject){_recDic = value;}}}DateTime HeartTime = DateTime.Now;private bool CustomerAIsConnect = false;private bool IsAlarm = false;private string Heade { get; } = "Header";public CustomerA(){socket.Address = MesApp.Instance.MyMesConfig.MesAddress;socket.Point = MesApp.Instance.MyMesConfig.CustomerA.Port;socket.CreatSocket();Receive();HeartTimeAndIsConnect();}private object lockObj = new object();private void Receive(){Task MesContralMachine = Task.Run(async () =>{while (true){lock (lockObj){if (socket.SocketQueue.Count > 0){string message = socket.SocketQueue.Dequeue();MesLog.Info($"接收客户AMes数据: {message}");MesProcessData ProcessData = MesXml.DeserializeXml(message);string MesInterface = ProcessData.FindValueByPath(new string[] { "Header", "MESSAGENAME" }, 0).ToString();switch (MesInterface){case "EAP_LinkTest_Request":EAP_LinkTest_Request_Accept(ProcessData);break;case "DATE_TIME_CALIBREATION_COMMAND":ChangeTime(ProcessData);break;case "ALARM_REPORT_R":case "EQP_STATUS_REPORT_R":case "JOB_RECEIVE_REPORT_R":case "JOB_SEND_REPORT_R":case "EDC_REPORT_R":case "JOB_REMOVE_RECOVERY_REPORT_R":CheckSend(ProcessData);break;case "123"://预留Mes控制设备部分MesApp.Instance.MesQueueAccept.Enqueue(ProcessData); break;default:break;}}}// 等待一段时间,避免忙等待await Task.Delay(10);}});}public bool MesEnable(){return MesApp.Instance.MyMesConfig.IsEnableMes && socket.handler.Connected;}public async Task<bool> AlarmInformation(string message, int Level){List<MesProcessData> datas = new List<MesProcessData>();datas.Add(new MesProcessData { MesName = "EquipmentID", MesValue = MesApp.Instance.MyMesConfig.EquipmentID });datas.Add(new MesProcessData { MesName = "AlarmStatus", MesValue = 0 });datas.Add(new MesProcessData { MesName = "AlarmLevel", MesValue = 0 });datas.Add(new MesProcessData { MesName = "AlarmCode", MesValue = "00" });datas.Add(new MesProcessData { MesName = "AlarmText", MesValue = message });MesDynamic dynamic = new MesDynamic();dynamic.AddressInterface = "ALARM_REPORT";IsAlarm = true;return await MesSend(datas, dynamic);}public void ProcessParameters(){return;}/// <summary>/// 过站检查/// </summary>/// <param name="BoardCode"></param>/// <returns></returns>public async Task<bool> CheckBoard(MesDynamic dynamic){List<MesProcessData> datas = new List<MesProcessData>();if (dynamic.BoardCode == "" || dynamic.BoardCode == null){dynamic.BoardCode = MesApp.Instance.Const.NowTimeF;}datas.Add(new MesProcessData { MesName = "EquipmentID", MesValue = MesApp.Instance.MyMesConfig.EquipmentID });datas.Add(new MesProcessData { MesName = "JobID", MesValue = dynamic.BoardCode });dynamic.AddressInterface = "JOB_RECEIVE_REPORT";bool result = await MesSend(datas, dynamic);return result;}/// <summary>/// 发送设备状态/// </summary>/// <param name="on"></param>public async Task<bool> ProcessStop(MesEnum.MachineState on){if (!MesApp.Instance.Mes.MesEnable()){return true;}int EquipmentStatus = -1;switch (on){case MesEnum.MachineState.stop:EquipmentStatus = 3;break;case MesEnum.MachineState.start:EquipmentStatus = 1;break;case MesEnum.MachineState.RedLight:EquipmentStatus = 2;break;case MesEnum.MachineState.AwaitEnterBoard:EquipmentStatus = 4;break;case MesEnum.MachineState.AwaitOutBoard:EquipmentStatus = 5;break;default:EquipmentStatus = 2;break;}List<MesProcessData> datas = new List<MesProcessData>();datas.Add(new MesProcessData { MesName = "EquipmentID", MesValue = MesApp.Instance.MyMesConfig.EquipmentID });List<MesProcessData> StationInfoList = new List<MesProcessData>();List<MesProcessData> StationInfo = new List<MesProcessData>();StationInfo.Add(new MesProcessData { MesName = "Station", MesValue = MesApp.Instance.MyMesConfig.CustomerA.CustomerAStatueName });StationInfo.Add(new MesProcessData { MesName = "EquipmentStatus", MesValue = EquipmentStatus });StationInfoList.Add(new MesProcessData { MesName = "StationInfo", MesValue = StationInfo.ToArray() });datas.Add(new MesProcessData { MesName = "StationInfoList", MesValue = StationInfoList.ToArray() });MesDynamic dynamic = new MesDynamic();dynamic.AddressInterface = "EQP_STATUS_REPORT";return await MesSend(datas, dynamic);}public async Task<bool> RemovePCB(MesDynamic data){if (!MesApp.Instance.Mes.MesEnable()){return true;}List<MesProcessData> datas = new List<MesProcessData>();datas.Add(new MesProcessData { MesName = "EquipmentID", MesValue = MesApp.Instance.MyMesConfig.EquipmentID });datas.Add(new MesProcessData { MesName = "JobID", MesValue = data.BoardCode });datas.Add(new MesProcessData { MesName = "RemoveFlag", MesValue = 0 });data.AddressInterface = "JOB_REMOVE_RECOVERY_REPORT";return await MesSend(datas, data);}public async Task<bool> Result(MesDynamic data){if (!MesApp.Instance.Mes.MesEnable()){return true;}List<MesProcessData> datas = new List<MesProcessData>();datas.Add(new MesProcessData { MesName = "EquipmentID", MesValue = MesApp.Instance.MyMesConfig.EquipmentID.ToString() });datas.Add(new MesProcessData { MesName = "JobID", MesValue = data.BoardCode.ToString() });datas.Add(new MesProcessData { MesName = "ProcessTime", MesValue = data.TimeCost.ToString() });datas.Add(new MesProcessData { MesName = "ProcessStartTime", MesValue = data.ProcessStartTime.ToString() });datas.Add(new MesProcessData { MesName = "ProcessEndTime", MesValue = data.ProcessEndTime.ToString() });List<MesProcessData> MesProcessDataList = new List<MesProcessData>();List<MesProcessData> ProcessData1 = new List<MesProcessData>();ProcessData1.Add(new MesProcessData { MesName = "Station", MesValue = MesApp.Instance.MyMesConfig.CustomerA.CustomerAStatueName.ToString() });ProcessData1.Add(new MesProcessData { MesName = "Name", MesValue = "TotalResult" });ProcessData1.Add(new MesProcessData { MesName = "value", MesValue = int.Parse(data.VerifiedBoardResult) == 0 ? "NG" : "OK" });List<MesProcessData> ProcessData2 = new List<MesProcessData>();ProcessData2.Add(new MesProcessData { MesName = "Station", MesValue = MesApp.Instance.MyMesConfig.CustomerA.CustomerAStatueName.ToString() });ProcessData2.Add(new MesProcessData { MesName = "Name", MesValue = "ProductCode" });ProcessData2.Add(new MesProcessData { MesName = "value", MesValue = data.BoardCode.ToString() });List<MesProcessData> ProcessData3 = new List<MesProcessData>();ProcessData3.Add(new MesProcessData { MesName = "Station", MesValue = MesApp.Instance.MyMesConfig.CustomerA.CustomerAStatueName.ToString() });ProcessData3.Add(new MesProcessData { MesName = "Name", MesValue = "ProcessName" });ProcessData3.Add(new MesProcessData { MesName = "value", MesValue = data.ProduceName.ToString() });List<MesProcessData> ProcessData4 = new List<MesProcessData>();ProcessData4.Add(new MesProcessData { MesName = "Station", MesValue = MesApp.Instance.MyMesConfig.CustomerA.CustomerAStatueName.ToString() });ProcessData4.Add(new MesProcessData { MesName = "Name", MesValue = "PartNum" });ProcessData4.Add(new MesProcessData { MesName = "value", MesValue = data.TPNumber.ToString() });List<MesProcessData> ProcessData5 = new List<MesProcessData>();ProcessData5.Add(new MesProcessData { MesName = "Station", MesValue = MesApp.Instance.MyMesConfig.CustomerA.CustomerAStatueName.ToString() });ProcessData5.Add(new MesProcessData { MesName = "Name", MesValue = "NGNum" });ProcessData5.Add(new MesProcessData { MesName = "value", MesValue = data.NGTPNumber.ToString() });JArray Detailes = new JArray();foreach (var item in data.TPNGs){string codeName = string.Join(",", item.NGCodeName);JObject Detailedata = new JObject{{"Code", item.SubBoardCode},{"Results", "NG"},{"TEST_ITEM", item.TagNumber},{"Result", codeName},{"PartName", item.PartNumber}};Detailes.Add(Detailedata);}List<MesProcessData> ProcessData6 = new List<MesProcessData>();ProcessData6.Add(new MesProcessData { MesName = "Station", MesValue = MesApp.Instance.MyMesConfig.CustomerA.CustomerAStatueName });ProcessData6.Add(new MesProcessData { MesName = "Name", MesValue = "Details" });ProcessData6.Add(new MesProcessData { MesName = "value", MesValue = Detailes.ToString() });MesProcessDataList.Add(new MesProcessData { MesName = "ProcessData", MesValue = ProcessData1.ToArray() });MesProcessDataList.Add(new MesProcessData { MesName = "ProcessData", MesValue = ProcessData2.ToArray() });MesProcessDataList.Add(new MesProcessData { MesName = "ProcessData", MesValue = ProcessData3.ToArray() });MesProcessDataList.Add(new MesProcessData { MesName = "ProcessData", MesValue = ProcessData4.ToArray() });MesProcessDataList.Add(new MesProcessData { MesName = "ProcessData", MesValue = ProcessData5.ToArray() });MesProcessDataList.Add(new MesProcessData { MesName = "ProcessData", MesValue = ProcessData6.ToArray() });datas.Add(new MesProcessData { MesName = "ProcessDataList", MesValue = MesProcessDataList.ToArray() });MesDynamic dynamic = new MesDynamic{AddressInterface = "EDC_REPORT"};return await MesSend(datas, dynamic, true); ;}public void SwitchPrograms(){return;}private static DateTime _lastCallTime = DateTime.MinValue;/// <summary>/// 发送客户A数据/// </summary>/// <param name="datas">数据源</param>/// <param name="dynamic">接口</param>/// <returns></returns>public async Task<bool> MesSend(List<MesProcessData> datas, MesDynamic dynamic, bool IsUseFix = false){Random random = new Random();int number = GetUniqueFiveDigitNumber();List<MesProcessData> Message = new List<MesProcessData>();List<MesProcessData> Head = new List<MesProcessData>();Head.Add(new MesProcessData { MesName = "MESSAGENAME", MesValue = dynamic.AddressInterface });Head.Add(new MesProcessData { MesName = "TRANSACTIONID", MesValue = MesApp.Instance.Const.NowTime.ToString("yyyyMMddHHmmssffff") + number });Head.Add(new MesProcessData { MesName = "MESSAGEID", MesValue = number.ToString() });Head.Add(new MesProcessData { MesName = "REPLYSUBJECTNAME", MesValue = MesApp.Instance.MyMesConfig.MesAddress + ":" + MesApp.Instance.MyMesConfig.CustomerA.Port });Message.Add(new MesProcessData { MesName = "Header", MesValue = Head.ToArray() });Message.Add(new MesProcessData { MesName = "Body", MesValue = datas.ToArray() });List<MesProcessData> MessageReturn = new List<MesProcessData>();MessageReturn.Add(new MesProcessData { MesName = "ReturnCode", MesValue = "" });MessageReturn.Add(new MesProcessData { MesName = "ReturnMessage", MesValue = "" });Message.Add(new MesProcessData { MesName = "Return", MesValue = MessageReturn.ToArray() });MesProcessData Top = new MesProcessData();Top.MesName = "Message";Top.MesValue = Message.ToArray();if ((DateTime.Now - _lastCallTime).TotalMilliseconds >= 500){_lastCallTime = DateTime.Now;MesSend_Accept(Top);}return await AwaitReceive(Top);}/// <summary>/// 发送数据后,将参数存储到字典中,等待超时和反馈信号/// </summary>/// <param name="mesData"></param>/// <returns></returns>private async Task<bool> AwaitReceive(MesProcessData Data){CancellationTokenSource cts1 = new CancellationTokenSource();CustomerARec meiDiRec = new CustomerARec();string Time = DateTime.Now.ToString();meiDiRec.Cts = cts1;string MESSAGEID = Data.FindValueByPath(new string[] { Heade, "MESSAGEID" }).ToString();RecDic.Add(MESSAGEID, meiDiRec);bool Result = false;Task task1 = Task.Run(async () =>{int timeoutCount = 0;while (true){try{Task.Delay(TimeSpan.FromMilliseconds(MesApp.Instance.MyMesConfig.MesTimeOut), cts1.Token).Wait(cts1.Token);}catch (OperationCanceledException ex){if (ex.CancellationToken == cts1.Token){if (RecDic[MESSAGEID].Result){RecDic.Remove(MESSAGEID);MesLog.Info(MESSAGEID + "return true");Result = true;return;}RecDic.Remove(MESSAGEID);Result = false;return;}timeoutCount++;if (timeoutCount >= MesApp.Instance.MyMesConfig.CustomerA.CustomerAReNumber){RecDic.Remove(MESSAGEID);MesLog.Error("Mes重新发送3次失败。接口为:" + Data.FindValueByPath(new string[] { Heade, "MESSAGENAME" }).ToString() + " 时间为:" + Time +"随机ID为:" + MESSAGEID);CustomerAIsConnect = false;MesApp.Instance.Const.SetMachinAlarm();break;}else{MesSend_Accept(Data);continue;}}await Task.Delay(5000 * 100);}Result = false;return;}, cts1.Token);await task1;return Result;}private void CheckSend(MesProcessData data){string MESSAGEID = data.FindValueByPath(new string[] { "Header", "MESSAGEID" }).ToString();if (RecDic.ContainsKey(MESSAGEID)){if (!data.FindValueByPath(new string[] { "Return", "ReturnCode" }).ToString().Contains("1")){MesLog.Error("Mes执行失败。 接口为:" + data.FindValueByPath(new string[] { "Header", "MESSAGENAME" }).ToString());}RecDic[MESSAGEID].Result = true;RecDic[MESSAGEID].Cts.Cancel();}else{MesLog.Error("Mes没有此发送数据");}}public void MesSend_Accept(MesProcessData Data){string data = MesXml.SerializeToXml(Data);data = Regex.Replace(data, @"<\?.*?\?>", "");//整理XMl的缩进XmlDocument xmlDoc = new XmlDocument();xmlDoc.LoadXml(data);StringBuilder sb = new StringBuilder();XmlWriterSettings settings = new XmlWriterSettings();settings.Indent = true; // 设置缩进为 truesettings.IndentChars = " "; // 设置缩进字符,这里使用两个空格using (XmlWriter writer = XmlWriter.Create(sb, settings)){xmlDoc.Save(writer);}data = sb.ToString();if (!socket.handler.Connected){MesApp.Instance.Const.SetMachineLog("客户AMes 没有连接");MesApp.Instance.Const.SetMachinAlarm();return;}socket.SendObject(data);}/// <summary>/// 接收心跳,反馈接收的心跳/// </summary>public void EAP_LinkTest_Request_Accept(MesProcessData Data){HeartTime = DateTime.Now;Data = Data.ModifyValueByPath(new string[] { "Header", "MESSAGENAME" }, "EAP_LinkTest_Request_R");Data = Data.ModifyValueByPath(new string[] { "Header", "REPLYSUBJECTNAME" }, MesApp.Instance.MyMesConfig.MesAddress + ":" + MesApp.Instance.MyMesConfig.CustomerA.Port);Data = Data.ModifyValueByPath(new string[] { "Return", "ReturnCode" }, "01");Data = Data.ModifyValueByPath(new string[] { "Return", "ReturnMessage" }, "指令执行成功");MesSend_Accept(Data);}public bool AwaitHeartTime(TimeSpan time){if (HeartTime + time < DateTime.Now){return false;}return true;}public async Task<bool> OutBoard(MesDynamic dynamic){List<MesProcessData> datas = new List<MesProcessData>();if (dynamic.BoardCode == "" || dynamic.BoardCode == null){dynamic.BoardCode = MesApp.Instance.Const.NowTimeF;}datas.Add(new MesProcessData { MesName = "EquipmentID", MesValue = MesApp.Instance.MyMesConfig.EquipmentID });datas.Add(new MesProcessData { MesName = "JobID", MesValue = dynamic.BoardCode });dynamic.AddressInterface = "JOB_SEND_REPORT";bool result = await MesSend(datas, dynamic);return result;}/// <summary>/// Mes校准系统时间/// </summary>/// <param name="data"></param>public void ChangeTime(MesProcessData data){data = data.ModifyValueByPath(new string[] { "Header", "MESSAGENAME" }, "DATE_TIME_CALIBREATION_COMMAND_R");data = data.ModifyValueByPath(new string[] { "Header", "REPLYSUBJECTNAME" }, MesApp.Instance.MyMesConfig.MesAddress + ":" + MesApp.Instance.MyMesConfig.CustomerA.Port);try{string input = data.FindValueByPath(new string[] { "Body", "DateTime" }).ToString();string format = "yyyyMMddHHmmss";DateTime result = DateTime.ParseExact(input, format, null);DateTime dt = result;bool r = UpdateTime.SetDate(dt);if (r){data = data.ModifyValueByPath(new string[] { "Return", "ReturnCode" }, "01");data = data.ModifyValueByPath(new string[] { "Return", "ReturnMessage" }, "指令执行成功");}else{data = data.ModifyValueByPath(new string[] { "Return", "ReturnCode" }, "02");data = data.ModifyValueByPath(new string[] { "Return", "ReturnMessage" }, "指令执行失败");}}catch (Exception ex){data = data.ModifyValueByPath(new string[] { "Return", "ReturnCode" }, "02");data = data.ModifyValueByPath(new string[] { "Return", "ReturnMessage" }, "指令执行失败");MesLog.Error("Mes矫正时间失败:" + ex.Message);}MesSend_Accept(data);}private static Random _random = new Random();private static HashSet<int> _uniqueNumbers = new HashSet<int>();/// <summary>/// 获取五位的随机数/// </summary>/// <returns></returns>static int GetUniqueFiveDigitNumber(){int fiveDigitNumber;do{fiveDigitNumber = _random.Next(10000, 100000);} while (!_uniqueNumbers.Add(fiveDigitNumber));return fiveDigitNumber;}public async Task<bool> CancelAlarmInformation(string message){if (!IsAlarm){return true;}IsAlarm = false;message += ",报警已消除";List<MesProcessData> datas = new List<MesProcessData>();datas.Add(new MesProcessData { MesName = "EquipmentID", MesValue = MesApp.Instance.MyMesConfig.EquipmentID });datas.Add(new MesProcessData { MesName = "AlarmStatus", MesValue = 1 });datas.Add(new MesProcessData { MesName = "AlarmLevel", MesValue = 0 });datas.Add(new MesProcessData { MesName = "AlarmCode", MesValue = "10" });datas.Add(new MesProcessData { MesName = "AlarmText", MesValue = message });MesDynamic dynamic = new MesDynamic();dynamic.AddressInterface = "ALARM_REPORT";return await MesSend(datas, dynamic);}/// <summary>/// 监听心跳/// </summary>private void HeartTimeAndIsConnect(){Task task1 = Task.Run(() =>{while (true){if (!AwaitHeartTime(MesApp.Instance.MyMesConfig.CustomerA.HeartTime)){MesLog.Error("Mes心跳异常");MesApp.Instance.Const.SetMachinAlarm();CloseMes();break;}}});}public void CloseMes(){socket.Close();}public async Task<MesProcessData> MesLogin(MesDynamic dynamic){List<MesProcessData> resultList1 = new List<MesProcessData>();resultList1.Add(new MesProcessData { MesName = "IsEnable", MesValue = true });return new MesProcessData { MesValue = resultList1.ToArray() };}public Task<MesProcessData> Dynamic(MesDynamic dynamic){throw new NotImplementedException();}public class CustomerARec{public CancellationTokenSource Cts { get; set; } = new CancellationTokenSource();public bool Result { get; set; } = false;}}
}
3.7,框架整体解析
如下图所示:整体框架包含7个部分。
1:Client,客户类。由于每个客户都有独特的定制需求,所以所有的客户定制的内容都存放再Client中,便于管理
2:CommModel,通讯类。存放每种不同通讯方式的方法,通常最常使用的是Http和Socket,如果有另外特殊的通讯模式还可以单独编写。
3:Config,配置文件类。推荐使用可读的Json格式。一开始编写Mes时候就有一个客户是在登陆工程师级别账户时要求Mes通讯同意,但是由于客户Mes在升级无法通讯,所有软件登陆不了工程师级别,无法关闭Mes,造成需要软件重新配置参数。所以需要在特殊情况下,可以手动配置Mes参数。不同的客户Mes配置也是单独做出区分即可,在打开软件时将json反序列化到MesApp下面的Config字段中,即可全局使用。
4.Const,参数类。MesConst,用于存放软件中需要使用但是不需要保存的配置参数。MesData,用于存放自定义的数据结构。MesDynamic,一个动态数据结构类,这个类存在的意义在于,所有的接口传入传出都可以使用这个类对象,所有的数据都可以在这个类中建立新的字段,这个是考虑在,某些定制的客户中,一些框架满足不了的需求,可以在这里做新增内容。MesEnum,枚举类,用来存入Mes的枚举,例如客户枚举,设备状态代码等。其中包含,访问Description参数的方法。
5.Log,日志类,将mes的日志与原有的软件日志做区分防止单日的日志过多。
6.Model,方法类,这里存放着Json和Xml的序列化和反序列化的方法类,就暂时来说,客户的序列化都是Json或者Xml的形式。
7.接口类和MesApp的单例入口
3.8,举例部分方法使用方式
报警和取消报警的调用
public async void Alarm(bool enable, int emgLight = -1, string message = "Alarm")
{ChangeEMGLight(enable ? MachineConsts.EMG_ERROR : emgLight == -1 ? MachineConsts.EMG_RUNNING : emgLight);if (MesApp.Instance.Mes.MesEnable() && !enable){MesApp.Instance.Const.ProcessState = MesEnum.MachineState.GreenLight;if (!await MesApp.Instance.Mes.CancelAlarmInformation(message)){LogController.Instance.Error("上传Mes取消报警信息失败");}}if (MesApp.Instance.Mes.MesEnable() && enable){MesApp.Instance.Const.ProcessState = MesEnum.MachineState.RedLight;if (!await MesApp.Instance.Mes.AlarmInformation(message, 2)){LogController.Instance.Error("上传Mes报警信息失败");}}_machine.EnableBuzzer(enable);
}
3.9,UI部分
由于我使用的WPF框架,对winform,QT的框架并不是很熟悉,所以这里只使用WPF框架的内容作为参考
1.选择厂商的UI。WPF的UI其实编写很简单,核心在于Visibility=“{Binding }”>的使用,例如,在选择厂商前,需要将选择厂商的选项卡显示,厂商选项卡隐藏,那么将选择厂商选项卡的binding设置为显示,其他全部厂商选项卡设置为隐藏。同理选择完厂商后就将指定厂商的选项卡显示即可。
2.参数的Binding,如果是需要保存在配置文件中的,可以使用Binding的Value指向Config的类即可。
3.由于UI部分难度不高,并且大家UI都是不一样的,所以这里仅说一下我是怎么使用UI的
<TabItemWidth="200"Height="24"FontSize="15"Header="请选择Mes厂商"Style="{StaticResource OverrideMaterialDesignNavigationRailTabItem}"Visibility="{Binding IsShowSelectCustomer}"><Grid IsEnabled="{Binding Source={x:Static service:ApplicationStateService.Instance}, Path=LoginUser.UserGroupKey, Converter={StaticResource LoginUserAuthorityToIsEnableConverter}, ConverterParameter={x:Static enums:AuthorityKeys.SoftwareOptions}}"><Grid.RowDefinitions><RowDefinition Height="auto" /><RowDefinition Height="auto" /></Grid.RowDefinitions><StackPanel><Grid Margin="8"><Grid.ColumnDefinitions><ColumnDefinition Width="200" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions><TextBlockHorizontalAlignment="Right"VerticalAlignment="Center"Text="选择需要使用的Mes系统:" /><ComboBoxName="客户名称"Grid.Column="1"Height="30"Margin="0,0,8,0"Cursor="IBeam"ItemsSource="{Binding Path=CustomerName}"Style="{DynamicResource MaterialDesignComboBox}"Text="{Binding SelectCustomer, UpdateSourceTrigger=PropertyChanged}" /></Grid><ButtonMargin="5"HorizontalAlignment="Center"VerticalAlignment="Center"Command="{Binding OpenMesUI}"CommandParameter="ALM"Content="开启Mes配置界面" /></StackPanel></Grid>
</TabItem>
4.总结
整个标准Mes框架并不是很难。难点只有一个是如果将数据灵活应用,在常见编写Mes时,由于每个客户的数据结构都是不一样的,每个客户都需要单独开多个数据结构类去序列化和反序列化,当然这个并没有错,只是这样子会导致代码冗杂量非常大,而且维护难度大,代码命名混乱的问题。所以标准框架主要还是提供一个如何解决数据灵活性的思路而已。除去这个以外,其他内容并不是很难,剩下的就是如何规范后续客户的扩展性,和如何高效的实现解耦和代码迁移。
最后,如果有什么想法可以持续交流。有时间的话,这个标准接口框架还会随着我接入Mes的次数而优化。