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 que preserva la privacidad? De esta manera, los monitores de frecuencia cardíaca, las bombillas que cantan e incluso 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. La API de Web Bluetooth tiene como objetivo cambiar esto y también lo lleva a los navegadores web.

Antes de comenzar

En este documento, se asume que tienes 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á completa, los autores de especificaciones buscan activamente que desarrolladores entusiastas prueben esta API y envíen comentarios sobre las especificaciones 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 y conectarte a dispositivos Bluetooth de bajo consumo cercanos, leer y escribir las características de Bluetooth, recibir notificaciones GATT, saber cuándo se desconecta un dispositivo Bluetooth e incluso leer y escribir en descriptores Bluetooth. Consulta la tabla de 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 Web Bluetooth en el campo, Chrome agregó esta función a Chrome 53 como 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 recomendamos leer la publicación sobre el 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 Bluetooth web.

Solo HTTPS

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

Gesto del usuario requerido

Como función de seguridad, el descubrimiento de dispositivos Bluetooth con navigator.bluetooth.requestDevice se debe activar mediante un gesto del usuario, como un toque o un clic del mouse. Estamos hablando de escuchar los eventos pointerup, click y touchend.

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

Ingresa al código

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

Cómo solicitar dispositivos Bluetooth

Esta versión de la especificación de la API de Web Bluetooth permite que los sitios web que se ejecutan con la función 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 un dispositivo o cancelar la solicitud.

Mensaje del usuario del dispositivo Bluetooth.

La función navigator.bluetooth.requestDevice() toma un objeto obligatorio que define filtros. Estos filtros se usan para mostrar solo dispositivos que coincidan con algunos servicios 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 Bluetooth GATT, haz lo siguiente:

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

Sin embargo, si tu servicio Bluetooth GATT no está en la lista de servicios estandarizados de Bluetooth GATT, puedes proporcionar el UUID completo de Bluetooth 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 por 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 ese 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, aparecerá 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 en función de 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 de Bluetooth llamada companyIdentifier. También puedes proporcionar un prefijo de datos que filtre los datos del fabricante de los dispositivos Bluetooth que comiencen con él. Ten en cuenta que también deberás definir la clave optionalServices para poder acceder a cualquier servicio que no esté incluido en un filtro. De lo contrario, aparecerá 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 de filtros de datos de Bluetooth.

Filtros de exclusión

La opción exclusionFilters de navigator.bluetooth.requestDevice() te permite excluir algunos dispositivos del selector de 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 tecla 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, aparecerá un error más adelante cuando intentes acceder a ellas.

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

Conectarse a un dispositivo Bluetooth

Entonces, ¿qué haces ahora que tienes un BluetoothDevice? Conectémonos al servidor GATT remoto Bluetooth que contiene las definiciones de servicio y características.

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); });

Leer una característica de Bluetooth

Aquí nos conectamos al servidor GATT del dispositivo Bluetooth remoto. Ahora queremos obtener un servicio GATT principal y leer una característica que pertenece a este servicio. Por ejemplo, intentemos 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 completo de Bluetooth 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 en una característica para controlar la lectura de su valor. Consulta Cómo leer una muestra de cambio de valor de característica para ver cómo controlar, de manera 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 leerla. Esta vez, usaremos el punto de control de frecuencia cardíaca para restablecer el valor del campo Energía gastada a 0 en un monitor de frecuencia cardíaca.

Prometo que no hay magia en este lugar. Todo se explica en la página Características de puntos 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); });

Recibir notificaciones GATT

Ahora, veamos cómo recibir notificaciones cuando la característica de medición de la frecuencia cardíaca cambia 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 indica 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 brindar una mejor experiencia del usuario, es posible que quieras escuchar los eventos de desconexión y, luego, invitar 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 del dispositivo Bluetooth si otra app ya se está comunicando con el dispositivo Bluetooth. Consulta el ejemplo de desconexión del dispositivo y el ejemplo de reconexión automática para obtener más información.

Lee y escribe en descriptores de Bluetooth

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

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

En el siguiente ejemplo, health_thermometer es el servicio de Termómetro de salud, measurement_interval es la característica del intervalo de medición y gatt.characteristic_user_description el descriptor de descripción de usuario de características.

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 ya leímos la descripción del usuario del intervalo de medición del termómetro de salud 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 todas las muestras de Bluetooth web que aparecen a continuación. Para disfrutar al máximo estas muestras, te recomendamos instalar la [app de Simulador de periféricos de BLE para Android], 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

  • Información del dispositivo: Recupera información básica del dispositivo desde un dispositivo BLE.
  • Nivel de batería: Recupera información sobre la batería desde un dispositivo BLE con información sobre la batería.
  • Reset Energy: Restablece la energía gastada de una frecuencia cardíaca con publicidad en un dispositivo BLE.
  • Propiedades de las características: Se muestran todas las propiedades de una característica específica de un dispositivo BLE.
  • Notificaciones: Inicia y detén notificaciones de características de un dispositivo BLE.
  • Desconexión del dispositivo: Desconecta un dispositivo BLE y recibe una notificación cuando se desconecta de él.
  • Obtener características: Obtén todas las características de un servicio anunciado desde un dispositivo BLE.
  • Obtener descriptores: Obtiene todos los descriptores de las características de un servicio anunciado desde un dispositivo BLE.
  • Filtro de datos del fabricante: Recupera información básica del dispositivo desde un dispositivo BLE que coincida con los datos del fabricante.
  • Filtros de exclusión: Recupera información básica del dispositivo desde un dispositivo BLE que incluya filtros de exclusión básicos.

Cómo combinar varias operaciones

Consulta también nuestras demostraciones de Bluetooth web seleccionadas 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 una corrección de compatibilidad de la API de Bluetooth web disponible en noble, el módulo central de Node.js BLE más popular. Esto te permite webpack/browserify noble sin la necesidad de un servidor WebSocket u otros complementos.
  • angular-web-Bluetooth es un módulo para Angular que abstrae todo el código estándar necesario para configurar la API de Web Bluetooth.

Herramientas

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

Sugerencias

Hay una página de Bluetooth Internals disponible en Chrome en about://bluetooth-internals para que puedas inspeccionar todo lo que haya sobre dispositivos Bluetooth cercanos: estado, servicios, características y descriptores.

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

También te recomendamos consultar la página oficial Cómo informar errores de Bluetooth web, ya que la depuración de Bluetooth puede ser difícil a veces.

Próximos pasos

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

Aunque todavía está incompleto, aquí te mostramos un adelanto de lo que puedes esperar en el futuro cercano:

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

Muestra compatibilidad con la API

¿Tienes pensado usar la API de Web Bluetooth? Tu asistencia pública ayuda al equipo de Chrome a priorizar funciones y le muestra a otros proveedores de navegadores la importancia de brindar compatibilidad.

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

Recursos

Agradecimientos

Agradecemos a Kayce Basques por leer este artículo. Hero image de SparkFun Electronics de Boulder, EE.UU..