はじめに
現場の物品管理では、バーコードや QR コードをカメラで読み取る方式が定番です。便利なのですが、印字が擦れたり油で汚れたりすると途端に読めなくなります。屋外や暗所でもカメラのピントが合わずに苦戦します。
そこで今回は NFC タグです。タグを本体に貼り付けておけば、スマートフォンをかざすだけで値が入ります。光学式ではないので汚れや暗さの影響を受けません。
実は Chrome for Android には Web NFC API が載っていて、ブラウザから直接 NFC タグを読み書きできます。ネイティブアプリも追加ライブラリも不要です。プリザンターの拡張機能だけで、分類項目の横に「かざして入力」ボタンを追加してみます。
先に制約を確認する
Web NFC はブラウザ API の中でもかなり対応範囲が限定的です。実装に入る前に、できること・できないことを整理しておきます。
| 項目 | 内容 |
|---|---|
| 対応ブラウザ | Chrome for Android 89 以降のみ |
| 非対応 | デスクトップ版 Chrome / Edge、iOS の Safari・Chrome |
| 接続要件 | HTTPS 必須(localhost は例外) |
| 呼び出し条件 | ユーザー操作(クリック等)を起点にする必要がある |
| 読み取れるもの | NDEF フォーマットのタグのみ |
| 対応タグ | NFC Forum Type 1〜5(NTAG213 / NTAG215 / NTAG216 など) |
交通系ICカードや社員証などの FeliCa カードは、Web NFC ではカード固有の IDm や内部データを読み取れません。Web NFC が扱えるのは NDEF メッセージだけで、FeliCa の独自コマンドは投げられないためです。既存の社員証を鍵にしたい場合は、素直にネイティブアプリか専用リーダーを検討してください。
逆に言えば、自分たちで用意した NTAG シールを貼る運用であれば、Web NFC で十分に実用になります。NTAG213 のシールは 1 枚数十円で買えるので、備品や什器に貼っていく用途とは相性が良いです。
iOS が非対応な点は、バーコードリーダーと併用して逃げるのが現実的です。同じ項目にバーコードと NFC の両方のボタンを出しておけば、端末に応じて使い分けてもらえます。
全体の流れ
実装してみよう
今回は 2 つの拡張機能を組み合わせます。バーコードのときと違って外部ライブラリを使わないため、拡張HTML は不要です。
| 拡張機能 | 役割 |
|---|---|
| 拡張スクリプト | ボタンの追加・NFC読み取り処理 |
| 拡張スタイル | モーダルやボタンの見た目を定義 |
モーダルとボタンのスタイル(拡張スタイル)
バーコードリーダーのときと同じ date-field パターンで、入力欄の右端にボタンを配置します。拡張スタイルとして App_Data/Parameters/ExtendedStyles/ に配置します。
/* date-field と同じレイアウト:入力欄の右端にボタンを配置 */
.nfc-field {
position: relative;
display: block;
}
.nfc-field .control-textbox {
padding-right: 24px;
}
.nfc-scan-trigger {
position: absolute;
top: 0;
right: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 100%;
margin: 0;
padding: 0;
background: transparent;
border: none;
outline: none;
cursor: pointer;
}
.nfc-scan-trigger .material-symbols-sharp {
font-size: 16px;
}
#nfc-modal .nfc-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9998;
}
#nfc-modal .nfc-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 8px;
padding: 24px;
z-index: 9999;
width: min(90vw, 360px);
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
#nfc-modal .nfc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
font-weight: bold;
}
/* 待受中のアイコンをゆっくり明滅させる */
#nfc-modal .nfc-wave {
font-size: 64px;
color: #4a89dc;
animation: nfc-pulse 1.6s ease-in-out infinite;
}
@keyframes nfc-pulse {
0%, 100% { opacity: 0.3; transform: scale(0.92); }
50% { opacity: 1; transform: scale(1); }
}
#nfc-modal .nfc-message {
margin-top: 12px;
font-size: 14px;
}
#nfc-modal #nfc-error {
margin-top: 8px;
color: red;
font-size: 12px;
min-height: 1.2em;
}
ボタンの追加と読み取り処理(拡張スクリプト)
分類項目に NFC ボタンを追加し、読み取り処理を実装します。拡張スクリプトとして App_Data/Parameters/ExtendedScripts/ に配置します。
$(function () {
// Web NFC 非対応ブラウザではボタン自体を出さない
if (!('NDEFReader' in window)) { return; }
// nfc / nfc-serial クラスが付与された分類項目にボタンを追加
$('.control-textbox.nfc, .control-textbox.nfc-serial').each(function () {
var $input = $(this);
var useSerial = $input.hasClass('nfc-serial');
$input.wrap('<div class="nfc-field"></div>');
var $btn = $('<button>', {
type: 'button',
class: 'nfc-scan-trigger',
title: 'NFCタグを読み取る'
}).append(
$('<span>', { class: 'material-symbols-sharp is-fill', text: 'nfc' })
).on('click', function (e) {
e.preventDefault();
startScan($input.attr('id'), useSerial);
});
$input.after($btn);
});
});
var nfcAbort = null;
var nfcTargetId = null;
var nfcUseSerial = false;
// 読み取り開始
function startScan(controlId, useSerial) {
nfcTargetId = controlId;
nfcUseSerial = useSerial;
// モーダルが未生成なら作成
if (!$('#nfc-modal').length) {
$('body').append(
'<div id="nfc-modal">' +
'<div class="nfc-overlay"></div>' +
'<div class="nfc-dialog">' +
'<div class="nfc-header">' +
'<span>NFCタグ読み取り</span>' +
'<button type="button" id="nfc-close" class="nfc-scan-trigger" style="position:static;width:auto;height:auto;">' +
'<span class="material-symbols-sharp is-fill">close</span>' +
'</button>' +
'</div>' +
'<span class="material-symbols-sharp is-fill nfc-wave">contactless</span>' +
'<p class="nfc-message">タグに端末をかざしてください</p>' +
'<p id="nfc-error"></p>' +
'</div>' +
'</div>'
);
$('#nfc-close, .nfc-overlay').on('click', stopScan);
}
$('#nfc-error').text('');
$('#nfc-modal').show();
// scan() は AbortController で明示的に止める
nfcAbort = new AbortController();
var reader = new NDEFReader();
reader.scan({ signal: nfcAbort.signal }).then(function () {
reader.onreadingerror = function () {
$('#nfc-error').text('タグを読み取れませんでした。もう一度かざしてください');
};
reader.onreading = function (event) {
var value = nfcUseSerial
? event.serialNumber
: readNdefText(event.message);
if (!value) {
$('#nfc-error').text('対応するレコードが見つかりませんでした');
return;
}
$p.set($('#' + nfcTargetId), value);
stopScan();
};
}).catch(function (e) {
// NotAllowedError: 権限拒否 / NotSupportedError: NFCが無効
$('#nfc-error').text('読み取りを開始できませんでした(' + e.name + ')');
});
}
// NDEFメッセージから最初の text / url レコードを取り出す
function readNdefText(message) {
for (var i = 0; i < message.records.length; i++) {
var record = message.records[i];
if (record.recordType === 'text') {
return new TextDecoder(record.encoding || 'utf-8').decode(record.data);
}
if (record.recordType === 'url' || record.recordType === 'absolute-url') {
return new TextDecoder().decode(record.data);
}
}
return null;
}
// 読み取り停止
function stopScan() {
if (nfcAbort) {
nfcAbort.abort();
nfcAbort = null;
}
$('#nfc-modal').hide();
nfcTargetId = null;
}
ポイントを見ていきましょう。
-
'NDEFReader' in windowで対応判定し、非対応ブラウザではボタンを描画しません。iPhone や PC で押しても反応しないボタンが出ていると混乱の元なので、最初に弾いておきます - ボタンは日付項目の「今」ボタンと同じ
date-fieldパターンで、Material Symbols のnfcアイコンを使用しています -
reader.scan()を呼ぶと待受状態に入ります。カメラのように映像が出るわけではないので、contactlessアイコンを明滅させて「かざせる状態」であることを見せています -
scan()は一度始めると自動では止まりません。AbortControllerを握っておき、モーダルを閉じるときにabort()する必要があります。ここを忘れると、モーダルを閉じたあともタグをかざすたびにonreadingが飛んできます - コントロールCSS に
nfc-serialを指定した項目では、NDEF レコードではなくタグのシリアル番号(event.serialNumber)を入力します。中身が空のタグでも一意な ID が取れるので、備品台帳のキーとして使うならこちらが便利です
おまけ:タグに書き込む
NDEFReader は読むだけでなく書き込みもできます。レコードのキーをタグ側に焼いておきたい場合はこちらを使います。
// 現在のレコードIDをNFCタグに書き込む
function writeCurrentId() {
var writer = new NDEFReader();
return writer.write({
records: [
{ recordType: 'text', lang: 'ja', data: $p.id().toString() }
]
}).then(function () {
$p.setMessage('#Message', { Css: 'alert-success', Text: 'タグに書き込みました' });
}).catch(function (e) {
$p.setMessage('#Message', { Css: 'alert-error', Text: '書き込みに失敗しました(' + e.name + ')' });
});
}
write() は既存の NDEF メッセージを上書きします。書き込み済みのタグを誤って潰さないよう、実運用では確認ダイアログを挟んでおくことをおすすめします。NTAG は書き換え回数に上限(10 万回程度)がありますが、通常の用途で到達することはまずありません。
サイト設定
NFC 読み取りを使用したい分類項目の「コントロールCSS」を設定します。
テーブルの管理 → エディタ → 対象の分類項目 → 詳細設定 → コントロールCSS に、以下のいずれかを入力してください。
| コントロールCSS | 入力される値 |
|---|---|
nfc |
NDEF の text / url レコードの中身 |
nfc-serial |
タグのシリアル番号 |
バーコードリーダーのときと同様、選択肢一覧は空にしてテキスト入力の分類項目として使用してください。ドロップダウンやラジオボタンでは読み取った値を直接入力できません。
Web NFC は HTTPS 必須です。プリザンターを HTTP で公開している環境では動作しません。社内向けであっても、証明書を入れて HTTPS 化しておく必要があります。
実行してみる
動作確認環境
| 項目 | 内容 |
|---|---|
| 端末 | |
| OS | |
| ブラウザ | |
| タグ | |
| プリザンター |
まとめ
今回はプリザンターの分類項目に NFC タグの読み取り機能を追加する方法を紹介しました。
- Web NFC API はブラウザ標準なので、外部ライブラリも拡張HTMLも不要
- 拡張スクリプトと拡張スタイルだけで実装でき、本体の改修は不要
- コントロールCSS で
nfc/nfc-serialを切り替え、NDEF の中身とシリアル番号を使い分けられる -
scan()はAbortControllerで必ず止める。止め忘れると閉じたあとも読み取り続ける - Chrome for Android 限定・HTTPS 必須・NDEF のみという制約は事前に押さえておく
バーコードは「紙に印刷できる」「どの端末でも読める」という強みがあり、NFC は「汚れや暗所に強い」「かざすだけで速い」という強みがあります。どちらか一方ではなく、同じ項目に両方のボタンを出して現場に選ばせるのが、結局いちばん使われる形だと思います。
是非試してみてください。