Связь с Bluetooth-устройствами через JavaScript

Web Bluetooth API позволяет веб-сайтам связываться с Bluetooth-устройствами.

François Beaufort
François Beaufort

Что если я скажу, что веб-сайты могут связываться с ближайшими Bluetooth-устройствами безопасным способом с сохранением конфиденциальности? Таким образом, пульсометры, поющие лампочки и даже черепахи могут напрямую взаимодействовать с веб-сайтом.

До сих пор взаимодействовать с Bluetooth-устройствами могли только платформозависимые приложения. Web Bluetooth API нацелен это изменить, дав такую возможность веб-браузерам.

Перед началом

В этой статье предполагается, что у вас есть базовые знания о том, как работают технология Bluetooth с низким энергопотреблением (Bluetooth Low Energy, BLE) и профиль общих атрибутов (Generic Attribute Profile, GATT).

Несмотря на то, что спецификация Web Bluetooth API еще не окончательно доработана, авторы спецификации активно ищут энтузиастов-разработчиков, чтобы опробовать этот API и дать отзывы о спецификации и отзывы о реализации.

Сокращенная версия Web Bluetooth API доступна в ChromeOS, Chrome для Android 6.0, Mac (Chrome 56) и Windows 10 (Chrome 70). Это означает, что вы должны иметь возможность запрашивать и подключаться к ближайшим устройствам Bluetooth с низким энергопотреблением, читать и записывать характеристики Bluetooth, получать уведомления GATT, знать, когда устройство Bluetooth отключается, а также читать и записывать дескрипторы Bluetooth. Для получения дополнительной информации см. таблицу совместимости браузера в MDN.

Для Linux и более ранних версий Windows включите флаг #experimental-web-platform-features в about://flags.

Доступно для испытаний по схеме Origin Trial

Чтобы получить как можно больше отзывов от разработчиков, использующих Web Bluetooth API на практике, Chrome ранее добавил эту функцию в Chrome 53 для испытаний по схеме Origin Trial на ChromeOS, Android и Mac.

Испытания успешно завершились в январе 2017 года.

Требования безопасности

Чтобы разобраться в компромиссах безопасности, рекомендую прочесть публикацию о модели безопасности Web Bluetooth от Джеффри Яскина, инженера-программиста из команды Chrome, работающего над спецификацией Web Bluetooth API.

Только HTTPS

Поскольку этот экспериментальный API — новая мощная функция, добавленная в Интернет, он доступен только для защищенных контекстов. Это означает, что организовывать проект нужно исходя из требований протокола TLS.

Требуется жест пользователя

В целях безопасности обнаружение Bluetooth-устройств с помощью navigator.bluetooth.requestDevice должно запускаться жестом пользователя, например, касанием или щелчком мыши. Речь идет о прослушивании событий pointerup, click и touchend.

button.addEventListener('pointerup', function(event) {
  // Вызываем navigator.bluetooth.requestDevice
});

Займемся кодом

Web Bluetooth API в значительной степени полагается на обещания JavaScript. Если вы не знакомы с ними, прочтите это замечательное руководство по обещаниям. И еще: () => {} — это просто стрелочные функции ECMAScript 2015.

Запрос устройств Bluetooth

Эта версия спецификации Web Bluetooth API позволяет веб-сайтам, работающим в роли Central, подключаться к удаленным серверам GATT через соединение BLE. Поддерживается связь между устройствами, реализующими Bluetooth 4.0 или более поздней версии.

Когда веб-сайт запрашивает доступ к ближайшим устройствам с помощью navigator.bluetooth.requestDevice, браузер вызывает список устройств, где пользователь может выбрать одно устройство или отменить запрос.

Пользовательское диалоговое окно с устройствами Bluetooth.

Функция navigator.bluetooth.requestDevice() принимает обязательный объект, определяющий фильтры. Эти фильтры используются для возврата только тех устройств, которые соответствуют некоторым объявленным службам Bluetooth GATT или имени устройства.

Фильтр служб

Вот так можно запросить устройства Bluetooth, объявляющие службу батарей Bluetooth GATT:

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Если ваша служба Bluetooth GATT не входит в список стандартизированных служб Bluetooth GATT, вы можете предоставить либо полный UUID Bluetooth, либо короткую 16- или 32-разрядную форму.

navigator.bluetooth.requestDevice({
  filters: [{
    services: [0x1234, 0x12345678, '99999999-0000-1000-8000-00805f9b34fb']
  }]
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Фильтр имен

Вы также можете запрашивать устройства Bluetooth на основе имени устройства, которое объявляется с помощью ключа фильтров name, или даже префикса этого имени с ключом namePrefix. Обратите внимание, что в этом случае также необходимо будет определить ключ optionalServices, чтобы иметь доступ к любым службам, не включенным в фильтр служб. Если этого не сделать, позже при попытке доступа к ним вы получите сообщение об ошибке.

navigator.bluetooth.requestDevice({
  filters: [{
    name: 'Francois robot'
  }],
  optionalServices: ['battery_service'] // Необходимо для последующего доступа к службе.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Фильтр данных производителя

Также можно запрашивать устройства Bluetooth на основе данных производителя, объявляемых с помощью ключа фильтров manufacturerData. Этот ключ представляет собой массив объектов с обязательным ключом идентификатора компании Bluetooth под названием companyIdentifier. Также можно указать префикс данных, фильтрующий данные производителя устройств Bluetooth, которые с него начинаются. Обратите внимание, что также необходимо определить ключ optionalServices, чтобы иметь возможность доступа к любым службам, не включенным в фильтр служб. Если этого не сделать, позже при попытке доступа к ним вы получите сообщение об ошибке.

// Фильтруем Bluetooth-устройства от Google с байтами данных производителя
// которые начинаются с [0x01, 0x02].
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // Необходимо для последующего доступа к службе.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Маска также может использоваться с префиксом данных, чтобы соответствовать некоторым шаблонам в данных производителя. Чтобы узнать больше, ознакомьтесь с объяснением фильтров данных Bluetooth.

Без фильтров

Наконец, вместо filters можно использовать ключ acceptAllDevices для отображения всех ближайших Bluetooth-устройств. Вам также потребуется определить ключ optionalServices, чтобы иметь доступ к некоторым службам. Если этого не сделать, позже при попытке доступа к ним вы получите сообщение об ошибке.

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // Необходимо для последующего доступа к службе.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Подключение к устройству Bluetooth

Итак, что делать теперь, когда у вас есть BluetoothDevice? Давайте подключимся к удаленному Bluetooth-серверу GATT, который содержит определения служб и характеристик.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // Читаемое имя устройства.
  console.log(device.name);

  // Попытка соединиться с удаленным сервером GATT.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

Чтение характеристики Bluetooth

Итак, мы подключены к серверу GATT удаленного устройства Bluetooth. Теперь нам нужно получить первичную службу GATT и прочитать характеристику, которая принадлежит этой службе. Попробуем, например, узнать текущий уровень заряда аккумулятора устройства.

В приведенном ниже примере battery_level — это стандартизированная характеристика уровня заряда батареи.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
  // Получаем службу аккумулятора…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Получаем характеристику уровня заряда батареи…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Считываем заряд батареи…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Уровень заряда: ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

Если вы используете настраиваемую характеристику Bluetooth GATT, вы можете предоставить либо полный UUID Bluetooth, либо короткую 16- или 32-разрядную форму для service.getCharacteristic.

Обратите внимание, что также можно добавить прослушиватель событий characteristicvaluechanged к характеристике для обработки считывания значения. Чтобы узнать, как дополнительно обрабатывать предстоящие уведомления GATT, ознакомьтесь с примером «Как прочитать измененное значение характеристики».

…
.then(characteristic => {
  // Настраиваем прослушиватель событий для измененного значения характеристики.
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // Считываем заряд батареи…
  return characteristic.readValue();
})
.catch(error => { console.error(error); });

function handleBatteryLevelChanged(event) {
  const batteryLevel = event.target.value.getUint8(0);
  console.log('Уровень заряда: ' + batteryLevel);
}

Запись в характеристику Bluetooth

Записать значение в характеристику Bluetooth GATT так же просто, как ее прочитать. На этот раз давайте воспользуемся точкой контроля частоты пульса, чтобы сбросить значение поля «Расход энергии» на 0 на пульсометре.

Вот увидите, ничего магического здесь нет. Все объясняется на странице характеристик контрольной точки пульса.

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_control_point'))
.then(characteristic => {
  // Запись 1 сигнализирует о том, что нужно сбросить энергозатраты.
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Расход энергии был сброшен.');
})
.catch(error => { console.error(error); });

Получение уведомлений GATT

Теперь давайте посмотрим, как получить уведомление при изменении характеристики измерения пульса на устройстве:

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleCharacteristicValueChanged);
  console.log('Уведомления запущены.');
})
.catch(error => { console.error(error); });

function handleCharacteristicValueChanged(event) {
  const value = event.target.value;
  console.log('Получено ' + value);
  // Сделать: парсинг значения пульса.
  // См. https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

Пример уведомлений показывает, как отключить уведомления со stopNotifications() и правильно удалить добавленный прослушиватель событий characteristicvaluechanged.

Отключение от устройства Bluetooth

Чтобы улучшить взаимодействие с пользователем, вы можете прослушивать события отключения и предлагать пользователю повторно подключиться:

navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
  // Настраиваем прослушиватель событий для отключения устройства.
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // Попытка соединения с удаленным сервером GATT.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

function onDisconnected(event) {
  const device = event.target;
  console.log(`Устройство ${device.name} отключено.`);
}

Также можно вызвать device.gatt.disconnect(), чтобы отключить веб-приложение от устройства Bluetooth. Это запустит существующие прослушиватели событий gattserverdisconnected. Обратите внимание, что связь с Bluetooth-устройством не будет прекращена, если с ним уже взаимодействует другое приложение. Чтобы узнать больше, ознакомьтесь с примером отключения устройства и примером автоматического повторного подключения.

Чтение и запись дескрипторов Bluetooth

Дескрипторы Bluetooth GATT — это атрибуты, которые описывают значение характеристики. Их можно читать и записывать аналогично характеристикам Bluetooth GATT.

Давайте посмотрим, например, как читать пользовательское описание интервала измерения термометра.

В приведенном ниже примере health_thermometer является службой термометра, measurement_interval — характеристикой интервала измерения, а gatt.characteristic_user_descriptionдескриптором пользовательского описания характеристики.

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => descriptor.readValue())
.then(value => {
  const decoder = new TextDecoder('utf-8');
  console.log(`Пользовательское описание: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });

Теперь, когда мы прочитали пользовательское описание интервала измерения термометра, давайте посмотрим, как его обновить и записать пользовательское значение.

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => {
  const encoder = new TextEncoder('utf-8');
  const userDescription = encoder.encode('Определяет время между измерениями.');
  return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });

Примеры, демонстрации и кодовые лаборатории

Все приведенные ниже примеры Web Bluetooth были успешно протестированы. Чтобы как следует оценить эти примеры, я рекомендую установить Android-приложение BLE Peripheral Simulator, которое имитирует периферийное BLE-устройство с помощью службы батареи, службы сердечного ритма или службы термометра.

Для начинающих

Объединение нескольких операций

Также ознакомьтесь с нашими специально отобранными демонстрациями Web Bluetooth и официальными кодовыми лабораториями Web Bluetooth.

Библиотеки

  • web-bluetooth-utils — это npm-модуль, который добавляет некоторые удобные функции в API.
  • Оболочка Web Bluetooth API доступна в noble, наиболее популярном Node.js-модуле BLE Central. Она позволяет упаковывать и просматривать noble без необходимости использования сервера WebSocket или других плагинов.
  • angular-web-bluetooth — это модуль для Angular, который абстрагирует весь стереотипный код, необходимый для настройки Web Bluetooth API.

Инструменты

  • Начало работы с Web Bluetooth — простое веб-приложение, которое сгенерирует весь шаблонный код JavaScript для начала взаимодействия с Bluetooth-устройством. Введите имя устройства, службу, характеристику, определите его свойства, и все готово.
  • Если вы уже являетесь разработчиком Bluetooth, плагин Web Bluetooth Developer Studio также сгенерирует код JavaScript Web Bluetooth для вашего Bluetooth-устройства.

Полезные советы

Страница «Внутренние устройства Bluetooth» доступна в Chrome по адресу about://bluetooth-internals. Здесь вы можете проверить все, что связано с ближайшими Bluetooth-устройствами: состояние, службы, характеристики и дескрипторы.

Снимок экрана внутренней страницы для отладки Bluetooth в Chrome
Внутренняя страница в Chrome для отладки Bluetooth-устройств.

Также рекомендую ознакомиться с официальной страницей «Как зарегистрировать ошибки Web Bluetooth», поскольку отладка Bluetooth порой может быть сложной.

Что дальше

Сначала проверьте состояние реализации браузера и платформы, чтобы узнать, какие части интерфейса Web Bluetooth API реализуются на данный момент.

Интерфейс еще не завершен, но вот краткий анонс того, что ожидается в ближайшем будущем:

  • Поиск ближайших объявлений BLE будет выполняться с помощью navigator.bluetooth.requestLEScan().
  • Новое событие serviceadded будет отслеживать недавно обнаруженные службы Bluetooth GATT, в то время как событие serviceremoved будет отслеживать удаленные. Новое событие servicechanged будет срабатывать, когда какая-либо характеристика или дескриптор добавляются или удаляются из службы Bluetooth GATT.

Поддержите API

Собираетесь использовать Web Bluetooth API? Ваша публичная поддержка помогает команде Chrome расставлять функции в порядке приоритетности и показывает другим поставщикам браузеров, насколько важно их поддерживать.

Отправьте твит на @ChromiumDev, используя хэштег #WebBluetooth и расскажите нам, где и как вы его используете.

Ресурсы

Благодарности

Спасибо Кэйси Баскесу за рецензирование этой статьи. Баннер предоставлен SparkFun Electronics, Боулдер, США.