【ESP32】microPythonでスマートロックをつくってみた

# はじめに
研究室の鍵を借りに行くのが面倒くさい！スマートロックが欲しい！
↓
既製品は高価！(2万くらい）（しらんけど）
↓
じゃあ作ろうぜとなり，

ESP-WROOM-32とmicroPythonで研究室の鍵をスマートロック化させてみた話


## なぜmicroPython?
使ってみたかったから．ただそれだけ．

# 概要
### ソースコード&3Dデータ
[GitHub](https://github.com/hiraikiichi/smartlock_v1)にあります．

### 仕様&要件
- ブラウザ(ローカル)から開錠/施錠ができる
- 開錠/施錠の通知をSlackに投稿
- 鍵が閉まっているときLEDを点灯
- 物理キーでも開錠/施錠できること
- 内側からサムターンを操作できること

### 用意するもの
- マイコン ESP-WROOM-32
- サーボモータ SG-90 (サムターンが固い場合はSG-92Rをお勧めします)
- ブレッドボード
- LED
- 抵抗(10kΩ)
- ACアダプタ
- (3Dプリンタ)
- (Fusion360)
- Wi-Fi

ESP32をmicroPython環境にするやり方は[MicroPythonドキュメンテーション](https://micropython-docs-ja.readthedocs.io/ja/latest/esp32/tutorial/intro.html)などを見てくださいな．

# 配線図

<img width="400" alt="回路図" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794860/a04d1815-27fb-7eed-f18d-0a89eda52c9c.png">

# サムターンを回す機構
物理キーでも開錠/施錠できるようにするため，こんな感じに作ります．
左右90度回転させます．
### 施錠時

<img width="300" alt="open" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794860/b855d85b-d662-b368-13f0-53221f28aad4.png">

### 開錠時

<img width="300" alt="lock" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794860/ce712426-67e9-4be8-f11d-d923dcad076d.png">

元の状態に戻すことによって，物理キーを使用したときにサムターンを回す機構が邪魔にならないようにしています．

# 3Dプリンタで本体作成
Fusion360でモデリングしてプリントします．
サーボモータを差し込む穴はSG-90の寸法にしています．
<img width="400" alt="本体" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794860/111a5b96-9d38-12a8-84ed-93e478d836f4.png">


# Slackのトークン取得
以下の手順でトークンを取得します．
[Slack APIを使用してメッセージを送信する](https://qiita.com/rubytomato@github/items/6558bfdb37d982891c09)


# microPythonコード
<details><summary>main.py</summary><div>

```main.py
import gc
import ssl
import esp
import time
import socket
import network
from machine import Pin,PWM
from utime import sleep_ms, ticks_ms


gc.collect()
esp.osdebug(None)

ssid = 'SSIDをここに入力'
password = 'パスワードをここに入力'

station = network.WLAN(network.STA_IF)
station.active(True)
station.connect(ssid, password)

# ピン指定
ledpin = 32
servo = 13

led = Pin(ledpin, Pin.OUT)
servo1 = PWM(Pin(servo), freq = 100)

def web_page():
    
    if led.value() == 1:
        gpio_state="OPEN"
    else:
        gpio_state="CLOSE"
    
    html = """
    <html lang = "ja">
    <head>
    <title>Key</title>
    <meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" href="data:,">
    <style>
      html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;}
    h1{color: #0F3376; padding: 2vh;}p{font-size: 1.5rem;}.button{display: inline-block; background-color: #e7bd3b; border: none;
    border-radius: 4px; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}
    .button2{background-color: #4286f4;}
    </style>
    </head>
    <body>
    <h1>かぎ</h1>
    <p>now: <strong>""" + gpio_state + """</strong></p>
    <p><a href="/?led=on"><button class="button">OPEN</button></a></p>
    <p><a href="/?led=off"><button class="button button2">CLOSE</button></a></p>
    </body>
    </html>
   """
    return html

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 接続待ちするIpアドレスとポート番号指定
s.bind(('', 80))
# 接続を待つ,サーバー受け入れ最大数
s.listen(5)

def open():
    se = socket.socket()
    ai = socket.getaddrinfo("slack.com", 443)
    addr = ai[0][-1]
    se.connect(addr)
    se = ssl.wrap_socket(se)
    send_data = "token=【Slackのトークン】&channel=%23【チャンネル名】&text=Opend the lab from ESP32"
    se.write(b"POST /api/chat.postMessage HTTP/1.1\r\n")
    se.write(b"Host: slack.com\r\n")
    se.write(b"Accept: */*\r\n")
    se.write(b"Content-Length: %d\r\n" % len(send_data))
    se.write(b"Content-Type: application/x-www-form-urlencoded\r\n")
    se.write(b"Connection: close\r\n")
    se.write(b"\r\n")
    se.write(send_data)
    se.close()
    led.on()
    servo1.duty(240)
    time.sleep_ms(2000)
    servo1.duty(150)
    time.sleep_ms(2000)

def close():
    se = socket.socket()
    ai = socket.getaddrinfo("slack.com", 443)
    addr = ai[0][-1]
    se.connect(addr)
    se = ssl.wrap_socket(se)
    send_data = "token=【Slackのトークン】&channel=%23【チャンネル名】&text=Closed the lab from ESP32"
    se.write(b"POST /api/chat.postMessage HTTP/1.1\r\n")
    se.write(b"Host: slack.com\r\n")
    se.write(b"Accept: */*\r\n")
    se.write(b"Content-Length: %d\r\n" % len(send_data))
    se.write(b"Content-Type: application/x-www-form-urlencoded\r\n")
    se.write(b"Connection: close\r\n")
    se.write(b"\r\n")
    se.write(send_data)
    se.close()
    led.off()
    servo1.duty(60)
    time.sleep_ms(2000)
    servo1.duty(150)
    time.sleep_ms(2000)

while True:
    # 接続を受け付ける
    # 誰かがアクセスしてきたら、コネクションとアドレスを入れる
    conn, addr = s.accept()
    # byte形式で通信を行う
    # データを受け取る
    request = conn.recv(1024)
    request = str(request)
    led_on = request.find('/?led=on') #o
    led_off = request.find('/?led=off') #o

    if led_on == 6:
        open()
    if led_off == 6:
        close()

    response = web_page()
    conn.send('HTTP/1.1 200 OK\n')
    conn.send('Content-Type: text/html\n')
    conn.send('Connection: close\n\n')
    conn.sendall(response)
    conn.close()
```
</div></details>

# スマホからアクセス
uPyCraftを使用しESP32に書き込めたらスマホブラウザから```http://localhost:80```にアクセスしてみてください．たぶん何かしら表示されるはず．

# 実装
こんな感じで実装しました．


<img width="300" alt="実装例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794860/8000c819-14d7-1a0a-b651-abf62869e64b.jpeg"><img width="300" alt="実装例2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794860/75c89ae8-65f6-70eb-1a57-e093c5f536a3.jpeg">