
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、ここのところコンピュータとリアルを融合させたシステムを紹介する記事を書いている流れを受けて、またひとつ前からやってみたいと思っていた内容を思い出しました。
それは、以前見て「これはすごい!」と思った以下の記事が元になっています。
この記事は2017年に投稿(更新??)されたようなので少し前のものですが、USB形式の非接触カードリーダでユニークなIDを取得できるとなれば、QRコードを読み取るよりも利便性が高いシステムを作れるんじゃないか!?と考えていました。
例えば、財布を近づけるだけの「ラクラク入退室システム」が簡単につくれるというわけですね。
そして、この間オンラインショップで仕事の備品を購入する際に、このUSBカードリーダーを発見、さらに2,000円台で売っているということで思わずポチってしまいました(100%興味本位です・・・)
ということで、今回はせっかくなのでPasori RC-S380
を使って簡単な入退室システムをつくってみることにします。
ぜひ皆さんのお役に立てると嬉しいです
実行環境: Python 2.7, Ubuntu 18.04
コードをつくる前に
元々は冒頭の記事と同じくWebUSB
で実装することを考えていましたが、OSに限らずブラウザ(Google Chrome)からUSBにアクセスするには少し複雑な設定をしないといけないようなので、今回はシンプルにPythonのみで実装することにしました。
そして、今回の実装内容は次のとおりです。
- ICカード(Felica)を使って入室/退室処理ができる
- 整合性がとれない場合(入室、もしくは退室の連続実行)はエラーを出す
- 入室/退室した日時はMySQLに保存する
Pasoriの接続
Pasori
の接続は以下のページを参考にさせていただきました。正直なところ途中ハマってしまったので助かりました。ありがとうございます
Python で PaSoRiのセットアップ と FeliCaの読み取り
ちなみに、もし以下のコマンドを実行して「already used」と表示された場合、port100
がすでにLinux
のnfc
カーネルドライバーによって使われていることが原因です。
sudo python -m nfc
(省略)
found usb:054c:06c3 at usb:001:040 but it’s already used …
そこで、以下のコマンドでドライバーをカーネルから開放してあげましょう。
sudo modprobe -r port100
もしくは、毎回これをやるのがめんどうな場合は以下のコマンドでブラックリストに入れておけるようです。
sudo sh -c 'echo blacklist port100 >> /etc/modprobe.d/blacklist-nfc.conf'
そして、最終的に次のように表示されれば完了です。
(省略)
** found SONY RC-S380/P NFC Port-100 v1.11 at usb:001:042
パッケージのインストール
nfcpy
は先ほどのページでインストールしましたが、MySQL
にアクセスするにはmysql-connector
も必要ですのでpip
でインストールしておきましょう。
pip install mysql-connector
データベースの準備
必要になるデータベースは以下の3テーブルです。
- users ・・・ ユーザー情報(ここに事前にカードのIDを登録しておく)
- room_entries ・・・ 入室データ(いつ誰が入室したかが分かる)
- room_exits ・・・ 退室データ(いつだれが退室したかが分かる)
今回はPython
の記事ですが、テーブルの作成はいつも使い慣れたLaravel
が便利なのでマイグレーションを作って実行します。
(usersテーブル)
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('nfc_idm')->nullable();
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
そしてusers
テーブルには次のようなテストユーザーを作っておきます。テストするときにはnfc_idm
を実際のカードIDへ変更して実行します。
(room_entriesテーブル)
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateRoomEntriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('room_entries', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->dateTime('entered_at');
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('room_entries');
}
}
(room_exitsテーブル)
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateRoomExitsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('room_exits', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->dateTime('exited_at');
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('room_exits');
}
}
実際のソースコード
では、Pasori
から入退室処理をするコードを書いていきましょう。
今回は「入室」と「退室」の2パターンがあるので、独自クラスを作ってentry.py
とexit.py
からそれぞれ呼び出すようにしましょう。
まずは独自クラスentry_exit.py
です。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import nfc
import binascii
import mysql.connector
import datetime
import signal
class manager:
NFC_READER_ID = 'usb:054c:06c3' # Pasori-380
DB_HOST = 'DB_HOST'
DB_DATABASE = 'DB_DATABASE'
DB_USERNAME = 'DB_USER'
DB_PASSWORD = 'DB_PASSWORD'
db = None
mode = None
def __init__(self, mode):
self.mode = mode
self.connect_to_mysql()
try :
while True:
print("\n" + '--- 終了する場合は「Ctrl + C」を押し続けてください ---')
print('ICカードを近づけてください。')
clf = nfc.ContactlessFrontend(self.NFC_READER_ID)
clf.connect(rdwr={'on-connect': self.on_nfc_connect})
clf.close()
except:
print('[Error] Pasoriにアクセスできません。')
exit()
# nfc
def on_nfc_connect(self, tag):
idm = binascii.hexlify(tag.idm)
user = self.get_user(idm)
if user == None:
print('[Error] ユーザーが見つかりません。')
return True
mode_text = self.get_mode_text()
now = self.get_now()
self.insert_data(user)
return True
# DB
def connect_to_mysql(self):
try:
self.db = mysql.connector.connect(
host=self.DB_HOST,
database=self.DB_DATABASE,
user=self.DB_USERNAME,
passwd=self.DB_PASSWORD
)
except:
print('[Error] DB接続失敗。')
exit()
def insert_data(self, user):
user_id = user[0]
user_name = user[1].encode(encoding='UTF-8')
mode_text = self.get_mode_text()
if self.is_duplicated(user_id) == True:
print('すでに【%s処理】は完了しています。' % (mode_text))
return True
now = self.get_now()
query = ''
if self.mode == 'entry':
query = 'INSERT INTO room_entries (user_id, entered_at) VALUES("%s", "%s")' % (user_id, now)
elif self.mode == 'exit':
query = 'INSERT INTO room_exits (user_id, exited_at) VALUES("%s", "%s")' % (user_id, now)
cursor = self.db.cursor()
result = cursor.execute(query)
self.db.commit()
print(now +': %s さんの【%s処理】が完了しました。' % (user_name, mode_text))
def is_duplicated(self, user_id):
if self.mode == 'entry':
query = 'SELECT entered_at FROM room_entries WHERE user_id = "%s" ORDER BY entered_at DESC LIMIT 1' % (user_id)
elif self.mode == 'exit':
query = 'SELECT exited_at FROM room_exits WHERE user_id = "%s" ORDER BY exited_at DESC LIMIT 1' % (user_id)
cursor = self.db.cursor()
cursor.execute(query)
result = cursor.fetchone()
if result == None:
return False
last_accessed_at = result[0].strftime("%Y-%m-%d %H:%M:%S")
if self.mode == 'entry':
query = 'SELECT COUNT(id) FROM room_exits WHERE user_id = "%s" AND exited_at > "%s"' % (user_id, last_accessed_at)
elif self.mode == 'exit':
query = 'SELECT COUNT(id) FROM room_entries WHERE user_id = "%s" AND entered_at > "%s"' % (user_id, last_accessed_at)
cursor.execute(query)
result = cursor.fetchone()
data_count = int(result[0])
return (data_count == 0)
# Getter
def get_user(self, idm):
query = 'SELECT id, name FROM users WHERE nfc_idm = "%s"' % (idm)
cursor = self.db.cursor()
cursor.execute(query)
return cursor.fetchone()
def get_mode_text(self):
if self.mode == 'entry':
return '入室'
elif self.mode == 'exit':
return '退出'
return None
def get_now(self):
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
コードの中でやっていることは以下の2つだけです。
- Pasoriに接続してカードが近づくのを待機
- カードからIDが取得できたらデータベースを参照して入室/退室処理をする
重要なのは、入室 or 退出処理が完了した時点でプログラムまで終了してしまうと、そのたびにコード実行しないといけないため、while
文で永遠に処理を続けている部分です。(そのため、コードを終了するにはCtrl+C
を長押しする必要があります)
クラスの次は実行ファイルです。
・・・とはいっても、ほとんどクラスにコードを記述しているのでそれぞれ2行だけになります。
(entry.py)
import entry_exit
entry_exit.manager('entry')
(exit.py)
import entry_exit
entry_exit.manager('exit')
テストしてみる
では、実際にプログラムを動かしてみましょう。
実行コマンドはプログラムのあるフォルダに移動してsudo python entry.py
です。
うまく起動できると以下のように待機中のメッセージが表示されます。
では、この状態でPasori
にカードを近づけてみましょう。
はい!上手くDBにデータを書き込むことができました。
では、この状態でもう一度カードを近づけてみましょう。
退室する前に2度めの入室処理をしたので、エラーが表示されます。
では、退室の方もやってみます。
はい、今度は入室→退室の流れなので上手く処理ができました。
お疲れ様でした!
おわりに
今回久しぶりにPython
のコードを書くことになりましたが、やっぱりPython
はシンプルでいいですね。ただ、たまにいちいちインデントを動かしてテストしないといけなかったりする部分はちょっと・・・(汗)とはなりますけど
また、実は当初windows 10でもテストしていたのですが、その際に時間の流れを感じる表示がありました。それは、「Python 2.7は2020年1月でメンテンナンス期間が終わる」というものです。とはいえ、結構Python 2
っていろんな場所に深く食い込んでるからUbuntu
とかも全面的にPython 3
に移行できるのかちょっと疑問だったりもします。
とにもかくにも、今回はPasori
を使って入退室システムを作ってみました。
データの保管にMySQL
を使っているので、ウェブとの連携をしたい場合はAjaxで一定時間ごとにDBにアクセスして、入室/退室データが入ってきたらJavaScript
でページ変更するという流れにすれば、よりリアルタイムに近いシステム統合ができるんじゃないでしょうか。(もしくはPusher
とかですね)
ということで、これでまず間違いなくPasori
は封印ですが、いつかお仕事の依頼があるよう祈っときます(笑)
ではでは〜!
「ご依頼おまちしてます」