ESP32による美和ロックの制御

# 【備忘録】ESP32による美和ロックの制御

ビルドインなスマートロックやオートロックを作りたい方向け

## 概要

wiremo やスマートロック(Ex: Qrio Lock, Akerun)などの既製品では不都合な場合を想定し、ESP32 やリレーを用いた電動錠制御装置の実装だけでなく簡易な管理システムの実装及び装置とシステムの連携を実施します。
ユーザ管理や認証については当書では LINE BOT を用いることで省きます。
実際のプロダクトに寄せた内容にし後任者による既存設計の大雑把な理解と
設計の所々の決定の背景が伝わるようにすることが目的になります。
※関係者の方でその他把握しておきたいことがあれば私信お願いします。(現在の仕事もあるので簡素な対応になるかと思いますが予めご了承ください)

### GIF 動画

#### LINE 上で解錠の操作をした様子



#### 連動し電動で回るサムターン

![cow-lock.mp4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133081/7053ff55-a836-1593-be0b-b80e7d83fd3e.gif)

### 機電関連

- ESP32
    - マイコン
    - 技適有り
    - WiFi 通信可
    - Bluetooth 通信可
    - 国内でも安価(数百円/個) 
- 美和ロック製電動錠
    - 鍵の有名ブランド
    - 回路図などは公式サイトから参照されたい: https://www.rrrmaji.com/product-list/31
    - 当書では 3pin*3pin のものを扱っています。
    - ⚠専用ケーブル及びコネクタが必要です。
      - 現場のノリで 3D プリンタを用いてコネクタ部分を再現できたものの内部の金具まで実装するのは困難でした。工数も取られます。
    - 内蔵DCモータの必要電圧は 24V のものを扱っています。 

### デプロイ関連

- GitHub&Heroku/Git&Dokku

### 開発ツール等

- Ubuntu Desktop
- Visual Studio Code
- PlatformIO
    - Arduino 系の開発などで便利です。
- semistandard

#### 設定ファイルのサンプル `platformio.ini`

```
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
```

## 実装

### ハードウェア

#### 外観

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133081/c171338a-e811-ef11-82c3-5079c11bda3c.png)

- ⚠ボディの CAD の対応と 3D プリントを対応していただいた業者がケチったのかボディ版の厚みは 2mm しかなく折れやすいので丁寧に扱うこと
- 設定密度次第ですが 3~4mm が家庭用 3D プリンタならば無難です。※用途による

#### 各種配線接続案内

- 右ACDCアダプタ: 24V 電動錠用 → UPSに接続し停電対策
- 中ACDCアダプタ: 05V マイコン用 → UPSに接続し停電対策 
- 左ACDCアダプタ: 05V 停電検知用 → コンセントに接続
- 白スイッチ: 通常モードとアクセスポイントモード(設定用)の切り替え
- MIWA-DEN ケーブル: 電動錠のコネクタと接続
    - 錠前側(制御装置側)は逆なので後ほど入れ替えました。

#### 実装案内

#### 回路・組み込み

- 電動錠の回路図を元に開発
- 過電流や落雷などに対する効果を期待し電磁リレーを採用
    - 寿命的には半導体リレーが良いかと思いますが設置場所の詳細な情報が不足していたためこのような決定になったという背景があります。
    - 電動錠の制御用だけでなく停電検知用の電磁リレーも基板に載っています。
- 3pin*3pin 電動錠ではショートしてる配線を調べることで鍵の施錠状況や扉の開閉状況を取得可能
    - 制御用のクラスを実装すると便利
- Web API のエンドポイント、WiFi の SSID や PW の保存は SPIFFS へ格納し再起動時自動接続
    - WiFi&HTTPS 接続クラスを実装すると便利
    - WiFiClientSecure で SSL で保護されたエンドポイントへ接続できるようにする
    - SPIFFS は簡単にスキャンできるので制御装置は当然安全な場所に設置
    - よくある実装なので当書では割愛
- 施錠し忘れたら施錠するしないについては Web API 側で制御できるようにするため電子回路関連の Arduino で記述するコードは以上になります。

```
#ifndef ESP32_SRC_MIWA_H_
#define ESP32_SRC_MIWA_H_

enum lockStatus { unlocked, locked, error };

class Miwa {
  public:
    void init(void);
    lockStatus getLockStatus(void);
    boolean isClosed(void);
    boolean forceLock(void);
    boolean lock(void);
    boolean forceUnLock(void);
    boolean unLock(void);
  private:
    int lockPin;
    int reversePin;
    int doorStatusPin;
    int lockStatusPin;
    int unLockStatusPin;
};

#endif
```

```cpp
#include "Arduino.h"
#include "Miwa.h"

void Miwa::init(void) {
  Serial.println("Miwa!");
  lockPin = 22;
  reversePin = 21;
  // 以下に登場する各色は美和社 Web サイト上の案内を参考にする
  doorStatusPin = 4; // 白: 扉の開閉状況がわかります。
  lockStatusPin = 17; // 黄: 施錠してればショートするケーブルです。
  Serial.println(lockStatusPin);
  unLockStatusPin = 16; // 赤: 解錠してればショートするケーブルです。
  pinMode(lockPin, OUTPUT);
  pinMode(reversePin, OUTPUT);
  pinMode(doorStatusPin, INPUT_PULLDOWN);
  pinMode(lockStatusPin, INPUT_PULLDOWN);
  pinMode(unLockStatusPin, INPUT_PULLDOWN);
}

lockStatus Miwa::getLockStatus(void) {
  // digitalRead は 0 か 1 が返ってくるので ! で boolean 型に変換してから再度否定することで元の意図になります。
  boolean lockStatus = !!digitalRead(lockStatusPin);
  boolean unLockStatus = !!digitalRead(unLockStatusPin);
  if (lockStatus && !unLockStatus) {
    return locked;
  } else if (!lockStatus && unLockStatus) {
    return unlocked;
  } else {
    Serial.println("lock sensor error");
    return error;
  }
}

boolean Miwa::isClosed(void) {
  // 白色のケーブル(doorStatusPin)がショートしてると扉が閉じていることになります。
  return !!digitalRead(doorStatusPin);
}

boolean Miwa::forceLock(void) {
  digitalWrite(lockPin, 1);
  delay(800);
  digitalWrite(lockPin, 0);
  delay(1000); // 鍵のコイルの燃え尽き対策
  switch (getLockStatus()) {
    case locked:
      return true;
    case unlocked:
    case error:
      return false;
  }
  return false;
}

boolean Miwa::lock(void) {
  // 扉が開いているのに施錠すると扉を閉じれなくなるので保護する
  if (!isClosed()) {
    return false;
  }
  switch (getLockStatus()) {
    case error:
      Serial.println("error");
      return false;
    case locked:
      return false;
    case unlocked:
      return forceLock();
  }
  return false;
}

boolean Miwa::forceUnLock(void) {
  digitalWrite(lockPin, 1);
  digitalWrite(reversePin, 1);
  delay(800);
  digitalWrite(lockPin, 0);
  digitalWrite(reversePin, 0);
  delay(1000); // 鍵コイルの燃え尽き対策
  switch (getLockStatus()) {
    case unlocked:
      return true;
    case locked:
    case error:
      return false;
  }
  return false;
}

boolean Miwa::unLock(void) {
  switch (getLockStatus()) {
    case error:
      Serial.println("error");
      return false;
    case locked:
      return forceUnLock();
    case unlocked:
      return false;
  }
  return false;
}
```

##### ハードウェア用 Web API 定義

- 当書では簡潔な文献を目指し制御装置とエンドポイントは一対一の前提で定義します。
- WebSocket などでなくポーリング
    - WebSocket の方が動作としては早いので何れこちらのバージョンの実装も試します。
    - インターネット上に REST な実装例が多く参考にし易かったという背景がありました。
- ESP32 は定期的に先述の鍵や扉の状態を POST し `lock` `unlock` `forceLock` `forceUnLock` の何れかの文字列をレスポンスされる
    - 先述 Miwa クラスや WiFi&HTTPS 接続クラスをライブラリ的に呼び出して Receiver クラスを実装すると便利
    - レスポンスされた内容次第で施錠や解錠を行う

### Web API

- Heroku や Dokku 上に載せるので Procfile を設定
- 当初では電動錠の状態の記録はオンメモリで行い、 `index.js` の 1 file で済むよう簡潔な案内を目指します。

Procfile　例
```
web: yarn start | tee >(split -d -b 100000 -)
```

⚠ ` | tee >(split -d -b 100000 -)` は docker コンテナへわざわざ入ってログを見たい方向け

```javascript
'use strict';
// ↑の記述は実際のプロダクトでは TypeScript なので真似しなくて良いです。

const express = require('express');
const line = require('@line/bot-sdk');
const app = express();

let status = 'lock'; // 今回はオンメモリ
const PORT = process.env.PORT || 5000;

// Env from LINE Developers
const config = {
  channelSecret: process.env.LINE_SECRET,
  channelAccessToken: process.env.LINE_ACCESS_TOKEN,
};
const client = new line.Client(config);

// line.middlewar との競合に注意されたい
app.use(express.json());
app.use(express.urlencoded({
  extended: true
}));
app.use(express.static('public')); // 200 を返せるよう index.html などは置いておくこと

const eventHandler = (event) => {
  if (event.type !== 'message') {
    return Promise.resolve(null);
  }
  if (event.message.type !== 'text') { // 位置情報など種類豊富
    return Promise.resolve(null);
  }

  // LINE Bot に あけて と送られたら…
  if (event.message.text.indexOf('あけて') !== -1) {
    // ステータスを更新
    status = 'unlock';

    // 解錠した旨伝える
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: 'unlocked',
    });
  }

  // LINE Bot に しめて と送られたら…
  if (event.message.text.indexOf('しめて') !== -1) {
    // ステータスを更新
    status = 'lock';

    // 施錠した旨伝える
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: 'locked',
    });
  }
};

// ESP32 からのポーリングに対してレスポンスする
app.get('/status', (req, res) => {
  res.status(200).send(status);
});

// LINE からの POST リクエストを処理
app.post('/webhook', line.middleware(config), (req, res) => {
  Promise
    .all(req.body.events.map(eventHandler))
    .then((result) => res.json(result));
});

app.listen(PORT, () => {
  console.log(`App: http://localhost:${PORT}`);
});
```

### まとめ

実際の現場において ESP32 による美和ロック制御装置がロールアウトされてから 1 年程経過しましたがこの部分は一度も障害が発生してなく返って怖いので該当の障害対応マニュアルを予めよく確認くださいますようお願い申し上げます。
とはいえ全体的に高レイヤの技術やプログラミング言語を採用しながらもここまで安定しているのは喜ばしいことです。以上、よろしくお願いします。

_cppの比較演算子に0と1入れても良さそうな気がするけど多分上手くいかなかったからこうしたのかもしれないがうろ覚えです。_