Share via


VB.Net: Serial Port Data Reception

About

Questions that are asked on Forums about serial ports are complicated because of the many approaches taken with device communication and / or not having a clear understanding of the device protocol.  Taking the approach presented to serial data reception removes part of the complexity, and leaves only a discussion of the protocol.

Basics

This is some basic information that you know, or should know to use this code.  It is not meant to insult your intelligence; just bear in mind that not all readers of this have the same knowledge level.

  • Modern PC’s have multiple cores and can execute multiple threads at the same time.  Take advantage of this.
  • Only one SerialPort event handler can execute at a time.  There are three operational SerialPort events.
  • When the SerialPort DataReceived event fires there may or may not be an entire protocol data unit (PDU or message) available.  This is a common forum question and one that used to stumped me.
  • SerialPort reads are blocking except ReadExisting.
  • SerialPort communication can take one of three forms
    • non-polled – the serial port receives data unsolicited.  This means that the device that is attached to the serial port sends messages without being asked to.  My GPS device does this.  As soon as it is connected it starts sending NMEA messages.
    • polled - the device that is attached to the serial port sends messages only in response to the receipt of a message from the application.
    • Combination of both of the above.
  • You must know the protocol of the device you are communicating with. 
  • Windows form controls can only be manipulated from the UI thread, and UI control updating is inherently slow.
  • Threading.Monitor Enter, TryEnter, and Exit.
  • Threading Manual and Auto ResetEvent classes.

The difficulty people have using the serial port tends to be with receiving data.  As I worked with different devices I kept searching for THE answer to receiving data.  What is presented below is as close as I have come to THE answer.  This code has been used for:

  • PC –> Loopback – PC communicating with itself for testing
  • PC –> PC – PC communicating with another PC
  • PC –> GPS – retrieval of NMEA messages
  • PC as ASCII terminal
  • PC –> microcontroller
  • etc.

BaudRate

The speed of the serial port is set using the BaudRate property, and is set based on the device you are connected to.  This number represents the maximum amount of data that can be received in one second.

If the BaudRate is 19200 the maximum bytes per second your application has to be able to process is 2400 bytes per second. or 1 byte every 0.000416 seconds,

If the BaudRate is 115200 the maximum bytes per second your application has to be able to process is 14400 bytes per second. or 1 byte every 0.000069 seconds,

Those are the maximum rates possible but the actual rate can be much slower depending on the device you are attached to.

In reality your application will read between 1 and n bytes depending on how the application is structured, and these bytes are further packaged into PDU’s.  If your application is presenting the data to the user you have to ask yourself if the user interface can be updated at the actual PDU arrival rate, and if it can’t what do you do↑

Code Overview

The structure of the code is:

  • Three event handlers that do as little as possible to communicate with the background threads without the loss of data.  Threading.AutoResetEvents are used for this communication.
  • Three background threads that do most of the work. 
    • a thread to accumulate the bytes received from the port into a buffer. This code reads bytes only! This code is rarely changed.
    • a thread to process PDU’s.  The base code just removes the bytes from the buffer.  This is just a placeholder because most of the coding for every new application occurs here.
    • a thread to deal with pin changed and error events.  This code is changed as needed by the protocol and application.  The base code just removes the items from the queue.
  • Code to start the threads, configure and open the port, and code to stop the threads and close the port.

Guiding principles:

  • handlers do a minimum of work
  • make use of as many cores as possible
  • no blocking serial port methods are executed on the UI

The base code as a forms project is included below and is also available for download.

Here are short movies showing the simulated complex PDU code running. The first movie is the general flow of the program, and the second is a more detailed look at data reception.

  MovieGeneral   MovieReceive

Here is what the UI looks like. Movie UI 

If you do not have a loopback plug I highly recommend you get one(see references).  They are inexpensive and as you will see it is sometimes useful to talk to yourself. 

You can create a simple loopback for RS232 DB9 / DB25 by connecting pins two and three together; be careful not to short the pins to the case.

Receive Data Flow

This is the basic flow of data through the code.  Other than several threads running this code is not complicated. 

Data from the serial port is first read into a Windows-created buffer.  The size of that buffer is controlled by the ReadBufferSize property. 

The code is notified that there are bytes to read by the firing of the DataReceived event.  The DataReceived event handler sets an AutoResetEvent that causes the Receive method to execute.

The Receive method reads all available bytes.  Bytes read are accumulated into a list of bytes, RcvBuffer.  This method then sets another AutoResetEvent that causes the Protocol method to execute.  Since RcvBuffer can be modified by more than one method access to it is controlled using thread synchronization.

The Protocol method determines if there are PDU’s, and removes the PDU bytes from RcvBuffer.  The PDU’s are then processed.  Based on how the code is written this is where most, if not all, application code is written.

  +----------+  +----------+

  +-------+  |  Serial  |  Bytes  |  Windows |

  |Device +------>+  Port   +--------->+  Buffer  |

  +-------+  +----------+  +----------+

   |

   |

+-------------------+ DataReceived  <---------------+

|

|

|  +--------------+  +---------------+

+--->|Receive method+---->  signal +--->+Protocol method|

  +--------------+  +---------------+

  |  ^

  |  |

  v  +

  bytes  bytes

  to  from

  +  ^

  |  |

  |  +---------------+  |

  +------> |  RcvBuffer  +------>+

  |  |

  | list of bytes |

  +---------------+

Back to top

Code Discussion

The code presented is:

  • the common code that works in all environments, forms or console, in this case a form application.
  • a device emulator that constantly sends PDU’s
  • a couple of protocol handlers to process the PDU’s

#Region directives are used to organize the code and are used to direct the conversation.

Common

Objects and Variables

This section of code contains the serial port object and variables used throughout.

The primary buffer and its lock object are defined here.  Also a ConcurrentQueue of Object is defined for pin changes and errors.

There are three threads defined with a corresponding AutoResetEvent to control event firing. In addition there is a ManualResetEvent that controls general thread execution.

Variables used for instrumentation are defined here. A variable to control instrumentation collection is defined.  Their meaning can be ascertained by usage.

Handlers

DataReceived – this handler first check is to see the event type is SerialData.Eof.  This event type is ignored. If the event type is SerialData.Chars rcvARE, the AutoResetEvent that control the Receive method, is set.

ErrorReceived – the SerialErrorReceivedEventArgs are added to a queue that holds both errors and pin event arguments. The AutoResetEvent that control the PinsAndErrors method, is set.

PinChanged - the SerialPinChangedEventArgs are added to a queue that holds both errors and pin event arguments. The AutoResetEvent that control the PinsAndErrors method, is set.

Receive method

This method is responsible for reading all available bytes from the serial port.  These bytes are accumulated into RcvBuffer for processing by the Protocol method.  This method is ran on a separate thread that is started before the port is opened, and is in a loop until runThreads is reset.  Until rcvARE is set by the DataReceived  event handler the thread is blocked by this line  rcvARE.WaitOne near the end of the method. 

When rcvARE is set by the DataReceived  event handler the code checks to see if runThreads is set.  If it is then the loop executes otherwise the thread ends.

RcvReadCT is set to zero.  This tracks how many times the loop that follows it executes for one firing of the DataReceived  event handler.

The inner do loop continually executes while the port is open and there are bytes to read.

The number of bytes to read is determined and a temporary array large enough to hold that number of bytes is created.

The bytes are then read. Note that the serial port read returns the number of bytes actually read which can be less than the number of bytes available.  If that is the case the temporary buffer is resized.

Next the temporary buffer is added to the primary buffer, RcvBuffer, which is protected by rcvBufLock.  RcvReadCT is incremented.

AutoResetEvent protoARE is set after the first read and if there are multiple reads, which causes the Protocol method to execute.

Which brings the code back to rcvARE.WaitOne,  blocking until another DataReceived  event  happens.

Instrumentation lines were skipped in the discussion.  They start with If RcvInstr Then.  RcvInstr is a Boolean that controls whether or not to do instrumentation. 

PinsAndErrors method

This method is responsible for processing serial port errors and pin changes.  This method is ran on a separate thread that is started before the port is opened, and is in a loop until runThreads is reset.  Until pinsErrorsARE is set the thread is blocked by this line  pinsErrorsARE.WaitOne near the end of the method. 

When pinsErrorsARE is set the code checks to see if runThreads is set.  If it is then the loop executes otherwise the thread ends.  The code in this method is a prototype that can be tailored based on the application.  As written the queue is emptied and a Debug statement is used to report the error.  The exception to this is SerialError.Overrun.  This is a hardware error and the method discards the input buffer when this occurs.

Open / Close

SerialPortOpen method

This method is responsible for opening the port and starting the threads.  The threads are started by a call to the StartSerialPortThreads method.

Then the ports settings are defined.  These setting must match the device that you are connected to.

Settings of Interest

  • ReceivedBytesThreshold – one is the default, recommend no change unless absolutely needed
  • ReadTimeout - default is infinite if not set. recommend some value
  • WriteTimeout - default is infinite if not set. recommend some value
  • ReadBufferSize - Windows-created input buffer 4096 is default, change if needed
  • WriteBufferSize - Windows-created output buffer 2048 is default, change if needed
  • Encoding – this is important if you are using character based read / write methods.  The code provided uses byte read methods so this setting has no affect.  If you are using writes that are character based this setting should match the attached devices encoding.
StartSerialPortThreads method

This method is responsible for starting the background threads used by the serial port.

This method resets the AutoResetEvents, then creates and starts the background threads.  Before starting the threads runThreads is set to the run state.

Back to top

SerialPortClose method

This method is responsible for closing the port and stopping the threads. 

At the end of each of the threads is Loop While runThreads.WaitOne(0). The first thing this method does is to Reset runThreads which causes the Loop While to fail, thus ending the thread.  To make sure that the threads get to the Loop While their individual AutoResetEvent’s are then Set.

Once those steps are taken all of the threads are joined and the port is closed. If the application is ending this method should be called with True which causes the ResetEvent’s to be disposed.

Protocol (Region App specific)

At this point if you use the code discussed so far and connect to a device what you would see is all of the bytes received from the device in RcvBuffer.  (If the device needs to be polled to get it to send add that in a button.)  What is needed now is a Protocol method to see that.  Here is a simple version.

01.Private Sub  Protocol()
02.  Do
03.  'with this you will eventually get an out of memory exception
04. 
05.  Debug.WriteLine(RcvBuffer.Count.ToString("n0"))
06. 
07.  protoARE.WaitOne() 'wait for Receive to signal potential message
08.  Loop While  runThreads.WaitOne(0) 'loop while running
09.End Sub

Give it a try with a breakpoint set inside the loop.  Each time the break is encountered examine RcvBuffer.  You will see that the data from the device is being accumulated into RcvBuffer and if you let the code run long enough it will run out of memory.  Not very useful.  Lets look at another example.

Included with the code is a class, PDUCommon.  Also included is another class, LinePDU, that inherits PDUCommon.  LinePDU retrieves messages from RcvBuffer that end with the ControlChars.Lf.  Here is a Protocol method for receiving lines.

01.Private Sub  Protocol()
02.  Dim TotByteCT As Long  = 0L
03.  Dim linesRcvd As New  List(Of LinePDU)
04.  Do
05.  Dim PDU As LinePDU
06.  Do
07.  PDU = New  LinePDU(RcvBuffer, rcvBufLock)
08.  If PDU.isValid Then
09.  TotByteCT += PDU.DataLength
10.  PDU.Data = PDU.Data.TrimEnd 'get rid of trailing white space
11.  linesRcvd.Add(PDU)
12.  End If
13.  Loop While  PDU.isComplete AndAlso RcvBuffer.Count > 2
14. 
15.  'process linesRcvd
16. 
17. 
18.  linesRcvd.Clear() 'replace this with your code to process the lines
19. 
20. 
21.  protoARE.WaitOne() 'wait for Receive to signal potential message
22.  Loop While  runThreads.WaitOne(0) 'loop while running
23.End Sub

What you do with the lines received is up to you. 

Another PDU that is simulated in the code provided looks like this (see Movie above)

  • STX = one byte = 2
  • ID = four bytes = sequence
  • DL = one byte = length of data (1 - 255)
  • data - string <= 255 chars
  • last byte = CRC = one byte = XOR of all data bytes

There is a class, ComplexDeviceProto, that receives these PDU’s.

Included with the code are two loop back device simulators, and several Protocol methods that are surrounded with #Region’s.  To use the code uncomment one of each, and comment out the others.  Also included are several PDU classes, two which may be useful.

Back to top

The Code

Imports System.IO.Ports
 
Public Class  Form1
#Region "Common"
#Region "Objects and Variables"
  ''' <summary>
  ''' the serial port
  ''' </summary>
  ''' <remarks></remarks>
  Private WithEvents  mySerialPort As New  SerialPort
 
  ''' <summary>
  ''' accumulates bytes received from mySerialPort
  ''' </summary>
  ''' <remarks>primary buffer</remarks>
  Private RcvBuffer As New  List(Of Byte) 'PRIMARY BUFFER! <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  Private rcvBufLock As New  Object 'lock for the above
  ''' <summary>
  ''' controls threads looping
  ''' </summary>
  ''' <remarks></remarks>
  Private runThreads As New  Threading.ManualResetEvent(False) 'not running
 
  ''' <summary>
  ''' thread that reads bytes and adds them to the rcvBuf
  ''' </summary>
  ''' <remarks></remarks>
  Private rcvThrd As Threading.Thread
  Private rcvARE As New  Threading.AutoResetEvent(False)
  ''' <summary>
  ''' responsible for determining if rcvBuf has a message
  ''' </summary>
  ''' <remarks></remarks>
  Private protoThrd As Threading.Thread
  Private protoARE As New  Threading.AutoResetEvent(False)
  ''' <summary>
  ''' handles PinChanged and ErrorReceived events
  ''' </summary>
  ''' <remarks></remarks>
  Private pinserrorsThrd As Threading.Thread
  Private pinsErrorsARE As New  Threading.AutoResetEvent(False)
  ''' <summary>
  ''' Queue of PinChanged and ErrorReceived events
  ''' </summary>
  ''' <remarks></remarks>
  Private PinsErrorsQ As New  Concurrent.ConcurrentQueue(Of Object)
  '
  Private RcvReadCT As Integer  = 0 'used by method receive
  Private RcvError As Boolean  = False
  'some receive instrumentation / metrics
  Private RcvInstr As Boolean  = True  'False ' controls receive instrumentation
  Private RcvDRECT As Long  = 0L 'used by event DataReceived
  Private RcvDREEofs As Long  = 0L 'used by event DataReceived
  Private RcvByteTot As Long  = 0L
  Private RcvMultiRead As Long  = 0L
  Private RcvNoData As Long  = 0L
  Private RcvThrdTime As New  Stopwatch
  Private RcvBuffAddTime As New  Stopwatch
#End Region
 
#Region "Handlers"
  Private Sub  mySerialPort_DataReceived(sender As Object, e As  SerialDataReceivedEventArgs) Handles mySerialPort.DataReceived
  If e.EventType = SerialData.Eof Then
  'it is ok to throw this away, caused by EOF (byte=26) in buffer
  'the byte that is equal to 26 will be read
  If RcvInstr Then RcvDREEofs += 1
  Else 'SerialData.Chars
  rcvARE.Set() 'read the bytes, see method Receive
  If RcvInstr Then RcvDRECT += 1L
  End If
  End Sub
 
  Private Sub  mySerialPort_ErrorReceived(sender As Object, e As  SerialErrorReceivedEventArgs) Handles mySerialPort.ErrorReceived
  PinsErrorsQ.Enqueue(e)
  pinsErrorsARE.Set()
  End Sub
 
  Private Sub  mySerialPort_PinChanged(sender As Object, e As  SerialPinChangedEventArgs) Handles mySerialPort.PinChanged
  PinsErrorsQ.Enqueue(e)
  pinsErrorsARE.Set()
  End Sub
#End Region
 
#Region "Receive / PinsAndErrors"
  Private Sub  Receive()
  'because of how the .DataReceived event handler and this method are written
  '.DataReceived events can fire but there are not bytes to be read
  Do
  RcvReadCT = 0
  'note: BytesToRead can't be checked if the port is closed!!!
  Do While  mySerialPort.IsOpen AndAlso mySerialPort.BytesToRead > 0 ''read all bytes available
  If RcvInstr Then RcvThrdTime.Start() 'instrumentation
  RcvError = False
  Try
  Dim numBytRd As Integer  = mySerialPort.BytesToRead 'number of bytes to read
  Dim tmpBuf(numBytRd - 1) As Byte  'create a temporary buffer to hold bytes
  numBytRd = mySerialPort.Read(tmpBuf, 0, numBytRd) 'READ the bytes
  If numBytRd <> tmpBuf.Length Then 'read less than initial .BytesToRead↑
  Array.Resize(tmpBuf, numBytRd) 'yes
  End If
  If RcvInstr Then RcvByteTot += numBytRd 'instrumentation
 
  'add temporary buffer to public buffer
  If RcvInstr Then RcvBuffAddTime.Start() 'instrumentation
  Threading.Monitor.Enter(rcvBufLock) '<<< LOCK
  RcvBuffer.AddRange(tmpBuf)
  Threading.Monitor.Exit(rcvBufLock) '<<< UNLOCK
  If RcvInstr Then RcvBuffAddTime.Stop()
 
  RcvReadCT += 1 'increment count of reads
  If RcvReadCT = 1 Then 'first read↑
  protoARE.Set() 'yes, check for possible message, see method Protocol
  End If
  Catch ex As Exception
  'queue the error
  RcvError = True
  PinsErrorsQ.Enqueue(ex)
  pinsErrorsARE.Set()
  End Try
  If RcvInstr Then RcvThrdTime.Stop()
  Loop
  If RcvReadCT > 1 Then 'more than one read
  'yes
  If RcvInstr Then RcvMultiRead += 1L 'instrumentation
  protoARE.Set() 'check for possible message
  ElseIf RcvInstr AndAlso RcvReadCT = 0 Then
  RcvNoData += 1L 'instrumentation
  End If
  rcvARE.WaitOne() 'WAIT for .DataReceived event handler to fire <<<<<<  
  Loop While  runThreads.WaitOne(0) 'loop while running
  End Sub
 
  Private Sub  PinsAndErrors()
  Dim LastErr As SerialErrorReceivedEventArgs
  Dim LastPin As SerialPinChangedEventArgs
  Dim LastEx As Exception
  Do
  Dim obj As Object
  Do While  PinsErrorsQ.Count > 0
  Dim serrea As SerialErrorReceivedEventArgs
  Dim spinchea As SerialPinChangedEventArgs
  Dim excp As Exception
  If PinsErrorsQ.TryDequeue(obj) Then
  If TypeOf  obj Is  SerialErrorReceivedEventArgs Then
  'see https://msdn.microsoft.com/en-us/library/system.io.ports.serialerror(v=vs.110).aspx
  serrea = DirectCast(obj, SerialErrorReceivedEventArgs)
  LastErr = serrea
  'todo SerialErrorReceivedEventArgs
  Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.f") & " " & serrea.EventType.ToString)
  Select Case  serrea.EventType
  Case SerialError.Frame
  Case SerialError.Overrun 'hardware error
  mySerialPort.DiscardInBuffer()
  Case SerialError.RXOver
  'should not happen, might need to increase ReadBufferSize
  Case SerialError.RXParity
  Case SerialError.TXFull
  End Select
  ElseIf TypeOf  obj Is  SerialPinChangedEventArgs Then
  spinchea = DirectCast(obj, SerialPinChangedEventArgs)
  LastPin = spinchea
  Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.f") & " " & spinchea.EventType.ToString)
  'todo SerialPinChangedEventArgs
  ElseIf TypeOf  obj Is  Exception Then
  excp = DirectCast(obj, Exception)
  LastEx = excp
  Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.f") & " " & excp.InnerException.Message)
  'todo Exception
  End If
  Else
  Threading.Thread.Sleep(10) 'Dequeue failed
  End If
  Loop
  pinsErrorsARE.WaitOne() 'wait for event handler to fire
  Loop While  runThreads.WaitOne(0) 'loop while running
  End Sub
#End Region
 
#Region "Open / Close"
 
  Private Sub  SerialPortOpen()
  If mySerialPort.IsOpen Then Exit  Sub
  '
  'start the threads before opening the port
  StartSerialPortThreads()
  '
  'Modify settings as needed <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  mySerialPort.PortName = "COM1"
  mySerialPort.BaudRate = 115200 '  19200 '  
  mySerialPort.DataBits = 8
  mySerialPort.Parity = Parity.None
  mySerialPort.StopBits = StopBits.One
 
  'other settings as needed - consider speed and max size to determine settings  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  mySerialPort.ReceivedBytesThreshold = 1 'one is the default, recommend no change unless absolutely needed
  mySerialPort.ReadTimeout = 1000 'default is infinite if not set
  mySerialPort.WriteTimeout = 1000 'default is infinite if not set
  mySerialPort.ReadBufferSize = 1024 * 4 'Windows-created input buffer 4096 is default, change if needed
  mySerialPort.WriteBufferSize = 1024 * 2  'Windows-created output buffer 2048 is default, change if needed
 
  'this setting is informational only.  the code only reads bytes  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  'if the device is sending strings recommend setting this to match encoding device is using  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  'if the application is writing strings this must match devices encoding  <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  mySerialPort.Encoding = System.Text.Encoding.GetEncoding(28591) 'default is 7 bit ascii, this is an 8 bit flavor of asciii
 
  Try
  mySerialPort.Open()
  'some devices require the following
  mySerialPort.DtrEnable = True  'only after Open. may cause multiple PinChanged events
  Catch ex As Exception
  SerialPortClose(False)
  Debug.WriteLine(ex.Message)
  End Try
  End Sub
 
  Private Sub  StartSerialPortThreads()
  'Running↑
  If runThreads.WaitOne(0) Then Exit  Sub
  'reset AutoResetEvents
  rcvARE.Reset()
  pinsErrorsARE.Reset()
  protoARE.Reset()
 
  'set up the threads
  '
  'this thread reads bytes and adds them to the buffer
  rcvThrd = New  Threading.Thread(AddressOf Receive)
  rcvThrd.IsBackground = True
  '
  ' why Threading.ThreadPriority.AboveNormal ↑??
  ' at low data rates this will not affect performance
  ' at high data rates it is imperative to Read the bytes from the
  '  port quickly!
  '
  rcvThrd.Priority = Threading.ThreadPriority.AboveNormal
 
  'this thread examines the buffer to see if a 
  'complete message (PDU), as defined by the protocol, is available
  protoThrd = New  Threading.Thread(AddressOf Protocol)
  protoThrd.IsBackground = True
 
  'this thread handles PinChanged and ErrorReceived events
  pinserrorsThrd = New  Threading.Thread(AddressOf PinsAndErrors)
  pinserrorsThrd.IsBackground = True
 
  runThreads.Set() 'indicate we are running before starting threads
  'start threads that do the work
  'do this before opening SerialPort
  pinserrorsThrd.Start()
  protoThrd.Start()
  rcvThrd.Start() 'start last
  'wait for threads to start
  Do While  (pinserrorsThrd.ThreadState And Threading.ThreadState.Unstarted) = Threading.ThreadState.Unstarted OrElse
  (protoThrd.ThreadState And  Threading.ThreadState.Unstarted) = Threading.ThreadState.Unstarted OrElse
  (rcvThrd.ThreadState And  Threading.ThreadState.Unstarted) = Threading.ThreadState.Unstarted
 
  Threading.Thread.Sleep(10)
  Loop
  End Sub
 
  Private Sub  SerialPortClose(isAppEnd As Boolean)
  runThreads.Reset() 'not running
 
  'then some code to get the threads to stop
  protoARE.Set() 'protocol first
  rcvARE.Set()
  pinsErrorsARE.Set()
  'wait for threads to stop
  protoThrd.Join()
  rcvThrd.Join()
  pinserrorsThrd.Join()
 
  'then close the port
  If mySerialPort.IsOpen Then
  mySerialPort.DtrEnable = False
  mySerialPort.Close()
  End If
 
  If isAppEnd Then 'is the application ending
  Threading.Thread.Sleep(100)
  rcvARE.Dispose()
  pinsErrorsARE.Dispose()
  runThreads.Dispose()
  protoARE.Dispose() 'protocol last
  End If
  End Sub
 
  ''' <summary>
  ''' for forms only
  ''' </summary>
  ''' <remarks></remarks>
  Private Sub  FormIsClosingHelper()
  SerialPortClose(True)
  Me.BeginInvoke(Sub()
  Me.Close() 'fire close again
  End Sub)
  End Sub
#End Region
#End Region
 
#Region "App specific"
#Region "PDU Classes"
  Private Class  PDUCommon
  Friend _valid As Boolean  = False  'not valid
  Friend _complete As Boolean  = False  'True = a complete message
  Friend _data() As Byte
 
  Public Function  isComplete() As Boolean
  Return Me._complete
  End Function
 
  Public Function  isValid() As  Boolean
  Return Me._valid
  End Function
 
  ''' <summary>
  ''' find a byte, the needle, in the buffer, the haystack
  ''' </summary>
  ''' <param name="needle">the byte value to find</param>
  ''' <param name="someData">buffer, a List(Of Byte), where to look</param>
  ''' <returns>index of needle in the buffer</returns>
  ''' <remarks></remarks>
  Public Function  FindByte(needle As Byte, ByRef  someData As  List(Of Byte), ByRef  someDataLock As Object) As  Integer
  Dim idxNeedle As Integer
  Threading.Monitor.Enter(someDataLock)
  idxNeedle = someData.IndexOf(needle)
  Threading.Monitor.Exit(someDataLock)
  Return idxNeedle
  End Function
 
  ''' <summary>
  ''' get bytes up to and including the 'needle'
  ''' </summary>
  ''' <param name="needle">the byte value to find</param>
  ''' <param name="someData">buffer, a List(Of Byte)</param>
  ''' <param name="someDataLock">an object used to lock the buffer</param>
  ''' <returns>nothing if needle not found in buffer, else byte()</returns>
  ''' <remarks></remarks>
  Public Function  GetBytes(needle As Byte, ByRef  someData As  List(Of Byte), ByRef  someDataLock As Object) As  Byte()
  Dim rv() As Byte  = Nothing
  Threading.Monitor.Enter(someDataLock)
  Dim idxNeedle As Integer  = someData.IndexOf(needle)
  If idxNeedle >= 0 Then
  'has needle
  idxNeedle += 1 'convert to count
  rv = someData.GetRange(0, idxNeedle).ToArray()
  someData.RemoveRange(0, idxNeedle)
  End If
  Threading.Monitor.Exit(someDataLock)
  Return rv
  End Function
 
  ''' <summary>
  ''' get bytes based on count
  ''' </summary>
  ''' <param name="someData">buffer, a List(Of Byte)</param>
  ''' <param name="someDataLock">an object used to lock the buffer</param>
  ''' <param name="count">number of bytes to return</param>
  ''' <returns>nothing if count .lt. number bytes in buffer, else byte()</returns>
  ''' <remarks></remarks>
  Public Function  GetBytes(ByRef someData As List(Of Byte), ByRef  someDataLock As Object, count As  Integer) As Byte()
  Dim rv() As Byte  = Nothing
  If someData.Count >= count AndAlso count > 0 Then
  Threading.Monitor.Enter(someDataLock)
  rv = someData.GetRange(0, count).ToArray
  someData.RemoveRange(0, count)
  Threading.Monitor.Exit(someDataLock)
  End If
  Return rv
  End Function
 
  ''' <summary>
  ''' gets all bytes currently in the buffer
  ''' </summary>
  ''' <param name="someData">the buffer</param>
  ''' <param name="someDataLock">lock for the buffer</param>
  ''' <returns></returns>
  ''' <remarks></remarks>
  Public Function  GetAllBytes(ByRef someData As List(Of Byte), ByRef  someDataLock As Object) As  Byte()
  Dim rv() As Byte  = Nothing
  If someData.Count > 0 Then
  Threading.Monitor.Enter(someDataLock)
  rv = someData.GetRange(0, someData.Count).ToArray
  someData.RemoveRange(0, someData.Count)
  Threading.Monitor.Exit(someDataLock)
  End If
  Return rv
  End Function
 
  ''' <summary>
  ''' remove all bytes from the buffer
  ''' </summary>
  ''' <param name="someData">buffer, a List(Of Byte)</param>
  ''' <param name="someDataLock">an object used to lock the buffer</param>
  ''' <param name="getBytes">if true _data will contain byes in buffer before clear</param>
  ''' <returns>count of bytes removed</returns>
  ''' <remarks></remarks>
  Public Function  FlushBuffer(ByRef someData As List(Of Byte), ByRef  someDataLock As Object, Optional  getBytes As  Boolean = False) As  Integer
  Dim rv As Integer  = 0
  If someData.Count > 0 Then
  Threading.Monitor.Enter(someDataLock)
  rv = someData.Count
  If getBytes Then
  Me._data = someData.GetRange(0, someData.Count).ToArray
  End If
  someData.Clear()
  Threading.Monitor.Exit(someDataLock)
  End If
  Return rv
  End Function
  End Class
 
  Private Class  LinePDU
  Inherits PDUCommon
  Public Data As String  = "" 'if valid this has the line of data
  Const lineTerm As Byte  = Asc(ControlChars.Lf)  'linefeed
 
  Public Sub  New(ByRef someData As List(Of Byte), ByRef  someDataLock As Object)
  Dim PDU() As Byte  = Me.GetBytes(lineTerm, someData, someDataLock)
  If PDU IsNot Nothing Then
  Me.Data = System.Text.Encoding.GetEncoding(28591).GetChars(PDU, 0, PDU.Length)
  Me._complete = True
  Me._valid = True
  End If
  End Sub
 
  Public Function  DataLength() As Integer
  Return Me.Data.Length
  End Function
  End Class
 
  Private Class  ComplexDeviceProto 'sample
  Inherits PDUCommon
  'protocol is
  '0  STX = one byte = 2
  '1  ID = four bytes = sequence
  '5  DL = one byte = length of data (1 - 255)
  '6  data - string <= 255 chars
  '  CRC = one byte = XOR of all data bytes
  Public Data As String  = ""
  Public ID As Integer  = Integer.MinValue
  Const stx As Byte  = 2
 
  Public Sub  New(ByRef someData As List(Of Byte), ByRef  someDataLock As Object)
  '
  Dim oneTime As Boolean  = True
  Threading.Monitor.Enter(someDataLock) 'lock
  Do While  someData.Count > 0 AndAlso someData(0) <> stx
  someData.RemoveAt(0)
  If oneTime Then
  Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.f") & " Protocol Error")
  oneTime = False
  End If
  Loop
  If someData.Count >= 6 Then 'have DL
  If someData.Count >= (7 + someData(5)) Then 'complete PDU?
  'yes
  Dim PDU() As Byte  = someData.GetRange(0, 7 + someData(5)).ToArray 'get it
  someData.RemoveRange(0, 7 + someData(5)) 'remove it
  Threading.Monitor.Exit(someDataLock) 'unlock
  Dim crc As Integer
  For x As Integer  = 6 To  6 + (PDU(5) - 1) 'calc crc
  crc = crc Xor  PDU(x)
  Next
  If crc = PDU(6 + PDU(5)) Then 'check crc
  'good
  Me.ID = BitConverter.ToInt32(PDU, 1)
  Me.Data = System.Text.Encoding.GetEncoding(28591).GetChars(PDU, 6, PDU(5))
  Me._complete = True
  Me._valid = True
  Else
  Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.f") & " CRC Error")
  End If
  End If
  End If
  If Threading.Monitor.IsEntered(someDataLock) Then Threading.Monitor.Exit(someDataLock)
  'Me.FlushBuffer(someData, someDataLock)
  End Sub
 
  Public Function  DataLength() As Integer
  Return Me.Data.Length
  End Function
  End Class
#End Region
 
#Region "Protocol Methods"
 
  Private stpw As Stopwatch = Stopwatch.StartNew
  Private UIstpw As Stopwatch = Stopwatch.StartNew
 
#Region "simple"
  'Private Sub Protocol()
  '  Do
  '  'with this you will eventually get an out of memory exception
 
  '  Debug.WriteLine(RcvBuffer.Count.ToString("n0"))
 
  '  protoARE.WaitOne() 'wait for Receive to signal potential message
  '  Loop While runThreads.WaitOne(0) 'loop while running
  'End Sub
#End Region
 
#Region "flush"
  'Private Sub Protocol()
  '  Dim TotByteCT As Long = 0L
  '  Dim upd As Stopwatch = Stopwatch.StartNew
  '  Do
  '  Dim PDU As New DeviceProtocolCommon
  '  Dim bf As Integer = PDU.FlushBuffer(RcvBuffer, rcvBufLock, True)
  '  If bf > 0 Then
  '  TotByteCT += bf
  '  If upd.ElapsedMilliseconds >= 5000 Then 'update
  '  upd.Restart()
  '  Debug.WriteLine(TotByteCT.ToString("n0"))
  '  End If
  '  End If
  '  protoARE.WaitOne() 'wait for Receive to signal potential message
  '  Loop While runThreads.WaitOne(0) 'loop while running
  'End Sub
#End Region
 
#Region "lines"
  'Private Sub Protocol()
  '  Dim TotByteCT As Long = 0L
  '  Dim linesRcvd As New List(Of LinePDU)
  '  Do
  '  Dim PDU As LinePDU
  '  Do
  '  PDU = New LinePDU(RcvBuffer, rcvBufLock)
  '  If PDU.isValid Then
  '  TotByteCT += PDU.DataLength
  '  PDU.Data = PDU.Data.TrimEnd 'get rid of trailing white space
  '  linesRcvd.Add(PDU)
  '  End If
  '  Loop While PDU.isComplete AndAlso RcvBuffer.Count > 2
 
  '  'process linesRcvd
 
  '  If UIstpw.ElapsedMilliseconds >= 100 Then 'update UI 10 times per second
  '  UIstpw.Restart()
  '  Me.Invoke(Sub()
  '  If linesRcvd.Count > 0 Then
  '  RichTextBox1.Clear()
  '  RichTextBox1.Lines = (From l In linesRcvd Select l.Data).ToArray
  '  RichTextBox1.Refresh()
  '  End If
  '  linesRcvd.Clear()
  '  ''Show(bits / Second())
  '  TextBox1.Text = (TotByteCT / stpw.Elapsed.TotalSeconds * 8).ToString("n0")
  '  End Sub)
  '  End If
  '  protoARE.WaitOne() 'wait for Receive to signal potential message
  '  Loop While runThreads.WaitOne(0) 'loop while running
  'End Sub
#End Region
 
#Region "complex"
  Private Sub  Protocol()
  'protocol is
  '0  STX = one byte = 2
  '1  ID = four bytes = sequence
  '5  DL = one byte = length of data (1 - 255)
  '6  data - string <= 255 chars
  '  CRC = one byte = XOR of all data bytes
 
  Dim addThese As New  List(Of ComplexDeviceProto)
  Dim TotMessCT As Long  = 0L
  Dim TotByteCT As Long  = 0L
  Dim MaxDepth As Integer  = 0
  Do
  Dim PDU As ComplexDeviceProto
  Do
  ''pass in the buffer and lock
  PDU = New  ComplexDeviceProto(RcvBuffer, rcvBufLock) 'potential message?
  If PDU.isValid Then 'valid message?
  addThese.Add(PDU)
  TotByteCT += PDU.DataLength
  End If
  Loop While  PDU.isComplete AndAlso RcvBuffer.Count >= minMessSz 'complete, see if another message present
 
  If addThese.Count > 0 Then 'new messages
  ''yes
  If addThese.Count > MaxDepth Then MaxDepth = addThese.Count
  TotMessCT += addThese.Count
  addThese.Clear()
  Try
  If UIstpw.ElapsedMilliseconds >= 250 Then 'update UI 4 times per second
  UIstpw.Restart()
  If Not  dispPaused Then
  Me.BeginInvoke(Sub()
   Dim bps As Integer  = CInt((TotByteCT / stpw.Elapsed.TotalSeconds) * 8)
   TextBox1.Text = String.Format("RMsgs: {0,-11:n0} Msgs/s: {1,-6:n0} bps: {2,-8:n0} MaxD: {3:n0}",
    TotMessCT, TotMessCT / stpw.Elapsed.TotalSeconds, bps, MaxDepth)
   'no data / second
   TextBox3.Text = "NoData/s: "  & (RcvNoData / stpw.Elapsed.TotalSeconds).ToString("n0")
   bps \= 1000
   If bps <= ProgressBar1.Maximum Then
   ProgressBar1.Value = bps
   Else
   ProgressBar1.Value = ProgressBar1.Maximum
   End If
   End Sub)
  End If
  End If
  Catch ex As Exception
 
  End Try
  End If
  protoARE.WaitOne() 'wait for Receive to signal potential message
  Loop While  runThreads.WaitOne(0) 'loop while running
  End Sub
#End Region
 
#End Region
#End Region
 
#Region "Form Load / Shown / Close"
  Private Sub  Form1_FormClosing(sender As Object, e As  FormClosingEventArgs) Handles Me.FormClosing
  Static t As Task
  If t Is Nothing  Then
  t = Task.Run(Sub()
  FormIsClosingHelper()
  End Sub)
  e.Cancel = True
  Else
  t.Wait()
  End If
  End Sub
 
  Private Sub  Form1_Load(sender As Object, e As  EventArgs) Handles Me.Load
  Debug.WriteLine("")
  Debug.WriteLine(DateTime.Now.ToLongTimeString)
  End Sub
 
  Private Sub  Form1_Shown(sender As Object, e As  EventArgs) Handles Me.Shown
  SetDoubleBuffering(TextBox1)
  SetDoubleBuffering(TextBox2)
  SerialPortOpen()
  If mySerialPort.IsOpen Then SimulateDevice() '<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Comment this line if connected to device <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  ProgressBar1.Maximum = mySerialPort.BaudRate \ 1000
  End Sub
 
  Private Sub  SetDoubleBuffering(aControl As Control)
  '
  'for some controls, like DataGridView, this can have a dramatic effect
  '
  Dim ctrlType As Type
  Dim pi As Reflection.PropertyInfo
 
 
  ctrlType = aControl.GetType
  Try
  pi = ctrlType.GetProperty("DoubleBuffered", Reflection.BindingFlags.Instance Or  Reflection.BindingFlags.NonPublic)
  pi.SetValue(aControl, True, Nothing)
  Catch ex As Exception
 
  End Try
  End Sub
#End Region
 
#Region "Device emulator"
  Private Shared  prng As  New Random
  Private minMessSz As Integer
  Private slow As Boolean  = False
 
#Region "send lines"
  'Private Sub SimulateDevice()
  '  '
  '  ' SIMULATE DEVICE - reqires Loopback
  '  '
  '  'protocol is
  '  'send lines
  '  Dim lorem As String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Four score and seven years ago..."
  '  Dim loremch() As Char = lorem.ToCharArray
  '  Dim t As Task = Task.Run(Sub()
  '  Dim ct As Integer = 1
  '  Dim dbg As Boolean = False '  True '  
  '  Dim mess As New List(Of Byte) 'used to construct the message
  '  Dim uiUpd As Stopwatch = Stopwatch.StartNew
  '  Const cr As Byte = 13
  '  Const lf As Byte = 10
  '  Const lastCH As Byte = 126 '~
  '  Do
  '   mess.Clear()
  '   Dim endCHIdx As Integer = prng.Next(1, loremch.Length)
  '   'some chars
  '   mess.AddRange(System.Text.Encoding.GetEncoding(28591).GetBytes(loremch, 0, endCHIdx))
  '   mess.Add(lastCH)
  '   mess.Add(cr)
  '   mess.Add(lf)
  '   Dim sendThis() As Byte = mess.ToArray 'convert list to array for sending
  '   Try
  '   mySerialPort.Write(sendThis, 0, sendThis.Length) '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< SEND
  '   Catch ex As Exception
  '   Stop
  '   End Try
  '   ct += 1
  '   If uiUpd.ElapsedMilliseconds >= 250 Then
  '   uiUpd.Restart()
  '   Me.BeginInvoke(Sub()
  '   TextBox2.Text = "SMsgs: " & ct.ToString("n0")
  '   End Sub)
  '   End If
  '   If dbg OrElse slow Then
  '   Threading.Thread.Sleep(25) 'slow down for testing
  '   End If
  '  Loop While runThreads.WaitOne(0)
  '  End Sub)
  'End Sub
#End Region
 
#Region "complex"
  Private Sub  SimulateDevice()
  '
  ' SIMULATE DEVICE - reqires Loopback
  '
  'protocol is
  '0  STX = one byte = 2
  '1  ID = four bytes = sequence
  '5  DL = one byte = length of data (1 - 255)
  '6  data - string <= 255 chars
  '  CRC = one byte = XOR of all data bytes
  Dim lorem As String  = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean non vehicula sem, lacinia consequat sapien. Four score and seven years ago..."
  lorem = lorem & lorem '& lorem
  Dim loremch() As Char  = lorem.ToCharArray
  minMessSz = 8
  Dim t As Task = Task.Run(Sub()
   Const stx As Byte  = 2
   Dim ct As Integer  = 1
   Dim dbg As Boolean  = False  '  True '  
   Dim mess As New  List(Of Byte) 'used to construct the message
   Dim uiUpd As Stopwatch = Stopwatch.StartNew
   Do
   mess.Clear()
   mess.Add(stx)
   'send seq
   mess.AddRange(BitConverter.GetBytes(ct))
   'Dim endCHIdx As Integer = prng.Next(1, 5)'four bytes. for debug
   Dim endCHIdx As Integer  = prng.Next(1, 256)
   mess.Add(CByte(endCHIdx)) 'data length
   'some chars
   mess.AddRange(System.Text.Encoding.GetEncoding(28591).GetBytes(loremch, 0, endCHIdx))
   Dim crc As Integer  = 0
   For x As Integer  = 6 To  mess.Count - 1
   crc = crc Xor  mess(x)
   Next
   mess.Add(CByte(crc And  255))
   Dim sendThis() As Byte  = mess.ToArray 'convert list to array for sending
   Try
   mySerialPort.Write(sendThis, 0, sendThis.Length) '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< SEND
   Catch ex As Exception
   Stop
   End Try
   ct += 1
   If uiUpd.ElapsedMilliseconds >= 250 Then
   uiUpd.Restart()
   Me.BeginInvoke(Sub()
   TextBox2.Text = "SMsgs: " & ct.ToString("n0")
   End Sub)
   End If
   If dbg OrElse slow Then
   Threading.Thread.Sleep(25) 'slow down for testing
   End If
   Loop While  runThreads.WaitOne(0)
  End Sub)
  End Sub
#End Region
#End Region
 
#Region "Other"
  Private dispPaused As Boolean  = False
 
  Private Sub  Button1_Click(sender As Object, e As  EventArgs) Handles Button1.Click
  'Pause button
 
  dispPaused = Not  dispPaused
  If dispPaused Then
  Button1.Text = "Resume Display"
  Else
  Button1.Text = "Pause Display"
  End If
  End Sub
 
  Private Sub  Timer1_Tick(sender As Object, e As  EventArgs) Handles Timer1.Tick
  Label1.Text = DateTime.Now.ToString("HH:mm:ss.f")
  End Sub
 
  Private Sub  chkSlow_CheckedChanged(sender As Object, e As  EventArgs) Handles chkSlow.CheckedChanged
  slow = chkSlow.Checked
  End Sub
#End Region
End Class

Back to top

  The complete windows form project is here. SerialPortBaseProject

  The project is also in the TechNet Glallery. SerialPortProject 

Conclusion

By using the approach presented for all serial port projects the only coding needed will be for a devices specific protocol and how the data is used.

References