OpenBlocks IoT BX1 で SensorTag のデータを AWS IoT に送信する

 ぷらっとホームの IoT ゲートウェイ OpenBlocks IoT BX1 を試す機会があったので、SensorTag のデータを BX1 から AWS IoT に送信する処理を試してみました。

f:id:akanuma-hiroaki:20170812204308j:plain:w300

openblocks.plathome.co.jp

初期設定

 まずは BX1 の初期設定を行います。付属のUSBケーブルで BX1 と MacBook Pro を接続すると BX1 が起動します。

f:id:akanuma-hiroaki:20170812204611j:plain:w300

 初期状態ではAPとして起動するので、 iotfamily_シリアル番号 となっているAPに初期パスワードで接続します。

f:id:akanuma-hiroaki:20170812205057p:plain:w300

 APに接続したらブラウザから https://192.168.254.254:4430 にアクセスすると初回は使用許諾画面や管理者アカウント設定画面が表示されるので、許諾への同意やアカウント設定を行います。

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

 次にネットワークの設定です。BX1 はSimを挿して3G回線に接続することもできますが、まずは自宅のLANに接続してみます。LANに接続する場合には使用モードを「クライアントモード」に設定し、接続するAPの情報や、IPアドレス等を設定します。

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

 ネットワーク設定を変更すると BX1 の再起動が求められますので、「メンテナンス」メニューから再起動を行います。再起動後は先ほど設定したIPアドレスでアクセスできるようになりますので、MacBookの接続先APをいつもの自宅のLANのAPに戻し、 https://192.168.10.100:4430 でアクセスします。

 そして時刻の設定を行えば最低限の設定は完了です。

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

BLEデバイスの接続設定

 BX1 では IoT デバイスとして Bluetooth インタフェースを標準でサポートしており、 SensorTag はサポート対象デバイスにも含まれています。

OpenBlocks IoT BX1 対応センサー/デバイス
http://openblocks.plathome.co.jp/products/obs_iot/bx1/sensor_dev/index.html

 まずは BT サービスを起動します。「サービス」メニューから BT の使用設定を「使用する」に変更して保存します。

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

 「BT関連」等のタブが追加されますので、「BT関連」タブを選択し、 SensorTag の Advertising を開始してから「BLEデバイス検出」の 検出 ボタンをクリックします。すると SensorTag が検出されますので、「使用設定」にチェックを入れて保存します。

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

 すると一覧に SensorTag が追加されます。   f:id:akanuma-hiroaki:20170812212651p:plain

 「BT編集」タブでは各デバイスのメモの編集等が行えますので、わかりやすいようにメモを登録しておきます。

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

データ収集設定

 BX1 はデータ収集機能を持っていて、 Web UI から設定することが可能です。収集したデータをクラウドに送信することもできますが、まずはBLEデバイスからのデータ収集ができるところまでを確認してみます。

 「サービス」メニューの「基本」タブから、データ収集と PD Handler BLE を「使用する」に設定して保存します。

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

 すると「収集設定」タブが追加されます。各種クラウドサービスへの送信の設定が行えますが、まずは BX1 で SensorTag の情報が取得できているかを確認するため、「本体内(local)」のみ「使用する」に設定します。また、画面下部のデバイス情報送信設定にて、 SensorTag の送信対象を「送信する」に設定し、送信先設定で「local」のみにチェックを入れて保存します。

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

 正しく設定されていれば SensorTag からの値の取得が始まり、「収集ログ」タブで pd-handler-stdout.log を確認すると、センサーデータが収集されていることが確認できます。

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

 また、「データ表示」タブでは、収集しているデータをグラフやテーブル表示で確認することができます。

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

3G回線への接続設定

 ここまでで SensorTag のデータが収集できていることは確認できたので、AWS IoT へのデータ送信を行いたいと思いますが、以前の記事同様に SORACOM Beam 経由で送信してみたいと思いますので、まずは3G回線への接続設定を行いたいと思います。

 一旦 BX1 をシャットダウンして SORACOM Air Sim を挿した後で起動し、再度管理画面にアクセスします。

 3G回線に接続する場合は Wireless LAN の設定をAPモードにする必要がありますので、SSIDやパスフレーズ等を設定し直します。

 モバイル回線設定では SORACOM Air の APN やユーザ名、パスワード等を設定して保存し、再起動します。

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

 再起動後はAPモードでの起動になりますので、MacBook から先ほど設定したAPへ接続した後、 https://192.168.254.254:4430 で管理画面にアクセスします。ネットワークメニューの「状態」タブを見ると、3G回線への接続が確認できます。

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

データ送信設定

 それでは SensorTag のデータを AWS IoT へ Publish するように設定します。 BX1 では AWS IoT 用の送信設定も可能ですが、今回は SORACOM Beam 経由で送信するため、 MQTT の送信設定を使用します。 サービスメニューの「収集設定」タブから、MQTTサーバを「使用する」に設定します。送信先ホストやポートは SORACOM Beam の情報を設定します。トピックプレフィックスには任意の文字列を設定します。

 また、デバイス情報送信設定の方でも、送信先設定で「MQTT」にチェックを入れます。ここで表示される ユニークID と、先ほどの トピックプレフィックス/ で繋いだものが、 Publish 先のトピックとして使用されます。

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

 ここまでの設定を保存すると、 SensorTag の情報が MQTT サーバに Publish されるようになります。AWS IoT のコンソールから該当のトピックに Subscribe してみると、下記のようにセンサーデータが Publish されていることが確認できます。

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

まとめ

 SensorTag は BX1 でサポートされているということもあり、コードを書くことなく手軽にセンサーデータをクラウドに送信することができました。今回は SORACOM Beam 経由にするために MQTTサーバへの送信にしましたが、直接 AWS IoT に送信するのであれば、送信設定で Shadow のフォーマットにも対応できるようです。また、非対応デバイスでも収集用のアプリを実装すればデータ収集ができるようですので、今後試してみたいと思います。

SensorTag のデータを Amazon Polly で読み上げる

 前回の記事では SensorTag で取得した値を AWS IoT に送信して、照度の値によって LED を点灯したり、SNSからメールを送信したりしてみましたが、今回はさらに Polly で照度の値を読み上げる音声ファイルを生成し、Raspberry Pi で再生する処理を追加してみたいと思います。

全体構成

 全体の構成は下記のようになります。

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

 前回と違うのは図の右下の赤線で囲んだ部分で、Lambda、Polly、s3との連携が追加になっています。

 処理の流れとしては、 AWS IoT Rule で SNS によるメール送信を行なっていた部分の Action に Lambda の Function 呼び出しを追加し、その Function の中から Polly のAPIを呼び出して照度の値を読み上げる mp3 ファイルを生成して s3 に格納し、 presigned URL を生成して AWS IoT に Publish します。Raspberry Pi 側では新たに音声ファイルの presigned URL が Publish されるトピックにも Subscribe しておき、 presigned URL を受け取った場合はそこから mp3 をダウンロードして再生します。

Raspberry Pi での音声ファイルの再生

 まずは下記ページを参考に Raspberry Pi 上で音声ファイルの再生を試してみます。

qiita.com

 Raspberry Pi のイヤホンジャックにヘッドフォンを挿し、下記コマンドを実行してみると、すんなり再生されました。

pi@raspberrypi:~ $ aplay /usr/share/sounds/alsa/Front_Center.wav
Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono

 下記のようなテスト用のコマンドも用意されているようです。

pi@raspberrypi:~ $ speaker-test -t sine -f 1000

speaker-test 1.0.28

Playback device is default
Stream parameters are 48000Hz, S16_LE, 1 channels
Sine wave rate is 1000.0000Hz
Rate set to 48000Hz (requested 48000Hz)
Buffer size range from 512 to 65536
Period size range from 512 to 65536
Using max buffer size 65536
Periods = 4
was set period_size = 16384
was set buffer_size = 65536
 0 - Front Left
Time per period = 1.409599
 0 - Front Left
Time per period = 2.563393

 また、amixer というコマンドで現在のデバイスの設定の確認や変更ができます。

pi@raspberrypi:~ $ amixer info
Card default 'ALSA'/'bcm2835 ALSA'
  Mixer name    : 'Broadcom Mixer'
  Components    : ''
  Controls      : 6
  Simple ctrls  : 1
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer scontrols
Simple mixer control 'PCM',0
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer scontents
Simple mixer control 'PCM',0
  Capabilities: pvolume pvolume-joined pswitch pswitch-joined
  Playback channels: Mono
  Limits: Playback -10239 - 400
  Mono: Playback -2000 [77%] [-20.00dB] [on]
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer controls
numid=3,iface=MIXER,name='PCM Playback Route'
numid=2,iface=MIXER,name='PCM Playback Switch'
numid=1,iface=MIXER,name='PCM Playback Volume'
numid=5,iface=PCM,name='IEC958 Playback Con Mask'
numid=4,iface=PCM,name='IEC958 Playback Default'

 下記のようにハードウェアの情報を確認することもできます。

pi@raspberrypi:~ $ cat /proc/asound/modules 
 0 snd_bcm2835
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat /proc/asound/cards
 0 [ALSA           ]: bcm2835 - bcm2835 ALSA
                      bcm2835 ALSA
pi@raspberrypi:~ $ 

 aplay は mp3 ファイルは再生できないので、 mpg321 を使ってみます。下記コマンドでインストールします。

pi@raspberrypi:~ $ sudo apt-get install mpg321

 下記のように mp3 ファイルを指定することで再生することができます。

pi@raspberrypi:~/sounds $ mpg321 mondo_01.mp3

Polly と連携するための Lambda Function

 まずは Lambda Function の全文を掲載しておきます。

import json
import boto3
from boto3 import Session
from boto3 import resource
from contextlib import closing

REGION         = 'ap-northeast-1'
POLLY_REGION   = 'us-east-1'
BUCKET_NAME    = 'hiroaki.akanuma.iot'
FILE_NAME_BASE = 'voices/%s_lux.mp3'
SPEECH_BASE    = '現在の照度は、%sルクスです。'
AWS_IOT_TOPIC  = '/iot/sensor_tag/voices'

session = Session(region_name = POLLY_REGION)
polly   = session.client('polly')
s3      = resource('s3')
bucket  = s3.Bucket(BUCKET_NAME)
iot     = boto3.client('iot-data')

def synthesize_speech(lux):
    speech_text = SPEECH_BASE % lux

    response = polly.synthesize_speech(
        Text         = speech_text,
        OutputFormat = 'mp3',
        VoiceId      = 'Mizuki'
    )
    
    return response['AudioStream']

def put_to_s3(audio_stream, file_name):
    with closing(audio_stream) as stream:
        bucket.put_object(
            Key         = file_name,
            Body        = stream.read(),
            ContentType = 'audio/mpeg'
        )

def generate_presigned_url(file_name):
    return boto3.client('s3').generate_presigned_url(
        ClientMethod = 'get_object',
        Params       = {'Bucket' : BUCKET_NAME, 'Key' : file_name},
        ExpiresIn    = 3600,
        HttpMethod   = 'GET'
    )

def publish_to_iot(speech_url):
    iot.publish(
        topic = AWS_IOT_TOPIC,
        qos   = 0,
        payload = json.dumps({'speech_url': speech_url})
    )

def lambda_handler(event, context):
    lux = event['lux']
    
    audio_stream = synthesize_speech(lux)

    file_name = FILE_NAME_BASE % lux

    put_to_s3(audio_stream, file_name)

    presigned_url = generate_presigned_url(file_name)

    publish_to_iot(presigned_url)

    return presigned_url

 event に照度の値が入ってくるのでまずはそれを取り出し、読み上げ用のテキストを作成したらそれを Polly の synthesize_speech メソッドに渡して、結果を AudioStream として取得します。出力フォーマットには mp3 を指定し、日本語での読み上げなので VoiceId には Mizuki を指定しています。レスポンスの中にはメタデータ等も含んでいますが、今回は使用しなかったので、 AudioStream のみを取り出しています。ちなみに Polly はまだ東京リージョンでは使用できないので、 Polly のクライアント取得時のリージョンにバージニアを指定しています。

 次に AudioStream から音声データを取得し、それを s3 に格納します。ファイル名は読み上げている照度の値を使って、それぞれの値ごとの音声ファイルを作成します。

 そして Raspberry Pi から認証なしで s3 上の音声ファイルにアクセスできるように、 presigned URL を発行して、その URL を音声ファイルの URL のやりとり用の AWS IoT トピックに Publish しています。AWS IoT Things の Shadow 更新用とは別のトピックになります。

 この Lambda Function を Rule の Action として追加します。

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

Raspberry Pi 側での読み上げ

 Raspberry Pi 側では音声ファイルのURLのやりとり用のトピックに Subscribe して、音声ファイルを読み上げる処理を追加します。

SPEECH_TOPIC = '/iot/sensor_tag/voices'
SOUNDS_DIR = '/home/pi/sounds/voices'

def run_speech_thread(log)
  log.info("Running speech thread.")
  Thread.new do
    begin
      MQTT::Client.connect(host: BEAM_URL) do |client|
        client.subscribe(SPEECH_TOPIC)
        log.info("Subscribed to the topic: #{SPEECH_TOPIC}")

        client.get do |topic, json|
          speech_url = JSON.parse(json)['speech_url']
          speech_uri = URI.parse(speech_url)
          speech_file = speech_uri.path.split('/').last
          speech_file_path = "#{SOUNDS_DIR}/#{speech_file}"

          unless File.exist?(speech_file_path)
            log.info("Opening URL: #{speech_url}")
            open(speech_url) do |file|
              open(speech_file_path, 'w+b') do |out|
                out.write(file.read)
              end
            end
          end

          log.info("Speaking: #{speech_file_path}")
          Open3.capture3("mpg321 #{speech_file_path}")
        end
      end
    rescue => e
      log.error(e.backtrace.join("\n"))
    end
  end
end

 MQTT でトピックに Subscribe して client.get するとそこで待ち受けるようになるため、とりあえず今回は今までの Shadow 更新用の処理とは別スレッドで音声ファイル用トピックに Subscribe して処理を行うようにしました。トピックからメッセージを受信したら、音声ファイルのURLをパースして、同名のファイルがローカルにない場合は s3 からダウンロードします。そして mpg321 コマンドを実行して音声ファイルを再生しています。

動作確認

 Lambda Function の保存や Rule での Action の追加を行ったら、Subscribeします。

$ sudo bundle exec ruby subscribe.rb

 そして SensorTag のデータの Publish を開始します。

$ sudo bundle exec ruby publish.rb

 すると、照度の値によって照明の ON/OFF が切り替わるタイミングで、音声ファイルも再生され、照度が読み上げられることになります。

まとめ

 Polly を使うことで手軽に音声ファイルを作成することができ、生成されたファイルをローカルに保存して繰り返し使うこともできますので、上手く使えばリーズナブルに音声を使ったサービスを作ることができます。今回は AWS SDK の認証情報をデバイス上におきたくなかったので、 SORACOM Beam 経由の AWS IoT の Rule から使うやり方をとりましたが、 AWS SDK から直接 Polly を使えばもっと直接的に手軽に音声ファイルを利用できると思います。

 今回のコードは下記リポジトリにも公開しています。

github.com

 ちなみに今回は下記ページを参考にさせていただきました。

qiita.com

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 の利用申請はしてみたので、使えるようになったら試してみたいと思います。