🐾

NFCタグをAndroid単体で、JavaScriptのみで読み取ろうとしたお話

2024/01/12に公開

最近(?)の某アパレルショップでの自動レジはお買い物カゴに入れたまま決算ができます。
割と前からそんな感じですが、バーコードでレジを通すわけではなく電波を使って近距離通信してレジを通しているからできるらしい。

ところでAndroid限定ではあるものの、JavaScriptでその通信を行うWebAPIがあるらしいので、実際にAndroid端末単体で読み取ってみた。そんな記事。
(ちなみに、実際に読み込みを試みたが、某アパレルショップのタグはスマフォ対応していないらしい)

この記事では以下の項目に分けて書いていきます。ネットを漁ると、もっと組み込み系のバイナリ列やらのディープな話題も見つかるけど、ここではJavaScriptをつかって、Android単体で、簡単に触ってみる程度の内容です。
Android単体なのでデバイスを買うとかしなくてよい。JavaScriptのみなのでブラウザ上で完結できる。その範囲内のみのお話です。あしからず

  1. 近距離通信(NFC)
  2. NFCで使われるプロトコル
  3. NFCを呼び出すためのJS機能
  4. 実際に読み込んでみる

1. 近距離通信(NFC)

NFCはNear Field Communicationの頭文字をとったもの。要するに"すぐ近くの場で通信"のこと。(ざっくりニュアンス翻訳)
レジかごの範囲での近距離内のみで通信してお会計を自動化しているというわけです。

他にも身近な使用例ではSuicaとかワオンとか、Visaのタッチで決済するアレでも使われてたりする。地味なところだと運転免許証にもついてたりする。

と、お財布や身分証として使っていることもあり、危ないのでJavaScript単体で使える機能は制限されています。
実際にその辺にあったSuicaを読み取ろうとしてもエラー吐かれました。他にもワオンとnanacoと運転免許証でもエラー。せちがらい。

じゃぁ何に使えるんだよ。せめてタグで一意になるシリアル番号は見せてくれよ。とは思うのですが、みられないものはしょうがない。
仕様書によると、リーダー、要は端末側からエラーが送出された時に起こるとのこと。セキュリティが必要なものに関しては端末側からブロックされてしまうようですね。
(一意になる番号さえわかれば、カラオケの機械みたくかざしてログインとかそういう機能作れそうで楽しそうだったのに)

2. NFCで使われるプロトコル

さて、微妙に使えるような使えないような。そんな微妙な気分になったのだが、気を取り直してNFCでどんなデータを通信できるかみていこう。
NFCで使われるプロトコルとして、NDEFというものがある。
NFC Data Exchange Formatの頭文字をとって、NDEF。
このプロトコルではデータがNDEFであることを示すプレフィックスの他には配列めいた形で通信できます。
Mozillaによると実際にJSで受け取るデータ構造はこんな感じ。

{
	serealNumber: "タグごとの一意の番号",
	message: [
		{
			data: "転送されるデータ",
			id: "レコードの開発者が定義する識別子",
			encoding: "レコードのエンコーディング",
			lang: "言語タグ", 
			mediaType: "MIMEタイプ",
			recordType: "dataに格納されているデータの種類を表す文字列"
		}
	]
}

このオブジェクトにのっとりメッセージのやり取りを行うようです。

タグ1個ごとにシリアル番号が振られて、そのタグ内でmessageが配列としてたくさん入っているって感じ。なるほどぉ。

3. NFCを呼び出すためのJS機能

さて、先のセクションではNDEFで読み取るメッセージであるNDEFMessageオブジェクトを見ました。メッセージがあるということは、読み取ってこのメッセージを返すイベントがあるわけです。

NDEF読み取りイベントを用意するメソッドはこんな感じ。NuxtもといVueで実装したのがこれ

async click_scanstart(){
     try{
        this.rfctest="mounted"
        const reader=new NDEFReader()

        await reader.scan()

        reader.addEventListener("readingerror", (e)=>{this.rfctest+=`エラー: type: ${e.type}`})

        reader.addEventListener("reading", (e)=>{
            const ndefmessage=e.message
            this.rfctest+=`Succeed: ${e.serialNumber}`
                   
            this.rfctest+=` length: ${ndefmessage.records.length}`
                    
            for(record of ndefmessage.records){
                 const record=ndefmessage.records[0]
                 this.rfctest+=`\n{recodtype: ${record.recordtype}, mediatype: ${record.mediatype}, id: ${record.id}, data: ${record.data}, encoding: ${record.encoding}, lang: ${record.lng}}\n`
            }
        })
    }catch(e){
        console.log("CATCH",e)
        this.rfctest+=`catch-${e}`
    }
}

この関数内3行目でreaderとしてWeb APIのNDEFReaderを用意してあげて、await reader.scan()でRFC読み込みを開始します。

RFCが読み込まれた、あるいは読み込みエラーが発生した時、このreaderでのイベントが発火するので続くreader.addEventListenerで発火した時の処理をかいていきます。

スキャン成功時の"reading"イベントでは、イベントオブジェクトeからメッセージe.messageを読み出して表示している変数に追記してあげるだけの単純なコード。

reader.addEventListener("reading", (e)=>{
	取得メッセージを追記する処理
}

スキャン失敗時にはエラーであることを変数についいしてあげているだけ。簡単だね。

reader.addEventListener("readingerror",(e)=>{
	this.rfctest+=`エラー: type: ${e.type}`
}

4. 実際に読み込んでみる

実際に読み込めるようにデプロイしたサイトがこれ

Androidで開いて、画面に表示されている文字Start_Scanをクリックすると読み込み許可を求めるダイアログが出てきます。
ユーザーの操作なしに勝手に読み取りが発生しないようになっているわけです。えらいね

もしもこのダイアログで拒否すると、reader自体がエラーを返すのでtry~catchの部分でNotAllowedErrorが創出される。これで許可されなかった時の分岐ができると。

パーミッションを許可し直して、その辺にあったSuicaを読み取ってみました。うん。Errorが発生して読み込めないみたい。これはAndroid本体から情報をブロックされているらしい。ざんねん。

Androidからのパーミッションを回避できるようなタグを用意する必要があります。その辺の野良NFCタグを読み込んでみるとこんな感じ。

なにやらデータが取れている。データ自体は何も入っていないようだ。

シリアル番号は取得できているので、なにかかざしてログイン的な方法としては使えそうであるが...Suicaとか身近にあるもので取得できないのがとても痛い。そのうち対応してくれることを願ってこの記事はおしまい。

Discussion