TOWN株式会社の今のオフィスは個室トイレが1つしかない。
トイレに行って空いていなくて待っているか、席に戻って開くのを待っている。
トイレに行く際もいちいちセキュリティドアを出ないといけないので、戻るのもめんどくさい。
トイレ難民続出中。。。
なので、その場で個室の空き状況が確認できたら便利だと思って空き状況確認システムを作ってみた!
構成図
最初はAPI GatewayとLambdaは使わずに、Google Apps ScriptとGoogle Spread Sheetで作ろうと思って実装してたけど、センサーがGoogleのリダイレクトに対応できなくて諦めました。笑
(リダイレクトに対応したHTTPSRedirectというセンサーのモジュールがあるのですが、上手く動作しなかった。)
事前に用意するもの
- ESPr® Door Sensor
- FTDI USBシリアル変換アダプター(5V/3.3V切り替え機能付き)
- Micro USB ×2
- AWS API GatewayでAWS Lambdaを叩けるWebhook
- Slack Appsでアプリを作成して、トイレチャンネルに所属させたトークン
(Scope[admin, channels:history, chat:write:bot, chat:write:user]) - S3(バケットにjson(DB)を置いたもの)
ESPr® Door Sensor + FTDI USBシリアル変換アダプター
センサー周りの操作
センサーにプログラムを書き込むにはArduino IDEが必要です。
こちらからダウンロードしました。
環境設定
環境設定を開いて以下のように設定します。
追加するURLはこちら https://arduino.esp8266.com/stable/package_esp8266com_index.json
次に ツール > ボード > ボードマネージャー **をクリックしてESP8266**をインストール。
最後に** ツール > ボード から「Generic ESP8266 Module」**を選択して以下のように設定で終了。
スケッチのマイコンボードに書き込むからプログラムを書き込めます。
※シリアルケーブルの基盤にある3pinの部分をPROG2本に挿すと書き込みモード、PROGとRUNに挿すと実行モードになります。
コード
事前に作っておいてAPI GatewayのWebhookのドメイン部分、それ以外の部分を以下のコードに入れます。
今回API Gatewayに送信するPOSTデータ
{ "gender": "性別(今回はMANのみの実装)", "value": "開閉値" }
センサーに書き込むコード
#include <ESP8266WiFi.h> #include <HTTPSRedirect.h> #include <DebugMacros.h> #define HTTPS_PORT 443 #define HOST "Amazon API GatewayのWebhookのドメイン部分" #define URL "Amazon API GatewayのWebhookのドメイン以降部分" #define CLOSE 0 const char* ssid = "WiFi SSID"; const char* password = "WiFi Pass"; HTTPSRedirect* client = nullptr; extern "C" { #include "user_interface.h" } boolean flag = false; int LED = 4; int reed_sw = 5; void setup() { Serial.begin(74800); delay(10); // We start by connecting to a WiFi network Serial.println(); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); } void loop() { int door_state; door_state = digitalRead(reed_sw); Serial.print("Door State:"); if(door_state == CLOSE){ Serial.println("Close"); } else{ Serial.println("Open"); } // Use WiFiClient class to create TCP connections Serial.println(doRedirectGet(door_state)); delay(10); if(door_state == CLOSE){ Serial.println("DEEP SLEEP 60s"); ESP.deepSleep(0, WAKE_RF_DEFAULT); //ドアが閉じている間はドアが開くまで待機 } else{ Serial.println("DEEP SLEEP"); ESP.deepSleep(0, WAKE_RF_DEFAULT); //ドアが開いている間はドアが閉じるまで待機 } delay(1000); } // Redirectを回避してHTTP-GETする String doRedirectGet(int door_state) { String body = ""; client = new HTTPSRedirect(HTTPS_PORT); client->setPrintResponseBody(false); client->setContentTypeHeader("application/json"); Serial.print("Connecting to "); Serial.println(HOST); // 10回まで接続を行う bool connect_flag = false; for (int i=0; i<10; i++) { int retval = client->connect(HOST, HTTPS_PORT); if (retval == 1) { Serial.print("Connected to "); Serial.println(HOST); connect_flag = true; break; } else { Serial.println("Connection failed. Retrying..."); } } if (!connect_flag) { Serial.print("Connection failed to server: "); Serial.println(HOST); return ""; } // payload判定 String state = ""; if(door_state == CLOSE){ state = "CLOSE"; } else { state = "OPEN"; } String payload = "{\"gender\": \"MAN\", \"value\":\"" + state + "\"}"; // POST bool post_flag = false; for (int i=0; i<10; i++){ client->POST(URL, HOST, payload); body = client->getResponseBody(); if (client->getStatusCode() == 200) { Serial.println("POST Success"); post_flag = true; break; } } if (!post_flag) { Serial.println("POST Failed"); } Serial.println("closing connection"); client = nullptr; delete client; return body; }
Lambdaでの操作
今回はS3をデータベースとして使いました。
なので、ロールにはS3FullAccessをアタッチしました。
また、S3には以下のようなJSONを置いておきました。
{ "MAN": "OPEN", "WOMAN": "CLOSE" }
コード
トイレに入るたびにSlackに通知が来ては邪魔なので、Slackのメッセージ更新機能を使いました。
細かいメソッドについてはこちら
トークンを使ってLambdaやCurlコマンドからあらかじめメッセージを送って置いて、それを更新する感じです。
更新にはts(タイムスタンプ)が必要なので、SlackのWeb版でコメントのリンクをコピーをすると「1543312999067300」みたいなのが取れるので、「1543312999.067300」のように右から6桁目に小数点を入れます。
Channel IdもWeb版のURL(https://.slack.com/messages/〇〇〇〇/)に表示されます。
また、Lambdaにはない外部ライブラリを使うので、ローカルでpip install slackclient -t ./ などとやってディレクトリにライブラリをインストールし、Lambdaのスクリプトも含めてzipで固めてアップロードしました。(やり方はググれば出てきます)
import json import boto3 from datetime import datetime from slackclient import SlackClient # S3(DB) S3_BUCKET_NAME = '' S3_DB_NAME = '' S3_client = boto3.client('s3') response = S3_client.get_object(Bucket=S3_BUCKET_NAME, Key=S3_DB_NAME) DB = json.loads(response["Body"].read()) # Slack slack_token = '' slack_client = SlackClient(slack_token) channel_id="" ts_num="" #残すメッセージts def channel_list(slack_client):#使わない channels = slack_client.api_call("channels.list") if channels['ok']: return channels['channels'] else: return None def send_message(slack_client, channel_id, message): #使わない response = slack_client.api_call( "chat.postMessage", channel=channel_id, text=message, ) return response def update_message(slack_client, channel_id, message,ts): response = slack_client.api_call( "chat.update", channel=channel_id, text=message, ts=ts ) return response def delete_message(slack_client,channel_id,ts): response = slack_client.api_call( "chat.delete", channel=channel_id, ts=ts ) return response def get_history(slack_client,channel_id): # 使わない response = slack_client.api_call( "channels.history", channel=channel_id, ) return response def print_json(json_data): return print("{}".format(json.dumps(json_data,indent=4))) def lambda_handler(event, context): date = datetime.now().strftime("%Y/%m/%d %H:%M:%S") gender = event["gender"] value = event["value"] # DB更新 DB[gender] = value S3_client.put_object(Body=json.dumps(DB, indent=4), Bucket=S3_BUCKET_NAME, Key=S3_DB_NAME) # Slack output Man_data = value output = "\tMan 👨 :`" + Man_data + "`\n" + "Woman 👩 :`Coming soon...`" for message in get_history(slack_client,channel_id)["messages"]: ts=message["ts"] if ts != ts_num: response=delete_message(slack_client,channel_id,str(ts)) response=update_message(slack_client, channel_id, output, ts_num) return { 'statusCode': 200, 'body': date + " " + gender + " " + value }
動作確認
設置するとこんな感じ。上に丁度いい凹みがあったので、そこに電池類は配置、センサーだけにゅっと出してあります。
上から見るとこう。この凹みがあるおかげでバッテリーもそこまで目立ちません。
トイレ内部から見るとこんな感じでかなりきれいに隠れてます。
なので、トイレの中に入っている人が不審な機器がある!と怖がらずに済みます。
内側の方が簡単だけど、なんか機器あったら怖い。。。
動作させた動画がこちら。
女子トイレはまだ実装していないのでComing Soonです。
表示はこんな感じ。
これでトイレ難民を救えた!
かかる費用はこんな感じ。
2018/12/25追記
2000mAhのモバイルバッテリーで動作をさせていましたが、11月28日に動作開始で約1ヶ月程度電池はもちました。
序盤は開け締めやテストを行っていたのを、そのまま使用したので1ヶ月程度は2000mAhのバッテリーでも持ちそうです。