C#标准Mes接口框架(持续更新)
前言
由于近期我做了好几个客户的接入工厂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; // 5MB
private 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 DEBUG
Write(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; // 设置缩进为 true
settings.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的
<TabItem
Width="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>
<TextBlock
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="选择需要使用的Mes系统:" />
<ComboBox
Name="客户名称"
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>
<Button
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding OpenMesUI}"
CommandParameter="ALM"
Content="开启Mes配置界面" />
</StackPanel>
</Grid>
</TabItem>
4.总结
整个标准Mes框架并不是很难。难点只有一个是如果将数据灵活应用,在常见编写Mes时,由于每个客户的数据结构都是不一样的,每个客户都需要单独开多个数据结构类去序列化和反序列化,当然这个并没有错,只是这样子会导致代码冗杂量非常大,而且维护难度大,代码命名混乱的问题。所以标准框架主要还是提供一个如何解决数据灵活性的思路而已。除去这个以外,其他内容并不是很难,剩下的就是如何规范后续客户的扩展性,和如何高效的实现解耦和代码迁移。
最后,如果有什么想法可以持续交流。有时间的话,这个标准接口框架还会随着我接入Mes的次数而优化。