Raspberry Piで自宅の鍵をオートロック化してみた

自作オートロックのベストプラクティスを目指して、試行錯誤した結果、それなりに使えるものができたので、本記事では、そのノウハウを共有します。
完成物はこんな感じです。
内側は、ドアに近づいたら解錠。
<iframe width="560" height="315" src="https://www.youtube.com/embed/O1rbnOsO3yM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
外側は、RFIDカードを読み取って解錠。
<iframe width="560" height="315" src="https://www.youtube.com/embed/azc8MRuurN8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

元々、自宅にRaspberry Pi 3 Model B+があったので、Raspberry Pi 3 Model B+を使用していますが、Raspberry Pi ZERO WHを使用すれば、合計10,000円程で作れると思います。

# 使用したもの
* Raspberry Pi 3 Model B+
* サーボモータ: [DS3225](https://www.amazon.co.jp/gp/product/B08LBHQ881/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1)
* [サーボモータ用電源](https://www.amazon.co.jp/gp/product/B0798JTWTD/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1)
* 超音波距離センサ: [SR04](https://www.amazon.co.jp/gp/product/B081GXLTK6/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1)
* RFIDリーダー: [RC522](https://www.amazon.co.jp/gp/product/B07N3PZG7W/ref=ppx_yo_dt_b_asin_title_o09_s00?ie=UTF8&psc=1)
* [リードスイッチ](https://www.amazon.co.jp/gp/product/B0833LJQDV/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1)
* LED(距離測定中に光らせる用)
* 抵抗

です。
リードスイッチは、扉が開いてるか閉まっているかの判定に使用しています。
抵抗は、SR04の出力を5Vから3.3Vに落とすためと、GPIOピンでLEDを光らせるために使用しています。

モーターの固定は、ホームセンターで買ってきた、L字の部品を組み合わせて高さ調整可能な土台を作ることで解決したのですが、
最大の問題は、鍵によって異なる形状をどうやって吸収するかでした。
結果として、モーターと鍵の接合部は100均で買った石粉粘土をコネコネして作ることがベストプラクティスという結論に至りました。


# 方式設計
### 状態遷移
![State.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101736/37e6b300-5634-226f-df16-b1a041252588.png)

状態には、鍵が開いてる状態と、閉まっている状態があります。
鍵が閉まっているときは、扉の内側に人が立つか、外側からRFIDを読み込んだら、鍵を開けて、
鍵が開いているときは、10秒以上扉が閉まった状態が続くと、鍵を閉まるようにしています。
また、扉の内側に人を検知した際に、Google Homeで通知する機能と、RFID認証に成功すると、LINEで帰宅を通知する機能も用意します。
### クラス設計
![auto_lock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101736/89475e80-0598-f60a-add6-57c07cfa8504.png)

デザインパターンで言うと、Stateパターンを採用しています。
図のように、DoorクラスがStateを持っていて、状態に応じた処理を切り分けます。
各種のセンサ・アクチュエータとのインターフェースは

* サーボモータを制御するサーバー(DS3225)
* 超音波センサにより距離を測定するサーバー(SR04)
* RFIDを読み取るサーバー(RC522)
* リードスイッチを読み取るサーバー(LEAD_SW)

を用意し、HTTPリクエストにより、制御します。
HTTPリクエストは処理のオーバーヘッドが大きいですが、ブラウザでテストしやすいため、このようなマイクロサービス設計（？）にしました。
さらに、Google Homeで人の検知を通知するgoogle_homeクラスと、LINEで帰宅を通知するLINEクラスを用意します。

Raspberry PiはPythonを推奨しているため、全てPythonで実装します。
各モジュールが揃ったら、systemdを使用して、daemon化し、常駐させます。

# 詳細設計
それでは、それぞれのモジュールの設計について説明していきます。
## サーボモータサーバー
以下のように、HTTPリクエスト(GETメソッド)により、モーターを制御することを目標としました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/io4URR0oBSw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

`http://raspberrypi.local:3001/servo`にアクセスすると、現在角度がjsonで帰ってきて、
`http://raspberrypi.local:3001/servo/{pos}`にアクセスすると、角度をposまで回転させます。
ルーティング機能を簡単に実装できる軽量なライブラリを探していたら、FastAPIというものを発見したので、それを使用しています。

サーボモータは、角度を指定して制御できるモーターです。角度の指定には、PWMを使用します。最初は、ソフトPWMを使用していたのですが、制御出力が安定せず、モーターが震えたため、ハードPWMを使用しています。
SG92Rだと、トルクが足りなくて、我が家の鍵は回らなかったため、DS3225を使用しました。しかし、DS3225が回転する時に、電源ランプが一瞬消え、カーネルメッセージに`Under-voltage detected!`のエラーが出るので、
~~ラズパイの事を思うなら、モーターの電源は別供給のほうが良いかもしれません。~~
最悪ラズパイが過電流で壊れるため、モーターは、別電源を用意しました。

ソースコードは、[GitHub](https://github.com/kim-tom/DS3225)にあります。
### daemon化
以下のファイル({username}は適宜置き換えてください。)を作成して、`sudo systemctl enable ds3225`とコマンドすると、サーボモータサーバーが常駐化します。次回起動時から、有効です。
workerを複数にすると、Servoオブジェクトが複数出来て、角度の整合性が取れなくなるので、`-w 1`オプションをつけています。
ログは、`journalctl -u ds3225`で見れます。

```systemd:/lib/systemd/system/ds3225.service
[Unit]
Description = DS3225

[Service]
WorkingDirectory = /home/{username}/development/ds3225
ExecStart=/home/{username}/.pyenv/shims/gunicorn main:app -b 0.0.0.0:3001 -w 1 -k uvicorn.workers.UvicornWorker --forwarded-allow-ips "*"
Restart=always
Type=simple

[Install]
WantedBy=multi-user.target
```

## 距離測定サーバー
次に、超音波によって距離を取得するサーバーを立てます。
`http://raspberrypi.local:3003/`にアクセスすると、以下のようなjsonが帰ってくることを目標とします。単位はcmです。

```json
{"distance": 226}
```
測定原理は単純で、超音波の帰ってくる時間と、音速から距離を求めます。時間の測定には、time.perf_counter()関数を使用しています。time.time()関数よりも精度が高いらしいです。
超音波が跳ね返ってくるまでの間に、待ち時間が発生するので、その間LEDが点灯するようにしました。

サーバーとして求められる機能は非常に単純なので、Pythonに元々組み込まれている、wsgirefを使用しました。

ソースコードは、[GitHub](https://github.com/kim-tom/HC-SR04)にあります。
### daemon化
以下のファイル({username}は適宜置き換えてください。)を作成して、`sudo systemctl enable sr04`とコマンドすると、距離測定サーバーが常駐化します。次回起動時から、有効です。
ログは、`journalctl -u sr04`で見れます。

```systemd:/lib/systemd/system/sr04.service
[Unit]
Description = SR04

[Service]
ExecStart=/home/{username}/.pyenv/shims/python /home/{username}/development/sr04/sr04.py
Restart=always
Type=simple

[Install]
WantedBy=multi-user.target
```
## RFID読み取りサーバー
`http://raspberrypi.local:3000/`にアクセスすると、以下のようなjsonが帰ってくることを目標とします。

```json
{"id": [00,00,00,00]}
```
RFIDの読み取りには、[RC522用のライブラリ](https://github.com/ondryaso/pi-rc522)を使用しました。ただし、wait_for_tag()関数にタイムアウト機能がないため、オーバーライドして使用しています。
`sudo raspi-config`から、SPIを有効にすれば、すぐに使用できます。
ソースコードは、[GitHub](https://github.com/kim-tom/MFRC-522/)にあります。
### daemon化
以下のファイル({username}は適宜置き換えてください。)を作成して、`sudo systemctl enable rc522`とコマンドすると、RFID読み取りサーバーが常駐化します。次回起動時から、有効です。
ログは、`journalctl -u rc522`で見れます。

```systemd:/lib/systemd/system/rc522.service
[Unit]
Description = RC522

[Service]
ExecStart=/home/{username}/.pyenv/shims/python /home/{username}/development/rc522/rc522.py
Restart=always
Type=simple

[Install]
WantedBy=multi-user.target
```

## リードスイッチ読み取りサーバー
ご想像の通り、作り方は他と同じです。
ソースコードは、[GitHub](https://github.com/kim-tom/Lead-Switch)にあります。
内部で、プルアップ抵抗を使用させるのが、ポイントです。
daemon化の手法も他と同様です。

## オートロック制御部分
HTTPリクエストを、それぞれのサーバーに送って、測定、制御します。
HTTP通信のオーバーヘッドによって、応答が遅くなるのが勿体ないため、Python標準機能のasyncioを使用して、並列で処理を行うようにしました。特に、Google HomeとLINEの通知は遅れても問題ないため、リクエストを投げっぱなし(英語でFire and Forgetというらしいです。)にしました。
ソースコードは、[GitHub](https://github.com/kim-tom/auto_lock)にあります。



### daemon化
以下のファイル({username}は適宜置き換えてください。)を作成して、`sudo systemctl enable auto_lock`とコマンドすると、オートロック処理が常駐化します。次回起動時から、有効です。
ログは、`journalctl -u auto_lock`で見れます。

.serivceファイルのpython実行時に、`-u`オプションがないと、ログが見れないので気をつけてください。

```systemd:/lib/systemd/system/auto_lock.service
[Unit]
Description = AUTO_LOCK

[Service]
WorkingDirectory=/home/{username}/development/auto_lock
ExecStart=/home/{username}/.pyenv/shims/python -u /home/{username}/development/auto_lock/auto_lock.py
Restart=always
Type=simple

[Install]
WantedBy=multi-user.target
```
# 参考サイト
以下、参考にしたサイトです。

[Raspberry Pi講座 Servo ( SG90, SG92 )](https://sites.google.com/site/memorandumjavaandalgorithm/raspberry-pi-jiang-zuo7-servo-sg90-sg92)
[Raspberry PiのハードウェアPWMをpigpioで出力する](https://qiita.com/s417-lama/items/0ef64a7af3fcf6a56cc5)
[pythonスクリプトをdaemonにする[systemd編]](https://qiita.com/katsuNakajima/items/7ece6c74f992f652d732)
[Python 製 Web フレームワークを Flask から FastAPI に変えた話](https://note.com/navitime_tech/n/nc0381517d067)
[超音波距離センサ(HC-SR04)を使う](http://make.bcde.jp/raspberry-pi/%E8%B6%85%E9%9F%B3%E6%B3%A2%E8%B7%9D%E9%9B%A2%E3%82%BB%E3%83%B3%E3%82%B5hc-sr04%E3%82%92%E4%BD%BF%E3%81%86/)
[Use an RFID reader with the Raspberry Pi.](https://howtoraspberrypi.com/rfid-raspberry-pi-2/)
[【IoT】Raspberry piでスイッチを使ってLEDを操作しよう](https://inakita-monolab.com/iot-191229/)
[Source code for requests.exceptions](https://docs.python-requests.org/en/master/_modules/requests/exceptions/)

# 解決できていないこと
~~`netstat -an`コマンドを叩くと、TIME_WAIT状態のコネクションが大量にポートを食っていることが分かります。
今の所、動作には問題ないのですが、リソースの無駄遣いなので、直し方が分かったら、また書きます。~~

requestsをクラスメソッドとして使うと、毎回、TCPコネクションを作るため、ポートを大量に使用してしまいます。したがって、クラス変数として、requests.Session()を用意して、それを再利用します。
ただし、Pythonに元々組み込まれている、wsgirefはKeep-Aliveに対応していないため、解決するためには、FastAPIを使用する必要があります。ここまですると、あまりにリーソースの無駄使いなので、次回は、HTTPではなくDbusを使用してオートロックシステムを作った結果を、別記事にてまとめます。