うちIoT

廊下通行量カウンター

なんで作ったの?

今日はドタバタした一日だった、と思うことがある。家の中のドタバタ度を定量化できないか。右往左往という表現もある。右へ左へ、よくわからず動き回る。そんな家の中でのひとの動きを計ってみたい。得られた数値も記録したい。これが動機です。

なにをするもの?

PIR (passive infrared) sensorを使って人の動きをカウントする。測定は定点で行う。毎日23:59に今日一日のカウントをファイルに記録し、ついでにSlackにポストする。

log fileの最新部分
ある日の記録

どういう仕組み?

PIR sensorで人の動きを感知してカウントし、現在のカウントをLEDパネルに表示します。毎日23:59に今日一日のカウントを記録しますが、それはRaspberry pi 3B(一号機)上で行っています。Raspberry piからSlack channelへ記録をポストします。

このPIR sensor、人感センサーともいわれるものなので、いたるところで使われているのですが、仕組みがなかなかオモシロイ。この辺を見てみてください。

使っている技(わざ)

興味のある方は、ここに出てくる文や単語をGoogle検索してさらに調べてみてください。
  • ESP8266からhttp POSTする
  • Flaskでhttpサーバーを作る
  • PythonでSlackに投稿する

作り方

材料

  • Adafruit HUZZAH ESP8266 Breakout (Adafruit, product id: 2471)
  • PIR (motion) sensor (Adafruit, product id: 189)
  • Adafruit 0.56″ 4-Digit 7-Segment Display w/I2C Backpack – Red (Adafruit, product id: 878)
  • SparkFun Logic Level Converter – Bi-Directional
  • Raspberry pi + Flask
  • Bread board
  • Jumper pins

接続

ESP8266のロジック電圧が3.3Vなので、5Vで使うLEDボードへのI2C接続にはレベルシフターをはさんでいます。PIRからのOUTPUTはそのままつないでいますが、Adafruitのページによると、今回使っているセンサーのアウトプットが3.3Vだからです。

PIR sensorの反応範囲を狭める

PIR sensorはかなりの範囲に反応します。あまりに広すぎて、廊下を通行しない動きまで拾ってしまいましたので、トイレットペーパーの芯を使ってスコープを狭めてみたところ、狙い通り反応するようになりました。

見た目はよくないがばっちり狙える

code

ESP8266側のコードはこんな感じです。

// human_traffic_counter_auto.ino

// to use time(), localtime()
#include <Time.h>
#define SGT 3600*8

// ESP8266
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>

// 7 seg on I2C
#include <Wire.h>
#include "Adafruit_LEDBackpack.h"
#include "Adafruit_GFX.h" // for LED backpack

// json
#include <ArduinoJson.h>

// vars
// LED backpack
Adafruit_7segment LED_board = Adafruit_7segment();

// to connect wifi AP
char ssid[] = "your ap name";
char pass[] = "key for connection";

// pins
const int pirPin = 12; // signal from PIR

// status monitor vars
int pirState = LOW;
int val = LOW;
int ht_count = 0;
int ht_reported = 0;

// set time to send traffic count
  int H = 23;
  int M = 59;


/*
 * ========== setup ==========
*/
void setup() {
  // serial is only for debugging
  Serial.begin(9600);

  // pin modes
  pinMode(pirPin, INPUT);

  /////  [ESP8266 wifi]  /////
  WiFi.begin(ssid, pass);
  while(WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.println(".");
  }
  Serial.println("WiFi connnected to AP.");
  
  // I2C
  Wire.begin(4,5); // SDA, SCL
  // LED backpack
  LED_board.begin(0x70); // red
  LED_board.setBrightness(2);

  // internet time
  configTime( SGT, 0, "ntp.nict.jp", "sg.pool.ntp.org"); 
}


/*
 * ========== loop ==========
*/
void loop() {
  val = digitalRead(pirPin);    // read input value
  if (val == HIGH) {            // check if the input is HIGH
    if (pirState == LOW) {
      ht_count ++;
      Serial.print("Motion detected!  ");
      Serial.println(ht_count);
      pirState = HIGH;
    }
  } else {
    if (pirState == HIGH){
      Serial.println("Motion ended!");
      pirState = LOW;
    }
  }

  time_t t;
  struct tm *tm;
  static const char *wd[7] = {"Sun","Mon","Tue","Wed","Thr","Fri","Sat"};

  t = time(NULL);
  tm = localtime(&t);
  // this is to check if time is correctly set
  Serial.printf(" %04d/%02d/%02d(%s) %02d:%02d:%02d\n",
        tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
        wd[tm->tm_wday],
        tm->tm_hour, tm->tm_min, tm->tm_sec);
  
  if ((tm->tm_hour == H) and (tm->tm_min == M)) {
    if (ht_reported == 0) {
      // send ht_count to raspi
      sendToRaspi(ht_count);
      // set it to reported
      ht_reported = 1;
      // display 8888
      ht_count = 8888;
      updateLED(ht_count);
    }
  }

  // update LED
  updateLED(ht_count);
  delay(100);
}


/*
 *  ========== functions ==========
 */
void sendToRaspi(int count){
  DynamicJsonDocument doc(100);  // v6
  doc["traffic"] = count;  // v6

  if (WiFi.status() == WL_CONNECTED){
    HTTPClient http;

    http.begin("http://<ip address of 3B1>/toslack");
    http.addHeader("Content-Type", "application/json");

    String json_data;
    serializeJson(doc, json_data);  // v6

    int httpCode = http.POST(json_data);
    String payload = http.getString();

    Serial.println("#++++++++++++++++++++++#"); //--------(1)
    Serial.print("httpCode: ");
    Serial.println(httpCode); //Print HTTP return code
    Serial.print("payload ");
    Serial.println(payload); // Print request response payload
    Serial.println("#++++++++++++++++++++++#\n"); //------(1)

    http.end();
  }
}

void updateLED(int count)
{
  // update LED display
  LED_board.print(count);
  LED_board.writeDisplay();
}

Raspberry piのほうは、flaskで”/toslack”に送られてきたリクエストをさばいています。write_log2()ではログファイルにlineをappendしているだけ、slackへの投稿はsendToSlack2()で行っています。

# receive json from ESP8266 (human traffic counter),
# write a log then
# post slack

from flask import Flask, abort, request
import requests
import json
import datetime


def timestamp():
    # returns time stamp
    dt = datetime.datetime.now()
    ts = dt.strftime('%d%b%Y-%H:%M:%S')
    return ts


webhook_url = 'https://hooks.slack.com/services/'
webhook_url += 'your slack key'

app = Flask(__name__)


@app.route('/toslack', methods=['POST'])
def foo():
    print("\n=== POST received! ===")
    print("request.json is shown below.")
    print(request.json)

    if not request.json:
        abort(400)
    else:
        if "traffic" in request.json:  # from human traffic counter
            sendToSlack2(request.json)
            write_log2(request.json)

    return json.dumps(request.json)


def sendToSlack2(json_in_dict):
    print("sendToSlack2() called.")

    msg = "Human Traffic Counter\n"
    msg += "Report time: " + timestamp() + '\n'
    msg += 'Count: ' + str(json_in_dict['traffic']) + "\n"
    msg += ':1234: :walking: :woman-walking: :woman-walking: :man-walking:'

    slack_data = {'text': msg}
    response = requests.post(
        webhook_url, data=json.dumps(slack_data),
        headers={'Content-Type': 'application/json'}
    )

    print('response: ', response)

    if response.status_code != 200:
        print('### Request to slack returned an error: ', response.status_code)
        print('### Response text: ', response.text)


log_file2 = './humanTraffic_log.txt'


def write_log2(json_in_dict):
    data = [timestamp()]
    data.append(str(json_in_dict['traffic']))
    data_line = '\t'.join(data)
    # append data_line to the log file
    with open(log_file2, mode='a') as log:
        log.write(data_line + '\n')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=<ser your port here>, debug=True)

使ってみて

日によってもちろんばらつきますが、200前後が普通の日のカウントです。旅行前の準備でバタバタしていたり、おじいちゃんおばあちゃんたちが来ている期間には300を超えます。普通ならだいたい一人あたり50ぐらい、ドタバタすると1.5倍になるという結論です。

それだけの話なのですが、ログ好きにはたまらないのです、こういうことのロギング。そもそも家のある一点を家人が一日に何回横切るのかなんて、数えるまで予想もつかなかった。ドタバタしていると感じる日の活動量が普通の日の何倍か、などというのも数えてみるまでわからない。

一人暮らしの人の見守りにも使えますね。カメラじゃないので見られている感じはまるでありませんし、一日普通にしていればこのぐらいのカウントになる、ということを前もって知っておけば、少なすぎるとき、あるいは多すぎるときを異常としてとらえることもできます。うちでは一日分をまとめていますが、特定の時間に活動があるかどうかを調べることも簡単です。今はSlackにその日のカウントを投稿していますが、reportingは別のものに変えることもできます。

このあとの予定

センサーを二か所につけると、その反応タイミングの違いから、どちらからどちらへ人が移動したのかを知ることができそうです。が、考えてみただけでやってみる予定はありません。

うらばなし

PIR sensorが検出するのは熱源の移動。別に人じゃなくてもヨイ。のですが、夜中に誰も横切っていないのにカウントが上がることがあるのは少しコワイ。この記事の紹介では省きましたが、実機にはpiezo buzzerがつけてあり、検出のたびに小さな音が鳴るようにしてあります。誰もいないのにぴ、ぴ、ぴ。今では慣れっこになってしまい、だあれ?と話しかけたりしています。

ABOUT ME
misson
ものづくりが趣味。頭の中でモヤモヤと渦巻くガスを形として沈殿させるのが趣味。座右の銘その2は:未来は今の積分値


follow us in feedly

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です