Una introducción a WebBluetooth

Publicado: 2022-03-10
Resumen rápido ↬ Con Progressive Web Apps, ahora puede usar la web para crear aplicaciones completas. Gracias a una enorme cantidad de nuevas especificaciones y características, podemos hacer cosas con la web para las que antes necesitabas escribir aplicaciones nativas. Sin embargo, hablar con dispositivos de hardware todavía era un puente demasiado lejano hasta ahora. Gracias a WebBluetooth, ahora podemos crear PWA que pueden controlar sus luces, conducir un automóvil o incluso controlar un dron.

Con Progressive Web Apps, la web se ha acercado cada vez más a las aplicaciones nativas. Sin embargo, con los beneficios adicionales que son inherentes a la web, como la privacidad y la compatibilidad entre plataformas.

Tradicionalmente, la web ha sido fantástica para comunicarse con los servidores de la red y específicamente con los servidores de Internet. Ahora que la web se está moviendo hacia las aplicaciones, también necesitamos las mismas capacidades que tienen las aplicaciones nativas.

La cantidad de nuevas especificaciones y funciones que se han implementado en los últimos años en los navegadores es asombrosa. Tenemos especificaciones para trabajar con 3D, como WebGL y la próxima WebGPU. Podemos transmitir y generar audio, ver videos y usar la cámara web como dispositivo de entrada. También podemos ejecutar código a velocidades casi nativas usando WebAssembly. Además, a pesar de ser inicialmente un medio solo de red, la web se ha movido hacia el soporte fuera de línea con trabajadores de servicio.

Eso es genial y todo, pero un área ha sido casi el dominio exclusivo de las aplicaciones nativas: la comunicación con los dispositivos. Ese es un problema que hemos estado tratando de resolver durante mucho tiempo, y es algo que probablemente todos hayan encontrado en algún momento. La web es excelente para hablar con servidores, pero no para hablar con dispositivos . Piense, por ejemplo, en intentar configurar un enrutador en su red. Lo más probable es que haya tenido que ingresar una dirección IP y usar una interfaz web a través de una conexión HTTP simple sin ningún tipo de seguridad. Eso es solo una mala experiencia y mala seguridad. Además de eso, ¿cómo sabes cuál es la dirección IP correcta?

¡Más después del salto! Continúe leyendo a continuación ↓

HTTP también es el primer problema con el que nos encontramos cuando intentamos crear una aplicación web progresiva que intenta comunicarse con un dispositivo. Los PWA son solo HTTPS y los dispositivos locales siempre son solo HTTP. Necesita un certificado para HTTPS y, para obtener un certificado, necesita un servidor disponible públicamente con un nombre de dominio (me refiero a dispositivos en nuestra red local que están fuera de alcance).

Entonces, para muchos dispositivos, necesita aplicaciones nativas para configurar los dispositivos y usarlos porque las aplicaciones nativas no están sujetas a las limitaciones de la plataforma web y pueden ofrecer una experiencia agradable para sus usuarios. Sin embargo, no quiero descargar una aplicación de 500 MB para hacer eso. Tal vez el dispositivo que tiene ya tenga algunos años y la aplicación nunca se actualizó para ejecutarse en su nuevo teléfono. Tal vez desee usar una computadora de escritorio o portátil, y el fabricante solo creó una aplicación móvil. Tampoco es una experiencia ideal.

WebBluetooth es una nueva especificación que se ha implementado en Chrome y Samsung Internet que nos permite comunicarnos directamente con dispositivos Bluetooth Low Energy desde el navegador. Las aplicaciones web progresivas en combinación con WebBluetooth ofrecen la seguridad y la comodidad de una aplicación web con el poder de comunicarse directamente con los dispositivos.

Bluetooth tiene un nombre bastante malo debido a su alcance limitado, mala calidad de audio y problemas de emparejamiento. Pero, casi todos esos problemas son cosa del pasado. Bluetooth Low Energy es una especificación moderna que tiene poco que ver con las antiguas especificaciones de Bluetooth , además de utilizar el mismo espectro de frecuencia. Más de 10 millones de dispositivos se envían con soporte Bluetooth todos los días. Eso incluye computadoras y teléfonos, pero también una variedad de dispositivos como monitores de frecuencia cardíaca y glucosa, dispositivos IoT como bombillas y juguetes como autos y drones controlados a distancia.

Lectura recomendada : Comprender las plataformas basadas en API: una guía para gerentes de productos

La parte teórica aburrida

Dado que Bluetooth en sí mismo no es una tecnología web, utiliza un vocabulario que puede parecernos desconocido. Entonces, repasemos cómo funciona Bluetooth y parte de la terminología.

Cada dispositivo Bluetooth es un 'dispositivo central' o un 'periférico'. Solo los dispositivos centrales pueden iniciar la comunicación y solo pueden hablar con los periféricos. Un ejemplo de dispositivo central sería un ordenador o un teléfono móvil.

Un periférico no puede iniciar la comunicación y solo puede hablar con un dispositivo central. Además, un periférico solo puede hablar con un dispositivo central al mismo tiempo. Un periférico no puede comunicarse con otro periférico.

un teléfono en el medio, hablando con varios periféricos, como un dron, un robot de juguete, un monitor de frecuencia cardíaca y una bombilla
Un dispositivo central puede comunicarse con múltiples periféricos. (Vista previa grande)

Un dispositivo central puede hablar con varios periféricos al mismo tiempo y puede transmitir mensajes si así lo desea. Por lo tanto, un monitor de frecuencia cardíaca no podría hablar con sus bombillas, sin embargo, podría escribir un programa que se ejecute en un dispositivo central que reciba su frecuencia cardíaca y encienda las luces rojas si la frecuencia cardíaca supera un cierto umbral.

Cuando hablamos de WebBluetooth, nos referimos a una parte específica de la especificación de Bluetooth llamada Perfil de atributo genérico, que tiene la abreviatura muy obvia GATT. (Aparentemente, GAP ya estaba tomado).

En el contexto del GATT, ya no estamos hablando de dispositivos centrales y periféricos, sino de clientes y servidores. Sus bombillas son servidores. Eso puede parecer contrario a la intuición, pero en realidad tiene sentido si lo piensas. La bombilla ofrece un servicio, es decir, la luz. Al igual que cuando el navegador se conecta a un servidor en Internet, su teléfono o computadora es un cliente que se conecta al servidor GATT en la bombilla.

Cada servidor ofrece uno o más servicios. Algunos de esos servicios son oficialmente parte del estándar, pero también puede definir los suyos propios. En el caso del pulsómetro, existe un servicio oficial definido en el pliego de condiciones. En el caso de la bombilla, no la hay, y casi todos los fabricantes intentan reinventar la rueda. Cada servicio tiene una o más características. Cada característica tiene un valor que se puede leer o escribir. Por ahora, sería mejor pensar en él como una matriz de objetos, cada uno de los cuales tiene propiedades que tienen valores.

la jerarquía de servicios y características en comparación con construcciones más familiares de JavaScript: un servidor es similar a una matriz de objetos, un servicio a un objeto en esa matriz, una característica a una propiedad de ese objeto y ambos tienen valores
Una jerarquía simplificada de servicios y características. (Vista previa grande)

A diferencia de las propiedades de los objetos, los servicios y las características no se identifican mediante una cadena. Cada servicio y característica tiene un UUID único que puede tener una longitud de 16 o 128 bits. Oficialmente, el UUID de 16 bits está reservado para los estándares oficiales, pero casi nadie sigue esa regla. Finalmente, cada valor es una matriz de bytes. No hay tipos de datos sofisticados en Bluetooth.

Una mirada más cercana a una bombilla de luz Bluetooth

Así que echemos un vistazo a un dispositivo Bluetooth real: una Mipow Playbulb Sphere. Puede usar una aplicación como BLE Scanner o nRF Connect para conectarse al dispositivo y ver todos los servicios y características. En este caso, estoy usando la aplicación BLE Scanner para iOS.

Lo primero que ve cuando se conecta a la bombilla es una lista de servicios. Hay algunos estandarizados como el servicio de información del dispositivo y el servicio de batería. Pero también hay algunos servicios personalizados. Estoy particularmente interesado en el servicio con el UUID de 16 bits de 0xff0f . Si abre este servicio, puede ver una larga lista de características. No tengo idea de qué hacen la mayoría de estas características, ya que solo se identifican mediante un UUID y porque, lamentablemente, forman parte de un servicio personalizado; no están estandarizados y el fabricante no proporcionó ninguna documentación.

La primera característica con el UUID de 0xfffc parece particularmente interesante. Tiene un valor de cuatro bytes. Si cambiamos el valor de estos bytes de 0x00000000 a 0x00ff0000 , la bombilla se vuelve roja. Cambiarlo a 0x0000ff00 hace que la bombilla se vuelva verde y 0x000000ff azul. Estos son colores RGB y corresponden exactamente a los colores hexadecimales que usamos en HTML y CSS.

¿Qué hace ese primer byte? Bueno, si cambiamos el valor a 0xff000000 , la bombilla se vuelve blanca. La bombilla contiene cuatro LED diferentes y, cambiando el valor de cada uno de los cuatro bytes, podemos crear todos los colores que queramos.

La API WebBluetooth

Es fantástico que podamos usar una aplicación nativa para cambiar el color de una bombilla, pero ¿cómo hacemos esto desde el navegador? Resulta que con el conocimiento sobre Bluetooth y GATT que acabamos de aprender, esto es relativamente sencillo gracias a la API de WebBluetooth. Solo se necesitan un par de líneas de JavaScript para cambiar el color de una bombilla.

Repasemos la API WebBluetooth.

Conexión a un dispositivo

Lo primero que debemos hacer es conectarnos desde el navegador al dispositivo. Llamamos a la función navigator.bluetooth.requestDevice() y proporcionamos a la función un objeto de configuración. Ese objeto contiene información sobre qué dispositivo queremos usar y qué servicios deberían estar disponibles para nuestra API.

En el siguiente ejemplo, estamos filtrando por el nombre del dispositivo, ya que solo queremos ver los dispositivos que contienen el prefijo PLAYBULB en el nombre. También estamos especificando 0xff0f como un servicio que queremos usar. Dado que la función requestDevice() devuelve una promesa, podemos esperar el resultado.

 let device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix: 'PLAYBULB' } ], optionalServices: [ 0xff0f ] });

Cuando llamamos a esta función, aparece una ventana con la lista de dispositivos que se ajustan a los filtros que hemos especificado. Ahora tenemos que seleccionar el dispositivo al que queremos conectarnos manualmente. Ese es un paso esencial para la seguridad y la privacidad y le da control al usuario. El usuario decide si la aplicación web puede conectarse y, por supuesto, a qué dispositivo puede conectarse. La aplicación web no puede obtener una lista de dispositivos ni conectarse sin que el usuario seleccione manualmente un dispositivo.

el navegador Chrome con la ventana que el usuario necesita usar para conectarse a un dispositivo, con la bombilla visible en la lista de dispositivos
El usuario tiene que conectarse manualmente seleccionando un dispositivo. (Vista previa grande)

Después de obtener acceso al dispositivo, podemos conectarnos al servidor GATT llamando a la función connect() en la propiedad gatt del dispositivo y esperar el resultado.

 let server = await device.gatt.connect();

Una vez que tenemos el servidor, podemos llamar a getPrimaryService() en el servidor con el UUID del servicio que queremos usar como parámetro y esperar el resultado.

 let service = await server.getPrimaryService(0xff0f);

Luego llame a getCharacteristic() en el servicio con el UUID de la característica como parámetro y espere nuevamente el resultado.

Ahora tenemos nuestras características que podemos usar para escribir y leer datos:

 let characteristic = await service.getCharacteristic(0xfffc);

Escritura de datos

Para escribir datos, podemos llamar a la función writeValue() en la característica con el valor que queremos escribir como un ArrayBuffer, que es un método de almacenamiento de datos binarios. La razón por la que no podemos usar una matriz regular es que las matrices regulares pueden contener datos de varios tipos e incluso pueden tener huecos vacíos.

Dado que no podemos crear o modificar un ArrayBuffer directamente, en su lugar estamos usando una 'matriz tipificada'. Cada elemento de una matriz con tipo es siempre del mismo tipo y no tiene agujeros. En nuestro caso, vamos a usar un Uint8Array , que no tiene signo, por lo que no puede contener números negativos; un número entero, por lo que no puede contener fracciones; y es de 8 bits y puede contener solo valores de 0 a 255. En otras palabras: una matriz de bytes.

 characteristic.writeValue( new Uint8Array([ 0, r, g, b ]) );

Ya sabemos cómo funciona esta bombilla en particular. Tenemos que proporcionar cuatro bytes, uno para cada LED. Cada byte tiene un valor entre 0 y 255, y en este caso solo queremos usar los LED rojo, verde y azul, por lo que dejamos el LED blanco apagado, usando el valor 0.

Lectura de datos

Para leer el color actual de la bombilla, podemos usar la función readValue() y esperar el resultado.

 let value = await characteristic.readValue(); let r = value.getUint8(1); let g = value.getUint8(2); let b = value.getUint8(3);

El valor que obtenemos es un DataView de un ArrayBuffer y ofrece una forma de sacar los datos del ArrayBuffer. En nuestro caso, podemos usar la función getUint8() con un índice como parámetro para extraer los bytes individuales de la matriz.

Cómo recibir notificaciones de cambios

Finalmente, también hay una forma de recibir una notificación cuando cambia el valor de un dispositivo. Eso no es realmente útil para una bombilla, pero para nuestro monitor de frecuencia cardíaca tenemos valores que cambian constantemente, y no queremos sondear el valor actual manualmente cada segundo.

 characteristic.addEventListener( 'characteristicvaluechanged', e => { let r = e.target.value.getUint8(1); let g = e.target.value.getUint8(2); let b = e.target.value.getUint8(3); } ); characteristic.startNotifications();

Para obtener una devolución de llamada cada vez que cambia un valor, debemos llamar a la función addEventListener() en la característica con el parámetro characteristicvaluechanged y una función de devolución de llamada. Siempre que cambie el valor, se llamará a la función de devolución de llamada con un objeto de evento como parámetro, y podemos obtener los datos de la propiedad de valor del objetivo del evento. Y, finalmente, extraiga los bytes individuales nuevamente de DataView de ArrayBuffer.

Debido a que el ancho de banda en la red Bluetooth es limitado, tenemos que iniciar manualmente este mecanismo de notificación llamando a startNotifications() en la característica. De lo contrario, la red se inundará con datos innecesarios. Además, debido a que estos dispositivos suelen usar una batería, cada byte que no tengamos que enviar definitivamente mejorará la duración de la batería del dispositivo porque la radio interna no necesita encenderse con tanta frecuencia.

Conclusión

Ya hemos superado el 90 % de la API de WebBluetooth. Con solo unas pocas llamadas de función y el envío de 4 bytes, puede crear una aplicación web que controle los colores de sus bombillas. Si agrega algunas líneas más, incluso puede controlar un automóvil de juguete o volar un dron. Con más y más dispositivos Bluetooth apareciendo en el mercado, las posibilidades son infinitas.

Más recursos

  • Bluetooth.rocks! demostraciones | (Código fuente en GitHub)
  • “Especificación de Bluetooth web”, Grupo de comunidad de Bluetooth web
  • Registro GATT abierto Una colección no oficial de documentación para servicios de atributos genéricos para dispositivos Bluetooth de bajo consumo.