Using Ruby to write to LCD 20x4 Bricklet – Tinkerforge

- Getting Started
  - Primer
  - Tutorials
  - FAQ
- Hardware
  - Bricks
  - Bricklets
  - Master Extensions
  - Power Supplies
  - Accessories
  - Timeline
- Software
  - Brick Daemon (brickd)
  - Brick Viewer (brickv)
  - API Bindings
  - Firmwares and Plugins
  - Device Identifier
  - Source Code and Bug Tracking
  - Programming Interface
- Starter Kits
  - Weather Station
    - Features
    - Description
    - Technical Specifications
    - Resources
    - Firmware updating and first tests
    - Demo Application
    - Construction
    - Projects
      - Display Environment Measurements on LCD
        - Using C/C++
        - Using C#
        - Using Delphi/Lazarus
        - Using Java
        - Using PHP
        - Using Python
        - Using Ruby
          - Goals
          - Step 1: Discover Bricks and Bricklets
          - Step 2: Initialize Bricklets on Enumeration
          - Step 3: Show measurements on display
          - Step 4: Error handling and Logging
          - Step 5: Everything put together
        - Using Visual Basic .NET
      - Internet of Things / Connect to Xively
      - Embed Live Measurements on Website
      - Show Statistics with Button Control
      - Smart Home Integration using openHAB
    - Further Enhancements
  - Hardware Hacking
  - Server Room Monitoring
  - Blinkenlights
  - Internet of Things
  - Camera Slider
- Embedded Boards
  - Raspberry Pi
- Specifications
  - Technical Data
  - TCP/IP Protocol
  - Modbus Protocol
  - Wireshark Dissector

# Using Ruby to write to LCD 20x4 Bricklet

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

If you are totally new to Ruby 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:

    HOST = 'localhost'
    PORT = 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 `::CALLBACK_ENUMERATE` callback and the `::CALLBACK_CONNECTED` callback and trigger a first enumerate:

    ipcon = IPConnection.new
    ipcon.connect HOST, PORT

    ipcon.register_callback(IPConnection::CALLBACK_ENUMERATE) do |uid, connected_uid, position,
                                                                  hardware_version, firmware_version,
                                                                  device_identifier, enumeration_type|
    end

    ipcon.register_callback(IPConnection::CALLBACK_CONNECTED) do |connected_reason|
    end

    ipcon.enumerate

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:

    ipcon.register_callback(IPConnection::CALLBACK_CONNECTED) do |connected_reason|
      if connected_reason == IPConnection::CONNECT_REASON_AUTO_RECONNECT
        ipcon.enumerate
      end
    end

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:

    HOST = 'localhost'
    PORT = 4223

    ipcon = IPConnection.new
    ipcon.connect HOST, PORT

    ipcon.register_callback(IPConnection::CALLBACK_ENUMERATE) do |uid, connected_uid, position,
                                                                  hardware_version, firmware_version,
                                                                  device_identifier, enumeration_type|
    end

    ipcon.register_callback(IPConnection::CALLBACK_CONNECTED) do |connected_reason|
      if connected_reason == IPConnection::CONNECT_REASON_AUTO_RECONNECT
        ipcon.enumerate
      end
    end

    ipcon.enumerate

## 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`):

    ipcon.register_callback(IPConnection::CALLBACK_ENUMERATE) do |uid, connected_uid, position,
                                                                  hardware_version, firmware_version,
                                                                  device_identifier, enumeration_type|
      if enumeration_type == IPConnection::ENUMERATION_TYPE_CONNECTED or
         enumeration_type == IPConnection::ENUMERATION_TYPE_AVAILABLE

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

    if device_identifier == BrickletLCD20x4::DEVICE_IDENTIFIER
      lcd = BrickletLCD20x4.new uid, ipcon
      lcd.clear_display
      lcd.backlight_on

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

    elsif device_identifier == BrickletAmbientLight::DEVICE_IDENTIFIER
      ambient_light = BrickletAmbientLight.new uid, ipcon
      ambient_light.set_illuminance_callback_period 1000
      ambient_light.register_callback(BrickletAmbientLight::CALLBACK_ILLUMINANCE) do |illuminance|
      end
    elsif device_identifier == BrickletHumidity::DEVICE_IDENTIFIER
      humidity = BrickletHumidity.new uid, ipcon
      humidity.set_humidity_callback_period 1000
      humidity.register_callback(BrickletHumidity::CALLBACK_HUMIDITY) do |humidity|
      end
    elsif device_identifier == BrickletBarometer::DEVICE_IDENTIFIER
      barometer = BrickletBarometer.new uid, ipcon
      barometer.set_air_pressure_callback_period 1000
      barometer.register_callback(BrickletBarometer::CALLBACK_AIR_PRESSURE) do |air_pressure|
      end
    end

This means that the Bricklets will call the `CALLBACK_ILLUMINANCE`, `CALLBACK_HUMIDITY` and `CALLBACK_AIR_PRESSURE` callback functions whenever the value has changed, but with a maximum period of 1000ms.

Step 2 put together:

    ipcon.register_callback(IPConnection::CALLBACK_ENUMERATE) do |uid, connected_uid, position,
                                                                  hardware_version, firmware_version,
                                                                  device_identifier, enumeration_type|
      if enumeration_type == IPConnection::ENUMERATION_TYPE_CONNECTED or
         enumeration_type == IPConnection::ENUMERATION_TYPE_AVAILABLE
        if device_identifier == BrickletLCD20x4::DEVICE_IDENTIFIER
          lcd = BrickletLCD20x4.new uid, ipcon
          lcd.clear_display
          lcd.backlight_on
        elsif device_identifier == BrickletAmbientLight::DEVICE_IDENTIFIER
          ambient_light = BrickletAmbientLight.new uid, ipcon
          ambient_light.set_illuminance_callback_period 1000
          ambient_light.register_callback(BrickletAmbientLight::CALLBACK_ILLUMINANCE) do |illuminance|
          end
        elsif device_identifier == BrickletHumidity::DEVICE_IDENTIFIER
          humidity = BrickletHumidity.new uid, ipcon
          humidity.set_humidity_callback_period 1000
          humidity.register_callback(BrickletHumidity::CALLBACK_HUMIDITY) do |humidity|
          end
        elsif device_identifier == BrickletBarometer::DEVICE_IDENTIFIER
          barometer = BrickletBarometer.new uid, ipcon
          barometer.set_air_pressure_callback_period 1000
          barometer.register_callback(BrickletBarometer::CALLBACK_AIR_PRESSURE) do |air_pressure|
          end
        end
      end
    end

## 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".

    text = "%6.2f" % 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.

    ambient_light.register_callback(BrickletAmbientLight::CALLBACK_ILLUMINANCE) do |illuminance|
      text = 'Illuminanc %6.2f lx' % (illuminance/10.0)
      lcd.write_line 0, 0, text
    end

    humidity.register_callback(BrickletHumidity::CALLBACK_HUMIDITY) do |humidity|
      text = 'Humidity   %6.2f %%' % (humidity/10.0)
      lcd.write_line 1, 0, text
    end

    barometer.register_callback(BrickletBarometer::CALLBACK_AIR_PRESSURE) do |air_pressure|
      text = 'Air Press %7.2f mb' % (air_pressure/1000.0)
      lcd.write_line 2, 0, text
    end

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 `CALLBACK_AIR_PRESSURE` callback function:

    barometer.register_callback(BrickletBarometer::CALLBACK_AIR_PRESSURE) do |air_pressure|
      text = 'Air Press %7.2f mb' % (air_pressure/1000.0)
      lcd.write_line 2, 0, text

      temperature = barometer.get_chip_temperature
      text = 'Temperature %5.2f %sC' % [(temperature/100.0), 0xDF.chr]
      lcd.write_line 3, 0, text
    end

Step 3 put together:

    ambient_light.register_callback(BrickletAmbientLight::CALLBACK_ILLUMINANCE) do |illuminance|
      text = 'Illuminanc %6.2f lx' % (illuminance/10.0)
      lcd.write_line 0, 0, text
    end

    humidity.register_callback(BrickletHumidity::CALLBACK_HUMIDITY) do |humidity|
      text = 'Humidity   %6.2f %%' % (humidity/10.0)
      lcd.write_line 1, 0, text
    end

    barometer.register_callback(BrickletBarometer::CALLBACK_AIR_PRESSURE) do |air_pressure|
      text = 'Air Press %7.2f mb' % (air_pressure/1000.0)
      lcd.write_line 2, 0, text

      temperature = barometer.get_chip_temperature
      # 0xDF == ° on LCD 20x4 charset
      text = 'Temperature %5.2f %sC' % [(temperature/100.0), 0xDF.chr]
      lcd.write_line 3, 0, text
    end

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 `CALLBACK_AIR_PRESSURE` 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
      begin
        ipcon.connect HOST, PORT
        break
      rescue Exception => e
        puts 'Connection Error: ' + e
        sleep 1
      end
    end

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

    while true
      begin
        ipcon.enumerate
        break
      rescue Exception => e
        puts 'Enumerate Error: ' + e
        sleep 1
      end
    end

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:

    ambient_light.register_callback(BrickletAmbientLight::CALLBACK_ILLUMINANCE) do |illuminance|
      if lcd != nil
        text = 'Illuminanc %6.2f lx' % (illuminance/10.0)
        lcd.write_line 0, 0, text
        puts "Write to line 0: #{text}"
      end
    end

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

    if device_identifier == BrickletAmbientLight::DEVICE_IDENTIFIER
      begin
        ambient_light = BrickletAmbientLight.new uid, ipcon
        ambient_light.set_illuminance_callback_period 1000
        ambient_light.register_callback(BrickletAmbientLight::CALLBACK_ILLUMINANCE) do |illuminance|
        end
        puts 'Ambient Light initialized'
      rescue Exception => e
        ambient_light = nil
        puts 'Ambient Light init failed: ' + e
      end
    end

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):

    #!/usr/bin/env ruby
    # -*- ruby encoding: utf-8 -*-

    require 'tinkerforge/ip_connection'
    require 'tinkerforge/bricklet_lcd_20x4'
    require 'tinkerforge/bricklet_ambient_light'
    require 'tinkerforge/bricklet_humidity'
    require 'tinkerforge/bricklet_barometer'

    include Tinkerforge

    HOST = 'localhost'
    PORT = 4223

    lcd = nil
    ambient_light = nil
    humidity = nil
    barometer = nil

    ipcon = IPConnection.new
    while true
      begin
        ipcon.connect HOST, PORT
        break
      rescue Exception => e
        puts 'Connection Error: ' + e
        sleep 1
      end
    end

    ipcon.register_callback(IPConnection::CALLBACK_ENUMERATE) do |uid, connected_uid, position,
                                                                  hardware_version, firmware_version,
                                                                  device_identifier, enumeration_type|
      if enumeration_type == IPConnection::ENUMERATION_TYPE_CONNECTED or
         enumeration_type == IPConnection::ENUMERATION_TYPE_AVAILABLE
        if device_identifier == BrickletLCD20x4::DEVICE_IDENTIFIER
          begin
            lcd = BrickletLCD20x4.new uid, ipcon
            lcd.clear_display
            lcd.backlight_on
            puts 'LCD 20x4 initialized'
          rescue Exception => e
            lcd = nil
            puts 'LCD 20x4 init failed: ' + e
          end
        elsif device_identifier == BrickletAmbientLight::DEVICE_IDENTIFIER
          begin
            ambient_light = BrickletAmbientLight.new uid, ipcon
            ambient_light.set_illuminance_callback_period 1000
            ambient_light.register_callback(BrickletAmbientLight::CALLBACK_ILLUMINANCE) do |illuminance|
              if lcd != nil
                text = 'Illuminanc %6.2f lx' % (illuminance/10.0)
                lcd.write_line 0, 0, text
                puts "Write to line 0: #{text}"
              end
            end
            puts 'Ambient Light initialized'
          rescue Exception => e
            ambient_light = nil
            puts 'Ambient Light init failed: ' + e
          end
        elsif device_identifier == BrickletHumidity::DEVICE_IDENTIFIER
          begin
            humidity = BrickletHumidity.new uid, ipcon
            humidity.set_humidity_callback_period 1000
            humidity.register_callback(BrickletHumidity::CALLBACK_HUMIDITY) do |relative_humidity|
              if lcd != nil
                text = 'Humidity   %6.2f %%' % (relative_humidity/10.0)
                lcd.write_line 1, 0, text
                puts "Write to line 1: #{text}"
              end
            end
            puts 'Humidity initialized'
          rescue Exception => e
            humidity = nil
            puts 'Humidity init failed: ' + e
          end
        elsif device_identifier == BrickletBarometer::DEVICE_IDENTIFIER
          begin
            barometer = BrickletBarometer.new uid, ipcon
            barometer.set_air_pressure_callback_period 1000
            barometer.register_callback(BrickletBarometer::CALLBACK_AIR_PRESSURE) do |air_pressure|
              if lcd != nil
                text = 'Air Press %7.2f mb' % (air_pressure/1000.0)
                lcd.write_line 2, 0, text
                puts "Write to line 2: #{text}"

                begin
                  temperature = barometer.get_chip_temperature
                rescue Exception => e
                  puts 'Could not get temperature: ' + e
                  return
                end

                # 0xDF == ° on LCD 20x4 charset
                text = 'Temperature %5.2f %sC' % [(temperature/100.0), 0xDF.chr]
                lcd.write_line 3, 0, text
                puts "Write to line 3: #{text.sub(0xDF.chr, '°')}"
              end
            end
            puts 'Barometer initialized'
          rescue Exception => e
            barometer = nil
            puts 'Barometer init failed: ' + e
          end
        end
      end
    end

    ipcon.register_callback(IPConnection::CALLBACK_CONNECTED) do |connected_reason|
      if connected_reason == IPConnection::CONNECT_REASON_AUTO_RECONNECT
        puts 'Auto Reconnect'
        while true
          begin
            ipcon.enumerate
            break
          rescue Exception => e
            puts 'Enumerate Error: ' + e
            sleep 1
          end
        end
      end
    end

    while true
      begin
        ipcon.enumerate
        break
      rescue Exception => e
        puts 'Enumerate Error: ' + e
        sleep 1
      end
    end

    puts 'Press key to exit'
    $stdin.gets
    ipcon.disconnect

The content of this page is licensed under Creative Commons Attribution 3.0 Unported License.