MaxWolodin
MaxWolodin

Reputation: 149

C# SerialPort multithread send/receive collisions

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?

Upvotes: 3

Views: 4110

Answers (1)

MaxWolodin
MaxWolodin

Reputation: 149

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.

Upvotes: 2

Related Questions