对CustomSerialPort类库的改进

CustomSerialPort 项目地址:flyfire.CustomSerialPort。Github主页上对其介绍为:一个增强的自定义串口类,实现协议无关的数据帧完整接收功能,支持跨平台使用。

经过查看其源码,发现其核心思想是在SerialPortStream类库的基础上,将128ms(默认)时间内接收的串口数据当作一个完整的数据包,通过事件机制将数据分发出去。

该类库是对SerialPortStream类库的再封装,实现了跨平台的串口开发,因此具有一定普适性,但是其源码存在一定问题,下面进行概述。

Gitee项目地址:SerialPortStreamHelper

注意:Gitee平台中的代码是最新的,如有修改,本文将不再提示。

1、CustomSerialPort存在的问题

(1) CustomSerialPort只在串口接收最新的数据128ms之后才将之前累积的缓存发送出去,因此如果在128ms之内有大于一个包数据到来,那么就会产生粘包问题。

(2) CustomSerialPort在处理数据的时候不是线程安全的,存在接收数据线程和数据处理线程共用数据的情况。

(3) CustomSerialPort会在一个数据处理周期(128ms)内创建一个线程,导致线程被频繁创建,造成资源浪费。

2、可能的解决方法

(1) CustomSerialPort假定每一个数据接收周期(默认128ms)之内收到的数据都是完整数据包(可能有多个),因此不必处理粘包问题。

(2) 如果在一个接收数据时间周期之内发送多个命令,可能在这个周期内会收到多个完整包,因此需要对收到的多个包进行处理,以保证数据的完整性。

(3) 将数据处理封装在一个基于信号量的线程处理代码逻辑中,可以解决串口数据线程安全的问题。同时,这样处理也能避免频繁创建线程,节约系统资源。

3、对CustomSerialPort源码的重构:SerialPortStreamHelper

(1) 创建对象时只传入是否启用超时机制以及超时参数。

(2) SerialPortStream的参数只暴露读取属性。

(3) 封装Open、Close、Dispose、Write等方法,将串口参数在Open方法中体现。

(4) 创建串口数据帮助类SerialPortDataHelper及数据处理接口ISerialPortDataObserver。在SerialPortDataHelper中创建线程,基于信号量处理完整的串口数据包。

4、具体实现

ISerialPortDataObserver.cs

namespace Xhubobo.IO.Ports
{
    interface ISerialPortDataObserver
    {
        void ReceiveData(byte[] data);
    }
}

SerialPortDataHelper.cs

using System;
using System.Collections.Generic;
using System.Threading;

namespace Xhubobo.IO.Ports
{
    internal class SerialPortDataHelper
    {
        #region 线程

        private Thread _threadWorker;
        private bool _threadWorking;
        private readonly object _threadWorkingLockHelper = new object();

        private bool IsThreadWorking
        {
            get
            {
                bool ret;
                lock (_threadWorkingLockHelper)
                {
                    ret = _threadWorking;
                }

                return ret;
            }
            set
            {
                lock (_threadWorkingLockHelper)
                {
                    _threadWorking = value;
                }
            }
        }

        #endregion

        #region 队列

        private readonly Queue<byte[]> _messageQueue;
        private readonly Semaphore _messageSemaphore;

        #endregion

        private readonly byte[] _buffer;
        private int _offset;
        private int _lastMessageTick;
        private readonly int _timeout;

        private readonly ISerialPortDataObserver _dataObserver;

        public SerialPortDataHelper(
            ISerialPortDataObserver dataObserver,
            int timeout = 128, int bufferSize = 4096)
        {
            _dataObserver = dataObserver;
            _timeout = timeout;
            _buffer = new byte[bufferSize];

            _messageQueue = new Queue<byte[]>();
            _messageSemaphore = new Semaphore(0, byte.MaxValue);
        }

        public void Start()
        {
            IsThreadWorking = true;
            _threadWorker = new Thread(DoWork)
            {
                IsBackground = true
            };
            _threadWorker.Start();
        }

        public void Stop()
        {
            IsThreadWorking = false;
            AddMessage(null);
            if (_threadWorker != null)
            {
                _threadWorker.Join();
                _threadWorker = null;
            }

            ClearMessage();
        }

        #region 队列操作

        public void AddMessage(byte[] message)
        {
            if (IsThreadWorking)
            {
                lock (_messageQueue)
                {
                    _messageQueue.Enqueue(message);
                }

                _messageSemaphore.Release();
            }
        }

        private byte[] PickMessage()
        {
            byte[] message = null;
            lock (_messageQueue)
            {
                if (_messageQueue.Count > 0)
                {
                    message = _messageQueue.Peek();
                    _messageQueue.Dequeue();
                }
            }

            return message;
        }

        private void ClearMessage()
        {
            lock (_messageQueue)
            {
                _messageQueue.Clear();
            }
        }

        #endregion

        /// <summary>
        /// 线程执行方法
        /// </summary>
        private void DoWork()
        {
            while (IsThreadWorking)
            {
                if (_messageSemaphore.WaitOne(1))
                {
                    var message = PickMessage();
                    HandleMessage(message);
                }

                DispatchData();
            }
        }

        #region HandleMessage

        private void HandleMessage(byte[] message)
        {
            _lastMessageTick = Environment.TickCount;

            if (_offset + message.Length > _buffer.Length)
            {
                ResetBuffer();
                return;
            }

            Buffer.BlockCopy(message, 0, _buffer, _offset, message.Length);
            _offset += message.Length;
        }

        private void DispatchData()
        {
            if (_offset > 0 && Environment.TickCount - _lastMessageTick > _timeout)
            {
                var buffer = new byte[_offset];
                Buffer.BlockCopy(_buffer, 0, buffer, 0, _offset);
                _dataObserver?.ReceiveData(buffer);
                ResetBuffer();
            }
        }

        private void ResetBuffer()
        {
            _offset = 0;
        }

        #endregion
    }
}

SerialPortStreamHelper.cs

using RJCP.IO.Ports;
using System;
using System.Linq;

namespace Xhubobo.IO.Ports
{
    public class SerialPortStreamHelper : ISerialPortDataObserver
    {
        public event Action<string, byte[]> DataReceived = (portName, data) => { };
        public string LastErrorMessage { get; private set; }

        #region SerialPortStream Attributes

        public string PortName => _serialPortStream.PortName;
        public int BaudRate => _serialPortStream.BaudRate;
        public Parity Parity => _serialPortStream.Parity;
        public int DataBits => _serialPortStream.DataBits;
        public StopBits StopBits => _serialPortStream.StopBits;

        public bool IsOpen => _serialPortStream.IsOpen;

        public bool DtrEnable
        {
            set { _serialPortStream.DtrEnable = value; }
            get { return _serialPortStream.DtrEnable; }
        }

        public bool RtsEnable
        {
            set { _serialPortStream.RtsEnable = value; }
            get { return _serialPortStream.RtsEnable; }
        }

        #endregion

        /// <summary>
        /// 是否使用接收超时机制
        /// 默认为true
        /// 接收到数据后计时,计时期间收到数据,累加数据,重新开始计时。超时后返回接收到的数据。
        /// </summary>
        private readonly bool _enableTimeout;

        private readonly SerialPortStream _serialPortStream;
        private readonly SerialPortDataHelper _dataHelper;

        public SerialPortStreamHelper(bool enableTimeout = true, int timeout = 128, int bufferSize = 4096)
        {
            _enableTimeout = enableTimeout;
            _serialPortStream = new SerialPortStream()
            {
                DtrEnable = true,
                RtsEnable = true
            };
            _serialPortStream.DataReceived += OnDataReceived;
            _dataHelper = new SerialPortDataHelper(this, timeout, bufferSize);
        }

        public static string[] GetPortNames() => SerialPortStream.GetPortNames();

        public static string BytesToHexStr(byte[] bytes)
        {
            return string.Join(" ", bytes.Select(t => t.ToString("X2")));
        }

        public bool Open(string portName, int baudRate = 115200, Parity parity = Parity.None, int dataBits = 8,
            StopBits stopBits = StopBits.One)
        {
            _serialPortStream.PortName = portName;
            _serialPortStream.BaudRate = baudRate;
            _serialPortStream.Parity = parity;
            _serialPortStream.DataBits = dataBits;
            _serialPortStream.StopBits = stopBits;

            try
            {
                _serialPortStream.Open();
                _dataHelper.Start();
                return true;
            }
            catch (Exception e)
            {
                LastErrorMessage = e.Message;
                return false;
            }
        }

        public void Close()
        {
            if (_serialPortStream.IsOpen)
            {
                _dataHelper.Stop();
                _serialPortStream.Close();
            }
        }

        public void Dispose()
        {
            _dataHelper.Stop();
            _serialPortStream.Dispose();
        }

        public void ReceiveData(byte[] data)
        {
            DataReceived?.Invoke(PortName, data);
        }

        private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            if (_enableTimeout)
            {
                while (_serialPortStream.BytesToRead > 0)
                {
                    var bytesToRead = _serialPortStream.BytesToRead;
                    var buffer = new byte[bytesToRead];

                    var bytesRead = _serialPortStream.Read(buffer, 0, bytesToRead); //此处可能存在数组越界问题
                    if (bytesRead != bytesToRead)
                    {
                        throw new Exception("Serial port receives exception!");
                    }

                    _dataHelper.AddMessage(buffer);
                }
            }
            else
            {
                var bytesToRead = _serialPortStream.BytesToRead;
                if (bytesToRead == 0)
                {
                    return;
                }

                var buffer = new byte[bytesToRead];
                var offset = 0;
                while (offset < bytesToRead)
                {
                    //读取数据到缓冲区
                    offset += _serialPortStream.Read(buffer, offset, bytesToRead - offset);
                }

                DataReceived?.Invoke(PortName, buffer);
            }
        }

        #region Write

        public void Write(byte[] buffer)
        {
            if (IsOpen)
            {
                _serialPortStream.Write(buffer, 0, buffer.Length);
            }
        }

        public void Write(byte[] buffer, int offset, int count)
        {
            if (IsOpen)
            {
                _serialPortStream.Write(buffer, offset, count);
            }
        }

        public void Write(string text)
        {
            if (IsOpen)
            {
                _serialPortStream.Write(text);
            }
        }

        public void WriteLine(string text)
        {
            if (IsOpen)
            {
                _serialPortStream.WriteLine(text);
            }
        }

        #endregion
    }
}
posted @ 2021-03-29 15:30  xhubobo  阅读(560)  评论(0编辑  收藏  举报