[M5StickC] 愛（のあるメッセージ）は市民ランナーを救う。 

師走ですね。世のお父さんお母さん、頑張れ！
[[M5Stack Advent Calendar 2019]](https://qiita.com/advent-calendar/2019/m5stack) 15日目です。

<img width="270" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140921/9397d6cf-59d4-2b30-d299-cb513b5324b4.png">

# TL;DR(まとめ)
- M5StickC, Beebotte, Slack, Google Home Mini を用いて市民ランナー応援システムを開発しました。
- IoTによって市民ランナーの頑張りとその家族の応援を結びつけることに成功しました。
- M5StickCは加速度センサー、内蔵バッテリー、Wi-Fi、モニタ、スピーカーHAT（後付け）ありでPoCに最適なデバイスでした。
- 皆さんのランナー生活に少しでもお役に立てれば幸いです。（ただ1点重大な課題が・・・。）

# きっかけ
世は空前の全国民マラソン時代です。
そんな中、以下のような市民マラソンあるあるが生まれました。

- **ランナーあるある**
    - 最初は勢いよくスタートする。
    - 途中から何のために走っているかわからなくなる。
    - リタイヤしたい、けど高い参加費用が無駄になるのはもったいない。の無限ループ。
    - **足が止まったタイミングで誰かに応援してもらいたい。**

- **ランナー家族あるある**
    - 応援したいけど、いつ走ってくるかわからない。
    - 大会の開催が秋～冬のため、寒空の下待ち続けるのは苦行。
    - 応援できたとしてもほんの一瞬でつまらない。
    - **なるべくなら家から応援したい。**（個人の感想です。）

<img width="1128" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140921/98f4a82e-4f63-5435-77b7-f5fa4e8f2688.png">

そこで、家からランナーを応援するシステムを開発しました。

# システム構成
<img width="1128" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140921/71352b04-84d5-316a-21c7-4cd1e4d8a8d0.png">

1. M5StickCでランナーの5分ごとの歩数を集計する。
2. 歩数が 100 歩未満の場合、 Slack に応援メッセージ要求を HPPTS/POST する。
3. Slack をみた家族が Google Home に応援メッセージを話しかけて Beebotte に HTTPS/POST する。
4. M5StickC で応援メッセージを MQTT/Subscribe する。（SPK HAT つけて、メッセージ受信を音で知らせる）
5. 応援メッセージのおかげでまた走り出し、ゴールまでたどり着ける。

# 構築手順（ブラッシュアップのアドバイス大募集中です！）
### M5StickC スケッチ（技術要素：Slack, Beebotte）
[Special Thanks] の記事を大いに参考にさせていただき、機能①,②,④の機能を実装します。

```c:jogging_cheering.ino
#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;
  }
}
```
```c:config.h
// 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";
```

```c:certificate.h
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） を設定します。

<img width="454" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140921/6a659387-a579-579f-0d18-4da793791860.png">

# 利用イメージ

ランナーの足が止まった場合、Slackに以下のようなメッセージがPOSTされます。

<img width="270" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140921/1b39facc-f54a-dd59-9010-a69f42209ad3.png">

家族がGoogle Homeに応援メッセージを話すと、M5StickCに冒頭のメッセージが受信されます。

<img width="270" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140921/9397d6cf-59d4-2b30-d299-cb513b5324b4.png">


# 残課題
- M5StickC のバッテリーが持たない問題
    - フル充電した場合でもバッテリーが2時間も持たないため、本番では**世界新記録レベルでゴールする必要があります。**

# Special Thanks
以下の2の記事を **大いに** 参考にしました。

[M5StickCで歩数計を作る](https://qiita.com/ufoo68/items/d0d12c666a19405c5625)（ランナーの歩数計測は @ufoo68 さんの記事が非常に参考になりました。）
[Arduino(M5StickC)でefont Unicodeフォント表示 完結編](https://lang-ship.com/blog/?p=646)（M5StickCは日本語対応してないことを知り挫折しそうになりましたが、こちらを利用させていただきました。）