Reputation: 149
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
Reputation: 149
Thank you for commenting my question!
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)
#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
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);
});
}
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