Mit Ruby auf das LCD 20x4 Bricklet schreiben

Für diese Projekt setzen wir voraus, dass eine Ruby Entwicklungsumgebung eingerichtet ist und ein grundsätzliches Verständnis der Ruby Programmiersprache vorhanden ist.

Falls dies nicht der Fall ist sollte hier begonnen werden. Informationen über die Tinkerforge API sind dann hier zu finden.

Ziele

Wir setzen uns folgende Ziele für dieses Projekt:

  • Temperatur, Helligkeit, Luftfeuchte und Luftdruck sollen auf dem LCD 20x4 Bricklet angezeigt werden,
  • die gemessenen Werte sollen automatisch aktualisiert werden sobald sie sich verändern und
  • die gemessenen Werte sollen in einem verständlichen Format angezeigt werden.

Da dieses Projekt wahrscheinlich 24/7 laufen wird, wollen wir sicherstellen, dass das Programm möglichst robust gegen externe Einflüsse ist. Das Programm sollte weiterhin funktionieren falls

  • Bricklets ausgetauscht werden (z.B. verwenden wir keine fixen UIDs),
  • Brick Daemon läuft nicht oder wird neu gestartet,
  • WIFI Extension ist außer Reichweite oder
  • Wetterstation wurde neu gestartet (Stromausfall oder USB getrennt).

Im Folgenden werden wir Schritt für Schritt zeigen wie diese Ziele erreicht werden können.

Schritt 1: Bricks und Bricklets dynamisch erkennen

Als Erstes legen wir fest wohin unser Programm sich verbinden soll:

HOST = 'localhost'
PORT = 4223

Falls eine WIFI Extension verwendet wird, oder der Brick Daemon auf einem anderen PC läuft, dann muss "localhost" durch die IP Adresse oder den Hostnamen der WIFI Extension oder des anderen PCs ersetzt werden.

Nach dem Start des Programms müssen der ::CALLBACK_ENUMERATE Callback und der ::CALLBACK_CONNECTED Callback registriert und ein erstes Enumerate ausgelöst werden:

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

Der Enumerate Callback wird ausgelöst wenn ein Brick per USB angeschlossen wird oder wenn die #enumerate Funktion aufgerufen wird. Dies ermöglicht es die Bricks und Bricklets im Stapel zu erkennen ohne im Voraus ihre UIDs kennen zu müssen.

Der Connected Callback wird ausgelöst wenn die Verbindung zur WIFI Extension oder zum Brick Daemon hergestellt wurde. In diesem Callback muss wiederum ein Enumerate angestoßen werden, wenn es sich um ein Auto-Reconnect handelt:

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

Ein Auto-Reconnect bedeutet, dass die Verbindung zur WIFI Extension oder zum Brick Daemon verloren gegangen ist und automatisch wiederhergestellt werden konnte. In diesem Fall kann es sein, dass die Bricklets ihre Konfiguration verloren haben und wir sie neu konfigurieren müssen. Da die Konfiguration beim Enumerate (siehe unten) durchgeführt wird, lösen wir einfach noch ein Enumerate aus.

Schritt 1 zusammengefügt:

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

Schritt 2: Bricklets beim Enumerate initialisieren

Während des Enumerierungsprozesse sollen alle messenden Bricklets konfiguriert werden. Dadurch ist sichergestellt, dass sie neu konfiguriert werden nach einem Verbindungsabbruch oder einer Unterbrechung der Stromversorgung.

Die Konfiguration soll beim ersten Start (ENUMERATION_TYPE_CONNECTED) durchgeführt werden und auch bei jedem extern ausgelösten Enumerate (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

Die Konfiguration des LCD 20x4 ist einfach, wir löschen den aktuellen Inhalt des Displays und schalten das Backlight ein:

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

Das Ambient Light, Humidity und Barometer Bricklet werden so eingestellt, dass sie uns ihre jeweiligen Messwerte höchsten mit einer Periode von 1000ms (1s) mitteilen:

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

Dies bedeutet, dass die Bricklets die CALLBACK_ILLUMINANCE, CALLBACK_HUMIDITY und CALLBACK_AIR_PRESSURE Callback-Funktionen immer dann aufrufen wenn sich der Messwert verändert hat, aber höchsten alle 1000ms.

Schritt 2 zusammengefügt:

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

Schritt 3: Messwerte auf dem Display anzeigen

Wir wollen eine hübsche Darstellung der Messwerte auf dem Display. Zum Beispiel:

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

Die Dezimaltrennzeichen und die Einheiten sollen in jeweils einer Spalte übereinander stehen. Daher verwenden wird zwei Zeichen für jede Einheit, zwei Nachkommastellen und kürzen die Namen so, dass sie in den restlichen Platz der jeweiligen Zeile passen. Das ist auch der Grund, warum dem "Illuminanc" das letzte "e" fehlt.

text = "%6.2f" % value

Der obige Ausdruck wandelt eine Fließkommazahl in eine Zeichenkette um, gemäß der gegebenen Formatspezifikation. Das Ergebnis ist dann mindestens 6 Zeichen lang mit 2 Nachkommastellen. Fall es weniger als 6 Zeichen sind wird von Links mit Leerzeichen aufgefüllt.

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

Es fehlt noch die Temperatur. Das Barometer Bricklet kann auch die Temperatur messen, aber es hat dafür keinen Callback. Als einfacher Workaround können wir die Temperatur in der CALLBACK_AIR_PRESSURE Callback-Funktion abfragen:

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

Schritt 3 zusammengefügt:

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

Das ist es. Wenn wir diese drei Schritte zusammen in eine Datei kopieren und ausführen, dann hätten wir jetzt eine funktionierenden Wetterstation.

Es gibt einige offensichtliche Möglichkeiten die Ausgabe der Messdaten noch zu verbessern. Die Namen könnten dynamisch exakt gekürzt werden, abhängig vom aktuell freien Raum der jeweiligen Zeile. Auch könnten die Namen können noch ins Deutsche übersetzt werden. Ein anderes Problem ist die Abfrage der Temperatur in der CALLBACK_AIR_PRESSURE Callback-Funktion. Wenn sich der Luftdruck nicht ändert dann wird auch die Anzeige der Temperatur nicht aktualisiert, auch wenn sich diese eigentlich geändert hat. Es wäre besser die Temperatur jede Sekunde in einem eigenen Thread anzufragen. Aber wir wollen das Programm für den Anfang einfach halten.

Wie dem auch sei, wir haben noch nicht alle Ziele erreicht. Das Programm ist noch nicht robust genug. Was passiert wenn die Verbindung beim Start des Programms nicht hergestellt werden kann, oder wenn das Enumerate nach einem Auto-Reconnect nicht funktioniert?

Wir brauchen noch Fehlerbehandlung!

Schritt 4: Fehlerbehandlung und Logging

Beim Start des Programms versuchen wir solange die Verbindung herzustellen, bis es klappt:

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

und es wird solange versucht ein Enumerate zu starten bis auch dis geklappt hat:

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

Mit diesen Änderungen kann das Programm schon gestartet werden bevor die Wetterstation angeschlossen ist.

Es muss auch sichergestellt werden, dass wir nur auf das LCD schreiben nachdem es initialisiert wurde:

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

und es müssen mögliche Fehler während des Enumerierungsprozesses behandelt werden:

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

Zusätzlich wollen wir noch ein paar Logausgaben einfügen. Diese ermöglichen es später herauszufinden was ein Problem ausgelöst hat, wenn die Wetterstation nach einer Weile möglicherweise nicht mehr funktioniert wie erwartet.

Zum Beispiel, wenn die Wetterstation über WLAN angebunden ist und häufig Auto-Reconnects auftreten, dann ist wahrscheinlich die WLAN Verbindung nicht sehr stabil.

Schritt 5: Alles zusammen

Jetzt sind alle für diese Projekt gesteckten Ziele erreicht.

Das gesamte Programm für die Wetterstation (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_ambient_light_v2'
require 'tinkerforge/bricklet_ambient_light_v3'
require 'tinkerforge/bricklet_humidity'
require 'tinkerforge/bricklet_humidity_v2'
require 'tinkerforge/bricklet_barometer'
require 'tinkerforge/bricklet_barometer_v2'

include Tinkerforge

HOST = 'localhost'
PORT = 4223

lcd = nil
ambient_light = nil
ambient_light_v2 = nil
ambient_light_v3 = nil
humidity = nil
humidity_v2 = nil
barometer = nil
barometer_v2 = 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 == BrickletAmbientLightV2::DEVICE_IDENTIFIER
      begin
        ambient_light_v2 = BrickletAmbientLightV2.new uid, ipcon
        ambient_light_v2.set_configuration(BrickletAmbientLightV2::ILLUMINANCE_RANGE_64000LUX,
                                           BrickletAmbientLightV2::INTEGRATION_TIME_200MS)
        ambient_light_v2.set_illuminance_callback_period 1000
        ambient_light_v2.register_callback(BrickletAmbientLightV2::CALLBACK_ILLUMINANCE) do |illuminance|
          if lcd != nil
            text = 'Illumina %8.2f lx' % (illuminance/100.0)
            lcd.write_line 0, 0, text
            puts "Write to line 0: #{text}"
          end
        end
        puts 'Ambient Light 2.0 initialized'
      rescue Exception => e
        ambient_light = nil
        puts 'Ambient Light 2.0 init failed: ' + e
      end
    elsif device_identifier == BrickletAmbientLightV3::DEVICE_IDENTIFIER
      begin
        ambient_light_v3 = BrickletAmbientLightV3.new uid, ipcon
        ambient_light_v3.set_configuration(BrickletAmbientLightV3::ILLUMINANCE_RANGE_64000LUX,
                                           BrickletAmbientLightV3::INTEGRATION_TIME_200MS)
        ambient_light_v3.set_illuminance_callback_configuration 1000, false, 'x', 0, 0
        ambient_light_v3.register_callback(BrickletAmbientLightV3::CALLBACK_ILLUMINANCE) do |illuminance|
          if lcd != nil
            text = 'Illumina %8.2f lx' % (illuminance/100.0)
            lcd.write_line 0, 0, text
            puts "Write to line 0: #{text}"
          end
        end
        puts 'Ambient Light 3.0 initialized'
      rescue Exception => e
        ambient_light = nil
        puts 'Ambient Light 3.0 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 == BrickletHumidityV2::DEVICE_IDENTIFIER
      begin
        humidity_v2 = BrickletHumidityV2.new uid, ipcon
        humidity_v2.set_humidity_callback_configuration 1000, true, 'x', 0, 0
        humidity_v2.register_callback(BrickletHumidityV2::CALLBACK_HUMIDITY) do |relative_humidity|
          if lcd != nil
            text = 'Humidity   %6.2f %%' % (relative_humidity/100.0)
            lcd.write_line 1, 0, text
            puts "Write to line 1: #{text}"
          end
        end
        puts 'Humidity 2.0 initialized'
      rescue Exception => e
        humidity_v2 = nil
        puts 'Humidity 2.0 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
    elsif device_identifier == BrickletBarometerV2::DEVICE_IDENTIFIER
      begin
        barometer_v2 = BrickletBarometerV2.new uid, ipcon
        barometer_v2.set_air_pressure_callback_configuration 1000, false, 'x', 0, 0
        barometer_v2.register_callback(BrickletBarometerV2::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_v2.get_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 2.0 initialized'
      rescue Exception => e
        barometer_v2 = nil
        puts 'Barometer 2.0 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