会社で流しの使用状況を可視化しました

# 経緯
僕はお弁当を会社に持っていくタイプの人間なんですが、なんといっても洗うのがめんどくさい。家に帰って弁当箱を洗って、という工程が大変辛い。なので、会社の流しで洗いたい、という気持ちです。
ところが会社では同じように考える人が結構いて、昼間とか流しの空き待ちをしないといけないんですね。これはこれで面倒ということがあり、空いたら通知してほしいよねみたいな話がありました。
そしてこれは稀によくあることだと思うんですが、会社に未使用のRaspberry PI(以下ラズパイ)が落ちていて「これで人がいたら通知とかできそうだぞ？」と思って作った、という経緯です。

# 作るもの
一定時間流しに人がいたら通知、一定時間人がいなくなったらいなくなったことを通知してくれるものです。通知先にはSlackを使います。

# センサーの調達とか
センサーとかジャンパーワイヤーとかが必要になるんですが、[マルツ秋葉原本店](https://www.marutsu.co.jp/pc/static/shop/akihabara)ですべてのものが揃いました。ラズパイ本体も売ってます。

# 用意するもの(今回使ったもの)
* Rasbianが動作するラズパイ一式(Wi-Fiドングル、SDカード等々)
    * 補足として、電源となるACアダプタは5.0V/3.0Aのものを使用しました。
    * ラズパイ自体は初期モデルなのでその辺のACアダプタでも動くと思いますが、今回はセンサーを乗っけることもあり、ちょっと出力高めのものを選んでます。
* 人感センサー
* ジャンパーワイヤー（メス-メス）

## 裸でラズパイを置きたくない、ということであれば以下
* ラズパイの箱
* ピンバイス
* デザインナイフ

もしくはプラ版とかで自作もありかなと思います。

# 前提条件
* Rasbianがインストールされている
* 無線/有線で通信ができる
* ラズパイにSSH接続できる
* Slackに通知用のチャンネルがある
* Incoming Webhookの設定が済んでいる

# 手順
## 人感センサーを接続・テスト
次のリンクの通り、人感センサーとラズパイを繋ぎ、プログラムをコピペして動かします。
[【Raspberry Pi】自作人感センサーの使い方と活用法 \- CHASUKE\.com](https://chasuke.com/motionsensor/)

問題なく動けば接続はOKです。

## 人が居続けている場合Slackに投げるようにする
人感センサーは、人間を感知してすぐ反応してしまうので、人が通っただけで通知してしまいます。なので、一定時間居たら通知するようにします。加えて、人が居ても居ないと検知してしまうケースが目立ったので、N秒間人が居ない場合、流しを使え終えたと判定します。それが次のコードです。

```python:sink_observer.py
from datetime import datetime
import RPi.GPIO as GPIO
import json
import logging
import requests
import sys
import time
​
GPIO_PIN = 18
SLACK_SETTINGS = {
    'url'        : '{Incoming WebhookのURL}',
    'botname'    : 'その目だれの目',
    'icon'       : ':eyes',
    'start_using': '流しに人がいます。',
    'finished'   : '使い終わったようです。'
}
is_using = False
​
formatter = '%(levelname)s \t%(asctime)s\t%(message)s'
logging.basicConfig(filename='sink_observer.log', level=logging.INFO, format=formatter)
​
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
​
def push_to_slack(status):
    datas = {
        'username'   : SLACK_SETTINGS['botname'],
        'icon_emoji' : SLACK_SETTINGS['icon'],
        'text'       : SLACK_SETTINGS[status],
        'contentType': 'application/json'
    }
    payload = json.dumps(datas)
    requests.post(SLACK_SETTINGS['url'], payload)
​
def start_using():
    global is_using
​
    if is_using:
        return
    is_using = True
    push_to_slack('start_using')
    logger.info('start_using')
​
def finished():
    global is_using
​
    if not is_using:
        return
    is_using = False
    push_to_slack('finished')
    logger.info('finished')
​
def main(sleeptime, interval, retry):
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(GPIO_PIN, GPIO.IN)
​
    detect_cnt = 0
    no_detect_cnt = 0
    try:
        logger.info('終了:Ctrl+C')
        while True:
            if GPIO.input(GPIO_PIN) == GPIO.HIGH:
                detect_cnt += 1
                # 動作確認用のログ
                logger.info('detect\tdetect_cnt:{}'.format(str(detect_cnt)))
                if detect_cnt <= 2: # HACK: 本当はここも指定できた方が調整しやすいです
                    time.sleep(interval)
                    continue
                detect_cnt = 0
                no_detect_cnt = 0
                start_using()
                time.sleep(sleeptime)
            else:
                no_detect_cnt += 1
                logger.info('no detect\tdetect_cnt:{}'.format(str(detect_cnt)))
                if no_detect_cnt <= retry:
                    time.sleep(interval)
                    continue
                detect_cnt = 0
                no_detect_cnt = 0
                finished()
                time.sleep(interval)
    except KeyboardInterrupt:
        logger.info('終了処理中...')
    finally:
        GPIO.cleanup()
        logger.info('GPIO clean完了')
​
if __name__ == '__main__':
    sleeptime = 10
    interval  = 3
    retry     = 10
    # パラメータ調整用
    if len(sys.argv) > 4:
        sleeptime = int(sys.argv[1])
        interval  = int(sys.argv[2])
        retry     = int(sys.argv[3])
​
    main(sleeptime, interval, retry)
```

## 起動
ラズパイにSSH接続して以下を行ないます。pipインストールは初回のみ行なえば大丈夫です。

### 初回のみ行なう
```bash
$ pip3 install requests
```

### 次回以降はこれだけでOK
```bash
$ nohup python3 sink_observer.py &
```

`nohup`と`&`をしているので、SSH接続切っても動き続けてくれます。終了させたくなったら`ps aux | grep sink_observer.py`とかでプロセス番号を調べて`kill {プロセス番号}`で終了できます。

## ラズパイの箱を再利用
裸はなーーーと思い、ラズパイの入っていたプラ製の箱を加工しました。
GPIO・USB・電源・LANケーブルを通すところに適当にマーカーを引いて、ピンバイスで穴あけした後、デザインナイフで縁を整えました。
今回加工した箱はポリプロピレン系のプラスチック製っぽくて、横着したら割れてしまったので、割とピンバイスで穴を開けておくの重要でした。
あとはケースの適当な場所に穴を開けて針金を通して、人感センサーを固定すれば完成です。こんな感じになりました。
ノートの紙が手前にあるのは、人感センサーの仕様的に100°の視野角ということで、誤検知しないように視野を制限するためにおいてます（完全に応急処置です）。
![IMG_5364.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222089/c899c122-1d5c-db7e-b114-16c5e84e1f47.jpeg)

# 追記(2019/01/23)
あまりにもひどく誤検知をするので、下のようにしました。
![2DA5332C-AF54-4545-B96F-2FC82EF70528.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222089/bb4f56d2-fab3-77cd-99ce-11467e1d58bd.jpeg)

これをやった結果、誤検知率がグッと下がりました。

## 誤検知について
あくまで仮説です。結論から言うと、センサが可視光に反応していた説です。
誤検知の具体的な内容は

* 人がいないのに通知が飛ぶ
* 夜間に通知が飛ぶ

というものでした。センサの特性は焦電素子を使った赤外線センサです。焦電素子自体は、ざっくりした解釈だと、赤外線および光(可視光)を検出することができるものです。人体は赤外線を発しているので人感センサとして使える、という理屈です。ただし、赤外線のみを検出するわけではなく、**可視光、つまり我々が見ている光**にも反応を示します。ラズパイおよびWi-Fiドングルは明らかに可視光を発していましたので、これを検出してしまうと誤検知となってしまいます。行なった改良は、ラズパイが発する光とWi-Fiドングルが発する光からセンサを遠ざけた上で、センサの持つ100°の視野角を制限し指向性を持たせる、という目的のもと行なった改良です。プラ板やプラ棒で大掛かりなものを作りましたが、同じことができればここまで作りこむ必要もないかな、と思います。
これによって、流しの前を何人かが通過したことによって起こる誤検知や、ラズパイ・Wi-Fiドングル光を検知することによる夜間の誤検知をなくしました。これをやってから、誤検知率は1%もなさそうな感じです。

# まとめ
~~まぁまぁちゃんと誤検知します。人通りが多い場所に流しがあるので仕方ない事ではあるんですが…。とはいえちゃんと機能はしてるので、アルゴリズムを工夫したりパラメータの調整をすれば~~実用レベルにはなると思います。これで流し待ちしたり、使ってるから一旦席に戻るみたいなことをしなくて済むようになる…といいな！