SensorTag のデータを AWS IoT から CloudWatch と LED で可視化する

 以前の記事(TEXAS INSTRUMENTS の SimpleLink SensorTag CC2650 から BLE でデータ取得)で SensorTag から BLE でデータを取得できるようになったので、今回はそのデータを AWS IoT に送信し、Rule によって CloudWatch に送信して可視化してみたいと思います。また、ついでに照度の値によって、LEDを点灯させて、 Amazon SNS からメールを送信するようにしてみます。

全体構成

 全体の構成は下記の図のようにしています。

f:id:akanuma-hiroaki:20170730141445p:plain

 まず図の左上の 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 からメールが送られてきます。

f:id:akanuma-hiroaki:20170730113324p:plain:w450

 このままデータの取得を続けていくと、一分ごとに CloudWatch にもデータが投入されていき、下記のようにグラフが確認できるようになります。

f:id:akanuma-hiroaki:20170730113035p:plain

 

まとめ

 SensorTagのデータをBLEで取得する部分はそれなりに大変でしたが、そこさえクリアしてしまえば、 AWS IoT でのデータの連携や Rule を用いた他サービスとの連携は簡単にできますので、可視化や簡単な通知ぐらいであれば自前でサーバを構築する必要もなく、手軽に試すことができます。それと LED 等の物理デバイスを組み合わせて動作させることができると、単純なことではあっても元々ソフトウェアエンジニアの自分としてはとても面白く感じますね。

 今回は CloudWatch を使って可視化しましたが、 SORACOM でも Harvest 等のサービスがありますし、色々試して効率的な組み合わせを探してみられると良いかと思います。コードを書く上でも Beam を使うと認証情報を気にすることなくスッキリ書けて良い感じですね。

 今回使用したコードの全ては下記リポジトリにも公開しましたので、興味のある方はご参照ください。

github.com

 また、今回は下記の記事を参考にさせていただきました。ありがとうございました。

qiita.com