M5Stack とニフクラ mobile backend でお菓子カウンターを作る

この記事は、 [富士通クラウドテクノロジーズ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fjct) の13日目の記事です。

12日目は @tily さんの **[FJCT エンジニアタスクフォース 2020 レポート](https://tech.fjct.fujitsu.com/entry/etf-report-2020)** でした。

例年と違い原則オンラインでの活動と、その整備という勝手が違う一年でしたね。。。自分も大変お世話になりました。

## はじめに

ニフクラ mobile backend に携わっております @morisatoru と申します。

昨今の情勢によるところや職場の配慮もあり在宅での勤務が多くなっているのですが、人目を気にせずお菓子を食べることができるため、ちょっと健康面での心配な気持ちがでてまいりました。自分がどの程度お菓子を食べているのか自覚的になれば過剰摂取はしないだろうと考え、M5Stack から mobile backend を操作するライブラリを用いてお菓子カウンターを実装してみます。

## M5Stack

[M5Stack](https://m5stack.com/) とはプログラミング可能なマイコンモジュールで、標準でディスプレイとボタン、スピーカーなどが装備されており拡張することで各種センサー類を追加することが可能です。冷蔵庫に貼れるタイマーのような見た目のうえ、磁石が入っているため本当に冷蔵庫へ貼ることが可能です。

![m5stack.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869769/ff73d162-d829-4c12-eb7f-a87b094dfa8c.jpeg)


今回は M5Stack Basic のモデルを拡張モジュール無しで利用し、開発環境は Arduino IDE に ESP32 ライブラリを追加したものを利用します。

## ニフクラ mobile backend

[ニフクラ mobile backend](https://mbaas.nifcloud.com/) とは富士通クラウドテクノロジーズが提供するクラウドサービスで、データ共有やプッシュ通知などスマートフォンアプリを実装する際に利用できる機能を用意しています。

今回は最近提供された ncmb_m5stack という M5Stack から mobile backend を操作するライブラリを用いて実装します。

## 開発の準備

[MacでM5stackをはじめる](https://qiita.com/hmmrjn/items/2b2da09eecffcbdbad85) を参考に開発環境を用意し、ncmb_m5stack の [ReadMe にあるインストール方法](https://github.com/NIFCLOUD-mbaas/ncmb_m5stack/blob/master/README.md#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB) よりライブラリの追加をおこないました。

## アプリケーションの実装

Arduino IDE にてお菓子カウンターの実装をします。仕組みとしては以下の通りです。

- お菓子を食べる際に M5Stack 上の3つあるボタンのうち、左側のボタンを押す
- 左側のボタンが押されると mobile backend にボタンが押されたことをデータ登録する
- PC などよりお菓子が何回食べられたのかを見ることができる

### 起動時設定

まず M5Satck へのプログラム内での起動時設定を実装します。実装内容としては以下の通りです。

- ディスプレイの初期設定
- Wifi の初期設定
- NTP から時刻取得設定（標準の M5Stack では内蔵時計が存在しないため）
- mobile backend へのアクセスのための初期設定

上記処理を `setup()` 関数内でおこないます。

```cpp
#include <M5Stack.h>
#include <WiFi.h>
#include <time.h>
#include "ncmb.h"

const char wifiSsid[] = "WIFI SSID";
const char wifiPassphrase[] = "WIFI PASSPHRASE";
const char ncmbApplicationKey[] = "NCMB APPLICATION KEY";
const char ncmbClientKey[] = "NCMB CLIENT KEY";

NCMB ncmb; // mobile backed アクセス用

void setup() {
  // ディスプレイ
  M5.begin();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(0, 0);

  // WiFi
  WiFi.begin(wifiSsid, wifiPassphrase);
  while (WiFi.status() != WL_CONNECTED) {
    delay(50);
  }

  // 時計機能（nict より時刻取得）
  configTime(0, 0, "ntp.nict.jp");

  // mobile backend
  ncmb.init(ncmbApplicationKey, ncmbClientKey);
}
```

コード内に記述のある `WIFI SSID` 、 `WIFI PASSPHRASE` は利用可能な Wifi の ID、パスフレーズを設定ください。また `NCMB APPLICATION KEY` 、 `NCMB CLIENT KEY` については mobile backed でアプリを作成した際に渡されたアプリケーションキー、クライアントキーを設定ください。mobile backed でアプリ作成については [こちら](https://mbaas.nifcloud.com/doc/current/introduction/div_quickstart_javascript.html#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E6%96%B0%E8%A6%8F%E4%BD%9C%E6%88%90) をご覧ください。

### データ登録

左側のボタンが押されたら mobile backend へデータ登録する処理ですが `loop()` 関数内にて `M5.BtnA.wasPressed()` を取得することにより押されたことが判定できます。ただし注意点としては事前に `M5.update()` を処理しないと正しく判定できません。これより以下の実装としました。

```cpp
void loop() {
  M5.update();
  if (M5.BtnA.wasPressed()) {
    registerHistory("okashi");
  }
  delay(50);
}

void registerHistory(String item) {
  // 現在時刻の取得
  struct tm timeInfo;
  getLocalTime(&timeInfo);
  String dateString = getDateString(&timeInfo);
  String timeString = getTimeString(&timeInfo);
  String timestamp = getTimestamp(&timeInfo);

  // ディスプレイへ表示
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println(timeString + " " + item);

  // mobile backend への登録
  String content = "{\"item\":\"" + item + "\",\"date\":\"" + dateString + "\"}";
  NCMBResponse response = ncmb.registerObject("history", content, timestamp);
  M5.Lcd.println((response.isSuccess) ? "registration is succeeded" : "registration is failed");
}

```

ボタンが押された際の処理としては、まず現在時刻を取得したうえで後述する日時関連の文字列を取得し、ボタンが押されたことを確認できるようディスプレイに取得した時刻文字列と `okashi` という文字列を表示しています。

そして `setup()` にて設定した `ncmb` にてデータ登録をおこなっています。 ncmb_m5stack は最低限の機能しか有していないため、mobile backend へのデータ登録リクエストには Json 形式のデータが求められるのですがライブラリには Json を操作する機能はありません。別途 Json 操作用のライブラリを利用する方法もありますが今回は登録する内容が限られるため文字列操作で登録する Json を作成します。データ登録として作成する Json 内容は以下の通りです。

- `item` フィールドに関数の引数の文字列（今回の場合は "okashi" ）
- `date` フィールドに現在日付の "YYYY-MM-DD" 形式

データ登録処理 `registerObject()` は戻り値を返し、このなかの `.isSuccess` の boolean 値が `true` の場合は処理が成功したこととなります。

日時関連の文字列を取得する `getDateString()` 、 `getTimeString()` 、 `getTimestamp()` 関数については以下の実装としています。mobile backend へのデータ登録処理に使うタイムスタンプと共用利用としているため世界標準時であり、日本時間と比べると9時間ずれがあるので改善の余地がありそうですが、深夜は前日分のカウントとしたかったのでそのままとしています。

```cpp
// 引数より "YYYY-MM-DD" 形式の日付文字列を返す
String getDateString(struct tm *timeInfo) {
  String date = "20YY-MM-DD";
  date.setCharAt(2, ((0 < (timeInfo->tm_year - 100) / 10) ? (timeInfo->tm_year - 100) / 10 : 0) + 48);
  date.setCharAt(3, (timeInfo->tm_year - 100) % 10 + 48);
  date.setCharAt(5, ((0 < (timeInfo->tm_mon + 1) / 10) ? (timeInfo->tm_mon + 1) / 10 : 0) + 48);
  date.setCharAt(6, (timeInfo->tm_mon + 1) % 10 + 48);
  date.setCharAt(8, ((0 < timeInfo->tm_mday / 10) ? timeInfo->tm_mday / 10 : 0) + 48);
  date.setCharAt(9, timeInfo->tm_mday % 10 + 48);
  return date;  
}

// 引数より "HH:mm:ss.000Z" 形式の時刻文字列を返す
String getTimeString(struct tm *timeInfo) {
  String time = "HH:mm:ss.000Z";
  time.setCharAt(0, ((0 < timeInfo->tm_hour / 10) ? timeInfo->tm_hour / 10 : 0) + 48);
  time.setCharAt(1, timeInfo->tm_hour % 10 + 48);
  time.setCharAt(3, ((0 < timeInfo->tm_min / 10) ? timeInfo->tm_min / 10 : 0) + 48);
  time.setCharAt(4, timeInfo->tm_min % 10 + 48);
  time.setCharAt(6, ((0 < timeInfo->tm_sec / 10) ? timeInfo->tm_sec / 10 : 0) + 48);
  time.setCharAt(7, timeInfo->tm_sec % 10 + 48);
  return time;  
}

// 引数よりISO形式 "YYYY-MM-DDTHH:mm:ss.000Z" の日時文字列を返す
String getTimestamp(struct tm *timeInfo) {
  String timestamp = getDateString(timeInfo);
  timestamp.concat("T");
  timestamp.concat(getTimeString(timeInfo));
  return timestamp;
}
```

上記を Arduino IDE 上で実装し、M5Stack に書き込み実行することでボタンを押した際にデータ登録をおこなうことができます。

![sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869769/32eadc05-49b8-b3c3-16fb-b1db18b0d236.jpeg)


そして登録されたデータは mobile backend サービス管理画面より確認することが可能です。

![mobile_backend_console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869769/ab381e05-3482-b14b-6481-f3d477ca995d.png)


## データ利用

確認されたデータは時系列のものであり、当日何回お菓子を食べたのかは集計してみないとわかりません。データ利用ということで集計結果を表示する画面を作成します。

[こちらのドキュメント](https://mbaas.nifcloud.com/doc/current/introduction/div_quickstart_javascript.html) の 「Node.js以外の場合」を参考に html ページを作成します。

```html
<html lang="ja">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>お菓子カウンター</title>
    </head>
    <body>
        <h1>お菓子カウンター</h1>
        <p id="todayOkashi" />
        <!-- https://github.com/NIFCLOUD-mbaas/ncmb_js より ncmb.min.js を取得し、html ページ配下の js ディレクトリに格納してください -->
        <script src="js/ncmb.min.js" charset="utf-8"></script> 
        <script>
            // 日付の取得
            const today = (new Date(Date.now())).toISOString().substring(0, 10); // "yyyy-MM-dd"

            // mobile backend からの登録件数取得
            // 抽出条件は本日日付であり、かつ item フィールドが "okashi" であるもの
            const ncmb = new NCMB("YOUR_APPLICATION_KEY","YOUR_CLIENT_KEY");
            const history = ncmb.DataStore("history");
            history.equalTo("date", today)
                    .equalTo("item", "okashi")
                    .count()
                    .fetchAll()
                    .then(function(results){
                        // 結果が取得できた場合 
                        document.getElementById("todayOkashi").textContent = "本日(" + today + ")はお菓子を" + results.count + "回食べました。";
            }).catch(function(err){
                        // 結果の取得に失敗した場合 
                        document.getElementById("todayOkashi").textContent = "本日のお菓子回数の取得に失敗しました。エラー:" + err;
            });
        </script>
    </body>
</html>
```

このページを実行すると、本日何回お菓子を食べたのかを集計し表示されます。ページをリロードすると集計結果が最新化されます。

![webpage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869769/f1abba9d-9de9-b3dd-80d5-b58b616b38d7.png)

## おわりに

今回は左端のボタンのみ使用していますが、 M5Stack にはボタンが3つあるので M5Stack 中央、右端のボタンでのデータ登録処理 を追加して

- 中央のボタンは喫煙回数
- 右端のボタンはエナジードリンクなどカフェイン摂取回数

などを計測することもできそうです。また、ボタンを自主的(?)に押すことでお菓子を食べた回数を計測していますが、こちらをセンサーからの入力結果によってデータ登録することも応用としてできます。

このようなものを実装しましたが、自分としては実装したことに満足せずに趣旨を忘れず、お菓子を食べ過ぎないようにしたいです。

明日は @kzmake さんの **hatobaでdaprをうごかしてみた** です。2日目に dapr にて開発されていましたが、hatoba で動かす際にはどうなるのでしょうか。おたのしみに。

## 参考リンク

- [M5Stack サイト](https://m5stack.com/)
- [スイッチサイエンス M5Stack Basic カタログ](https://www.switch-science.com/catalog/3647/)
- [MacでM5stackをはじめる](https://qiita.com/hmmrjn/items/2b2da09eecffcbdbad85)
- [ニフクラ mobile backend](https://mbaas.nifcloud.com/)
- [NIFCLOUD-mbaas / ncmb_m5stack](https://github.com/NIFCLOUD-mbaas/ncmb_m5stack)