M5Stack遠隔押印装置

# はじめに
リモートワークが増えている中で押印してもらうためだけに出社することがあります。世間では判子をなくそうという風潮です。しかし、判子は日本の文化だと思います。

- 自分の判子をもらったときの嬉しさ
- 上司になって押印したときの責任感

時代は電子承認の流れですが、1クリックで電子書類に印影が入るだけじゃダメなんです！
物理的に判子を押す感覚が欲しいんです！

ということで、物理的に判子を押して、無駄にクラウドを使用して電子承認する仕組みを作りました。

デモ動画は「承認野郎Aチーム」(ネタが古い...)
[![承認野郎Aチーム](https://img.youtube.com/vi/jjtHGKba0pA/0.jpg)](https://www.youtube.com/watch?v=jjtHGKba0pA)

# 開発環境
- M5Stack
- [感圧センサー(円形・大)](https://www.switch-science.com/catalog/2613/)
- 判子(今回は鈴木さん限定)
- 押印用スタンプ台
- 承認欄(この裏に感圧センサーを仕込む)
- Firebase Realtime Database
- Android端末(Nexus7)

![parts_list.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543183/06f80e20-68aa-7bb1-4d00-f2fef85810e3.jpeg)

# 構成図と押印の仕組み
## 構成図
![hanko_develop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543183/43d40a61-dcd2-08be-7d26-5fdecbf3696e.png)

## 承認依頼と押印フロー
- Android端末がRealtime Databaseから依頼を監視します。
- 依頼があると音声案内されます。
- 承認する場合は、承認蘭(物理)で押印します。  
ここで押印の強さと時間を計測して、押印の濃さに利用します。
- 押印が確定したら、Realtime Databaseへ押印情報(結果・強さ・時間など)を書き込みます。
- Android端末が承認情報を監視しているので、押印情報から画面へ押印します。  
押印の強さと時間により押印の濃さが変わります。

## 押印検出
- 感圧センサーをADして開始閾値(0.5V)超えたら押印開始と判断します。
- 完了閾値(0.2V)未満なら押印完了と判断します。
- 押印監視から押印完了までを押印時間(msec)とします。
- 押印の強さの判定用に最大電圧を保存しておきます。

![voltage_measure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543183/5112fe7f-3e75-5ce8-3d6c-a7748fc2d72a.png)

## Realtime Databaseデータ構造
Realtime Databaseのデータ構造をAndroidのコードから￥説明します。

```kotlin
data class ApprovalStatus(
    var status : Int = 0,       // 押印ステータス(0:初期状態 1:押印開始 2:押印完了)
    var result : Int = 0,       // 承認結果(0:否認 1:承認)
    var type : Int = 0,         // 押印タイプ(0:真っ直ぐ 1:お辞儀 2:逆さま)
    var power : Float = 0.0f,   // 押印力(V)
    var presstime : Int = 0     // 押印時間(msec)
)
```

- 押印ステータスは、承認依頼がきたときに0、M5Stackで押印完了したときに2です。(1は未使用)
- 押印タイプは、現在M5Stackからは0固定です。(Androidアプリ側で制御)
- 押印力は、最大電圧値を設定します。
- 押印時間は、押印開始を検出してから押印完了までの時間を設定します。


# ソースコード
M5StackとAndroid(承認取得と表示)のソースコードを説明します。

## M5Stack
### 定義など
- AD計測ピン番号、押印検出状態、Firebase接続情報を定義します。

```c
#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>

// 定義関連
// AD計測ピン番号
#define ANALOG_READ_PIN 35

#define TH_PUSH_DETECT    (0.5)   // 押印検出閾値(V)
#define TH_PUSH_FINISH    (0.2)   // 押印終了閾値(V)

// 押印検出状態
enum StatusDetect {
  DETECT_INIT = 0,     // 初期状態
  DETECT_START = 1,    // 検出開始
  DETECT_FINISH = 2    // 検出終了
};

StatusDetect statusDetect = DETECT_INIT;  // 押印検出状態
unsigned long timeDetectStart = 0;        // 押印検出開始時間(msec)
unsigned long timeDetectEnd = 0;          // 押印検出終了時間(msec)
float peekVoltage = 0.0;                  // 最大電圧

// WiFiクライント(Firebase通信で使用)
WiFiClientSecure client;

// Firebase接続先
const char* firebase_host = "xxxx.firebaseio.com";
// Firebaseのシークレットキー
const char* firebase_secrets = "xxxx";
// Realtime Database保存パス
const char* path_hankoplus = "hankoplus/status";

// 押印検出情報初期化
void initDetect() {
  statusDetect = DETECT_INIT;
  timeDetectStart = 0;
  timeDetectEnd = 0;
  peekVoltage = 0.0;
}
```

### 初期化処理とメイン処理
- メイン処理では、押印検出を監視し、押印完了で送信データ8JSON)を生成してFirebaseへ保存します。
- Firebaseへの保存は下記のサイトを参考にさせていただきました。(詳細は割愛します)  
[Firebase Realtime Database のデータ保存、取得、ストリーミング受信実験( ESP32 , M5Stack )](https://www.mgo-tec.com/blog-entry-firebase-realtime-database-sever-sent-events-esp32-m5stack.html)

```c
void setup() {
  M5.begin();

  // スピーカーOFF
  M5.Speaker.write(0);

  // AD計測ピン設定
  pinMode(ANALOG_READ_PIN, ANALOG);

  // Wifi接続(省略)

  // 押印検出情報初期化
  initDetect();
}

void loop() {
  M5.update();

  // 押印計測処理
  measure();

  // 押印終了していればFirebaseへ保存
  if (statusDetect == DETECT_FINISH) {
    int statusPush = 2;  // 2:完了
    int result = 1;      // 1:成功
    int typePush = 0;    // 真っ直ぐ
    unsigned long presstime = timeDetectEnd - timeDetectStart;
    
    // 送信データ(JSON)生成
    String body = "";
    body += "{\n";
    body += "\"status\":" + String(statusPush) + ",\n";
    body += "\"type\":" + String(typePush) + ",\n";
    body += "\"result\":" + String(result) + ",\n";
    body += "\"power\":" + String(peekVoltage) + ",\n";
    body += "\"presstime\":" + String(presstime) + "\n";
    body += "}";

    // Firebaseへ保存
    postServerSentEvents(body);
  }
}
```

### 感圧センサーの押印検出処理
- 感圧センサーからの電圧を計測して開始閾値超えを検出したら、押印を開始したと判断します。
- 電圧が完了閾値未満を検出したら、押印が完了したと判断します。
- 押印開始から押印完了までの時間(msec)と最大値(V)を記憶しておきます。(Firebaseへ保存時に使用)

```c
// 押印計測処理
void measure() {
  int val = 0;
  float voltage = 0.0;
  float voltageTotal = 0.0;
  float voltageAvg = 0.0;
  int count = 100;

  // 電圧を100回計測して平均化
  for (int i = 0; i < count; i++) {
    val = analogRead(ANALOG_READ_PIN);
    voltage = (float)(val) * (3.3 / 4096.0);
    voltageTotal += voltage;
    delay(1);
  }
  voltageAvg = voltageTotal / (float)count;
  
  switch (statusDetect) {
    case DETECT_INIT:
      // 検出閾値を超えるまで待つ
      if (voltageAvg > TH_PUSH_DETECT) {
        statusDetect = DETECT_START;   // 押印検出状態へ遷移
        timeDetectStart = millis();    // 検出開始時間を保存
        peekVoltage = voltageAvg;      // 最大値を保存
      }
      break;
    case DETECT_START:
      // 最大値チェック
      if (voltageAvg > peekVoltage) {
        peekVoltage = voltageAvg;      // 最大値を保存
      }
      // 検出閾値を下回るまで待つ
      if (voltageAvg < TH_PUSH_FINISH) {
        statusDetect = DETECT_FINISH;  // 検出終了へ遷移
        timeDetectEnd = millis();      // 検出終了時間を保存
      }
      break;
    default:
      break;
  }
```

## Android(承認取得と表示)
Androidは承認状態の監視と印影画像作成の処理のみ示します。

### 承認状態の監視
- 承認状態(ApprovalStatus.status)が押印完了(2)になるのを監視します。
- modestampが0のときは真っ直ぐな印影(社長は必ずこれ)、1のときはお辞儀した印影を表示します。

```kotlin
// 承認状態監視Listener
private val listenerApprovalStatus = object : ChildEventListener {
  override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
    // hankoplus/statusの変更あり
    val value = snapshot.getValue(ApprovalStatus::class.java)
    value?.let {
      // 承認状態で分岐
      when (it.status) {
        0 -> {}  // 初期状態
        1 -> {}  // 押印判定開始
        2 -> {   // 押印完了(承認/否認いずれも)
          if (it.result == 1) {
            // 押印パワー取得(最大2.0Vとする)
            var power = it.power
            if (power > 2.0f) {
              power = 2.0f
            }
            // 押印時間取得
            val presstime = it.presstime

            // 押印濃度計算(割合 -> パワー=0.4 時間=0.6)
            val arph = (((power / 2.0f) * 0.4f + (presstime.toFloat() / 8000.0f) * 0.6f) * 256).toInt()
            var bmp: Bitmap? = null
            if (modeStamp == 0) {
              // 社長は必ず真っ直ぐ
              bmp = createApproval("鈴木", 0, arph)
            } else {
              bmp = createApproval("鈴木", typeStamp, arph)
            }
            setApproval(bmp)

            // 次の承認ように承認状態を初期化
            val updates = mutableMapOf<String, Any?>("status" to 0)
            refApprovalStatus?.child("status")?.updateChildren(updates)
          }
        }
      }
    }
  }

  override fun onCancelled(databaseError: DatabaseError) {}
  override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
  override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {}
  override fun onChildRemoved(snapshot: DataSnapshot) {}
}
```

### 印影画像の作成
- 押印濃度を計算したアルファ値で印影画像を作成します。  
計算式は、押印した実験結果から独自に決定したものです。
- Android端末(Nexus7用)と承認用紙の画面に合わせて作成してます。

```kotlin
// 承認画像の作成
private fun createApproval(name: String, type: Int, arph: Int): Bitmap {
  val width = 400
  val height = 400
  val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
  val canvas = Canvas()
  canvas.setBitmap(bmp)

  val paintBackground = Paint()
  paintBackground.setColor(Color.WHITE)
  val rect = Rect(0, 0, width, height)
  canvas.drawRect(rect, paintBackground)

  val paintName = Paint()
  paintName.textSize = 170.0f
  paintName.isAntiAlias = true
  paintName.setColor(Color.argb(arph, 255, 0, 0))

  val paintFrame = Paint()
  paintFrame.setColor(Color.argb(arph, 255, 0, 0))
  paintFrame.style = Paint.Style.STROKE
  paintFrame.strokeWidth = 16.0f

  val cx = width / 2.0f
  val cy = height / 2.0f
  val radius = (width * 0.9f) / 2.0f
  canvas.drawCircle(cx, cy, radius , paintFrame)
  var str = name.get(0).toString()
  canvas.drawText(str, 115.0f, 175.0f, paintName)
  str = name.get(1).toString()
  canvas.drawText(str, 115.0f, 340.0f, paintName)

  val mat = Matrix()
  when (type) {
    0 -> {  // 真っ直ぐ
      mat.postRotate(0.0f, cx, cy)
    }
    1 -> {  // お辞儀
      mat.postRotate(-30.0f, cx, cy)
    }
    2 -> {  // 逆さま
      mat.postRotate(180.0f, cx, cy)
    }
  }
  val bmpMat = Bitmap.createBitmap(bmp, 0, 0, width, height, mat, true)
  val bmpStamp = Bitmap.createBitmap(bmpMat, (bmpMat.width / 2.0f - width / 2.0f).toInt(), (bmpMat.height / 2.0f - height / 2.0f).toInt(), width, height)
  return bmpStamp
}
```

## 参考サイト
[Firebase Realtime Database のデータ保存、取得、ストリーミング受信実験( ESP32 , M5Stack )](https://www.mgo-tec.com/blog-entry-firebase-realtime-database-sever-sent-events-esp32-m5stack.html)

# おわりに
物理判子を押すというアナログな操作をM5Stackでデジタルに変換して、Firebaseでクラウドを使ってタブレットと連携までまるっとできるのは楽しいです。

開発当初は、押印方向(真っ直ぐ、お辞儀、逆さま)をM5Stackのボタンで選択していましたが、試験してもらったときに操作が面倒だったので、現在は承認書類が押印方向の情報を持っていて書類によりきまるようにしています。(デモ動画では書類の内容や情報は内部で固定ですが)
現在は鈴木さん限定なので、いろいろなお名前やオリジナル印影にも挑戦したいですね。

M5Stackはセンサー類も充実していますし、感圧センサーのような変化を電圧値として計測できれば、いろいろと応用ができるのが楽しいので、今後もネタかつ実用的なものを作っていきたいです。