サークルの部室をスマートロック化したお話

# はじめに

はじめまして。私は現在大学生であり、プログラミングをしたりゲームを制作したりするサークルに所属しています。
部室はPCが置かれていて冷暖房もあるためかなり快適な環境になっているのですが少し問題がありました。

さて何でしょう
...
...
...
**鍵が使いにくすぎる！！！！！！！！**:rage::rage:

説明しますと、鍵収納BOXというダイヤル式で開けることのできる箱の中に部屋に入るための鍵が保管されているのですが、この鍵収納BOXがなかなか開け辛いのです。。。
これではユーザーエクスペリエンスが低すぎ！ってことなので自分でUX高めなシステムを構築しました。
# 制作したもの
 - 全体像
![IMG_20181211_150345.jpg](https://qiita-image-store.s3.amazonaws.com/0/262314/7c62cccc-64ac-3949-ed2b-5db075dee153.jpeg)
 - モーター周辺
![IMG_20181211_150350.jpg](https://qiita-image-store.s3.amazonaws.com/0/262314/45bf29e9-b648-dadb-5c96-afa59c2de829.jpeg)
 - メイン装置中身
![IMG_20181211_150740.jpg](https://qiita-image-store.s3.amazonaws.com/0/262314/884abf73-cd0e-b416-7925-35b82dca0b7d.jpeg)

PaSoRiに大学の学生証をかざすとサーボモーターがサムターンを回すことで開錠できるようになっています。
また、扉が閉まると施錠されるオートロックとなっており、出るときには青いボタンで解錠することが可能になっています。
# 使用部品
- RaspberryPi zero W (WHでも可)
- [PaSoRi RC-S380](https://www.amazon.co.jp/dp/B00948CGAG)
- [サーボモータ MG-92B](http://akizukidenshi.com/catalog/g/gM-13228/)
- [A/D Converter MCP3002-I/P](http://akizukidenshi.com/catalog/g/gI-02584/)
- [圧力センサFSR400 SHORT](http://akizukidenshi.com/catalog/g/gP-05747/)
- スイッチ
- LED

# 使用ライブラリ
- [nfcpy](https://pypi.org/project/nfcpy/)
- [pigpio](http://abyz.me.uk/rpi/pigpio/download.html)

# 製作にあたって
私は回路もRaspberryPiもPythonも触るのが初めてだったため、インターネットで先人たちの多大なる努力の結果実現されたライブラリやその使用方法を書き記したブログなどを大変利用させていただきました。おかげで完成させることができ大変助かりました。ありがとうございました。

# 製作
RaspberryPiにはGPIOというピンが生えており、センサーの読み取りやサーボモーターの制御に使用することができます。
初RaspberryPiのためGPIO操作の方法を調べまくった結果pigpioといのが良さそうということでpigpioを採用することに決定しました。
割り込みと高精度PWMをどちらも行えるのが制作していてとても役に立ちました。
参考サイト:
- [pigpioホームページ](http://abyz.me.uk/rpi/pigpio/)
- https://karaage.hatenadiary.jp/entry/2017/02/10/073000

pigpioはとても便利なのですが、pythonでライブラリを読み込むだけでは実行できず、raspberryPiの起動ごとにpidpiodというデーモンを起動させないと動作しません。流石に面倒なので自動化します。下記の参考サイトに載っているものを使わせていただき、service化することで自動起動を実現しました。
参考サイト：
- https://tomosoft.jp/design/?p=8768

ここまでできると、あとはひたすらPythonでプログラムを組んでいくだけです。

### サーボモータ
まずはサーボモーターを動かしてみましょう。
サーボモーターはPWMという信号で制御することができます。このサーボモーターだと、20msのうちに電圧がHighになっていた割合で回転角度を決定することができます。この(Hiの時間/20ms)*100をデューティー比と呼び、例えば10msだけHighになっていた場合はデューティー比50%となります。
データシートが公開されてなく正確な値を割り出すことはできませんが、ほかのサーボモータ（SG90）の場合だと2.5%~12％の間で操作します。
また、サーボモータのPWMはデータシートを見ると5VがHighとなっていますが、raspberryPiでは3.3Vしか出力できません。しかし、3.3Vでも操作可能でしたので昇圧せずこのまま使用することにしました。

``` python:/home/pi/smartlock/servo.py
from time import sleep

class Servo:
    # 変えてね
    servo_pin = 18

    def __init__(self, pi):
        # pigpioのインスタンスを渡してね
        self.pi = pi
 
    # 任意の角度に回転0-180
    def servo_rotate(self, degree):
        motor_pulse = ((degree * 100 / 9) + 500)
        self.pi.set_servo_pulsewidth(Servo.servo_pin, motor_pulse)
        sleep(0.5)
        self.pi.set_servo_pulsewidth(Servo.servo_pin, 0)

    # 鍵を開ける方向に回転
    def open_lock(self):
        # duty:11.25%
        self.pi.set_servo_pulsewidth(Servo.servo_pin, 2250)
        sleep(0.2)
        # サーボ開放
        self.pi.set_servo_pulsewidth(Servo.servo_pin, 0)

    # 鍵を締める方向に回転
    def close_lock(self):
        # duty:6.5%
        self.pi.set_servo_pulsewidth(Servo.servo_pin, 1300)
        sleep(0.2)
        # サーボ開放
        self.pi.set_servo_pulsewidth(Servo.servo_pin, 0)
```
set_servo_pulsewidthの2つ目の引数の値を変えていろいろ試してみてください。
### PaSoRi
次はPaSoRiを触ってみましょう。raspberryPiにUSBで接続し、nfcpyを使用して操作します。
nycpyをインストールするときは公式のInstallationを参考にしましょう。libusb入れる必要があるのを忘れがち。
https://nfcpy.readthedocs.io/en/latest/topics/get-started.html#installation

次に公式のexampleであるtagtool.pyを動かしてみましょう
https://github.com/nfcpy/nfcpy

ここまでできるとnfcpyを利用する準備はばっちり！
使用したいカードを解析していきましょう！
なんですが、ここだけ大学の先輩がコードを書いてくれたため方法があまりわかりません:bow_tone1:

大学の先輩が書いてくれたコードでは学籍番号とカード固有のIDmをハッシュ化して取り出しています。

### 扉の開閉検知
はじめはリードスイッチ（磁気に感知するセンサ）を使用していたのですが、むき出して使用していたことによる破損に加えてマグネットの位置がいつのまにかずれている事件が発生したので圧力センサを使用することに途中で変更しました。扉の軸がある方に圧力センサを設置し、ある閾値を超えると扉がしまったと検知します。
また、今回使用した圧力センサはアナログ出力なためraspberryPiで使用するにはA/D変換が必要でした。なのでA/DコンバーターのMCP3002-I/Pを使用しました。
参考サイト：
- https://qiita.com/f_nishio/items/4b9723c4e622a51aaeb5
- http://akizukidenshi.com/download/ds/microchip/mcp3002.pdf

```python:/home/pi/smartlock/door.py
class Door:
    MINIMUM_VALUE = 250
    CLOSE = 0
    OPEN = 1

    def __init__(self, pi):
        self.pi = pi
        self.h = pi.spi_open(0, 200000, 0) # 0ch 200kHz(5V) mode0

    def door_status(self):
        (count, rx_data) = self.pi.spi_xfer(self.h, [0x68, 0x00])
        value = ((rx_data[0] & 3) << 8) + rx_data[1]
        if value >= Door.MINIMUM_VALUE:
            return Door.CLOSE
        else:
            return Door.OPEN
```

### データベース
同時アクセスが不要なためPythonに標準で付属するライブラリSQLiteを採用

- User Database

| Student_id       |       Name        |    IDm             |
|:----------------:|:-----------------:|:------------------:|

こんな感じで保持してあります。一応IDmはSHA256によりHash化してあります。

- Logs database

|  num             |       Student_id  |    Name            |    enter_time      |    leave_time      |
|:----------------:|:-----------------:|:------------------:|:------------------:|:------------------:|

ログはこんな感じで保持してあります。numはauto incrementになっており、自動で数字が入るようになっております。
timeを分けた理由は聞かないでください:innocent:

### スイッチ
nfcpyではカードを読み取るまで無限ループが発生しボタンの検知を行うことができません。そんなときにもpigpioが大活躍してくれます！
pigpioには入力割り込みという機能があり、電圧レベルの変化を検知してプログラムを実行する機能が備わっているのです。

```python:/home/pi/smartlock/switch.py

import time
import door
import pigpio


class Switch:
    switch_pin = 22

    def __init__(self, pi, servo):
        self.pi = pi
        self.door = Door(pi)
        self.time_old = time.time()
        self.servo = servo
        # switchのピンを入力に設定
        pi.set_mode(Switch.switch_pin, pigpio.INPUT)
        # プルダウン抵抗を有効化
        pi.set_pull_up_down(Switch.switch_pin, pigpio.PUD_DOWN)
        # 割り込みの関数を設定　RISING_EDGE→LOWからHIGHになると実行
        self._cb = pi.callback(Switch.switch_pin, pigpio.RISING_EDGE, self._cbf)

    def _cbf(self, gpio, level, tick):
        # チャタリング防止
        if time.time() - self.time_old < 1:
            return
        else:
            # 鍵開ける
            self.servo.open_lock()
            time.sleep(5)
            # 扉しまってなかったら鍵閉めるまで待つ
            while self.door.door_status() == self.door.OPEN:
                time.sleep(0.5)
            self.servo.close_lock()
        # チャタリング防止用の時間更新
        self.time_old = time.time()
```

### Slackへ送信
部内での連絡にSlackを使用しているので入退室情報をSlackに送信するように設定しました。
SlackにIncoming webhookを利用できるように設定し、requestsライブラリを使って送信しようと思ったのです**が**RaspberryPi Zeroには重すぎたのかrequestsライブラリを全然読み込まない事件が発生したので標準のurllib2を使用するようにしました。

```python:/home/pi/smartlock/slack.py(一部）
import urllib2
import json

url = 'https://hooks.slack.com/services/XXXXXXXX/XXXXXXX/XXXXXXXXXXXXXXXXXXXX'


def send_slack(Name):
    body = "%sが入室しました" % Name
    data = json.dumps({"text": body})
    req = urllib2.Request(url, data, {'Content-Type': 'application/json'})
    try:
        f = urllib2.urlopen(req)
    except urllib2.HTTPError:
        print "slack_error"
    f.close()
```

### 自動起動
ここまででほとんど機能的なものは完成したのですが、再起動すると自動ではpythonプログラムは実行されないためmainのプログラムをサービス化して自動起動するようにしましょう。

```sh:/home/pi/smartlock.sh
cd ~/smartlock
python __main__.py
```
↑このファイルを/home/pi/において

```/etc/systemd/system/smartlock.service
[Unit]
Description = smart lock

[Service]
ExecStart=/bin/sh /home/pi/smartlock.sh
ExecStop=/bin/kill -INT ${MAINPID}
Restart=always
Type=simple

[Install]
WantedBy=multi-user.target
```
こんな感じでサービスを定義して、

```sh
# 自動起動有効化
$ sudo systemctl enable smartlock.service
# 起動
$ sudo systemctl start smartlock.service
# 状態確認
$ sudo systemctl status smartlock.service
```
できた:relaxed:
### 登録画面
さて、ここまででスマートロックの機能の９９％を制作できたはずなのですが、肝心のユーザー登録画面がありませんね。いちいちssh接続してユーザー登録用のpythonを実行するのもかなり面倒なのでPCからクリック１回で登録・削除・確認ができるシステムを作っていきましょう。
PCはWindowsがほとんどだと思うのでPowershellを使っていきます。
事前準備としてRaspberryPiとPCとの間で公開鍵を交換します。linuxにはssh-copy-idという便利コマンドがあるのですが、Windowsには当然ながらありません。**しかし**同等機能を実現するスクリプトを公開してくださってる方がいたのでありがたく利用させていただきました。

- http://d.hatena.ne.jp/rti7743/20100101/1262347941

上記の手順に従ってもらえば鍵交換が簡単に終わります。
更に事前準備としてユーザーを登録するpythonコードとシェルスクリプトを用意しましょう

```python:/home/pi/smartlock/add_user.py
# -*- coding: utf-8 -*-
import hashlib
import time
from logging import DEBUG, Formatter, StreamHandler, getLogger

# from reader import Reader
import database
import slack

logger = getLogger(__name__)
handler = StreamHandler()
handler.setFormatter(Formatter("[%(levelname)s] %(message)s"))
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False


def main():
    name = raw_input("追加するユーザー名を入力してください（英数字のみ）:")

    #　ここでカード読み込み

    database.make_info("ここに番号", name, "ここにIDm")
    print ("追加完了しました。")
    slack.add_user("ここに番号", name)
    time.sleep(1)


if __name__ == "__main__":
    main()
```

```sh:/home/pi/add_user.sh
cd ~/smartlock
# 一旦止めないと読み取りができない
sudo systemctl stop smartlock.service
# 追加用pythonコード実行
python add_user.py
# 再開
sudo systemctl start smartlock.service
```

やっと事前準備が完了しましたね。
次にPowershellで実行するスクリプトを用意していきましょう

```powershell:manager.ps1
Write-Output "入退室管理システム管理スクリプト"

$input = Read-Host "何を実行しますか？（1：ユーザー追加　2：名前変更　3：ユーザー削除 その他:終了）"

switch($input){

  "1"{
  Write-Output "しばらくお待ちください。"
  ssh pi@192.168.xxx.xxx -t 'stty erase ^H
  sh add_user.sh'
  }
 
  "2"{
  Write-Output "しばらくお待ちください。"
  ssh pi@192.168.xxx.xxx -t 'stty erase ^H
  sh rename_user.sh'
  }
 
  "3"{
  Write-Output "しばらくお待ちください。"
  ssh pi@192.168.xxx.xxx -t 'stty erase ^H
  sh delete_user.sh'
  }
  
default{
Write-Output "終了します。" 
}

}

Read-Host "終了しました。Enterを押してください。"
```
xxxのところは環境に応じて変更お願いします。
リネームと削除は同じようにpythonとシェルスクリプトを用意してください。
上記のファイルを好きなところに保存し右クリック→送る→デスクトップにショートカットを作成
の手順でショートカットを作成します。それだけだとpowershellが自動で立ち上がらないので、
デスクトップのショートカットを右クリック→プロパティ→リンク先の前に
> powershell -ExecutionPolicy RemoteSigned -File

を追加する。
> 例) powershell -ExecutionPolicy RemoteSigned -File C:\Users\XXX\manager.ps1

はいこれでダブルクリックで実行できるようになりました:relaxed:

### 仕上げ
はんだ付けしましょう！ 回路はこんな感じで接続しました
![smartlock_circuit.png](https://qiita-image-store.s3.amazonaws.com/0/262314/7f924420-6895-b2cc-0e4f-5fbfe163254b.png)
少し余計な部品がついていますが、こんな感じで接続しました！
ハンダきたねえとかの苦情は受け付けません。ハンダ付け２回目なんで許してください。
ピンヘッダがなくて頑張って線でつないだのですが、**皆さんはピンヘッダ使いましょう。** 地獄を見たければ真似してください。
![IMG_20180604_013144.jpg](https://qiita-image-store.s3.amazonaws.com/0/262314/a318b604-480a-0e4b-9339-623220b4d637.jpeg)

### ケースづくり
何を思ったのか3Dプリンタを購入してしまったので3Dプリンタで制作していきます。
Fusion360を使用し３回ぐらい試行錯誤したらいい感じのが出来上がりました
![2018-12-12.png](https://qiita-image-store.s3.amazonaws.com/0/262314/788eae8f-b7e7-7342-3d71-6f5cf3366e9c.png)
穴が一個多いのはサーボモータの電源用に準備していたのですが、不要とわかったので使いません。
あと、蓋のデータが行方不明になっちゃいました:sob:

### サーボマウンタ作り
こちらも3Dプリンタで制作していきます。
Fusion360を使ってこんな感じのものを作ってみました
![2018-12-12 (1).png](https://qiita-image-store.s3.amazonaws.com/0/262314/3c26099f-e25c-233b-4bba-ec71d45f5842.png)
![2018-12-12 (2).png](https://qiita-image-store.s3.amazonaws.com/0/262314/ac67c0bf-86d6-3fec-2dea-f22b2ee43ba9.png)
![IMG_20181211_150350.jpg](https://qiita-image-store.s3.amazonaws.com/0/262314/45bf29e9-b648-dadb-5c96-afa59c2de829.jpeg)
説明が難しいのでこの３枚で察してください:stuck_out_tongue_winking_eye:
扉には強力両面テープで接着してあります。
ちなみに、このマウンタだけで印刷に３時間近くかかりました。

##これにて完成！！
![smartlock.gif](https://qiita-image-store.s3.amazonaws.com/0/262314/4dadf1f0-f741-d320-8bcd-9ddee556a6a1.gif)

超ベンリ:smile::smile:

##Github
何もかもが初心者の書いたコードでも良いなら、まとめておいておきます。
カード読み込み部分だけ自分が書いたわけではないので省略させていただきました。
https://github.com/konikoni428/smartlock


## 最後に
自分で制作したものが日常で活躍できてるってたのしー。
部員にも喜んでいただけたようで、大変よい経験となりました。
今後もどんどん制作していこうと思うので、そのときはまたシェアしたいと思います。
読んでいただきありがとうございました。