課題
保育園で送迎バスに置き去りにされた園児が亡くなった事故が発生した。もう二度と起こってほしくない。
高価なシステムを導入するより、現場の課題を現場の人が安価に解決する方法があるはず。
従来の方法
GPSを使用した運行状況報告について:
送迎バス位置・GPS全12システム徹底比較!保育園・幼稚園向けおすすめは?
置き去り事故防止安全装置について:
【徹底比較】幼稚園バス置き去り対策|各装置のメリット・デメリット
今回作ってみた
今回作ってみた「送迎バス運行状況報告&置き去り事故防止安全装置」の説明はこれ
Proto Pediaのページ内の作品紹介
送迎バス運行状況報告&置き去り事故防止安全装置
動画はこれ
https://youtu.be/YnzLMCrD4Ew
システム構成
ディスプレイ表示
上に、現在位置の経度・緯度と、上右に振動値
画面真ん中左側に直近の1分毎の人感回数(積算)/分、1分したら0に戻してまた積算開始
人感回数/分を経時グラフで表示(縦軸:人感回数/分、横軸:経過時間(横全体で240分)、1分毎の人感回数を白い●でプロットする。最新値が右端、一番古い239分前が左端で表示。
Groveケーブルで、M5Stack用GPSユニット [U032]を接続する。
Hatとして、M5StickC PIR Hat(AS312搭載)[U054]を接続する。
人感センサーが感知したら、M5StickC Plus内蔵のLED(赤)が一瞬光る。
送迎ルートを走り保育園の駐車場に戻ってきた時から5分後から60分後までの時間に、人感センサーが感知したときと振動を感知したときに、緊急連絡し、M5StickC Plus内蔵のLED(赤)が光る。
送迎バスのルートについて
定められたルートを送迎バスが走ることが前提条件。
定められたルート上の有名地点または停車地点などを緯度・経度で登録する。
今回、喫茶店の駐車場をうさぎ幼稚園の駐車場として設定してテストした。一番遠い地点(この図の例だと鎌倉市役所)で引き返し、同じ地点を往復で2回通過するルートを例とした。多くの場合、1回のみ通過する地点と往復で2回通過する地点を含むルートがあると思われるが、今回はこのルートを設定しテストした。
緯度経度の設定はGoogle map内で、右クリックで緯度経度情報画面を表示、緯度経度の行にマウスを合わせて左クリックでクリップボードに入るので、その情報を入れていく。
送迎バスが駐車場を発車するときのルール
送迎バスが駐車場を発車するときは、運転手さんが、毎回、M5StickCPlusの正面ボタン(ボタンA)を押す。「送迎バスが、うさぎ幼稚園を出発しました」とLine Notifyで送る(下図のメッセージ1部分)。Line Notifyは、送迎バスを利用する園児の保護者がメンバーになっているLINEグループに参加設定して、保護者全員に送られる想定。
送迎バスが定められたルートを走っているときの運行状況報告
定められたルート上の登録された地点を通過したメッセージがLine Notifyで送られてくる(下図のメッセージ2部分)。実際は、登録した地点を中心に半径約50メートルの円の領域に入ったら認識している。保護者は自宅を出て停車地点に行く時間が正確に把握でき、助かる。
送迎バスが定められたルートを走った後に幼稚園に到着したら
送迎バスが定められたルートを走った後に幼稚園に到着(GPSで認識)したら、時間カウントを始める。
5分以内に園児を降ろして、定められた幼稚園の駐車場に駐車すると仮定。
5分後に、人感センサー(PIRセンサー)が感知した場合、または、5分後に、M5StickC Plusのジャイロセンサーが振動を感知した場合、緊急連絡をLine Notifyで送信する。(下図のメッセージ3部分)
運転手さんが数時間後に運行のため乗車したときに反応しないように5分後60分以内の時間範囲で緊急連絡する。
Ambientに送る
Ambientで人感カウント数、振動(一定時間幅の最大振動値)、駐車場からの距離、速度(一定時間幅の最大速度)、地図で位置確認が確認できる。バスの運行状況を詳しく知りたい管理者が見ることを想定。
表示件数を1に設定すると、現時点の場所を地図で見ることもできる。
Line Notify と Ambientの設定
Line Notify と Ambientの設定は以下の記事を見て設定する。
LINE Notifyの始め方
Ambientの設定方法
保育園・幼稚園の関係者の方(現場の方)への希望
保育園・幼稚園の関係者の方(現場の方)が、このソースコードを見て(現場バスに)実装してほしい。
保育園・幼稚園DXです。
=======================
ソースコードを表示(折りたたみ)
========================
#include <M5StickCPlus.h>
#include <TinyGPS++.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "Ambient.h"
HardwareSerial GPSRaw(2);
TinyGPSPlus gps;
static const uint32_t GPSBaud = 9600;
double latitude, longitude; // 緯度と経度
double altitude, speed; // 高度と速度
double speedMax = 0; // 最大速度
double site0_lat = 35.33040193621184; //任意zeroのlatitude うさぎ幼稚園(架空)
double site0_lng = 139.51837556716126; //任意zeroのlongitude うさぎ幼稚園(架空)
double site1_lat = 35.32663444161487; //任意site1のlatitude 梶原口交番前
double site1_lng = 139.52392397610822; //任意site1のlongitude 梶原口交番前
double site2_lat = 35.32337394807017; //任意site2のlatitude 常盤口
double site2_lng = 139.52525469451245; //任意site2のlongitude 常盤口
double site3_lat = 35.322061037493214; //任意site3のlatitude 八雲神社前
double site3_lng = 139.52671997962727; //任意site3のlongitude 八雲神社前
double site4_lat = 35.322299605085206; //任意site4のlatitude 仲ノ坂
double site4_lng = 139.53067230805888; //任意site4のlongitude 仲ノ坂
double site5_lat = 35.32113475930285; //任意site5のlatitude 長谷大谷戸
double site5_lng = 139.5386609060957; //任意site5のlongitude 長谷大谷戸
double site6_lat = 35.32054597407315; //任意site6のlatitude 佐助一丁目
double site6_lng = 139.54265514635728; //任意site6のlongitude 佐助一丁目
double site7_lat = 35.319573181894484; //任意site7のlatitude 鎌倉市役所 ユーターン地点
double site7_lng = 139.5472149016764; //任意site7のlongitude 鎌倉市役所 ユーターン地点
double error_lat = 0.001; //latitudeの誤差default0.0005
double error_lng = 0.0014; //longitudeの誤差default0.0007
int site0flag = 0; //site0での送信flag うさぎ幼稚園(架空)出発地点
int site1flag = 0; //site1での送信flag 梶原口交番前 往路
int site2flag = 0; //site2での送信flag 常盤口 往路
int site3flag = 0; //site3での送信flag 八雲神社前 往路
int site4flag = 0; //site4での送信flag 仲ノ坂 往路
int site5flag = 0; //site5での送信flag 長谷大谷戸 往路
int site6flag = 0; //site6での送信flag 佐助一丁目 往路
int site7flag = 0; //site7での送信flag 鎌倉市役所 ユーターン地点
int site8flag = 0; //site8での送信flag 佐助一丁目 復路
int site9flag = 0; //site9での送信flag 長谷大谷戸 復路
int site10flag = 0; //site10での送信flag 仲ノ坂 復路
int site11flag = 0; //site11での送信flag 八雲神社前 復路
int site12flag = 0; //site12での送信flag 常盤口 復路
int site13flag = 0; //site13での送信flag 梶原口交番前 復路
int site14flag = 0; //site14での送信flag うさぎ幼稚園(架空)到着地点
int timecount = 0; //幼稚園に戻ってきてから1分毎にカウント
int alarmflag1 = 0; //緊急連絡送信回数(人感)
int alarmflag2 = 0; //緊急連絡送信回数(振動)
double TARGET_LAT = 35.33040193621184, TARGET_LNG = 139.51837556716126; //うさぎ幼稚園(架空)からの距離用
#define LED_PIN 10 //M5StickCPlusの赤色LEDはGPIO10
#define LED_ON LOW
#define LED_OFF HIGH
const int pir_sensing = 36; //GPIO36 M5StickCPlusのHatのGPIO36
int last_value = 0;
int cur_value = 0;
int countHat = 0;
int set_parameter = 0; //初回すぐグラフ描画用
unsigned long previoustime = 0;
WiFiClient ambientclient; //ambient用のclientオブジェクトをambientclientとする
Ambient ambient;
const char* ssid = "xxxxxxxx"; //WiFi SSIDを入力
const char* password = "xxxxxxxxxxxx"; //WiFi パスワードを入力
unsigned int channelId = xxxxx; // AmbientのチャネルIDを入力
const char* writeKey = "xxxxxxxxxxxxxxxx"; // Ambientのライトキーを入力
const char* token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; //LINE notify tokenを入力
const char* host = "notify-api.line.me";
int sequence[240] = {};
int SequencePosition = 0;
float ymax =10;
float accX = 0, accY = 0, accZ = 0;
float gyroX = 0, gyroY = 0, gyroZ = 0;
float sq_gyro = 0, sq_gyro_Max =0;
void setup() {
M5.begin();
M5.Imu.Init(); // Init IMU.
M5.Lcd.setRotation(3);
GPSRaw.begin(GPSBaud, SERIAL_8N1, 33, 32);
M5.Lcd.fillScreen(BLACK);
pinMode(pir_sensing,INPUT_PULLUP);
pinMode(LED_PIN,OUTPUT);
digitalWrite(LED_PIN, LED_ON);
delay(100);
digitalWrite(LED_PIN, LED_OFF);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(100);
}
Serial.print("WiFi connected\r\nIP address: ");
Serial.println(WiFi.localIP());
ambient.begin(channelId, writeKey, &ambientclient); // チャネルIDとライトキーを指定してAmbientの初期化
}
void loop() {
M5.Lcd.setCursor(0, 0, 2);
M5.Imu.getGyroData(&gyroX, &gyroY, &gyroZ);
M5.Imu.getAccelData(&accX, &accY, &accZ);
auto now = millis();
//PIRセンサーの0,1の値を読む
cur_value = digitalRead(pir_sensing);// read the value of BUTTON
//PIRセンサーが感知したらcountHat値を1増加しLED赤点灯
if (cur_value != last_value) {
if(cur_value == 1){
countHat++;
digitalWrite(LED_PIN, LED_ON);
delay(100);
digitalWrite(LED_PIN, LED_OFF);
}else{
digitalWrite(LED_PIN, LED_OFF);
}
last_value = cur_value;
}
while (GPSRaw.available() > 0) {
if (gps.encode(GPSRaw.read())) {
break;
}
delay(0);
}
double latitude = gps.location.lat(); // 緯度
double longitude = gps.location.lng(); // 経度
double altitude = gps.altitude.meters(); // 高度
double speed = gps.speed.kmph(); // 速度
double speedMax = speed > speedMax ? speed : speedMax; //時間幅内のspeedMax
sq_gyro = sq(gyroX) + sq(gyroY) + sq(gyroZ); //これを振動値と定義する
sq_gyro_Max = sq_gyro > sq_gyro_Max ? sq_gyro : sq_gyro_Max; //時間幅内の振動最大値sq_gyro_Max
M5.Lcd.setCursor(20,0); M5.Lcd.setTextColor(YELLOW, BLACK); M5.Lcd.setTextSize(1); M5.Lcd.printf("%.6f", latitude);
M5.Lcd.setCursor(95,0); M5.Lcd.setTextColor(YELLOW, BLACK); M5.Lcd.setTextSize(1); M5.Lcd.printf("%.6f", longitude);
M5.Lcd.fillRect(184, 0, 55, 14, BLACK);M5.Lcd.setCursor(184,0); M5.Lcd.setTextColor(GREEN, BLACK); M5.Lcd.setTextSize(1); M5.Lcd.printf("%.0f", sq_gyro);
M5.Lcd.setCursor(55,45); M5.Lcd.setTextColor(YELLOW, BLACK); M5.Lcd.setTextSize(1); M5.Lcd.setTextFont(7); M5.Lcd.print(countHat);M5.Lcd.setTextFont(1);
//グラフ表示書き換え時間 & ambient送信時間
if (now - previoustime >= 60000 || set_parameter == 0)
{
//初回すぐグラフ描画用
set_parameter = set_parameter + 1;
//グラフ表示の開始前に全部消す
M5.Lcd.fillRect(0, 13, 240, 123, BLACK);
//y軸表示
M5.Lcd.fillRect(2, 13, 15, 6, BLACK);M5.Lcd.setCursor(2,13,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax));
M5.Lcd.fillRect(2, 72, 15, 6, BLACK);M5.Lcd.setCursor(2,72,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax/2));
M5.Lcd.fillRect(2, 125, 15, 6, BLACK);M5.Lcd.setCursor(2,125,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println("0");
//中間水平線赤
for(int i=0; i <= 46; i++){
M5.Lcd.drawPixel(5+5*i, 14, RED);
M5.Lcd.drawPixel(5+5*i, 44, RED);
M5.Lcd.drawPixel(5+5*i, 74, RED);
M5.Lcd.drawPixel(5+5*i, 104, RED);
M5.Lcd.drawPixel(5+5*i, 134, RED);
}
//グラフ横軸の表示
M5.Lcd.setCursor(40,122,1);M5.Lcd.print("240min"); //横軸の全時間幅を表示
//グラフプロット
SequencePosition = (SequencePosition + 1) % (sizeof(sequence) / sizeof(int));
sequence[SequencePosition] = countHat;
int len = sizeof(sequence) / sizeof(int);
for (int i = 0; i < len; i++) {
auto value = sequence[(SequencePosition + 1 + i) % len];
M5.Lcd.fillCircle(i, (int)(134-value/ymax*120), 4, WHITE);
}
//グラフ内プロット値の最大値を求める
float max_value = 0;
for (int i = 0; i < len ; i++) {
if (sequence[(SequencePosition + 1 + i) % len ] > max_value){
max_value = sequence[(SequencePosition + 1 + i) % len ];
}
}
//グラフの縦軸をグラフ内プロット値の最大値に応じて変更する
if (max_value < 10){
ymax = 10;
M5.Lcd.fillRect(0, 13, 15, 15, BLACK);M5.Lcd.setCursor(2,13,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax));
M5.Lcd.fillRect(0, 72, 15, 15, BLACK);M5.Lcd.setCursor(2,72,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax/2));
M5.Lcd.fillRect(2, 125, 15, 15, BLACK);M5.Lcd.setCursor(2,125,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println("0");
delay(10);
}
else if (max_value < 30){
ymax = 30;
M5.Lcd.fillRect(0, 13, 15, 15, BLACK);M5.Lcd.setCursor(2,13,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax));
M5.Lcd.fillRect(0, 72, 15, 15, BLACK);M5.Lcd.setCursor(2,72,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax/2));
M5.Lcd.fillRect(2, 125, 15, 15, BLACK);M5.Lcd.setCursor(2,125,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println("0");
delay(10);
}
else if (max_value < 100){
ymax = 100;
M5.Lcd.fillRect(0, 13, 15, 15, BLACK);M5.Lcd.setCursor(2,13,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax));
M5.Lcd.fillRect(0, 72, 15, 15, BLACK);M5.Lcd.setCursor(2,72,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println(int(ymax/2));
M5.Lcd.fillRect(2, 125, 15, 15, BLACK);M5.Lcd.setCursor(2,125,1);M5.Lcd.setTextColor(WHITE);M5.Lcd.println("0");
delay(10);
}
//ユーターン地点(site7)を通過した後に幼稚園に戻ってきてから動作
if (latitude > site0_lat - error_lat && latitude < site0_lat + error_lat && longitude > site0_lng - error_lng && longitude < site0_lng + error_lng && site7flag > 0){
timecount++;
//幼稚園に戻ってきて5分後60分以内に人感センサーが作動した場合にLineNotifyに3回緊急連絡
if(timecount > 5 && timecount <= 60 && alarmflag1 < 3 && countHat > 0){
send("緊急連絡です。駐車中の送迎バス内で人の動きが感知されました。確認してください。");
alarmflag1++;
digitalWrite(LED_PIN,LED_ON);
}
//幼稚園に戻ってきて5分後60分以内に振動値が100000を超えた場合にLineNotifyに3回緊急連絡(振動値閾値は状況により変更)
if(timecount > 5 && timecount <= 60 && alarmflag2 < 3 && sq_gyro_Max > 100000){
send("緊急連絡です。駐車中の送迎バス内で大きな振動が感知されました。確認してください。");
alarmflag2++;
digitalWrite(LED_PIN,LED_ON);
}
}
//Ambientに送る
if(gps.location.isValid()) {
char buf[16];
Serial.println("send to ambient");
Serial.printf("lat: %f, lng: %f\r\n", gps.location.lat(), gps.location.lng());
ambient.set(1, countHat);
ambient.set(2, sq_gyro_Max);
dtostrf(TinyGPSPlus::distanceBetween(gps.location.lat(), gps.location.lng(), TARGET_LAT, TARGET_LNG), 4, 2, buf);
ambient.set(3, buf);
dtostrf(gps.altitude.meters(), 4, 2, buf);
ambient.set(4, buf);
dtostrf(speedMax, 4, 2, buf);
ambient.set(5, buf);
dtostrf(gps.location.lat(), 12, 8, buf);
ambient.set(9, buf);
dtostrf(gps.location.lng(), 12, 8, buf);
ambient.set(10, buf);
ambient.send();
}
speedMax = 0;
countHat = 0;
sq_gyro_Max = 0;
previoustime = now;
}
//通過情報をLineNotifyで送る
if (latitude > site1_lat - error_lat && latitude < site1_lat + error_lat && longitude > site1_lng - error_lng && longitude < site1_lng + error_lng && site1flag < 1) {
site1flag++;
send("梶原口交番前を通過しました。");
delay(5000);
} else if (latitude > site2_lat - error_lat && latitude < site2_lat + error_lat && longitude > site2_lng - error_lng && longitude < site2_lng + error_lng && site2flag < 1){
site2flag++;
send("常盤口を通過しました。");
delay(5000);
} else if (latitude > site3_lat - error_lat && latitude < site3_lat + error_lat && longitude > site3_lng - error_lng && longitude < site3_lng + error_lng && site3flag < 1) {
site3flag++;
send("八雲神社前を通過しました。");
delay(5000);
}else if (latitude > site4_lat - error_lat && latitude < site4_lat + error_lat && longitude > site4_lng - error_lng && longitude < site4_lng + error_lng && site4flag < 1) {
site4flag++;
send("仲ノ坂を通過しました。");
delay(5000);
}else if (latitude > site5_lat - error_lat && latitude < site5_lat + error_lat && longitude > site5_lng - error_lng && longitude < site5_lng + error_lng && site5flag < 1) {
site5flag++;
send("長谷大谷戸を通過しました。");
delay(5000);
}else if (latitude > site6_lat - error_lat && latitude < site6_lat + error_lat && longitude > site6_lng - error_lng && longitude < site6_lng + error_lng && site6flag < 1) {
site6flag++;
send("佐助一丁目を通過しました。");
delay(5000);
}else if (latitude > site7_lat - error_lat && latitude < site7_lat + error_lat && longitude > site7_lng - error_lng && longitude < site7_lng + error_lng && site7flag < 1) {
site7flag++;
send("鎌倉市役所を通過しました。");
delay(5000);
}else if (latitude > site6_lat - error_lat && latitude < site6_lat + error_lat && longitude > site6_lng - error_lng && longitude < site6_lng + error_lng && site8flag < 1 && site7flag > 0) {
site8flag++;
send("佐助一丁目を通過しました。");
delay(5000);
}else if (latitude > site5_lat - error_lat && latitude < site5_lat + error_lat && longitude > site5_lng - error_lng && longitude < site5_lng + error_lng && site9flag < 1 && site7flag > 0) {
site9flag++;
send("長谷大谷戸を通過しました。");
delay(5000);
}else if (latitude > site4_lat - error_lat && latitude < site4_lat + error_lat && longitude > site4_lng - error_lng && longitude < site4_lng + error_lng && site10flag < 1 && site7flag > 0) {
site10flag++;
send("仲ノ坂を通過しました。");
delay(5000);
} else if (latitude > site3_lat - error_lat && latitude < site3_lat + error_lat && longitude > site3_lng - error_lng && longitude < site3_lng + error_lng && site11flag < 1 && site7flag > 0) {
site11flag++;
send("八雲神社前を通過しました。");
delay(5000);
} else if (latitude > site2_lat - error_lat && latitude < site2_lat + error_lat && longitude > site2_lng - error_lng && longitude < site2_lng + error_lng && site12flag < 1 && site7flag > 0){
site12flag++;
send("常盤口を通過しました。");
delay(5000);
} else if (latitude > site1_lat - error_lat && latitude < site1_lat + error_lat && longitude > site1_lng - error_lng && longitude < site1_lng + error_lng && site13flag < 1 && site7flag > 0) {
site13flag++;
send("梶原口交番前を通過しました。");
delay(5000);
}else if (latitude > site0_lat - error_lat && latitude < site0_lat + error_lat && longitude > site0_lng - error_lng && longitude < site0_lng + error_lng && site14flag < 1 && site7flag > 0) {
site14flag++;
send("送迎バスが、うさぎ幼稚園に到着しました。");
delay(5000);
}
//出発するときにAボタンを押す(ルール)
if(M5.BtnA.isPressed()){
send("送迎バスが、うさぎ幼稚園を出発しました。");
site0flag = 0;
site1flag = 0;
site2flag = 0;
site3flag = 0;
site4flag = 0;
site5flag = 0;
site6flag = 0;
site7flag = 0;
site8flag = 0;
site9flag = 0;
site10flag = 0;
site11flag = 0;
site12flag = 0;
site13flag = 0;
site14flag = 0;
timecount = 0;
alarmflag1 = 0;
alarmflag2 = 0;
}
Serial.printf("%d %.2f %.2f %.2f %.2f %.2f", countHat, gyroX, gyroY, gyroZ, sq_gyro, sq_gyro_Max);
Serial.println(" ");
delay(100);
M5.update();
}
/* LINE Notifyに送る */
void send(String message) {
WiFiClientSecure client;
client.setInsecure(); // サーバー証明書の検証を行わずに接続する場合に必要
Serial.println("Try");
//LineのAPIサーバに接続
if (!client.connect(host, 443)) {
Serial.println("Connection failed");
return;
}
Serial.println("Connected");
//リクエストを送信
String query = String("message=") + message;
String request = String("") +
"POST /api/notify HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Authorization: Bearer " + token + "\r\n" +
"Content-Length: " + String(query.length()) + "\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n\r\n" +
query + "\r\n";
client.print(request);
//受信終了まで待つ
while (client.connected()) {
String line = client.readStringUntil('\n');
Serial.println(line);
if (line == "\r") {
break;
}
}
String line = client.readStringUntil('\n');
Serial.println(line);
}