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 toolboxMy 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