Edited at

ラズパイとSuicaで玄関を開ける

More than 1 year has passed since last update.

この記事は、おうちハック Advent Calendar 2017の14日目の記事です。

タイトルに見覚えのある方もいるかもしれません。

2年前のおうちハック Advent Calendar 2015の23日目に書かれた、Coro氏による玄関をSuicaで開けるパクりインスパイア的な記事です。

Coro氏が作成したものを参考に少し機能を加えました。


仕組み

なんでわざわざVPS上で認証しているのか。という話ですが、その方がクラウド()な感じがしてカッコいいからです。

認証するスピードがかなり落ちそうな気がしますが、実際には思ったほど落ちませんでした。

最後の方に説明しますが、VPS上で認証させるのはメリットが少ない(ほぼない)です。


仕様について


  • SuicaやICOCAなどの交通系ICカード、学生証などをカードキーとして使用する

  • 施錠はリードスイッチを用いてドアの開閉を検知してオートロックにする

  • 認証はカードのidmとデータベース(今回はMySQL)を使って行う

  • ブラウザからアクセスできる管理画面を作成して、開錠履歴、ユーザ登録などを行えるようにする


使ったもの

ハード面


  • RaspberryPi3 Model B

  • PaSoRi(RC-S380)

  • サーボモータ(GWS03N)

  • タクトスイッチ(TVGP01-G73BB)

  • リードスイッチ(MC-14AG)

  • ブレッドボード

  • 抵抗

ソフト面


  • Python(プログラム全般)

  • MySQL(ユーザ管理)

  • Apache(管理画面で使用)

  • PHP(管理画面で使用)

また、以下のPythonライブラリを使用しました。

nfcpy - GitHub

WiringPi-Python - GitHub


ラズパイとパーツの接続

分かりにくくてすみません...


ソフト面(プログラム)

制御のためのプログラムは主に3つ書きました。

1.idmを調べる用

2.カードリーダ解錠用

3.タクトスイッチ解錠用

1は実際の制御には使いません。

2と3についてはまとめられそうですが、わからなかったので分けて実装しました。

とても無駄が多いクソコードなので、こう書いた方がスマートだよってあれば教えてください。


MySQLでテーブルを作成する

テーブルは2種類(ユーザ管理と開錠履歴)作成しました。

mysql> create database 'door_lock';

mysql> use 'door_lock';
mysql> create table 'member'(
-> 'id' int(100) NOT NULL AUTO_INCREMENT,
-> 'idm' varchar(16) NOT NULL,
-> 'name' varchar(50) NOT NULL,
-> 'mail' varchar(50) NOT NULL,
-> 'type' varchar(20) NOT NULL
-> );
mysql> create table 'access_log'(
-> 'idm' varchar(16) NOT NULL,
-> 'time' timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP
-> );
mysql> desc member;
+-------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+----------------+
| id | int(100) | NO | | NULL | auto_increment |
| idm | varchar(16) | NO | | NULL | |
| name | varchar(50) | NO | | NULL | |
| mail | varchar(50) | NO | | NULL | |
| type | varchar(100) | NO | | NULL | |
+-------+--------------+------+-----+---------+----------------+
mysql> desc access_log;
+-------+--------------+------+-----+---------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------------------+-----------------------------+
| idm | varchar(16) | NO | | NULL | |
| time | timestamp | NO | | NULL | on update CURRENT_TIMESTAMP |
+-------+--------------+------+-----+---------------------+-----------------------------+

ツッコミどころ満載なDBだと思いますが初心者なのでお許しください><

ユーザ管理用のテーブルに入力する内容は以下の通りです。

項目
内容

id
1~100(auto_increment)

idm
16桁のidm

name
利用者名

mail
利用者メールアドレス

type
カードの種類(SUICA,ICOCAなど)

認証のキーとなるidmを平文で格納することを考えて16桁に設定していますが、プログラム側でハッシュ計算させてハッシュ値で認証する方がセキュリティ的にはよさそうです。

今回は平文で格納しています。

開錠履歴のテーブルにはプログラムから利用者のidmと解錠時間を追加していきます。


1.idmを調べる用


idm.py

# -*- coding: utf-8 -*-

import nfc
import binascii

#カードを読み取り
def connected(tag):
global idm
idm = binascii.hexlify(tag.idm)

clf = nfc.ContactlessFrontend('usb')
clf.connect(rdwr={'on-connect': connected}) # now touch a tag
clf.close()

#IDmを表示
print idm


認証のキーにはidmを使います。idmはカードに付けられた固有のものです。

セキュリティ的にはidmを使うのはよくないって何かの記事で見たことありますが...


2.カードリーダ解錠用


door_lock.py

#coding:utf-8

import nfc
import binascii
import RPi.GPIO as GPIO
import time
import MySQLdb
import wiringpi

#リードスイッチの設定
readswitch_pin = 3
wiringpi.wiringPiSetupGpio()

#施錠解錠処理
def check():
c = connect.cursor()
sql = "select * from member"
c.execute(sql)
for row in c.fetchall():
if(row[1] == idm):
print "Unlock!"
unlock()
log()
print "Wait..."
while True:
if( wiringpi.digitalRead(readswitch_pin) == 0 ):
time.sleep(5)
lock()
break
else:
None
print "Lock!"
c.close()

#ログ記録
def log():
c = connect.cursor()
sql = "insert into access_log values (%s,%s)"
c.execute(sql,(idm,None))
connect.commit()
c.close()

#解錠
def unlock():
GPIO.setmode(GPIO.BCM)
gp_out = 18
GPIO.setup(gp_out, GPIO.OUT)
servo = GPIO.PWM(gp_out, 50)
servo.start(0.0)
servo.ChangeDutyCycle(7.0)
time.sleep(0.5)
GPIO.cleanup()

#施錠
def lock():
GPIO.setmode(GPIO.BCM)
gp_out = 18
GPIO.setup(gp_out, GPIO.OUT)
servo = GPIO.PWM(gp_out, 50)
servo.start(0.0)
servo.ChangeDutyCycle(2.5)
time.sleep(0.5)
GPIO.cleanup()

#カード読み取り
def read():
def connected(tag):
global idm
idm = binascii.hexlify(tag.idm)
clf = nfc.ContactlessFrontend('usb')
clf.connect(rdwr={'on-connect': connected}) # now touch a tag
clf.close()

print "Weclome back."
while True:
print "Please touch a card."

#MySQL接続
connect = MySQLdb.connect(user="user",passwd="password",host="your host",db="felica")
read()
check()
connect.close()


データベースに接続して認証を行います。

ドアクローザー(玄関のドアの上についてるやつ)の調整によっては、リードスイッチが反応するくらい近づいているが、サムターンを回しても鍵は閉められないなどの状況も考えられるのでスイッチが反応してもすぐには閉まらないように5秒時間を挟んでいます。


3.タクトスイッチ解錠用


button.py

#coding:utf-8

import nfc
import binascii
import RPi.GPIO as GPIO
import time
import wiringpi

button_pin = 2
readswitch_pin = 3
wiringpi.wiringPiSetupGpio()
wiringpi.pinMode( button_pin, 0 )
wiringpi.pullUpDnControl( button_pin, 2 )
wiringpi.pullUpDnControl( button_pin, 3 )

#解錠
def unlock():
GPIO.setmode(GPIO.BCM)
gp_out = 18
GPIO.setup(gp_out, GPIO.OUT)
servo = GPIO.PWM(gp_out, 50)
servo.start(0.0)
servo.ChangeDutyCycle(7.0)
time.sleep(0.5)
GPIO.cleanup()

#施錠
def lock():
GPIO.setmode(GPIO.BCM)
gp_out = 18
GPIO.setup(gp_out, GPIO.OUT)
servo = GPIO.PWM(gp_out, 50)
servo.start(0.0)
servo.ChangeDutyCycle(2.5)
time.sleep(0.5)
GPIO.cleanup()

while True:
#ボタンが押されたら解錠する
if( wiringpi.digitalRead(button_pin) == 0 ):
None
else:
unlock()
time.sleep(5)
while True:
#ドアが閉まったら施錠する
if( wiringpi.digitalRead(readswitch_pin) == 0 ):
time.sleep(5)
lock()
break
else:
None


玄関の内側はサーボモータが邪魔でサムターンが使えなくなっているので、出る時はボタンで解錠します。

ちなみに鍵で解錠して家に入った場合もオートロックにならないので、手動でボタンを押して施錠する必要があります。まあこの辺の問題はリードスイッチで開閉検知してロックすることもできそうですね。


Webによる管理画面

ログイン関係の実装についてはこちらの記事にお世話になりました。ありがとうございます。

管理画面は処理部分が完成していないので、スクリーンショットだけ貼っておきます。

Bootstrap臭さがヤバい管理画面ができました。

ログイン画面

解錠履歴画面

ここらへんの知識がなさすぎて、ナウい実装ができませんでした。

フレームワークとか使えるようになりたいですね。


ハード面(工作)

完成するまでの作業工程を撮影するのを忘れていました。(撮影するほどのことしてない)

適当に木材を買ってきて適当に調整してボンドでペタペタしました。

ちなみに作業時間は3~4時間程でプログラムを書くよりも時間がかかりました。

実際にドアに取り付けた様子がこちらです。

(※写真はタクトスイッチを付ける前のものなので、写真では付いてません。)



めちゃくちゃにダサいですね...3Dプリンターとかで作ればもっとスマートなのができると思います。

サーボモータとサムターンの間には、ターンクリップを挟んでいます。モータとサムターンがしっかり固定しないとズレが生じてちゃんと動作しません。

これが外側

うわあ盗られそう><


色々な問題

実際に運用してみて色々な問題が見つかりました。

本格導入はまだまだ遠そうです。


停電した時どうするの?

ラズパイは自宅のコンセントから給電されて動いています。

もしも落雷による停電やコンセントが抜けちゃうなど、何らかの理由でラズパイが動かなくなった時はおしまいです。(ちなみに鍵があれば開けることができる)

停電はUPSなどを利用することによって対策できそうです。

ラズパイだとモバイルバッテリーとかで代用できそうですがどうなんでしょう。


ネットワーク障害発生した時どうするの?

認証にはさくらVPS上に構築したデータベースを使用しています。

なのでVPS側に障害が発生した場合や、ラズパイを繋いでいるWi-Fiが切れるなどネットワーク関係に障害が発生した場合も開けることができなくなります。

そもそもデータベースで認証させないで、プログラム上で認証させる仕様にすればネットワーク関連の問題は解決できます。

あとはラズパイ上のデータベースに認証させるのも手ですね。


NFCカードリーダーがパクられた時どうするの?

Coro氏の場合は家の内側に設置していましたが、私の家の場合はドアの形状から内側に設置することができませんでした。

玄関の外側にカードリーダーを強力両面テープで貼り付けて使っていましたが、パクられたり線を切られたりすると認証できなくなります。

最悪、玄関前だとWi-Fiに繋がると思うのでスマホにSSHクライアントを入れてラズパイに接続して手動でプログラムを動かすと開けることができます。


マンションだったらキーレスにできないよね?

マンションの場合は大抵エントランスのオートロックを解除するのに、部屋の玄関を開ける時の鍵が必要です。

なのでマンションでこのおうちハックを実装しても、エントランスのオートロックを解錠するために結局鍵を持ち歩く必要があります。

ちなみに私の家はマンションです。


最後に

ここまで読んで頂いてありがとうございます。

APIを作成してAkerunのように鍵をシェアしたり、家族分カードを登録してLINENotifyで誰が何時に帰宅したかを通知したりなどまだまだ機能追加できそうですね。

明日は、@tokutoku393さんの記事です。


参考記事/元ネタ

玄関をSuicaで開ける

Raspberry-PiにおけるGPIO関係ツールのインストール方法