C # - How to pass which stream is read from a serial port?

Background

The client asked me to find out why their application in C # (we will call it XXX, delivered by the consultant who left the scene) is so flaky and fixed it. The application controls the measuring device through a serial connection. Sometimes the device provides continuous reading (which is displayed on the screen), and sometimes the application must 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 sequential input. When the timer fires, C # starts the ElapsedEventHandler timer, using some thread from its pool. The XXX event handler uses commPort.ReadLine() lock with a few seconds timeout, and then calls the delegate when a useful measurement arrives at the serial port. This part works fine, however ...

When its time to stop real-time measurements and force the device to do something else, the application tries to pause background processing from the GUI stream by setting the timer Enabled = false . Of course, this simply sets a flag to prevent further events, and the background thread, already waiting for sequential input, continues to wait. Then the GUI thread sends a command to the device and tries to read the response - but the response is received by the background thread. Now the background thread gets tangled up as its not expected dimension. Meanwhile, the GUI thread is getting confused as it did not receive the expected command. Now we know why XXX is so flaky.

Possible Method 1

In another similar application, I used the System.ComponentModel.BackgroundWorker stream for measurements without participation. To pause background editing, I did two things in the GUI thread:

  • calling the CancelAsync method in the stream and
  • call commPort.DiscardInBuffer() , which causes the pending (blocked, pending) comport to read in the background thread to throw 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 quickly clear it, and everything works as intended. Unfortunately, DiscardInBuffer , which DiscardInBuffer an exception in another read lock chain, does not document 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 lock reading (documented behavior).

Possible Method 2

Directly use the BaseClass Stream.ReadAsync method with a monitor cancellation token, using the supported background I / O interrupt method.

Since the number of characters to be received is a variable (terminated by a new line) and there is no ReadAsyncLine method in the structure, I do not know if this is possible. I could process each character individually, but it could hit performance (it may 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 "I have a serial port" lock. No one reads, writes, or drops input from the port unless they have a lock (including repeating a lock read in the background thread). Change the timeout value in the background thread to 1/4 second for an acceptable GUI response without too much overhead.

Question

Does anyone have a proven solution to solve this problem? How can I stop background processing of a serial port completely? I googled and read dozens of articles that mourn the C # SerialPort class, but didn't find a good solution.

Thanks in advance!

+5
source share
2 answers

MSDN Article for SerialPort The class clearly states:

If the SerialPort object is blocked during a read operation, do not interrupt the stream . Instead, close the underlying thread or delete the SerialPort object .

Thus, the best approach, from my point of view, is the second one, when reading async and step-by-step checking the line termination character. As you said, checking for each char is a very big performance loss, I suggest you study the ReadLine implementation for some ideas on how to do this faster. Note that they use the NewLine property of the SerialPort class.

I also want to note that by default ReadLineAsync does not exist as indicated on MSDN :

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

So, maybe in your shell you can implement similar logic, so your Task will cancel if there is no end of line at some point in time. In addition, you should note this:

Since the SerialPort data buffers the class and the stream contained in the BaseStream property, BaseStream are no two conflicts that can have many bytes available. The BytesToRead property may indicate that there are bytes to read, but these bytes may not be available for the stream contained in the BaseStream property , because they are buffered to the SerialPort class.

So, again, I suggest that you implement some kind of shell logic with asynchronous reading and checking after each reading whether there is a line end or not, which should lock and wrap it inside the async method, which will cancel Task after some time.

Hope this helps.

+2
source

OK, here's what I did ... Comments would be appreciated, since C # is still somewhat new to me!

Its crazy to have multiple threads trying to access the serial port (or any resource, especially an asynchronous resource) at the same time. To fix this application without completely rewriting it, I entered a SerialPortLockObject lock to guarantee exclusive access to the serial port as follows:

  • The GUI thread contains a SerialPortLockObject , unless the background is running.
  • The SerialPort class is wrapped so that any read or write by a stream that does not contain a SerialPortLockObject throws an exception (it helps to find several competing errors).
  • The timer class is wrapped (the SerialOperationTimer class), so the desktop function is called scribble, acquiring SerialPortLockObject . SerialOperationTimer allows you to start only one timer at a time (it helped to find several errors when the GUI forgot to stop background processing before starting another timer). This can be improved by using a specific thread for the timer to work, and this thread holds the lock for as long as the timer is active (but will work even more, since the encoded System.Timers.Timer acts as a worker from the thread pool).
  • When SerialOperationTimer is stopped, it turns off the main timer and flushes the serial port buffers (causing an exception to be made from any blocked serial port operation, as explained in possible method 1 above). Then SerialPortLockObject restored by the GUI thread.

Here's the wrapper for SerialPort :

 /// <summary> CheckedSerialPort class checks that read and write operations are only performed by the thread owning the lock on the serial port </summary> // Just check reads and writes (not basic properties, opening/closing, or buffer discards). public class CheckedSerialPort : SafePort /* derived in turn from SerialPort */ { private void checkOwnership() { try { if (Monitor.IsEntered(XXX_Conn.SerialPortLockObject)) return; // the thread running this code has the lock; all set! // Ooops... throw new Exception("Serial IO attempted without lock ownership"); } catch (Exception ex) { StringBuilder sb = new StringBuilder(""); sb.AppendFormat("Message: {0}\n", ex.Message); sb.AppendFormat("Exception Type: {0}\n", ex.GetType().FullName); sb.AppendFormat("Source: {0}\n", ex.Source); sb.AppendFormat("StackTrace: {0}\n", ex.StackTrace); sb.AppendFormat("TargetSite: {0}", ex.TargetSite); Console.Write(sb.ToString()); Debug.Assert(false); // lets have a look in the debugger NOW... throw; } } public new int ReadByte() { checkOwnership(); return base.ReadByte(); } public new string ReadTo(string value) { checkOwnership(); return base.ReadTo(value); } public new string ReadExisting() { checkOwnership(); return base.ReadExisting(); } public new void Write(string text) { checkOwnership(); base.Write(text); } public new void WriteLine(string text) { checkOwnership(); base.WriteLine(text); } public new void Write(byte[] buffer, int offset, int count) { checkOwnership(); base.Write(buffer, offset, count); } public new void Write(char[] buffer, int offset, int count) { checkOwnership(); base.Write(buffer, offset, count); } } 

And here is the wrapper for System.Timers.Timer :

 /// <summary> Wrap System.Timers.Timer class to provide safer exclusive access to serial port </summary> class SerialOperationTimer { private static SerialOperationTimer runningTimer = null; // there should only be one! private string name; // for diagnostics // Delegate TYPE for user callback function (user callback function to make async measurements) public delegate void SerialOperationTimerWorkerFunc_T(object source, System.Timers.ElapsedEventArgs e); private SerialOperationTimerWorkerFunc_T workerFunc; // application function to call for this timer private System.Timers.Timer timer; private object workerEnteredLock = new object(); private bool workerAlreadyEntered = false; public SerialOperationTimer(string _name, int msecDelay, SerialOperationTimerWorkerFunc_T func) { name = _name; workerFunc = func; timer = new System.Timers.Timer(msecDelay); timer.Elapsed += new System.Timers.ElapsedEventHandler(SerialOperationTimer_Tick); } private void SerialOperationTimer_Tick(object source, System.Timers.ElapsedEventArgs eventArgs) { lock (workerEnteredLock) { if (workerAlreadyEntered) return; // don't launch multiple copies of worker if timer set too fast; just ignore this tick workerAlreadyEntered = true; } bool lockTaken = false; try { // Acquire the serial lock prior calling the worker Monitor.TryEnter(XXX_Conn.SerialPortLockObject, ref lockTaken); if (!lockTaken) throw new System.Exception("SerialOperationTimer " + name + ": Failed to get serial lock"); // Debug.WriteLine("SerialOperationTimer " + name + ": Got serial lock"); workerFunc(source, eventArgs); } finally { // release serial lock if (lockTaken) { Monitor.Exit(XXX_Conn.SerialPortLockObject); // Debug.WriteLine("SerialOperationTimer " + name + ": released serial lock"); } workerAlreadyEntered = false; } } public void Start() { Debug.Assert(Form1.GUIthreadHashcode == Thread.CurrentThread.GetHashCode()); // should ONLY be called from GUI thread Debug.Assert(!timer.Enabled); // successive Start or Stop calls are BAD Debug.WriteLine("SerialOperationTimer " + name + ": Start"); if (runningTimer != null) { Debug.Assert(false); // Lets have a look in the debugger NOW throw new System.Exception("SerialOperationTimer " + name + ": Attempted 'Start' while " + runningTimer.name + " is still running"); } // Start background processing // Release GUI thread lock on the serial port, so background thread can grab it Monitor.Exit(XXX_Conn.SerialPortLockObject); runningTimer = this; timer.Enabled = true; } public void Stop() { Debug.Assert(Form1.GUIthreadHashcode == Thread.CurrentThread.GetHashCode()); // should ONLY be called from GUI thread Debug.Assert(timer.Enabled); // successive Start or Stop calls are BAD Debug.WriteLine("SerialOperationTimer " + name + ": Stop"); if (runningTimer != this) { Debug.Assert(false); // Lets have a look in the debugger NOW throw new System.Exception("SerialOperationTimer " + name + ": Attempted 'Stop' while not running"); } // Stop further background processing from being initiated, timer.Enabled = false; // but, background processing may still be in progress from the last timer tick... runningTimer = null; // Purge serial input and output buffers. Clearing input buf causes any blocking read in progress in background thread to throw // System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n" if(Form1.xxConnection.PortIsOpen) Form1.xxConnection.CiCommDiscardBothBuffers(); bool lockTaken = false; // Now, GUI thread needs the lock back. // 3 sec REALLY should be enough time for background thread to cleanup and release the lock: Monitor.TryEnter(XXX_Conn.SerialPortLockObject, 3000, ref lockTaken); if (!lockTaken) throw new Exception("Serial port lock not yet released by background timer thread "+name); if (Form1.xxConnection.PortIsOpen) { // Its possible there still stuff in transit from device (for example, background thread just completed // sending an ACQ command as it was stopped). So, sync up with the device... int r = Form1.xxConnection.CiSync(); Debug.Assert(r == XXX_Conn.CI_OK); if (r != XXX_Conn.CI_OK) throw new Exception("Cannot re-sync with device after disabling timer thread " + name); } } /// <summary> SerialOperationTimer.StopAllBackgroundTimers() - Stop all background activity </summary> public static void StopAllBackgroundTimers() { if (runningTimer != null) runningTimer.Stop(); } public double Interval { get { return timer.Interval; } set { timer.Interval = value; } } } // class SerialOperationTimer 
0
source

All Articles