当前位置: 首页 > article >正文

C#标准Mes接口框架(持续更新)

前言

由于近期我做了好几个客户的接入工厂Mes系统的需求。但是每个客户的Mes都有不同程度的定制需求,原有的代码复用难度其实很大。所以打算将整个接入Mes系统的框架单独拿出来作为一个项目使用,同时因为不同的设备接入同一个Mes系统,所以代码的迁移规范同样非常重要。

1.需求分析

这部分的需求分析,主要来自于我在接入不同客户Mes系统时发现的一些问题和解决方案,同时也了解过工厂Mes系统供应商的朋友们。列举了一些比较实际的功能(主要是代码方面的)。

  1. 需要有同一个的接入方式,方便接入不同客户的Mes系统。
  2. 需要有全局的参数列表。
  3. 只允许存在一个Mes系统的入口。
  4. 保证数据统一性,在多线程访问,设备不同轨道运行时,数据需要做区分。
  5. 尽可能的减少后续的代码修改。
  6. UI上,不同的客户的选项卡要做区分,但是只显示一个选项卡。
  7. 所有关于Mes的操作需要独立于设备内容,方便在不同设备软件上迁移,设备软件只做传入数据的功能。
  8. 需要考虑由Mes控制设备的功能

2.设计项目内容

  1. 首先会有一个MesApp的入口,用来访问整个Mes系统,但是这个入口只能有一个,所有MesApp应当使用单例模式。
  2. 不同的客户需要继承于同一个接口,根据客户的名称信息去访问指定的客户类,所以MesApp应当具有工厂模式,通过工厂生产客户类。
  3. 根据全局参数列表,需要一个可通过MesApp访问的全局静态类Const,和枚举类
  4. 保证数据做好区分,但是又保证数据统一性,所以Mes需要全部公用一个数据类对象,并且可以在外部写入参数,并作为类对象进行传参
  5. UI上面仍然使用MVVM框架去实现,同时使用Visibility的binding的形式来控制在选项卡中显示UI(根据实际情况可以选择公用选项卡或者,多选项卡的形式)
  6. 考虑数据的通用性,所以数据应到以类对象的形式存在(类型为String的Name,类型为Object的Value)
  7. 需要独立的数据存储部分,通讯方式需要独立,
  8. 在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的次数而优化。


http://www.kler.cn/a/518492.html

相关文章:

  • 【C】memory 详解
  • Java 实现Excel转HTML、或HTML转Excel
  • 「 机器人 」系统辨识实验浅谈
  • Linux通过docker部署京东矩阵容器服务
  • StarRocks BE源码编译、CLion高亮跳转方法
  • Kafka 深入服务端 — 时间轮
  • 三分钟简单了解一些HTML的标签和语法_02
  • 技术总结:FPGA基于GTX+RIFFA架构实现多功能SDI视频转PCIE采集卡设计方案
  • Linux 命令行网络连接指南
  • AIGC的企业级解决方案架构及成本效益分析
  • RocketMQ 的 Topic 和消息队列MessageQueue信息,是怎么分布到Broker的?怎么负载均衡到Broker的?
  • 数据结构——二叉树——堆(1)
  • 【后端开发】字节跳动青训营之性能分析工具pprof
  • 正则表达式以及Qt中的使用
  • 为什么UI导入png图会出现白边
  • Zbrush导入笔刷
  • Android中Service在新进程中的启动流程
  • “AI视觉贴装系统:智能贴装,精准无忧
  • 《论文翻译》KIMI K1.5:用大语言模型扩展强化学习
  • 保存复合型数据到h5文件
  • ptp同步时钟、ptp网络时间服务器、ptp主时钟、ptp从时钟、ptp精密同步时钟
  • 15 分布式锁和分布式session
  • ElasticSearch JavaRestClient查询之快速入门
  • antdesignvue统计数据源条数、计算某列合计值、小数计算不精确多了很多小数位
  • 媒体新闻发稿要求有哪些?什么类型的稿件更好通过?
  • navicat无法连接虚拟机的docker中的mysql