C#如何完美处理SerialPort读写数据?

How to deal with C# SerialPort read and write data perfectly?

代码在 .Net4 上运行。

一个设备,一个操作:'Op_A',一个队列:'QuData',三个命令:'Cmd_Req','Cmd_Res'和'Cmd_Report'。

  1. 写入'Cmd_Req'时,设备returns'Cmd_Res'和用户可以执行'Op_A'。
  2. 如果设备returns'Cmd_Res'正确,则提示用户可以执行'Op_A'和一个数据入队'QuData'。
  3. 当用户执行'Op_A'时,设备报告'Cmd_Report'。如果设备正确地 reprots 'Cmd_Report',则一个数据从 'QuData'.
  4. 出列

这是我使用的代码的简化版。

    //The method DataEqual is to determine that the data in the two BYTE[] are the same.

    bool isSend = false;
    private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        if(isSend)
            return;
        byte[] Cmd_Report = new byte[serialPort.BytesToRead];
        serialPort.Read(Cmd_Report, 0, serialPort.BytesToRead);

        if (DataEqual(Cmd_Report, CMD_REPORT))
        {
            //var data = QuData.Dequeue();
            //do something X about data then save data
        }
    }

    public void DoWork()
    {
        //do something 
        byte[] Cmd_Req = new byte[10];
        byte[] Cmd_Res = new byte[CMD_RES.Length];
        isSend = true;
        serialPort.Write(Cmd_Req, 0, Cmd_Req.Length );
        stopWatch.Restart();
        while(serialPort.BytesToRead < CMD_RES.Length &&  stopWatch.ElapsedMilliseconds< 1000)
            System.Threading.Thread.Sleep(50);

        int resCount = 0;
        if (serialPort.BytesToRead < CMD_RES.Length)
            resCount = serialPort.Read(Cmd_Res, 0, serialPort.BytesToRead);
        else
            resCount = serialPort.Read(Cmd_Res, 0, CMD_RES.Length);
        isSend = false;

        if (DataEqual(Cmd_Res, CMD_RES))
        {
            //create data, do something A about data
            //QuData.Enqueue(data)
        }
        else
        {
            //do something B
        }
    }  

我的问题:

  1. DoWork 和 'Op_A' 并不总是按顺序执行。有时我需要在几次之后调用DoWork并等待'Op_A',然后在用户执行'Op_A'时继续调用DoWork。所以我要知道Cmd_Req是否成功,判断设备是否报Cmd_Report。但是在频繁读写的时候,有时在SerialPort_DataReceived方法中会读到Cmd_Res,而在DoWork方法中会读到Cmd_Report。这会导致我认为Cmd_Req没有执行成功或者设备没有报告Cmd_Result。我该如何解决?
  2. 我的方法错了吗?什么是正确的做法?

编辑

'OP_A'是按下设备上的按钮的动作。

根据您的情况,您希望按顺序访问串行端口(一次一个)。我从我的旧项目中提取了这段代码,应该对你有帮助。

using System;
using System.IO.Ports;
using System.Linq;
using System.Threading;

public class SPHandler
{
    /// <summary>
    /// Your serial port
    /// </summary>
    private SerialPort _serialPort;
    private int _timeOut, _timeOutDefault;
    private AutoResetEvent _receiveNow;
    /// <summary>
    /// Possible device end responses such as \r\nOK\r\n, \r\nERROR\r\n, etc.
    /// </summary>
    private string[] _endResponses;

    public SPHandler()
    {
    }

    public void SetPort(string portName, int baudRate, int timeOut, string[] endResponses = null)
    {
        _timeOut = timeOut;
        _timeOutDefault = timeOut;
        _serialPort = new SerialPort(portName, baudRate);
        _serialPort.Parity = Parity.None;
        _serialPort.Handshake = Handshake.None;
        _serialPort.DataBits = 8;
        _serialPort.StopBits = StopBits.One;
        _serialPort.RtsEnable = true;
        _serialPort.DtrEnable = true;
        _serialPort.WriteTimeout = _timeOut;
        _serialPort.ReadTimeout = _timeOut;

        if (endResponses == null)
            _endResponses = new string[0];
        else
            _endResponses = endResponses;
    }

    public bool Open()
    {
        try
        {
            if (_serialPort != null && !_serialPort.IsOpen)
            {
                _receiveNow = new System.Threading.AutoResetEvent(false);
                _serialPort.Open();
                _serialPort.DataReceived += new SerialDataReceivedEventHandler(_serialPort_DataReceived);
                return true;
            }
            else
            {
                return false;
            }
        }
        catch
        {
            return false;
        }
    }

    private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        try
        {
            if (e.EventType == SerialData.Chars)
            {
                _receiveNow.Set();
            }
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    public bool Close()
    {
        try
        {
            if (_serialPort != null && _serialPort.IsOpen)
            {
                _serialPort.Close();
                return true;
            }
            else
            {
                return false;
            }
        }
        catch
        {
            return false;
        }
    }

    public string ExecuteCommand(string cmd)
    {
        _serialPort.DiscardOutBuffer();
        _serialPort.DiscardInBuffer();
        _receiveNow.Reset();
        _serialPort.Write(cmd); // Sometimes  + "\r" is needed. Depends on the device

        string input = ReadResponse(); // Returns device response whenever you execute a command

        _timeOut = _timeOutDefault;

        return input;
    }

    private string ReadResponse()
    {
        string buffer = string.Empty;
        try
        {
            do
            {
                if (_receiveNow.WaitOne(_timeOut, false))
                {
                    string t = _serialPort.ReadExisting();
                    buffer += t;
                }

            } while (!_endResponses.Any(r => buffer.EndsWith(r, StringComparison.OrdinalIgnoreCase))); // Read while end responses are not yet received
        }
        catch
        {
            buffer = string.Empty;
        }
        return buffer;
    }
}

用法:

SPHandler spHandler = new SPHandler();

spHandler.SetPort(params);
spHandler.Open();

string response = spHandler.ExecuteCommand("Cmd_Req");

if (response == "Cmd_Res")
{
    // Inform user that operation OP_A is allowed
    // Enqueue data
}
else
{
    // Oh no! Cmd_Res failed?!
}

// ... etc.

spHandler.Close(); // If you need to

基本上你在执行命令后立即等待响应。这个 SPHandler 的问题是它需要一个字符串命令和响应。您可以将其转换为 read/send 字节。

您应该注意用于顺序访问的AutoResetEvent。即使你使用多线程,它也会相应地处理。对 _receiveNow.WaitOne 的调用为您带来了魔力。 也许 这就是您需要在代码中应用的所有内容,因为您当前的问题是同时读取 Cmd_ResCmd_Req

编辑:

回顾一下你的代码。您可以简单地取消注册 SerialPort_DataReceived,因为您只需要 DoWork 来顺序执行命令和处理响应。

受到JohnEphraimTugado代码的启发,想到了解决方案,并在应用场景中通过了测试

using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;

public class SerialPortHandler
{
    public delegate void OnReportHandler(byte[] data);
    public delegate void OnReadExceptionHander(Exception error);
    public delegate void OnHandlingExceptionHandler(Exception error);

    public SerialPortHandler(string portName, Predicate<byte[]> reportPredicate, Func<Queue<byte>, byte[]> dequeueFunc)
        : this(reportPredicate, dequeueFunc)
    {
        this._serialPort = new SerialPort(portName);
    }

    public SerialPortHandler(string portName, int baudRate, Predicate<byte[]> reportPredicate, Func<Queue<byte>, byte[]> dequeueFunc)
       : this(reportPredicate, dequeueFunc)
    {
        this._serialPort = new SerialPort(portName, baudRate);
    }

    public SerialPortHandler(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Predicate<byte[]> reportPredicate, Func<Queue<byte>, byte[]> dequeueFunc)
      : this(reportPredicate, dequeueFunc)
    {
        this._serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
    }

    private SerialPortHandler(Predicate<byte[]> reportPredicate, Func<Queue<byte>, byte[]> dequeueFunc)
    {
        _thrdRead = new Thread(new ThreadStart(Read));
        _thrdHandle = new Thread(new ThreadStart(DataHandling));
        _isRun = false;
        _quCmdRespone = new Queue<byte[]>();
        _quReceiveBuff = new Queue<byte>();
        _cmdResponseReset = new AutoResetEvent(false);
        _reportPredicate = reportPredicate;
        _dequeueFunc = dequeueFunc;
    }

    SerialPort _serialPort;
    Thread _thrdRead;
    Thread _thrdHandle;
    bool _isRun;
    /// <summary>
    /// Save all data read from the serial port
    /// </summary>
    Queue<byte> _quReceiveBuff;
    /// <summary>
    /// Save the response of the last command
    /// </summary>
    Queue<byte[]> _quCmdRespone;
    AutoResetEvent _cmdResponseReset;
    bool _isSending;
    /// <summary>
    /// A method to determine whether a byte[] is a spontaneous report of a serial port
    /// </summary>
    Predicate<byte[]> _reportPredicate;
    /// <summary>
    /// Dequeuing a command from the received data queue method
    /// </summary>
    Func<Queue<byte>, byte[]> _dequeueFunc;

    /// <summary>
    /// Called when the serial interface is actively reporting data.
    /// </summary>
    public event OnReportHandler OnReport;
    public event OnReadExceptionHander OnReadException;
    public event OnHandlingExceptionHandler OnHandlingException;

    public bool IsOpen
    {
        get { return this._serialPort == null ? false : this._serialPort.IsOpen; }
    }

    /// <summary>
    /// Read data from serial port.
    /// </summary>
    private void Read()
    {
        while (_isRun)
        {
            try
            {
                if (this._serialPort == null || !this._serialPort.IsOpen || this._serialPort.BytesToRead == 0)
                {
                    SpinWait.SpinUntil(() => this._serialPort != null && this._serialPort.IsOpen && this._serialPort.BytesToRead > 0, 10);
                    continue;
                }
                byte[] data = new byte[this._serialPort.BytesToRead];
                this._serialPort.Read(data, 0, data.Length);
                Array.ForEach(data, b => _quReceiveBuff.Enqueue(b));
            }
            catch (InvalidOperationException)
            {
                if (!_isRun || this._serialPort ==null)
                    return;
                else
                    this._serialPort.Open();
            }
            catch (Exception ex)
            {
                this.OnReadException?.BeginInvoke(new Exception(string.Format("An error occurred in the reading processing: {0}", ex.Message), ex), null, null);
            }
        }
    }

    /// <summary>
    /// Data processing
    /// </summary>
    private void DataHandling()
    {
        while (_isRun)
        {
            try
            {
                if (_quReceiveBuff.Count == 0)
                {
                    SpinWait.SpinUntil(() => _quReceiveBuff.Count > 0, 10);
                    continue;
                }
                byte[] data = _dequeueFunc(_quReceiveBuff);
                if (data == null || data.Length == 0)
                {
                    SpinWait.SpinUntil(() => false, 10);
                    continue;
                }

                if (_reportPredicate(data))
                    OnReport?.BeginInvoke(data, null, null);    //If the data is spontaneously reported by the serial port, the OnReport event is called
                else
                {                                               //If the command response returned by the serial port, join the command response queue
                    if (_quCmdRespone.Count > 0)
                        _quCmdRespone.Clear();                  //The queue is cleared to ensure that if a command timed out does not affect subsequent command results

                    _quCmdRespone.Enqueue(data);
                    _cmdResponseReset.Set();
                }
            }
            catch (Exception ex)
            {
                this.OnHandlingException?.BeginInvoke(new Exception(string.Format("An error occurred in the data processing: {0}", ex.Message), ex), null, null);
            }
        }
    }

    /// <summary>
    /// Read the response of the last command.
    /// </summary>
    /// <param name="timeOut"></param>
    /// <returns></returns>
    private byte[] ReadCommandResponse(int timeOut)
    {
        byte[] buffer = null;
        if (_cmdResponseReset.WaitOne(timeOut, false))
            buffer = _quCmdRespone.Dequeue();
        return buffer;
    }

    /// <summary>
    /// Send a command
    /// </summary>
    /// <param name="sendData">command buff</param>
    /// <param name="receiveData">REF: response of command</param>
    /// <param name="timeout">timeout(millseconds)</param>
    /// <returns>count of response, -1: failure, -2: port is busy</returns>
    public int SendCommand(byte[] sendData, ref byte[] receiveData, int timeout)
    {
        if (_isSending)
            return -2;
        if (this._serialPort.IsOpen)
        {
            try
            {
                _isSending = true;
                _cmdResponseReset.Reset();  //update 11-13
                this._serialPort.Write(sendData, 0, sendData.Length);
                int ret = 0;
                receiveData = ReadCommandResponse(timeout);
                ret = receiveData == null ? -1 : receiveData.Length;
                return ret;
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Send command is failure:{0}", ex.Message), ex);
            }
            finally
            {
                _isSending = false;
            }
        }
        return -1;
    }

    public bool Open()
    {

        if (this._serialPort == null || this._serialPort.IsOpen)
            return false;
        this._serialPort.Open();
        _isRun = true;
        _thrdRead.Start();
        _thrdHandle.Start();
        return true;
    }

    public bool Close()
    {
        _isRun = false;
        if (_thrdHandle.IsAlive)
            _thrdHandle.Join();
        if (_thrdRead.IsAlive)
            _thrdRead.Join();
        if (this._serialPort == null)
            return false;
        if (this._serialPort.IsOpen)
            this._serialPort.Close();
        return true;
    }
}

用法

    SerialPortHandler spHandler;
    public void Init()
    {
        SerialPortHandler spHandler = new SerialPortHandler("COM1", IsReport, DequeueResponse);
        spHandler.OnReport += SpHandler_OnReport;
    }

    bool IsReport(byte[] data)
    {
        //Determines whether the command is Cmd_Reprot
        return true;
    }

    byte[] DequeueResponse(Queue<byte> quReceive)
    {
        byte[] response = null;
        //Dequeuing a valid response based protocol rules
        return response;
    }

    private void SpHandler_OnReport(byte[] data)
    {
        if (DataEqual(Cmd_Report, CMD_REPORT))
        {
            //do something X about data then save data
        }
    }

    public void DoWork()
    {
        //do something 
        byte[] Cmd_Req = null;
        byte[] Cmd_Res = new byte[CMD_RES.Length];
        int ret = spHandler.SendCommand(Cmd_Req, Cmd_Req, 1000);

        if (ret > 0 && DataEqual(Cmd_Res, CMD_RES))
        {
            //create data, do something A about data
        }
        else
        {
            //do something B
        }
    }

nuget 包

Install-Package SerialHandler -Version 0.0.1-beta3