Introduction
In Part 1 we developed a standard Arduino Sketch framework. Now we will build a .NET Class Library which will encapsulate all the communications with the board. The download contains a full Visual Studio (2015) solution including a demo Windows Froms application. The demo app will be discussed in Part 3.
Background
In Part 1 we developed a framework for a sketch which responded to defined standard commands with a defined response format. It is capable of returning information about what commands the sketch responds to and what values and status data the sketch provides as well as what input and output pins are being used.
Now we are going to switch to the control computer end. We are specifically using a Windows environment so we are using Visual Studio 2015 as the development environment.
I make no apology for the fact that the code here is in VB.NET. At the time we started work with Arduinos VB was still a slightly more developed language than C#, and also it is considerably more readable to part-time programmers whose main interest is getting a research application written and the results published rather than producing finely crafted code.
Recently of course C# has made considerable strides, and Microsoft are indicating that this is now their main focus, so in fact we have switched to using C# at work, but this library is still in VB.NET. If you are a C# coder who has never used VB then it is probably easier for you to convert VB to C# than it is for a VB programmer who hasn’t used C# to convert the other way.
OK, so what problems are we going to solve.
The first issue you come up against if you are needing to use serial comms to talk to your Arduino on a Windows PC is that you must have the appropriate serial driver installed. This happens automatically when you install the Arduino IDE, or you can install the drivers manually if you cannot, or do not want to install the IDE on the machine. (We have some kits going out to secondary schools that have PCs with heavily locked down desktops – OneClick application install generally works ok, but installing the driver in these cases can be a nightmare.)
Assuming you’ve got the driver installed your next problem is to find which virtual serial port Windows has assigned to your board. Rather than mess around manually finding and changing the Windows assigned port number the first function we will produce is simply to scan the available ports and find ones which have “Arduino” in the name. Generally all the boards report a name in the form “Arduino [boardtype]”.
Since you may have more than one board connected we want to get a list of all the boards found.
Using the code
Start a new project in Visual Studio of type ClassLibrary. Call it “ArduinoSerialClassLib
”
Add a new item of type Class to your project. Call it “DetectArduinoClass
”
We will be using the System.Management namespace to scan the ManagementObjects in Windows for serial ports whose name matches what we are looking for.
Imports System.Management
You will probably need to add the reference to System.Management in the References section under My Project.
This class is going to have one public property of type Dictionary(Of String, String)
which will contain a list of Com Ports as the items and their names as the definition where the name matches what we are looking for.
As well as the constructor New()
we will have a function Rescan()
which we can call if we didn’t find anything the first time without having to dispose and recreate the object. We can also make this look for a different name pattern and add any matches to the existing dictionary.
Public Class DetectArduinoClass
Private _portList As New Dictionary(Of String, String)
Public ReadOnly Property PortList As Dictionary(Of String, String)
Get
Return _portlist
End Get
End Property
Public Sub New(Optional target As String = "Arduino")
ScanPorts(target)
End Sub
Public Sub Rescan(Optional target As String = "Arduino", Optional clear As Boolean = False)
If clear Then _portList.Clear
ScanPorts(target)
End Sub
Private Sub ScanPorts(ByVal target As String)
Try
Dim searcher As New ManagementObjectSearcher("root\cimv2", "SELECT * FROM Win32_SerialPort")
Dim name, pname As String
For Each queryObj As ManagementObject In searcher.Get()
name = queryObj("Name")
If name.Substring(0, Len(target)) = target Then
pname = queryObj("DeviceID")
_portList.Add(pname,name)
End If
Next
Catch err As ManagementException
End Try
End Sub
End Class
That's it. We now have a property PortList
containing a collection of Comport names and device names.
As you can see in both the constructor New() and Rescan() take an optional parameter of a string to match the start of the name, so if you want to look for a specific type of board you could call New(“Arduino Leonardo”)
or any other name to search for other serial port devices
One of Windows many quirks is that if the Arduino is already connected when the computer is booted then it fails to read the name from the device and simply reports it as “USB Serial Device” In this case you either need to disconnect and reconnect the board so that the name is read correctly, or you can search for “USB Serial”. You could modify the ScanPorts()
function above to automatically look for “USB Serial Device” if “Arduino” is not found.
Once this is compiled and the DLL is included as a reference in your main project you can simply include code like this, for example in Form.Load()
Dim det As New DetectArduinoClass()
If det.PortList.Count > 0 Then
Else
Det.Rescan("USB Serial")
End If
Don’t forget to add the reference to your new class library and include
Imports ArduinoSerialClassLib
at the top of the code.
OK, now we have identified what port has got our board connected let’s add a new class to the library to encapsulate connecting to and communicating with the chosen board.
Add a new class to the project called “ArduinoSerialClass
”
We are going to be handling IO Ports so we will need to Import the System.IO.Ports namespace and also pay attention to when things need to be declared as Shared so that they can be used across threads.
Imports System.IO.Ports
Public Class ArdunioSerialClass
Private Shared WithEvents _comPort As New SerialPort
When we create the object of type ArduinoSerialClass we are going to tell it which COM Port to use because we have already used a DetectArduinoClass object to get the list of devices and ports. We might also want to tell it what baud rate to use in case we are expecting something non-standard, but we will default to 115,200 baud.
Shared Private _baud As integer
Public Sub New(ByVal pname As String, Optional ByVal baud As Integer = 115200)
ClearProperties()
_baud = baud
If SetupPort(pname) Then
_portFound = True
_comPortName = pname
GetFullStatus()
Else
_portFound = False
_comPortName = ""
End If
End Sub
Private Function SetupPort(pname As String) As Boolean
With _comPort
.BaudRate = _baud
.Parity = Parity.None
.DataBits = 8
.StopBits = StopBits.One
.Handshake = Handshake.None
.DtrEnable = True
.RtsEnable = True
.PortName = pname
.WriteTimeout = 2000
.ReadTimeout = 500
.NewLine = chr(10)
.ReadBufferSize = 16384
End With
_comPort.Close()
If Not _comPort.IsOpen Then
Try
_comPort.Open()
Return True
Catch ex As Exception
Return False
End Try
End If
Return False
End Function
OK, so we have private variables to flag if we have connected to the serial port and the port name. These will both be available as ReadOnly Public properties.
Private Shared _portFound As Boolean = False
Public ReadOnly Property PortFound As Boolean
Get
Return _portFound
End Get
End Property
Private _comPortName As String = "n/a"
Public ReadOnly Property ComPortName As String
Get
Return _comPortName
End Get
End Property
We will have a number of other Public properties which we will be exposing, many of these will be ReadOnly status reports, but LoopTime, SendSerialValues, and SendIntValues will be ReadWrite.
ClearProperties()
is going to clear down any existing values in case we are rescanning and GetFullStatus()
is going to issue appropriate commands to populate all the definitions for commands, status, values and pins.
Private Sub ClearProperties()
_comPortName = ""
_loopTime = -1
_fstatus.Sketch="n/a"
_fstatus.Author="n/a"
_fstatus.CompDate="n/a"
_fstatus.Ver="n/a"
_fstatus.UnitID="n/a"
_cmds.Clear()
_vals.Clear()
_valDesc.Clear()
_stats.Clear()
_statDesc.Clear()
_ins.Clear()
_outs.Clear()
End Sub
Private Sub GetFullStatus()
_comPort.Write(qSketch)
_comPort.Write(qCmdDefn)
_comPort.Write(qStatusDefn)
_comPort.Write(qValDefn)
_comPort.Write(qIns)
_comPort.Write(qOuts)
_comPort.WriteLine(cmdLoopTime)
End Sub
Incoming serial data will be handled entirely by a private function that responds to _comPort.DataReceived
events.
Private Shared Sub Receiver(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs) Handles _comPort.DataReceived
Try
Do
Dim gotsome As String = _comPort.ReadLine
If Not IsNothing(gotsome) Then
ParseRxString(gotsome)
End If
Loop Until (_comPort.BytesToRead = 0)
Catch ex As Exception
End Try
End Sub
So we are going to have a function ParseRxString()
which will deal with complete lines of received data. Essentially this is going to recognise response strings and update relevant property the data structure.
The standard commands and response strings are pre-defined, but the Sketch specific commands and responses will be read using GetFullStatus()
and appropriate data structures populated to expose them as properties of the class object.
Before we look at ParseRxString()
we need to do two things. Firstly, define the data structures for the properties, and secondly define all of the standard commands and response matching those we created as consts in the sketch in Part 1.
The private variables behind the properties will need to be Shared so that they can be accessed across threads – the update is responding to a DataRecieved event which may be on a different thread to the instance of the class object in the main programme.
For example to handle getting and setting the main programme loopTime on the Arduino:
Public Const cmdLoopTime As Char = Chr(20)
Shared Private _loopTime As integer
Public Property LoopTime As Integer
Get
Return _loopTime
End Get
Set(value As Integer)
_comPort.WriteLine(cmdLoopTime & value)
End Set
End Property
When we set the new value we don’t directly update the local private variable. We simply issue the command to the board, which will respond with the new value and the serial data receive handler will do the local update.
Similarly for the properties SendSerialValues
, and SendIntValues
both of which are Boolean type.
Mostly we will use Dictionaries to store the collections of identifiers and values or descriptions. For the sketch info we will create a simple structure
Public Structure FirmwareInfo
Dim Sketch As String
Dim Ver As String
Dim Author As String
Dim CompDate As String
Dim UnitID As String
End Structure
Shared Private _fstatus As New FirmwareInfo
Public ReadOnly Property FStatus As FirmwareInfo
Get
Return _fstatus
End Get
End Property
If we wanted to be clever we could parse the version string into a Version data type so that the application can easily check for a minimum or maximum required version. Similarly we could also parse the compile date string into a DateTime type – but as noted previously it is in a stupid format so I’ll leave that to you to sort out.
Here is the definition for the structure to hold the actual live values that the sketch returns, and also the descriptors for them, both of which can be accessed by the control programme as properties of the ArduinoSerialClass object it has created.
Shared Private _vals As New Dictionary(Of String, String)
Public ReadOnly Property Vals As Dictionary(Of String, String)
Get
Return _vals
End Get
End Property
Shared Private _valDesc As New Dictionary(Of String, String)
Public ReadOnly Property ValDesc As Dictionary(Of String, String)
Get
Return _valDesc
End Get
End Property
Similar structures will be used for the sketch status and descriptions, the commands that the sketch responds to in addition to the standard ones, and the input and output pins.
Now we can look at the basics of the ParseRxString()
function. We will take a string consisting of one or more lines from the serial input. We will break it into separate lines on any valid line break or carriage return character.
For each line we will try and parse the first character as an identifier, the second character as either "=" indicating a status string follows, or ":" indicating that a dynamic value follows, and the remainder of the string as the useful bit.
If it is a status line then we can look for the standard identifiers and update the appropriate status properties, or if the identifier is in the dictionary of sketch status definitions _stats
as a key then we update the value there.
For the standard status lines which are returning command, value, pin or status definitions of the form "C=L=Enable LED" then we further break the data part "L=Enable LED" into a key "L" and a definition "Enable LED" and add it to the dictionary of valid sketch commands _cmds
Imports System.Text.RegularExpressions
Private Shared Sub ParseRxString(rxstr As String)
Dim lines As String() = rxstr.Split(New String() {"\n\r", "\r", "\n"}, StringSplitOptions.RemoveEmptyEntries)
Dim item As String
Dim ky, op, cont As String
For i = 0 To lines.Count - 1
item = Regex.Replace(lines(i).Trim, "\p{C}+", "")
If Len(item) > 2 Then
ky = item.Substring(0, 1)
op = item.Substring(1, 1)
data = item.Substring(2)
Select Case op
Case "="
Select Case ky
Case lblSendSerialValues
_sendSerialEnabled = (data="1")
Case lblCmdDesc
Dim tmp As String() = data.Split("=")
If _cmds.ContainsKey(tmp(0))
_cmds(tmp(0)) = tmp(1)
Else
_cmds.Add(tmp(0),tmp(1))
End If
Case lblSketch
_fstatus.Sketch = data
Case lblLoopTime
Try
_loopTime = CInt(data)
Catch ex As Exception
End Try
Case Else
If _stats.ContainsKey(ky) Then
_stats(ky) = data
Else
End If
End Select
Case ":"
If _vals.ContainsKey(ky) Then
_vals(ky) = data
Else
End If
Case Else
End Select
Else
End If
Next
End Sub
In addition to all that we will need to have defined all the constants for the identifiers and commands. Download the full class library to see the detail.
A Close()
function to dispose of the ComPort nicely rather than wait for garbage collection will be useful, and we will have a DoCmd()
function which will simply write a valid command to the serial port. This will be the primary means by which the control program executes sketch specific commands.
Public Sub Close()
_comPort.Close()
_comPort.Dispose()
_portFound = False
_comPortName = ""
End Sub
Public Sub DoCmd(ByVal cmd As String)
If cmds.ContainsKey(Left(cmd(1)) Then
_comPort.WriteLine(cmd)
End If
End Sub
A SendRawString()
function could also be made available in order to send arbitary data to the board - but this would rather break the point of encapsulating the whole thing and having the sketch on the board efine what commands it is going to recognise.
In the download is a Visual Studio Soultion containing two projects - the Class Library, and a Demo Control programme which will use the class library to find a connected Arduino, attach to it and download its capabilities and display its status and values.
Looking briefly at the demo application there is a single Form. When it is loaded it creates a DetectArduinoClass object and if any boards are found creates a ArduinoSerialClass object to communicate with the first board found.
It then dynamically creates controls for the specific board and displays the values as recieved. We will look at this in more detail in part 3.
A note on polling and interupt events.
On the Arduino board the main programme loop is polling the input pins and reporting values. The interval is defined by the loopTime
variable in the Sketch which can be set using the LoopTime
property of the ArduinoSerialClass object.
The control programme will need to poll the ArduinoSerialClass object to read the values as required. If we are not to miss any changes in value then we must poll at at least twice the polling rate in the Arduino - see Nyquist frequency for background info.
In this simple demo we are using a Forms.Timer object to do the polling, so every time the Arduino loopTime is changed we adjust the timer interval to be slightly less than half that.
An alternative technique not included in this demo is to have the ArduinoSerialClass object raise an event every time a value that matters to us is changed. This works well for slowly changing values (for example heart rate) and means we don't need to poll the value property in the ArduinoSerialClass. It is also apprpriate for inputs that generate an interupt on the Arduino where we may have quite a long loopTime but want to respond immediately when a trigger is detected.
Demo App
There is a simple Demo App included in the sample code to illustrate the use of the ArduinoSerialClass. A description, code samples, screen shots and suggestions for improvement will form a third part of this series.
Points of Interest
We have created a class library which contains two objects. The first scans available comports listed in the Windows management and identifes that match a name we are looking for.
Having found a port with an Arduino connected we now use the other object to handle all the communications with the board. It automatically reads the board capabilities and makes the properties available to the application.
The Demo application shows how to use these objects. The full class library code also includes the ability for the application to define specific values to raise an event when they change rather than having to poll them continuously.
Future Developments
How you use this will depend on the specific devices you are interfacing. Obvious improvements are to the error handling both in the Sketch and in the Class Library - all of this code has been stripped out for the purposes of these articles.
History
First published 28th July 2016
2nd August updated Code file now all tested and working