かぴぱらの日記

かぴぱらの雑貨置き場です。バイク(Honda Hornet250)や一眼(Nikon D5600)でいろいろしたものを載せていきます。晴れ時々電子工作… 某田舎の高専生だったりします。

NFCで認証するプリペイド型決済システムを作ってみる③ -Ver2.00リリースと Python で データベース 接続 のお話-

②→NFCで認証するプリペイド型決済システムを作ってみる② - かぴぱらの日記
①→NFCで認証するプリペイド型決済システムを作ってみる - かぴぱらの日記

こんちには。
長らく更新していなかったこの企画,田舎高専が夏休みに入ったので,一段落するまで開発しちゃいました!

とりあえずまずは,今回実装できたところと次回の予定を紹介していきます。
今回実装できたところ
CUIで一通りの操作ができるように実装しました。
・リモートデータベースを使いました。

今後作りたいところ
GUIの実装
・SlackAPIとの連携
・筐体もつくってみたい
・ユーザー削除/カード削除とかの面倒くさそうな機能

コードはこちらに置いてあります。
POSっぽいので,POS-System(possys)と名付けました。
github.com
基本的にMITライセンスしてますが,もし万が一何かの間違いで商用利用したいという方がおられましたら,一言ご連絡ください。
possys ver2.00を運用するのに必要なものは,以下の3つです。

  • Raspberry Pi 3b
  • SONY NFCリーダー(②で紹介したものです)
  • MySQLサーバー(後述の設定を変えればローカル/リモートは問いません)

ローカルデータベースを使うのは面倒くさかったので,最初からリモートデータベースにしてます。現行バージョンはVer.2.00です。Ver.1.00はあってないようなものです。

また,このプログラムは製作者がゼミ室で活用することを前提として開発しているので,かなり性善説に頼っているところがあります。SQLがちょっとできる人なら簡単に攻撃できちゃいます。

それでは,データベース周りの開発日記を連々と書いていきます。
当初は,DjangoのORMラップ機能を使ってラッピングして実装する予定でしたが,学習コストが高そうだったので,生SQLに甘えてしまいました。
使ったライブラリはこちら。
MySQL :: MySQL Connector/Python Developer Guide :: 10 Connector/Python API Reference

pip3 install mysql-connector-python

でインストールできます。
possysからコピーしてきただけですが,単純にデータベースを使いたいだけなら次のコードでできます。

import mysql.connector

db = mysql.connector.connect(host = 'hostname',
                                              user = 'username',
                                              password = 'password',
                                              database = 'database'
                                             )
cursor = db.cursor()
cursor.execute("hogeeeeeee")    # hogeの部分はSQL文
tmp = cursor.fetchall()    # SQL文に一致したものを全件取得
# db.commit()    # 返り血がないSQL文にはこっちを使う

cursor.close()
db.close()

hostnameのところに,データベースのホストネーム,ユーザーのところに……とやっていくことで任意のデータベースに接続できます。possysでは,データベースは1個しか用いてないので,データベース名も指定していますが,複数のデータベースを用いる際にはなくても大丈夫です。(それくらい複雑になるならORMラッパーを使った方が良いですが……)
また,fetchallした際の値はlist型で帰ってくるため,単純に1足したいとかの時には,2次元配列のような扱いをする必要があります。
これを前回のと組み合わせて,IDmがデータベースにあるか確認するプログラムを作るとこのようになります。

#! /usr/bin/python
#! -*- coding:utf-8 -*-
# Python3で動くよ!

# configファイル
import configparser
# 時間取得用
import time
# データベースアクセス
import mysql.connector
# IDm取得用のライブラリ(Python2をサブプロセスで実行)
import subprocess
import os
import sys

# データベース操作クラス
class Database:
    def __init__(self):
        # configファイルを参照
        config = configparser.SafeConfigParser()
        config.read('setting.ini')

        # データベースを参照
        # 各値はconfigファイルのDATABASEセクションから取得
        self.db = mysql.connector.connect(host     = config.get('DATABASE','hostname'),
                                          user     = config.get('DATABASE','username'),
                                          password = config.get('DATABASE','password'),
                                          database = config.get('DATABASE','databaseName')
                                         )
        # データベースとの,対話クラスのインスタンスを作成
        self.cursor = self.db.cursor()
        print("[  OK  ]: Establish database connection")

    # IDm照合処理
    def checkIDm(self, userIDm):
        try:
            print("[START ]: check NFC IDm...")
            # NFCIDテーブルから条件付き全件取得
            # executeで実行コマンドを指定,fetchallで一致データすべてを取得
            self.cursor.execute("SELECT IDm  FROM NFCID WHERE IDm='%s'"%str(userIDm))   # 関数内はSQL文
            serverData = self.cursor.fetchall()  # 取得データ代入
            print("[  OK  ]: Got server side IDm data")
            # 重複データがあっても,[0][0]にはとりあえず取得データがある
            # ない場合,list型の範囲外参照エラーが起きるのでexceptで拾ってあげる
            try:
                if str(serverData[0][0]) == str(userIDm):
                    return True
            except:
                return False

        except:
            self.cursor.close()
            self.db.close()
            print("[ERROR ]: Database Connection ERROR!")
            return False
            
    # ユーザ追加
    def addUser(self):
        try:
            cond = True
            print("[START ]: add User...")
            while cond:
                print("\n新規ユーザー登録を行います。")
                print("UserName:")
                name = input(">> ")
                print("EmailAddress:")
                mail = input(">> ")
                print("\nYour input data:")
                print("UserName:" + name)
                print("EmailAddress:" + mail)
                print("\nConfirm? [y/n]\n(nothing default, only [y/n])")
                confirm = None
                confirm = input(">> ")
                cond = False
                if(confirm == 'n'):
                    cond = True
                elif(confirm == 'y'):
                    break
                else:
                    print("Plz only input y/n or Nothing!!!\n")
                    cond = True
                
            # MemberListテーブルからMemberNum最大値取得
            # SQL文の意味は,「MemberNumのデータが欲しい,MemberListから,次の条件に一致するもの → (MemberNumが,MemberNumカラムの中で最大値のとき,そのカラムはMemberListにあるよ)」
            self.cursor.execute("SELECT MemberNum FROM MemberList WHERE MemberNum=(SELECT MAX(MemberNum) FROM MemberList)")  # 関数内はSQL文
            newMemberNum = self.cursor.fetchall()  # 取得データ代入
            newMemberNum = newMemberNum[0][0] + 1
            print(newMemberNum)
            print("[  OK  ]: Got most new MemberNum")
            # 新規ユーザデータをデータベースへ入力
            self.cursor.execute("INSERT INTO MemberList (MemberNum, Name, Email, wallet) VALUES ('%d','%s','%s',0)"%(newMemberNum, name, mail)) # 関数内はSQL文 変数はタブタプ
            self.db.commit()    # SQL文をデータベースへ送信(返り血はないのでcommitメソッド)
            print("[  OK  ]: Add new user")

        except:
            self.cursor.close()
            self.db.close()
            print("[ERROR ]: Database Connection ERROR!")
            return False

class idmRead:
    def __init__(self):
        pass
    
    def getMain(self):
        print("[START ]: Getting NFC card IDm...")
        command = "python2 idmRead.py"      # 同一ディレクトリ内のidm取得プログラムをpython2で実行
        # サブシステムでcommandを実行,stringに変換してスペースでスプリット
        output = str(subprocess.check_output(command.split()))
        temp = output.split()
        flag = 0
        for tag in temp:
            if flag == 1:
                # 「hogehoge\n'」と取得できるので,後ろから3字消去
                tag = tag[:-3]
                flag = 0
                print("[  OK  ]: Got your cards IDm")
                return(tag)
            # 「IDm」の後にスペースを置いてIDmが来るようにしてあるので,フラグ付けて次ループで回収
            if tag.find("IDm=") is not -1:
                flag = 1

temp = Database()
temp2 = idmRead()
test = temp2.getMain()
print(temp.checkIDm(test))

残念ながら,nfcpyはPython2でしか動作しないので,IDm取得はサブプロセスからPython2でidmRead.pyを呼んでいます。IDmを取得する処理は前回紹介したので割愛させて頂きますが,コードはこちらに置いてあります。
github.com
masterブランチには既にないですが,各ブランチのtestCode/内には迷走具合が記されています……。
idmRead.pyとsetting.iniはそちらに置いてあるので,もし動かしたい方がいたら使ってくださいね!

コーディングで疲弊したので今日はこの辺で締めます。
ここまで読んで頂いてありがとうございました!