トイレハック!!

Adways Advent Calendar 4日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar/archive


こんにちは。久保田です。

さて、突然ですが、我が社アドウェイズは大きな問題を抱えています。

それは、 「トイレ」 です。
人数に対して若干トイレが少ないのか、よく辛そうに並んでいる方々を見受けます。

この問題はただ心や体が辛いだけでなく、待ち時間があることで業務も滞ります。
チリも詰まれば、なので、最終的に会社にとって大きな損害であるわけです。

問題があったら解決するのがエンジニアの仕事。
というわけで、今回は試験的に僕が作った「トイレセンサー」と周辺技術のお話をご紹介します。

概要

今回この状況を改善するため、 「自席からでもトイレの空き状況がわかるシステム」 を作ることにしました。
そのため、こちらの記事を参考にさせていただき、トイレセンサーを作りました。

qiita.com

このセンサーを中心にシステムを作り上げて行きます。

以下のような設計になります。

f:id:AdwaysEngineerBlog:20161205175030p:plain

トイレのドアにセンサーを仕掛けておき、開閉状況を受信機であるラズベリーパイに送信し、ラズベリーパイは信号を受け取ったら保存しておきます。
そしてラズベリーパイをAPIサーバーとしても稼働させておき、SlackBotやwebページからトイレの状況を確認できるようにします。

トイレセンサー

上でも述べたように、

qiita.com

こちらの記事を参考にさせていただき、ほぼそのまま行いました。
道具などもほぼ同じものを使っています。
電子工作初心者の僕でもわかりやすく、非常に助かりました!

僕はTWE-Lite Rの設定をMacで行ったので、少しメモしておきます。

ファームウェアの書き込み

まず、SDKをインストールします。

http://mono-wireless.com/jp/products/TWE-NET/TWESDK.html

そして、こちらの公式のページを参考にファームウェアの書き込みを行いました。

http://mono-wireless.com/jp/tech/misc/jenprog/index.html

usbでtwe-liteとPCをつなぎ、デバイスの確認をします。

$ ls /dev/tty.usbserial*
#=> /dev/tty.usbserial-xxx

次に、SDKの中に入っているjenprogを使ってファームウェアを書き込みます。
書き込むファイルはこちらのページからインストールします。

http://mono-wireless.com/jp/products/Software_download/index.html

jenprog -t /dev/tty.usbserial-xxx App_TweLite\Master\Build\App_TweLite_Master_JN5164_1_7_1.bin

ハマったところは、書き込みがうまく行かず、何度か試したらうまくいったことです。
マイコンてそんなものかな。。と思い進みました。

TWE-Lite Rの設定

次はUSB経由でTEW-LiteをPCと接続し、設定を行います。

sudo cu -s 115200 -l /dev/tty.usbserial-***

+を三回押すと、こんな画面に切り替わるので、

f:id:AdwaysEngineerBlog:20161205175252p:plain

センサー側、受信機側の設定を行います。 ここの設定でデータを送信する間隔などが設定できます。

回路の作成

こちらも参考にさせていただき、真似させていただきました。 ドアが閉じた瞬間の検出にするように組んであります。

f:id:AdwaysEngineerBlog:20161205175501j:plain

受信側の設定

受信側に設定したTWE-Liteをラズベリーパイに接続します。
まずは、ちゃんとセンサーに変化があった時にデータが送られることを確認します。

ラズパイにcuを入れて
sudo apt-get install cu

出力します。

cu -s 115200 -l /dev/ttyUSB0

こんな感じで送られてきたら成功です。

f:id:AdwaysEngineerBlog:20161205175531j:plain

35桁目が0だったらセンサー同士が離れている(空いている)、1だったら近づいている(閉まっている)
という状況です。

これで動作確認ができたので、このデータを好きなプログラミング言語で処理するプログラムを作って、プロセスを動かしておけばOKです!
僕はRubyで処理しました。
今回はセンサーごとにファイルを作り、ファイルに状態を保存することにしました。

require 'serialport'

sp = SerialPort.new('/dev/ttyUSB0', 115200)

Process.deamon
loop do
  line = sp.gets
  if line.length > 5
    puts line
    File.open("/home/pi/ngx_mruby/build/nginx/mruby/#{line[1]}#{line[2]}", "w") do |f|
      f.puts("#{line[33]}#{line[34]}")
    end
  end
end

このプログラムを動かしておくと、センサーごとにファイルが作られ、
01のファイルに00
02のファイルに01
といった形式でデータが保存されます。
(状態が2連続で続くと一桁目が8になります。)

APIサーバーの構築

次はラズベリーパイをAPIサーバー化します。
ラズベリーパイにあまり負担をかけたくないので、比較的軽量で動かすため、Railsなどは使わず、
nginxにmrubyを組み込んだ、ngx_mruby(https://github.com/matsumotory/ngx_mruby)を使います。

github.com

少し話が横道にそれますが、ラズベリーパイでこちらのngx_mrubyがうまく動かない問題が発生しました。
僕の力ではどうにもならなかったので、プルリクを作らせていただいたところ、解決まで導いていただきました。
OSSってすごい、、と思いました。
ありがとうございました。

https://github.com/matsumotory/ngx_mruby/issues/229

ngx_mrubyからmrubyを使い、保存したデータをjsonに直して、ホストするようにしました。

#files = ['01', '02', '03']
files = ['01'] #(※今はテスト段階なので、01のセンサーのみ)
base_path = '/home/pi/ngx_mruby/build/nginx/mruby/'
h = {}
files.each do |file|
  File.open(base_path + file) do |f|
    h[file] = f.read
  end
end
r = Nginx::Request.new
r.content_type = "application/json"
Nginx.echo h.to_json

こうして、nginx.confに上のプログラムが動くように設定し、リクエストを投げると、以下のようなレスポンスが返ってきます。

pi@raspberrypi:~/ngx_mruby/build/nginx $ curl 127.0.0.1 -i -s
HTTP/1.1 200 OK
Server: nginx/1.11.6
Date: Sun, 04 Dec 2016 16:58:08 GMT
Content-Type: application/json
Content-Length: 14
Connection: keep-alive

{"01":"80\n"}

これでAPIサーバーは完成です。

Botの作成

最後にクライアントである、Botを完成させます。
なんでも作りたがりの僕は、hubotなどは使わずに構築しました。

SlackのRTM API(https://api.slack.com/rtm)を使ってwebソケットで常に繋いでおくようにしたのですが、
意外と簡単に接続が切れてBotが落ちてしまうため、
lxcを使ってコンテナ化し、監視用のプログラムを作り、落ちたらコンテナごと再起動しまたbotを動かす、という風な設計にしました。

bot本体
bot.rb

require 'websocket-client-simple'
require 'open-uri'
require 'faraday'
require 'json'

class RestRoom
  module Util
    SDGRESTROOMS = (1..3)
    class << self
      def sdg?(no)
        SDGRESTROOMS.include?(no.to_i)
      end

      def available?(s)
        s.to_i == 0
      end
    end
  end

  def initialize(state)
    @state = JSON.parse(state)
  end

  def to_message
    "to_message"
    @state.map do |no, s|
      "room #{no}[#{Util.sdg?(no) ? 'SDG' : 'BDG'}] is #{Util.available?(s) ? 'available' : 'close now...'} "
    end.join("\n")
  end
end


pid = fork do
  conn = Faraday.new(:url => 'https://slack.com') do |faraday|
    faraday.request  :url_encoded             
    faraday.adapter  Faraday.default_adapter
  end

  res = conn.post "/api/rtm.start", :token => "token"

  url = JSON.parse(res.body).to_h['url']

  wss = WebSocket::Client::Simple.connect url
  id = 1

  wss.on :message do |msg|
    data = JSON.parse(msg.data)
    if data['text'] && (data['text'] == '<> tell me')  # どのような形式で話しかけてきたら答えるようにするかの条件
      type = {
        'id': id,
        "type": "typing",
        "channel": ch
      }.to_json

      wss.send type
      id += 1

      rr = nil
      open('ラズパイのエンドポイント') do |f|
        rr = RestRoom.new(f.read)
      end

      j = {
        "id": id,
        "type": "message",
        "channel": ch,
        "text": rr.to_message
      }.to_json

      wss.send j
    end
    id += 1
  end

  loop do
  end
end

監視用のプログラム
monitoring.rb

require 'faraday'
require 'json'

def bot_run
  `sudo lxc-stop -r -n slack-bot-poo-001`  #  lxcによるコンテナの再起動
end

token = "slackのtoken"
user  = 'botのユーザーID'

while true
  conn = Faraday.new(:url => 'https://slack.com') do |faraday|
    faraday.request  :url_encoded
    faraday.adapter  Faraday.default_adapter 
  end

  res = conn.post "/api/users.getPresence", {:token => token, :user => user} # 10秒に一度botの状態をチェックする。

  JSON.parse(res.body)['presence'] == 'active' ? true : bot_run

  sleep 10
end

lxcでコンテナを作り、bot.rbを動かし、
monitoring.rbをホストOSで動かして監視しておけばOKです!

f:id:AdwaysEngineerBlog:20161205180138p:plain

まとめ

さて、これで全ての用意が整いましたので、今週中にトイレに設置しテストを始めたいと思います。

これで社員の皆様のトイレの悩みがなくなり、皆様の業務が捗ることを期待しております。

ではまたどこかで。。。


次は大曲さんの記事です。

http://blog.engineer.adways.net/entry/advent_calendar/05