Help us understand the problem. What is going on with this article?

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

師走ですね。世のお父さんお母さん、頑張れ!
[M5Stack Advent Calendar 2019] 15日目です。

TL;DR(まとめ)

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

きっかけ

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

  • ランナーあるある

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

    • 応援したいけど、いつ走ってくるかわからない。
    • 大会の開催が秋~冬のため、寒空の下待ち続けるのは苦行。
    • 応援できたとしてもほんの一瞬でつまらない。
    • なるべくなら家から応援したい。(個人の感想です。)

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

システム構成

  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] の記事を大いに参考にさせていただき、機能①,②,④の機能を実装します。

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;
  }
}
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";
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) を設定します。

利用イメージ

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

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

残課題

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

Special Thanks

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

M5StickCで歩数計を作る(ランナーの歩数計測は @ufoo68 さんの記事が非常に参考になりました。)
Arduino(M5StickC)でefont Unicodeフォント表示 完結編(M5StickCは日本語対応してないことを知り挫折しそうになりましたが、こちらを利用させていただきました。)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした