超音波センサー + SORACOM Beam でSlack通知

 IoTエンジニア養成読本のハンズオンのラスト、距離を測定するための超音波センサーの測定結果を元にした情報をSORACOM Beamを経由してSlackに通知する処理を実装してみました。

gihyo.jp

超音波センサーの接続

 まずは超音波センサー(HC-SR04)を下記の図のようにRaspberry Piに接続します。動作状況の確認用にLEDも一緒に接続しています。センサーの向きを間違えるとショートしてセンサーやRaspberry Piが壊れる可能性があるということなので注意して配線します。

f:id:akanuma-hiroaki:20170504232728p:plain:w300:left

超音波センサー
赤:2番ピン(+5V) - VCC
黒:20番ピン(GND) - GND
黄:13番ピン(GPIO27) - ECHO
青:11番ピン(GPIO17) - TRIG

LED
黒:6番ピン(GND)
黄:12番ピン(GPIO18)


f:id:akanuma-hiroaki:20170526062708j:plain:w500

距離の測定処理

 まずは距離の測定処理を実装します。超音波センサーでは送信した超音波が物体に当たって跳ね返り、戻ってくるまでにかかる時間から距離を測定しています。超音波センサーの端子としては、送信用がTRIG、受信用がECHOになります。今回はそれぞれGPIO17と27を使用しています。

require 'bundler/setup'
require 'pi_piper'

TRIG_GPIO = 17
ECHO_GPIO = 27

def read_distance(trig_pin_no, echo_pin_no)
  # 送信用(TRIG)、受信用(ECHO)のピンの設定
  trig_pin = PiPiper::Pin.new(pin: trig_pin_no, direction: :out)
  echo_pin = PiPiper::Pin.new(pin: echo_pin_no, direction: :in, trigger: :both)
  trig_pin.off
  sleep(0.3)

  # TRIGに短いパルスを送る
  trig_pin.on
  sleep(0.00001)
  trig_pin.off

  # ECHOがONになる(待ち受け状態になる)まで待ち、時間を記録
  echo_pin.wait_for_change
  signal_off = Time.now

  # ECHOがOFFになる(音波を受信して停止する)まで待ち、時間を記録
  echo_pin.wait_for_change
  signal_on = Time.now

  # 送出時刻と受信時刻の差分を求め、距離を計算する
  time_passed = signal_on - signal_off
  distance = time_passed * 17_000

  # ピンを解放
  PiPiper::Platform.driver.unexport_pin(trig_pin_no)
  PiPiper::Platform.driver.unexport_pin(echo_pin_no)

  return distance if distance <= 500
end

if $0 == __FILE__
  loop do
    start_time = Time.now
    distance = read_distance(TRIG_GPIO, ECHO_GPIO)
    unless distance.nil?
      puts "Distance: %.1f cm" % distance
    end

    wait = start_time + 3 - Time.now
    sleep(wait) if wait > 0
  end
end

 今回使用している距離センサーの使い方は、TRIGに10μs電圧をかけるとパルスが8回送出され、同時にECHOがHIGHになり、音波を受信するとLOWになるとのことでした。

www.switch-science.com

 元のPythonコードではループでECHOのHIGH/LOWの状態を検知して処理していますが、同僚からエッジ検出という手があると聞いて調べたところ、ピンの状態の変化を検知する wait_for_change というメソッドがあったのでこれを使っています。

PiPiper::Pin#wait_for_change
Method: PiPiper::Pin#wait_for_change — Documentation for jwhitehorn/pi_piper (master)

 このメソッドは指定した方向のピンの状態変化を検知するまでそこで待ち、検知するとそれ以降の処理に進みます。wait_for_change のソースは下記のようになっていて、 pin_wait_for というメソッドを呼び出しています。

def wait_for_change
  Platform.driver.pin_wait_for(@pin, @trigger)
end

 pin_wait_for メソッドの実装は下記のようになっていて、ループでピンの状態を検知して、ピンのオブジェクト作成時に trigger オプションに指定した内容次第で、ONになった時、OFFになった時、もしくは両方の場合で変更を検知します。今回は trigger に :both を指定していますので、両方の場合で変更を検知してループを抜けます。

def self.pin_wait_for(pin, trigger)
  pin_set_edge(pin, trigger)

  fd = File.open("/sys/class/gpio/gpio#{pin}/value", "r")
  value = nil
  loop do
    fd.read
    IO.select(nil, nil, [fd], nil)
    last_value = value
    value = self.pin_read(pin)
    if last_value != value
      next if trigger == :rising and value == 0
      next if trigger == :falling and value == 1
      break
    end
  end
end

 自前でループ処理を書かなくても良いのでコードがスッキリするのですが、タイムアウトは設定できません。今回使用している距離センサーはケースによっては状態の変化が起こらなくなってしまうケースもあるようなので、それを考慮すると一定時間でタイムアウトさせて再度測定を開始する必要があります。その場合はやはり自前でループ処理を書いてタイムアウトを設定した方が良いかもしれません。今回はひとまずこのまま pin_for_wait を使った実装で進めてみます。

 これを実行すると下記のように測定結果が出力されていきます。

pi@raspberrypi:~ $ sudo bundle exec ruby distance.rb                                                                                                                                                                                          
Distance: 9.3 cm
Distance: 9.2 cm
Distance: 9.1 cm
Distance: 9.0 cm
Distance: 9.0 cm
Distance: 9.3 cm
Distance: 9.4 cm
Distance: 8.9 cm
Distance: 9.0 cm
Distance: 9.3 cm
Distance: 9.5 cm
Distance: 9.1 cm
Distance: 8.6 cm
Distance: 9.4 cm
Distance: 10.1 cm
Distance: 10.1 cm
Distance: 9.4 cm
Distance: 8.8 cm
Distance: 8.3 cm

 ちなみに書籍で紹介されていたPythonのコードで測定すると、上記と同じ条件で実行しても下記のように測定距離やばらつき度合いが異なります。

pi@raspberrypi:~ $ python distance.py 
Distance: 67.7 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.7 cm
Distance: 10.0 cm
Distance: 10.0 cm
Distance: 10.1 cm
Distance: 10.0 cm
Distance: 10.1 cm

 実際の距離は10cm程なので、Pythonの方がばらつきが少なく、実際の距離との誤差も少ないようです。この辺りはECHOでの待ち受け処理やピンの状態検知処理の部分に差があるように思いますが、まだ詳細は調べられてないので、今後時間があるときに調べてみたいと思います。今回はそんなに厳密な測定精度は必要ないので、このままRubyの実装を使用します。

SORACOM Beam + Slackの使用設定

 今回は状態判定結果をSlackへ通知するために、SORACOM Beamを使用します。もちろんRaspberry Piから直接SlackのAPIにリクエストを送ることもできますが、その場合、セキュリティートークンを含むURLをコードの中に書く必要がありますし、IoTデバイスを設置した後で送信先を変更したくなった場合などは設置場所まで行くか、デバイスを回収する等の対応が必要になってしまいます。SORACOM Beamを利用すると、デバイスからはBeamのAPIにリクエストを投げておけば、Beamからアクセスする先のURLはWebコンソールから後からでも変更できますし、その際にセキュリティートークンなどの情報を追加することができます。また、デバイスからBeamへはAir SIMから3G/LTE回線での接続になるので安全に接続できますし、そこから外部のAPIへリクエストを投げる時にTLSによる暗号化を行うことで、デバイス側で暗号化の処理を行う必要もなくなります。

 SORACOM Beamを使うには、ユーザーコンソールからあらかじめ設定しておく必要があります。SIMグループのメニューから該当するグループの「SORACOM Beam設定」をクリックして開き、「+」メニューから「HTTPエントリポイント」を選び、開いたフォームにSlackのIncoming webhook APIのURL等を入力します。ここではSlack側でのIncoming webhook APIの設定方法は割愛します。

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

測定結果からの状態判定 + SORACOM Beamへの送信処理実装

 ここまでで実装した距離の測定処理を使って、測定結果から状態を判定し、今までと状態が変わった場合はSORACOM Beamへリクエストを送信する処理を実装します。また、検知状態がわかるようにLEDのON/OFFも行います。

 弊社の会議室はドアを閉めてしまうと外から中の様子は見えないので、使用中かどうかを示すためにドアに「使用中 / 空室」のプレートを貼ってあるのですが、入退室時に変更を忘れてそのままになってしまって結局使っているのかどうか開けてみないとわからないという状態がよく発生します。そこで今回は距離センサーを使って、会議室のテーブルに誰かいる = 会議室使用中ということで、その状態変化をSlackに通知して、会議室が使われているのかどうかわかるようにしてみたいと思います。

require 'bundler/setup'
require 'httpclient'
require 'logger'
require 'pi_piper'
require './distance.rb'

BEAM_URL = 'http://beam.soracom.io:8888/room'

TRIG_GPIO = 17
ECHO_GPIO = 27
LED_GPIO  = 18
INTERVAL  = 5.0 # sec

logger = Logger.new('room_notify.log')

led_pin  = PiPiper::Pin.new(pin: LED_GPIO,  direction: :out)

occupied = false
state_changed_at = Time.now

threshold = 50 # cm
unless ARGV.empty?
  threshold = ARGV.first.to_f
end

http_client = HTTPClient.new
loop do
  start_time = Time.now
  distance = read_distance(TRIG_GPIO, ECHO_GPIO)

  current_status = distance < threshold
  if occupied != current_status
    duration = Time.now - state_changed_at
    state_changed_at = Time.now
    message = "Distance: %.1f cm - Status changed to %s. (Duration: %d sec)" % [distance, current_status ? 'OCCUPIED' : 'EMPTY', duration]
    payload = '{"text":"%s"}' % message
    res = http_client.post(BEAM_URL, payload, 'Content-Type' => 'application/json')
    logger.info("PAYLOAD: #{payload} / BEAM Response: #{res.status}")
    occupied = current_status
  end

  occupied ? led_pin.on : led_pin.off

  if Time.now < start_time + INTERVAL
    sleep(start_time + INTERVAL - Time.now)
    next
  end
end

 5秒毎にセンサーから距離を読み取り、閾値(デフォルト50cm)を下回った場合は会議室使用中と判定しています。そして前回のステータスと異なっていた場合はSORACOM Beamへリクエストを送信します。また、使用中の場合はLEDを点灯させています。

 これを実行するとセンサーの測定距離に応じてステータスが判定され、ステータスが変更になった場合はSORACOM Beamへリクエストが送信されます。

pi@raspberrypi:~ $ sudo bundle exec ruby room_notify.rb &                                                                                                                                                                                     
[2] 9426

 ログファイルにはPAYLOADとBEAMからのレスポンスを出力しています。

pi@raspberrypi:~ $ tail room_notify.log                                                                                                                                                                                                       
I, [2017-05-24T23:09:50.528556 #5596]  INFO -- : PAYLOAD: {"text":"Distance: 52.7 cm - Status changed to EMPTY. (Duration: 25 sec)"} / BEAM Response: 200
I, [2017-05-24T23:10:05.498609 #5596]  INFO -- : PAYLOAD: {"text":"Distance: 10.1 cm - Status changed to OCCUPIED. (Duration: 14 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:39.312326 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 9.9 cm - Status changed to OCCUPIED. (Duration: 0 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:49.242184 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 98.0 cm - Status changed to EMPTY. (Duration: 10 sec)"} / BEAM Response: 200
I, [2017-05-24T23:18:54.314298 #7964]  INFO -- : PAYLOAD: {"text":"Distance: 14.3 cm - Status changed to OCCUPIED. (Duration: 4 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:33.491639 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 11.3 cm - Status changed to OCCUPIED. (Duration: 0 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:48.761726 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 54.2 cm - Status changed to EMPTY. (Duration: 15 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:53.451585 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 46.9 cm - Status changed to OCCUPIED. (Duration: 5 sec)"} / BEAM Response: 200
I, [2017-05-24T23:23:58.791505 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 51.0 cm - Status changed to EMPTY. (Duration: 5 sec)"} / BEAM Response: 200
I, [2017-05-24T23:24:18.531670 #9430]  INFO -- : PAYLOAD: {"text":"Distance: 10.2 cm - Status changed to OCCUPIED. (Duration: 19 sec)"} / BEAM Response: 200

 Slackにも下記のように通知されます。

f:id:akanuma-hiroaki:20170504232956p:plain:w500

 IoTデバイスはWebサービスと違って、設置してしまうとコードを変更したりするのは大変なので、SORACOM Beamのようなサービスで設置後も柔軟に設定を変更できるようになり、同時にセキュリティ面の信頼性も向上できるというのはとても便利だと感じました。

 今回実装したコードは下記リポジトリにも公開してあります。

github.com