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.
We are setting the following goals for this project:
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
In the following we will show step-by-step how this can be achieved.
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
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
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!
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.
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_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