kintone, M5StickC, Slack でスケジュール忘れ防止アラーム付きスマートウォッチを作る⌚️

**Happy Holidays!!**
遅ればせながら [kintone Advent Calendar 2019](https://qiita.com/advent-calendar/2019/kintone) 21日目の記事です🎄

最近ゲットした**[「みんなの M5Stack 入門本」](https://www.amazon.co.jp/%E3%81%BF%E3%82%93%E3%81%AA%E3%81%AEM5Stack%E5%85%A5%E9%96%80-%E4%B8%8B%E5%B3%B6-%E5%81%A5%E5%BD%A6/dp/4865942092)**をきっかけに、**[M5StickC](https://m5stack.com/collections/m5-core/products/stick-c)** を使ったカスタマイズにチャレンジしました！

時々やっちゃう MTG 忘れ...
そんなうっかりを未然に防ぎたい！
ということで、**スケジュール忘れ防止アラーム付きの、なんちゃってスマートウォッチ**を **kintone & M5StickC & Slack 連携**で開発したので、その方法を紹介していきます。
題して、**「kintone スマートウォッチ対応を夢見るカスタマイズ:watch:」**です(｀・∀・´)

これ見て第一声 「スマホで良いんじゃん」 と言う人がいて∑(ﾟДﾟ)ってなりましたが、お手洗いに行くとき含めいつも着けてる時計だからこそ意味があるんです...！

# 🕰 概要
まずシステム構成・完成イメージ・詳細シナリオなどの紹介です。

### ざっくりシステム構成
![Structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/5c737087-5569-94f1-bc2c-a4c5b759f81a.png)

### 完成イメージ
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/kintone?src=hash&amp;ref_src=twsrc%5Etfw">#kintone</a> <a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> <a href="https://twitter.com/hashtag/slack?src=hash&amp;ref_src=twsrc%5Etfw">#slack</a> でスケジュール忘れ防止のスマートウォッチカスタマイズ⌚️ <a href="https://t.co/YdmhkN26GQ">pic.twitter.com/YdmhkN26GQ</a></p>&mdash; Tomoko Miyake (@tomo63ko) <a href="https://twitter.com/tomo63ko/status/1209518423182458882?ref_src=twsrc%5Etfw">December 24, 2019</a></blockquote>

**M5StickC のディスプレイがみづらいので拡大ver.です↓**
<img width="1027" alt="M5StickC.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/78c11dc5-2d64-1bd0-5ba4-832991e51a07.png">

### 詳細シナリオ
- 前提
    - kintone でスケジュール管理をしている
- カスタマイズ内容
    - Slack チャットボットとの対話で kintone にスケジュール登録
    - M5StickC で NTP 時刻サーバにアクセスして、現在時刻表示（1分おき）
    - M5StickC から kintone に HTTP リクエストして、直近10分以内に開始のスケジュールがあった場合にお知らせ（5分おき）
        - ディスプレイにスケジュールタイトルと開始時刻の表示
        - SPEAKER HAT から Beep音出力
        - 内蔵 LED 点灯

### 注意事項
- 今回 Arduino での日本語フォント対応はしていないので、スケジュールのタイトルは英語で入力してください。
- SPEAKER HAT のノイズ音が少しうるさいです。色々カスタマイズしたら何とかできるみたいですが、今回はスキップします。ノイズもご愛嬌(笑)
- HTTP リクエストをしているため、M5StickC ウォッチ着用時はプログラムに記述した WiFi の通信圏内にいる必要がありますのでご注意ください。（仕事場利用のイメージ）

# 🕰 カスタマイズ手順
1. kintone アプリ作成
2. Slack App 作成
3. Flow XO フロー作成
4. M5StickC 開発環境セットアップ
5. Arduino IDE でのプログラム開発

> Slack チャットボット連携は「スケジュールも毎回 kintone に登録しにいくの面倒だな」と思って作ったおまけです。
> Step2, 3 をスキップして M5StickC 連携のみ試すこともできます。

# 🕰 1. kintone アプリ作成
スケジュールデータの DB となる kintone アプリを作成します。

- 以下のフィールド構成で `Schedule アプリ`を作成
<img width="852" alt="kintoneApp.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/ea09705e-828e-e5b1-57a5-b0fccc8efc81.png">

| フィールド名       |       フィールドタイプ |    フィールドコード    |
|:-----------------:|:------------------:|:------------------:|
| User             |              文字列一行（Text） |        User        |
| Title           |            文字列一行（Text） |       Title       |
| Date             |              日付（Date） |        Date        |
| Start Time               |                時刻（Time） |         StartTime         |

- API トークンの生成（Arduino IDE に認証情報として記載）
<img width="1015" alt="APIToken.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/f0a2f3ef-cef0-0383-29aa-26ae21b602ff.png">

# 🕰 2. Slack App 作成
kintone にスケジュール登録をするための Slack チャットボットを作成します。

- 以下のボタンリンクより Schedule Bot を Slack Workspace に追加（事前に Slack Workspace 作成が必要）

<a href="https://slack.com/oauth/authorize?client_id=262137495715.880792646646&scope=bot"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x"></a>

- Workspace へのアクセスを許可
<img width="661" alt="スクリーンショット 2019-12-24 19.31.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/37b72106-6863-ec6b-64c0-e178d183dc79.png">

- この画面に移動して、Schedule Bot が話しかけてきたら準備 OK!
<img width="546" alt="スクリーンショット 2019-12-24 19.31.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/05b69dfe-6418-4131-09e2-4ae2263af187.png">

# 🕰 3. Flow XO フロー作成
チャットボットの会話ロジックを [Flow XO](https://flowxo.com/) というツールで作成します。（kintone への HTTP POST リクエストを含む）     

2つ以上のスケジュールを連続で追加できるように繰り返し処理を組んでいるところが今回のポイントです💡

**会話ロジックはこちら↓**
![Chatbot flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/fc5b61dd-731f-81ef-789a-75a5d44bc80c.png)


- [こちらのページ](https://flowxo.com/)より `Flow XO のアカウント` を作成
<img width="1036" alt="スクリーンショット 2019-12-24 19.44.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/e01262d4-fa50-86c4-2850-f11f459c3302.png">

- [こちらのページ](https://flowxo.com/share/r4dg2qnv) より `AddSchedule Bot Flow` をインストール
<img width="501" alt="InstallFlow.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/fb872d89-d34a-4bd0-5a2e-ab32e275b3ad.png">
<img width="1222" alt="FlowXOFlow.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/8545ca74-dd6f-cbcc-abcf-52397d0f5546.png">

- 少し下にスクロールして、`Make a HTTP Request` Action のみ、自分の環境情報（`ドメイン名`と `API トークン`）に変更して設定保存（`Next` > `Save` で保存）
<img width="520" alt="HTTPReuest.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/a6019c4f-4406-a186-fa27-fcb2b829a5ba.png">
<img width="704" alt="HTTPRequestSettings.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/83e45b40-6550-1a3b-d9e5-775ce33d3840.png">

- `Bots` 項目より Slack App との連携作成（[こちらの記事](https://developer.cybozu.io/hc/ja/articles/115005537606-Slack%E3%81%A8kintone%E3%81%A8Flow-XO%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%87%BA%E5%8B%A4%E9%80%A3%E7%B5%A1%E3%83%9C%E3%83%83%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8D%E3%81%86-#step6)の `Flow XO の設定` > `手順 11` を参照）
<img width="509" alt="Bots.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/71321667-bbca-e4fe-d67f-f6468b149352.png">

- 設定完了したら、以下のような対話が可能
<img width="583" alt="Slack.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/b021eb11-e382-4b60-db05-4ebcf473eba8.png">

> Flow XO での一からのフロー作成手順については、以前別の記事で紹介しているので、気になる方はご覧ください。
>  [SlackとkintoneとFlow XOを使って出勤連絡ボットを作ろう！
](https://developer.cybozu.io/hc/ja/articles/115005537606)

# 🕰 4. M5StickC 開発環境セットアップ
ここからが本題です。いよいよ M5StickC のセットアップと開発フェーズになります。

- 以下の記事を参考に、デバイスと開発環境のセットアップ
    - [M5StickC Quick Start - Arduino Mac](https://docs.m5stack.com/#/en/quick_start/m5stickc/m5stickc_quick_start_with_arduino_MacOS)
    - [Ambient](https://ambidata.io/) の [M5StickCで小型環境センサ端末を作る記事](https://ambidata.io/samples/m5stack/m5sitckc/)

- 以下は追加のボードマネージャとライブラリマネージャの設定（Built-in の設定利用も複数あり）

| ボードマネージャURL       | ボードマネージャ |  ライブラリマネージャ |
|:-----------------:|:------------------:|:------------------:|
| https://dl.espressif.com/dl/package_esp32_index.json             |              [esp32](https://github.com/espressif/arduino-esp32) |        [M5StickC](https://github.com/m5stack/M5StickC)        |
|           |       |       [ArduinoJson](https://arduinojson.org/?utm_source=meta&utm_medium=library.properties)       |

> [EspExceptionDecoder](https://github.com/me-no-dev/EspExceptionDecoder) もインストールしておくとエラー箇所特定に役立ちます。
> <img width="716" alt="Decoder.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/bd9e75d3-956e-ef40-4591-fea037b66b9b.png">

# 🕰 5. Arduino IDE でのプログラム開発
M5StickC は、M5Stack が提供している Web ベースのプログラミングツール [UIFlow](https://m5stack.github.io/UIFlow_doc/ja/) にて [MicroPython](https://micropython.org/) や Blockly（ビジュアルプログラミング） での開発も可能ですが、今回は [Arduino IDE](https://www.arduino.cc/en/main/software) を使って C/C++言語で開発を行います。

- 以下の `スケッチ` を作成して保存

| プログラム内の要編集箇所       |
|:-----------------:|
| WiFi SSID    　|
| WiFi PASSWORD  |
| ドメイン名        |
| アプリID         |
| API トークン     |

```c++:GetSchedule.ino
#include <M5StickC.h>
#include <WiFi.h>
#include <time.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <stdio.h>
#include <string.h>

// WiFi setting
const char* ssid       = "[WiFi SSID]";
const char* password   = "[WiFi PASSWORD]";

// Time setting
const char* ntpServer =  "ntp.jst.mfeed.ad.jp";
const long  gmtOffset_sec = 9 * 3600;
const int   daylightOffset_sec = 0;

// Sound setting
#define GPIO_PIN 26
uint8_t beepVolume = 50;

#define INTERVAL 58
RTC_DATA_ATTR static uint8_t seq = 1;

void setup() {

  M5.begin();
  Serial.begin(115200);
  Serial.println("\nStart processing...");
  
  M5.Axp.ScreenBreath(9);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);

  WiFi.begin(ssid, password);
  int i = 0;
  while (WiFi.status() != WL_CONNECTED) {
    if (i > 10) break;
    delay(100);
    Serial.println("Connecting to WiFi...");
    i++;
  }
  Serial.println("Connected to the WiFi network");
  delay(2000);
}

void loop() {

  // Get the current time
  Serial.println("\nStart loop processing...");
  M5.Lcd.setCursor(0, 0, 1);
  M5.Lcd.setTextColor(GREEN);
  M5.Lcd.setTextSize(4);
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  struct tm timeinfo;
  char s[20];
  if (!getLocalTime(&timeinfo)) {
    M5.Lcd.println("Error!");
  }else{
    sprintf(s, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
    Serial.println(s);
    M5.Lcd.println(s);
  }

  if((seq % 5) == 1){
    // Get the record datas from kintone
    int respCode = 0;
    HTTPClient http;
    http.begin("https://[ドメイン名].cybozu.com/k/v1/records.json?app=[アプリID]&query=Date=TODAY()&totalCount=true");
    http.addHeader("X-Cybozu-API-Token", "[API トークン]");
    respCode = http.GET();
    if (respCode > 0) {
      M5.Lcd.setTextColor(WHITE);
      M5.Lcd.setTextSize(1);
      Serial.printf("HTTP Response Code = %d \n", respCode);
      M5.Lcd.printf("HTTP Resp: %d \n", respCode);
      String payload = http.getString();
      Serial.println(payload);
      DynamicJsonDocument doc(50000);
      deserializeJson(doc, payload);
  
      // totalCount of Get Records
      const char* totalCount = doc["totalCount"]; 
      Serial.printf("totalCount = %s \n", totalCount);
      int numTotalCount = atoi(totalCount);
  
      if (int(strcmp(totalCount, "null")) == 0) {
        Serial.println("No schedules today");
      } else {
        // datas of Get Records
        JsonObject obj = doc.as<JsonObject>();
        String getRecords = obj[String("records")];
        Serial.println(getRecords);
  
        // Loop getRecords to find upcoming schedules
        int i;
        for (int i = 0; i < numTotalCount; i++) {
          DynamicJsonDocument doc(50000);
          deserializeJson(doc, getRecords);
          JsonObject datas = doc[i];
          const char* title = datas["Title"]["value"];
          const char* startTime = datas["StartTime"]["value"];
  
          // Display the schedule title if it begins in 10 mins
          #define N 3
          char hours[N];
          char mins[N];
      
          snprintf(hours, N, "%.*s", 2, startTime);
          snprintf(mins, N, "%.*s", 2, startTime + 3);
          int numHours = atoi(hours);
          int numMins = atoi(mins);
  
          // Change to time_t type data
          struct tm hm;
          time_t currentT, startT;
          double diffTime;
          memset(&hm, 0, sizeof(hm));
          hm.tm_hour = timeinfo.tm_hour;
          hm.tm_min = timeinfo.tm_min;
          currentT = mktime(&hm);
          hm.tm_hour = numHours;
          hm.tm_min = numMins;
          startT = mktime(&hm);
  
          diffTime = difftime(startT, currentT);
          Serial.println(diffTime);
  
          if (0 <= diffTime && diffTime <= 600) {
            M5.Lcd.setTextSize(2);
            Serial.printf("%s will start from %s\n", title, startTime);
            M5.Lcd.printf("%s: %s~\n", title, startTime);

            // LED ON
            pinMode(GPIO_NUM_10, OUTPUT);
            digitalWrite(GPIO_NUM_10, LOW);

            // Output beep sound
            for (int i = 0; i <= 1000; i++) {
              dacWrite(GPIO_PIN, 0);
              delayMicroseconds(500);
              dacWrite(GPIO_PIN, beepVolume);
              delayMicroseconds(500);
            }
          }
        }
      }
    } else {
      Serial.println("Error on HTTP Request");
    }
    http.end();
  }

  WiFi.disconnect();
  seq++;
  delay(3000);
  M5.Axp.ScreenBreath(8);

  // Deep sleep
  esp_deep_sleep(1000000LL * INTERVAL); 
}
```
- `コンパイル` して、`マイコンボードに書き込み` 
<img width="1014" alt="sketch.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/8af6c2a3-52d9-b5e5-cce6-f7ce89bfbcc0.png">

- エラーの場合は、`シリアルモニタ` への出力結果も見ながらデバッグ
<img width="900" alt="Serial.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/1a713811-1430-cf7f-b566-73b8da0c4603.png">

### プログラムのポイント解説
ポイントとなる箇所をピックアップして簡単に説明します。

- 必要なライブラリは、最初にインクルード

```c++:
#include <M5StickC.h>
#include <WiFi.h>
#include <time.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <stdio.h>
#include <string.h>
```

- 基礎関数をセット
    - void setup()：起動時に一回実行される処理を記載（初期設定）
        - WiFi の接続確認
    - void loop()：繰り返し実行したい処理を記載（メイン）
        - 時刻表示
        - スケジュールの表示（kintone への HTTP リクエスト）
        - LED 点灯 / SPEAKER HAT からの Beep音出力

- NTP 時刻サーバにアクセスして、現在時刻を取得

```c++
// Time setting
const char* ntpServer =  "ntp.jst.mfeed.ad.jp";
const long  gmtOffset_sec = 9 * 3600;
const int   daylightOffset_sec = 0;

/// 中略 ///

void loop() {

  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  struct tm timeinfo;
  char s[20];
  if (!getLocalTime(&timeinfo)) {
    M5.Lcd.println("Error!");
  }else{
    sprintf(s, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
    Serial.println(s);
    M5.Lcd.println(s);
  }
}
```

- ArduinoJson ライブラリを利用して、kintone への GET リクエストで取得した JSON オブジェクトを扱う
    - [ArduinoJson Assistant](https://arduinojson.org/v6/assistant/) がテストに便利！

```c++
String payload = http.getString();
Serial.println(payload);
DynamicJsonDocument doc(50000);
deserializeJson(doc, payload);
```

- time_t型に変換して、difftime関数で時間比較（直近10分以内に開始の予定があるかの確認のため）

```c++
const char* title = datas["Title"]["value"];
const char* startTime = datas["StartTime"]["value"];

// Display the schedule title if it begins in 10 mins
#define N 3
char hours[N];
char mins[N];

snprintf(hours, N, "%.*s", 2, startTime);
snprintf(mins, N, "%.*s", 2, startTime + 3);
int numHours = atoi(hours);
int numMins = atoi(mins);

// Change to time_t type data
struct tm hm;
time_t currentT, startT;
double diffTime;
memset(&hm, 0, sizeof(hm));
hm.tm_hour = timeinfo.tm_hour;
hm.tm_min = timeinfo.tm_min;
currentT = mktime(&hm);
hm.tm_hour = numHours;
hm.tm_min = numMins;
startT = mktime(&hm);

diffTime = difftime(startT, currentT);
Serial.println(diffTime);

if (0 <= diffTime && diffTime <= 600) {
  M5.Lcd.setTextSize(2);
  Serial.printf("%s will start from %s\n", title, startTime);
  M5.Lcd.printf("%s: %s~\n", title, startTime);
}
```

- GPIO ピンを指定して、LED 点灯と SPEAKER HAT からの Beep音の出力

```c++
// Sound setting
#define GPIO_PIN 26
uint8_t beepVolume = 50;

/// 中略 ///

// LED ON
pinMode(GPIO_NUM_10, OUTPUT);
digitalWrite(GPIO_NUM_10, LOW);

// Output beep sound
for (int i = 0; i <= 1000; i++) {
  dacWrite(GPIO_PIN, 0);
  delayMicroseconds(500);
  dacWrite(GPIO_PIN, beepVolume);
  delayMicroseconds(500);
}
```

> その他、Deep Sleep による省電力化 / カウントアップして kintone への HTTP リクエスト頻度の制限などを行なっています。

# 🕰 今後やってみたいこと
開発してると色々とアイデアも浮かぶもので、次回この辺もチャレンジしてみたいところです。

- スケジュールの内容を Beep音ではなく、テキストの読み上げにて出力する
- M5Stack（画面が大きな別デバイス）単独でのスケジュール登録 ~ お知らせまでの実装
- 併せて買った GROVE ポート用の Unit（[Button](https://docs.m5stack.com/#/en/unit/dual_button), [Proto](https://docs.m5stack.com/#/en/unit/proto)） を使ってみる

# 🕰 参考リンク
色んな記事を参考にさせていただきました。
**Special thanks to...:star2:**

### kintone
- [レコードの取得（GET）](https://developer.cybozu.io/hc/ja/articles/202331474#step2)
- [Google Home でイベントスケジュールを自動アナウンス](https://developer.cybozu.io/hc/ja/articles/360030528812)
- [SlackとkintoneとFlow XOを使って出勤連絡ボットを作ろう！
](https://developer.cybozu.io/hc/ja/articles/115005537606)

### M5Stack
- [M5StickC ESP32-PICO Mini IoT Development Kit](https://m5stack.com/collections/m5-core/products/stick-c)
- [M5StickC Quick Start - Arduino Mac](https://docs.m5stack.com/#/en/quick_start/m5stickc/m5stickc_quick_start_with_arduino_MacOS)
- [M5StickC非公式日本語リファレンス](https://lang-ship.com/reference/unofficial/M5StickC/)
- [M5StickCで小型環境センサ端末を作る](https://ambidata.io/samples/m5stack/m5sitckc/)
- [M5StickC で kintone に環境計測データをアップする時計を作る](https://qiita.com/yukataoka/items/058fb6d754257f74f3cd)
- [M5StickC + Speaker Hat で音を出す](https://qiita.com/onigiritech/items/36f35beb018cba6f473f)
- [M5StickCの内蔵LEDを使う](https://lang-ship.com/blog/?p=817)

### Arduino
- [Arduino IDE Development](https://docs.m5stack.com/#/en/related_documents/Arduino_IDE?id=install-usb-driver)
- [ArduinoJson API Reference](https://arduinojson.org/v6/api/)
- [ArduinoJson Assistant](https://arduinojson.org/v6/assistant/)
- [Arduino 日本語リファレンス](http://www.musashinodenpa.com/arduino/ref/)
- [Arduinoで遊ぶページ](https://garretlab.web.fc2.com/arduino/)
- [公開 NTP](http://jjy.nict.go.jp/tsp/PubNtp/index.html)
- [ESP32でネットワーク上から現在時刻を取得する（NTP）](https://wak-tech.com/archives/833)
- [ESP8266でHTTP GETするならWiFiClientじゃなくてHTTPClientのほうが良い](https://relativelayout.hatenablog.com/entry/2016/12/21/234344)
- [ESP8266のスタックトレースから原因の調査を試みる](https://qiita.com/matobaa/items/cc92fb105bdb5cdbe6f3)
- [ESP32のディープスリープを調べる](https://lang-ship.com/blog/?p=786)


:santa_tone1: :keyboard: :calendar_spiral: :watch: :santa_tone1: :keyboard: :calendar_spiral: :watch: :santa_tone1: :keyboard: :calendar_spiral: :watch:

最後まで読んでいただきありがとうございましたm(_ _)m
Arduino と C/C++言語は初チャレンジだったので、もしおかしなところがあったらぜひご指摘ください。

:santa_tone1: :keyboard: :calendar_spiral: :watch: :santa_tone1: :keyboard: :calendar_spiral: :watch: :santa_tone1: :keyboard: :calendar_spiral: :watch: