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

Raspberry Pi Zero W を USB OTG でセットアップ

 先週、国内でも発売になった Raspberry Pi Zero W を運良く購入することができたので、セットアップしてみました。

www.switch-science.com

 Raspberry Pi Zero W は USB On-The-Go でのセットアップが可能なようなので、micro SD カードだけ買い足して、手持ちのUSBケーブルで Mac に接続して動かしてみました。手順については下記サイトを参考にさせていただきました。

qiita.com

本体外観

 パッケージとしては特に箱や説明書はなく、封筒でポストインでの配送でした。想像していたよりもさらに小さかったです。

f:id:akanuma-hiroaki:20170723031600j:plain:w450

 Raspberry Pi 3 Model B と比べてみるとこんな感じです。

f:id:akanuma-hiroaki:20170723031610j:plain:w450

OSイメージの用意

 まずはSDカードにOSのイメージを用意していきます。基本的な手順は以前 Raspberry Pi 3 Model B でやった時と同様にしました。

blog.akanumahiroaki.com

 イメージは最新のものを下記URLからダウンロードして使用しています。

http://ftp.jaist.ac.jp/pub/raspberrypi/raspbian_lite/images/raspbian_lite-2017-07-05/

 OSのイメージを書き込んだ後はSDカードが boot というドライブで認識され、下記のようにファイルが書き込まれています。

$ ls -l /Volumes/boot
total 42298
-rwxrwxrwx  1 akanuma  staff    18693 Aug 21  2015 COPYING.linux
-rwxrwxrwx  1 akanuma  staff     1494 Nov 18  2015 LICENCE.broadcom
-rwxrwxrwx  1 akanuma  staff    18974 Jul  5 11:45 LICENSE.oracle
-rwxrwxrwx  1 akanuma  staff    15660 May 15 19:09 bcm2708-rpi-0-w.dtb
-rwxrwxrwx  1 akanuma  staff    15456 May 15 19:09 bcm2708-rpi-b-plus.dtb
-rwxrwxrwx  1 akanuma  staff    15197 May 15 19:09 bcm2708-rpi-b.dtb
-rwxrwxrwx  1 akanuma  staff    14916 May 15 19:09 bcm2708-rpi-cm.dtb
-rwxrwxrwx  1 akanuma  staff    16523 May 15 19:09 bcm2709-rpi-2-b.dtb
-rwxrwxrwx  1 akanuma  staff    17624 May 15 19:09 bcm2710-rpi-3-b.dtb
-rwxrwxrwx  1 akanuma  staff    16380 May 15 19:09 bcm2710-rpi-cm3.dtb
-rwxrwxrwx  1 akanuma  staff    50248 Jul  3 10:07 bootcode.bin
-rwxrwxrwx  1 akanuma  staff      190 Jul  5 11:45 cmdline.txt
-rwxrwxrwx  1 akanuma  staff     1590 Jul  5 10:53 config.txt
-rwxrwxrwx  1 akanuma  staff     6674 Jul  3 14:07 fixup.dat
-rwxrwxrwx  1 akanuma  staff     2583 Jul  3 14:07 fixup_cd.dat
-rwxrwxrwx  1 akanuma  staff     9813 Jul  3 14:07 fixup_db.dat
-rwxrwxrwx  1 akanuma  staff     9813 Jul  3 14:07 fixup_x.dat
-rwxrwxrwx  1 akanuma  staff      145 Jul  5 11:45 issue.txt
-rwxrwxrwx  1 akanuma  staff  4379032 Jul  3 10:07 kernel.img
-rwxrwxrwx  1 akanuma  staff  4579248 Jul  3 10:07 kernel7.img
drwxrwxrwx  1 akanuma  staff    10240 Jul  5 11:44 overlays
-rwxrwxrwx  1 akanuma  staff  2855556 Jul  3 14:07 start.elf
-rwxrwxrwx  1 akanuma  staff   659492 Jul  3 14:07 start_cd.elf
-rwxrwxrwx  1 akanuma  staff  4993604 Jul  3 14:07 start_db.elf
-rwxrwxrwx  1 akanuma  staff  3939492 Jul  3 14:07 start_x.elf

 SSHを有効にするために、下記のように空ファイルを配置します。

$ touch /Volumes/boot/ssh
$ ls -l /Volumes/boot/ssh
-rwxrwxrwx  1 akanuma  staff  0 Jul 23 02:02 /Volumes/boot/ssh

 また、MacからUSB接続できるように設定します。 /boot/cmdline.txtrootwaitquiet の間に modules-load=dwc2,g_ether を追加します。追加後のファイルは下記のようになります。

$ cat /Volumes/boot/cmdline.txt 
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=a8790229-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait modules-load=dwc2,g_ether quiet init=/usr/lib/raspi-config/init_resize.sh

 それと /boot/config.txtdtoverlay=dwc2 という設定を追加します。

$ echo "dtoverlay=dwc2" >> /Volumes/boot/config.txt
$ tail /Volumes/boot/config.txt 
#dtparam=spi=on

# Uncomment this to enable the lirc-rpi module
#dtoverlay=lirc-rpi

# Additional overlays and parameters are documented /boot/overlays/README

# Enable audio (loads snd_bcm2835)
dtparam=audio=on
dtoverlay=dwc2

Raspberry Pi Zero W 起動

 ここまででOSのイメージの準備は終わりなので、micro SD カードを Raspberry Pi Zero W に挿し、USBケーブルで Mac と接続します。USB On-The-Go で接続するには内側の micro USB ポートを使用します。

f:id:akanuma-hiroaki:20170723100204j:plain:w450

 接続してしばらく待つと Raspberry Pi が起動します。起動したら下記のように ssh で接続します。

$ ssh pi@raspberrypi.local

 以前の記事と同様に Wi-Fi 接続の設定をします。

$ wpa_passphrase MY_AP_SSID MY_AP_PASSWORD | sudo tee -a /etc/wpa_supplicant/wpa_supplicant.conf

 そして一度シャットダウンし、次は micro USB を電源用の外側のポートに繋いで起動します。

f:id:akanuma-hiroaki:20170723100215j:plain:w450

 すると起動時に Wi-Fi に接続されますので、ssh で接続可能です。また、私の環境だとすでに Raspberry Pi が一台あり、ホスト名が重複してしまうので、下記コマンドで設定メニューを起動してホスト名を変更します。

$ sudo raspi-config

 すると下記のように変更後のホスト名で ssh できるようになります。

$ ssh pi@raspberrypi-zero.local

 あとはパッケージのアップデートやログインパスワードを変更して、ひとまずセットアップ終了です。USB On-The-Go でセットアップできると外付けのモニタやキーボードも不要で、とても手軽にセットアップできます。

 Raspberry Pi Zero W はBLEやWi-Fi接続もできるようになり、電源さえ確保すればワイヤレスでいろんなことができそうなので、活用方法を考えてみたいと思います。

TEXAS INSTRUMENTS の SimpleLink SensorTag CC2650 から BLE でデータ取得

 TEXAS INSTRUMENTS の SimpleLink CC2650 というセンサータグを買ってみたので、Raspberry Pi から BLE でセンサーデータを取得してみたいと思います。

www.tij.co.jp

 SimpleLink SensorTag CC2650 は気温、湿度、気圧、加速度、ジャイロ、磁気、照度などのセンサーを搭載しています。

 外箱は下記のような感じです。

f:id:akanuma-hiroaki:20170717174130j:plain:w450

 デバイス本体は下記のようになります。写真だと大きく見えてしまいますが、約5cm x 4cm ぐらいの大きさです。

f:id:akanuma-hiroaki:20170717174141j:plain:w450

スマートフォンアプリからの接続

 まずは TEXAS INSTRUMENTS のセンサータグ用スマートフォンアプリからの接続を試してみたいと思います。自分で開発しなくても手軽にセンサーが取得しているデータを確認することができます。

TI SensorTag

TI SensorTag

  • Texas Instruments
  • ユーティリティ
  • 無料

 アプリを起動して、SensorTagの側面にあるスイッチを押すと Advertising が始まり、リストにSensorTagが表示されます。そこからセンサーが取得しているデータや、GATT Service や Characteristic の UUID を確認できます。

f:id:akanuma-hiroaki:20170717174406p:plain:w300 f:id:akanuma-hiroaki:20170717174413p:plain:w300 f:id:akanuma-hiroaki:20170717174420p:plain:w300 f:id:akanuma-hiroaki:20170717174427p:plain:w300

Raspberry Pi からの接続

 それでは Raspberry Pi から SensorTag に接続して、データを取得してみたいと思います。基本的な処理内容は以前の記事(D-BusからBLEデバイスのNotificationを受け取る)で試したものと同様ですが、今回はデバイス固有の処理とBLEデバイス共通の処理を分割してみました。

 まずはBLEデバイス共通の処理のコード全体を載せておきます。BlueZによる処理をラップする形で、Device、Service、Characteristic のクラスを用意しています。エラーハンドリングはまだ考慮してません。Bluetoothのインタフェースもとりあえず hci0 を直接指定しています。

require 'bundler/setup'
require 'dbus'

class BLE
  attr_reader :bus

  SERVICE_NAME = 'org.bluez'
  SERVICE_PATH = '/org/bluez'
  ADAPTER      = 'hci0'

  DEVICE_IF          = 'org.bluez.Device1'
  SERVICE_IF         = 'org.bluez.GattService1'
  CHARACTERISTIC_IF  = 'org.bluez.GattCharacteristic1'
  DBUS_PROPERTIES_IF = 'org.freedesktop.DBus.Properties'

  SERVICE_RESOLVED_PROPERTY = 'ServicesResolved'
  UUID_PROPERTY             = 'UUID'

  PROPERTIES_CHANGED_SIGNAL = 'PropertiesChanged'

  SERVICE_RESOLVE_CHECK_INTERVAL = 0.1 # デバイスに接続後にサービスが解決されたかをチェックするインターバル
  DISCOVERY_WAITING_SECOND       = 10 # デバイス検出の待機時間

  module UUID
    GENERIC_ATTRIBUTE_SERVICE  = '00001801-0000-1000-8000-00805f9b34fb'
    DEVICE_INFORMATION_SERVICE = '0000180a-0000-1000-8000-00805f9b34fb'
    BATTERY_SERVICE            = '0000180f-0000-1000-8000-00805f9b34fb'

    BATTERY_DATA = '00002a19-0000-1000-8000-00805f9b34fb'
  end

  class Device
    attr_reader :bluez, :name, :address

    def initialize(bluez, bluez_device, name, address)
      @bluez        = bluez
      @bluez_device = bluez_device
      @name         = name
      @address      = address
    end

    # Device への接続処理。接続後に GATT Service が解決状態になるまで待機する
    def connect
      @bluez_device.introspect
      @bluez_device.Connect
      @bluez_device.introspect

      while !properties[SERVICE_RESOLVED_PROPERTY] do
        sleep(SERVICE_RESOLVE_CHECK_INTERVAL)
      end
    end

    def disconnect
      @bluez_device.Disconnect
    end

    def properties
      @bluez_device.introspect
      @bluez_device.GetAll(DEVICE_IF).first
    end

    # Device が持つ GATT Service のリストを返す
    def services
      services = []
      @bluez_device.subnodes.each do |node|
        service = @bluez.object("#{@bluez_device.path}/#{node}")
        service.introspect
        properties = service.GetAll(SERVICE_IF).first
        services << Service.new(@bluez, service, properties[UUID_PROPERTY])
      end

      services
    end

    def service_by_uuid(uuid)
      services.each do |service|
        return service if service.uuid == uuid
      end

      raise 'Service not found.'
    end

    def read_battery_level
      service = service_by_uuid(BLE::UUID::BATTERY_SERVICE)
      characteristic = service.characteristic_by_uuid(BLE::UUID::BATTERY_DATA)
      yield(characteristic.read.first)
      characteristic.start_notify do |v|
        yield(v.first)
      end
    end
  end

  class Service
    attr_reader :uuid

    def initialize(bluez, bluez_service, uuid)
      @bluez         = bluez
      @bluez_service = bluez_service
      @uuid          = uuid
    end

    def properties
      @bluez_service.introspect
      @bluez_service.GetAll(SERVICE_IF).first
    end

    # Service が持つ Characteristic のリストを返す
    def characteristics
      characteristics = []
      @bluez_service.subnodes.each do |node|
        characteristic = @bluez.object("#{@bluez_service.path}/#{node}")
        characteristic.introspect
        properties = characteristic.GetAll(CHARACTERISTIC_IF).first
        characteristics << Characteristic.new(characteristic, properties[UUID_PROPERTY])
      end

      characteristics
    end

    def characteristic_by_uuid(uuid)
      characteristics.each do |characteristic|
        return characteristic if characteristic.uuid == uuid
      end

      raise 'Characteristic not found.'
    end
  end

  class Characteristic
    attr_reader :uuid

    def initialize(bluez_characteristic, uuid)
      @bluez_characteristic = bluez_characteristic
      @uuid = uuid
    end

    def properties
      @bluez_characteristic.introspect
      @bluez_characteristic.GetAll(CHARACTERISTIC_IF).first
    end

    # Characteristic のプロパティに変更があった時にシグナルを受け取る
    # ブロックを渡してシグナル検知時にブロックを実行する
    def start_notify
      @bluez_characteristic.StartNotify
      @bluez_characteristic.default_iface = DBUS_PROPERTIES_IF
      @bluez_characteristic.on_signal(PROPERTIES_CHANGED_SIGNAL) do |_, v|
        yield(v['Value'])
      end
    end

    def write(value)
      @bluez_characteristic.WriteValue(value, {})
    end

    def read
      @bluez_characteristic.ReadValue({}).first
    end

    def inspect
      @bluez_characteristic.inspect
    end
  end

  def initialize
    @bus = DBus::system_bus
    @bluez = @bus.service(SERVICE_NAME)

    @adapter = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}")
    @adapter.introspect
  end

  # Bluetoothアダプタ配下のBLEデバイスを検出して Device クラスのインスタンスのリストを返す
  # デバイス名や RSSI が nil のデバイスは除外
  def devices
    @adapter.StartDiscovery
    sleep(DISCOVERY_WAITING_SECOND)

    devices = []
    @adapter.introspect
    @adapter.subnodes.each do |node|
      device = @bluez.object("#{SERVICE_PATH}/#{ADAPTER}/#{node}")
      device.introspect

      next unless device.respond_to?(:GetAll)

      properties = device.GetAll(DEVICE_IF).first
      name    = properties['Name']
      address = properties['Address']
      rssi    = properties['RSSI']

      next if name.nil? || rssi.nil?

      devices << Device.new(@bluez, device, name, address)
    end

    @adapter.StopDiscovery
    devices
  end

  def device_by_name(name)
    devices.each do |device|
      return device if device.name.downcase.include?(name.downcase)
    end

    raise 'No devices found.'
  end
end

 BLEクラスを new して #device_by_name メソッドにデバイス名を渡すと、デバイス名を含んだBLEデバイスを検出して Device クラスのインスタンスとして返します。

 次にSensorTag固有の処理のコードです。こちらもまだエラーハンドリングは考慮してません。SensorTag クラスには基本的に各センサーを有効化する処理と、センサーデータ検出時にデータをコールバックのブロックに受け渡す処理を実装しています。各センサーから検出されるデータは生データなので、実際に利用するにはコンバートする必要があります。コンバート処理の内容はいろいろ調べたのですが間違っているかもしれないので、参考までということで。

require 'bundler/setup'
require './ble.rb' # 前述のBLEデバイス共通処理を読み込む

class SensorTag
  DEVICE_NAME = 'CC2650'
  SCALE_LSB = 0.03125

  module UUID
    IR_TEMPERATURE_SERVICE     = 'f000aa00-0451-4000-b000-000000000000'
    HUMIDITY_SERVICE           = 'f000aa20-0451-4000-b000-000000000000'
    BAROMETER_SERVICE          = 'f000aa40-0451-4000-b000-000000000000'
    MOVEMENT_SERVICE           = 'f000aa80-0451-4000-b000-000000000000'
    LUXOMETER_SERVICE          = 'f000aa70-0451-4000-b000-000000000000'
    SIMPLE_KEYS_SERVICE        = '0000ffe0-0000-1000-8000-00805f9b34fb'
    IO_SERVICE                 = 'f000aa64-0451-4000-b000-000000000000'
    REGISTER_SERVICE           = 'f000ac00-0451-4000-b000-000000000000'
    CONNECTION_CONTROL_SERVICE = 'f000ccc0-0451-4000-b000-000000000000'
    OAT_SERVICE                = 'f000ffc0-0451-4000-b000-000000000000'

    IR_TEMPERATURE_CONFIG = 'f000aa02-0451-4000-b000-000000000000'
    IR_TEMPERATURE_DATA   = 'f000aa01-0451-4000-b000-000000000000'
    HUMIDITY_CONFIG       = 'f000aa22-0451-4000-b000-000000000000'
    HUMIDITY_DATA         = 'f000aa21-0451-4000-b000-000000000000'
    MOVEMENT_CONFIG       = 'f000aa82-0451-4000-b000-000000000000'
    MOVEMENT_DATA         = 'f000aa81-0451-4000-b000-000000000000'
    BAROMETER_CONFIG      = 'f000aa42-0451-4000-b000-000000000000'
    BAROMETER_DATA        = 'f000aa41-0451-4000-b000-000000000000'
    LUXOMETER_CONFIG      = 'f000aa72-0451-4000-b000-000000000000'
    LUXOMETER_DATA        = 'f000aa71-0451-4000-b000-000000000000'
    IO_CONFIG             = 'f000aa66-0451-4000-b000-000000000000'
    IO_DATA               = 'f000aa65-0451-4000-b000-000000000000'
  end

  # SensorTag に接続
  def connect
    @ble = BLE.new
    @device = @ble.device_by_name(DEVICE_NAME)
    @device.connect
  end

  # 各センサーの有効化のための処理
  def enable(service, config_uuid, value = [0x01])
    characteristic = service.characteristic_by_uuid(config_uuid)
    characteristic.write(value)
  end

  def enable_ir_temperature
    service = @device.service_by_uuid(SensorTag::UUID::IR_TEMPERATURE_SERVICE)
    enable(service, SensorTag::UUID::IR_TEMPERATURE_CONFIG)
  end

  def enable_humidity
    service = @device.service_by_uuid(SensorTag::UUID::HUMIDITY_SERVICE)
    enable(service, SensorTag::UUID::HUMIDITY_CONFIG)
  end

  def enable_movement
    service = @device.service_by_uuid(SensorTag::UUID::MOVEMENT_SERVICE)
    enable(service, SensorTag::UUID::MOVEMENT_CONFIG, [0b11111111, 0b00000000])
  end

  def enable_barometer
    service = @device.service_by_uuid(SensorTag::UUID::BAROMETER_SERVICE)
    enable(service, SensorTag::UUID::BAROMETER_CONFIG)
  end

  def enable_luxometer
    service = @device.service_by_uuid(SensorTag::UUID::LUXOMETER_SERVICE)
    enable(service, SensorTag::UUID::LUXOMETER_CONFIG)
  end

  # 各センサーの生データをコンバートする処理
  def convert_ir_temperature_value(upper_byte, lower_byte)
    (((upper_byte << 8) + lower_byte) >> 2) * SCALE_LSB
  end

  def convert_temp_value(upper_byte, lower_byte)
    temp_byte = (upper_byte << 8) + lower_byte
    (temp_byte / 65536.0) * 165 - 40
  end

  def convert_humidity_value(upper_byte, lower_byte)
    hum_byte = (upper_byte << 8) + lower_byte
    (hum_byte / 65536.0) * 100
  end

  def convert_movement_value(upper_byte, lower_byte)
      value = (upper_byte << 8) + lower_byte
      if upper_byte > 0x7f
        value = ~value + 1
      end
      (value >> 2).to_f
  end

  def convert_gyro_value(upper_byte, lower_byte)
    convert_movement_value(upper_byte, lower_byte) / 128.0
  end

  def convert_acc_value(upper_byte, lower_byte)
    convert_movement_value(upper_byte, lower_byte) / (32768 / 2)
  end

  def convert_mag_value(upper_byte, lower_byte)
    convert_movement_value(upper_byte, lower_byte) * 4912.0 / 32768.0
  end

  def convert_barometer_value(upper_byte, middle_byte, lower_byte)
    ((upper_byte << 16) + (middle_byte << 8) + lower_byte) / 100.0
  end

  def convert_luxometer_value(upper_byte, lower_byte)
    lux = (upper_byte << 8) + lower_byte

    m = lux & 0x0fff
    e = (lux & 0xf000) >> 12
    e = (e == 0) ? 1 : 2 << (e - 1)
    m * (0.01 * e)
  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|
      amb_lower_byte = v[2]
      amb_upper_byte = v[3]
      ambient = convert_ir_temperature_value(amb_upper_byte, amb_lower_byte)

      obj_lower_byte = v[0]
      obj_upper_byte = v[1]
      object = convert_ir_temperature_value(obj_upper_byte, obj_lower_byte)

      yield(ambient, object)
    end
  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|
      temp_lower_byte = v[0]
      temp_upper_byte = v[1]
      temp = convert_temp_value(temp_upper_byte, temp_lower_byte)

      hum_lower_byte = v[2]
      hum_upper_byte = v[3]
      hum = convert_humidity_value(hum_upper_byte, hum_lower_byte)

      yield(temp, hum)
    end
  end

  def read_movement
    service = @device.service_by_uuid(SensorTag::UUID::MOVEMENT_SERVICE)
    characteristic = service.characteristic_by_uuid(SensorTag::UUID::MOVEMENT_DATA)
    characteristic.start_notify do |v|
      gyro_x_lower = v[0]
      gyro_x_upper = v[1]
      gyro_y_lower = v[2]
      gyro_y_upper = v[3]
      gyro_z_lower = v[4]
      gyro_z_upper = v[5]

      gyro_x = convert_gyro_value(gyro_x_upper, gyro_x_lower)
      gyro_y = convert_gyro_value(gyro_y_upper, gyro_y_lower)
      gyro_z = convert_gyro_value(gyro_z_upper, gyro_z_lower)

      acc_x_lower = v[6]
      acc_x_upper = v[7]
      acc_y_lower = v[8]
      acc_y_upper = v[9]
      acc_z_lower = v[10]
      acc_z_upper = v[11]

      acc_x = convert_acc_value(acc_x_upper, acc_x_lower)
      acc_y = convert_acc_value(acc_y_upper, acc_y_lower)
      acc_z = convert_acc_value(acc_z_upper, acc_z_lower)

      mag_x_lower = v[12]
      mag_x_upper = v[13]
      mag_y_lower = v[14]
      mag_y_upper = v[15]
      mag_z_lower = v[16]
      mag_z_upper = v[17]

      mag_x = convert_mag_value(mag_x_upper, mag_x_lower)
      mag_y = convert_mag_value(mag_y_upper, mag_y_lower)
      mag_z = convert_mag_value(mag_z_upper, mag_z_lower)

      yield(gyro_x, gyro_y, gyro_z, acc_x, acc_y, acc_z, mag_x, mag_y, mag_z)
    end
  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_lower  = v[0]
      temp_middle = v[1]
      temp_upper  = v[2]
      temp = convert_barometer_value(temp_upper, temp_middle, temp_lower)

      press_lower  = v[3]
      press_middle = v[4]
      press_upper  = v[5]
      press = convert_barometer_value(press_upper, press_middle, press_lower)

      yield(temp, press)
    end
  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_lower  = v[0]
      lux_upper  = v[1]
      lux = convert_luxometer_value(lux_upper, lux_lower)

      yield(lux)
    end
  end

  def read_battery_level
    @device.read_battery_level do |battery_level|
      yield(battery_level)
    end
  end

  # シグナルの待受を開始
  def run
    main = DBus::Main.new
    main << @ble.bus

    main.run
  end

  def disconnect
    @device.disconnect
  end
end

if $0 == __FILE__
  sensor_tag = SensorTag.new
  begin
    sensor_tag.connect

    ir_temperature_log = Logger.new('logs/ir_temperature.log')
    sensor_tag.enable_ir_temperature
    sensor_tag.read_ir_temperature do |ambient, object|
      ir_temperature_log.info("amb: #{ambient} obj: #{object}")
    end

    humidity_log = Logger.new('logs/humidity.log')
    sensor_tag.enable_humidity
    sensor_tag.read_humidity do |temp, hum|
      humidity_log.info("temp: #{temp} hum: #{hum}")
    end

    gyro_log = Logger.new('logs/gyro.log')
    acc_log = Logger.new('logs/acc.log')
    mag_log = Logger.new('logs/mag.log')
    sensor_tag.enable_movement
    sensor_tag.read_movement do |gyro_x, gyro_y, gyro_z, acc_x, acc_y, acc_z, mag_x, mag_y, mag_z|
      gyro_log.info("gyro: #{gyro_x} #{gyro_y} #{gyro_z}")
      acc_log.info("acc: #{acc_x} #{acc_y} #{acc_z}")
      mag_log.info("mag: #{mag_x} #{mag_y} #{mag_z}")
    end

    barometer_log = Logger.new('logs/barometer.log')
    sensor_tag.enable_barometer
    sensor_tag.read_barometer do |temp, press|
      barometer_log.info("temp: #{temp} press: #{press}")
    end

    lux_log = Logger.new('logs/lux.log')
    sensor_tag.enable_luxometer
    sensor_tag.read_luxometer do |lux|
      lux_log.info("lux: #{lux}")
    end

    battery_log = Logger.new('logs/battery.log')
    sensor_tag.read_battery_level do |battery_level|
      battery_log.info("battery: #{battery_level}")
    end

    sensor_tag.run
  rescue Interrupt => e
    puts e
  ensure
    sensor_tag.disconnect
  end
end

 今回はコールバックとして単純に各データをログに出力するようにしていますので、スクリプトを実行するとセンサーのデータが各ログファイルに出力されます。

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

 データのコンバート処理はちゃんと実装する必要ありますが、SensorTag では手軽にいろいろなセンサーデータを取得できて面白いですね。今回はとりあえずデータを出力するだけでしたが、このデータを使って何か面白いことができないか考えてみたいと思います。

SORACOM Beam から Ruby で AWS IoT の Device Shadow を更新する

 前回 Raspberry Pi を SORACOM Beam 経由で AWS IoT に接続できるところまで確認したので、今回は AWS IoT の Shadow を使って状態を管理するところまでやってみたいと思います。

AWS IoT メッセージブローカーに Pub/Sub する

 まずは AWS IoT メッセージブローカーを MQTT Broker として単純に Pub/Sub してみたいと思います。

 今回は Ruby の MQTT クライアントとして ruby-mqtt を使ってみます。

github.com

 Gemfile に下記を追加して bundle install しておきます。

gem 'mqtt'

 Subscriber 側の実装は下記のようにしました。 SORACOM Beam に接続し、 topic/sample というトピックに Subscribe してメッセージを待ち受けます。

require 'bundler/setup'
require 'mqtt'

BEAM_URL = 'beam.soracom.io'
TOPIC = 'topic/sample'

MQTT::Client.connect(host: BEAM_URL) do |client|
  client.subscribe(TOPIC)
  puts "Subscribed to the topic: #{TOPIC}"

  client.get do |topic, message|
    puts "#{topic}: #{message}"
  end
end

 SORACOM Beam を使っているので認証情報等を書く必要がなく、とてもスッキリ書けます。

 次に Publisher 側です。 topic/sample というトピックにメッセージを一つ Publish して終了します。

require 'bundler/setup'
require 'mqtt'

BEAM_URL = 'beam.soracom.io'
TOPIC = 'topic/sample'

MQTT::Client.connect(host: BEAM_URL) do |client|
  now = Time.now
  client.publish(TOPIC, "Test from publisher.rb: #{now}")
  puts "Published to the topic '#{TOPIC}': #{now}"
end

 では動作を確認してみます。まずは Subscriber を起動してメッセージを待ち受けます。

pi@raspberrypi:~/aws_iot $ bundle exec ruby subscriber.rb  
Subscribed to the topic: topic/sample                           

 そして Publisher を起動してメッセージを Publish します。下記の例では二回起動して二回メッセージを Publish しています。

pi@raspberrypi:~/aws_iot $ bundle exec ruby publisher.rb 
Published to the topic 'topic/sample': 2017-07-12 23:17:33 +0000
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ bundle exec ruby publisher.rb 
Published to the topic 'topic/sample': 2017-07-12 23:17:48 +0000

 すると先ほど待ち受け状態にした Subscriber 側で下記のように受け取ったメッセージが表示されます。

pi@raspberrypi:~/aws_iot $ bundle exec ruby subscriber.rb  
Subscribed to the topic: topic/sample                           
topic/sample: Test from publisher.rb: 2017-07-12 23:17:33 +0000 
topic/sample: Test from publisher.rb: 2017-07-12 23:17:48 +0000 

Device Shadow を更新して内容を表示する

 では続いて AWS IoT の Device Shadow を使ってみます。Device Shadow は AWS IoT でモノ(Thing)の状態を保存するために使用されるJSONドキュメントで、Thing Shadows サービスによって管理され、参照、更新することができます。

docs.aws.amazon.com

 Thing Shadows サービスでは MQTT トピックを使用してメッセージがやりとりされます。まずはシンプルに Device Shadow の更新を行い、その内容を受信して表示してみます。Thing Shadows のトピック名のベースは $aws/things/モノの名前/shadow となります。今回はモノの名前を raspberry_pi としていますので、 $aws/things/raspberry_pi/shadow となります。

 Subscriber は下記のようにしました。 Device Shadow に対する更新が正常に行われると、Thing Shadows では $awsthings/raspberry_pi/shadow/update/accepted というトピックにメッセージが Publish されますので、このトピックに Subscribe しておきます。

require 'bundler/setup'
require 'mqtt'
require 'json'

BEAM_URL = 'beam.soracom.io'
TOPIC = '$aws/things/raspberry_pi/shadow/update/accepted'

MQTT::Client.connect(host: BEAM_URL) do |client|
  client.subscribe(TOPIC)
  puts "Subscribed to the topic: #{TOPIC}"

  client.get do |topic, json|
    puts "#{topic}: #{JSON.parse(json)}"
  end
end

 次に Publisher 側です。Device Shadow の状態を更新するには $aws/things/raspberry_pi/shadow/update というトピックにメッセージを Publish します。

require 'bundler/setup'
require 'mqtt'
require 'json'

BEAM_URL = 'beam.soracom.io'
TOPIC = '$aws/things/raspberry_pi/shadow/update'

MQTT::Client.connect(host: BEAM_URL) do |client|
  statement = {
    state: {
      desired: {
        power: 'on'
      }
    }
  }

  client.publish(TOPIC, statement.to_json)
  puts "Published to the topic: '#{TOPIC}'"
end

 では動作確認です。まずは Subscriber を起動して、メッセージを待ち受けます。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_subscriber.rb                                                  
Subscribed to the topic: $aws/things/raspberry_pi/shadow/update/accepted                                               

 そして Publisher を起動して Device Shadow の更新を行います。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_publisher.rb 
Published to the topic: '$aws/things/raspberry_pi/shadow/update'
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_publisher.rb 
Published to the topic: '$aws/things/raspberry_pi/shadow/update'

 すると下記のように Subscriber 側で Device Shadow の更新内容が表示されます。実際に Publish した内容以外に metadate, version, timestamp が追加されています。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_subscriber.rb                                                  
Subscribed to the topic: $aws/things/raspberry_pi/shadow/update/accepted                                               
$aws/things/raspberry_pi/shadow/update/accepted: {"state"=>{"desired"=>{"power"=>"on"}}, "metadata"=>{"desired"=>{"power"=>{"timestamp"=>1499902096}}}, "version"=>8, "timestamp"=>1499902096}                                                
$aws/things/raspberry_pi/shadow/update/accepted: {"state"=>{"desired"=>{"power"=>"on"}}, "metadata"=>{"desired"=>{"power"=>{"timestamp"=>1499902128}}}, "version"=>9, "timestamp"=>1499902128}                                                

Device Shadow の更新差分を表示する

 先ほどの例では Device Shadow の更新のために Publish された情報をそのまま受信して表示していましたが、今度は Device Shadow の状態に変更があった場合だけその差分を表示するようにしてみます。

 Device Shadow にはモノの実際の状態を示す reported セクションと、アプリケーション側で望む状態である desired というセクションが含まれます。Device Shadow に対する更新が行われた時に、 reported セクションと desired セクションに差異が発生した場合には $aws/things/raspberry_pi/shadow/update/delta というトピックにメッセージが Publish されますので、このトピックに対して Subscribe しておけば差分が発生したことを検知できます。

 また、差分を検知するためにはまずモノの状態を reported セクションで登録しておく必要がありますので、下記の Subscriber の実装の中ではまず起動時にモノの状態を Publish してから、 delta トピックに Subscribe しています。

require 'bundler/setup'
require 'mqtt'
require 'json'

BEAM_URL = 'beam.soracom.io'
TOPIC = '$aws/things/raspberry_pi/shadow/update'
DELTA_TOPIC = "#{TOPIC}/delta"

MQTT::Client.connect(host: BEAM_URL) do |client|
  initial_statement = {
    state: {
      reported: {
        power: 'off'
      }
    }
  }

  client.publish(TOPIC, initial_statement.to_json)
  puts "Published initial statement."

  client.subscribe(DELTA_TOPIC)
  puts "Subscribed to the topic: #{DELTA_TOPIC}"

  client.get do |topic, json|
    puts "#{topic}: #{JSON.parse(json)}"
  end
end

 そして Publisher 側では desired セクションでモノの状態を Publish します。下記の例では Publish を4回行い、power の状態を on -> off -> off -> on と変更しています。

require 'bundler/setup'
require 'mqtt'
require 'json'

BEAM_URL = 'beam.soracom.io'
TOPIC = '$aws/things/raspberry_pi/shadow/update'

def statement(power)
  {
    state: {
      desired: {
        power: power
      }
    }
  }
end

def publish(client, power)
  client.publish(TOPIC, statement(power).to_json)
  puts "Published to the topic: '#{TOPIC}'. power: #{power}"
end

MQTT::Client.connect(host: BEAM_URL) do |client|
  power = 'on'
  publish(client, power)

  sleep(3)

  power = 'off'
  publish(client, power)

  sleep(3)

  publish(client, power)

  sleep(3)

  power = 'on'
  publish(client, power)
end

 では動作確認です。まず Subscriber を起動してメッセージを待ち受けます。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_subscriber.rb                                                  
Published initial statement.                                                                                           
Subscribed to the topic: $aws/things/raspberry_pi/shadow/update/delta                                                  

 そして Publisher を起動して Device Shadow を更新します。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_publisher.rb 
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: on
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: off
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: off
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: on

 すると Subscriber 側でメッセージが受信されます。Publisher からはメッセージが4回 Publish されていますが、Subscriber 側では2件しか受信されていません。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_subscriber.rb                                                  
Published initial statement.                                                                                           
Subscribed to the topic: $aws/things/raspberry_pi/shadow/update/delta                                                  
$aws/things/raspberry_pi/shadow/update/delta: {"version"=>17, "timestamp"=>1499903040, "state"=>{"power"=>"on"}, "metad
ata"=>{"power"=>{"timestamp"=>1499903040}}}                                                                            
$aws/things/raspberry_pi/shadow/update/delta: {"version"=>20, "timestamp"=>1499903049, "state"=>{"power"=>"on"}, "metad
ata"=>{"power"=>{"timestamp"=>1499903049}}}                                                                            

 これは、 Subscriber 起動時に Device Shadow の reported を power: off で更新しているので、 Publisher から desired を power: off で Publish した場合は差分がないため delta にメッセージが Publish されないためです。また、差分を受け取っても特に reported の更新は行なっていないため、 reported は power: off のままになります。

Device Shadow の更新差分を受け取って reported を更新する

 では更新差分を受け取った場合はモノ(Thing)から reported を更新するようにしてみたいと思います。更新差分のメッセージを受信した場合はそのJSONをパースして、power の値を読み取り、reported の状態変更メッセージを Publish します。

require 'bundler/setup'
require 'mqtt'
require 'json'

BEAM_URL = 'beam.soracom.io'
TOPIC = '$aws/things/raspberry_pi/shadow/update'
DELTA_TOPIC = "#{TOPIC}/delta"

def statement(power)
  {
    state: {
      reported: {
        power: power
      }
    }
  }
end

MQTT::Client.connect(host: BEAM_URL) do |client|
  power = 'off'
  client.publish(TOPIC, statement(power).to_json)
  puts "Published initial statement. power: #{power}"

  client.subscribe(DELTA_TOPIC)
  puts "Subscribed to the topic: #{DELTA_TOPIC}"

  client.get do |topic, json|
    power = JSON.parse(json)['state']['power']
    client.publish(TOPIC, statement(power).to_json)
    puts "Changed power state to: #{power}"
    puts json
  end
end

 Publisher は一つ前の例とほぼ同じです。

require 'bundler/setup'
require 'mqtt'
require 'json'

BEAM_URL = 'beam.soracom.io'
TOPIC = '$aws/things/raspberry_pi/shadow/update'

def statement(power)
  {
    state: {
      desired: {
        power: power
      }
    }
  }
end

def publish(client, power)
  client.publish(TOPIC, statement(power).to_json)
  puts "Published to the topic: '#{TOPIC}'. power: #{power}"
end

MQTT::Client.connect(host: BEAM_URL) do |client|
  power = 'on'
  publish(client, power)

  sleep(3)

  power = 'off'
  publish(client, power)

  sleep(3)

  publish(client, power)

  sleep(3)

  power = 'on'
  publish(client, power)

  sleep(3)
end

 では動かしてみます。まず Subscriber を起動。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_subscriber.rb                                      
Published initial statement. power: off                                                                    
Subscribed to the topic: $aws/things/raspberry_pi/shadow/update/delta                                      

 そして Publisher を起動します。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_publisher.rb 
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: on
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: off
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: off
Published to the topic: '$aws/things/raspberry_pi/shadow/update'. power: on

 すると Subscriber 側で下記のようにメッセージが受信されます。

pi@raspberrypi:~/aws_iot $ bundle exec ruby shadow_subscriber.rb                                      
Published initial statement. power: off                                                                    
Subscribed to the topic: $aws/things/raspberry_pi/shadow/update/delta                                      
Changed power state to: on                                                                                 
{"version":38,"timestamp":1499903887,"state":{"power":"on"},"metadata":{"power":{"timestamp":1499903887}}} 
Changed power state to: off                                                                                
{"version":40,"timestamp":1499903889,"state":{"power":"off"},"metadata":{"power":{"timestamp":1499903889}}}
Changed power state to: on                                                                                 
{"version":43,"timestamp":1499903895,"state":{"power":"on"},"metadata":{"power":{"timestamp":1499903895}}} 

 今回はメッセージが3回受信されています。 Subscriber 側で差分通知を受け取った時に reported を更新するようにしたので、 Publisher から二回連続で power: off が Publish された時はメッセージが受信されていませんが、それ以外は差分が通知され、メッセージが受信されています。

 これでひとまず Device Shadow を使ったモノの管理ができるようになりました。SORACOM Beam を使うことで認証情報等は Beam にオフロードしてコードをシンプルにできますし、AWS IoT からは他のAWSサービスとの連携も簡単なので、色々と試してみたいと思います。

Raspberry Pi を SORACOM Beam から AWS IoT に接続する

 前回の記事で Raspberry Pi を AWS SDK を使って AWS IoT に接続してみましたが、今回は SORACOM Beam 経由で AWS IoT に接続してみたいと思います。

soracom.jp

 SORACOM Beam はデバイスからの接続先の設定やプロトコル変換処理をオフロードできるサービスで、例えばデバイスの中に認証情報を置いておかなくても、Beam から接続先にアクセスする際に認証用の情報を追加することができるので、セキュリティの面でも有効なサービスです。

 今回は下記のガイドを参考に Raspberry Pi から MQTT で AWS IoT に接続し、Amazon SNS からメールを送信する処理を実行してみたいと思います。

dev.soracom.io

AWS側の設定

 AWS IoT のモノの登録については、前回登録したものをそのまま使います。

 また、AWS IoT のルール作成時にIAMのロールの作成・参照権限が必要になりますので、今回はとりあえずIAMFullAccessポリシーをアタッチしてフル権限を付与しておきます。

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

 そしてメール通知を行うためのSNSトピックを作成しておきます。サブスクリプション作成時の Protocol は Email を指定し、メールアドレスを登録しておきます。

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

 ここではSNSのトピックとサブスクリプションの作成方法の詳細は割愛しますが、下記AWSドキュメントに記載されています。 

AWS IoT ルールの作成

 デバイスから送信された内容をハンドリングするためのルールを作成します。AWS IoTコンソールの左側のメニューから、 ルール をクリックします。

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

 まだルールが作成されていない場合は下記のような画面が表示されますので、 ルールを作成する をクリックします。

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

 ルールの作成フォームが表示されますので、「名前」と「説明」に自分がわかりやすい内容を設定します。

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

 画面下部の「メッセージのリソース」の各項目で今回対象とするデータの条件を設定します。

 SQLバージョンはデフォルトのままです。また、今回はひとまず全データを対象とするので、属性には「*」を、トピックフィルターには「#」を設定します。条件はブランクのままにします。

 そしてデータが送信された時の処理を設定するために、 アクションの追加 をクリックします。

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

 アクションの選択画面が表示されますので、今回は SNSプッシュ通知としてメッセージを送信する`` を選択し、画面下部のアクションの設定``` ボタンをクイックします。

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

 アクションの設定画面では、SNSターゲットとしてあらかじめ作成しておいたSNSトピックを選択し、メッセージ形式はRAW形式を選択します。

 そして、SNSリソースにAWS IoTへのアクセス権限を付与するためのロールを作成するため、 新しいロールの作成 をクリックします。

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

 ロール名の入力フォームが表示されますので、任意のロール名を入力して 新しいロールの作成 をクリックします。

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

 作成したロールが選択できるようになりますので、選択して アクションの追加 をクリックします。

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

 下記のようにルールにアクションが追加されたことが確認できます。

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

SORACOM Beam の設定

 まずは SORACOM Beam からのアクセス先となる、 AWS IoT 側のカスタムエンドポイントを確認しておきます。カスタムエンドポイントは AWS IoT を使用する各AWSアカウントごとに割り当てられるエンドポイントです。

 AWS IoT コンソールの左下の 設定 をクリックします。

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

 するとカスタムエンドポイントが確認できます。

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

 次に SORACOM Beam の設定を行います。

 SORACOM Consoleのメニューから Cellular -> Groups を選択し、対象のAir Sim が属するグループを選択したら、Basic settingsタブの SORACOM Beam をクリックして展開し、 から MQTT entry point を選択します。

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

 入力フォームが表示されますので、 Configuration name には後で自分がわかりやすい任意の名前を設定します。

 Protocol は MQTTS、Host name には先ほど確認した AWS IoT のカスタムエンドポイント、Port number には 8883 を設定します。また、AWS IoT の認証に証明書を使うので、 Client cert を ON にします。そして、証明書を登録するために + をクリックします。

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

 登録には秘密鍵、証明書、ルート証明書が必要になります。このうち秘密鍵、証明書は前回ダウンロードした connect_device_package.zip に含まれる下記ファイルを使います。

  • 秘密鍵:raspberry_pi.private.key

  • 証明書:raspberry_pi.cert.pem

 また、ルート証明書はSymantec社のサイトからダウンロードできます

https://www.symantec.com/content/en/us/enterprise/verisign/roots/VeriSign-Class%203-Public-Primary-Certification-Authority-G5.pem

 これらの内容ををそれぞれ Key、Cert、CA のフォームに入力します。Typeはデフォルトで X.509 certificate が選択されているのでそのままにしておきます。Credentials set ID と Description はわかりやすい内容を任意で登録します。入力が終わったら Register をクリックします。

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

 するとプルダウンで先ほど登録した Credentials Set が選択できるようになっているので選択し、 Save をクリックします。

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

 SORACOM Beam の設定が追加されたことが確認できます。

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

Raspberry Pi から接続してみる

 ここまでで設定は完了なので、 Raspberry Pi から接続してみます。今回は MQTT Broker のオープンソース実装である Mosquitto を使用しますので、下記コマンドでインストールします。

pi@raspberrypi:~ $ sudo apt-get install mosquitto mosquitto-clients

 そして SORACOM Air で接続した上で、下記コマンドを実行してデータを送信します。

pi@raspberrypi:~ $ mosquitto_pub -d -h beam.soracom.io -t beamdemo -m "Hello, World from AWS IoT!"
Client mosqpub/1224-raspberryp sending CONNECT
Client mosqpub/1224-raspberryp received CONNACK
Client mosqpub/1224-raspberryp sending PUBLISH (d0, q0, r0, m1, 'beamdemo', ... (26 bytes))
Client mosqpub/1224-raspberryp sending DISCONNECT

 すると下記のようにメールが送信されます。

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

 ひとまずこれで Raspberry Pi から SORACOM Beam を経由して AWS IoT に送信されたデータがルールによって処理され Amazon SNS へ送信され、メールが送られたことが確認できました。Raspberry Pi からの送信時は特に証明書等は指定せず、 SORACOM Beam に登録した証明書が使われていますので、 Raspberry Pi に証明書を置く必要なく処理が行えるようになっています。

 デバイスからの送信だけであれば SORACOM Funnel でも良いかもしれませんが、サーバからデバイスへの送信も行いたい場合は SORACOM Beam を使う必要があります。今回はデバイスからの送信だけでしたが、デバイスへの送信も試してみたいと思います。

 また、先日の SORACOM Conference 2017 で SORACOM Inventory が発表されました。

新サービス: SORACOM Inventory を発表 - SORACOM Blog

 まだ Limited Preview ということで実際には使えていませんが、用途としては AWS IoT に近いものだと思いますので、 SORACOM Air を使うデバイスであれば、 SORACOM Inventory だけでも良いのかもしれません。とりあえず Limited Preview の利用申請はしてみたので、使えるようになったら試してみたいと思います。

Raspberry Pi を AWS IoT に接続する

 今回はRaspberry PiをAWS IoTに接続してみたいと思います。AWS IoTとは簡単に言うと、IoTデバイスをAWS上のプラットフォームに登録しておき、デバイスの状態を記録するとともに、複数のデバイス間やデバイスとAWSサービス間の通信のハンドリングを行うことができるサービスです。

docs.aws.amazon.com

 デバイスとAWS IoTプラットフォーム間のプロトコルとしては MQTT, HTTP, WebSocket に対応していて、AWSサービスとの連携としては Lambda や DynamoDB、Kinesis、SNSなどに対応しています。

 今のところまだどういった連携をしていくか決めていませんが、ひとまずは Raspberry Pi を AWS IoT に登録して、接続ができるところまでを行ってみたいと思います。

AWS側の環境構築

 AWS IoT コンソールからデバイスの登録等を行うにはAWS IoT関連の権限が付与されている必要があります。今回はテストということで、 AWSIoTFullAccess ポリシーをアタッチして、AWS IoTに関する全ての権限を付与しました。実際のサービス提供時は最低限の権限に絞って付与した方が良いかと思います。

docs.aws.amazon.com

 権限が付与されたら該当のユーザでAWSマネジメントコンソールにログインし、サービスのリストから AWS IoT を選択して AWS IoT のコンソールへ移動します。

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

 今すぐ始める をクリックします。

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

 AWS IoT コンソールが表示されますので、 「AWS IoT に接続する」の 接続オプションの表示 をクリックします。

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

 今回は Raspberry Pi を接続するので、「デバイスの設定」の 今すぐ始める をクリックします。

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

 ステップの案内ページが表示されますので、 今すぐ始める をクリックします。

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

 プラットフォームとSDKの選択画面が表示されますので、今回は Linux と Python を選択して、画面右下の 次へ をクリックします。

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

 モノ(IoTデバイス)の登録画面になりますので、名前を決めて入力し、 次のステップ をクリックします。

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

 作成されるモノの情報と接続キットの情報が表示されますので、 Linux/OSX をクリックして接続キットをダウンロードします。ダウンロードすると画面右下の 次のステップ ボタンが活性化しますので、クリックして次へ進みます。

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

 デバイス上でのテスト手順が表示されます。

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

Raspberry Pi 側からの接続操作

 ダウンロードした接続キットをscp等で Raspberry Pi 上に転送し、上記手順を実行します。

pi@raspberrypi:~/aws_iot $ ls
connect_device_package.zip
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ unzip connect_device_package.zip 
Archive:  connect_device_package.zip
  inflating: raspberry_pi.private.key  
  inflating: raspberry_pi.public.key  
  inflating: raspberry_pi.cert.pem   
  inflating: start.sh                
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ ls
connect_device_package.zip  raspberry_pi.cert.pem  raspberry_pi.private.key  raspberry_pi.public.key  start.sh
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ chmod +x start.sh 
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ ls -l
total 20
-rw-r--r-- 1 pi pi 3620 Jul  1 02:05 connect_device_package.zip
-rw-r--r-- 1 pi pi 1224 Jul  1 02:01 raspberry_pi.cert.pem
-rw-r--r-- 1 pi pi 1679 Jul  1 02:01 raspberry_pi.private.key
-rw-r--r-- 1 pi pi  451 Jul  1 02:01 raspberry_pi.public.key
-rwxr-xr-x 1 pi pi  928 Jul  1 02:01 start.sh
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ ./start.sh 

Downloading AWS IoT Root CA certificate from Symantec...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1758  100  1758    0     0  12381      0 --:--:-- --:--:-- --:--:-- 12468

Installing AWS SDK...
Cloning into 'aws-iot-device-sdk-python'...
remote: Counting objects: 116, done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 116 (delta 3), reused 15 (delta 3), pack-reused 92
Receiving objects: 100% (116/116), 117.74 KiB | 0 bytes/s, done.
Resolving deltas: 100% (35/35), done.
Checking connectivity... done.
~/aws_iot/aws-iot-device-sdk-python ~/aws_iot
running install
running build
running build_py
creating build
creating build/lib.linux-armv7l-2.7
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK
copying AWSIoTPythonSDK/MQTTLib.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK
copying AWSIoTPythonSDK/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core
copying AWSIoTPythonSDK/core/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/operationTimeoutException.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/AWSIoTExceptions.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/operationError.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
copying AWSIoTPythonSDK/exception/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/exception
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
copying AWSIoTPythonSDK/core/shadow/shadowManager.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
copying AWSIoTPythonSDK/core/shadow/deviceShadow.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
copying AWSIoTPythonSDK/core/shadow/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/shadow
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/sigV4Core.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/offlinePublishQueue.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
copying AWSIoTPythonSDK/core/util/progressiveBackoffCore.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/util
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol
copying AWSIoTPythonSDK/core/protocol/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol
copying AWSIoTPythonSDK/core/protocol/mqttCore.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho
copying AWSIoTPythonSDK/core/protocol/paho/client.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho
copying AWSIoTPythonSDK/core/protocol/paho/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho
creating build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho/securedWebsocket
copying AWSIoTPythonSDK/core/protocol/paho/securedWebsocket/securedWebsocketCore.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho/securedWebsocket
copying AWSIoTPythonSDK/core/protocol/paho/securedWebsocket/__init__.py -> build/lib.linux-armv7l-2.7/AWSIoTPythonSDK/core/protocol/paho/securedWebsocket
running install_lib
creating /usr/local/lib/python2.7/dist-packages/AWSIoTPythonSDK
error: could not create '/usr/local/lib/python2.7/dist-packages/AWSIoTPythonSDK': Permission denied
pi@raspberrypi:~/aws_iot $ 
pi@raspberrypi:~/aws_iot $ sudo ./start.sh                                                                                                                                                                                                    

Running pub/sub sample application...
Traceback (most recent call last):
  File "aws-iot-device-sdk-python/samples/basicPubSub/basicPubSub.py", line 18, in <module>
    from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
ImportError: No module named AWSIoTPythonSDK.MQTTLib
pi@raspberrypi:~/aws_iot $ 

 AWSIoTPythonSDK がみつからないということなので、インストールします。

pi@raspberrypi:~/aws_iot $ sudo pip install AWSIoTPythonSDK                                                                                                                                                                                   
Downloading/unpacking AWSIoTPythonSDK
  Downloading AWSIoTPythonSDK-1.1.2.tar.gz (55kB): 55kB downloaded
  Running setup.py (path:/tmp/pip-build-06EpCW/AWSIoTPythonSDK/setup.py) egg_info for package AWSIoTPythonSDK
    
Installing collected packages: AWSIoTPythonSDK
  Running setup.py install for AWSIoTPythonSDK
    
Successfully installed AWSIoTPythonSDK
Cleaning up...
pi@raspberrypi:~/aws_iot $ 

 再度テスト用スクリプトを実行します。

pi@raspberrypi:~/aws_iot $ sudo ./start.sh                                                                                                                                                                                                    

Running pub/sub sample application...
2017-07-01 02:14:37,153 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Paho MQTT Client init.
2017-07-01 02:14:37,153 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - ClientID: basicPubSub
2017-07-01 02:14:37,153 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Protocol: MQTTv3.1.1
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Register Paho MQTT Client callbacks.
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - mqttCore init.
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load CAFile from: root-CA.crt
2017-07-01 02:14:37,154 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load Key from: raspberry_pi.private.key
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Load Cert from: raspberry_pi.cert.pem
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for backoff timing: baseReconnectTime = 1 sec
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for backoff timing: maximumReconnectTime = 32 sec
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for backoff timing: minimumConnectTime = 20 sec
2017-07-01 02:14:37,155 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for publish queueing: queueSize = -1
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for publish queueing: dropBehavior = Drop Newest
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Custom setting for draining interval: 0.5 sec
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Set maximum connect/disconnect timeout to be 10 second.
2017-07-01 02:14:37,156 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Set maximum MQTT operation timeout to be 5 second
2017-07-01 02:14:37,157 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Connection type: TLSv1.2 Mutual Authentication
2017-07-01 02:14:37,507 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Connect result code 0
2017-07-01 02:14:37,510 - AWSIoTPythonSDK.core.protocol.mqttCore - INFO - Connected to AWS IoT.
2017-07-01 02:14:37,510 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Connect time consumption: 70.0ms.
2017-07-01 02:14:37,511 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Started a subscribe request 1
2017-07-01 02:14:37,559 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - _resubscribeCount: -1
2017-07-01 02:14:37,560 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Subscribe request 1 sent.
2017-07-01 02:14:37,561 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Subscribe request 1 succeeded. Time consumption: 50.0ms.
2017-07-01 02:14:37,562 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Recover subscribe context for the next request: subscribeSent: False
2017-07-01 02:14:39,565 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 2 in the TCP stack.
2017-07-01 02:14:39,566 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 2 succeeded.
Received a new message: 
New Message 0
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:40,568 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 3 in the TCP stack.
2017-07-01 02:14:40,569 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 3 succeeded.
Received a new message: 
New Message 1
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:41,571 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 4 in the TCP stack.
2017-07-01 02:14:41,572 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 4 succeeded.
Received a new message: 
New Message 2
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:42,575 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 5 in the TCP stack.
2017-07-01 02:14:42,576 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 5 succeeded.
Received a new message: 
New Message 3
from topic: 
sdk/test/Python
--------------


2017-07-01 02:14:43,578 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Try to put a publish request 6 in the TCP stack.
2017-07-01 02:14:43,579 - AWSIoTPythonSDK.core.protocol.mqttCore - DEBUG - Publish request 6 succeeded.
Received a new message: 
New Message 4
from topic: 
sdk/test/Python
--------------

 エラーなく実行され、メッセージが送信され続けているようです。コンソール側を確認すると、 デバイスからのメッセージを待機中 となっていたところが下記のように変わり、メッセージが受信されていることが確認できます。

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

 上記画面の「ステップ 4: デバイスにメッセージを送信する」フォームに「Hello, IoT!!」のように入力して メッセージの送信 をクリックすると、Raspberry Pi 側でメッセージが受信され、下記のように出力されます。

Received a new message: 
Hello, IoT!!
from topic: 
sdk/test/Python

 ここまででひとまずデバイスの登録から接続の確認までは完了です。AWS IoT コンソールのダッシュボードを表示すると、下記のように処理の状況が確認できます。

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

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

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

 テスト用のサンプルはMQTTによる接続なので、プロトコルとしてMQTTがカウントされています。

 ちなみにダッシュボードの情報を表示するには CloudWatch の表示権限が必要になります。今回はテストということで、 CloudWatchFullAccess ポリシーをアタッチしました。実際のサービス提供時は最低限の権限に絞って許可した方が良いかと思います。

 ひとまず今回は接続まででしたが、ダッシュボード上でデバイスの通信状況が確認できるというのはとても便利そうに思えました。他のAWSとの連携や、SORACOM Beamを通しての接続も簡単にできるようなので、色々と組み合わせて動かしていってみたいと思います。

D-BusからBLE Advertisementを送信する

 今まではRaspberry PiをBLEのCentralとして他のデバイスへの接続などを試していましたが、今回はPeripheralとしてAdvertisementを送信してみました。

 PythonでAdvertisementを送信している例があったので下記サイトを参考にさせてもらいました。

qiita.com

 また、BLEについての下記サイトも参考にさせてもらいました。

http://events.linuxfoundation.org/sites/events/files/slides/Doing%20Bluetooth%20Low%20Energy%20on%20Linux.pdf

インタフェースの確認と設定

 Advertisementを送信するには、 org.bluez.LEAdvertisingManager1RegisterAdvertisement() メソッドを使うようなので、まずはインタフェースを確認してみます。

irb(main):001:0> require 'dbus'
irb(main):002:0> bus = DBus::SystemBus.instance
irb(main):003:0> bluez = bus.service('org.bluez')
irb(main):004:0> adapter = bluez.object('/org/bluez/hci0')
irb(main):005:0> adapter.introspect
〜〜〜ここまでのアウトプットは省略〜〜〜
irb(main):006:0> adapter.interfaces
=> ["org.freedesktop.DBus.Introspectable", "org.bluez.Adapter1", "org.freedesktop.DBus.Properties", "org.bluez.GattManager1", "org.bluez.Media1", "org.bluez.NetworkServer1"]

 該当のインタフェースが存在していない様子。試しに bluetoothctl でも見てみます。

[bluetooth]# advertise on
LEAdvertisingManager not found

 やはりインタフェースがみつからないようです。

 BlueZのドキュメントを確認してみます。

advertising-api.txt\doc - bluez.git - Bluetooth protocol stack for Linux

Service org.bluez
Interface org.bluez.LEAdvertisingManager1 [Experimental]
Object path /org/bluez/{hci0,hci1,…}

 org.bluez.LEAdvertisingManager1 は Experimental となっているようです。

 BlueZのインストール時の内容を確認してみましたが、 --enable-experimental オプションは付けてビルドしてました。

blog.akanumahiroaki.com

 bluetoothd のプロセスを確認してみます。

pi@raspberrypi:~ $ ps aux | grep bluetoothd
root       658  0.0  0.3   4780  3280 ?        Ss   22:23   0:00 /usr/local/libexec/bluetooth/bluetoothd
pi        1122  0.0  0.2   4276  2008 pts/3    S+   22:54   0:00 grep --color=auto bluetoothd

 下記記事を参照したところ、 --experimental オプションを付けて bluetoothd を起動する必要があるようです。

blog.mrgibbs.io

 /etc/systemd/system/bluetooth.target.wants/bluetooth.service を下記のように編集します。

ExecStart=/usr/local/libexec/bluetooth/bluetoothd  
 ↓  
ExecStart=/usr/local/libexec/bluetooth/bluetoothd --experimental

 編集後にRaspberry Piを再起動して、 bluetoothd を再確認します。

pi@raspberrypi:~ $ ps aux | grep bluetoothd
root       717  0.0  0.3   4780  3248 ?        Ss   23:05   0:00 /usr/local/libexec/bluetooth/bluetoothd --experimental
pi         910  0.0  0.1   4276  1812 pts/1    S+   23:09   0:00 grep --color=auto bluetoothd

 --experimental オプション付きで bluetoothd が起動しました。 bluetoothctl で確認してみます。

[bluetooth]# advertise on
Advertising object registered
[bluetooth]# advertise off
Advertising object unregistered
Agent unregistered

 bluetoothctl でも advertise コマンドが使えるようになりました。Rubyからも確認してみます

irb(main):006:0> adapter.interfaces
=> ["org.freedesktop.DBus.Introspectable", "org.bluez.Adapter1", "org.freedesktop.DBus.Properties", "org.bluez.GattManager1", "org.bluez.Media1", "org.bluez.NetworkServer1", "org.bluez.LEAdvertisingManager1"]

 org.bluez.LEAdvertisingManager1 インタフェースが追加されました。

Advertisementの送信(bluetoothctl)

 bluetoothctl でPeripheralとしてAdvertisementを送信してみます。

pi@raspberrypi:~ $ sudo bluetoothctl
[bluetooth]# advertise peripheral
Advertising object registered

 上記実行時のHCIの動作を hcidump で確認すると下記のようになりました。

pi@raspberrypi:~ $ sudo hcidump -i hci0
HCI sniffer - Bluetooth packet analyzer ver 5.23
device: hci0 snap_len: 1500 filter: 0xffffffff
< HCI Command: LE Set Advertising Parameters (0x08|0x0006) plen 15
    min 1280.000ms, max 1280.000ms
    type 0x00 (ADV_IND - Connectable undirected advertising) ownbdaddr 0x00 (Public)
    directbdaddr 0x00 (Public) 00:00:00:00:00:00
    channelmap 0x07 filterpolicy 0x00 (Allow scan from any, connection from any)
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertising Parameters (0x08|0x0006) ncmd 1
    status 0x00
< HCI Command: LE Set Advertise Enable (0x08|0x000a) plen 1
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertise Enable (0x08|0x000a) ncmd 1
    status 0x00

 PeripheralとしてAdvertisementを送信しているので、 ADV_IND - Connectable undirected advertising となっており、他のデバイスからの接続が可能なタイプのAdvertisementになっていることがわかります。

 ではLightBlueから接続してみます。

f:id:akanuma-hiroaki:20170621074917p:plain:w300 f:id:akanuma-hiroaki:20170621074923p:plain:w300

 デバイスの一覧に raspberrypi が表示され、タップして接続することができました。

 次にBroadcastで送信してみます。

[bluetooth]# advertise broadcast
Advertising object registered

 hcidump での出力は下記の通り。

< HCI Command: LE Set Advertising Data (0x08|0x0008) plen 32
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertising Data (0x08|0x0008) ncmd 1
    status 0x00
< HCI Command: LE Set Random Address (0x08|0x0005) plen 6
    bdaddr 32:DC:0C:A9:69:2B
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Random Address (0x08|0x0005) ncmd 1
    status 0x00
< HCI Command: LE Set Advertising Parameters (0x08|0x0006) plen 15
    min 1280.000ms, max 1280.000ms
    type 0x03 (ADV_NONCONN_IND - Non connectable undirected advertising) ownbdaddr 0x01 (Random)
    directbdaddr 0x00 (Public) 00:00:00:00:00:00
    channelmap 0x07 filterpolicy 0x00 (Allow scan from any, connection from any)
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertising Parameters (0x08|0x0006) ncmd 1
    status 0x00
< HCI Command: LE Set Advertise Enable (0x08|0x000a) plen 1
> HCI Event: Command Complete (0x0e) plen 4
    LE Set Advertise Enable (0x08|0x000a) ncmd 1
    status 0x00

 今度は ADV_NONCONN_IND - Non connectable undirected advertising となっており、他のデバイスからの接続はできない(Advertisementパケットの参照だけできる)タイプのAdvertisementになっていることがわかります。

 とりあえず bluetoothctl からAdvertisementを送信して他のデバイスから検知することができましたが、Rubyから送信することにはかなり調べたもののまだ成功していないので、今後実現していきたいと思います。