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

时间:2017-10-23 05:55:28

标签: c# .net .net-4.0 serial-port

代码在.Net4上运行。

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

  1. 当写入'Cmd_Req'时,设备返回'Cmd_Res',用户可以执行'Op_A'。
  2. 如果设备正确返回'Cmd_Res',则提示用户可以执行'Op_A'并将一个数据排入'QuData'。
  3. 当用户执行'Op_A'时,设备会报告'Cmd_Report'。如果设备正确重新打印'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. 我的方式错了吗?什么是正确的做法?
    3. 修改

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

2 个答案:

答案 0 :(得分:2)

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

{
   "tokens": [
      {
         "token": "tshirts",
         "start_offset": 10,
         "end_offset": 18,
         "type": "<ALPHANUM>",
         "position": 2
      }
   ]
}

<强>用法

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;
    }
}

答案 1 :(得分:1)

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

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的问题是它需要一个字符串命令和响应。您可以将其转换为读取/发送字节。

您应该注意用于顺序访问的AutoResetEvent。即使您使用多线程,也会相应地进行处理。对_receiveNow.WaitOne的调用对你来说是神奇的。 可能这是您在代码中应用的全部内容,因为您当前的问题是同时阅读Cmd_ResCmd_Req

修改

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