音声強化版PaPeRo i のTinker Board S にソニー製カードリーダーライターPaSoRi RC-S380を接続してNFCカードエミュレーションを行い、AndroidへNDEFデータ送信を行う方法について調べました。
※音声強化版PaPeRo i とAndroidでNDEFデータ通信を行う(NFCリーダーライター)もご参照ください
nfcpyでカードエミュレーション
PythonでNFCを扱うにはnfcpyを利用します。長らくPython2専用だったと思うのですが2019年からPython3に対応していた様です。音声強化版パペロのTinker Board S のデフォルトのPythonにもpip3コマンドで普通にインストールできました。
pip3 install nfcpy
import名はnfcpyではなくnfcです。カードエミュレーションについては公式ページGetting startedのEmulate a cardという章で説明されています。2020/4時点で、
- nfcpyのカードエミュレーションはType3限定
- 対応デバイスも限られるがRC-S380であればType3タグエミュレーションがフルサポートされる(他のリーダライタでは制約がある)
とのことです。処理手順は
(1) NFCカードとしてのデータ用バッファを用意してデータを書き込む
(2) 接続(タッチ)時のコールバック関数を指定して接続待ちAPI(connect)を呼ぶ
(3) 接続コールバック関数内でデータリードコールバック(+データライトコールバック)を指定
(4) ブロック番号指定でリードコールバックが呼ばれる
という流れになっていてサンプルコードも提示されています。読まれるデータを変更しない場合はサンプルコードそのままで良さそうですが、リードコールバックはブロックごとに非同期で呼ばれるため、複数ブロックにまたがるデータを動作中に変更したい場合には排他制御が必要になります。その場合データ更新処理はNFC通信を阻害しない様十分短い時間で終わるようにする必要がありそうです。
NDEFデータの生成
NDEFデータの生成にはnfcpyと同時にインストールされるndeflib(import名ndef)を利用します。例えばテキストレコード一つだけのNDEFメッセージは
lst = [ndef.TextRecord('hello world', 'en', 'UTF-8')]
dat = b''.join(ndef.message_encoder(lst))
で生成でき、これを先ほどのNFCカードとしてのデータ用バッファに書き込めばNFCリーダでNDEFデータとして読めるはずです。
クラス化
上記を組み合わせて、テキストをセットして実行するとNDEFテキストレコードがNFCリーダで読める様になるクラスにしてみました。
card = NdefCardEmu()
card.set_ndef_text('Hello World')
card.run()
という感じで使えます。簡単のため排他制御は入れていないので動作中のデータの書き換えはできません。
ndefcardemu.py
import struct
import time
from logging import (getLogger, basicConfig, Formatter,
DEBUG, INFO, WARN, ERROR, CRITICAL)
import ndef
import nfc
logger = getLogger(__name__)
NDEF_AIB_SIZE_OFS = 11 # 3byte、bigendianでndefデータ長を書く
NDEF_AIB_DATA_OFS = 16 # データオフセット(==AIB長)
class NdefCardEmu:
def __init__(self, dev='usb:054c:06c3', idm='03FEFFE011223344',
pmm='01E0000000FFFF00', sys='12FC', brty='212F',
open_retry_sec=10.0):
self.dev = dev
self.idm = idm
self.pmm = pmm
self.sys = sys
self.brty = brty
self.open_retry_sec = open_retry_sec
self.fin = False
# AIBブロックを書き込む
bs = 128 * 3 * 16
# self.ndef_data_area = bytearray(64 * 16)
self.ndef_data_area = bytearray(bs)
self.ndef_data_area[0] = 0x10 # NDEF mapping version '1.0'
self.ndef_data_area[1] = 12 # Number of blocks that may be read at once
self.ndef_data_area[2] = 8 # Number of blocks that may be written at once
# self.ndef_data_area[4] = 63 # Number of blocks available for NDEF data
self.ndef_data_area[3] = 0xff & ((bs-1) >> 8) # Number of blocks available for NDEF data
self.ndef_data_area[4] = 0xff & ((bs-1) >> 0) # Number of blocks available for NDEF data
self.ndef_data_area[10] = 1 # NDEF read and write operations are allowed
# まだ[11:13]のlengthを書いていない
#cs = sum(self.ndef_data_area[0:14])
#self.ndef_data_area[14:16] = struct.pack('>H', cs) # Checksum
def set_aib_sum(self):
# set aib checksum
cs = sum(self.ndef_data_area[0:14])
self.ndef_data_area[14:16] = struct.pack('>H', cs) # Checksum
def set_aib_length(self, v):
# set aib length
lp = NDEF_AIB_SIZE_OFS
self.ndef_data_area[lp+0] = 0xff & (v >> (8 * (2 - 0)))
self.ndef_data_area[lp+1] = 0xff & (v >> (8 * (2 - 1)))
self.ndef_data_area[lp+2] = 0xff & (v >> (8 * (2 - 2)))
# set aib checksum
self.set_aib_sum()
def ndef_read(self, block_number, rb, re):
logger.debug('block={} rb={} re={}'.format(block_number, rb, re))
if block_number < len(self.ndef_data_area) / 16:
first, last = block_number*16, (block_number+1)*16
block_data = self.ndef_data_area[first:last]
return block_data
def ndef_write(self, block_number, block_data, wb, we):
if block_number < len(self.ndef_data_area) / 16:
first, last = block_number*16, (block_number+1)*16
self.ndef_data_area[first:last] = block_data
return True
def on_startup(self, target):
logger.info('on_startup')
idm, pmm, sys = self.idm, self.pmm, self.sys
target.sensf_res = bytearray.fromhex('01' + idm + pmm + sys)
target.brty = self.brty
return target
def on_connect(self, tag):
logger.info('tag activated')
tag.add_service(0x0009, self.ndef_read, self.ndef_write)
tag.add_service(0x000B, self.ndef_read, lambda: False)
self.on_tag_connected(tag)
return True
def on_tag_connected(self, tag):
# タッチ開始イベント。
# 必要なら派生クラスでオーバーライドしてください
# raise NotImplementedError
pass
def set_raw_data(self, dat):
# set raw data
cnt = len(dat)
ms = len(self.ndef_data_area)
ofs = NDEF_AIB_DATA_OFS
wlen = 0
if (cnt + ofs) >= ms:
logger.error('size over: {} >= {}'.format(cnt + ofs, ms))
self.ndef_data_area[ofs:] = dat[0:ms]
wlen = ms
else:
# logger.info('size: {} < {}'.format(cnt + ofs, ms))
self.ndef_data_area[ofs:ofs+cnt] = dat
wlen = cnt
self.set_aib_length(wlen)
def set_ndef_text(self, v):
msg = None
if isinstance(v, str):
msg = [ndef.TextRecord(v)]
elif isinstance(v, list) or isinstance(v, tuple):
msg = [ndef.TextRecord(x) for x in v]
if msg is not None:
dat = b''.join(ndef.message_encoder(msg))
self.set_raw_data(dat)
return len(dat)
return 0
def quit(self):
self.fin = True
def clf_terminate(self):
return self.fin
def run(self):
logger.info('start')
while not self.fin:
try:
with nfc.ContactlessFrontend(self.dev) as clf:
card = {
'on-startup': self.on_startup,
'on-connect': self.on_connect,
}
while clf.connect(card=card, terminate=self.clf_terminate):
logger.info('tag released')
self.on_tag_released()
if self.fin:
break
except OSError as e:
logger.error(e)
if e.errno == 19:
time.sleep(self.open_retry_sec)
logger.info('retry open.')
else:
break
except Exception as e:
logger.error(e)
break
logger.info('end')
def on_tag_released(self):
# 必要なら派生クラスでオーバーライドしてください
# raise NotImplementedError
pass
def _test():
card = NdefCardEmu()
card.set_ndef_text('Hello World')
card.run()
if __name__ == '__main__':
loglevel = INFO
LOG_FMT = '%(asctime)s %(levelname)s %(thread)d.%(name)s.%(funcName)s %(message)s'
basicConfig(level=loglevel, format=LOG_FMT)
_test()
AndroidでNDEFデータを読む
AndroidでNFCのNDEFデータを読むアプリを作成するにはAndroidManifest.xmlにintent-filterの指定を追加します。これによりNDEFデータが読めたイベントによってアプリが起動される様になります。getNdefText()でテキストが取得できるはずなのですが、先頭に
0x02 0x65(‘e’) 0x6e(‘n’)
という3バイトのデータがついて来てしまいます。これはNDEFテキストレコードのヘッダで先頭の0x02は
- UTF-8
- 後ろに続くlanguage指定が2byte長
ということを意味しておりそのlanguage指定が”en”です。このNDEFテキストレコードのヘッダについてはAndroid側では処理されずアプリレベルで対応する必要があるという事になりますが今回の例で言えば先頭の3バイトを単にスキップすれば十分です。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.ndefread002">
<uses-permission android:name="android.permission.NFC" />
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button clearbtn = findViewById(R.id.button);
clearbtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
TextView tv = (TextView)findViewById(R.id.textView);
tv.setText("");
}
});
Intent intent = getIntent();
String action = intent.getAction();
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)
|| NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
// IDmを表示
//String txt = getIdm(getIntent());
// NDEFデータを表示
String txt = getNdefText(getIntent());
if (txt != null) {
TextView txtview = (TextView) findViewById(R.id.textView);
txtview.setText(txt);
}
}
}
private String getIdm(Intent intent) {
String idm = null;
StringBuffer idmByte = new StringBuffer();
byte[] rawIdm = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
if (rawIdm != null) {
for (int i = 0; i < rawIdm.length; i++) {
idmByte.append(Integer.toHexString(rawIdm[i] & 0xff));
}
idm = idmByte.toString();
}
return idm;
}
private String getNdefText(Intent intent) {
String txt = null;
StringBuffer buf = new StringBuffer();
Parcelable[] dat = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
NdefMessage[] msgs = new NdefMessage[dat.length];
for (int j = 0; j < dat.length; j++) {
msgs[j] = (NdefMessage)dat[j];
for (NdefRecord record : msgs[j].getRecords()) {
String nt = new String(record.getType());
buf.append(String.format("Type : %s\n", nt));
buf.append(String.format("TNF : %s\n", record.getTnf()));
byte[] payload = record.getPayload();
if (payload == null) {
break;
}
if (!nt.equals("T")) {
buf.append(String.format("Length : %d", payload.length));
break;
}
// text type
// テキストの場合先頭に 0x02, 'e', 'n' の様な言語指定がつくがAndroidは対応していないらしい
int idx = 0;
int startp = payload[0];
buf.append("LANG:");
for (byte data : payload) {
buf.append(String.format("%c", data));
if (idx == startp) {
buf.append(String.format("\nText[%d]:\n", payload.length - startp));
}
idx++;
}
}
}
if (buf != null) {
txt = buf.toString();
}
return txt;
}
}
※端末がFelicaに対応している必要があります。Nexus7 (2013)(Android6.0.1)で動作確認を行いました。