Firebase + Reaspberry Pi + TWE-Lite でトイレ監視システムを作った話

  • 36
    いいね
  • 0
    コメント

我々Sansan社では年々社員が増加し、それに伴いトイレのリソース問題が顕著になってきました。そこでトイレの空き状況がリアルタイムにわかると嬉しい!という声があがりトイレの空き状況を監視するシステムを作ってみました。

概要

作るシステムは以下の図のような感じです。

  • ドア開閉センサー (リードスイッチ + TWE-Liteによるワイヤレス・システム)
  • Raspberry Pi でセンサーの信号を受取サーバに送信
  • Firebase databaseで状態を管理
  • webクライアントでリアルタイムに情報を更新

トイレ管理システム.png

ドア開閉センサー

こちらの記事を参考に作りました。

無線ドア開閉センサを作る
http://qiita.com/ksasao/items/1a221843894fdb57677b

部品や回路などは全くこの記事にある通りにしてあります。

トイレ管理システム.png

TWE-Liteへのファームウェアの焼き込みに苦労したので、以下記録です。

TWE-Liteへのファームウェアの焼き込み

http://mono-wireless.com/jp/tech/misc/jenprog/index.html
からpyserialをダウンロードして展開し

sudo python setup.py install

を実行してpyserialをインストール

http://mono-wireless.com/jp/products/TWE-NET/TWESDK.html
から 2014/08月号 SDK ファイル をダンロードして展開

cd TWESDK/Tools/jenprog
chmod +x jenprog
chmod +x tweusb

http://www.ftdichip.com/Drivers/VCP.htm
から Mac OS X 10.9 and above 2015-04-15 2.3 をダウンロード
FTDI USB Serial Driver をインストール

TWI-LITE-RをUSBに差し込んで確認

 $ls /dev/tty.usbserial*
> /dev/tty.usbserial-MW7LR3T

jenprogの使い方

Usage: jenprog.py [options]

Options:
  -h, --help            show this help message and exit
  -a ADDR, --address=ADDR
                        start reading at address
  -l LEN, --len=LEN     number of bytes to read
  -m MAC, --mac=MAC     reset the mac addr (e.g. -m 01234567ABCDABCD)
  -k KEY, --key=KEY     reset the license key
  -v, --verify          also verify after writing
  -z, --compare         compare between flash content and specified file
  -s, --show            show mac address and license key
  -e, --erase           erasing the flash after reading mac and license key
  -b BAUD, --baud=BAUD  baud rate for serial connection.
  -t TARGET, --target=TARGET
                        target for connection
  -F, --force           skip firmware compatibility
  -C, --list-com-ports  skip firmware compatibility
  -D CDIR, --current_dir=CDIR
                        current directory

これでターゲットの情報を取得する

jenprog -t /dev/tty.usbserial-MW7LR3T -s

↑この操作はターゲットのプログラムボタンを押しながら、リセットしてプログラムモードにする必要がある。

  flash   : JN516x Internal Flash
  chip id : 0x10408686
  mac addr: 0x001bc501210e016b

こんな感じで情報がとれます。

次にファームウェアをインストール。
http://mono-wireless.com/jp/products/Software_download/index.html

ver 1.7.1 ソフトウェア(ソース含) 
※ 実験的な実装。(最新版ではありません。v1.6.6 以降は反映されていません)
※ オプションビット2の追加(bit0:3⇒DI1-4, bit4:7⇒DO1-4 のプルアップ停止)
※ バイナリは Master/Build 以下に格納 (*_JN5164*.bin ⇒ TWE-Lite 用)

ちょっと怖いけどこれを焼き込む

焼きこむファイル

App_TweLite_1_7_1_unoff\App_TweLite\Master\Build\App_TweLite_Master_JN5164_1_7_1.bin

以下のコマンドで書き込み

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

むー、書き込みに失敗するなあ

$ jenprog -t /dev/tty.usbserial-MW7LR3T ./App_TweLite/Master/Build/App_TweLite_Master_JN5164_1_7_1.bin
*** jenprog ver 1.3 ***
 file info: 04 03 0008
writing...
  0%..10%..
ERROR(2): communication with the target

むー 30%までいって失敗。。。

writing...
  0%..10%..20%..30%..
ERROR(2): communication with the target

どうもこれと同じ問題みたい。

TWE-Lite-Rでtwe-liteのファームウェア書き込みしたらエラーが出た
http://qiita.com/tittea/items/56e3aa5cd5f64b8328e9

$ jenprog -t /dev/tty.usbserial-MW7LR3T App_TweLite_Master_JN5164_1_7_1.bin -b 38400
*** jenprog ver 1.3 ***
 file info: 04 03 0008
writing...
  0%..10%..20%..30%..40%..50%..60%..70%..80%..90%..done - 2.66 kb/s
done

OK: firmware is successfully programmed.

ケーブルを何本かためしてみて、ボーレート 38400で成功!

できあがり

IMG_2507.jpg

こんな感じです。
Sansanらしく名刺ケースに入れてみました。

Raspberry Pi

Rspberry PiにはMono stickを挿して、TWE-Liteからの信号を受取り、
HuaweiのモバイルルータE8231を挿して、FreetelのSIMで運用しています。

14890445_192909701154810_6364703619249552546_o.jpg

コードはpythonで記述しています。

pythonのコード

メイン関数

メインループでシリアルからデータを受け取り、必要に応じて各センサーデバイスに対応するハンドラで情報を処理しています。

main.py
import device_state

import serial_reader

import server_api


if __name__ == "__main__":
    print("start")
    reader = serial_reader.SerialReader()

    api = server_api.ServerApi()
    state_11f_gentlemen_a = device_state.DeviceState(1, "11f_gentlemen_a", api)
    state_11f_gentlemen_b = device_state.DeviceState(2, "11f_gentlemen_b", api)

    while True:
        try:
            state_11f_gentlemen_a.health_check()
            state_11f_gentlemen_b.health_check()
            line = reader.read()
            state_11f_gentlemen_a.handle_serial(line)
            state_11f_gentlemen_b.handle_serial(line)
        except KeyboardInterrupt:
            break
        except:
            continue

    reader.close()

Mono stickからの信号のうけとり

シリアルポートから読み出します。

serial_reader.py
import serial


class SerialReader:

    def __init__(self, port="/dev/ttyUSB0", borate=115200, timeout=0.5):
        self.ser = serial.Serial(port, borate, timeout=timeout)

    def read(self):
        return self.ser.readline().decode("utf-8")

    def close(self):
        self.ser.close()

デバイスの状態の管理

デバイスごとにシリアル通信で取得したデータを受取り、必要に応じてサーバに状態を送信します。
また、デバイスから60秒間何のデータも送られて来ない場合にhealth checkにデータを投げます。

device_state.py
from datetime import datetime, timedelta


class DeviceState:
    def __init__(self, device_id, device_name, server_api):
        self.server_api = server_api
        self.device_id = device_id
        self.device_name = device_name
        self.is_open = True
        self.last_call = datetime.now()
        self.health = "initial"

    def open(self):
        if self.is_open:
            return
        self.time_delta = (datetime.now() - self.start_time)
        self.is_open = True
        self.server_api.set_vacant(self.device_name)
        self.server_api.set_log(
            self.device_name, self.start_time, datetime.now())

    def close(self):
        if not self.is_open:
            return
        self.start_time = datetime.now()
        self.is_open = False
        self.server_api.set_occupied(self.device_name)

    def handle_serial(self, line):
        if not int(line[2]) == self.device_id:
            return False

        self.last_call = datetime.now()
        self.set_health("good")
        if int(line[34]) == 1:
            print("device:{0} call close".format(self.device_id))
            self.close()
        else:
            print("device:{0} call open".format(self.device_id))
            self.open()
        return True

    def set_health(self, health):
        print("device:{0} health:{1}".format(self.device_id, health))
        if not self.health == health or health == "good":
            self.health = health
            print("device:{0} send_health:{1}".format(self.device_id, health))
            self.server_api.send_health(self.device_name, health, datetime.now())

    def health_check(self):
        delta = datetime.now() - self.last_call
        if delta > timedelta(seconds=60):
            self.set_health("bad")

Firebaseサーバへのデータの送信

  • 空き/使用中の状態の通知
  • health check
  • トイレの使用時間のログ を送信しています。
server_api.py
import requests
import json
from datetime import timezone, timedelta, datetime


class ServerApi:

    def __init__(self):
        self.jst = timezone(timedelta(hours=+9), 'JST')

    def set_occupied(self, device_name):
        url = "https://xxxxxx.firebaseio.com/{0}/status.json".format(
            device_name)
        print(url)
        requests.put(url, "\"occupied\"")
        return

    def set_vacant(self, device_name):
        url = "https://xxxxxx.firebaseio.com/{0}/status.json".format(
            device_name)
        print(url)
        requests.put(url, "\"vacant\"")
        return

    def send_health(self, device_name, health, last_update):
        url = "https://xxxxxx.firebaseio.com/{0}/health.json".format(
            device_name)
        print(url)
        str = json.dumps(
            {"status": health,
             "last_update": last_update.replace(tzinfo=self.jst).isoformat()
             })
        requests.put(url, str)

    def set_log(self, device_name, start_time, end_time):
        start_time = start_time.replace(tzinfo=self.jst)
        end_time = end_time.replace(tzinfo=self.jst)
        path = start_time.strftime('%Y/%m/%d/%H')
        url = "https://xxxxxx.firebaseio.com/{0}/log/{1}.json".format(
            device_name, path)
        print(url)
        str = json.dumps(
            {"start_time": start_time.isoformat(),
             "end_time": end_time.isoformat()})
        print(str)
        requests.post(url, str)

Raspberry Piの起動時にスクリプトを実行する

/etc/local.rc

に以下の通り実行処理を追加

python3 /home/pi/main.py &

Raspberry Piへのファイルの転送方法

HuaweiのモバイルルータE8231は、Wifiルータでもあるので、このルータにmacからwifiで接続することでRaspberry Piとは同じネットワーク上に入れるので、そこでssh接続することが出来ます。

ラズパイにE8321が刺さっている状態で電源を入れる 以下にWifi接続

SSID: HUAWEI-E8231-ddo2
Key: XXXXXXXXXXX
(KeyはE8321本体に記載)

SSHで接続

$ ssh pi@192.168.8.100
password: raspberry

とても簡単!
以下のコマンドでファイルを転送

$ scp -C ./*py pi@192.168.8.100:/home/pi/sansan-wc
password: raspberry

sshログインし以下のコマンドを実行

sudo reboot

Firebase

Firebase databaseのデータ構造は以下の通りです。

Firebase_Console.png

Webクライアント

WebのクライアントはFirebaseのjavascriptクラアントを使い、リアルタイムで情報を取得するようにしています。

$(function() {
    listen('11f_gentlemen_a')
    listen('11f_gentlemen_b')

    function listen(device){
        firebase.database().ref(device + '/status').on('value', function(data) {
            updateStatus(device, data.val())
        });
        firebase.database().ref(device + '/health').on('value', function(data) {
            updateHealth(device, data)
        });
    }

    function updateStatus(device, value){
        var node = $('#' + device + '_status')
        switch(value){
            case "vacant": {
                node.text("空き")
                break;
            }
            case "occupied": {
                node.text("使用中")
                break;
            }
        }
    }
    function updateHealth(device, data){
        var healthNode = $('#' + device + '_health')
        switch(data.val().status){
            case "good":{
                healthNode.text("稼働中")
                break;
            }
            case "bad":{
                healthNode.text("停止中")
                break;
            }
        }
        var lastUpdateNode = $('#' + device + '_last_update')
        var date = new Date(data.val().last_update)
        lastUpdateNode.text(date)
    }
});

まとめ

製作期間はトータルで5日くらい。
みるからに怪しいので問い合わせが相次ぎました。

この投稿は Sansan Advent Calendar 20169日目の記事です。