M5StickCとSORACOMで作る在席確認システム

# はじめに
こちらは[M5Stack Advent Calendar 2024](https://qiita.com/advent-calendar/2024/m5stack) 8日目の記事です。

https://qiita.com/advent-calendar/2024/m5stack

以下のM5Stack版の別バージョンとなります。

https://qiita.com/ozk009/items/588f105cc76abb7c7f33

# 経緯
[M5Stack版](https://qiita.com/ozk009/items/588f105cc76abb7c7f33)でも説明しているのですが、上司から役員の在席状況を分かるようにしてほしいという要望があり、作ったものです。

# 構成
## ハードウェア
ハードウェアは以下となります。
- [M5StickC](https://www.switch-science.com/products/5517)（販売終了・現行の機種は[M5StickC Plus](https://www.switch-science.com/products/6470)）
- [M5StickC PIR Hat](https://www.switch-science.com/products/5756)
- [M5Stack UnitV2](https://www.switch-science.com/products/7160)付属のスタンド
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/a1433197-ba32-5c83-1e78-62476e77d9d3.png)
- 100均のスマホ三脚

これらを組み合わせ、以下のようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/29e93765-94b2-77c8-edbe-c776919c930e.png)

# 仕様
基本的には[M5Stack版](https://qiita.com/ozk009/items/588f105cc76abb7c7f33)と一緒ですが、取り込み中への切り替えは本体を斜めに倒すようにしました。
（M5StickCには加速度センサーがあるので、傾斜が検知できます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/916611/e5b1c3cf-42cf-0b42-979f-0481e39bf30f.png)

また、PIRセンサーはしきい値とかもないので、よりシンプルです。

# 準備
[こちら](https://qiita.com/ozk009/items/588f105cc76abb7c7f33#%E6%BA%96%E5%82%99)を参考に開発環境とSORACOM Arcの準備をします。

# 開発
## M5StickC
[M5Stack版](https://qiita.com/ozk009/items/588f105cc76abb7c7f33)同様、こちらも生成AIにベースを作ってもらい、以下のようにしました。

```c++
#include <M5StickC.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WireGuard-ESP32.h>

// Wi-Fi設定
const char* ssid = "（無線APのSSID）";         // Wi-Fi SSID
const char* password = "（無線APのパスワード）"; // Wi-Fiパスワード
const String serverUrl = "http://uni.soracom.io";

// SORACOM ArcのWireGuard情報
const char* private_key = "（SORACOM Arcのprivate_key）";
IPAddress local_ip(XXX, XXX, XXX, XXX);              // SORACOM ArcのIPアドレス
const char* peer_public_key = "（SORACOM Arcのpublic_key）";
const char* endpoint_address = "xxx.arc.soracom.io"; // SORACOM Arcのエンドポイント
const int endpoint_port = 11010;                     // WireGuardのデフォルトポート

// GPIOピン
const int pirPin = 36;    // PIRセンサーのピン
const int tiltPin = 26;   // M5StickCの傾斜センサー（加速度センサー）

int currentStatus = -1;   // 現在の状態（初期状態は不在）
int previousStatus = -1;  // 前回送信した状態

int currentRotation = -1; // 現在の回転状態（-1で初期値）

WireGuard wg;

const int sampleSize = 30;   // サンプリング回数
int sampleData[sampleSize];  // サンプリングデータの格納配列
int sampleIndex = 0;         // サンプルインデックス

// 関数プロトタイプ宣言
void connectToWiFi();
void connectToWireGuard();
void sendStatusToServer(int status);
void displayStatus(int status);
void updateRotation(float accX, float accY);
int judgeState();

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);   // 画面をクリア
  M5.Lcd.setTextColor(WHITE); // テキストカラーを白に設定
  M5.Lcd.setTextSize(2);      // テキストサイズを設定
  Serial.begin(115200);

  // PIRセンサーのピンを入力モードに設定
  pinMode(pirPin, INPUT);

  displayStatus(0); // LCDに「Initializing」を表示

  // Wi-Fiに接続
  connectToWiFi();

  // WireGuardトンネルの設定
  connectToWireGuard();

  // 起動状態をサーバーに送信 (0)
  sendStatusToServer(0);
}

void loop() {
  M5.update();

  int pirState = digitalRead(pirPin);        // PIRセンサーの状態を読み取る
  sampleData[sampleIndex] = pirState;        // サンプルを格納
  sampleIndex = (sampleIndex + 1) % sampleSize;  // サンプルインデックスを更新
  int state = judgeState();                  // 在席判定

  M5.IMU.Init();                             // 加速度センサーの初期化

  float accX, accY, accZ;
  M5.IMU.getAccelData(&accX, &accY, &accZ);  // 加速度センサーのデータ取得

  // 加速度データに基づいて画面の回転を変更
  updateRotation(accX, accY);

  if (state == HIGH) {  // PIRセンサーが反応している場合
    if (accY > 0.8 || accY < -0.8) {
      currentStatus = 1;  // 直立している場合は在席
    } else {
      currentStatus = 2;  // 傾いている場合は取り込み中
    }
  } else {
    currentStatus = -1;   // PIRセンサーが反応しない場合は不在
  }

  // 状態が変化した場合にのみ送信および表示
  if (currentStatus != previousStatus) {
    sendStatusToServer(currentStatus);
    displayStatus(currentStatus);   // LCDに状態を表示
    previousStatus = currentStatus; // 状態を更新
  }

  delay(1000);  // 1秒ごとに状態をチェック*/
}

// Wi-Fiに接続する関数
void connectToWiFi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  delay(1000);
}

void connectToWireGuard() {
  Serial.print("Adjusting system time: ");
  configTime(9 * 60 * 60, 0, "ntp.jst.mfeed.ad.jp", "ntp.nict.jp", "time.google.com");
  delay(3000); // Wait for adjust
  Serial.println("done.");
  Serial.print("Connect to SORACOM Arc (WireGuard):");
  wg.begin(local_ip, private_key, endpoint_address, peer_public_key, endpoint_port);
  Serial.println("done.");
  delay(1000);
}

// サーバーに状態を送信する関数（POSTリクエストでJSON送信）
void sendStatusToServer(int status) {
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin(serverUrl);  // サーバーのURLを指定

    // POSTリクエストの設定
    http.addHeader("Content-Type", "application/json");
    
    // POSTリクエストの送信
    char payload[512];
    sprintf(payload, "{\"status\": %d}", status);
    int httpResponseCode = http.POST(payload);

    // レスポンスを取得
    if (httpResponseCode > 0) {
      String response = http.getString();  // サーバーからの応答を取得
      Serial.println("Response: " + response);
    } else {
      Serial.println("Error in sending POST request");
    }

    http.end();  // 接続終了
  } else {
    Serial.println("WiFi is not connected");
    displayStatus(-2);
    delay(1000);
    ESP.restart();  // デバイスを再起動
  }
}

// LCDに状態を表示する関数（英語表示）
void displayStatus(int status) {
  M5.Lcd.fillScreen(BLACK);  // 画面をクリア
  M5.Lcd.setCursor(0, 0);    // カーソル位置を設定
  
  switch (status) {
    case 0:
      M5.Lcd.fillScreen(WHITE);       // 画面背景色
      M5.Lcd.setTextColor(BLACK);     // 文字色
      M5.Lcd.print("Initializing..."); // 起動中
      break;
    case 1:
      M5.Lcd.fillScreen(BLACK);       // 画面背景色
      M5.Lcd.setTextColor(GREEN);     // 文字色
      M5.Lcd.print("Available");      // 在席
      break;
    case 2:
      M5.Lcd.fillScreen(RED);         // 画面背景色
      M5.Lcd.setTextColor(WHITE);     // 文字色
      M5.Lcd.print("Busy");           // 取り込み中
      break;
    case -1:
      M5.Lcd.fillScreen(BLACK);       // 画面背景色
      M5.Lcd.setTextColor(WHITE);     // 文字色
      M5.Lcd.print("Away");           // 不在
      break;
    case -2:
      M5.Lcd.fillScreen(BLACK);       // 画面背景色
      M5.Lcd.setTextColor(YELLOW);    // 文字色
      M5.Lcd.print("Restarting...");  // 再起動中
      break;
    default:
      M5.Lcd.fillScreen(WHITE);       // 画面背景色
      M5.Lcd.setTextColor(BLACK);     // 文字色
      M5.Lcd.print("Unknown Status"); // 未知の状態
      break;
  }
}

// 加速度センサーのデータを基に画面の回転を調整する関数
void updateRotation(float accX, float accY) {
  int newRotation = -1;
  
  if (accY > 0.8) {
    newRotation = 0;  // デバイスが直立している場合
  } else if (accY < -0.8) {
    newRotation = 2;  // デバイスが上下逆の向きの場合
  } else if (accX > 0.8) {
    newRotation = 1;  // デバイスが右向きの場合
  } else if (accX < -0.8) {
    newRotation = 3;  // デバイスが左向きの場合
  }

  // 回転が変わった場合のみ変更
  if (newRotation != currentRotation) {
    M5.Lcd.setRotation(newRotation);
    currentRotation = newRotation;
  }
}

// 在席判定を行う関数
int judgeState() {
  int sum = 0;
  for (int i = 0; i < sampleSize; i++) {
    sum += sampleData[i];
  }
  // 5回以上HIGHなら1（HIGH）、それ以外は0（LOW）
  return (sum >= 5) ? HIGH : LOW;
}
```

あとは[SORACOM側の設定](https://qiita.com/ozk009/items/588f105cc76abb7c7f33#soracom)ができれば完了です。

# まとめ
かなり説明を端折ってしまいましたが、デバイス側のプログラムを変更するだけであとはそのまま流用できるSORACOMは素敵だと思います。
実はこちらは[M5Stack版](https://qiita.com/ozk009/items/588f105cc76abb7c7f33)より先に作ったのですが、PIRセンサーの値が結構ばらつくので、見送りとなりました。
というのも、PIRセンサーは動きを検知するので、集中して動かずに作業していると「不在」判定されることあったためです。
しかし[M5Stack版](https://qiita.com/ozk009/items/588f105cc76abb7c7f33)よりもシンプルですし、個人的には気に入っているので、記事として残しておくことにしました。

みなさまもM5デバイスとSORACOMの連携を試してみてください。