以前の記事(TEXAS INSTRUMENTS の SimpleLink SensorTag CC2650 から BLE でデータ取得)で SensorTag から BLE でデータを取得できるようになったので、今回はそのデータを AWS IoT に送信し、Rule によって CloudWatch に送信して可視化してみたいと思います。また、ついでに照度の値によって、LEDを点灯させて、 Amazon SNS からメールを送信するようにしてみます。
全体構成
全体の構成は下記の図のようにしています。
まず図の左上の SensorTag で温度や湿度、気圧、照度などのデータを取得し、Raspberry Pi Zero Wで受け取ったら、Wi-Fi経由で AWS IoT に Publish して Shadow を更新します。
Raspberry Pi 3 からはあらかじめ SORACOM Beam 経由で AWS IoT に Subscribe しておき、SensorTag の Shadow に更新があった場合に差分を受け取ります。その際に照明のON/OFFのパラメータによってLEDのON/OFFを切り替えます。
また、AWS IoT 側では Rule によって、Lambda 経由で CloudWatch にセンサーデータを投入し、照明のON/OFFの値が変わった場合には Amazon SNS からメールを送信します。
SensorTag からのデータ取得
まずは SensorTag からのデータ取得についてです。以前の記事では SensorTag からのデータをシグナルを待ち受ける形で取得していましたが、今回はジャイロや加速度センサーは使わず、頻繁にデータを取得する必要はないので、一分間隔で Raspberry Pi 側からデータを読み取りに行きます。
SensorTag からのデータ取得のコードについて以前の記事から変更した部分だけ下記に記載しておきます。
def handle_ir_temperature_values(values)
amb_lower_byte = values[2]
amb_upper_byte = values[3]
ambient = convert_ir_temperature_value(amb_upper_byte, amb_lower_byte)
obj_lower_byte = values[0]
obj_upper_byte = values[1]
object = convert_ir_temperature_value(obj_upper_byte, obj_lower_byte)
return ambient, object
end
def read_ir_temperature
service = @device.service_by_uuid(SensorTag::UUID::IR_TEMPERATURE_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::IR_TEMPERATURE_DATA)
characteristic.start_notify do |v|
ambient, object = handle_ir_temperature_values(v)
yield(ambient, object)
end
end
def read_ir_temperature_once
service = @device.service_by_uuid(SensorTag::UUID::IR_TEMPERATURE_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::IR_TEMPERATURE_DATA)
v = characteristic.read
ambient, object = handle_ir_temperature_values(v)
return ambient, object
end
def handle_humidity_values(values)
temp_lower_byte = values[0]
temp_upper_byte = values[1]
temp = convert_temp_value(temp_upper_byte, temp_lower_byte)
hum_lower_byte = values[2]
hum_upper_byte = values[3]
hum = convert_humidity_value(hum_upper_byte, hum_lower_byte)
return temp, hum
end
def read_humidity
service = @device.service_by_uuid(SensorTag::UUID::HUMIDITY_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::HUMIDITY_DATA)
characteristic.start_notify do |v|
temperature, humidity = handle_humidity_values(v)
yield(temp, hum)
end
end
def read_humidity_once
service = @device.service_by_uuid(SensorTag::UUID::HUMIDITY_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::HUMIDITY_DATA)
v = characteristic.read
temperature, humidity = handle_humidity_values(v)
return temperature, humidity
end
def handle_barometer_values(values)
temp_lower = values[0]
temp_middle = values[1]
temp_upper = values[2]
temp = convert_barometer_value(temp_upper, temp_middle, temp_lower)
press_lower = values[3]
press_middle = values[4]
press_upper = values[5]
press = convert_barometer_value(press_upper, press_middle, press_lower)
return temp, press
end
def read_barometer
service = @device.service_by_uuid(SensorTag::UUID::BAROMETER_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::BAROMETER_DATA)
characteristic.start_notify do |v|
temp, press = handle_barometer_values(v)
yield(temp, press)
end
end
def read_barometer_once
service = @device.service_by_uuid(SensorTag::UUID::BAROMETER_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::BAROMETER_DATA)
v = characteristic.read
temp, press = handle_barometer_values(v)
return temp, press
end
def handle_luxometer_values(values)
lux_lower = values[0]
lux_upper = values[1]
convert_luxometer_value(lux_upper, lux_lower)
end
def read_luxometer
service = @device.service_by_uuid(SensorTag::UUID::LUXOMETER_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::LUXOMETER_DATA)
characteristic.start_notify do |v|
lux = handle_luxometer_values(v)
yield(lux)
end
end
def read_luxometer_once
service = @device.service_by_uuid(SensorTag::UUID::LUXOMETER_SERVICE)
characteristic = service.characteristic_by_uuid(SensorTag::UUID::LUXOMETER_DATA)
v = characteristic.read
handle_luxometer_values(v)
end
一分ごとに処理を行うループ処理は呼び出し元でやるので、ここでは一度だけ Characeristic の値を読み出す処理(read_xxxxx_once)を用意しています。読み出した値の処理(handle_xxxxx_values)は以前のシグナルを待ち受ける処理と共通化しています。
AWS IoT にセンサーデータを Publish する
SensorTag から読み出した値を Raspberry Pi Zero W から AWS IoT へ Publish する処理は下記のようにしています。
Publisher 側では SORACOM Beam 経由ではなく AWS IoT に直接アクセスしているので、 MQTT での接続時にホスト名だけでなく認証情報も一緒に指定しています。
また、照度(lux)の値が一定以上だった場合は部屋の照明が点灯したという扱いにして、照明のON/OFFのパラメータ(light_power)を追加しています。
require 'bundler/setup'
require 'mqtt'
require 'json'
require './sensortag.rb'
AWS_IOT_URL = 'XXXXXXXXXXXXXX.iot.ap-northeast-1.amazonaws.com'
AWS_IOT_PORT = 8883
TOPIC = '$aws/things/sensor_tag/shadow/update'
PUBLISH_INTERVAL = 60
LUX_THRESHOLD = 100
log = Logger.new('logs/publish.log')
def statement(ambient:, object:, humidity:, pressure:, lux:)
{
state: {
desired: {
ambient: ambient,
object: object,
humidity: humidity,
pressure: pressure,
lux: lux,
light_power: lux >= LUX_THRESHOLD ? 'on' : 'off'
}
}
}
end
sensor_tag = SensorTag.new
begin
sensor_tag.connect
sensor_tag.enable_ir_temperature
sensor_tag.enable_humidity
sensor_tag.enable_barometer
sensor_tag.enable_luxometer
MQTT::Client.connect(host: AWS_IOT_URL, port: AWS_IOT_PORT, ssl: true, cert_file: 'raspberry_pi.cert.pem', key_file: 'raspberry_pi.private.key', ca_file: 'root-CA.crt') do |client|
loop do
ambient, object = sensor_tag.read_ir_temperature_once
_, humidity = sensor_tag.read_humidity_once
_, pressure = sensor_tag.read_barometer_once
lux = sensor_tag.read_luxometer_once
desired_state = statement(ambient: ambient, object: object, humidity: humidity, pressure: pressure, lux: lux).to_json
client.publish(TOPIC, desired_state)
log.info("Desired state: #{desired_state}")
sleep PUBLISH_INTERVAL
end
end
rescue Interrupt => e
puts e
ensure
sensor_tag.disconnect
end
AWS IoT に Publish されたセンサーデータの差分を受け取る
AWS IoT に Publish されたセンサーデータの差分を Raspberry Pi 3 から読み取る処理は下記のようにしています。
require 'bundler/setup'
require 'mqtt'
require 'json'
require './led.rb'
BEAM_URL = 'beam.soracom.io'
TOPIC = '$aws/things/sensor_tag/shadow/update'
DELTA_TOPIC = "#{TOPIC}/delta"
LED_GPIO = 22
log = Logger.new('logs/subscribe.log')
def statement(ambient:, object:, humidity:, pressure:, lux:, light_power:)
reported = {}
reported[:ambient] = ambient unless ambient.nil?
reported[:object] = object unless object.nil?
reported[:humidity] = humidity unless humidity.nil?
reported[:pressure] = pressure unless pressure.nil?
reported[:lux] = lux unless lux.nil?
reported[:light_power] = light_power unless light_power.nil?
{
state: {
reported: reported
}
}
end
def toggle_led(led:, light_power:)
return if light_power.nil?
light_power == 'on' ? led.on : led.off
end
led = LED.new(pin: LED_GPIO)
MQTT::Client.connect(host: BEAM_URL) do |client|
initial_state = statement(ambient: 0, object: 0, humidity: 0, pressure: 0, lux: 0, light_power: 'off').to_json
client.publish(TOPIC, initial_state)
log.info("Published initial statement: #{initial_state}")
client.subscribe(DELTA_TOPIC)
log.info("Subscribed to the topic: #{DELTA_TOPIC}")
client.get do |topic, json|
state = JSON.parse(json)['state']
ambient = state['ambient']
object = state['object']
humidity = state['humidity']
pressure = state['pressure']
lux = state['lux']
light_power = state['light_power']
toggle_led(led: led, light_power: light_power)
reported_state = statement(
ambient: ambient,
object: object,
humidity: humidity,
pressure: pressure,
lux: lux,
light_power: light_power
).to_json
client.publish(TOPIC, reported_state)
log.info("Reported state: #{reported_state}")
end
end
こちらは SORACOM Beam 経由なので MQTT での接続時にはホスト名だけ指定しています。
また、 light_power の値によって LED のON/OFFを切り替えています。LED の制御は別クラスで行なっています。
require 'bundler/setup'
require 'pi_piper'
class LED
def initialize(pin:)
@pin = PiPiper::Pin.new(pin: pin, direction: :out)
end
def on
@pin.on
end
def off
@pin.off
end
def flash
loop do
@pin.on
sleep 1
@pin.off
sleep 1
end
end
end
if $0 == __FILE__
led = LED.new(pin: ARGV[0].to_i)
puts led.inspect
led.flash
end
LEDの制御については以前書いたこの辺りの記事をご参照いただければと思います。
Raspberry Pi + RubyでLチカ - Tech Blog by Akanuma Hiroaki
AWS CloudWatch へのセンサーデータの投入
AWS IoT では Rule によって色々な処理ができますが、今回はセンサーデータの値を Lambda に渡して、 Lambda から CloudWatch にデータを投入してみました。
Lambda の Function は下記のような内容で作成しておきます。受け取った値を MetricData として CloudWatch に投げるだけのシンプルな構成です。
import json
import math
import datetime
import boto3
CloudWatch = boto3.client('cloudwatch')
def put_cloudwatch(metricName, value, unit):
try:
now = datetime.datetime.now()
CloudWatch.put_metric_data(
Namespace = "SensorTag",
MetricData = [{
"MetricName" : metricName,
"Timestamp" : now,
"Value" : value,
"Unit" : unit
}]
)
except Exception as e:
print e.message
raise
def lambda_handler(event, context):
ambient = event['ambient']
object = event['object']
humidity = event['humidity']
pressure = event['pressure']
lux = event['lux']
try:
put_cloudwatch("AmbientTemperature", ambient, "None")
put_cloudwatch("ObjectTemperature", object, "None")
put_cloudwatch("Humidity", humidity, "Percent")
put_cloudwatch("Pressure", pressure, "None")
put_cloudwatch("Lux", lux, "None")
except Exception as e:
raise
return
AWS IoT Rule のルールクエリステートメントでは下記のような内容を設定しておきます。
SELECT
state.desired.ambient as ambient,
state.desired.object as object,
state.desired.humidity as humidity,
state.desired.pressure as pressure,
state.desired.lux as lux
FROM
'$aws/things/sensor_tag/shadow/update'
Publish された全ての値を対象にしているので、 FROM は update トピックを指定し、 WHERE 条件は指定していません。Publish される state には desired と reported が含まれますが、今回は desired として登録された値を対象としていますので、 SELECT 句でも state.desired 配下のパラメータを参照しています。
また、アクションには メッセージデータを渡す Lambda 関数を呼び出す
を指定し、先ほどの Lambda Function を指定しておきます。
Amazon SNS からメール送信
AWS IoT Rule でもう一つ、 Amazon SNS からメール送信するための設定をしておきます。ルールクエリステートメントは下記のようにしておきます。
SELECT
state.light_power
FROM
'$aws/things/sensor_tag/shadow/update/delta'
WHERE
state.light_power = 'on' or state.light_power = 'off'
Shadow に差分が発生した場合だけ処理が行われればいいので、対象のトピックは delta トピックにします。 delta トピックには照明のON/OFF以外の差分も Publish されるため、 WHERE 条件で照明のON/OFFのパラメータが含まれている場合だけ処理を行うように指定しておきます。 update トピックの時と違って Publish される state は差分だけなので、 state.light_power
という指定にしています。
アクションの設定は以前下記記事で行なった時と同様に行います。
Raspberry Pi を SORACOM Beam から AWS IoT に接続する - Tech Blog by Akanuma Hiroaki
動作確認
それでは動作を確認してみます。まずは Raspberry Pi 3 から SORACOM Air で3G回線に接続した上で、 Subscribe します。
$ sudo bundle exec ruby subscribe.rb
すると下記のように初期ステートメントを Publish した上で Subscribe した旨がログに出力されます。
I, [2017-07-28T11:22:56.859902
I, [2017-07-28T11:22:56.861190
次に Raspberry Pi Zero W から Publish します。 SensorTag の Advertising を開始した上で、下記のように実行します。
$ sudo bundle exec ruby publish_aws_iot.rb
SensorTag に接続され、センサーデータが Publish されると下記のようにログに出力され、一分ごとにログが出力されていきます。
I, [2017-07-28T11:23:53.473676
I, [2017-07-28T11:25:05.242317
Raspberry Pi 3 側では Publish されたセンサーデータの差分が取得され、下記のようにログ出力されます。
I, [2017-07-28T11:23:53.719914
I, [2017-07-28T11:25:05.480031
ここで SensorTag を裏返してみると、SensorTag の裏側にある照度センサーに光が当たり、値が閾値を超えるので、 Publisher 側で下記のように light_power
の値が on
になります。
I, [2017-07-28T11:26:15.037787
するとその差分が Subscriber 側でも受信され、ログに出力され、LED が点灯することになります。
I, [2017-07-28T11:26:15.350194
そして Rule で設定していた通り、Amazon SNS からメールが送られてきます。
このままデータの取得を続けていくと、一分ごとに CloudWatch にもデータが投入されていき、下記のようにグラフが確認できるようになります。
まとめ
SensorTagのデータをBLEで取得する部分はそれなりに大変でしたが、そこさえクリアしてしまえば、 AWS IoT でのデータの連携や Rule を用いた他サービスとの連携は簡単にできますので、可視化や簡単な通知ぐらいであれば自前でサーバを構築する必要もなく、手軽に試すことができます。それと LED 等の物理デバイスを組み合わせて動作させることができると、単純なことではあっても元々ソフトウェアエンジニアの自分としてはとても面白く感じますね。
今回は CloudWatch を使って可視化しましたが、 SORACOM でも Harvest 等のサービスがありますし、色々試して効率的な組み合わせを探してみられると良いかと思います。コードを書く上でも Beam を使うと認証情報を気にすることなくスッキリ書けて良い感じですね。
今回使用したコードの全ては下記リポジトリにも公開しましたので、興味のある方はご参照ください。
github.com
また、今回は下記の記事を参考にさせていただきました。ありがとうございました。
qiita.com