Raspberry Pi 2 + PaSoRi RC-S380 + nfcpyにて、FeliCa読み取り時にPowerOffし、Slackへ通知してみた

以前、Raspberry Pi 2 + パソリ RC-S320 + libpafeにて、FeliCa読み取り時にPowerOffし、Slackへ通知したことがありました。

その後、

などがあったため、 nfcpy を使い、同じことを実現したくなりました。

ただ、いくつかハマったところがあったため、メモを残します。

 
目次

環境

  • Raspberry Pi 2 Model B
  • Raspberry Pi OS 32-bit
    • Released 2022-01-28
    • OSインストール先のmicro SD カード
      • KIOXIA KMSDER45N016G
      • 前回と異なり、bootとrootの両方ともmicroSDに入れている
  • PaSoRi RC-S380
  • 任意のFeliCa (Suica/KURURUなど)
  • systemd 247 (247.3-6+rpi1)
    • systemctl --version にて確認
  • Python 3.9.2
  • nfcpy 1.0.4
  • slack_sdk 3.15.2
  • PyCharm Professional Edition

 
なお、以前の記事とは異なり、今回はSlack BotにてSlackへ通知します。

そのため、今回の記事を試す場合の前提として、

  • Bot token を持つアプリを作成
  • アプリの権限に chat:write を設定

というSlack appを用意してあるものとします。
Basic app setup | Slack

 

ラズパイのセットアップ

macにて、Raspberry Pi Imager にてOSをmicroSDに書き込む

最近のラズパイでは Raspberry Pi Imager を使ってOSをmicroSDに書き込むのが良さそうです。
https://www.raspberrypi.com/software/

今回はOSとして、 Raspberry Pi OS (32-bit) 、バージョンは Released 2022-01-28 を使用します。

なお、 Raspberry Pi Imager 実行後はmacからmicroSDが取り外されてしまいます。

ただ、次の作業を行うため、microSDを抜き差しして、macmicroSDを認識させておきます。

Finderの 場所boot と表示されていればOKです。

 

macにて、SSHを可能にするファイルを作成する

以下のコマンドでSSHを可能にするためのファイルを作成します。

% touch /Volumes/boot/ssh

 
これでmac上でのmicroSDに対する作業は完了したため、macからmicroSDを抜きます。

 

macからラズパイへSSH接続

microSDをラズパイに挿入してから電源を入れ、まずはパスワード認証でSSHできることを確認します。

% ssh pi@raspberrypi.local
The authenticity of host 'raspberrypi.local (***:***:***:***:***:***:***:***)' can't be established.
ECDSA key fingerprint is SHA256:***.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'raspberrypi.local,***:***:***:***:***:***:***:***' (ECDSA) to the list of known hosts.
pi@raspberrypi.local's password: 
Linux raspberrypi 5.10.92-v7+ #1514 SMP Mon Jan 17 17:36:39 GMT 2022 armv7l
...
pi@raspberrypi:~ $

 

ラズパイのログインを公開鍵認証でも可能にする

毎回パスワード認証するのは手間なので、公開鍵認証でもログインできるように設定します。

セキュリティなどを考えると公開鍵認証のみに絞ることも考えられますが、今回は試すだけなので併用する形でOKとしておきます。

 

macにて、SSH接続用のSSH鍵を生成する

macのターミナルでSSH鍵を生成します。今回はパスフレースなしでOKとします。

% ssh-keygen -t rsa -b 4096 -f ~/.ssh/pi_felica_rsa

Enter passphrase (empty for no passphrase): <そのままEnter(パスフレースなし)>

 

SSHの公開鍵をラズパイに登録

ssh-copy-id を使って、macからラズパイへ登録します。

% ssh-copy-id -i ~/.ssh/pi_felica_rsa.pub pi@raspberrypi.local

/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "~/.ssh/pi_felica_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
pi@raspberrypi.local's password: 

Number of key(s) added:        1

Now try logging into the machine, with:   "ssh 'pi@raspberrypi.local'"
and check to make sure that only the key(s) you wanted were added.

 

公開鍵でラズパイへSSHする

以下でログインできればOKです。

ssh -i ~/.ssh/pi_felica_rsa pi@raspberrypi.local

 

vimを入れる

インストールされているのは vim-tiny なため、ふつうのvimに差し替えます。 RaspberryPi3のセットアップ続き〜VimやNFS設定 - Qiita

# 確認
$ dpkg -l | grep vim
ii  vim-common  2:8.2.2434-3+deb11u1
ii  vim-tiny    2:8.2.2434-3+deb11u1

# アンインストール
$ sudo apt-get --purge remove vim-common vim-tiny

# インストール
$ sudo apt-get install vim

 

ラズパイを固定IP化する

以前の通り、ラズパイを固定IP化します。
Python2 + Scapyで、Raspberry Pi 2 Model B をブリッジにできるか試してみた #router_jisaku - メモ的な思考的な

# 対象のファイルを開く
$ vi /etc/dhcpcd.conf


# 以下を末尾に追加
interface eth0
static ip_address=192.168.0.52/24
static routers=192.168.0.1
static domain_name_servers=192.168.0.1


# 有効化
$ sudo service dhcpcd reload
sending signal HUP to pid 393

 
しばらく待つと、ラズパイでの入力受け付けるようになり、固定IP化されます。

 

Pythonスクリプトの作成

今回の開発方法について

今回は、PyCharmのremote sdk機能を使い

という形で開発してみます。

 

ラズパイのPythonバージョンを確認

以前は2系でしたが、現在は3.9系でした。

# ラズパイ上で確認
$ python --version
Python 3.9.2

 

ラズパイのプロジェクトディレクトリを作成

今回使うPythonスクリプトは以下のディレクトリに入れるため、作成しておきます。

/home/pi/projects/rpi_felica_poweroff_with_nfcpy

 

PyCharmの SSH Interpreter まわりの設定を追加し、動作確認

今回、PyCharmmの SSH Interpreter 機能を使って、PyCharmでラズパイ上のファイルを動かしながら開発を行います。

まずはSSH Interpreter 機能が動作するかを確認します。

 

確認用ファイルを用意

ローカルの任意のディレクトリをPyCharmのプロジェクトルートとして、 <Project root>/src/main.py ファイルを確認用ファイルとして作成します。

main.py の実装は以下です。ラズパイ上で実行できれば Linux と表示されるはずです。

import platform

print(platform.system())

 

PyCharmの設定を追加

3箇所設定を追加します。

 

PyCharmで SSH Configurations を追加

まずはPyCharmにSSH設定を追加します。

  • メニューから Preference > Tools > SSH Configurations を開く
  • 左上の + をクリック
  • 以下を入力
    • Host: 192.168.0.52
    • User name: pi
    • Authentication type: Key pair
    • Private key file: 上記で生成したSSH秘密鍵(ディレクトリより選択)
  • Test Connection ボタンをクリックし、接続が成功すればOK

 

PyCharmに SSH Interpreter を追加

続いて、PyCharmからラズパイのPythonインタプリタを扱えるよう、設定を追加します。

  • メニューから Preference > Project > Python Interpreter を開く
  • 右の歯車マークから Add を選択
  • 左側で SSH Interperter を選択し、右側で Existing server configuration を選択、上記で作成したSSH設定を指定
    • 今回の例だと pi@192.168.0.52:22
  • Next をクリック
  • 以下を設定し、 Finish をクリック
    • Interpreter: /usr/bin/python (デフォルトのまま)
      • 今回はシステムのPythonを使うため
    • Sync folders
      • Local Path: /path/to/rpi_felica_poweroff_with_nfcpy/src
        • プロジェクトルートの下に作った src ディレクトリを指定
      • Remote Path: /home/pi/projects/rpi_felica_poweroff_with_nfcpy
    • Authmatically upload project file to the server にチェックを入れる

 

実行設定を追加

最後に、PyCharmから実行するときの設定を追加します。

  • メニューから Run > Edit Configurations をクリック
  • 左上の + をクリックし、 Python を選択
  • 以下を入力し、OK をクリック
    • Name: 任意
    • Script path: mac上の src/main.py を選択
    • Python interpreter: 上記で作成した Remote *** を選択
  • PyCharmのindexingが走るので、しばらく待つ

 

動作確認

実行ボタンをクリックすると、以下が表示されることを確認します。

ssh://pi@192.168.0.52:22/usr/bin/python -u /home/pi/projects/rpi_felica_poweroff_with_nfcpy/main.py
Linux

 
macのターミナルで実行した時と結果が異なるため、ラズパイ上で実行できていることが確認できました。

% python src/main.py 
Darwin

 

udevのrulesファイルを作成し、PaSoRi RC-S380をラズパイで扱えるようにする

rulesファイルの置き場所について

以前の記事では /lib/udev/rules.d/ の下に置いていました。

ただ、今回nfcpyを使ったところ、 /etc/udev/rules.d/ の下に置いても動作したため、 /etc の方にします。

 

PaSoRiの idVendor と idProduct を確認

以前と同じく確認します。

$ dmesg | grep usb
...
[    4.363804] usb 1-1.2: New USB device found, idVendor=054c, idProduct=06c3, bcdDevice= 1.11
...
[    4.363875] usb 1-1.2: Product: RC-S380/P
[    4.363903] usb 1-1.2: Manufacturer: SONY
[    4.363924] usb 1-1.2: SerialNumber: 0514577
...

 

udevのrulesファイルを作成

nfcpyからPaSoRiを扱えるよう、以前 PaSoRi RC-S320を使ったときのrulesファイルを流用して作成します。
Raspberry Pi 2 + systemd + udevで、USBデバイス挿入時にサービスを起動する - メモ的な思考的な

 
sudo vi /etc/udev/rules.d/90-rc-s380.rules として、以下を設定します。

SUBSYSTEM=="usb", ACTION=="add", ATTRS{idProduct}=="06c3", ATTRS{idVendor}=="054c", GROUP="plugdev", TAG+="systemd", ENV{SYSTEMD_WANTS}+="rc-s380.service", NAME="pasori380"

 
変更点としては、

  • GROUPusb から plugdev へと変更
    • nfcpyのドキュメントに「the device is owned by 'root' but you are 'stephen'. also members of the 'root' group would be permitted. you could use 'sudo' but this is not recommended. it's better to add the device to the 'plugdev' group」とあったため
    • 念のため、 groups pi したところ、 pi : pi adm dialout cdrom sudo audio video plugdev games users input render netdev spi i2c gpio lpadmin だったので、 plugdev で大丈夫そう
  • SYSTEMD_WANTS は後ほど作成するサービスの名前へと変更
  • NAME も、とりあえず変更

です。

 
sudo reboot で再起動した後にSSHでログインしてみると、PaSoRiが認識されていました。

$ systemctl --type device -a --full
  UNIT                        LOAD   ACTIVE   SUB     DESCRIPTION
  dev-bus-usb-001-004.device  loaded active   plugged RC-S380
...

 

nfcpyでFeliCaを読み込む

ラズパイ上で nfcpy をインストールします。

$ pip install nfcpy

 
先ほど用意した main.py を以下のように書き換えます。

import binascii
import nfc

def on_connect(tag):
    idm = binascii.hexlify(tag._nfcid).decode('utf-8')
    print(idm)

    # ラズパイのACT LEDをheartbeatにする
    cmd = shlex.split("echo heartbeat")
    cmd_redirect = shlex.split("sudo tee /sys/class/leds/led0/trigger")
    
    p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    p2 = subprocess.Popen(cmd_redirect, stdin=p1.stdout)
    p1.stdout.close()
    p2.communicate()[0]

if __name__ == "__main__":
    print('読み取りを開始します')
    with nfc.ContactlessFrontend('usb:054c:06c3') as cf:
        cf.connect(rdwr={'on-connect': on_connect})

 
main.py をラズパイに転送した後に実行すると、

ssh://pi@192.168.0.52:22/usr/bin/python -u /home/pi/projects/rpi_felica_poweroff_with_nfcpy/main.py
読み取りを開始します

で停止します。

その状態でPaSoRiFeliCaをタッチすると

011***

のように、FeliCaIDmが表示されます。

また、ラズパイのACT LEDがheartbeatになります。

 

systemdのserviceファイルを作成する

以前同様、systemdでmain.pyをサービスとして動作させるようにします。

serviceファイルは以前のものを流用し、 sudo vi /etc/systemd/system/rc-s380.service で作成します。
Raspberry Pi 2 + パソリ RC-S320 + libpafeにて、FeliCa読み取り時にPowerOffし、Slackへ通知する - メモ的な思考的な

[Unit]
Description=Pasori RC-S380 service
Requires=dev-bus-usb-001-004.device
After=dev-bus-usb-001-004.device

[Service]
Type=simple
User=pi
Restart=on-failure
RestartSec=5
ExecStart=/usr/bin/python /home/pi/projects/rpi_felica_poweroff_with_nfcpy/main.py

 
変更点としては

です。

 

サービスの動作確認

sudo reboot で再起動後、 sytemctl でサービスの状態を確認します。動作しているようです。

$ systemctl status rc-s380.service
● rc-s380.service - Pasori RC-S380 service
     Loaded: loaded (/etc/systemd/system/rc-s380.service; static)
     Active: active (running) since Mon 2022-03-28 07:17:34 BST; 1min 47s ago
   Main PID: 322 (python)
      Tasks: 2 (limit: 1597)
        CPU: 6.196s
     CGroup: /system.slice/rc-s380.service
             └─322 /usr/bin/python /home/pi/projects/rpi_felica_poweroff_with_nfcpy/main.py

 
この状態でPaSoRiFeliCa接触させると、ラズパイのACT LEDがheartbeatになります。

 

Slackへの通知とシャットダウン

通知に使うライブラリについて

以前は os/slacker を使って通知していました。
https://github.com/os/slacker

しかし、現在ではアーカイブされています。

 
ただ、slackerのREADMEからSlack公式の python-slack-sdk へのリンクが張られているため、今回は python-slack-sdk を使ってみます。

 
上記記事では WebClient を使っていましたが、asyncioに対応したものがないのかを調べたところ、別の記事にてpython-slack-sdkでは対応している旨の記載がありました。
asyncio アプリ内から Slack にメッセージを投稿しよう - Qiita

せっかくなので、今回はasyncio版を試してみることにします。

まず、ラズパイ上で必要なものをインストールします。

なお、 python-dotenv は必須ではありませんが、Slackのトークンを .env ファイルから読み込むようにするために使っています。

$ pip install slack_sdk aiohttp python-dotenv

 

asyncio版のslack_sdkで通知とシャットダウンを実装

上記の記事に従い、slack_sdkでの通知を実装します。また、合わせてシャットダウン機能も追加します。

追加後の実装は以下のとおりです。

import binascii
import os
import shlex
import subprocess

import asyncio
import nfc
from dotenv import load_dotenv
from slack_sdk.web.async_client import AsyncWebClient


async def post():
    client = AsyncWebClient(os.environ['SLACK_BOT_TOKEN'])
    response = await client.chat_postMessage(
        channel=os.environ['DESTINATION'],
        text=':zzz:',
    )
    print(response)

def on_connect(tag):
    idm = binascii.hexlify(tag._nfcid).decode('utf-8')
    print(idm)

    # ラズパイのACT LEDをheartbeatにする
    cmd = shlex.split("echo heartbeat")
    cmd_redirect = shlex.split("sudo tee /sys/class/leds/led0/trigger")

    p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    p2 = subprocess.Popen(cmd_redirect, stdin=p1.stdout)
    p1.stdout.close()
    p2.communicate()[0]

    # Slackにポストする
    asyncio.run(post())

    # シャットダウン
    cmd_power_off = shlex.split("sudo systemctl poweroff")
    subprocess.Popen(cmd_power_off)


if __name__ == "__main__":
    load_dotenv()

    print('読み取りを開始します')
    with nfc.ContactlessFrontend('usb:054c:06c3') as cf:
        cf.connect(rdwr={'on-connect': on_connect})

 
なお、 .env ファイルから

を読み込むことから、 /src/.env として環境に合わせて設定します。

SLACK_BOT_TOKEN=
DESTINATION=

 
一通りの準備ができたので、

  • mac上の <プロジェクトルート>/src 以下をラズパイへデプロイ
  • ラズパイの再起動

をしておきます。

 

動作確認

すべての準備が整ったので、全体を通した動作確認をします。

まずはラズパイにSSH接続し、サービスが起動していることを確認します。

# macからSSH
% ssh -i ~/.ssh/pi_felica_rsa pi@raspberrypi.local


# ラズパイ上でサービスの動作確認 (動作中)
$ systemctl status rc-s380.service
● rc-s380.service - Pasori RC-S380 service
     Loaded: loaded (/etc/systemd/system/rc-s380.service; static)
     Active: active (running) since Mon 2022-03-28 07:45:45 BST; 5min ago
   Main PID: 321 (python)
      Tasks: 2 (limit: 1597)
        CPU: 19.731s
     CGroup: /system.slice/rc-s380.service
             └─321 /usr/bin/python /home/pi/projects/rpi_felica_poweroff_with_nfcpy/main.py

 
PaSoRiFeliCaをタッチすると、

  • ラズパイのACT LEDをheartbeatになる
  • 指定したチャンネル(ユーザーのDM)へSlackへ :zzz: のemojiが通知される
  • シャットダウンする

が行われます。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/rpi_felica_poweroff_with_nfcpy