Chrome для Android теперь может читать и записывать NFC-метки.
Что такое Web NFC?
NFC (Near Field Communications) — это технология беспроводной связи ближнего действия на частоте 13,56 МГц, которая обеспечивает передачу данных между устройствами на расстоянии до 10 см со скоростью до 424 кбит/с.
Web NFC дает сайтам возможность читать и записывать NFC-метки, находящиеся в непосредственной близости от устройства пользователя (обычно это 5-10 см). Пока что работает только NDEF (NFC Data Exchange Format) — упрощенный формат обмена двоичными сообщениями для меток различных форматов.
Предлагаемые варианты использования
В Web NFC используется только NDEF, поскольку свойства системы безопасности для чтения и записи данных NDEF легче измерить количественно. Низкоуровневые операции ввода-вывода (например, ISO-DEP, NFC-A/B, NFC-F), режим одноранговой связи и эмуляция карт на устройстве пользователя (HCE) не поддерживаются.
Примеры сайтов, которые могут использовать Web NFC: - Музеи и художественные галереи: отображение дополнительных сведений об экспонате, когда пользователь касается его NFC-карты телефоном. - Сайты инвентарного учета: чтение и запись данных в NFC-метку на контейнере для обновления сведений о его содержимом. - Сайты конференций: сканирование NFC-бейджиков на мероприятии. - Эту функцию можно использовать для обмена исходными секретными ключами, необходимыми для подготовки устройств или сервисов, а также для развертывания данных конфигурации в рабочем режиме.
Текущее состояние
Использование Web NFC
Обнаружение функции
Обнаружение функций для оборудования отличается от привычного.
Наличие NDEFReader
сообщает, что браузер поддерживает Web NFC, но не говорит
о наличии необходимого оборудования. В частности, если оборудования нет,
то промис от определенных вызовов будет отклонен. Подробнее —
ниже, в описании NDEFReader
.
if ('NDEFReader' in window) { /* Сканирование и запись NFC-меток */ }
Терминология
NFC-метка — это пассивное NFC-устройство: оно получает питание от магнитной индукции, когда поблизости находится активное устройство (например, телефон). NFC-метки бывают разных форм и видов: наклейки, пластиковые карты, браслеты и т. д.
Объект NDEFReader
— это точка входа в Web NFC, которая предоставляет
функции для подготовки действий чтения и (или) записи, выполняемых при появлении
NDEF-метки поблизости. Аббревиатура NDEF
в NDEFReader
означает NFC Data Exchange
Format — упрощенный формат обмена двоичным сообщениями по стандарту NFC Forum.
Объект NDEFReader
предназначен для действий с сообщениями NDEF от NFC-меток
и для записи сообщений NDEF в NFC-метки в радиусе действия.
NFC-метка с поддержкой NDEF напоминает записку: кто угодно может ее прочитать, а также записать (если запись разрешена). В ней содержится одно сообщение NDEF, в котором находится одна или несколько NDEF-записей. Каждая NDEF-запись — это двоичная структура с полезными данными и сведениями о соответствующем типе. Web NFC поддерживает следующие типы записей стандарта NFC Forum: пустая, текст, URL-адрес, смарт-плакат, MIME-тип, абсолютный URL-адрес, внешний тип, неизвестный и локальный тип.
Сканирование NFC-меток
Чтобы сканировать NFC-метки, создайте экземпляр объекта NDEFReader
. Вызов scan()
возвращает промис. Если ранее доступ не был получен, пользователю может
быть отправлен запрос. Объект Promise будет разрешен, если выполнятся следующие условия:
- Пользователь разрешил веб-сайту использовать NFC-устройства при прикосновении ими к телефону.
- Телефон поддерживает NFC.
- Пользователь включил NFC на телефоне.
После разрешения промиса можно подписаться чтение
входящих
сообщений NDEF через прослушиватель событий. Также следует подписаться
на события readingerror
— чтобы знать, когда поблизости
оказываются несовместимые NFC-метки.
const ndef = new NDEFReader();
ndef.scan().then(() => {
console.log("Сканирование запущено.");
ndef.onreadingerror = () => {
console.log("Не удается прочитать данные из NFC-метки. Попробовать другую?");
};
ndef.onreading = event => {
console.log("NDEF-сообщение прочитано.");
};
}).catch(error => {
console.log(`Ошибка! Не удалось запустить сканирование. ${error}.`);
});
Если поблизости NFC-метка, срабатывает событие NDEFReadingEvent
с
двумя уникальными свойствами:
serialNumber
— серийный номер устройства (например, 00-11-22-33-44-55-66) или пустая строка, если его нет;message
— NDEF-сообщение, хранящееся в NFC-метке.
Чтобы прочитать содержимое NDEF-сообщения, запустите цикл по message.records
и обработайте члены data
согласно их recordType
(тип).
Члены data
представлены как DataView
поскольку это позволяет
обрабатывать случаи с кодировкой UTF-16.
ndef.onreading = event => {
const message = event.message;
for (const record of message.records) {
console.log("Тип записи: " + record.recordType);
console.log("MIME-тип: " + record.mediaType);
console.log("Идентификатор записи: " + record.id);
switch (record.recordType) {
case "text":
// TODO. Прочитать текстовую запись согласно данным, языку и кодировке записи.
break;
case "url":
// TODO. Прочитать запись URL согласно данным записи.
break;
default:
// TODO. Обработка других записей согласно данным записи.
}
}
};
Запись NFC-меток
Чтобы записывать NFC-метки, создайте экземпляр объекта NDEFReader
. Вызов write()
возвращает промис. Если ранее доступ не был получен, пользователю может
быть отправлен запрос. На этом этапе NDEF-сообщение уже «подготовлено». Объект
Promise будет разрешен, если выполнятся следующие условия:
- Пользователь разрешил веб-сайту использовать NFC-устройства при прикосновении ими к телефону.
- Телефон поддерживает NFC.
- Пользователь включил NFC на телефоне.
- Пользователь коснулся NFC-метки и NDEF-сообщение было записано.
Чтобы записать в NFC-метку текст, передайте в метод write()
строку.
const ndef = new NDEFReader();
ndef.write(
"Hello World"
).then(() => {
console.log("Сообщение записано.");
}).catch(error => {
console.log(`Ошибка записи. Повторите попытку. ${error}.`);
});
Чтобы записать в NFC-метку URL-адрес, передайте в метод write()
словарь,
представляющий NDEF-сообщение. В примере ниже NDEF-сообщение — это словарь с ключом
records
, значение которого — массив записей. В этом случае
запись URL, определенная как объект, у которого ключ recordType
имеет значение
"url"
, а data
— строка URL-адреса.
const ndef = new NDEFReader();
ndef.write({
records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }]
}).then(() => {
console.log("Сообщение записано.");
}).catch(error => {
console.log(`Ошибка записи. Повторите попытку. ${error}.`);
});
В NFC-метки можно заносить и несколько записей.
const ndef = new NDEFReader();
ndef.write({ records: [
{ recordType: "url", data: "https://w3c.github.io/web-nfc/" },
{ recordType: "url", data: "https://web.dev/nfc/" }
]}).then(() => {
console.log("Сообщение записано.");
}).catch(error => {
console.log(`Ошибка записи. Повторите попытку. ${error}.`);
});
Если NFC-метка содержит NDEF-сообщение, не предназначенное для перезаписи,
то в параметрах для метода write()
нужно задать свойству overwrite
значение false
. В этом случае, если в NFC-метке уже сохранено
NDEF-сообщение, возвращенный промис будет отклонен.
const ndef = new NDEFReader();
ndef.write("Круто! Пишем данные в пустую NFC-метку!", { overwrite: false })
.then(() => {
console.log("Сообщение записано.");
}).catch(_ => {
console.log(`Ошибка записи. Повторите попытку. ${error}.`);
});
Безопасность и разрешения
Команда Chrome разработала и внедрила Web NFC согласно принципам, определенным в Контроле доступа к функциям веб-платформы с широкими возможностями, включая пользовательский контроль, прозрачность и удобство.
NFC расширяет область, в которой информация может быть доступна вредоносным сайтам, поэтому использование NFC ограничено, с тем чтобы максимально информировать пользователей и дать им контроль над этой функцией.
Web NFC используется только во фреймах верхнего уровня и контекстах безопасного
просмотра веб-страниц (только HTTPS). Источник при обработке жеста пользователя (например, нажатия кнопки) сначала
запрашивает разрешение "nfc"
. Если ранее доступ не был получен, методы scan()
и write()
объекта NDEFReader отправляют запрос пользователю.
document.querySelector("#scanButton").onclick = async () => {
const ndef = new NDEFReader();
// Запрос пользователю разрешить сайту взаимодействовать с NFC-устройствами.
await ndef.scan();
ndef.onreading = event => {
// TODO. Обработка входящих NDEF-сообщений.
};
};
Сочетание инициированного пользователем запроса на разрешение и физического перемещения устройства по целевой NFC-метке отражает паттерн выбирающего субъекта, который можно найти в других API доступа к файлам и устройствам.
Для сканирования (записи) веб-страница должна быть видимой, когда пользователь касается NFC-метки устройством. Касание отражается браузером с помощью тактильной обратной связи. Если дисплей выключен или устройство заблокировано, доступ к NFC-передатчику блокируется. Если веб-страница невидима, получение и отправка содержимого приостанавливается и возобновляется, когда она становится видимой.
Изменение видимости документа можно отслеживать с помощью Page Visibility API.
document.onvisibilitychange = event => {
if (document.hidden) {
// Если документ скрыт, все операции NFC автоматически приостанавливаются.
} else {
// При необходимости все операции NFC возобновляются.
}
};
Справочник
Ниже приведено несколько примеров кода.
Проверка разрешения
С помощью Permissions API можно проверить, было ли получено разрешение
"nfc"
. В примере показано, как сканировать NFC-метки без взаимодействия с пользователем,
если доступ уже есть (и отобразить кнопку, если он еще не был получен). Такой же
механизм действует и для записи, поскольку используется то же
разрешение.
const ndef = new NDEFReader();
async function startScanning() {
await ndef.scan();
ndef.onreading = event => {
/* обработка NDEF-сообщений */
};
}
const nfcPermissionStatus = await navigator.permissions.query({ name: "nfc" });
if (nfcPermissionStatus.state === "granted") {
// Доступ NFC уже был предоставлен — можно начинать сканирование.
startScanning();
} else {
// Показать кнопку сканирования.
document.querySelector("#scanButton").style.display = "block";
document.querySelector("#scanButton").onclick = event => {
// Запросить у пользователя разрешение на отправку и получение информации при касании NFC-устройств.
startScanning();
};
}
Прерывание операции с NFC
С помощью примитива AbortController
можно прервать операции
с NFC. В примере ниже показано, как передать signal
из AbortController
через параметры методов scan()
и write()
объекта NDEFReader и
одновременно прервать обе операции с NFC.
const abortController = new AbortController();
abortController.signal.onabort = event => {
// Все операции с NFC были прерваны.
};
const ndef = new NDEFReader();
await ndef.scan({ signal: abortController.signal });
await ndef.write("Hello world", { signal: abortController.signal });
document.querySelector("#abortButton").onclick = event => {
abortController.abort();
};
Чтение и создание текстовой записи
Данные data
текстовой записи можно декодировать с помощью экземпляра
TextDecoder
, созданного со свойством encoding
записи. Язык
текстовой записи хранится в свойстве lang
.
function readTextRecord(record) {
console.assert(record.recordType === "text");
const textDecoder = new TextDecoder(record.encoding);
console.log(`Текст: ${textDecoder.decode(record.data)} (${record.lang})`);
}
Сделать простую текстовую запись можно, передав в метод write()
объекта NDEFReader строку.
const ndef = new NDEFReader();
await ndef.write("Hello World");
Текстовые записи по умолчанию в кодировке UTF-8 и предполагают язык текущего
документа, однако NDEF-запись можно настроить, используя полный синтаксис и указав
свойства encoding
и lang
.
function a2utf16(string) {
let result = new Uint16Array(string.length);
for (let i = 0; i < string.length; i++) {
result[i] = string.codePointAt(i);
}
return result;
}
const textRecord = {
recordType: "text",
lang: "fr",
encoding: "utf-16",
data: a2utf16("Bonjour, François !")
};
const ndef = new NDEFReader();
await ndef.write({ records: [textRecord] });
Чтение и создание записи с URL-адресом
Для декодирования данных data
записи используйте TextDecoder
.
function readUrlRecord(record) {
console.assert(record.recordType === "url");
const textDecoder = new TextDecoder();
console.log(`URL: ${textDecoder.decode(record.data)}`);
}
Создать запись с URL-адресом можно, передав в метод write()
объекта NDEFReader
словарь с NDEF-сообщением. Запись с URL-адресом в NDEF-сообщении определяется как объект, у которого
ключ recordType
имеет значение "url"
, а data
—
строка URL.
const urlRecord = {
recordType: "url",
data:"https://w3c.github.io/web-nfc/"
};
const ndef = new NDEFReader();
await ndef.write({ records: [urlRecord] });
Чтение и создание записи с MIME-типом
Свойство mediaType
записи с MIME-типом представляет собой MIME-тип полезной
нагрузки NDEF-записи, что позволяет декодировать data
. Например, для декодирования
JSON-текста используется JSON.parse
, а для данных изображения — элемент Image.
function readMimeRecord(record) {
console.assert(record.recordType === "mime");
if (record.mediaType === "application/json") {
const textDecoder = new TextDecoder();
console.log(`JSON: ${JSON.parse(decoder.decode(record.data))}`);
}
else if (record.mediaType.startsWith('image/')) {
const blob = new Blob([record.data], { type: record.mediaType });
const img = new Image();
img.src = URL.createObjectURL(blob);
document.body.appendChild(img);
}
else {
// TODO. Добавить обработку других MIME-типов.
}
}
Создать запись с MIME-типом можно, передав в метод write()
объекта NDEFReader
словарь с NDEF-сообщением. Запись с MIME-типом в NDEF-сообщении определяется как объект, у которого
ключ recordType
имеет значение "mime"
, mediaType
— MIME-тип
контента, а data
— либо объект ArrayBuffer
,
либо его представление (например,
Uint8Array
, DataView
).
const encoder = new TextEncoder();
const data = {
firstname: "François",
lastname: "Beaufort"
};
const jsonRecord = {
recordType: "mime",
mediaType: "application/json",
data: encoder.encode(JSON.stringify(data))
};
const imageRecord = {
recordType: "mime",
mediaType: "image/png",
data: await (await fetch("icon1.png")).arrayBuffer()
};
const ndef = new NDEFReader();
await ndef.write({ records: [jsonRecord, imageRecord] });
Чтение и создание записи с абсолютным URL-адресом
Данные data
записи с абсолютным URL-адресом можно декодировать с помощью простого TextDecoder
.
function readAbsoluteUrlRecord(record) {
console.assert(record.recordType === "absolute-url");
const textDecoder = new TextDecoder();
console.log(`Абсолютный URL: ${textDecoder.decode(record.data)}`);
}
Создать запись с абсолютным URL-адресом можно, передав в метод write()
объекта
NDEFReader словарь с NDEF-сообщением. Запись с абсолютным URL в NDEF-сообщении определяется как объект,
у которого ключ recordType
имеет значение "absolute-url"
, а data
—
строка URL.
const absoluteUrlRecord = {
recordType: "absolute-url",
data:"https://w3c.github.io/web-nfc/"
};
const ndef = new NDEFReader();
await ndef.write({ records: [absoluteUrlRecord] });
Чтение и создание записи со смарт-плакатом
Запись со смарт-плакатом (реклама журналов, листовки, рекламные щиты и т. д.)
описывает определенный веб-контент как NDEF-запись с NDEF-сообщением
в качестве полезной нагрузки. Чтобы преобразовать data
в список записей из смарт-плаката,
нужно вызвать record.toRecords()
. В нем должна быть запись URL, текстовая
запись для заголовка, запись с MIME-типом для изображения и [пользовательские
записи локального типа], например ":t"
, ":act"
и ":s"
для типа,
действия и размера записи со смарт-плакатом соответственно.
Записи локального типа уникальны в рамках локального контекста содержащей
их записи NDEF. Они используются, когда смысл типов вне локального контекста содержащей
их записи не имеет значения и когда размер хранилища является
существенным ограничением. В Web NFC имена записей локального типа всегда начинаются с :
(например, ":t"
, ":s"
, ":act"
). Это позволяет отличить, например, текстовую запись
локального типа от обычной текстовой записи.
function readSmartPosterRecord(smartPosterRecord) {
console.assert(record.recordType === "smart-poster");
let action, text, url;
for (const record of smartPosterRecord.toRecords()) {
if (record.recordType == "text") {
const decoder = new TextDecoder(record.encoding);
text = decoder.decode(record.data);
} else if (record.recordType == "url") {
const decoder = new TextDecoder();
url = decoder.decode(record.data);
} else if (record.recordType == ":act") {
action = record.data.getUint8(0);
} else {
// TODO. Обработка записей других типов, например `:t`, `:s`.
}
}
switch (action) {
case 0:
// Выполнить действие
break;
case 1:
// Сохранить на потом
break;
case 2:
// Открыть для редактирования
break;
}
}
Создать запись со смарт-плакатом можно, передав в метод write()
объекта NDEFReader
NDEF-сообщение. Запись со смарт-плакатом в NDEF-сообщении определяется как объект, у которого
ключ recordType
имеет значение "smart-poster"
, а data
—
объект, представляющий, опять же, NDEF-сообщение, содержащееся в
записи смарт-плаката.
const encoder = new TextEncoder();
const smartPosterRecord = {
recordType: "smart-poster",
data: {
records: [
{
recordType: "url", // Запись с URL-адресом для содержимого смарт-плаката
data: "https://my.org/content/19911"
},
{
recordType: "text", // Запись с заголовком для содержимого смарт-плаката
data: "Забавный танец"
},
{
recordType: ":t", // Запись типа, локальный тип для смарт-плаката
data: encoder.encode("image/gif") // MIME-тип смарт-плаката
},
{
recordType: ":s", // Запись размера, локальный тип для смарт-плаката
data: new Uint32Array([4096]) // Размер в байтах для смарт-плаката
},
{
recordType: ":act", // Запись действия, локальный тип для смарт-плаката
// Выполнить действие, в этом случае — открыть в браузере
data: new Uint8Array([0])
},
{
recordType: "mime", // Запись MIME-типа со значком
mediaType: "image/png",
data: await (await fetch("icon1.png")).arrayBuffer()
},
{
recordType: "mime", // Еще одна запись со значком
mediaType: "image/jpg",
data: await (await fetch("icon2.jpg")).arrayBuffer()
}
]
}
};
const ndef = new NDEFReader();
await ndef.write({ records: [smartPosterRecord] });
Чтение и создание записи с внешним типом
Внешний тип нужен для возможности создания записей, определяемых приложением. В них
в качестве полезной нагрузки может быть NDEF-сообщение, доступное посредством toRecords()
. В
имени содержится доменное имя организации-эмитента, двоеточие и имя типа
длиной не менее одного символа, например "example.com:foo"
.
function readExternalTypeRecord(externalTypeRecord) {
for (const record of externalTypeRecord.toRecords()) {
if (record.recordType == "text") {
const decoder = new TextDecoder(record.encoding);
console.log(`Текст: ${textDecoder.decode(record.data)} (${record.lang})`);
} else if (record.recordType == "url") {
const decoder = new TextDecoder();
console.log(`URL: ${decoder.decode(record.data)}`);
} else {
// TODO. Обработка записей других типов.
}
}
}
Создать запись с внешним типом можно, передав в метод write()
объекта
NDEFReader словарь с NDEF-сообщением. Запись с внешним типом в NDEF-сообщении определяется
как объект, у которого в ключе recordType
содержится имя внешнего
типа, а в data
— объект, представляющий NDEF-сообщение, содержащееся в записи
с внешним типом. Ключ data
также может являться либо объектом
ArrayBuffer
, либо его представлением
(например, Uint8Array
, DataView
).
const externalTypeRecord = {
recordType: "example.game:a",
data: {
records: [
{
recordType: "url",
data: "https://example.game/42"
},
{
recordType: "text",
data: "Здесь дается контекст игры"
},
{
recordType: "mime",
mediaType: "image/png",
data: await (await fetch("image.png")).arrayBuffer()
}
]
}
};
const ndef = new NDEFReader();
ndef.write({ records: [externalTypeRecord] });
Чтение и создание пустой записи
У пустой записи нет полезной нагрузки.
Создать пустую запись можно, передав в метод write()
объекта NDEFReader
словарь с NDEF-сообщением. Пустая запись в NDEF-сообщении определяется как объект, у которого
ключ recordType
имеет значение "empty"
.
const emptyRecord = {
recordType: "empty"
};
const ndef = new NDEFReader();
await ndef.write({ records: [emptyRecord] });
Поддержка в браузере
Web NFC поддерживается на Android в Chrome 89.
Советы разработчикам
Ниже — то, что было бы неплохо знать до начала работы с Web NFC:
- Android обрабатывает NFC-метки на уровне ОС до того, как начинает действовать Web NFC.
- Значок NFC можно найти на сайте material.io.
- Различать NDEF-записи легче всего, используя их
id
. - NFC-метка без формата с поддержкой NDEF содержит одну запись пустого типа.
- Ссылка на приложение Android создается легко — см. ниже.
const encoder = new TextEncoder();
const aarRecord = {
recordType: "android.com:pkg",
data: encoder.encode("com.example.myapp")
};
const ndef = new NDEFReader();
await ndef.write({ records: [aarRecord] });
Демонстрация
Попробуйте официальный пример и посмотрите пару классных демонстраций Web NFC: - Карточки. - Список продуктов. - Intel RSP Sensor NFC. - Мультимедийная записка.
Отзывы
Группа сообщества Web NFC и команда Chrome с радостью выслушает ваше мнение об Web NFC и опыте использования этой функции.
Ваше мнение о дизайне API
Что-то в API не работает должным образом? Или, может, отсутствуют методы или свойства, необходимые для реализации вашей идеи?
Отправьте заявку о проблеме со спецификацией (Spec issue) в GitHub-репозиторий Web NFC или прокомментируйте существующую.
Сообщите о проблеме с реализацией
Нашли ошибку в реализации функции в браузере Chrome? Реализация отличается от спецификации?
Сообщите об ошибке на странице https://new.crbug.com. Опишите
проблему как можно подробнее, дайте простые инструкции по ее воспроизведению
и для Components укажите Blink>NFC
. Для демонстрации этапов воспроизведения
ошибки удобно использовать Glitch.
Окажите поддержку
Планируете использовать Web NFC? Ваша публичная поддержка помогает команде Chrome определять приоритет функций и показывает другим поставщикам браузеров, насколько важно их поддерживать.
Упомяните в твите @ChromiumDev, поставьте хештег
#WebNFC
и расскажите, как вы используете эту функцию.
Полезные ссылки
- Спецификация.
- Демонстрационный пример Web NFC | Исходный код демопримера Web NFC.
- Отслеживание ошибок.
- Запись на ChromeStatus.com.
- Компонент Blink:
Blink>NFC
.
Благодарности
Большое спасибо ребятам из Intel за реализацию Web NFC. Google Chrome опирается на сообщество разработчиков, вместе продвигающих проект Chromium вперед. Не все авторы кода Chromium — сотрудники Google, поэтому они заслуживают особой благодарности!