Python3でnfcpyのタイムアウト処理を扱う (Windows対応)
2020-01-16
azblob://2022/11/11/eyecatch/2020-01-16-handle-timeout-with-nfcpy-python3-000.jpg

Python3.x 系で nfcpy が動作するようになったので、以前の記事のバッドプラクティスを打ち消すべくスレッドによるタイムアウトのコンテキストマネージャを使ってコードを書き直してみました。

環境

  • Windows 10 64bit
  • Python 3.7.3
  • nfcpy 1.0.3
  • PaSoRi RC-S380

事前準備

以前の記事を参考に Python からFelicaリーダーを操作できるようにします。
WindowsでNFCタグを扱うには? (Windows nfcpy FeliCa)
nfcpy のインストールが初めてなら、最新バージョンを入れるだけで Python3.x 系でも nfcReader.py は動きます。

nfcReader.pyにタイムアウト処理を追加する

nfcpy で NFCタグを読み込もうとする場合、タグが読み込まれるまで待機するため、タイムアウト手段を別に用意する必要があります。 contextlib.contextmanager というデコレーター にリソースの開放を任せ、 pythonapi にある PyThreadState_SetAsyncExc を使ってスレッドの割り込みを行い、タイムアウト処理を実現します。

タイムアウト処理に関しては、
@Jacomb 氏の記事 【python】コードブロックにタイムアウトを付けようと試行錯誤する話
を参考に作成いたしました。この場を借りて御礼申し上げます。
詳細は上記記事をご覧ください。

#!/usr/bin/python3

import nfc
import binascii
import errno
import contextlib
import ctypes
import threading


class TimeoutException(IOError):
    errno = errno.EINTR

@contextlib.contextmanager
def time_limit_with_thread(timeout_secs):
    thread_id = ctypes.c_ulong(threading.get_ident())
    def raise_exception():
        modified_thread_state_nums :int = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(TimeoutException))
        if modified_thread_state_nums == 0:
            raise ValueError('Invalid thread id. thread_id:{}'.format(thread_id))
        elif modified_thread_state_nums > 1:
            # Normally do not go through this path, but clear unthrown Exceptions just in case
            ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, None)
            raise SystemError('PyThreadState_SetAsyncExc failure.')

    timer = threading.Timer(timeout_secs, raise_exception)
    timer.setDaemon(True)
    timer.start()
    try:
        yield
    finally:
        timer.cancel()
        timer.join()

def nfc_reader():
    clf = nfc.ContactlessFrontend('usb')
    print('please touch card:')
    try:
        tag = clf.connect(rdwr={'on-connect': lambda tag: False})
    finally:
        clf.close()
    if tag is False:
        print('Cannot read card')
    else:
        idm = binascii.hexlify(tag.idm)
        print(idm)
        print('please released card')

if __name__=='__main__':
    timeout_secs = 3
    with time_limit_with_thread(timeout_secs):
        try:
            nfc_reader()
        except TimeoutException:
            print('timeout.')

実行して3秒以内にNFCタグをタッチ出来ない場合は自動で処理が終了します。

> python3 nfcread.py
please touch card:
Cannot read card

タイムアウトしても timeout. が表示されない (TimeoutException が送出されていない)原因は不明です。
尚、今回のスレッドを使った方法の他に通知イベントを送出する方法も試してみましたが、残念ながらタイムアウトは実現できませんでした。