20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

トイレの空き状況をチャットツールから確認できるようにしてみた。これってIoT!?

Last updated at Posted at 2016-06-25

はじまり

むかしむかし、IoTというバズワードと共に、トイレ運用アプリが流行った時期がありました。
もう何番煎じかもわからないけど、負けじと作ってみたものをここに公開します。

仕組み

私が足繁く通っている会社では、東日本大震災の教訓を忘れずに節電の意識を保っていることから、基本的にトイレの照明はその都度消しているという運用です。
と、いうことで、空き状況のチェックは光センサーひとつでOKでした。

20160402235311.png

トイレ

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に変えればいいので置き換え可能です。

使用イメージ

20160403005246.jpg

20160403012942.png

感想

当初、設置の自由度のために、乾電池やバッテリー駆動を目指してみましたが、思ったより電力を消費しています。
コンセントが近くにあるなら、素直にそこから電源をとったほうが良さそうです。
電池でやるとしたら、パーツ構成とプログラムの見直しが必要そうです。

ソースコード

sketch.ino
#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円ショップで適当なケースを探してきました。
センサーを覆っていますが、明るさの違いはなんとか検出できています。

DSC_0024.JPG

おまけ

とある人にHTTPで状態取得できるようにしてほしいと言われ、Node.jsでHTTPサーバを立てました。

httpd.js
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];
});
20
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?