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.
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 |
+---------------+
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.
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.
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
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
- Loopback Plugs...
- Author participates on MSDN and VBForums as dbasnett