Handling SerialPort & GUI Threads

Joined
Apr 13, 2008
Messages
8
Programming Experience
3-5
Hello everyone,

I know there's been a decent amount of discussion on here about Serial Port communications, but I can't seem to find what I need.

I'm writing a program that sets certain variables in a servo drive. Basically, I don't think I understand enough about the separate threads that the GUI and serial port use, to get this accomplished yet.

Basically, I want to send out a command like "I996" (with a linefeed/CR)... no problem. But I want to take what the drive responds back with, and store that value into a textbox on the GUI.

Also, when I set a value with a command like "I11=4", I want to wait a short period, and make sure it doesn't respond with "ERR03" or somethign similar.

I've tried some serial port comms before, but I always have a problem with GUI / SerialPort thread issues. Any explanation and help would be so greatly appreciated.

Sincerely,
Jonathon Reinhart
 
Start with the following :-

VB.NET:
Option Explicit On
Option Strict On

Imports System.IO.Ports
Imports System.Data.SqlClient

Public Class frmMain

    Public RackToDisplay As Integer = 1

    Public WithEvents COM1 As SerialPort

End Class

Code to open COM port when form is created
VB.NET:
    Public Sub New()

        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

        ' open COM port
        COM1 = New SerialPort("COM1", 9600, Parity.None, 8, StopBits.One)
        COM1.ReadBufferSize = 1024
        COM1.Open()

    End Sub

Close the COM port when you get rid of the form
VB.NET:
    Private Sub frmMain_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        If COM1.IsOpen Then COM1.Close()
        COM1.Dispose()
    End Sub

VB.NET:
    Private SendDataToServo(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        COM1.Write(String.Format("I996{0}", Environment.NewLine))
    End Sub

Check you can get that working before I show you the receive code :D
 
Hey, thanks for the quick reply. I've gotten most of that written, and although it's been a while since I worked on this, I am 99% sure that the sending data works. I'll get out my laptop and null-modem cable tomorrow, and open hyperterm, and verify that that data is sent.

After I send that data, I want to wait for a short while, and make sure it doesn't respond with an "ERR03" string or similar.

So actually, just a function that sends a string, then returns either FALSE, or the string of data returned from the remote device would be exactly what I need:


VB.NET:
strCurI996 = SendCommand("I996")
msgBox(strCurI996)    ' This would show whatever the drive responded with.

VB.NET:
strError = SendCommand("I11=5")
If (strError = "") Then
    ' Success!
Else
    msgBox("Error writing I11: " & vbNewLine & strError)
End If
 
Last edited:
OK, to read the response data, here's an example. Put a new label on your form - called Label1. Now add :-
VB.NET:
    Private DataRead as string = ""

    Private Sub COM1_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles COM1.DataReceived
        Label1.Invoke(New myDelegate(AddressOf updateLabel), New Object() {})
    End Sub

    Public Delegate Sub myDelegate()

    Public Sub updateLabel()
        DataRead = ""
        DataRead = COM1.ReadLine
        Label1.Text = String.Format ("#{0}#", DataRead)
    End Sub

I always add the '#' to the front and the back when displaying to see if any additonal spaces / newline characters have been sent.

If you dont get all the data you were expecting in the label, replace
VB.NET:
        DataRead = COM1.ReadLine
with
VB.NET:
        DataRead = ""
        While DataRead.Contains(ETX) = False
            DataRead &= COM1.ReadExisting
            System.Threading.Thread.Sleep(100)
        End While
although you will have to check in the servodrive manual as to what the termination character is, and replace ETX with your character.
 
In your example, the send and receive still happen asynchronously from each other, correct? In this application, and I'm sure others as well, we need to have the response before we carry on executing other commands. Your example doesn't have provisions for that.

So how about this plan:
Have two shared, global bits, gbGotData and gbTimeout. When we send a command, we clear both of those bits. Those bits will be set by the DataReceived and Timeout Events, respectively. So we send, and then wait indefinitely while those bits are both false. Then depending which bit becomes true, is how the function will carry on (I can't recall exactly what these are named, but you can get the picture):

VB.NET:
Shared gbGotData as Boolean
Shared gbTimeout as Boolean

Private Sub ... Handles SerialPort.NewDataReceived
   gbGotData = True
   ' Can we store that response to a global string here????
End Sub

Private Sub ... Handles SerialPort.Timeout
   gbTimeout = True
End Sub


Private Sub SendCommand(strCmd as String) as String
   gbGotData = False
   gbTimeout = False
   SerialPort.WriteLine(strCmd)
   System.Threading.Thread.Sleep(10)

   While (Not gbGotData And gbTimeout)
      ' Do Nothing
      ' This will probably cause much lag in the GUI
   End While

   If (gbGotData) Then
      ' Return the global returned string here?
   End If
   If (gbTimeout) Then
      ' Return empty string here?
   End If
End Sub
 
If you want to read synchronously you don't use the asynchronous event, but instead use one of the Read methods when you want to read.
 
we need to have the response before we carry on executing other commands. Your example doesn't have provisions for that.
Correct.

VB.NET:
Those bits will be set by the DataReceived and Timeout Events, respectively.  So we send, and then wait indefinitely while those bits are both false.
Logical, but for your specific example, you are dealing with a servodrive, so you are unlikely to ever get a timeout error unless the power to the drive is removed.

I'd start by thinking of it as a logical set of instructions :-
VB.NET:
Send "I996"
Wait for OK
Send "I932" (etc)

I'd rewrite it like this :-

VB.NET:
    Private CommandSent as string = ""
    Private LastCommand as string = ""
    Private ExpectedResponse as string = ""

    Private SendDataToServo(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        CommandSent = "SpeedInstruction"
        LastCommand = "I996"
        COM1.Write(String.Format("{1}{0}", Environment.NewLine, LastCommand))
    End Sub

    Private DataRead as string = ""

    Private Sub COM1_DataReceived(ByVal sender As Object, ByVal e As System.IO.Ports.SerialDataReceivedEventArgs) Handles COM1.DataReceived
        Label1.Invoke(New myDelegate(AddressOf updateLabel), New Object() {})
    End Sub

    Public Delegate Sub myDelegate()

    Public Sub updateLabel()
        DataRead = ""
        DataRead = COM1.ReadLine
        Label1.Text = String.Format ("#{0}#", DataRead)
        Select Case CommandSent
                Case "SpeedInstruction"
                        ExpectedResponse = "OK"
                Case "PositionInstruction"
                        ExpectedResponse = "PositionReached"
        End Select
        if CommandSent = "SpeedInstruction" andalso DataRead = ExpectedResponse andalso LastCommand = "I932" then
                'I996 Speed reached
                        CommandSent = "SpeedInstruction"
                        LastCommand = "I932"
                        COM1.Write(String.Format("{1}{0}", Environment.NewLine, LastCommand))
        elseif CommandSent = "PositionInstruction" andalso DataRead = ExpectedResponse then
                'Positon reached
        End If
    End Sub

It may all be asynchronous, but all the reponse code will be in one place. Remember - if you do it this way, you can act on data sent from the drive even if it hasnt been sent an instruction :D
 
Ah, OK, I'm starting to see it much clearer now. However, we're still a little off the same page. I know it doesn't change a whole lot about how the code will work, but just to clear up, this is what I'm trying to do. These are just setup parameters, for communication to the drive (over a fiber-optic ring). Here's how an example conversation would go with the drive (from Hyperterm). {CR} is enter key.

User Entry
Drive Response
Comment

VB.NET:
[COLOR="SeaGreen"]I996{CR}[/COLOR]              [COLOR="Orange"]Ask the drive for the value of I996[/COLOR]
[COLOR="Red"]$0F4001[/COLOR]               [COLOR="orange"]So I996 is currently $0F4001[/COLOR]

[COLOR="SeaGreen"]I996=$0F4020{CR}[/COLOR]      [COLOR="Orange"]Set I996 to be $0F4020[/COLOR]
[COLOR="Red"]ERR03[/COLOR]                 [COLOR="orange"]Error. Just try again.[/COLOR]
[COLOR="SeaGreen"]I996=$0F4020{CR}[/COLOR]      [COLOR="Orange"]Set I996 to be $0F4020[/COLOR]
                      [COLOR="orange"]No response, so it accepted.[/COLOR]

[COLOR="SeaGreen"]I996{CR}[/COLOR]              [COLOR="Orange"]Ask the drive for the value of I996[/COLOR]
[COLOR="Red"]$0F4020[/COLOR]               [COLOR="orange"]I996 is now $0F4020[/COLOR]

The procedure would continue like this for 11 other variables. So I'm looking to have my DownloadParams() function look this:

VB.NET:
SetValue("I11", "5")
SetValue("I12", "3")
SetValue("I996", "$0F4010")

Where SetValue will (eventually) attempt to write the param, and if it receives an error, try to write it again (up to MaxRetries times).
 
VB.NET:
    Private CommunicationInProgress as boolean = false
    Private ExpectedResponse as string = ""

    Private Sub SetValue(ByVal DriveParameter As string, ByVal ParameterValue As string)
        CommunicationInProgress = true
        LastCommand = DriveParameter
        ExpectedResponse = ParameterValue
        COM1.Write(String.Format("{1}{0}", Environment.NewLine, LastCommand))
    End Sub

    Private Sub DownloadParams()
        SetValue("I11", "5")
        While CommunicationInProgress = true
                system.threading.thread.sleep(10)
        End While
        SetValue("I12", "3")
        While CommunicationInProgress = true
                system.threading.thread.sleep(10)
        End While
        SetValue("I996", "$0F4010")
        While CommunicationInProgress = true
                system.threading.thread.sleep(10)
        End While
    End Sub

    Public Sub updateLabel()
        '...
        if DataRead = ExpectedResponse then
                'correct response received
                CommunicationInProgress = false
        End If
    End Sub

I havent tested the above code, so it may have bugs - but it should give you some direction. Given your drive doesnt respond on accepted values, I would just wait a short period after sending the Set command and send a Read command.
 
VB.NET:
    Private CommunicationInProgress as boolean = false

    Private Sub DownloadParams()
        SetValue("I11", "5")
        While CommunicationInProgress = true
                system.threading.thread.sleep(10)
        End While
        SetValue("I12", "3")
 ...
    End Sub

This doesn't really end up working correctly. The app hangs for me.

Nonetheless, I ended up getting it working exactly the way I wanted to. First of all, I found that my drive was using screwy line endings. It would always reply with LF (0x0A), after you send it a message followed by CR (0x0D). Then, if it replyed with text (either the stupid ERR03, or the value I'm requesting), it would reply with "Response CR LF". So It made it awkward. So I couldn't use any of the built in SerialPort.Read* functions. Here's what I came up with:

VB.NET:
    Private Function SerialPortRead() As String
        ' This function attempts to properly read the serial port, and return the data.
        ' Drive. Always replies with {0A} first.
        ' Then if there is data, it is returned, followed by {0D} {0A}

        Dim strData As String

        ' This will attempt to read the first byte.  If we can't get that, then we
        ' should get the timeout we're looking for.
        Try
            strData = SerialPort1.ReadTo(Chr(&HA))
            If (strData.Length > 0) Then
                MsgBox("Drive returned data before initial LF:" & _
                    vbNewLine & ShowNonPrint(strData)) 'This should be empty. always.
            End If
        Catch ex As Exception
            MsgBox("Coud not read to initial LF: " & vbNewLine & ex.Message)
        End Try

        ' Now read and trim the CR LF from the remaining data.
        strData = SerialPort1.ReadExisting()
        If (strData.Length > 0) Then
            Try
                strData = strData.Substring(0, strData.Length - 2)
            Catch ex As Exception
                MsgBox("Serial data out of sync." & vbNewLine & vbNewLine _
                    & ShowNonPrint(strData))
            End Try
        End If

        Return strData
    End Function


So that works to correctly parse any data that the drive spits back. ShowNonPrint() is what I wrote to show me text, with the CR's and LF's printed out as their hex values.

I'm no longer using the SerialPort.DataReceived event at all. I was at first, but successive read functions would spit out read requests too fast, and the DataReceived handler would end up mashing all of the responses together. So here's my ReadValueFromDrive() function:

VB.NET:
    Private Function ReadValueFromDrive(ByVal strName As String) As String
        Dim strData As String

        SerialPort1.DiscardInBuffer()
        SerialPort1.WriteLine(strName)      ' write it with 0x0D at the end.

        ' Allow __ms for reply.
        System.Threading.Thread.Sleep(My.Settings.ReadDelay)

        strData = SerialPortRead()
        'MsgBox("SerialPortRead returned: " & vbNewLine & ShowNonPrint(strData))

        Return strData
    End Function


And my WriteValueToDrive() function:
VB.NET:
    Private Function WriteValueToDrive(ByVal strName As String, ByVal strValue As String)
        Dim bResult As Boolean = False
        Dim strResponse As String = ""
        Dim strCommand As String = strName + "=" + strValue

        SerialPort1.DiscardInBuffer()
        SerialPort1.WriteLine(strCommand)   'Writes the line with 0x0D ending

        System.Threading.Thread.Sleep(My.Settings.WriteDelay)  'Wait 10ms for a full reply.

        strResponse = SerialPortRead()

        If strResponse = "" Then
            ' The drive just responded with a newline. so we're fine.
            bResult = True
        Else
            bResult = False     'We must have gotten an error.
            MsgBox("Response from drive after write attempt for " & strName & ": " & ShowNonPrint(strResponse))
        End If

        Return bResult
    End Function


Now, I left generous delays in there, to ensure all the data was in the buffer before I attempted to read it. Well when doing 12 parameters (maybe more in the future), this caused the GUI to hang a lot. So I implemented the successive reads and writes in a BackgroundWorker. (I can include that code, if anyone would like me to).


I really appreciate everyone's help with this. It did end up being much more straightforward than I expected. I think I was just caught in the thought that the reads would have to be done from the Delegate, and this was really confusing me. I'm successfully able to read and write all parameters to the drive now.
 
Back
Top