以前の記事(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 #1285] INFO -- : Published initial statement: {"state":{"reported":{"ambient":0,"object":0,"humidity":0,"pressure":0,"lux":0,"light_power":"off"}}} I, [2017-07-28T11:22:56.861190 #1285] INFO -- : Subscribed to the topic: $aws/things/sensor_tag/shadow/update/delta
次に Raspberry Pi Zero W から Publish します。 SensorTag の Advertising を開始した上で、下記のように実行します。
$ sudo bundle exec ruby publish_aws_iot.rb
SensorTag に接続され、センサーデータが Publish されると下記のようにログに出力され、一分ごとにログが出力されていきます。
I, [2017-07-28T11:23:53.473676 #832] INFO -- : Desired state: {"state":{"desired":{"ambient":26.21875,"object":19.71875,"humidity":73.358154296875,"pressure":1009.16,"lux":0.72,"light_power":"off"}}} I, [2017-07-28T11:25:05.242317 #945] INFO -- : Desired state: {"state":{"desired":{"ambient":26.1875,"object":19.65625,"humidity":73.74267578125,"pressure":1009.17,"lux":0.72,"light_power":"off"}}}
Raspberry Pi 3 側では Publish されたセンサーデータの差分が取得され、下記のようにログ出力されます。
I, [2017-07-28T11:23:53.719914 #1285] INFO -- : Reported state: {"state":{"reported":{"ambient":26.21875,"object":19.71875,"humidity":73.358154296875,"pressure":1009.16,"lux":0.72}}} I, [2017-07-28T11:25:05.480031 #1285] INFO -- : Reported state: {"state":{"reported":{"ambient":26.1875,"object":19.65625,"humidity":73.74267578125,"pressure":1009.17}}}
ここで SensorTag を裏返してみると、SensorTag の裏側にある照度センサーに光が当たり、値が閾値を超えるので、 Publisher 側で下記のように light_power
の値が on
になります。
I, [2017-07-28T11:26:15.037787 #945] INFO -- : Desired state: {"state":{"desired":{"ambient":26.21875,"object":21.40625,"humidity":76.171875,"pressure":1009.14,"lux":586.24,"light_power":"on"}}}
するとその差分が Subscriber 側でも受信され、ログに出力され、LED が点灯することになります。
I, [2017-07-28T11:26:15.350194 #1285] INFO -- : Reported state: {"state":{"reported":{"ambient":26.21875,"object":21.40625,"humidity":76.171875,"pressure":1009.14,"lux":586.24,"light_power":"on"}}}
そして Rule で設定していた通り、Amazon SNS からメールが送られてきます。
このままデータの取得を続けていくと、一分ごとに CloudWatch にもデータが投入されていき、下記のようにグラフが確認できるようになります。
まとめ
SensorTagのデータをBLEで取得する部分はそれなりに大変でしたが、そこさえクリアしてしまえば、 AWS IoT でのデータの連携や Rule を用いた他サービスとの連携は簡単にできますので、可視化や簡単な通知ぐらいであれば自前でサーバを構築する必要もなく、手軽に試すことができます。それと LED 等の物理デバイスを組み合わせて動作させることができると、単純なことではあっても元々ソフトウェアエンジニアの自分としてはとても面白く感じますね。
今回は CloudWatch を使って可視化しましたが、 SORACOM でも Harvest 等のサービスがありますし、色々試して効率的な組み合わせを探してみられると良いかと思います。コードを書く上でも Beam を使うと認証情報を気にすることなくスッキリ書けて良い感じですね。
今回使用したコードの全ては下記リポジトリにも公開しましたので、興味のある方はご参照ください。
また、今回は下記の記事を参考にさせていただきました。ありがとうございました。