🚰

NFCタグを活用して水を楽に買いたい

に公開

作ったもの

https://youtu.be/YAMeGrKGMuA

リポジトリ

https://github.com/seyamasan/NFCTagWriter.git

経緯

「やばい、水がなくなりそうAmazonで頼まないと」とか思ってるのにいつの間にかベッドの上。

NFCタグにスマホをかざすだけで商品ページに飛びたい。

よし遊んでみよう。

NFCとは

NFCNear Field Communicationの略で、近距離無線通信の規格です。

その名の通り、NFCを内蔵したスマホとタグなどをピッタリくっつけることで、データ通信を行うことができます。

キャッシュレス決済などを想像すると分かりやすいです。

今回はタグにURLを書き込んで、スマホをかざすことで商品ページが開く仕組みを作る。

買ったもの

こちらのNFCタグを購入しました。

NTAG215と商品名に書いてありますが、これはチップの型番らしくNTAG215は504バイト書き込むことができるようです。

今回は、URL(38バイト)を書き込みたいので十分そう。

実装

今回はAndroid端末で試したいのでKotlinで開発します。

権限

AndroidManifestには、NFCを使うための権限が必要です。

スマホのNFCハードウェアへアクセスするためのもの。

AndroidManifest.xml
<uses-permission android:name="android.permission.NFC" />

NfcAdpter

調べていくうちに見つけたのがこれ。

端末の NFC アダプター(コントローラ)を操作するためのクラスです。

NfcTagWriterScreen.kt
var nfcAdapter: NfcAdapter? = null

https://developer.android.com/reference/android/nfc/NfcAdapter

デフォルトのNFCアダプターを取得

そしてこれ。

getDefaultAdapter関数を呼び出すことで、デフォルトの NFC アダプターを取得することができます。

NfcTagWriterScreen.kt
nfcAdapter = NfcAdapter.getDefaultAdapter(activity)

NFC アダプターがない場合はnullを返されるので念のため。

設定で「NFCの使用」をON/OFF試してみたけど関係なさそう。

この時点でnullが返ってきたら使えないということで。

NfcTagWriterScreen.kt
if (nfcAdapter == null) {
    Toast.makeText(
        activity,
        "この端末はNFC非対応です。",
        Toast.LENGTH_SHORT
    ).show()
}

リーダーモードに制限

読み取り/書き込みとしてのみ動作させたいので、enableReaderMode関数を使います。

NfcTagWriterScreen.kt
val readerCallback = NfcAdapter.ReaderCallback { tag: Tag? ->
    if (tag == null) return@ReaderCallback

    // ここで書き込み処理を書く予定
}

// NTAG215を使う予定なので、Type A のフラグを使う
val flag = NfcAdapter.FLAG_READER_NFC_A

// NFC アダプターをリーダーモードに制限する
nfcAdapter?.enableReaderMode(activity, readerCallback, flag, null)

こちらが引数です。

引数 説明
activity Activityを渡せばいい。
callback 読み取りできた時にやりたい処理を渡せばいい。
flags NFC の種類を渡せばいい。
extras リーダーモード設定用の追加オプション。今回は特に必要ないのでnullを渡す。

NTAG215は、Type A の仕様に完全に準拠するように設計されています。

なのでFLAG_READER_NFC_Aflagsに渡している。

リーダーモードを元に戻す

リーダーモードを元に戻したいときは、disableReaderMode関数を使います。

NfcTagWriterScreen.kt
// リーダーモードを元に戻す
nfcAdapter?.disableReaderMode(activity)

NDEF

NDEF(NFC データ交換形式)は、型付きデータをカプセル化するために使用される軽量バイナリフォーマットです。

NFC フォーラムによって NFC による送信と保存のために仕様化されている。

データフォーマットは、後述する NdefRecord と NdefMessage で実装されています。

NdefRecord

NDEFレコードはデータであり、後述するNDEFメッセージを構成するために必要です。

NDEFレコードには、URIなどの型指定されたデータが含まれます。

https://developer.android.com/reference/android/nfc/NdefRecord

createUri

今回はURLを書き込みたいので、 createUri 関数を使ってURIのレコードを作成する。

inputText はString型であり、テキストフィールドから入力したもの

NfcTagWriterScreen.kt
// URIレコードを作成
val uriRecord = NdefRecord.createUri(inputText.toUri())

NdefMessage

NDEFメッセージは、1つ以上のNDEFレコードを持っているコンテナです。

https://developer.android.com/reference/android/nfc/NdefMessage

NDEFメッセージ作成

このようにコンストラクタを呼び出すことで、 NdefMessage のインスタンスを生成できる。

NfcTagWriterScreen.kt
// URIレコードを作成
val message = NdefMessage(arrayOf(uriRecord))

書き込み処理

NdefRecord と NdefMessage を作り終えたので、次は書き込みの処理。

NDEF のクラスがあるので、タグ上の NdefMessage を上書きする。

https://developer.android.com/reference/android/nfc/tech/Ndef

get

get 関数があるので前述した NfcAdapter.ReaderCallback で得たタグの NDEF を取得する。

タグが NDEF 形式でないか、 NDEF 形式だけど Android デバイスが実装していない仕様の場合は、 null が返される。

NfcTagWriterScreen.kt
val readerCallback = NfcAdapter.ReaderCallback { tag: Tag? ->
    if (tag == null) return@ReaderCallback

    try {
        val ndef = Ndef.get(tag)
    } catch (e: Exception) {
    }
}

connect

NDEF への 入力/出力 操作を有効にします。

NfcTagWriterScreen.kt
ndef.connect()

writeNdefMessage

NDEF の NDEFメッセージを上書きします。

NfcTagWriterScreen.kt
ndef.writeNdefMessage(message)

close

NDEF への 入力/出力 操作を無効にし、リソースを解放します。

NfcTagWriterScreen.kt
ndef.close()

書き込み処理の全体像

こちらが書き込み処理の全体像です。

あとは、Android端末をかざしてNFCタグを読み取るだけ。

NfcTagWriterScreen.kt
val readerCallback = NfcAdapter.ReaderCallback { tag: Tag? ->
    if (tag == null || !isWritingUrlToTag) return@ReaderCallback

    try {
        // URIレコードを作成
        val uriRecord = NdefRecord.createUri(inputText.toUri())
        val message = NdefMessage(arrayOf(uriRecord))

        // 読み込んだタグのNDEFを取得
        val ndef = Ndef.get(tag)

        if (ndef != null) {
            ndef.connect()

            // 書き込み可能かチェック
            if (!ndef.isWritable) {
                ndef.close()

                // UIスレッドで表示する
                activity.runOnUiThread {
                    Toast.makeText(
                        activity,
                        "タグが書き込み禁止です。",
                        Toast.LENGTH_SHORT
                    ).show()
                }

                return@ReaderCallback
            }

            // 容量チェック
            val needed = message.toByteArray().size
            val max = ndef.maxSize
            if (max < needed) {
                ndef.close()

                activity.runOnUiThread {
                    Toast.makeText(
                        activity,
                        "容量不足: 必要 $needed / 上限 $max バイト",
                        Toast.LENGTH_SHORT
                    ).show()
                }

                return@ReaderCallback
            }

            // 書き込み
            ndef.writeNdefMessage(message)
            ndef.close()

            activity.runOnUiThread {
                Toast.makeText(
                    activity,
                    "URLを書き込みました。",
                    Toast.LENGTH_SHORT
                ).show()
            }
        } else {
            activity.runOnUiThread {
                Toast.makeText(
                    activity,
                    "書き込みできませんでした。",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    } catch (e: Exception) {
        activity.runOnUiThread {
            Toast.makeText(
                activity,
                "書き込みエラー: ${e.message ?: ""}",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
}

inputText はString型であり、テキストフィールドから入力したもの
isWritingUrlToTag はBoolean型であり、書き込みを制御するために作ったもの

まとめ

NFCタグを読み取って水の購入ページを開くことができた!

普通に NFC Tools アプリ使って書き込んだらええやん案件ですが、

興味から勉強していくのは楽しいですね。

これで水を買うのが楽しくなりそう。

参考にした記事

https://developer.android.com/develop/connectivity/nfc/nfc?hl=ja

https://developer.android.com/reference/android/nfc/NfcAdapter

https://developer.android.com/reference/android/nfc/tech/Ndef

https://developer.android.com/reference/android/nfc/NdefRecord

https://developer.android.com/reference/android/nfc/NdefMessage

https://www.nxp.jp/products/NTAG213_215_216

https://qiita.com/takagimeow/items/48b37c55ad8d73d5da88

https://engawapg.net/jetpack-compose/1946/lifecycleeventobserver/

Discussion