タイムカードぽちぽち by M5StickC

🎄 Happy Holidays!
[kintone Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kintone) 20日目の記事です🎉

# はじめに

昨年は M5StickC 開発に[チャレンジ](https://qiita.com/Mikei/items/424a6030cc3c7a67c074)したのですが、その時買った **[Dual Button Unit](https://m5stack.com/products/mini-dual-button-unit)** を使ってみたい！ということで、今年は**「ぽちぽちボタンでタイムカード登録する」**プログラムを開発しました٩( ᐛ )و

会社でも kintone でタイムカード管理しているのですが、よく忘れる & 時間を入力するのが手間...
はい、物理ボタンの出番ですね！
（押し心地が良いんで、ちょっとしたストレス解消グッズにもなります笑）

# 作ったもの

デモです🎥
<blockquote class="twitter-tweet" data-lang="ja" data-theme="dark"><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://t.co/sOixMDlmat">pic.twitter.com/sOixMDlmat</a></p>&mdash; Tomoko Miyake (@tomo63ko) <a href="https://twitter.com/tomo63ko/status/1340735170597376000?ref_src=twsrc%5Etfw">2020年12月20日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

![demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/146bed08-ef71-c640-9274-3e0400c29d5a.png)

# 仕組み

ざっくりこんな感じです。
![structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/781b65ef-af72-c817-20f7-d842b49c98c1.png)

# セットアップ
### kintone アプリ作成

まず、このような**タイムカードアプリ**を作成します。
![kintone_app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/fd8927e5-9d0f-49de-2952-a80b922edecd.png)

必須項目はこちらです。

| フィールド名 | フィールドタイプ | フィールドコード |
|:---:|:---:|:---:|
| 日付 | 日付 | Date |
| 出勤時刻 | 時刻 | In |
| 退勤時刻 | 時刻 | Out |

次に、Arduino プログラムから HTTP リクエストするための `API トークン（認証情報）`を生成します。
以下のアクセス権をつけてください。

- レコード閲覧
- レコード追加
- レコード編集

![api_token.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90265/2476cd41-cfce-9925-7e1e-e5a7a15756f0.png)

### M5StickC 開発環境準備

前回書いたので、[こちらの記事](https://qiita.com/Mikei/items/424a6030cc3c7a67c074#-4-m5stickc-%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97)を参考にしてください。

最近 Mac OSX を Big Sur にアップデートしてしまったが故に、ESP32 のセットアップに苦戦しました^^;
トラブルシュートで参考にした記事も`参考リンク欄`に載せてます。

# プログラム開発

下準備ができたら、Arduino IDE で以下の`スケッチ`を作成して保存します。
書き終わったら`コンパイル`して、`マイコンボードに書き込み`をして完成です！👏
（開発中は`シリアルモニタ`を使うとデバッグしやすいのでおすすめ）

※ プログラム内こちらの変数の値は書き換えてください。

- ドメイン名
- アプリ ID
- API トークン
- WiFi SSID
- WiFi PASSWORD

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

#define LED_PIN 10
#define LED_ON LOW
#define LED_OFF HIGH

// kintone setting
const char* domain = "[ドメイン名]";
const int appId = [アプリ ID];
const char* token = "[API トークン]";
char url[128];
char json[256];

// 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;
struct tm timeinfo;
char s[20];

// Button setting
int lastValueRed = 0;
int curValueRed = 0;
int lastValueBlue = 0;
int curValueBlue = 0;

// LED function
void blinkLed() {
  digitalWrite(LED_PIN, LED_ON);
  delay(500);
  digitalWrite(LED_PIN, LED_OFF);
  delay(500);
  digitalWrite(LED_PIN, LED_ON);
  delay(500);
  digitalWrite(LED_PIN, LED_OFF);
  delay(500);
}

void setup() {
  M5.begin();
  Serial.begin(115200);
  Serial.println("\n*********************\nStart processing...\n*********************");
  
  M5.Axp.ScreenBreath(9);
  M5.Lcd.setRotation(3);
  pinMode(32, INPUT);
  pinMode(33, INPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LED_OFF);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setCursor(15, 2);
  M5.Lcd.println("Timecard PochiPochi");
  M5.Lcd.setTextColor(WHITE);

  // Check WiFi connection
  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("Successfully connected to the WiFi\n");
  delay(2000);
}

void loop() {
  curValueRed = digitalRead(32);
  curValueBlue = digitalRead(33);
  
  M5.Lcd.setCursor(0,15); M5.Lcd.print("Blue Status: ");
  M5.Lcd.setCursor(0,30); M5.Lcd.print("IN Time: ");
  M5.Lcd.setCursor(0,45); M5.Lcd.print("Red Status: ");
  M5.Lcd.setCursor(0,60); M5.Lcd.print("OUT Time: ");

  // Click blue button (In time)
  if (curValueBlue != lastValueBlue) {
    M5.Lcd.fillRect(95,15,100,15,BLACK);
    M5.Lcd.fillRect(95,30,100,15,BLACK);
    if (curValueBlue==0) {
      M5.Lcd.setCursor(95,15); M5.Lcd.print("pressed");
      Serial.println("Button Status: blue pressed");
      Serial.println(" value: 0");

      // Get the current time
      configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
      if (!getLocalTime(&timeinfo)) {
        Serial.println("Error getting current time\n");
      } else {
        sprintf(s, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
        Serial.printf("\nCurrent Time = %s \n", s);
      }

      // GET Request to check today's record
      int respCode = 0;
      HTTPClient http;
      sprintf(url, "https://%s.cybozu.com/k/v1/records.json?app=%d&query=Date=TODAY()&totalCount=true", domain, appId);
      http.begin(url);
      http.addHeader("X-Cybozu-API-Token", token);
      respCode = http.GET();
      Serial.printf("\n------ GET records request ------\n");
      Serial.printf("HTTP Response Code = %d \n", respCode);
      String payload = http.getString();
      Serial.println(payload);
      DynamicJsonDocument doc(50000);
      deserializeJson(doc, payload);

      // Check totalCount
      const char* totalCount = doc["totalCount"];
      Serial.printf("totalCount = %s \n", totalCount);
      int numTotalCount = atoi(totalCount);

      if (numTotalCount == 0) {
        Serial.println("No today's record yet\n");

        // POST request to register IN time
        Serial.printf("\n------ POST record request ------\n");
        sprintf(json, "{\"app\":\"%d\",\"record\":{\"In\":{\"value\":\"%s\"}}}", appId, s);
        Serial.printf("Request body = %s \n", json);
  
        int postRespCode = 0;
        sprintf(url, "https://%s.cybozu.com/k/v1/record.json", domain);
        http.begin(url);
        http.addHeader("X-Cybozu-API-Token", token);
        http.addHeader("Content-Type", "application/json");
        postRespCode = http.POST(json);
        Serial.printf("HTTP Response Code = %d \n", postRespCode);
        String payload = http.getString();
        Serial.println(payload);
        Serial.println("----------");
        M5.Lcd.setCursor(95,30); M5.Lcd.println(s);
        blinkLed();
        M5.Lcd.setTextColor(GREEN);
        M5.Lcd.setCursor(15,70); M5.Lcd.println("Have a nice day!");
        M5.Lcd.setTextColor(WHITE);
        delay(5000);
        M5.Lcd.fillRect(15,70,100,70,BLACK);
      } else if (numTotalCount == 1) {
        // Check the record number
        JsonObject obj = doc.as<JsonObject>();
        String getRecords = obj[String("records")];
        Serial.println(getRecords);
        DynamicJsonDocument doc(50000);
        deserializeJson(doc, getRecords);
        JsonObject datas = doc[0];
        const char* recordNum = datas["RecordNumber"]["value"];
        Serial.printf("Today's record number = %s \n", recordNum);

        // PUT Request to update IN time
        Serial.printf("\n------ PUT record request ------\n");
        sprintf(json, "{\"app\":\"%d\",\"id\":\"%s\",\"record\":{\"In\":{\"value\":\"%s\"}}}", appId, recordNum, s);
        Serial.printf("Request body = %s \n", json);
  
        int putRespCode = 0;
        sprintf(url, "https://%s.cybozu.com/k/v1/record.json", domain);
        http.begin(url);
        http.addHeader("X-Cybozu-API-Token", token);
        http.addHeader("Content-Type", "application/json");
        putRespCode = http.PUT(json);
        Serial.printf("HTTP Response Code = %d \n", putRespCode);
        String payload = http.getString();
        Serial.println(payload);
        Serial.println("----------");
        M5.Lcd.setCursor(95,30); M5.Lcd.println(s);
        blinkLed();
        M5.Lcd.setTextColor(GREEN);
        M5.Lcd.setCursor(15,70); M5.Lcd.println("Have a nice day!");
        M5.Lcd.setTextColor(WHITE);
        delay(5000);
        M5.Lcd.fillRect(15,70,100,70,BLACK);
      } else {
        Serial.println("Error! Today's record is duplicated\n----------");
      }
    }
    else{
      M5.Lcd.setCursor(95,15); M5.Lcd.print("released");
      M5.Lcd.setCursor(95,30); M5.Lcd.println(s);
      Serial.println("Button Status: blue released");
      Serial.println(" value: 1");
    }
    lastValueBlue = curValueBlue;
  }

  // Click red button (Out time)
  if (curValueRed != lastValueRed) {
    M5.Lcd.fillRect(95,45,100,15,BLACK);
    M5.Lcd.fillRect(95,60,100,15,BLACK);
    if (curValueRed==0) {
      M5.Lcd.setCursor(95,45); M5.Lcd.print("pressed");
      Serial.println("Button Status: red pressed");
      Serial.println(" value: 0");

      // Get the current time
      configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
      if (!getLocalTime(&timeinfo)) {
        Serial.println("Error getting current time\n");
      } else {
        sprintf(s, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
        Serial.printf("\nCurrent Time = %s \n", s);
      }

      // GET Request to check today's record
      int respCode = 0;
      HTTPClient http;
      sprintf(url, "https://%s.cybozu.com/k/v1/records.json?app=%d&query=Date=TODAY()&totalCount=true", domain, appId);
      http.begin(url);
      http.addHeader("X-Cybozu-API-Token", token);
      respCode = http.GET();
      Serial.printf("\n------ GET records request ------\n");
      Serial.printf("HTTP GET Response Code = %d \n", respCode);
      String payload = http.getString();
      Serial.println(payload);
      DynamicJsonDocument doc(50000);
      deserializeJson(doc, payload);

      // Check totalCount
      const char* totalCount = doc["totalCount"];
      Serial.printf("totalCount = %s \n", totalCount);
      int numTotalCount = atoi(totalCount);

      if (numTotalCount == 0) {
        Serial.println("No today's record yet\n");

        // POST request to register OUT time
        sprintf(json, "{\"app\":\"%d\",\"record\":{\"Out\":{\"value\":\"%s\"}}}", appId, s);
        Serial.printf("Request body = %s \n", json);
  
        int postRespCode = 0;
        sprintf(url, "https://%s.cybozu.com/k/v1/record.json", domain);
        http.begin(url);
        http.addHeader("X-Cybozu-API-Token", token);
        http.addHeader("Content-Type", "application/json");
        postRespCode = http.POST(json);
        Serial.printf("HTTP Response Code = %d \n", postRespCode);
        String payload = http.getString();
        Serial.println(payload);
        Serial.println("----------");
        M5.Lcd.setCursor(95,60); M5.Lcd.println(s);
        blinkLed();
        M5.Lcd.setTextColor(GREEN);
        M5.Lcd.setCursor(15,70); M5.Lcd.println("Good job!");
        M5.Lcd.setTextColor(WHITE);
        delay(5000);
        M5.Lcd.fillRect(15,70,100,70,BLACK);
      } else if (numTotalCount == 1) {
        // Check the record number
        JsonObject obj = doc.as<JsonObject>();
        String getRecords = obj[String("records")];
        Serial.println(getRecords);
        DynamicJsonDocument doc(50000);
        deserializeJson(doc, getRecords);
        JsonObject datas = doc[0];
        const char* recordNum = datas["RecordNumber"]["value"];
        Serial.printf("Today's record number = %s \n", recordNum);

        // PUT Request to update OUT time
        Serial.printf("\n------ PUT record request ------\n");
        sprintf(json, "{\"app\":\"%d\",\"id\":\"%s\",\"record\":{\"Out\":{\"value\":\"%s\"}}}", appId, recordNum, s);
        Serial.printf("Request body = %s \n", json);
  
        int putRespCode = 0;
        sprintf(url, "https://%s.cybozu.com/k/v1/record.json", domain);
        http.begin(url);
        http.addHeader("X-Cybozu-API-Token", token);
        http.addHeader("Content-Type", "application/json");
        putRespCode = http.PUT(json);
        Serial.printf("HTTP Response Code = %d \n", putRespCode);
        String payload = http.getString();
        Serial.println(payload);
        Serial.println("----------");
        M5.Lcd.setCursor(95,60); M5.Lcd.println(s);
        blinkLed();
        M5.Lcd.setTextColor(GREEN);
        M5.Lcd.setCursor(15,70); M5.Lcd.println("Good job!");
        M5.Lcd.setTextColor(WHITE);
        delay(5000);
        M5.Lcd.fillRect(15,70,100,70,BLACK);
      } else {
        Serial.println("Error! Today's record is duplicated\n----------");
      }
    } else {
      M5.Lcd.setCursor(95,45); M5.Lcd.print("released");
      M5.Lcd.setCursor(95,60); M5.Lcd.println(s);
      Serial.println("Button Status: red released");
      Serial.println(" value: 1");
    }
    lastValueRed = curValueRed;
  }
  M5.update();
}
```

※ HTTP リクエストの部分は同じような処理が複数あるのでほんとは独自関数作って呼び出すのが良いですが、途中の型変換がうまくいかず冗長な書き方になってしまいました。良きようにアレンジしてください。(๑˃̵ᴗ˂̵)

# 参考リンク

- [macOS Big SurでArduino IDEからESP32の書き込みができない件](https://qiita.com/ashe1098/items/585d6474bd5d4833d39d)
- [macOS CatalinaのArduino IDEでM5StickCに書き込みができない問題の解決方法](https://tech.fusic.co.jp/posts/2020-02-12-macos-catalina-m5stickc/)
- [M5Stick-CでJsonをPOSTする](https://qiita.com/poruruba/items/4bf6a52520e431a8f4a5)
- [ArduinoJson Assistant](https://arduinojson.org/v6/assistant/)
- [M5StickCであそぶ 〜ボタンとLEDを使う〜](https://make-muda.net/2019/09/6906/)
- [Dual_Button example](https://github.com/m5stack/M5StickC/blob/master/examples/Unit/Dual_Button/Dual_Button.ino)

最後まで読んでくださりありがとうございます。
みなさん良いお年を🎍