WebNFC で遊びたいがゆえに、Google Pixel 3a XL を買いました。
今回は、WebNFC の動作を確認できたので、そんなまとめです。
最終的にこんなものができました。
音が流れます。ご注意ください。
目次
参考
WebNFC 有効化の方法
WebNFC は、現在デフォルトで有効な機能ではありません。
有効にする必要があります。
以下の手順で、有効化しましょう。
- Chrome を起動して、
chrome://flags
を開く。
Experimental Web Platform feature
の項目を有効化する。
- Chrome を再起動する。
設定した終わった、chrome://flags
の画面は、以下のようになります。
実装 1(動作確認)
先に挙げたCommunity Group Draft Reportは、サンプルコードが豊富です。
16 種類あるうちから、いくつかピックアップして参考にしながら触ってみます。
準備
Android の開発者モード、Chrome 開発ツールを介して localhost で立ち上げた、web サーバーにアクセスさせます。
まず、localhost で起動する web サーバーを用意します。
以下の通り実行します。
1 2 3 4 5
| npm init -y npm install http-server --save-dev mkdir public npx http-server
|
続いて、Android で開発マシンのlocalhost:8080
を参照できるようにします。
Android の開発者モードを有効化します。
詳しい手順はこちらを参照。
デバイスの開発者向けオプションを設定する
Android を USB で接続し、web サーバーを起動するマシンで Chrome を起動します。
開発者ツールを開き、More tools
->Remote devices
を開きます。
USB で接続した端末が見えるので、inspect
を押します
USB で接続した端末の画面が見えます。
ぱっと見タブレットモードで開いているだけに見えますが、手元の端末と連動しています。
そちらを操作してみましょう。
ここまで出来たら、public
以下に、html ファイルと js ファイルを作成します。
具体的な記述は、次の項目からです。
シンプルに読み込み
まずは、シンプルに NFC タグに書かれた内容を読み込んで、表示してみます。
public/test0.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous" ></script> <script src="app0.js"></script> </head> <body> <div id="message"></div> <button id="startbutton" onclick="scanStart()">Scan Start</button> </body> </html>
|
public/app0.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const reader = new NDEFReader();
const scanstart = async () => { await reader.scan();
reader.onerror = (event) => { $("#message").text("ERROR"); }; reader.onreading = (event) => { $("#message").empty();
$("#message").append(event.serialNumber).append("</br>"); $("#message").append(event.timeStamp).append("</br>"); $("#message").append(event.type).append("</br>"); console.log(event); }; };
scanstart();
|
こちらを動作させると、次のようになります。
それぞれのシリアルコードとタイムスタンプがそれぞれのカードで別のものを取得できています。
読み込みの停止
先の実装だと、一度scan()
を実行すると、リロードでもしない限り本体側に NFC の読み取り機能を返すことができません。
今度は、NFC タグへの読み込みを停止してみます。
一度読み込んだら、5 秒後に読み込みを解除するようにしてみます。
public/test1.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous" ></script> <script src="app1.js"></script> </head> <body> <div id="message"></div> <button id="startbutton" onclick="scanStart()">Scan Start</button> <button onclick="scanEnd()">Scan End</button> </body> </html>
|
public/app1.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| const reader = new NDEFReader();
let controller = null; let count = 5;
const scanStart = async () => { if (controller) { return; } controller = new AbortController();
await reader.scan({ signal: controller.signal });
reader.onerror = (event) => { $("#message").text("Error!"); }; reader.onreading = (event) => { $("#message").empty();
$("#message").append("Readed!").append("</br>"); $("#message").append(event.serialNumber).append("</br>"); $("#message").append(event.timeStamp).append("</br>"); $("#message").append(event.type).append("</br>"); console.log(event); };
$("#message").text("Device is Ready!");
setTimeout(scanEnd, 5000);
$("#startbutton").text(`${count}s`); countdown = setInterval(() => { count = count - 1; $("#startbutton").text(`${count}s`); }, 1000);
setTimeout(() => { clearInterval(countdown); count = 5; $("#startbutton").text("Scan Start"); }, 5100); };
const scanEnd = () => { if (!controller) { return; } controller.abort(); controller = null; $("#message").text("Device is Not Ready!"); };
|
Scan End
ボタンも用意したので、任意に停止もできます。
こちらを動作させると、次のようになります。
アプリで読み取りを開始し、カウントダウンが終わると本体側の NFC が反応するようになります。
シンプルに書き込み
ここまで読み込みをできたので、今度は書き込みをしてみます。
試しに、このブログの URL を書き込んでみます。
public/test2.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous" ></script> <script src="app2.js"></script> </head> <body> <div id="message"></div> <button id="startbutton" onclick="writeStart()">Write Start</button> </body> </html>
|
public/app2.js1 2 3 4 5 6 7 8 9 10 11 12 13
| const writer = new NDEFWriter();
const writeStart = async () => { try { await writer.write({ records: [{ recordType: "url", data: "https://www.google.com/" }], }); $("#message").text("Write Success"); } catch (error) { $("#message").text("Error!"); } };
|
こちらを動作させると、次のようになります。
書き込んだ瞬間に、本体側の NFC デバイスが、読み取りしてしまいました。
書き込み後に NFC 制御をデバイス本体にとられないようにする
先の動画のように、書き込んだ後すぐには本体側の NFC デバイスが反応してしまうので、バタバタした動きになります。
scan()
を実行し、NFC デバイスを Chrome で掌握したまま書き込んでみます。
今回は、ページを開いたら、常にscan()
を実行させます。
public/test3.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!DOCTYPE html> <html> <head> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous" ></script> <script src="app3.js"></script> </head> <body> <div id="message"></div> <button id="startbutton" onclick="writeStart()">Write Start</button> </body> </html>
|
public/app3.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const writer = new NDEFWriter(); const reader = new NDEFReader();
const writeStart = async () => { try { await writer.write({ records: [{ recordType: "url", data: "https://ccbaxy.xyz/blog/" }], }); $("#message").text("Write Success"); } catch (error) { $("#message").text("Error!"); } };
reader.scan();
|
こちらを動作させると、次のようになります。
アプリを起動すると、書き込んだ後も読み取りしなくなりました。
Chrome を閉じると、本体側の NFC デバイスが読み取りを行っています。
実装 2(アプリケーション試作)
ここまでで、NFC タグへの読み書きができるようになりました。
もう少し進めて、音と画面に動きをつけてみます。
とりあえずこんなものができました。
音が流れます。ご注意ください。
実装は以下の通りです。
読み込みアプリ
public/tatobareader.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css" /> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous" ></script> <script src="reader.js"></script> </head>
<body> <section class="hero"> <div class="container is-fullwidth"> <div class="hero-body"> <div class="container"> <h1 class="title">Tatoba Reader</h1> </div> </div> </div> <div class="container is-fullwidth"> <div class="columns is-mobile" id="message"></div> </div> </section> <section class="section"> <div class="container is-fullwidth" id="images"> <img src="img/blank.jpg" class="is-fullwidth" /> <img src="img/blank.jpg" class="is-fullwidth" /> <img src="img/blank.jpg" class="is-fullwidth" /> </div> </section>
<section class="section"> <div class="container is-fullwidth"> <div class="columns is-mobile"> <button class="button is-large is-success is-fullwidth" id="startbutton" onclick="scanStart()" > Scanstart </button> <button class="button is-large is-black is-fullwidth" id="endbutton" onclick="scanEnd()" > Scan Stop </button> </div> </div> </section> <section> <div class="container is-fullwidth"> <a class="button is-large is-link is-fullwidth" href="/tatobawriter" >Tatoba Writer</a > </div> </section> </body> </html>
|
public/reader.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| const reader = new NDEFReader(); let controller = null; const lorded_list = ["blank", "blank", "blank"];
const music_list = ["taka", "tora", "batta", "blank", "tatoba"]; const music_objects = [];
const init_music = () => { music_list.forEach((title) => { const m_obj = new Audio(); m_obj.preload = "auto"; m_obj.src = `./music/${title}.mp3`; m_obj.load(); music_objects.push(m_obj); }); };
const scanStart = async () => { if (controller) { return; } controller = new AbortController();
await reader.scan({ signal: controller.signal });
reader.onerror = () => { $("#message").text("Error!"); music_objects[music_list.findIndex((item) => item === "blank")].play(); }; reader.onreading = (event) => { $("#message").empty(); $("#images").empty();
if (event.message.records[0] == null) { $("#message").html( `<div class="notification is-danger is-fullwidth"> <h3 class="title is-4">ERROR</h3> </div>` ); music_objects[music_list.findIndex((item) => item === "blank")].play(); lorded_list.push("blank"); } else { const { data, mediaType, recordType } = event.message.records[0];
if (!(recordType === "mime" && mediaType === "application/json")) { $("#message").html( `<div class="notification is-danger is-fullwidth"> <h3 class="title is-4">ERROR</h3> </div>` ); music_objects[music_list.findIndex((item) => item === "blank")].play(); lorded_list.push("blank"); } else { $("#message").html( `<div class="notification is-primary is-fullwidth"> <h3 class="title is-4">Loaded</h3> </div>` ); const decoder = new TextDecoder(); const json = JSON.parse(decoder.decode(data));
music_objects[ music_list.findIndex((item) => item === json.element) ].play();
lorded_list.push(json.element); } } if (lorded_list.length > 3) { lorded_list.shift(); } $("#images").empty(); $("#images").html( `<img class="lazy" src="img/${convertFileName( lorded_list[0] )}" class="is-fullwidth" /> <img class="lazy" src="img/${convertFileName( lorded_list[1] )}" class="is-fullwidth" /> <img class="lazy" src="img/${convertFileName( lorded_list[2] )}" class="is-fullwidth" />` ); if ( lorded_list[0] === "taka" && lorded_list[1] === "tora" && lorded_list[2] === "batta" ) { setTimeout(() => { music_objects[music_list.findIndex((item) => item === "tatoba")].play(); }, 1200); } }; $("#message").html( `<div class="notification is-primary is-fullwidth"> <h3 class="title is-4">Device is Ready</h3> </div>` ); };
const convertFileName = (element) => { if (element == "taka" || element == "tora" || element == "batta") { return `${element}.jpg`; } return "blank.jpg"; };
const scanEnd = () => { if (!controller) { return; } controller.abort(); controller = null; $("#message").html( `<div class="notification is-primary is-fullwidth"> <h3 class="title is-4">Device is Not Ready</h3> </div>` ); };
init_music();
|
対応したデータを持ったタグが正しい順番で読み込まれると、専用音声を再生します。
画像と音源は公式の音源を使うわけにもいかず、フリー素材と自前で歌ったものを加工して使用しました。
使用させていただいた素材はこちらです。
画像
ブザー音
書き込みアプリ
最初はシリアルコードで見分けようとも考えましたが、書き込みツールを作ってみました。
読み込ませるデータの書き込みには、こちらを用意しました。
public/tatobawriter.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| <!DOCTYPE html> <html>
<head> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css" /> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs=" crossorigin="anonymous"></script> <script src="writer.js"></script> </head>
<body> <section class="hero"> <div class="container is-fullwidth"> <div class="hero-body"> <div class="container"> <h1 class="title"> Tatoba Writer </h1> </div> </div> </div> <div class="container is-fullwidth"> <div class="columns is-mobile" id="message" > </div> </div> </section> <section class="section"> <div class="container is-fullwidth"> <div class="columns is-mobile"> <button class="button is-large is-danger is-fullwidth" id="startbutton" onclick="writeStart('taka')"> 'TAKA' Write Start </button> </div> </section> <section class="section"> <div class="container is-fullwidth"> <div class="columns is-mobile"> <button class="button is-large is-warning is-fullwidth" id="startbutton" onclick="writeStart('tora')"> 'TORA' Write Start </button> </div> </div> </div> </section> <section class="section"> <div class="container is-fullwidth"> <div class="columns is-mobile"> <button class="button is-large is-success is-fullwidth" id="startbutton" onclick="writeStart('batta')"> 'BATTA' Write Start </button> </div> </div> </section> <section class="section"> <div class="container is-fullwidth"> <div class="columns is-mobile"> <button class="button is-large is-black is-fullwidth" id="endbutton" onclick="writeEnd()"> Write Stop </button> </div> </div> </section> <section> <div class="container is-fullwidth"> <a class="button is-large is-link is-fullwidth" href="/tatobareader" >Tatoba Reader</a > </div> </section> </body> </html>
|
public/writer.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| const writer = new NDEFWriter(); const reader = new NDEFReader();
let controller = null;
const writeStart = async (text) => { if (controller != null) { return; }
const encoder = new TextEncoder();
$("#message").html( `<div class="notification is-info is-fullwidth"> <h3 class="title is-4">Device is Ready</h3> </div>` );
controller = new AbortController();
try { await writer.write({ signal: controller.signal, records: [ { recordType: "mime", mediaType: "application/json", data: encoder.encode(JSON.stringify({ element: text })), }, ], }); $("#message").html( `<div class="notification is-primary is-fullwidth"> <h3 class="title is-4">${text} Write Success.</h3> </div>` ); controller.abort(); controller = null; } catch (error) { $("#message").html( `<div class="notification is-danger is-fullwidth"> <h3 class="title is-4">Error</h3> </div>` ); } };
const writeEnd = async () => { $("#message").html( `<div class="notification is-info is-fullwidth"> <h3 class="title is-4">Write is end.</h3> </div>` ); controller.abort(); controller = null; };
reader.scan();
|
書き込むデータをボタンで選択して、タカ・トラ・バッタのいずれかを書き込みます。
エラーのカードがトラに、トラのカードはバッタに代わりました。
NFC タグに json 形式データを書き込んでいます。
ブラウザの操作で、物理世界に干渉(NFC の操作が物理世界への干渉かという議論はあるとして)できるのはとても楽しいです。
タグにデータを書き込むことで、web の認証に NFC タグを使ったり、ソシャゲのアイテムに NFC タグカードを使ったりできそうです。
面白がって音源のために録音などしましたが、音源の加工が意外と面倒でした。
大体 10 年前からライダー玩具は 非接触 タグ使ってたんだからすごいですね。
多分オーズが初だったかな?違っていたら失礼しました。
ではでは。