并行字典优化会话集合,减少锁操作
大石头 authored at 2017-10-02 15:37:25
16.28 KiB
X
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32;
using NewLife.Data;
using NewLife.Log;
using NewLife.Threading;

namespace NewLife.Net
{
    /// <summary>串口传输</summary>
    /// <example>
    /// 标准例程:
    /// <code>
    /// var st = new SerialTransport();
    /// st.PortName = "COM65";  // 通讯口
    /// st.FrameSize = 16;      // 数据帧大小
    /// 
    /// st.Received += (s, e) =>
    /// {
    ///     Console.WriteLine("收到 {0}", e.ToHex());
    /// };
    /// // 开始异步操作
    /// st.Open();
    /// 
    /// //var buf = "01080000801A".ToHex();
    /// var buf = "0111C02C".ToHex();
    /// for (int i = 0; i &lt; 100; i++)
    /// {
    ///     Console.WriteLine("发送 {0}", buf.ToHex());
    ///     st.Send(buf);
    /// 
    ///     Thread.Sleep(1000);
    /// }
    /// </code>
    /// </example>
    public class SerialTransport : DisposeBase, ITransport, IDisposable
    {
        #region 属性
        private SerialPort _Serial;
        /// <summary>串口对象</summary>
        public SerialPort Serial
        {
            get { return _Serial; }
            set
            {
                _Serial = value;
                if (_Serial != null)
                {
                    PortName = _Serial.PortName;
                    BaudRate = _Serial.BaudRate;
                    Parity = _Serial.Parity;
                    DataBits = _Serial.DataBits;
                    StopBits = _Serial.StopBits;
                }
            }
        }

        /// <summary>端口名称。默认COM1</summary>
        public String PortName { get; set; } = "COM1";

        /// <summary>波特率。默认115200</summary>
        public Int32 BaudRate { get; set; } = 115200;

        /// <summary>奇偶校验位。默认None</summary>
        public Parity Parity { get; set; } = Parity.None;

        /// <summary>数据位。默认8</summary>
        public Int32 DataBits { get; set; } = 8;

        /// <summary>停止位。默认One</summary>
        public StopBits StopBits { get; set; } = StopBits.One;

        /// <summary>超时时间。超过该大小未收到数据,说明是另一帧。默认10ms</summary>
        public Int32 Timeout { get; set; } = 10;

        private String _Description;
        /// <summary>描述信息</summary>
        public String Description
        {
            get
            {
                if (_Description == null)
                {
                    var dic = GetNames();
                    if (!dic.TryGetValue(PortName, out _Description))
                        _Description = "";
                }
                return _Description;
            }
        }

        /// <summary>粘包处理接口</summary>
        public IPacket Packet { get; set; }

        /// <summary>字节超时。数据包间隔,默认20ms</summary>
        public Int32 ByteTimeout { get; set; } = 20;
        #endregion

        #region 构造
        /// <summary>串口传输</summary>
        public SerialTransport()
        {
            // 每隔一段时间检查一次串口是否已经关闭,如果串口已经不存在,则关闭该传输口
            timer = new TimerX(CheckDisconnect, null, 3000, 3000);
        }

        /// <summary>销毁</summary>
        /// <param name="disposing"></param>
        protected override void OnDispose(Boolean disposing)
        {
            base.OnDispose(disposing);

            try
            {
                if (Serial != null) Close();
                if (timer != null) timer.Dispose();
            }
            catch { }
        }
        #endregion

        #region 方法
        /// <summary>确保创建</summary>
        public virtual void EnsureCreate()
        {
            if (Serial == null)
            {
                Serial = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits);

                _Description = null;
            }
        }

        /// <summary>打开</summary>
        public virtual Boolean Open()
        {
            EnsureCreate();

            if (!Serial.IsOpen)
            {
                Serial.Open();
                if (Received != null) Serial.DataReceived += DataReceived;
            }

            return true;
        }

        /// <summary>关闭</summary>
        public virtual Boolean Close()
        {
            // 关闭时必须清空,否则更换属性后再次打开也无法改变属性
            var sp = Serial;
            if (sp != null)
            {
                Serial = null;
                if (Received != null) sp.DataReceived -= DataReceived;
                if (sp.IsOpen) sp.Close();

                OnDisconnect();
            }

            return true;
        }
        #endregion

        #region 发送
        /// <summary>写入数据</summary>
        /// <param name="pk">数据包</param>
        public virtual Boolean Send(Packet pk)
        {
            if (!Open()) return false;

            WriteLog("Send:{0}", pk.ToHex());

            var sp = Serial;
            lock (sp)
            {
                sp.Write(pk.Data, pk.Offset, pk.Count);
            }

            return true;
        }

        /// <summary>异步发送数据并等待响应</summary>
        /// <param name="pk"></param>
        /// <returns></returns>
        public virtual async Task<Packet> SendAsync(Packet pk)
        {
            if (!Open()) return null;

            if (Packet == null) Packet = new PacketProvider();

            var task = Packet.Add(pk, null, Timeout);

            if (pk != null)
            {
                WriteLog("SendAsync:{0}", pk.ToHex());

                // 发送数据
                Serial.Write(pk.Data, pk.Offset, pk.Count);
            }

            return await task;
        }

        /// <summary>接收数据</summary>
        /// <returns></returns>
        public virtual Packet Receive()
        {
            if (!Open()) return null;

            var task = SendAsync(null);
            if (Timeout > 0 && !task.Wait(Timeout)) return null;

            return task.Result;
        }
        #endregion

        #region 异步接收
        void DataReceived(Object sender, SerialDataReceivedEventArgs e)
        {
            // 发送者必须保持一定间隔,每个报文不能太大,否则会因为粘包拆包而出错
            try
            {
                var sp = sender as SerialPort;
                WaitMore();
                if (sp.BytesToRead > 0)
                {
                    var buf = new Byte[sp.BytesToRead];

                    var count = sp.Read(buf, 0, buf.Length);
                    //if (count != buf.Length) buf = buf.ReadBytes(0, count);
                    //var ms = new MemoryStream(buf, 0, count, false);
                    var pk = new Packet(buf, 0, count);

                    ProcessReceive(pk);
                }
            }
            catch (Exception ex)
            {
                //WriteLog("Error " + ex.Message);
                if (Log != null) Log.Error("DataReceived Error {0}", ex.Message);
            }
        }

        void WaitMore()
        {
            var sp = Serial;

            var ms = ByteTimeout;
            var end = DateTime.Now.AddMilliseconds(ms);
            var count = sp.BytesToRead;
            while (sp.IsOpen && end > DateTime.Now)
            {
                //Thread.SpinWait(1);
                Thread.Sleep(ms);
                if (count != sp.BytesToRead)
                {
                    end = DateTime.Now.AddMilliseconds(ms);
                    count = sp.BytesToRead;
                }
            }
        }

        void ProcessReceive(Packet pk)
        {
            try
            {
                if (Packet == null)
                    OnReceive(pk);
                else
                {
                    // 拆包,多个包多次调用处理程序
                    foreach (var msg in Packet.Parse(pk))
                    {
                        OnReceive(msg);
                    }
                }
            }
            catch (Exception ex)
            {
                if (!ex.IsDisposed()) Log.Error("{0}.OnReceive {1}", PortName, ex.Message);
            }
        }

        /// <summary>处理收到的数据。默认匹配同步接收委托</summary>
        /// <param name="pk"></param>
        internal virtual void OnReceive(Packet pk)
        {
            // 同步匹配
            if (Packet != null && Packet.Match(pk, null)) return;

            // 触发事件
            Received?.Invoke(this, new ReceivedEventArgs(pk));
        }

        /// <summary>数据到达事件</summary>
        public event EventHandler<ReceivedEventArgs> Received;
        #endregion

        #region 自动检测串口断开
        /// <summary>断开时触发,可能是人为断开,也可能是串口链路断开</summary>
        public event EventHandler Disconnected;

        Boolean isInEvent;
        void OnDisconnect()
        {
            if (Disconnected != null)
            {
                // 判断是否在事件中,避免外部在断开时间中调用Close造成死循环
                if (!isInEvent)
                {
                    isInEvent = true;

                    Disconnected(this, EventArgs.Empty);

                    isInEvent = false;
                }
            }
        }

        TimerX timer;
        /// <summary>检查串口是否已经断开</summary>
        /// <remarks>
        /// FX串口异步操作有严重的泄漏缺陷,如果外部硬件长时间断开,
        /// SerialPort.IsOpen检测不到,并且会无限大占用内存。
        /// </remarks>
        /// <param name="state"></param>
        void CheckDisconnect(Object state)
        {
            if (String.IsNullOrEmpty(PortName) || Serial == null || !Serial.IsOpen) return;

            // 如果端口已经不存在,则断开吧
            if (!SerialPort.GetPortNames().Contains(PortName))
            {
                WriteLog("串口{0}已经不存在,准备关闭!", PortName);

                //OnDisconnect();
                Close();
            }
        }
        #endregion

        #region 辅助
        /// <summary>获取带有描述的串口名,没有时返回空数组</summary>
        /// <returns></returns>
        public static String[] GetPortNames()
        {
            var list = new List<String>();
            foreach (var item in GetNames())
            {
                list.Add(String.Format("{0}({1})", item.Key, item.Value));
            }
            return list.ToArray();
        }

        /// <summary>获取串口列表,名称和描述</summary>
        /// <returns></returns>
        public static Dictionary<String, String> GetNames()
        {
            var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
            using (var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DEVICEMAP\SERIALCOMM", false))
            using (var usb = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\USB", false))
            {
                if (key != null)
                {
                    foreach (var item in key.GetValueNames())
                    {
                        var name = key.GetValue(item) + "";
                        var des = "";

                        // 尝试枚举USB串口
                        foreach (var vid in usb.GetSubKeyNames())
                        {
                            var usbvid = usb.OpenSubKey(vid);
                            foreach (var elm in usbvid.GetSubKeyNames())
                            {
                                var sub = usbvid.OpenSubKey(elm);
                                //if (sub.GetValue("Class") + "" == "Ports")
                                {
                                    var FriendlyName = sub.GetValue("FriendlyName") + "";
                                    if (FriendlyName.Contains("({0})".F(name)))
                                    {
                                        des = FriendlyName.TrimEnd("({0})".F(name)).Trim();
                                        break;
                                    }
                                }
                            }
                            if (!des.IsNullOrEmpty()) break;
                        }

                        // 最后选择设备映射的串口名
                        if (des.IsNullOrEmpty())
                        {
                            des = item;
                            var p = item.LastIndexOf('\\');
                            if (p >= 0) des = des.Substring(p + 1);
                        }

                        //dic.Add(name, des);
                        // 某台机器上发现,串口有重复
                        dic[name] = des;
                    }
                }
            }
            return dic;
        }

        /// <summary>从串口列表选择串口,支持自动选择关键字</summary>
        /// <param name="keyWord">串口名称或者描述符的关键字</param>
        /// <returns></returns>
        public static SerialTransport Choose(String keyWord = null)
        {
            var ns = GetNames();
            if (ns.Count == 0)
            {
                Console.WriteLine("没有可用串口!");
                return null;
            }

            var name = "";
            var des = "";

            Console.WriteLine("可用串口:");
            Console.ForegroundColor = ConsoleColor.Green;
            foreach (var item in ns)
            {
                if (item.Value == "Serial0") continue;

                if (keyWord != null && (item.Key.EqualIgnoreCase(keyWord) || item.Value.Contains(keyWord)))
                {
                    name = item.Key;
                    des = item.Value;
                }

                //Console.WriteLine(item);
                Console.WriteLine("{0,5}({1})", item.Key, item.Value);
            }
            // 没有自动选择,则默认最后一个
            if (name.IsNullOrEmpty())
            {
                var item = ns.Last();
                name = item.Key;
                des = item.Value;
            }
            while (true)
            {
                Console.ResetColor();
                Console.Write("请输入串口名称(默认 ");
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Write("{0}", name);
                Console.ResetColor();
                Console.Write("):");

                var str = Console.ReadLine();
                if (str.IsNullOrEmpty()) break;

                // 只有输入有效串口名称才行
                if (ns.ContainsKey(str))
                {
                    name = str;
                    des = ns[str];
                    break;
                }
            }

            Console.WriteLine();
            Console.Write("正在打开串口 ");
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("{0}({1})", name, des);

            Console.ResetColor();

            var sp = new SerialTransport
            {
                PortName = name
            };

            return sp;
        }
        #endregion

        #region 日志
        /// <summary>日志对象</summary>
        public ILog Log { get; set; } = Logger.Null;

        /// <summary>输出日志</summary>
        /// <param name="format"></param>
        /// <param name="args"></param>
        public void WriteLog(String format, params Object[] args)
        {
            if (Log != null && Log.Enable) Log.Info(format, args);
        }

        /// <summary>已重载</summary>
        /// <returns></returns>
        public override String ToString()
        {
            if (!String.IsNullOrEmpty(PortName))
                return PortName;
            else
                return "(SerialPort)";
        }
        #endregion
    }
}