なんで作ったの?
今日はドタバタした一日だった、と思うことがある。家の中のドタバタ度を定量化できないか。右往左往という表現もある。右へ左へ、よくわからず動き回る。そんな家の中でのひとの動きを計ってみたい。得られた数値も記録したい。これが動機です。
なにをするもの?
PIR (passive infrared) sensorを使って人の動きをカウントする。測定は定点で行う。毎日23:59に今日一日のカウントをファイルに記録し、ついでにSlackにポストする。
どういう仕組み?
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がつけてあり、検出のたびに小さな音が鳴るようにしてあります。誰もいないのにぴ、ぴ、ぴ。今では慣れっこになってしまい、だあれ?と話しかけたりしています。