はじまり
むかしむかし、IoTというバズワードと共に、トイレ運用アプリが流行った時期がありました。
もう何番煎じかもわからないけど、負けじと作ってみたものをここに公開します。
仕組み
私が足繁く通っている会社では、東日本大震災の教訓を忘れずに節電の意識を保っていることから、基本的にトイレの照明はその都度消しているという運用です。
と、いうことで、空き状況のチェックは光センサーひとつでOKでした。
トイレ
ESP-WROOM-02は、スイッチサイエンスで開発ボードを購入しました。
選定理由は、全部載せで楽だなと思ったので。
主な部品
商品名 | 価格 |
---|---|
ESP-WROOM-02開発ボード | 2,160円 |
TSL2561デジタル光センサボード | 702円 |
AWS
当初は、AWS IoTを使ってみたかった。
どうやら、ESP-WROOM-02ではTLS1.2に対応するのが難しいらしい、ということで断念しました。
で、EC2にMosquittoをインストールしました。 Hubotをインストールしなければならないことから、どちらにしろ別途サーバは必要だったかな。
特にEC2である必然性はなく、スペックも最低限のものでOKです。
Hubot #1
https://github.com/kunikada/hubot-toilet
Hubotのスクリプト。こんなことやります。
- Mosquittoにトイレ使用状況の問い合わせ
- チャットの返事、連絡など
- センサーデバイスの細かなパラメータの設定
Hubot #2
https://github.com/kunikada/hubot-chatwork
ChatWorkアダプタ
おそらく、みんなが使っているであろう、ググったら出てくるChatWorkアダプタに若干手を入れました。
ルームの指定をしなくてもいいようにしています。
チャット
うちの会社では、ChatWorkを使用しています。正直APIは使いにくいです。
専用アカウントを用意して、APIの使用申請をしています。
このあたりは、Slackを使用するのであれば、HubotのアダプタをSlackに変えればいいので置き換え可能です。
使用イメージ
感想
当初、設置の自由度のために、乾電池やバッテリー駆動を目指してみましたが、思ったより電力を消費しています。
コンセントが近くにあるなら、素直にそこから電源をとったほうが良さそうです。
電池でやるとしたら、パーツ構成とプログラムの見直しが必要そうです。
ソースコード
#include <Wire.h>
#include <Ethernet.h>
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <PubSubClient.h>
#include <TSL2561.h>
#define WIFI_SSID "YOUR_WIFI_SSID"
#define WIFI_PASS "YOUR_WIFI_PASSWORD"
IPAddress mqttHost(123, 45, 67, 89);
#define MQTT_PORT 1883
#define MQTT_USER "YOUR_MQTT_USER"
#define MQTT_PASS "YOUR_MQTT_PASSWORD"
#define TSL2561_VDD 2
#define TSL2561_GND 15
#define TSL2561_SDA 13
#define TSL2561_SCL 12
#define TSL2561_ADDR 0x39
void callback(char*, byte*, unsigned int);
void onCheck();
void onChange(int, int);
WiFiClient wifiClient;
NTPClient timeClient("ntp.jst.mfeed.ad.jp");
PubSubClient mqttClient(mqttHost, MQTT_PORT, callback, wifiClient);
String clientName;
int cntPub = 0;
int cntChange = 0;
TSL2561 tsl(TSL2561_ADDR);
int progress, lastResult;
int luxThreshold = 50;
int sleepMinOnCheck = 10;
int sleepMinOnChange = 0;
bool sleepHour[24] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
bool sleepDow[7] = {0,0,0,0,0,0,0};
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("connected");
while (timeClient.getRawTime() < 60) { // 1 minute timeout
timeClient.update();
delay(2000);
}
// 識別子としてMACアドレスを使用
uint8_t mac[6];
WiFi.macAddress(mac);
for (int i = 0; i < 6; i++) {
clientName += String(mac[i], HEX);
}
// MQTTサーバから設定を取得
mqttClient.connect(clientName.c_str(), MQTT_USER, MQTT_PASS);
String topic = "reset/" + clientName;
String payload = String(timeClient.getRawTime());
mqttClient.publish(topic.c_str(), payload.c_str(), true);
topic = clientName + "/settings/lux_threshold";
mqttClient.subscribe(topic.c_str(), 1);
topic = clientName + "/settings/sleepmin_oncheck";
mqttClient.subscribe(topic.c_str(), 1);
topic = clientName + "/settings/sleepmin_onchange";
mqttClient.subscribe(topic.c_str(), 1);
topic = clientName + "/settings/sleep_hour";
mqttClient.subscribe(topic.c_str(), 1);
topic = clientName + "/settings/sleep_dow";
mqttClient.subscribe(topic.c_str(), 1);
for (int i = 0; i < 15; i++) {
if (cntPub == 5) {
Serial.println("settings loaded");
break;
}
mqttClient.loop();
delay(1000);
Serial.print(".");
}
mqttClient.disconnect();
// ピン設定
pinMode(TSL2561_VDD, OUTPUT);
pinMode(TSL2561_GND, OUTPUT);
digitalWrite(TSL2561_VDD, HIGH);
digitalWrite(TSL2561_GND, LOW);
Wire.begin(TSL2561_SDA, TSL2561_SCL);
// 周囲の明暗状況に合わせて適切にゲインを変更してください
//tsl.setGain(TSL2561_GAIN_0X); // ゲインなし:周囲が明るい場合
tsl.setGain(TSL2561_GAIN_16X); // ゲインx16:周囲が暗い場合
// 積算時間を変更することで、光の測定時間を変更することができます
// 長時間測定すると、データの取得は遅くなりますが、低光度環境での測定性能が向上します
tsl.setTiming(TSL2561_INTEGRATIONTIME_13MS); // 短時間測定:明るい環境
//tsl.setTiming(TSL2561_INTEGRATIONTIME_101MS); // 中時間測定:中程度の明るさ
//tsl.setTiming(TSL2561_INTEGRATIONTIME_402MS); // 最長時間測定:暗い環境
}
void loop() {
onCheck();
// 明るさを取得
uint32_t lum = tsl.getFullLuminosity();
uint16_t ir = lum >> 16;
uint16_t full = lum & 0xFFFF;
uint32_t lux = tsl.calculateLux(full, ir);
// 同じ状態が5回連続した場合にステータスを変更
int result = 0;
progress = progress << 1;
if (lux > luxThreshold) {
progress++;
}
progress &= 0x1f;
if (progress == 0x1f) {
result = 1;
} else if (progress == 0) {
result = 2;
}
if (result > 0 && result != lastResult) {
onChange(result, lux);
}
delay(1000);
}
void callback (char* topic, byte* payload, unsigned int length) {
String strTopic = String(topic);
String s = String((char*) payload).substring(0, length);
strTopic.replace(clientName, "");
if (strTopic == "/settings/lux_threshold") {
luxThreshold = atoi(s.c_str());
} else if (strTopic == "/settings/sleepmin_oncheck") {
sleepMinOnCheck = atoi(s.c_str());
} else if (strTopic == "/settings/sleepmin_onchange") {
sleepMinOnChange = atoi(s.c_str());
} else if (strTopic == "/settings/sleep_hour") {
char h[2];
for (int i = 0; i < 24; i++) {
sprintf(h, "%02d", i);
if (s.indexOf(h) != -1) {
sleepHour[i] = 1;
}
}
} else if (strTopic == "/settings/sleep_dow") {
s.toLowerCase();
if (s.indexOf("sun") != -1) {
sleepDow[0] = 1;
}
if (s.indexOf("mon") != -1) {
sleepDow[1] = 1;
}
if (s.indexOf("tue") != -1) {
sleepDow[2] = 1;
}
if (s.indexOf("wed") != -1) {
sleepDow[3] = 1;
}
if (s.indexOf("thu") != -1) {
sleepDow[4] = 1;
}
if (s.indexOf("fri") != -1) {
sleepDow[5] = 1;
}
if (s.indexOf("sat") != -1) {
sleepDow[6] = 1;
}
}
Serial.println(topic);
Serial.write(payload, length);
Serial.println("");
cntPub++;
}
void onCheck() {
int dow = ((timeClient.getRawTime() + 32400) / 86400L + 4) % 7;
int hour = (timeClient.getRawTime() % 86400L / 3600 + 9) % 24;
if (sleepDow[dow] || sleepHour[hour]) {
ESP.deepSleep(sleepMinOnCheck * 60 * 1000 * 1000);
delay(1000);
}
}
void onChange(int result, int lux) {
mqttClient.connect(clientName.c_str(), MQTT_USER, MQTT_PASS);
String topic = clientName + "/result";
String payload = String(timeClient.getRawTime()) + " " + String(result) + " " + String(lux);
while (!mqttClient.publish(topic.c_str(), payload.c_str(), true)) {
delay(1000);
}
mqttClient.disconnect();
switch (result) {
case 1:
Serial.println("well-lighted");
break;
case 2:
Serial.println("get dark");
break;
}
lastResult = result;
cntChange++;
if (sleepMinOnChange > 0 && cntChange > 1) {
ESP.deepSleep(sleepMinOnChange * 60 * 1000* 1000);
}
}
その後
見た目があやしすぎる、とお叱りの声を多数いただきまして、100円ショップで適当なケースを探してきました。
センサーを覆っていますが、明るさの違いはなんとか検出できています。
おまけ
とある人にHTTPで状態取得できるようにしてほしいと言われ、Node.jsでHTTPサーバを立てました。
var resultStatus = '0';
const http = require('http');
const server = http.createServer((req, res) => {
function respond(code, body) {
switch (code) {
case 404:
body = '404 Not Found';
break;
case 405:
body = '405 Method Not Allowd';
break;
case 500:
body = '500 Internal Server Error';
break;
}
res.writeHead(code, {'Content-Type': 'text/plain'});
res.end(body);
}
if (req.method != 'GET') {
respond(405);
return;
}
var params = req.url.split('/');
if (params[1] != 'light') {
respond(404);
return;
}
switch (params[2]) {
case 'DEVICE_ID':
break;
default:
respond(404);
return;
}
switch (resultStatus) {
case '1':
respond(200, 'true');
break;
case '2':
respond(200, 'false');
break;
default:
respond(500);
}
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(80);
const mqtt = require('mqtt');
const client = mqtt.connect('mqtt://localhost:1883', {'username': 'USERNAME', 'password': 'PASSWORD'});
client.on('connect', () => {
client.subscribe('DEVICE_ID/result');
});
client.on('message', (topic, message) => {
var params = message.toString().split(' ');
resultStatus = params[1];
});