Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# - How to hand off which thread reads from serial port?

Background

A customer asked me to find out why their C# application (we'll call it XXX, delivered by a consultant who has fled the scene) is so flaky, and fix it. The application controls a measurement device over a serial connection. Sometimes the device delivers continuous readings (which are displayed on screen), and sometimes the app needs to stop continuous measurements and go into command-response mode.

How NOT to do it

For continuous measurements, XXX uses System.Timers.Timer for background processing of serial input. When the timer fires, C# runs the timer's ElapsedEventHandler using some thread from its pool. XXX's event handler uses a blocking commPort.ReadLine() with a several second timeout, then calls back to a delegate when a useful measurement arrives on the serial port. This portion works fine, however...

When its time to stop realtime measurements and command the device to do something different, the application tries to suspend background processing from the GUI thread by setting the timer's Enabled = false. Of course, that just sets a flag preventing further events, and a background thread already waiting for serial input continues waiting. The GUI thread then sends a command to the device, and tries to read the reply – but the reply is received by the background thread. Now the background thread becomes confused as its not the expected measurement. The GUI thread meanwhile becomes confused as it didn't receive the command reply expected. Now we know why XXX is so flaky.

Possible Method 1

In another similar application, I used a System.ComponentModel.BackgroundWorker thread for free-running measurements. To suspend background processing I did two things in the GUI thread:

  1. call the CancelAsync method on the thread, and
  2. call commPort.DiscardInBuffer(), which causes a pending (blocked, waiting) comport read in the background thread to throw a System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n".

In the background thread I catch this exception and clean up promptly, and all works as intended. Unfortunately DiscardInBuffer provoking the exception in another thread's blocking read is not documented behavior anywhere I can find, and I hate relying on undocumented behavior. It works because internally DiscardInBuffer calls the Win32 API PurgeComm, which interrupts the blocking read (documented behavior).

Possible Method 2

Directly use the BaseClass Stream.ReadAsync method, with a monitor cancellation token, using a supported way of interrupting the background IO.

Because the number of characters to be received is variable (terminated by a newline), and no ReadAsyncLine method exists in the framework, I don't know if this is possible. I could process each character individually but would take a performance hit (might not work on slow machines, unless of course the line-termination bit is already implemented in C# within the framework).

Possible Method 3

Create a lock "I've got the serial port". Nobody reads, writes, or discards input from the port unless they have the lock (including repeating the blocking read in background thread). Chop the timeout values in the background thread to 1/4 second for acceptable GUI responsiveness without too much overhead.

Question

Does anybody have a proven solution to deal with this problem? How can one cleanly stop background processing of the serial port? I've googled and read dozens of articles bemoaning the C# SerialPort class, but haven't found a good solution.

Thanks in advance!

like image 584
Dave Nadler Avatar asked Oct 31 '22 21:10

Dave Nadler


1 Answers

MSDN article for the SerialPort Class clearly states:

If a SerialPort object becomes blocked during a read operation, do not abort the thread. Instead, either close the base stream or dispose of the SerialPort object.

So the best approach, from my point of view, is second one, with async reading and step by step checking for the line-ending character. As you've stated, the check for each char is very big performance loss, I suggest you to investigate the ReadLine implementation for some ideas how to perform this faster. Note that they use NewLine property of SerialPort class.

I want also to note that there is no ReadLineAsync method by default as the MSDN states:

By default, the ReadLine method will block until a line is received. If this behavior is undesirable, set the ReadTimeout property to any non-zero value to force the ReadLine method to throw a TimeoutException if a line is not available on the port.

So, may be, in your wrapper you can implement similar logic, so your Task will cancel if there is no line end in some given time. Also, you should note this:

Because the SerialPort class buffers data, and the stream contained in the BaseStream property does not, the two might conflict about how many bytes are available to read. The BytesToRead property can indicate that there are bytes to read, but these bytes might not be accessible to the stream contained in the BaseStream property because they have been buffered to the SerialPort class.

So, again, I suggest you to implement some wrapper logic with asynchronous read and checking after each read, are there line-end or not, which should be blocking, and wrap it inside async method, which will cancel Task after some time.

Hope this helps.

like image 183
VMAtm Avatar answered Nov 12 '22 21:11

VMAtm