Sunday, 15 September 2013

Visual Basic and Serial Ports

BASIC was the first language I ever learned (BBC Micro BASIC to be precise).  And though I have moved on to many other languages, BASIC holds a special place in my heart. Last week, a friend of mine was complaining about having issues getting BASIC working on the latest version of Windows.
After asking him I realised that he was still using VB 6 (which was a 1990's application).  So I decided to put a morning aside to testing Visual Basic on Windows 7, and in particular getting the serial port code working.

So lets get to it!

1. Install VB.net 

I'm using the 2010 express version, but other versions should be quite similar.
A quick trip to the MS website

With the usual setup:
Its a 60MB download (not too bad)
And we are ready to go.

2. Start new project



3. Design the form

Make sure you add a serial port component from the toolbox

My form looked like this (see the serial port down at the bottom):


4. Add the code

The connection & sending was dead easy.

Private Sub btnConnect_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConnect.Click
    SerialPort1.Close() ' just in case
    ' setup serial port
    SerialPort1.PortName = txtPort.Text
    SerialPort1.BaudRate = 9600
    SerialPort1.Open()
End Sub

Private Sub BtnStop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnStop.Click
    SerialPort1.Write("0")
End Sub

Private Sub btnGo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGo.Click
    SerialPort1.Write("1")
End Sub

If you get any blue underlining, it just means its missing a library, right click on the offending item & import the library.

5. Add the receiving code

First task, select the serial port & get up its properties.  Then you need to give the name of a function to be called when there is data waiting to be received.

You then double click on the item & it brings you to the code editor & you can just type in your code:

Private Sub OnDataIn(ByVal sender As System.Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
    Dim sp As SerialPort = CType(sender, SerialPort)
    Dim indata As String = sp.ReadExisting()
    System.Diagnostics.Debug.WriteLine(indata)
End Sub

This ALMOST works, but often I found that VB would be reading too fast & not get the entire line of data, just part of it
So a small change to the code & we are good
Private Sub OnDataIn(ByVal sender As System.Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
    Dim sp As SerialPort = CType(sender, SerialPort)
    ' note: ReadExisting() will only read what is in the buffer right now
    '   which means you might get only half the message & have to reassemble it
    ' Its far easier to use ReadLine() which will block until the whole line is in
    'Dim indata As String = sp.ReadExisting()
     Dim indata As String = sp.ReadLine()
     System.Diagnostics.Debug.WriteLine(indata)
End Sub

6. Deal with the dragons

So last task is just take this text and update the UI. Simple, Right? Wrong!

Errr....
Lets try and address this one

To keep an eye on the serial port, VB runs a thread. But it’s not the same thread that is running the rest of the UI and all our other code.
Thread B is not allowed to access the UI stuff which is owned by thread A, because it’s basically very dangerous to let two threads play around with the same things at the same time.

7. Solving the problem with an invoke

I tracked down a fairly simple piece of code to get around this issue from http://www.dreamincode.net/forums/blog/143/entry-2337-handling-the-dreaded-cross-thread-exception/
It looked like this:

' this delegate will help us later
' for those from a C/C++ background, think 'function pointer'
Delegate Sub ThreadSafeDelegate(ByVal ctrl As Control, ByVal str As String)

'The method with the delegate signature
Private Sub ChangeText(ByVal ctrl As Control, ByVal str As String)
    If ctrl.InvokeRequired Then
        ctrl.Invoke(New ThreadSafeDelegate(AddressOf ChangeText), New Object() {ctrl, str})
    Else
        ctrl.Text = str
    End If
End Sub

What its doing, is if the function if called from thread B, the thread invokes the function (it puts the function & all the arguments in a safe place). Then later when thread A comes by, it notices the function needs to be called & calls it.
I then added my version to do the job I really wanted
Private Sub AppendText(ByVal ctrl As Control, ByVal str As String)
    ' if not thread safe, recall this function
    If ctrl.InvokeRequired Then
        ctrl.Invoke(New ThreadSafeDelegate(AddressOf AppendText), New Object() {ctrl, str})
        Return
    End If
    ' do the real work
    Dim lb As ListBox = CType(ctrl, ListBox) 'get the list box
    If lb.Items.Count >= 10 Then ' if its too ful remove the old stuff
        lb.Items.RemoveAt(0)
    End If
    lb.Items.Add(str) ' add the new stuff
End Sub

And my receive code works just fine now

Private Sub OnDataIn(ByVal sender As System.Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived
    Dim sp As SerialPort = CType(sender, SerialPort)
    Dim indata As String = sp.ReadLine()
    System.Diagnostics.Debug.WriteLine(indata)
    'lstDataIn.Items.Add(indata)
    AppendText(lstDataIn, indata) ' thread save version
End Sub

Conclusion:

Apart from the thread safe issue, this was so easy.  But then VB was always designed for a quick UI hackup.  All this work took less than 2 hours (including the time to overcome the dragon).  Having to add this extra thread code might make it a little harder, but VB.Net seems to be able to still deliver the quick prototypes that VB 6 did 20 years ago.
This is not the only way to some this problem.  I could have done all this with C# as well, it would probably have been the same thing, with the same cross thread issue as well.  But today was VB’s day.

Happy coding,
Mark

No comments:

Post a Comment