【Python】Pasori RC-S380で入退室システムをつくる

さてさて、ここのところコンピュータとリアルを融合させたシステムを紹介する記事を書いている流れを受けて、またひとつ前からやってみたいと思っていた内容を思い出しました。

それは、以前見て「これはすごい!」と思った以下の記事が元になっています。

WebUSBでFeliCaの一意なIDであるIDmを読む

この記事は2017年に投稿(更新??)されたようなので少し前のものですが、USB形式の非接触カードリーダでユニークなIDを取得できるとなれば、QRコードを読み取るよりも利便性が高いシステムを作れるんじゃないか!?と考えていました。

例えば、財布を近づけるだけの「ラクラク入退室システム」が簡単につくれるというわけですね。

そして、この間オンラインショップで仕事の備品を購入する際に、このUSBカードリーダーを発見、さらに2,000円台で売っているということで思わずポチってしまいました(100%興味本位です・・・😂)

ということで、今回はせっかくなのでPasori RC-S380を使って簡単な入退室システムをつくってみることにします。

ぜひ皆さんのお役に立てると嬉しいです😊✨

実行環境: Python 2.7, Ubuntu 18.04

コードをつくる前に

元々は冒頭の記事と同じくWebUSBで実装することを考えていましたが、OSに限らずブラウザ(Google Chrome)からUSBにアクセスするには少し複雑な設定をしないといけないようなので、今回はシンプルにPythonのみで実装することにしました。

そして、今回の実装内容は次のとおりです。

  1. ICカード(Felica)を使って入室/退室処理ができる
  2. 整合性がとれない場合(入室、もしくは退室の連続実行)はエラーを出す
  3. 入室/退室した日時はMySQLに保存する

Pasoriの接続

Pasoriの接続は以下のページを参考にさせていただきました。正直なところ途中ハマってしまったので助かりました。ありがとうございます😊✨

Python で PaSoRiのセットアップ と FeliCaの読み取り

ちなみに、もし以下のコマンドを実行して「already used」と表示された場合、port100がすでにLinuxnfcカーネルドライバーによって使われていることが原因です。

sudo python -m nfc

(省略)
found usb:054c:06c3 at usb:001:040 but it’s already used …

そこで、以下のコマンドでドライバーをカーネルから開放してあげましょう。

sudo modprobe -r port100

もしくは、毎回これをやるのがめんどうな場合は以下のコマンドでブラックリストに入れておけるようです。

6sudo 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.pyexit.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つだけです。

  1. Pasoriに接続してカードが近づくのを待機
  2. カードから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は封印ですが、いつかお仕事の依頼があるよう祈っときます(笑)

ではでは〜!

「ご依頼おまちしてます😊✨」