Using Visual Basic .NET to write to LCD 20x4 Bricklet

For this project we are assuming, that you have a Visual Basic .NET development environment set up and that you have a rudimentary understanding of the Visual Basic .NET language.

If you are totally new to Visual Basic .NET itself you should start here. If you are new to the Tinkerforge API, you should start here.

Goals

We are setting the following goals for this project:

  • Temperature, ambient light, humidity and air pressure should be shown on the LCD 20x4 Bricklet,
  • the measured values should be updated automatically when they change and
  • the measured values should be formated to be easily readable.

Since this project will likely run 24/7, we will also make sure that the application is as robust towards external influences as possible. The application should still work when

  • Bricklets are exchanged (i.e. we don't rely on UIDs),
  • Brick Daemon isn't running or is restarted,
  • WIFI Extension is out of range or
  • Weather Station is restarted (power loss or accidental USB removal).

In the following we will show step-by-step how this can be achieved.

Step 1: Discover Bricks and Bricklets

To start off, we need to define where our program should connect to:

Const HOST As String = "localhost"
Const PORT As Integer = 4223

If the WIFI Extension is used or if the Brick Daemon is running on a different PC, you have to exchange "localhost" with the IP address or hostname of the WIFI Extension or PC.

When the program is started, we need to register the EnumerateCallback callback and the Connected callback and trigger a first enumerate:

Sub Main()
    ipcon = New IPConnection()
    ipcon.Connect(HOST, PORT)

    AddHandler ipcon.EnumerateCallback, AddressOf EnumerateCB
    AddHandler ipcon.Connected, AddressOf ConnectedCB

    ipcon.Enumerate()
End Sub

The enumerate callback is triggered if a Brick gets connected over USB or if the Enumerate() function is called. This allows to discover the Bricks and Bricklets in a stack without knowing their types or UIDs beforehand.

The connected callback is triggered if the connection to the WIFI Extension or to the Brick Daemon got established. In this callback we need to trigger the enumerate again, if the reason is an auto reconnect:

Sub ConnectedCB(ByVal sender As IPConnection, ByVal connectedReason as Short)
    If connectedReason = IPConnection.CONNECT_REASON_AUTO_RECONNECT Then
        ipcon.Enumerate()
    End If
End Sub

An auto reconnect means, that the connection to the WIFI Extension or to the Brick Daemon was lost and could subsequently be established again. In this case the Bricklets may have lost their configurations and we have to reconfigure them. Since the configuration is done during the enumeration process (see below), we have to trigger another enumeration.

Step 1 put together:

Module WeatherStation
    Const HOST As String = "localhost"
    Const PORT As Integer = 4223

    Sub ConnectedCB(ByVal sender As IPConnection, ByVal connectedReason as Short)
        If connectedReason = IPConnection.CONNECT_REASON_AUTO_RECONNECT Then
            ipcon.Enumerate()
        End If
    End Sub

    Sub Main()
        ipcon = New IPConnection()
        ipcon.Connect(HOST, PORT)

        AddHandler ipcon.EnumerateCallback, AddressOf EnumerateCB
        AddHandler ipcon.Connected, AddressOf ConnectedCB

        ipcon.Enumerate()
    End Sub
End Module

Step 2: Initialize Bricklets on Enumeration

During the enumeration we want to configure all of the weather measuring Bricklets. Doing this during the enumeration ensures that Bricklets get reconfigured if the stack was disconnected or there was a power loss.

The configurations should be performed on first startup (ENUMERATION_TYPE_CONNECTED) as well as whenever the enumeration is triggered externally by us (ENUMERATION_TYPE_AVAILABLE):

Sub EnumerateCB(ByVal sender As IPConnection, ByVal uid As String, _
                ByVal connectedUid As String, ByVal position As Char, _
                ByVal hardwareVersion() As Short, ByVal firmwareVersion() As Short, _
                ByVal deviceIdentifier As Integer, ByVal enumerationType As Short)
    If enumerationType = IPConnection.ENUMERATION_TYPE_CONNECTED Or _
       enumerationType = IPConnection.ENUMERATION_TYPE_AVAILABLE Then

The LCD 20x4 configuration is simple, we want the current text cleared and we want the backlight on:

If deviceIdentifier = BrickletLCD20x4.DEVICE_IDENTIFIER Then
    brickletLCD = New BrickletLCD20x4(UID, ipcon)
    brickletLCD.ClearDisplay()
    brickletLCD.BacklightOn()

We configure the Ambient Light, Humidity and Barometer Bricklet to return their respective measurements continuously with a period of 1000ms (1s):

Else If deviceIdentifier = BrickletAmbientLight.DEVICE_IDENTIFIER Then
    brickletAmbientLight = New BrickletAmbientLight(UID, ipcon)
    brickletAmbientLight.SetIlluminanceCallbackPeriod(1000)
    AddHandler brickletAmbientLight.Illuminance, AddressOf IlluminanceCB
Else If deviceIdentifier = BrickletHumidity.DEVICE_IDENTIFIER Then
    brickletHumidity = New BrickletHumidity(UID, ipcon)
    brickletHumidity.SetHumidityCallbackPeriod(1000)
    AddHandler brickletHumidity.Humidity, AddressOf HumidityCB
Else If deviceIdentifier = BrickletBarometer.DEVICE_IDENTIFIER Then
    brickletBarometer = New BrickletBarometer(UID, ipcon)
    brickletBarometer.SetAirPressureCallbackPeriod(1000)
    AddHandler brickletBarometer.AirPressure, AddressOf AirPressureCB
End If

This means that the Bricklets will call the IlluminanceCB, HumidityCB and AirPressureCB callback functions whenever the value has changed, but with a maximum period of 1000ms.

Step 2 put together:

Sub EnumerateCB(ByVal sender As IPConnection, ByVal uid As String, _
                ByVal connectedUid As String, ByVal position As Char, _
                ByVal hardwareVersion() As Short, ByVal firmwareVersion() As Short, _
                ByVal deviceIdentifier As Integer, ByVal enumerationType As Short)
    If enumerationType = IPConnection.ENUMERATION_TYPE_CONNECTED Or _
       enumerationType = IPConnection.ENUMERATION_TYPE_AVAILABLE Then
        If deviceIdentifier = BrickletLCD20x4.DEVICE_IDENTIFIER Then
            brickletLCD = New BrickletLCD20x4(UID, ipcon)
            brickletLCD.ClearDisplay()
            brickletLCD.BacklightOn()
        Else If deviceIdentifier = BrickletAmbientLight.DEVICE_IDENTIFIER Then
            brickletAmbientLight = New BrickletAmbientLight(UID, ipcon)
            brickletAmbientLight.SetIlluminanceCallbackPeriod(1000)
            AddHandler brickletAmbientLight.Illuminance, AddressOf IlluminanceCB
        Else If deviceIdentifier = BrickletHumidity.DEVICE_IDENTIFIER Then
            brickletHumidity = New BrickletHumidity(UID, ipcon)
            brickletHumidity.SetHumidityCallbackPeriod(1000)
            AddHandler brickletHumidity.Humidity, AddressOf HumidityCB
        Else If deviceIdentifier = BrickletBarometer.DEVICE_IDENTIFIER Then
            brickletBarometer = New BrickletBarometer(UID, ipcon)
            brickletBarometer.SetAirPressureCallbackPeriod(1000)
            AddHandler brickletBarometer.AirPressure, AddressOf AirPressureCB
        End If
    End If
End Sub

Step 3: Show measurements on display

We want a neat arrangement of the measurements on the display, such as:

Illuminanc 137.39 lx
Humidity    34.10 %
Air Press  987.70 mb
Temperature 22.64 °C

The decimal marks and the units should be below each other. To achieve this we use two characters for the unit, two decimal places and crop the name to use the maximum characters that are left. That's why "Illuminanc" is missing its final "e".

Dim text As String = String.Format("{0,6:###.00}", value)

The code above converts a floating point value to a string according to the given format specification. The result will be at least 6 characters long with 2 decimal places, filled up with spaces from the left if it would be shorter than 6 characters otherwise.

Sub IlluminanceCB(ByVal sender As BrickletAmbientLight, ByVal illuminance As Integer)
    Dim text As String = String.Format("Illuminanc {0,6:###.00} lx", illuminance/10.0)
    brickletLCD.WriteLine(0, 0, text)
End Sub

Sub HumidityCB(ByVal sender As BrickletHumidity, ByVal humidity As Integer)
    Dim text As String = String.Format("Humidity   {0,6:###.00} %", humidity/10.0)
    brickletLCD.WriteLine(1, 0, text)
End Sub

Sub AirPressureCB(ByVal sender As BrickletBarometer, ByVal airPressure As Integer)
    Dim text As String = String.Format("Air Press {0,7:####.00} mb", airPressure/1000.0)
    brickletLCD.WriteLine(2, 0, text)
End Sub

We are still missing the temperature. The Barometer Bricklet can measure temperature, but it doesn't have a callback for it. As a simple workaround we can retrieve the temperature in the AirPressureCB callback function:

Sub AirPressureCB(ByVal sender As BrickletBarometer, ByVal airPressure As Integer)
    Dim text As String = String.Format("Air Press {0,7:####.00} mb", airPressure/1000.0)
    brickletLCD.WriteLine(2, 0, text)

    Dim temperature As Integer = sender.GetChipTemperature()
    text = String.Format("Temperature {0,5:##.00} {1}C", temperature/100.0, Chr(&HDF))
    brickletLCD.WriteLine(3, 0, text)
End Sub

Step 3 put together:

Sub IlluminanceCB(ByVal sender As BrickletAmbientLight, ByVal illuminance As Integer)
    Dim text As String = String.Format("Illuminanc {0,6:###.00} lx", illuminance/10.0)
    brickletLCD.WriteLine(0, 0, text)
End Sub

Sub HumidityCB(ByVal sender As BrickletHumidity, ByVal humidity As Integer)
    Dim text As String = String.Format("Humidity   {0,6:###.00} %", humidity/10.0)
    brickletLCD.WriteLine(1, 0, text)
End Sub

Sub AirPressureCB(ByVal sender As BrickletBarometer, ByVal airPressure As Integer)
    Dim text As String = String.Format("Air Press {0,7:####.00} mb", airPressure/1000.0)
    brickletLCD.WriteLine(2, 0, text)

    Dim temperature As Integer = sender.GetChipTemperature()
    ' &HDF == ° on LCD 20x4 charset
    text = String.Format("Temperature {0,5:##.00} {1}C", temperature/100.0, Chr(&HDF))
    brickletLCD.WriteLine(3, 0, text)
End Sub

That's it. If we would copy these three steps together in one file and execute it, we would have a working Weather Station!

There are some obvious ways to make the output better. The name could be cropped according to the exact space that is available (depending on the number of digits of the measured value). Also, reading the temperature in the AirPressureCB callback function is suboptimal. If the air pressure doesn't change, we won't update the temperature. It would be better to read the temperature in a different thread in an endless loop with a one second sleep after each read. But we want to keep this code as simple as possible.

However, we do not meet all of our goals yet. The program is not yet robust enough. What happens if it can't connect on startup? What happens if the enumerate after an auto reconnect doesn't work?

What we need is error handling!

Step 4: Error handling and Logging

On startup, we need to try to connect until the connection works:

while True
    Try
        ipcon.Connect(HOST, PORT)
        Exit While
    Catch e As System.Net.Sockets.SocketException
        System.Console.WriteLine("Connection Error: " + e.Message)
        System.Threading.Thread.Sleep(1000)
    End Try
End While

and we need to try enumerating until the message goes through:

while True
    try
        ipcon.Enumerate()
        Exit While
    Catch e As NotConnectedException
        System.Console.WriteLine("Enumeration Error: " + e.Message)
        System.Threading.Thread.Sleep(1000)
    End Try
End While

With these changes it is now possible to first start the program and connect the Weather Station afterwards.

We also need to make sure, that we only write to the LCD if it is already initialized:

Sub IlluminanceCB(ByVal sender As BrickletAmbientLight, ByVal illuminance As Integer)
    If brickletLCD IsNot Nothing Then
        Dim text As String = String.Format("Illuminanc {0,6:###.00} lx", illuminance/10.0)
        brickletLCD.WriteLine(0, 0, text)
        System.Console.WriteLine("Write to line 0: " + text)
    End If
End Sub

and that we have to deal with errors during the initialization:

If deviceIdentifier = BrickletAmbientLight.DEVICE_IDENTIFIER Then
    Try
        brickletAmbientLight = New BrickletAmbientLight(UID, ipcon)
        brickletAmbientLight.SetIlluminanceCallbackPeriod(1000)
        AddHandler brickletAmbientLight.Illuminance, AddressOf IlluminanceCB
        System.Console.WriteLine("Ambient Light initialized")
    Catch e As TinkerforgeException
        System.Console.WriteLine("Ambient Light init failed: " + e.Message)
        brickletAmbientLight = Nothing
    End Try
End If

Additionally we added some logging. With the logging we can later find out what exactly caused a problem, if the Weather Station failed for some time period.

For example, if we connect to the Weather Station via Wi-Fi and we have regular auto reconnects, it likely means that the Wi-Fi connection is not very stable.

Step 5: Everything put together

That's it! We are already done with our Weather Station and all of the goals should be met.

Now all of the above put together (download):

Imports Tinkerforge

Module WeatherStation
    Const HOST As String = "localhost"
    Const PORT As Integer = 4223

    Private ipcon As IPConnection = Nothing
    Private brickletLCD As BrickletLCD20x4 = Nothing
    Private brickletAmbientLight As BrickletAmbientLight = Nothing
    Private brickletAmbientLightV2 As BrickletAmbientLightV2 = Nothing
    Private brickletAmbientLightV3 As BrickletAmbientLightV3 = Nothing
    Private brickletHumidity As BrickletHumidity = Nothing
    Private brickletHumidityV2 As BrickletHumidityV2 = Nothing
    Private brickletBarometer As BrickletBarometer = Nothing
    Private brickletBarometerV2 As BrickletBarometerV2 = Nothing

    Sub IlluminanceCB(ByVal sender As BrickletAmbientLight, ByVal illuminance As Integer)
        If brickletLCD IsNot Nothing Then
            Dim text As String = String.Format("Illuminanc {0,6:###.00} lx", illuminance/10.0)
            brickletLCD.WriteLine(0, 0, text)
            System.Console.WriteLine("Write to line 0: " + text)
        End If
    End Sub

    Sub IlluminanceV2CB(ByVal sender As BrickletAmbientLightV2, ByVal illuminance As Long)
        If brickletLCD IsNot Nothing Then
            Dim text As String = String.Format("Illumina {0,8:###.00} lx", illuminance/100.0)
            brickletLCD.WriteLine(0, 0, text)
            System.Console.WriteLine("Write to line 0: " + text)
        End If
    End Sub

    Sub IlluminanceV3CB(ByVal sender As BrickletAmbientLightV3, ByVal illuminance As Long)
        If brickletLCD IsNot Nothing Then
            Dim text As String = String.Format("Illumina {0,8:###.00} lx", illuminance/100.0)
            brickletLCD.WriteLine(0, 0, text)
            System.Console.WriteLine("Write to line 0: " + text)
        End If
    End Sub

    Sub HumidityCB(ByVal sender As BrickletHumidity, ByVal humidity As Integer)
        If brickletLCD IsNot Nothing Then
            Dim text As String = String.Format("Humidity   {0,6:###.00} %", humidity/10.0)
            brickletLCD.WriteLine(1, 0, text)
            System.Console.WriteLine("Write to line 1: " + text)
        End If
    End Sub

    Sub HumidityV2CB(ByVal sender As BrickletHumidityV2, ByVal humidity As Integer)
        If brickletLCD IsNot Nothing Then
            Dim text As String = String.Format("Humidity   {0,6:###.00} %", humidity/100.0)
            brickletLCD.WriteLine(1, 0, text)
            System.Console.WriteLine("Write to line 1: " + text)
        End If
    End Sub

    Sub AirPressureCB(ByVal sender As BrickletBarometer, ByVal airPressure As Integer)
        If brickletLCD IsNot Nothing Then
            Dim text As String = String.Format("Air Press {0,7:####.00} mb", airPressure/1000.0)
            brickletLCD.WriteLine(2, 0, text)
            System.Console.WriteLine("Write to line 2: " + text)

            Dim temperature As Integer
            Try
                temperature = sender.GetChipTemperature()
            Catch e As TinkerforgeException
                System.Console.WriteLine("Could not get temperature" + e.Message)
                Return
            End Try

            ' &HDF == ° on LCD 20x4 charset
            text = String.Format("Temperature {0,5:##.00} {1}C", temperature/100.0, Chr(&HDF))
            brickletLCD.WriteLine(3, 0, text)
            System.Console.WriteLine("Write to line 3: " + text.Replace(Chr(&HDF), "°"C))
        End If
    End Sub

    Sub AirPressureV2CB(ByVal sender As BrickletBarometerV2, ByVal airPressure As Integer)
        If brickletLCD IsNot Nothing Then
            Dim text As String = String.Format("Air Press {0,7:####.00} mb", airPressure/1000.0)
            brickletLCD.WriteLine(2, 0, text)
            System.Console.WriteLine("Write to line 2: " + text)

            Dim temperature As Integer
            Try
                temperature = sender.GetTemperature()
            Catch e As TinkerforgeException
                System.Console.WriteLine("Could not get temperature" + e.Message)
                Return
            End Try

            ' &HDF == ° on LCD 20x4 charset
            text = String.Format("Temperature {0,5:##.00} {1}C", temperature/100.0, Chr(&HDF))
            brickletLCD.WriteLine(3, 0, text)
            System.Console.WriteLine("Write to line 3: " + text.Replace(Chr(&HDF), "°"C))
        End If
    End Sub

    Sub EnumerateCB(ByVal sender As IPConnection, ByVal uid As String, _
                    ByVal connectedUid As String, ByVal position As Char, _
                    ByVal hardwareVersion() As Short, ByVal firmwareVersion() As Short, _
                    ByVal deviceIdentifier As Integer, ByVal enumerationType As Short)
        If enumerationType = IPConnection.ENUMERATION_TYPE_CONNECTED Or _
           enumerationType = IPConnection.ENUMERATION_TYPE_AVAILABLE Then
            If deviceIdentifier = BrickletLCD20x4.DEVICE_IDENTIFIER Then
                Try
                    brickletLCD = New BrickletLCD20x4(UID, ipcon)
                    brickletLCD.ClearDisplay()
                    brickletLCD.BacklightOn()
                    System.Console.WriteLine("LCD 20x4 initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("LCD 20x4 init failed: " + e.Message)
                    brickletLCD = Nothing
                End Try
            Else If deviceIdentifier = BrickletAmbientLight.DEVICE_IDENTIFIER Then
                Try
                    brickletAmbientLight = New BrickletAmbientLight(UID, ipcon)
                    brickletAmbientLight.SetIlluminanceCallbackPeriod(1000)
                    AddHandler brickletAmbientLight.Illuminance, AddressOf IlluminanceCB
                    System.Console.WriteLine("Ambient Light initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("Ambient Light init failed: " + e.Message)
                    brickletAmbientLight = Nothing
                End Try
            Else If deviceIdentifier = BrickletAmbientLightV2.DEVICE_IDENTIFIER Then
                Try
                    brickletAmbientLightV2 = New BrickletAmbientLightV2(UID, ipcon)
                    brickletAmbientLightV2.SetConfiguration(BrickletAmbientLightV2.ILLUMINANCE_RANGE_64000LUX, _
                                                            BrickletAmbientLightV2.INTEGRATION_TIME_200MS)
                    brickletAmbientLightV2.SetIlluminanceCallbackPeriod(1000)
                    AddHandler brickletAmbientLightV2.Illuminance, AddressOf IlluminanceV2CB
                    System.Console.WriteLine("Ambient Light 2.0 initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("Ambient Light 2.0 init failed: " + e.Message)
                    brickletAmbientLightV2 = Nothing
                End Try
            Else If deviceIdentifier = BrickletAmbientLightV3.DEVICE_IDENTIFIER Then
                Try
                    brickletAmbientLightV3 = New BrickletAmbientLightV3(UID, ipcon)
                    brickletAmbientLightV3.SetConfiguration(BrickletAmbientLightV3.ILLUMINANCE_RANGE_64000LUX, _
                                                            BrickletAmbientLightV3.INTEGRATION_TIME_200MS)
                    brickletAmbientLightV3.SetIlluminanceCallbackConfiguration(1000, True, "x"C, 0, 0)
                    AddHandler brickletAmbientLightV3.IlluminanceCallback, AddressOf IlluminanceV3CB
                    System.Console.WriteLine("Ambient Light 3.0 initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("Ambient Light 3.0 init failed: " + e.Message)
                    brickletAmbientLightV3 = Nothing
                End Try
            Else If deviceIdentifier = BrickletHumidity.DEVICE_IDENTIFIER Then
                Try
                    brickletHumidity = New BrickletHumidity(UID, ipcon)
                    brickletHumidity.SetHumidityCallbackPeriod(1000)
                    AddHandler brickletHumidity.Humidity, AddressOf HumidityCB
                    System.Console.WriteLine("Humidity initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("Humidity init failed: " + e.Message)
                    brickletHumidity = Nothing
                End Try
            Else If deviceIdentifier = BrickletHumidityV2.DEVICE_IDENTIFIER Then
                Try
                    brickletHumidityV2 = New BrickletHumidityV2(UID, ipcon)
                    brickletHumidityV2.SetHumidityCallbackConfiguration(1000, True, "x"C, 0, 0)
                    AddHandler brickletHumidityV2.HumidityCallback, AddressOf HumidityV2CB
                    System.Console.WriteLine("Humidity 2.0 initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("Humidity 2.0 init failed: " + e.Message)
                    brickletHumidityV2 = Nothing
                End Try
            Else If deviceIdentifier = BrickletBarometer.DEVICE_IDENTIFIER Then
                Try
                    brickletBarometer = New BrickletBarometer(UID, ipcon)
                    brickletBarometer.SetAirPressureCallbackPeriod(1000)
                    AddHandler brickletBarometer.AirPressure, AddressOf AirPressureCB
                    System.Console.WriteLine("Barometer initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("Barometer init failed: " + e.Message)
                    brickletBarometer = Nothing
                End Try
            Else If deviceIdentifier = BrickletBarometerV2.DEVICE_IDENTIFIER Then
                Try
                    brickletBarometerV2 = New BrickletBarometerV2(UID, ipcon)
                    brickletBarometerV2.SetAirPressureCallbackConfiguration(1000, True, "x"C, 0, 0)
                    AddHandler brickletBarometerV2.AirPressureCallback, AddressOf AirPressureV2CB
                    System.Console.WriteLine("Barometer 2.0 initialized")
                Catch e As TinkerforgeException
                    System.Console.WriteLine("Barometer 2.0 init failed: " + e.Message)
                    brickletBarometerV2 = Nothing
                End Try
            End If
        End If
    End Sub

    Sub ConnectedCB(ByVal sender As IPConnection, ByVal connectedReason as Short)
        If connectedReason = IPConnection.CONNECT_REASON_AUTO_RECONNECT Then
            System.Console.WriteLine("Auto Reconnect")
            while True
                Try
                    ipcon.Enumerate()
                    Exit While
                Catch e As NotConnectedException
                    System.Console.WriteLine("Enumeration Error: " + e.Message)
                    System.Threading.Thread.Sleep(1000)
                End Try
            End While
        End If
    End Sub

    Sub Main()
        ipcon = New IPConnection()
        while True
            Try
                ipcon.Connect(HOST, PORT)
                Exit While
            Catch e As System.Net.Sockets.SocketException
                System.Console.WriteLine("Connection Error: " + e.Message)
                System.Threading.Thread.Sleep(1000)
            End Try
        End While

        AddHandler ipcon.EnumerateCallback, AddressOf EnumerateCB
        AddHandler ipcon.Connected, AddressOf ConnectedCB

        while True
            try
                ipcon.Enumerate()
                Exit While
            Catch e As NotConnectedException
                System.Console.WriteLine("Enumeration Error: " + e.Message)
                System.Threading.Thread.Sleep(1000)
            End Try
        End While

        System.Console.WriteLine("Press key to exit")
        System.Console.ReadLine()
        ipcon.Disconnect()
    End Sub
End Module