M5Stackにごみの日を表示する


## はじめに
- いつも今日は何のごみが出せるのか迷います
- 今現在何のごみが出せるか表示するアプリを作れば良いじゃん
- 使ってない M5Stack Core2 が自宅にころがってるのでこれを使おう

<img width="300" alt="m5stack.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/ee7c5fce-4063-b11d-813c-9fafb69218c2.png">

ということで、コレに表示するようにしましょう。

## 方針
- Googleカレンダーにゴミの日情報を登録しておきます
- Googleカレンダーからゴミの日を取得するGoogle Apps Scriptを用意します
- M5Stackから定期的にGoogle Apps Scriptを叩いてごみの日を画面に表示します

## 開発環境構築
VSCode ＋ PlatformIO を使っている人が多そうなのでこれにします。
手順は https://kumabelog.com/m5stack-development-environment-for-vscode/ こちらを参考にさせていただきました。
1. VSCodeのインストール
1. VSCodeの拡張機能のC/C++のインストール
1. VSCodeの拡張機能のPlatformIO IDE（VSCodeの再起動が必要）
1. PlatformIOのプロジェクト作成（私の場合、BoardはM5StackCore2を選択）
1. ライブラリを追加（私の場合はM5Core2とArduinoJsonを追加）

## Googleカレンダーに登録
Googleカレンダーにごみの日を登録しておきます。
IDで識別できるようにGoogleカレンダーに登録するイベント名には先頭にIDをつけておきます。
| ID | ゴミ種別 | イベント名 | 登録日|
| --- | --- | --- | --- |
| T00 | ごみが出せない日 |  |  |
| T01 | 可燃ごみ | T01_可燃ごみ | 毎週火曜日と金曜日 |
| T02 | 不燃ごみ | T02_不燃ごみ | 第一水曜日と第三水曜日 |
| T03 | 資源ごみ | T03_資源ごみ | 毎週月曜日 |
| T04 | ペットボトル | T04_ペットボトル | 第二水曜日と第四水曜日 |



<img width="640" alt="2023-11-29_21h52_41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/a535a506-cf27-e6b1-373f-d657a2948b51.png">

## アイコンを準備
いらすとやの素材を利用させていただきました。
適当に縮小してmicroSDに保存してM5Stackに挿します。
保存場所と名前は、/images/[ID].jpgとしました。

| ファイル名 | 画像|
| --- | --- |
| T00.jpg | <img width="64" alt="T00.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/5a842880-78c5-ce55-0c3c-af7f1d6b0430.jpeg"> | 
| T01.jpg | <img width="64" alt="T01.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/357ee68b-3995-8597-64fb-c04638b94a23.jpeg"> | 
| T02.jpg | <img width="64" alt="T02.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/cf36e28d-217a-4cf9-409a-c00435d47693.jpeg"> | 
| T03.jpg | <img width="64" alt="T03.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/fa819139-d624-85b5-a1ee-4358ac28c5f1.jpeg"> | 
| T04.jpg | <img width="64" alt="T04.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/54074acd-2aea-1879-59e1-d041fd5f67bf.jpeg"> | 


## Google Apps Scriptを準備
https://script.google.com/home ここから新しいプロジェクトを作ります。
<img width="640" alt="gas0.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/43b459a4-b7ec-834a-3502-2c5e46c8eefb.png">



Googleカレンダーに登録してあるごみの日を取得するGoogle Apps Scriptを準備します。
一応一週間分のイベントを取得して、先頭にIDがついているイベントだけを抽出してjsonで返すようにします。
```js:getEvent.gs
/// 一週間のごみの日の予定を取得します。
function doGet() {
  // カレンダーID
  const calendarId = 'カレンダー登録しているGmailアドレス';
  // カレンダー
  const calendar = CalendarApp.getCalendarById(calendarId);
  // ごみの日の正規表現
  const regex = /^T\d{2}_.*$/;
  // イベント取得開始日
  const startTime = new Date();
  // イベント取得終了日
  const endTime = new Date();
  endTime.setDate(startTime.getDate() + 7);
  // イベント（ごみの日）取得
  const events = calendar.getEvents(startTime, endTime);
  
  if (events.length === 0) {
    console.log("event not found.");
  }
  else
  {
    const response = [];
    for (event in events) {
      let title = events[event].getTitle();
      let time = Utilities.formatDate(events[event].getStartTime(), "JST", "yyyy/MM/dd HH:mm:ss");;
      if (title.match(regex)) {
        response.push({time: time, title: title});
        console.log(time + ' ' + title);
      }
    }
    return ContentService.createTextOutput(JSON.stringify(response));
  }
}
```

動作確認できたらデプロイします。（今回は雑に全員アクセスにしてます）
<img width="640" alt="gas1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/33980115-c3e1-4948-1710-951c28f556d8.png">

デプロイが終わるとウェブアプリのURLが発行されるのでコピーしておきます。
<img width="640" alt="gas3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/8c0dcece-b165-eda5-871a-ab947edb9882.png">


## M5Stack側のプログラムを準備
M5Stackに書き込むプログラムを準備します。
Wifi接続してウェブアプリからごみの日を取得して今日と明日の分を画面表示します。
10分置きに再取得するようにしています。

```c++:main.cpp
#include <M5Core2.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
/// @brief Api URL
const String api_url = "GoogleAppsScriptで用意したウェブアプリのURL";
/// @brief Wi-Fi SSID
const char* wifi_ssid = "WifiのSSID";
/// @brief Wi-Fi PASSWORD
const char* wifi_passwd = "Wifiのパスワード";
/// @brief Day of week
const char* week[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
// NTP設定
#define TIMEZONE_JST (3600 * 9)           // 日本標準時はUTC+9時間
#define DAYLIGHTOFFSET_JST (0)            // サマータイム時差(無し)
#define NTP_SERVER1 "ntp.nict.jp"         // NTPサーバー
#define NTP_SERVER2 "ntp.jst.mfeed.ad.jp" // NTPサーバー
/// @brief ごみの日情報
const size_t capacity = JSON_OBJECT_SIZE(3) + 2*JSON_OBJECT_SIZE(10) + 170;
StaticJsonDocument<capacity> doc;
JsonObject trash;
/// @brief ごみの日情報を取得します。
/// @param url ApiのURL
/// @return ごみの日のjson
JsonObject getTrash(String url) {
  HTTPClient http;
  const char* headers[] = {"Location"};
  http.collectHeaders(headers, 1);
  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.GET();
  if (httpCode == HTTP_CODE_OK) {
    Serial.println("HTTP_CODE_OK");
    String payload = "";
    payload = http.getString();
    deserializeJson(doc, payload);
    http.end();
    Serial.println(payload);
    return doc.as<JsonObject>();
  }else if(httpCode == 302){
    Serial.println("HTTP_CODE_302");
    http.end();
    return getTrash(http.header(headers[0]));
  } else {
    http.end();
    return doc.as<JsonObject>();
  }
}
/// @brief 特定の日のごみの日を表示します。
/// @param day 日付
void displayOneDay(char* day, char* wday, int index) {
  // 日付曜日描画
  std::string str_day = day;
  std::string str_wday = wday;
  str_day = str_day.substr(5, 5) + wday;
  M5.Lcd.setTextFont(4);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(index * 160 + 12, 32);
  M5.Lcd.println(str_day.c_str());
  // 画像描画
  char image[50];
  bool enabled = false;
  for (JsonObject elem : doc.as<JsonArray>()) {
    std::string time = elem["time"];
    std::string string_title = elem["title"];
    std::string title_code = string_title.substr(0, 3);
    bool found = time.find(day) != std::string::npos;
    if (found) {
      sprintf(image, "/images/%s.jpg", title_code.c_str());
      M5.Lcd.drawJpgFile(SD, image, index * 160 + 16, 70);
      enabled = true;
    }
  }
  if (!enabled) {
    sprintf(image, "/images/%s.jpg", "T00");
    M5.Lcd.drawJpgFile(SD, image, index * 160 + 16, 70);
  }
  M5.Lcd.println("");
}
/// @brief ごみの日情報を表示します。
/// @param json ごみの日情報
void displayTrash(JsonObject json) {
  // 画面初期化
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(2);
  // 現在時間表示
  struct tm t;
  if (getLocalTime(&t)) {
    // 当日
    char today[50];
    char today_wday[50];
    char tomorrow[50];
    char tomorrow_wday[50];
    sprintf(today, "%04d/%02d/%02d", (1900 + t.tm_year), (t.tm_mon + 1), t.tm_mday);
    sprintf(today_wday, "(%s)",  week[t.tm_wday]);
    // 翌日
    t.tm_mday += 1;
    if (t.tm_mday > 31) {
        t.tm_mday = 1;
        t.tm_mon += 1;
    }
    if (t.tm_mon > 11) {
      t.tm_mon = 0;
      t.tm_year += 1;
    }
    t.tm_wday = (t.tm_wday + 1) % 7;
    sprintf(tomorrow, "%04d/%02d/%02d", (1900 + t.tm_year), (t.tm_mon + 1), t.tm_mday);
    sprintf(tomorrow_wday, "(%s)",  week[t.tm_wday]);
    displayOneDay(today, today_wday, 0);
    displayOneDay(tomorrow, tomorrow_wday, 1);
    M5.Lcd.setTextSize(2);
    M5.Lcd.setTextFont(1);
    M5.Lcd.setCursor(0, 220);
    M5.Lcd.printf("%04d/%02d/%02d %02d:%02d:%02d UPDATE", (1900 + t.tm_year), (t.tm_mon + 1), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
  }
}
/// @brief 起動時に一回実行されます。
void setup() {
  M5.begin();
  Serial.begin(9600);
  // WiFi接続
  M5.Lcd.printf("Connecting to %s", wifi_ssid);
  WiFi.begin(wifi_ssid, wifi_passwd);
  while (WiFi.status() != WL_CONNECTED) {
    M5.Lcd.print(".");
    delay(100);
  }
  M5.Lcd.println(".");
  M5.Lcd.println("Success to connect");
  M5.Lcd.println(WiFi.localIP());
  // 時刻取得
  configTime(TIMEZONE_JST, DAYLIGHTOFFSET_JST, NTP_SERVER1, NTP_SERVER2);
}
/// @brief setup完了後に繰り返し実行されます。
void loop() {
  // ごみの日取得
  trash = getTrash(api_url);
  // ごみの日表示
  displayTrash(trash);
  // 600秒待ち
  delay(1000*600);
}
```
準備できたらビルドして、M5StackをUSB接続して書き込みます。

ビルドはVSCode下部のこのボタンでできます。
<img width="640" alt="vscode0.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/fabc843e-4ae0-cfe5-74eb-61504d70e2b1.png">

M5Stackへの書き込みはこのボタンでできます。
<img width="640" alt="vscode1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/4467dc6b-80ff-2092-12a2-f6b4d6077465.png">


## 実行結果
今日と明日のごみの日がわかるようになりました。

<img width="300" alt="m5stack2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988340/49f56f61-6927-bfb0-97f1-61a31bf7b45b.png">