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


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

# 概要

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

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


![トイレ管理システム.png](https://qiita-image-store.s3.amazonaws.com/0/8308/1eb23ec7-60ba-df39-7b24-46a12a9e16af.png "トイレ管理システム.png")



# ドア開閉センサー

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

[ボタン電池1個で数年持つ無線ドア開閉センサを作る - Qiita](https://qiita.com/ksasao/items/1a221843894fdb57677b)

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

![トイレ管理システム.png](https://qiita-image-store.s3.amazonaws.com/0/8308/a6251dc0-0cd4-2985-0d12-2e45ea124ebe.png "トイレ管理システム.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 以降は反映されていません）
※ オプションビット２の追加（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のファームウェア書き込みしたらエラーが出た - Qiita](https://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](https://qiita-image-store.s3.amazonaws.com/0/8308/2c154a51-8029-0e5c-d036-b48f14ac661d.jpeg "IMG_2507.jpg")

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


# Raspberry Pi

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

![14890445_192909701154810_6364703619249552546_o.jpg](https://qiita-image-store.s3.amazonaws.com/0/8308/571cf5ce-b7ba-ca11-fccf-53f5a0929c8d.jpeg "14890445_192909701154810_6364703619249552546_o.jpg")


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

## 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からの信号のうけとり

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

```python: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にデータを投げます。


```python: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
- トイレの使用時間のログ
を送信しています。

```python: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](https://qiita-image-store.s3.amazonaws.com/0/8308/1ab74307-a878-eb4d-3e64-a3de2b71e6e0.png "Firebase_Console.png")


# Webクライアント

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


```js
$(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)
	}
});
```

# まとめ

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