Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / IoT / Arduino

Sketch framework and Class Library - Part 2

4.60/5 (3 votes)
2 Aug 2016CPOL11 min read 11.2K   119  
A standard interface for multiple Arduino boards with different firmware

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.

VB.NET
 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.

VB.NET
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)
    'target will be matched against the start of the ManagementObject name
		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
            'Do something, possibly report err.Message and carry on ...
		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()

VB.NET
Dim det As New DetectArduinoClass()
If det.PortList.Count > 0 Then
    'Yay we’ve found at least one board, lets do some stuff
Else
    Det.Rescan("USB Serial")
    'Have we found one yet ...
End If

Don’t forget to add the reference to your new class library and include

VB.NET
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.

VB.NET
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.

VB.NET
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.RequestToSend. NB we need None on WIndows 8+
        .Handshake = Handshake.None
		.DtrEnable = True
		.RtsEnable = True
		.PortName = pname
		.WriteTimeout = 2000 ' Max time to wait for CTS =2sec
		.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
			'Do something about the error .. ex.Message
			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.

VB.NET
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.

VB.NET
Private Sub ClearProperties()
    'clear all public & private properties
    _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.

VB.NET
Private Shared Sub Receiver(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs) Handles _comPort.DataReceived
	Try
		Do
			Dim gotsome As String = _comPort.ReadLine
'There is a potential problem here if we miss an end of line, this will hang until we get one, or timeout occurs – you might like to fix this by reading byte by byte until you’ve got a complete line or the buffer is empty.
            If Not IsNothing(gotsome) Then
				ParseRxString(gotsome)
			End If
        Loop Until (_comPort.BytesToRead = 0)  
' Don't return if more bytes have become available in the meantime
        Catch ex As Exception 'Typically a TimeoutException
            'Handle this error nicely ... ex.Message 
        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:

VB.NET
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

VB.NET
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.

VB.NET
Shared Private _vals As New Dictionary(Of String, String) ' this will contain the actual values
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) ' this will contain the descriptions for the values
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

VB.NET
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
'remove any remaining control chars from start or end of the line
	item = Regex.Replace(lines(i).Trim, "\p{C}+", "") 
	If Len(item) > 2 Then
		'do we have a valid identifier character (ky) and is it a value or a status (op)?
		ky = item.Substring(0, 1)  ' this be the identifier
		op = item.Substring(1, 1)   ' this should be either = or :
		data = item.Substring(2)
		Select Case op
			Case "="	
'this is a status so do status stuff
				Select Case ky
					Case lblSendSerialValues
'here we are simply updating the property directly
						_sendSerialEnabled = (data="1")
					Case lblCmdDesc	
'in this case we have to identify the command
'and then either update or add to the dictionary
						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
'and so on for all the other definition returns...

					Case lblSketch 
						_fstatus.Sketch = data
'and similarly for Version, Date, Author and UnitID...

					Case lblLoopTime
						Try
							_loopTime = CInt(data)
						Catch ex As Exception
							'ignore it or report the error	
						End Try
'and likewise for the standard boolean properties SendSerialValues and SendIntValues ...

					Case Else
	'this should be a sketch specific status so we can update the value in _stats
						If _stats.ContainsKey(ky) Then
							_stats(ky) = data
						Else 
	'we have an unrecognised status ky so maybe we report it as an error or something...
						End If
				End Select
				Case ":"	'this is a value
				If _vals.ContainsKey(ky) Then  'update the value in the dictionary
					_vals(ky) = data
				Else 
	'we have an unrecognised value so do something with it ...
				End If 
			Case Else
	'this line had an op char that was not = or : - was it a fragment, is it an error?
			End Select
		Else 
	'this was some fragment of length less than 2 - is it an error?
		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.

VB.NET
Public Sub Close()
    _comPort.Close()
    _comPort.Dispose()
    _portFound = False
    _comPortName = ""
End Sub

Public Sub DoCmd(ByVal cmd As String)
' check that we are being send a valid command 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)