Cómo comunicarse con dispositivos Bluetooth a través de JavaScript

La API de Web Bluetooth permite que los sitios web se comuniquen con dispositivos Bluetooth.

François Beaufort
François Beaufort

¿Qué pasaría si te dijera que los sitios web pueden comunicarse con dispositivos Bluetooth cercanos de una manera segura y que preserva la privacidad? De esta manera, los monitores de frecuencia cardíaca, las bombillas cantantes y hasta las tortugas podrían interactuar directamente con un sitio web.

Hasta ahora, la capacidad de interactuar con dispositivos Bluetooth solo era posible para apps específicas de la plataforma. El objetivo de la API de Web Bluetooth es cambiar esto y también llevarlo a los navegadores web.

Antes de comenzar

En este documento, se supone que tienes algunos conocimientos básicos sobre el funcionamiento de Bluetooth de bajo consumo (BLE) y el perfil de atributos genéricos.

Aunque la especificación de la API de Web Bluetooth aún no está finalizada, los autores de la especificación están buscando activamente desarrolladores entusiastas que prueben esta API y proporcionen comentarios sobre la especificación y comentarios sobre la implementación.

Hay un subconjunto de la API de Web Bluetooth disponible en ChromeOS, Chrome para Android 6.0, Mac (Chrome 56) y Windows 10 (Chrome 70). Esto significa que deberías poder solicitar dispositivos Bluetooth de bajo consumo y conectarte a ellos cercanos, leer y write las características de Bluetooth, recibir notificaciones GATT, saber cuando se desconecta un dispositivo Bluetooth y también leer y escribir en descriptores de Bluetooth. Consulta la tabla Compatibilidad del navegador de MDN para obtener más información.

Para Linux y versiones anteriores de Windows, habilita la marca #experimental-web-platform-features en about://flags.

Disponible para pruebas de origen

Para obtener la mayor cantidad posible de comentarios de los desarrolladores que usan la API de Bluetooth web en el campo, Chrome agregó esta función en Chrome 53 como una prueba de origen para ChromeOS, Android y Mac.

La prueba finalizó correctamente en enero de 2017.

Requisitos de seguridad

Para comprender las compensaciones de seguridad, te recomiendo la publicación del Modelo de seguridad de Bluetooth web de Jeffrey Yasskin, un ingeniero de software del equipo de Chrome, que trabaja en la especificación de la API de Web Bluetooth.

Solo HTTPS

Como esta API experimental es una nueva y potente función que se agrega a la Web, solo está disponible en contextos seguros. Esto significa que deberás compilar teniendo en cuenta TLS.

Se requiere un gesto del usuario

Como función de seguridad, la detección de dispositivos Bluetooth con navigator.bluetooth.requestDevice debe activarse con un gesto del usuario, como un toque o un clic con el mouse. Nos referimos a escuchar eventos pointerup, click y touchend.

button.addEventListener('pointerup', function(event) {
  // Call navigator.bluetooth.requestDevice
});

Entra al código

La API de Bluetooth Web depende en gran medida de las promesas de JavaScript. Si no estás familiarizado con ellas, consulta este excelente instructivo sobre promesas. Un elemento más: () => {} son las funciones de flecha de ECMAScript 2015.

Solicita dispositivos Bluetooth

Esta versión de la especificación de la API de Web Bluetooth permite que los sitios web, que se ejecutan en el rol de elemento central, se conecten a servidores GATT remotos a través de una conexión BLE. Admite la comunicación entre dispositivos que implementan Bluetooth 4.0 o versiones posteriores.

Cuando un sitio web solicita acceso a dispositivos cercanos mediante navigator.bluetooth.requestDevice, el navegador le solicita al usuario un selector de dispositivos donde puede elegir uno o cancelar la solicitud.

Mensaje del usuario de dispositivos Bluetooth.

La función navigator.bluetooth.requestDevice() toma un objeto obligatorio que define los filtros. Estos filtros se usan para mostrar solo dispositivos que coinciden con algunos servicios de Bluetooth GATT anunciados o con el nombre del dispositivo.

Filtro de servicios

Por ejemplo, para solicitar dispositivos Bluetooth que promocionen el servicio de batería GATT de Bluetooth, haz lo siguiente:

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

Sin embargo, si tu servicio GATT de Bluetooth no está en la lista de servicios GATT de Bluetooth estandarizados, puedes proporcionar el UUID de Bluetooth completo o un formulario corto de 16 o 32 bits.

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

Filtro de nombre

También puedes solicitar dispositivos Bluetooth según el nombre del dispositivo que se anuncia con la clave de filtros name o incluso un prefijo de este nombre con la clave de filtros namePrefix. Ten en cuenta que, en este caso, también deberás definir la clave optionalServices para poder acceder a cualquier servicio que no esté incluido en un filtro de servicios. De lo contrario, recibirás un error más adelante cuando intentes acceder a ellos.

navigator.bluetooth.requestDevice({
  filters: [{
    name: 'Francois robot'
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Filtro de datos del fabricante

También es posible solicitar dispositivos Bluetooth según los datos específicos del fabricante que se anuncian con la clave de filtros manufacturerData. Esta clave es un array de objetos con una clave obligatoria de identificador de empresa Bluetooth llamada companyIdentifier. También puedes proporcionar un prefijo de datos que filtre los datos del fabricante desde los dispositivos Bluetooth que comienzan con él. Ten en cuenta que también deberás definir la clave optionalServices para poder acceder a cualquier servicio que no se incluya en un filtro de servicios. De lo contrario, recibirás un error más adelante cuando intentes acceder a ellos.

// Filter Bluetooth devices from Google company with manufacturer data bytes
// that start with [0x01, 0x02].
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

También se puede usar una máscara con un prefijo de datos para hacer coincidir algunos patrones en los datos del fabricante. Para obtener más información, consulta la explicación sobre los filtros de datos de Bluetooth.

Filtros de exclusión

La opción exclusionFilters de navigator.bluetooth.requestDevice() te permite excluir algunos dispositivos del selector del navegador. Se puede usar para excluir dispositivos que coinciden con un filtro más amplio, pero que no son compatibles.

// Request access to a bluetooth device whose name starts with "Created by".
// The device named "Created by Francois" has been reported as unsupported.
navigator.bluetooth.requestDevice({
  filters: [{
    namePrefix: "Created by"
  }],
  exclusionFilters: [{
    name: "Created by Francois"
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Sin filtros

Por último, en lugar de filters, puedes usar la clave acceptAllDevices para mostrar todos los dispositivos Bluetooth cercanos. También deberás definir la clave optionalServices para poder acceder a algunos servicios. De lo contrario, recibirás un error más adelante cuando intentes acceder a ellos.

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Conexión a un dispositivo Bluetooth

¿Qué debes hacer ahora que tienes un BluetoothDevice? Conectémonos al servidor GATT remoto de Bluetooth, que contiene las definiciones de servicio y característica.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // Human-readable name of the device.
  console.log(device.name);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

Cómo leer una característica Bluetooth

Aquí, nos conectamos al servidor GATT del dispositivo Bluetooth remoto. Ahora, queremos obtener un servicio GATT principal y leer una característica que pertenezca a este servicio. Por ejemplo, probemos leer el nivel de carga actual de la batería del dispositivo.

En el siguiente ejemplo, battery_level es la característica estandarizada de nivel de batería.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
  // Getting Battery Service…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Getting Battery Level Characteristic…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Reading Battery Level…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

Si usas una característica personalizada de Bluetooth GATT, puedes proporcionar el UUID de Bluetooth completo o un formulario corto de 16 o 32 bits a service.getCharacteristic.

Ten en cuenta que también puedes agregar un objeto de escucha de eventos characteristicvaluechanged a una característica para controlar la lectura de su valor. Consulta el ejemplo de cambio de valor de la característica de lectura para ver cómo controlar de forma opcional las próximas notificaciones de GATT.


.then(characteristic => {
  // Set up event listener for when characteristic value changes.
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // Reading Battery Level…
  return characteristic.readValue();
})
.catch(error => { console.error(error); });

function handleBatteryLevelChanged(event) {
  const batteryLevel = event.target.value.getUint8(0);
  console.log('Battery percentage is ' + batteryLevel);
}

Cómo escribir en una característica Bluetooth

Escribir en una característica GATT de Bluetooth es tan fácil como leerlo. Esta vez, vamos a usar el punto de control de frecuencia cardíaca para restablecer el valor del campo Energía gastada a 0 en un dispositivo monitor de frecuencia cardíaca.

Te prometo que no hay magia aquí. Todo se explica en la página de características del punto de control de la frecuencia cardíaca.

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 => {
  // Writing 1 is the signal to reset energy expended.
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });

Cómo recibir notificaciones GATT

Ahora, veamos cómo recibir notificaciones cuando cambie la característica Medición de frecuencia cardíaca en el dispositivo:

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('Notifications have been started.');
})
.catch(error => { console.error(error); });

function handleCharacteristicValueChanged(event) {
  const value = event.target.value;
  console.log('Received ' + value);
  // TODO: Parse Heart Rate Measurement value.
  // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

En el ejemplo de notificaciones, se muestra cómo detener las notificaciones con stopNotifications() y quitar correctamente el objeto de escucha de eventos characteristicvaluechanged agregado.

Cómo desconectarse de un dispositivo Bluetooth

Para ofrecer una mejor experiencia del usuario, te recomendamos que escuches los eventos de desconexión y que invites al usuario a volver a conectarse:

navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
  // Set up event listener for when device gets disconnected.
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

function onDisconnected(event) {
  const device = event.target;
  console.log(`Device ${device.name} is disconnected.`);
}

También puedes llamar a device.gatt.disconnect() para desconectar tu app web del dispositivo Bluetooth. Esto activará los objetos de escucha de eventos gattserverdisconnected existentes. Ten en cuenta que NO detendrá la comunicación con el dispositivo Bluetooth si otra app ya se está comunicando con este. Consulta el ejemplo de desconexión de dispositivos y el ejemplo de reconexión automática para obtener más información.

Lee y escribe en descriptores Bluetooth

Los descriptores GATT de Bluetooth son atributos que describen un valor de característica. Puedes leerlos y escribirlos de una manera similar a las características de Bluetooth GATT.

Veamos, por ejemplo, cómo leer la descripción del usuario del intervalo de medición del termómetro de estado del dispositivo.

En el siguiente ejemplo, health_thermometer es el servicio de Health Thermometer, measurement_interval es la característica Intervalo de medición y gatt.characteristic_user_description es el descriptor de descripción del usuario de la característica.

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(`User Description: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });

Ahora que leímos la descripción del usuario del intervalo de medición del termómetro de estado del dispositivo, veamos cómo actualizarlo y escribir un valor personalizado.

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('Defines the time between measurements.');
  return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });

Muestras, demostraciones y codelabs

Se probaron correctamente todos los ejemplos de Web Bluetooth que aparecen a continuación. Para disfrutar de estos ejemplos al máximo, te recomiendo que instales la [app para Android de BLE Peripheral Simulator] que simula un periférico BLE con un servicio de batería, un servicio de frecuencia cardíaca o un servicio de termómetro de salud.

Principiante

  • Device Info: Recupera información básica del dispositivo desde un dispositivo BLE.
  • Nivel de batería: Recupera información de la batería de un dispositivo BLE que anuncia información de la batería.
  • Restablecer energía: Restablece la energía gastada por un dispositivo BLE que anuncia frecuencia cardíaca.
  • Characteristic Properties: Muestra todas las propiedades de una característica específica de un dispositivo BLE.
  • Notificaciones: Inicia y detiene las notificaciones de características desde un dispositivo BLE.
  • Desconexión del dispositivo: Desconecta un dispositivo BLE y recibe notificaciones de esta después de conectarlo.
  • Get Characteristics: Obtén todas las características de un servicio anunciado desde un dispositivo BLE.
  • Get Descriptors: Obtén todos los descriptores de características de un servicio anunciado desde un dispositivo BLE.
  • Filtro de datos del fabricante: Recupera información básica del dispositivo de un dispositivo BLE que coincide con los datos del fabricante.
  • Filtros de exclusión: Recupera información básica del dispositivo desde un dispositivo BLE con filtros de exclusión básicos.

Combina varias operaciones

Consulta también nuestras demostraciones seleccionadas de Bluetooth web y los codelabs oficiales de Bluetooth web.

Bibliotecas

  • web-bluetooth-utils es un módulo de npm que agrega algunas funciones convenientes a la API.
  • Hay un shim de la API de Bluetooth Web disponible en noble, el módulo central de BLE de Node.js más popular. Esto te permite usar webpack/browserify noble sin necesidad de un servidor WebSocket ni otros complementos.
  • angular-web-bluetooth es un módulo para Angular que abstrae todo el código de plantilla necesario para configurar la API de Web Bluetooth.

Herramientas

  • Comienza a usar Web Bluetooth es una app web simple que generará todo el código de plantilla de JavaScript para comenzar a interactuar con un dispositivo Bluetooth. Ingresa un nombre de dispositivo, un servicio, una característica, define sus propiedades y listo.
  • Si ya eres desarrollador de Bluetooth, el complemento Web Bluetooth Developer Studio también generará el código JavaScript de Web Bluetooth para tu dispositivo Bluetooth.

Sugerencias

En Chrome, está disponible la página Bluetooth Internals en about://bluetooth-internals para que puedas inspeccionar todo sobre los dispositivos Bluetooth cercanos: estado, servicios, características y descriptores.

Captura de pantalla de la página interna para depurar Bluetooth en Chrome
Es una página interna de Chrome para depurar dispositivos Bluetooth.

También te recomiendo que consultes la página oficial Cómo informar errores de Bluetooth web, ya que la depuración de Bluetooth puede ser difícil en ocasiones.

¿Qué sigue?

Verifica el estado de implementación del navegador y la plataforma primero para saber qué partes de la API de Web Bluetooth se están implementando en este momento.

Aunque aún no está completa, aquí tienes un adelanto de lo que puedes esperar en un futuro cercano:

  • La búsqueda de anuncios BLE cercanos se realizará con navigator.bluetooth.requestLEScan().
  • Un nuevo evento serviceadded hará un seguimiento de los servicios de Bluetooth GATT recién descubiertos, mientras que el evento serviceremoved hará un seguimiento de los que se hayan quitado. Se activará un nuevo evento servicechanged cuando se agregue o quite cualquier característica o descriptor de un servicio GATT de Bluetooth.

Cómo mostrar compatibilidad con la API

¿Piensas usar la API de Web Bluetooth? Tu apoyo público ayuda al equipo de Chrome a priorizar las funciones y les muestra a otros proveedores de navegadores lo importante que es admitirlas.

Envía un tweet a @ChromiumDev con el hashtag #WebBluetooth y cuéntanos dónde y cómo lo usas.

Recursos

Agradecimientos

Gracias a Kayce Basques por revisar este artículo. Imagen hero de SparkFun Electronics de Boulder, EE.UU..