簡體   English   中英

如何完美處理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'時,設備返回'Cmd_Res',用戶可以執行'Op_A'。
  2. 如果設備正確返回'Cmd_Res',則提示用戶可以執行'Op_A'和一個數據入隊到'QuData'。
  3. 當用戶執行'Op_A'時,設備報告'Cmd_Report'。 如果設備正確地報告了“Cmd_Report”,則有一個數據從“QuData”中出列。

這是我使用的代碼的簡化。

    //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問題在於它需要一個字符串命令和響應。 您可以將其轉換為讀取/發送字節。

您應該注意用於順序訪問的AutoResetEvent 即使您使用多線程,也會相應地進行處理。 _receiveNow.WaitOne的調用為您_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 包

安裝包 SerialHandler -版本 0.0.1-beta3

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM