C# SerialPort多线程发送/接收冲突

huangapple go评论81阅读模式
英文:

C# SerialPort multithread send/receive collisions

问题

我正在尝试在C#命令行应用程序(状态机)和通过SerialPort连接的外部设备之间进行通信。我有一个.NET Core 3.1.0应用程序,并且正在使用System.IO.Ports(4.7.0)。我正在使用FTDI USB-serial适配器。

我必须监视通过发送到计算机的数据指示的设备状态,并根据设备状态回复命令以进入下一个状态。

当使用Putty发送和接收时,没有问题

当从键盘使用命令行作为输入并输出时,没有问题

不幸的是,当从另一个线程向设备发送数据时,似乎两个端点同时发送,并发生冲突,导致我的目标崩溃。

初始化:

_port = new SerialPort(
    Parameters.TelnetCOMport,
    Parameters.TelnetBaudrate,
    Parameters.TelnetParity,
    Parameters.TelnetDataBits,
    Parameters.TelnetStopBits);
_port.DataReceived += Port_DataReceived;
_port.ErrorReceived += Port_ErrorReceived;
_port.Handshake = Handshake.RequestToSend;
_port.RtsEnable = true;
_port.DtrEnable = true;
_port.Encoding = Encoding.ASCII;
_port.Open();

发送命令方法:

private void Send_Command(string command)
{
    lock (_portLock)
    {
        _port.Write(command + "\n");
    }
}

数据接收事件:

private string _dataReceived;

private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    lock (_portLock)
    {
        byte[] data = new byte[_port.BytesToRead];
        _port.Read(data, 0, data.Length);
        _dataReceived += Encoding.UTF8.GetString(data);
        ProcessDataReceived();
    }
}

我尝试使用锁来避免这种情况,但没有帮助。唯一有帮助的是在接收到一个状态指示器后添加一个很长的延迟,以确保在发送新命令之前设备肯定不会发送任何内容。

因此,我的问题是:
如何在多线程应用程序中通过SerialPort发送数据时避免冲突?

英文:

I am trying to communicate between a C# commandline application (State machine) and an external device connected via SerialPort. I have a .Net Core 3.1.0 application and am using Sytem.IO.Ports (4.7.0). I am using a FTDI USB-serial adapter.

I have to monitor the device state indicated by data send to my computer and reply with commands depending on the device state to get to the next state.

When using Putty send and receive works without problem.

When using the command line as input from keyboard and output it works without problem.

Unfortunately when sending data from another thread to the device it seems that both sites send at the same time and a collision occurs that crashes my target.

    Initialization:
    _port = new SerialPort(
    				Parameters.TelnetCOMport,
    				Parameters.TelnetBaudrate,
    				Parameters.TelnetParity,
    				Parameters.TelnetDataBits,
    				Parameters.TelnetStopBits);
    			_port.DataReceived += Port_DataReceived;
    			_port.ErrorReceived += Port_ErrorReceived;
    			_port.Handshake = Handshake.RequestToSend;
    			_port.RtsEnable = true;
    			_port.DtrEnable = true;
    			_port.Encoding = Encoding.ASCII;
    			_port.Open();

   private void Send_Command(string command)
    {
        lock (_portLock)
        {
            _port.Write(command + "\n");
        }
    }

    private string _dataReceived;

    private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        lock (_portLock)
        {
            byte[] data = new byte[_port.BytesToRead];
            _port.Read(data, 0, data.Length);
            _dataReceived += Encoding.UTF8.GetString(data);
            ProcessDataReceived();
        }
    }

I tried to avoid this situation with a lock but it did not help. The only thing that helped was to add a very long delay after receiving one status indicator to make sure that definetly nothing will be send by the device before sending a new command.

So my question is:
How to avoid a collision while sending data over SerialPort from a multithread application?

答案1

得分: 2

感谢您对我的问题进行评论!

结果证明,错误是因为.NET Framework的System.IO.Ports类质量非常差而发生的。

由于System.IO.Ports类不正确处理清除发送(CTS),因此可能会发生发送/接收冲突。

我将引用Ben Voigts非常好的文章

> 附带的System.IO.Ports.SerialPort类是一个明显的例外。委婉地说,它是由远离他们核心竞争力领域的计算机科学家设计的。他们既不了解串行通信的特性,也不了解常见的用例,这一点表现得很明显。此外,在发布之前,它不可能在任何真实世界的场景中进行测试,而不会发现这些问题,这些问题存在于文档界面和未记录的行为中,使得使用System.IO.Ports.SerialPort进行可靠通信成为一场真正的噩梦。(有大量证据表明这一点,从在Hyperterminal中工作但在.NET中不工作的设备到StackOverflow上的问题都可以证明这一点...
>
>最严重的System.IO.Ports.SerialPort成员,不仅不应该使用,而且是深层次代码异味的标志,需要重新架构所有IOPSP用法:
>
> - DataReceived事件(100%冗余,也完全不可靠)
> - BytesToRead属性(完全不可靠)
> - Read、ReadExisting、ReadLine方法(错误地处理错误,而且是同步的)
> - PinChanged事件(与您可能想知道的每个有趣的事情的顺序都不一致)

为了解决这个问题,我编写了自己的SerialPorts类,具体如下:

  1. 我阅读了这篇关于MSDN串行通信的完美文章
  2. 我从github下载了示例MTTTY代码
  3. 我将MTTTY示例重新设计为.dll文件,以便可以从C#中调用
  4. 我学会了如何从C#中调用回调函数
  5. 我将MTTTY示例重新设计为运行一个持续监听的ReaderThread和一个可以通过UDP数据报调用的WriterThread

C#代码如下所示:

    #region 加载C DLL
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate void ProgressCallback(IntPtr buffer, int length);

    [DllImport("mttty.dll")]
    static extern void ConnectToSerialPort([MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback telnetCallbackPointer,
        [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback statusCallbackPointer,
        [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback errorCallbackPointer);
    #endregion

    #region 回调函数 
    private void TelnetCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);
        Console.WriteLine(Encoding.UTF8.GetString(safeBuffer));
    }

    private void StatusCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = a byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);            
        Console.WriteLine("Status: "+Encoding.UTF8.GetString(safeBuffer));
    }

    private void ErrorCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);
        Console.WriteLine("Error: "+Encoding.UTF8.GetString(safeBuffer));
    }
    #endregion

然后通过以下方式调用监听线程

    private static Socket _sock;
    private static IPEndPoint _endPoint;

    public StartSerialPortListenerThread()
    {
        // 这个套接字用于向C dll中的线程发送写请求
        _sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        IPAddress serverAddr = IPAddress.Parse("127.0.0.1");
        _endPoint = new IPEndPoint(serverAddr, 5555);
        
        // 这启动了C dll中的监听线程
        Task.Factory.StartNew(() =>
        {
            ConnectToSerialPort(TelnetCallback, StatusCallback, ErrorCallback);
        });
    }

通过发送UDP数据报非常容易调用发送命令

 private void SendCommand(string command)
    {
        byte[] sendBuffer = Encoding.ASCII.GetBytes(command + '\n');
        _sock.SendTo(sendBuffer, _endPoint);
    }

我只是小心翼翼地修改了MTTTY示例,所以我的DLL与示例非常相似(非常可靠!)。

我还没有公开这段代码,因为我需要做一些清理。如果您希望尽快了解当前状态,我可以根据请求将其发送给任何人。

英文:

Thank you for commenting my question!

It turned out, that the error occurs because of the very bad quality of the System.IO.Ports class of the .NET Framework.

As the System.IO.Ports class does not handle the Clear-To-Send (CTS) correctly and therefore colliding send/receive could occur.

I will cite Ben Voigts very good article:

> The System.IO.Ports.SerialPort class which ships with .NET is a glaring exception. To put it mildly, it was designed by computer scientists operating far outside their area of core competence. They neither understood the characteristics of serial communication, nor common use cases, and it shows. Nor could it have been tested in any real world scenario prior to shipping, without finding flaws that litter both the documented interface and the undocumented behavior and make reliable communication using System.IO.Ports.SerialPort (henceforth IOPSP) a real nightmare. (Plenty of evidence on StackOverflow attests to this, from devices that work in Hyperterminal but not .NET...
>
>The worst offending System.IO.Ports.SerialPort members, ones that not only should not be used but are signs of a deep code smell and the need to rearchitect all IOPSP usage:
>
> - The DataReceived event (100% redundant, also completely unreliable)
> - The BytesToRead property (completely unreliable)
The Read, ReadExisting, ReadLine methods (handle errors completely wrong, and are synchronous)
> - The PinChanged event (delivered out of order with respect to every interesting thing you might want to know about it)

To work around this issue I have written my own SerialPorts class by doing the following:

  1. I read this perfect article about serial communication from MSDN
  2. I downloaded the example MTTTY code from github
  3. I redesigned the MTTTY example as a .dll so it can be invoked from C#
  4. I learned how to invoke callbacks in C# from C
  5. I have redesigned the MTTTY example to run a ReaderThread that listens continously and a WriterThread that can be invoked via an UDP datagram

The C# code looks as below:

    #region Load C DLL
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate void ProgressCallback(IntPtr buffer, int length);

    [DllImport("mttty.dll")]
    static extern void ConnectToSerialPort([MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback telnetCallbackPointer,
        [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback statusCallbackPointer,
        [MarshalAs(UnmanagedType.FunctionPtr)] ProgressCallback errorCallbackPointer);
    #endregion

    #region Callbacks 
    private void TelnetCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);
        Console.WriteLine(Encoding.UTF8.GetString(safeBuffer));
    }

    private void StatusCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);            
        Console.WriteLine("Status: "+Encoding.UTF8.GetString(safeBuffer));
    }

    private void ErrorCallback(IntPtr unsafeBuffer, int length)
    {
        byte[] safeBuffer = new byte[length];
        Marshal.Copy(unsafeBuffer, safeBuffer, 0, length);
        Console.WriteLine("Error: "+Encoding.UTF8.GetString(safeBuffer));
    }
    #endregion

The listener thread then is invoked as below

    private static Socket _sock;
    private static IPEndPoint _endPoint;

    public StartSerialPortListenerThread()
    {
        // This socket is used to send Write request to thread in C dll
        _sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        IPAddress serverAddr = IPAddress.Parse("127.0.0.1");
        _endPoint = new IPEndPoint(serverAddr, 5555);
        
        // This starts the listener thread in the C dll
        Task.Factory.StartNew(() =>
        {
            ConnectToSerialPort(TelnetCallback, StatusCallback, ErrorCallback);
        });
    }

The send command is invoked very easily by sending a UDP datagram

 private void SendCommand(string command)
    {
        byte[] sendBuffer = Encoding.ASCII.GetBytes(command + '\n');
        _sock.SendTo(sendBuffer, _endPoint);
    }

I only have touched the MTTTY example very carefully so my DLL works very similiar to the example (very reliable!).

I have not made this code available so far because I have to do some cleanup. If you want to get the status quo asap I can send it to anyone by request.

huangapple
  • 本文由 发表于 2020年1月3日 19:19:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/59577676.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定