師走ですね。世のお父さんお母さん、頑張れ!
[M5Stack Advent Calendar 2019] 15日目です。
TL;DR(まとめ)
- M5StickC, Beebotte, Slack, Google Home Mini を用いて市民ランナー応援システムを開発しました。
- IoTによって市民ランナーの頑張りとその家族の応援を結びつけることに成功しました。
- M5StickCは加速度センサー、内蔵バッテリー、Wi-Fi、モニタ、スピーカーHAT(後付け)ありでPoCに最適なデバイスでした。
- 皆さんのランナー生活に少しでもお役に立てれば幸いです。(ただ1点重大な課題が・・・。)
きっかけ
世は空前の全国民マラソン時代です。
そんな中、以下のような市民マラソンあるあるが生まれました。
-
ランナーあるある
- 最初は勢いよくスタートする。
- 途中から何のために走っているかわからなくなる。
- リタイヤしたい、けど高い参加費用が無駄になるのはもったいない。の無限ループ。
- 足が止まったタイミングで誰かに応援してもらいたい。
-
ランナー家族あるある
- 応援したいけど、いつ走ってくるかわからない。
- 大会の開催が秋~冬のため、寒空の下待ち続けるのは苦行。
- 応援できたとしてもほんの一瞬でつまらない。
- なるべくなら家から応援したい。(個人の感想です。)
そこで、家からランナーを応援するシステムを開発しました。
システム構成
- M5StickCでランナーの5分ごとの歩数を集計する。
- 歩数が 100 歩未満の場合、 Slack に応援メッセージ要求を HPPTS/POST する。
- Slack をみた家族が Google Home に応援メッセージを話しかけて Beebotte に HTTPS/POST する。
- M5StickC で応援メッセージを MQTT/Subscribe する。(SPK HAT つけて、メッセージ受信を音で知らせる)
- 応援メッセージのおかげでまた走り出し、ゴールまでたどり着ける。
構築手順(ブラッシュアップのアドバイス大募集中です!)
M5StickC スケッチ(技術要素:Slack, Beebotte)
[Special Thanks] の記事を大いに参考にさせていただき、機能①,②,④の機能を実装します。
#include <M5StickC.h>
#include "efontEnableJaMini.h"
#include "efont.h"
#include "efontM5StickC.h"
#include <WiFiClientSecure.h>
// PubSubClientライブラリでのパケットサイズは128バイトなのを拡張
#define MQTT_MAX_PACKET_SIZE 1024
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "config.h"
#include "certificate.h"
#include <HTTPClient.h>
// Speaker用
const int servo_pin = 26;
int freq = 50;
int ledChannel = 0;
int resolution = 10;
extern const unsigned char m5stack_startup_music[];
const char* mqtt_server = "mqtt.beebotte.com";
WiFiClientSecure wifiClient;
PubSubClient client(mqtt_server, 8883, wifiClient);
HTTPClient http;
// Step修正
float accX = 0;
float accY = 0;
float accZ = 0;
const int numOfSample = 50;
float sample[numOfSample];
float threshold = 0;
int countSample = 0;
float range = 50.0;
uint8_t countStep = 0;
float getDynamicThreshold(float *s) {
float maxVal = s[0];
float minVal = s[0];
for (int i=1; i<sizeof(s); i++) {
maxVal = max(maxVal, s[i]);
minVal = min(minVal, s[i]);
}
return (maxVal + minVal) / 2.0;
}
float getFilterdAccelData() {
static float y[2] = {0};
M5.MPU6886.getAccelData(&accX,&accY,&accZ);
y[1] = 0.8 * y[0] + 0.2 * (abs(accX) + abs(accY) + abs(accZ)) * 1000.0;
y[0] = y[1];
return y[1];
}
// M5StickC起動からの経過時間取得
long bef_time = millis();
long aft_time;
// Wi-Fi接続
void setup_wifi() {
if (WiFi.status() != WL_CONNECTED) {
Serial.print("connecting to ");
Serial.print(ssid);
Serial.println("...");
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
// Wi-Fiアクスポイントへの接続に失敗したら5秒間待ってリトライ
Serial.println("failed to connect");
delay(5000);
return;
} else {
Serial.print("WiFi connected: ");
Serial.println(WiFi.localIP());
}
wifiClient.setCACert(beebotte_ca_cert);
randomSeed(micros());
}
}
void callback(char* topic, byte* payload, unsigned int length) {
// PubSubClient.hで定義されているMQTTの最大パケットサイズ
char buffer[MQTT_MAX_PACKET_SIZE];
snprintf(buffer, sizeof(buffer), "%s", payload);
Serial.println("received:");
Serial.print("cheerTopic: ");
Serial.println(cheerTopic);
Serial.println(buffer);
// 受け取ったJSON形式のペイロードをデコードする
StaticJsonBuffer<MQTT_MAX_PACKET_SIZE> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(buffer);
if (!root.success()) {
Serial.println("parseObject() failed");
return;
}
const char* parsedPayload = root["data"];
char* parsedPayload_cast = const_cast<char*>(parsedPayload);
if (parsedPayload != NULL) {
Serial.print("payload: ");
Serial.println(parsedPayload);
M5.Lcd.setTextSize(2);
// 応援メッセージ表示
printEfont(parsedPayload_cast);
// メッセージ受信を知らせるBeep音発行
for (int i = 0; i < 3; i++) {
ledcWriteTone(ledChannel, 1000);
delay(30);
ledcWriteTone(ledChannel, 0);
delay(15);
ledcWriteTone(ledChannel, 1000);
delay(30);
ledcWriteTone(ledChannel, 0);
delay(300);
}
}
}
// ④ 応援メッセージの subscribe(Beebotte)
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Attempt to connect
String username = "token:";
username += channelToken;
// Create a random client ID
String clientId = "ESP32Client-";
clientId += String(random(0xffff), HEX);
if (client.connect(clientId.c_str(), username.c_str(), NULL)) {
Serial.println("connected");
// Subscribe
client.subscribe(cheerTopic);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
// 初期設定
void setup() {
//Serial.begin(115200);
M5.begin();
M5.Lcd.setCursor(0, 0);
M5.Axp.ScreenBreath(8); // 画面の輝度(MIN:7~MAX:15)
M5.Lcd.setRotation(3); // 左を上にする
M5.Lcd.fillScreen(BLACK); // 背景を黒にする
setup_wifi();
client.setServer(mqtt_server, 8883);
client.setCallback(callback);
M5.MPU6886.Init();
sample[countSample] = getFilterdAccelData();
// Speaker HAT設定
ledcSetup(ledChannel, freq, resolution);
ledcAttachPin(servo_pin, ledChannel);
}
// ② Slackに応援メッセージ要求
void slack_post() {
const char *slack_server = "hooks.slack.com";
const char *slack_json = "{\"text\":\"お父さんのペースが落ちています!\n応援メッセージをお願いします!\",\"icon_emoji\":\":runner:\",\"username\":\"M5StickC_post\"}";
http.begin( slack_server, 443, slack_service, slack_sub_ca );
http.addHeader("Content-Type", "application/json" );
http.POST((uint8_t*)slack_json, strlen(slack_json));
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
delay(100);
countSample++;
// 歩数集計の記事を大いに(ry
sample[countSample] = getFilterdAccelData();
if (abs(sample[countSample] - sample[countSample-1]) < range) {
sample[countSample] = sample[countSample-1];
countSample--;
}
if (sample[countSample] < threshold && sample[countSample-1] > threshold) {
countStep++;
}
if (countSample == numOfSample) {
threshold = getDynamicThreshold(&sample[0]);
countSample = 0;
sample[countSample] = getFilterdAccelData();
}
M5.Lcd.setCursor(0, 0);
M5.Lcd.setTextSize(2);
M5.Lcd.println(String(countStep) + " steps");
// 現在までの経過時間取得
aft_time = millis();
// 前回の歩数確認時から5分経過していた場合
if (aft_time - bef_time > 5*60*1000 && aft_time > 5*60*1000) {
// ① 歩数確が100歩未満だった場合
if (countStep < 100) {
slack_post();
}
// 歩数をリセット
countStep = 0;
// 歩数集計開始時間を設定
bef_time = aft_time;
}
}
// Wi-Fiアクセスポイント
const char *ssid = "XXX";
const char *password = "XXX";
// Beebotteチャンネルトークン
const char* channelToken = "token_XXX";
// Beebotteトピック名("channel/resource"の形式)
const char* cheerTopic = "M5StickC/cheerMsg";
// Slack service設定
const char* slack_service = "/services/XXX";
const char* beebotte_ca_cert = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIGezCCBWOgAwIBAgIRAIxEseYXgT0SAsfQQ8/I2sIwDQYJKoZIhvcNAQELBQAw\n" \
(snip)
"c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a\n" \
"mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=\n" \
"-----END CERTIFICATE-----\n" \
;
const char * slack_sub_ca = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh\n" \
(snip)
"c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0\n" \
"j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz\n" \
"-----END CERTIFICATE-----\n" \
;
Google Home周り(技術要素:Google Assistant, IFTTT, beebotte)
機能③の応援メッセージの送信は、IFTTTを利用しました。
[If] に Google Assistant を、 [Then] に Webhooks(Beebotte) を設定します。
利用イメージ
ランナーの足が止まった場合、Slackに以下のようなメッセージがPOSTされます。
家族がGoogle Homeに応援メッセージを話すと、M5StickCに冒頭のメッセージが受信されます。
残課題
- M5StickC のバッテリーが持たない問題
- フル充電した場合でもバッテリーが2時間も持たないため、本番では世界新記録レベルでゴールする必要があります。
Special Thanks
以下の2の記事を 大いに 参考にしました。
M5StickCで歩数計を作る(ランナーの歩数計測は @ufoo68 さんの記事が非常に参考になりました。)
Arduino(M5StickC)でefont Unicodeフォント表示 完結編(M5StickCは日本語対応してないことを知り挫折しそうになりましたが、こちらを利用させていただきました。)