Explorando la API de Web Bluetooth en PWAs: Conectando Tu Web con el Mundo Físico
Este tutorial te gu guiará a través de la integración de la API de Web Bluetooth en tus Progressive Web Apps. Aprenderás a escanear, conectar y comunicar con dispositivos Bluetooth de baja energía (BLE) desde el navegador, abriendo un mundo de posibilidades para interactuar con hardware físico.
📖 Introducción a la API de Web Bluetooth y las PWAs
Las Progressive Web Apps (PWAs) han revolucionado la forma en que pensamos sobre las aplicaciones web, difuminando la línea entre el entorno web y el nativo. Nos ofrecen capacidades offline, notificaciones push, y la posibilidad de ser instaladas en la pantalla de inicio. Sin embargo, una de las fronteras más emocionantes en la evolución de las PWAs es su capacidad para interactuar directamente con el hardware del dispositivo, y la API de Web Bluetooth es una pieza clave en esta interacción.
Tradicionalmente, la comunicación con dispositivos Bluetooth ha sido un dominio exclusivo de las aplicaciones nativas. Pero con la API de Web Bluetooth, tus PWAs pueden ahora descubrir y comunicarse con dispositivos Bluetooth de baja energía (BLE) cercanos, directamente desde el navegador web. Esto abre un abanico de posibilidades para aplicaciones en domótica, salud, fitness, IoT y mucho más.
En este tutorial, exploraremos en profundidad cómo utilizar la API de Web Bluetooth para escanear, conectar y leer/escribir datos en dispositivos BLE. Nos enfocaremos en un enfoque práctico, proporcionando ejemplos de código y las mejores prácticas para asegurar una experiencia de usuario robusta y segura.
¿Por qué Web Bluetooth en tu PWA? 🤔
Integrar Web Bluetooth en una PWA ofrece ventajas significativas:
- Accesibilidad: No requiere instalación desde una tienda de aplicaciones. Un simple enlace web puede dar acceso a la funcionalidad Bluetooth.
- Multiplataforma: Una única base de código web puede funcionar en diferentes sistemas operativos y dispositivos compatibles.
- Desarrollo Rápido: Aprovecha las herramientas y el ecosistema de desarrollo web, que suelen ser más ágiles que el desarrollo nativo.
- Actualizaciones Sencillas: Las actualizaciones de la aplicación se despliegan instantáneamente, sin necesidad de que el usuario descargue una nueva versión.
- Privacidad y Seguridad: La API de Web Bluetooth está diseñada con la seguridad en mente, requiriendo intervención del usuario para cada conexión y limitando el acceso a servicios específicos.
🛠️ Requisitos Previos y Entorno de Desarrollo
Antes de sumergirnos en el código, asegúrate de tener lo siguiente:
- Un navegador compatible con Web Bluetooth (Chrome 56+). Se recomienda usar la última versión de Chrome.
- Un dispositivo Bluetooth de baja energía (BLE) para probar. Puede ser un sensor de temperatura, un microcontrolador (como un ESP32 o Arduino con módulo BLE), o cualquier otro dispositivo que exponga servicios y características BLE.
- Conocimientos básicos de JavaScript, HTML y CSS.
- Un entorno de desarrollo web (VS Code, etc.) y un servidor web local para servir tu PWA (por ejemplo, con
live-servero Node.jshttp-server).
Estructura Básica de Nuestra PWA
Para este tutorial, utilizaremos una estructura de proyecto sencilla:
my-bluetooth-pwa/
├── index.html
├── style.css
├── app.js
├── manifest.json
└── service-worker.js
Por ahora, nos centraremos principalmente en index.html y app.js.
🔎 Escaneando Dispositivos Bluetooth Cercanos
El primer paso para interactuar con un dispositivo BLE es descubrirlo. La API de Web Bluetooth utiliza el método navigator.bluetooth.requestDevice() para mostrar un selector de dispositivos al usuario. Este método es el punto de entrada principal y requiere la intervención del usuario por razones de seguridad y privacidad.
Solicitando Permiso y Filtrando Dispositivos
El método requestDevice() acepta un objeto de opciones que permite filtrar los dispositivos basándose en sus servicios Bluetooth o su nombre. Esto es crucial para mejorar la experiencia del usuario, mostrándole solo los dispositivos relevantes.
Ejemplo de index.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PWA Web Bluetooth</title>
<link rel="stylesheet" href="style.css">
<link rel="manifest" href="manifest.json">
</head>
<body>
<header>
<h1>PWA Web Bluetooth: Conecta y Controla</h1>
</header>
<main>
<section class="hero">
<p>Descubre dispositivos Bluetooth de baja energía y interactúa con ellos.</p>
<button id="connectButton">Conectar a Dispositivo BLE</button>
<div id="deviceInfo" class="card hidden">
<h2>Dispositivo Conectado</h2>
<p><strong>Nombre:</strong> <span id="deviceName"></span></p>
<p><strong>ID:</strong> <span id="deviceId"></span></p>
<p><strong>Estado:</strong> <span id="deviceStatus">Desconectado</span></p>
<button id="disconnectButton" class="secondary-button hidden">Desconectar</button>
</div>
<div id="dataSection" class="card hidden">
<h3>Datos del Dispositivo</h3>
<p><strong>Último Valor:</strong> <span id="lastValue">N/A</span></p>
<button id="readButton">Leer Valor</button>
<input type="text" id="writeValueInput" placeholder="Valor a escribir">
<button id="writeButton">Escribir Valor</button>
</div>
<div id="log" class="card">
<h3>Registro</h3>
<pre id="logOutput"></pre>
</div>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
Ejemplo de app.js (parte de escaneo):
const connectButton = document.getElementById('connectButton');
const disconnectButton = document.getElementById('disconnectButton');
const readButton = document.getElementById('readButton');
const writeButton = document.getElementById('writeButton');
const writeValueInput = document.getElementById('writeValueInput');
const deviceInfo = document.getElementById('deviceInfo');
const deviceNameSpan = document.getElementById('deviceName');
const deviceIdSpan = document.getElementById('deviceId');
const deviceStatusSpan = document.getElementById('deviceStatus');
const dataSection = document.getElementById('dataSection');
const lastValueSpan = document.getElementById('lastValue');
const logOutput = document.getElementById('logOutput');
let currentDevice = null;
let currentCharacteristic = null; // Para la característica de lectura/escritura
function log(message) {
const timestamp = new Date().toLocaleTimeString();
logOutput.textContent += `[${timestamp}] ${message}\n`;
logOutput.scrollTop = logOutput.scrollHeight; // Scroll automático
}
connectButton.addEventListener('click', async () => {
if (!navigator.bluetooth) {
log('Web Bluetooth no está soportado en este navegador.');
deviceStatusSpan.textContent = 'No soportado';
return;
}
log('Buscando dispositivos Bluetooth...');
deviceStatusSpan.textContent = 'Buscando...';
try {
// Definimos los filtros para encontrar nuestro dispositivo BLE
// Aquí usamos un servicio conocido (ej. 'heart_rate') o puedes usar un UUID personalizado
// Para un UUID personalizado, puedes usar ['0000180d-0000-1000-8000-00805f9b34fb'] para heart_rate
// O si conoces el UUID de tu dispositivo (ej. un sensor DHT11 BLE), ponlo aquí.
// También puedes filtrar por nombre, por ejemplo: { namePrefix: 'MiSensor' }
const options = {
// filters: [{ services: ['heart_rate'] }], // Ejemplo: filtrar por servicio de frecuencia cardíaca
// filters: [{ namePrefix: 'ESP32' }], // Ejemplo: filtrar por dispositivos cuyo nombre empiece por 'ESP32'
acceptAllDevices: true, // Permite al usuario seleccionar cualquier dispositivo (menos recomendado en producción)
optionalServices: ['battery_service', '0000ff00-0000-1000-8000-00805f9b34fb'] // Servicios opcionales para acceder después de la conexión
};
currentDevice = await navigator.bluetooth.requestDevice(options);
if (currentDevice) {
log(`Dispositivo seleccionado: ${currentDevice.name || 'Desconocido'} (ID: ${currentDevice.id})`);
deviceNameSpan.textContent = currentDevice.name || 'Desconocido';
deviceIdSpan.textContent = currentDevice.id;
deviceInfo.classList.remove('hidden');
deviceStatusSpan.textContent = 'Seleccionado';
disconnectButton.classList.remove('hidden');
// Escuchar eventos de desconexión del dispositivo
currentDevice.addEventListener('gattserverdisconnected', onDisconnected);
// Proceder a la conexión GATT
await connectToDevice(currentDevice);
} else {
log('Ningún dispositivo seleccionado.');
deviceStatusSpan.textContent = 'Desconectado';
}
} catch (error) {
log(`Error al buscar o seleccionar dispositivo: ${error}`);
deviceStatusSpan.textContent = 'Error';
resetUI();
}
});
function onDisconnected(event) {
log(`Dispositivo ${event.target.name || 'Desconocido'} se ha desconectado.`);
deviceStatusSpan.textContent = 'Desconectado';
resetUI();
}
function resetUI() {
deviceNameSpan.textContent = '';
deviceIdSpan.textContent = '';
deviceInfo.classList.add('hidden');
dataSection.classList.add('hidden');
disconnectButton.classList.add('hidden');
currentDevice = null;
currentCharacteristic = null;
lastValueSpan.textContent = 'N/A';
writeValueInput.value = '';
}
Explicación del código:
navigator.bluetooth.requestDevice(options): Este es el método central. Cuando el usuario hace clic en el botón de conectar, se invoca. El navegador mostrará una ventana emergente con una lista de dispositivos BLE cercanos.filters: Es un array de objetos que especifican qué dispositivos son aceptables. Puedes filtrar porservices(un array de UUIDs de servicios Bluetooth),name(nombre exacto) onamePrefix(prefijo del nombre).acceptAllDevices: true: Si lo usas, se mostrarán todos los dispositivos disponibles, lo cual puede ser abrumador. Es mejor usar filtros específicos cuando sea posible.optionalServices: Los servicios listados aquí no son necesarios para que el dispositivo aparezca en el selector, pero sí para poder acceder a ellos después de la conexión. Es una medida de seguridad para limitar el acceso solo a los servicios que tu aplicación realmente necesita. Deben ser UUIDs válidos.
🤝 Conectando al Servidor GATT y Descubriendo Servicios/Características
Una vez que el usuario ha seleccionado un dispositivo, el siguiente paso es establecer una conexión con su Servidor GATT (Generic Attribute Profile). El servidor GATT es la interfaz que el dispositivo BLE utiliza para exponer sus datos y funcionalidades.
Estableciendo la Conexión GATT
async function connectToDevice(device) {
if (!device.gatt.connected) {
log('Conectando al servidor GATT...');
deviceStatusSpan.textContent = 'Conectando...';
try {
const server = await device.gatt.connect();
log('Conectado al servidor GATT.');
deviceStatusSpan.textContent = 'Conectado';
dataSection.classList.remove('hidden');
// Ahora que estamos conectados, podemos descubrir servicios y características
// Debes reemplazar '0000ff00-0000-1000-8000-00805f9b34fb' con el UUID de tu servicio y característica.
// Este es un ejemplo común para un servicio personalizado simple.
const serviceUuid = '0000ff00-0000-1000-8000-00805f9b34fb'; // UUID del servicio personalizado
const characteristicUuid = '0000ff01-0000-1000-8000-00805f9b34fb'; // UUID de la característica personalizada
const service = await server.getPrimaryService(serviceUuid);
log(`Servicio '${serviceUuid}' encontrado.`);
currentCharacteristic = await service.getCharacteristic(characteristicUuid);
log(`Característica '${characteristicUuid}' encontrada.`);
// Añadir listener para notificaciones si la característica soporta NOTIFY
if (currentCharacteristic.properties.notify) {
await currentCharacteristic.startNotifications();
currentCharacteristic.addEventListener('characteristicvaluechanged', handleCharacteristicNotifications);
log('Notificaciones activadas para la característica.');
}
} catch (error) {
log(`Error al conectar al servidor GATT o descubrir servicios/características: ${error}`);
deviceStatusSpan.textContent = 'Error';
resetUI();
}
} else {
log('Ya conectado al servidor GATT.');
}
}
disconnectButton.addEventListener('click', () => {
if (currentDevice && currentDevice.gatt.connected) {
log('Desconectando dispositivo...');
currentDevice.gatt.disconnect();
// onDisconnected se encargará de resetear la UI
} else {
log('No hay dispositivo conectado para desconectar.');
}
});
Explicación del código:
device.gatt.connect(): Este método establece la conexión GATT con el dispositivo. Retorna una instancia deBluetoothRemoteGATTServeruna vez que la conexión es exitosa.server.getPrimaryService(serviceUuid): Una vez conectado al servidor GATT, puedes solicitar un servicio específico utilizando su UUID. Es crucial conocer los UUIDs de los servicios y características de tu dispositivo BLE.service.getCharacteristic(characteristicUuid): Después de obtener el servicio, puedes acceder a una característica específica dentro de ese servicio mediante su UUID.
↔️ Leyendo y Escribiendo Datos en Características BLE
Con la característica obtenida, podemos realizar operaciones de lectura y escritura. Las operaciones disponibles (read, write, notify) dependen de las propiedades de la característica definidas por el fabricante del dispositivo.
Leyendo Valores de una Característica
readButton.addEventListener('click', async () => {
if (!currentCharacteristic) {
log('No hay característica seleccionada para leer.');
return;
}
log('Leyendo valor de la característica...');
try {
const value = await currentCharacteristic.readValue();
const decoder = new TextDecoder('utf-8'); // O DataView, dependiendo del formato de los datos
const decodedValue = decoder.decode(value);
lastValueSpan.textContent = decodedValue;
log(`Valor leído: ${decodedValue}`);
} catch (error) {
log(`Error al leer valor: ${error}`);
}
});
// Manejador para notificaciones (si la característica las soporta)
function handleCharacteristicNotifications(event) {
const value = event.target.value; // DataView
const decoder = new TextDecoder('utf-8');
const decodedValue = decoder.decode(value);
lastValueSpan.textContent = decodedValue;
log(`Notificación recibida: ${decodedValue}`);
}
Explicación del código:
currentCharacteristic.readValue(): Este método asíncrono lee el valor actual de la característica. Retorna unDataView.TextDecoder: Se utiliza para decodificar los bytes recibidos en una cadena de texto. El formato de los datos puede variar (números, strings, JSON, etc.), así que ajusta el decodificador según sea necesario.startNotifications()ycharacteristicvaluechanged: Si una característica soportanotify, puedes suscribirte a sus cambios. El eventocharacteristicvaluechangedse dispara cada vez que el dispositivo envía una nueva notificación.
Escribiendo Valores en una Característica
writeButton.addEventListener('click', async () => {
if (!currentCharacteristic) {
log('No hay característica seleccionada para escribir.');
return;
}
const valueToWrite = writeValueInput.value;
if (!valueToWrite) {
log('Por favor, ingresa un valor para escribir.');
return;
}
log(`Escribiendo valor '${valueToWrite}' en la característica...`);
try {
const encoder = new TextEncoder('utf-8');
const data = encoder.encode(valueToWrite);
await currentCharacteristic.writeValue(data);
log('Valor escrito con éxito.');
writeValueInput.value = '';
} catch (error) {
log(`Error al escribir valor: ${error}`);
}
});
Explicación del código:
TextEncoder: Convierte una cadena de texto en unUint8Array(bytes) que es el formato esperado porwriteValue().currentCharacteristic.writeValue(data): Envía los bytes al dispositivo para escribir en la característica.
🌐 Configurando la PWA para Web Bluetooth
Para que nuestra aplicación sea una PWA completa y pueda ser instalada, necesitamos un manifest.json y un service-worker.js.
manifest.json
Este archivo define cómo se comportará tu PWA una vez instalada.
{
"name": "PWA Web Bluetooth Connect",
"short_name": "BLE Connect",
"description": "Una Progressive Web App para conectar y controlar dispositivos Bluetooth de baja energía.",
"start_url": ".",
"display": "standalone",
"background_color": "#1a202c",
"theme_color": "#4A90D9",
"icons": [
{
"src": "./icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
(Nota: necesitarías crear las imágenes icon-192x192.png y icon-512x512.png en una carpeta icons.)
service-worker.js
El Service Worker es el corazón de la capacidad offline de tu PWA. Para este tutorial, usaremos un Service Worker básico para cachear los archivos estáticos de la aplicación.
const CACHE_NAME = 'web-bluetooth-pwa-v1';
const urlsToCache = [
'/',
'/index.html',
'/style.css',
'/app.js',
'/manifest.json',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response; // Si está en caché, lo servimos
}
return fetch(event.request); // Si no, vamos a la red
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
Para registrar el Service Worker, añade la siguiente línea a tu app.js (al final):
// ... (resto de app.js)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
log('Service Worker registrado con éxito:', registration.scope);
})
.catch(error => {
log('Fallo en el registro del Service Worker:', error);
});
});
}
🔒 Seguridad y Permisos en Web Bluetooth
La seguridad es una preocupación primordial al interactuar con hardware. Web Bluetooth está diseñado con un modelo de seguridad robusto:
- HTTPS Requerido: Como ya mencionamos, solo funciona en contextos seguros.
- Intervención del Usuario: El usuario siempre debe iniciar la acción de escaneo (
requestDevice()) y seleccionar explícitamente el dispositivo al que se conectará. No hay escaneo automático o silencioso. - Permisos Granulares: Los
optionalServicesaseguran que la aplicación solo pueda acceder a los servicios que ha declarado. El usuario es informado de los servicios a los que la PWA solicita acceso. - Alcance de Permiso: Los permisos se conceden por origen (dominio) y solo mientras el sitio está en primer plano. Si el usuario cierra la pestaña o navega a otro sitio, los permisos se revocan.
Manejo de Errores y Estados
Es fundamental manejar los posibles errores y proporcionar feedback claro al usuario. Algunos errores comunes incluyen:
NotSupportedError: Web Bluetooth no está disponible.NotFoundError: El usuario canceló la selección o no se encontró ningún dispositivo.NetworkError: Problemas de conexión Bluetooth.SecurityError: Intentos de acceder a servicios no autorizados.
Una buena interfaz de usuario debería mostrar mensajes de estado (conectado, desconectado, error, buscando) y guiar al usuario a través del proceso.
✨ Casos de Uso y Futuro de Web Bluetooth en PWAs
Las posibilidades que abre Web Bluetooth son vastas. Aquí algunos ejemplos:
- Salud y Fitness: Conectar con pulsómetros, balanzas inteligentes, glucómetros para registrar datos en una PWA.
- Domótica y IoT: Controlar luces inteligentes, termostatos o cerraduras que usan BLE.
- Retail: Conectar con beacons BLE para ofrecer información contextual o cupones dentro de una tienda.
- Industrial: Monitoreo de sensores en entornos de fábrica o logística.
- Educación: Proyectos interactivos con microcontroladores (ej. micro:bit, ESP32) para enseñar programación y electrónica.
Ejemplos Adicionales de Integración
Ejemplo: Leer múltiples características
Si tu servicio tiene varias características, puedes obtenerlas todas:// ... dentro de connectToDevice ...
const service = await server.getPrimaryService(serviceUuid);
const characteristics = await service.getCharacteristics();
log(`Se encontraron ${characteristics.length} características.`);
for (const char of characteristics) {
log(` - Característica UUID: ${char.uuid}, Propiedades: ${Object.keys(char.properties).filter(key => char.properties[key]).join(', ')}`);
// Puedes guardar referencias a las características que te interesen
}
Ejemplo: Manejo de diferentes formatos de datos
No todos los dispositivos envían datos como texto. Podrías recibir enteros, flotantes, o arrays de bytes. Aquí un ejemplo para leer un entero de 16 bits:
// ... dentro de readButton.addEventListener o handleCharacteristicNotifications ...
const value = await currentCharacteristic.readValue(); // DataView
const temperature = value.getInt16(0, true); // Leer un entero de 16 bits desde el offset 0, little-endian
log(`Temperatura: ${temperature}°C`);
✅ Conclusión
La API de Web Bluetooth empodera a las Progressive Web Apps para trascender las barreras tradicionales del navegador, permitiéndoles interactuar directamente con el mundo físico. Al integrar esta potente API, los desarrolladores pueden crear experiencias de usuario ricas y contextuales que antes solo eran posibles con aplicaciones nativas.
Hemos cubierto los pasos esenciales para escanear, conectar y comunicar con dispositivos BLE desde tu PWA, así como consideraciones importantes sobre seguridad y buenas prácticas. El futuro de las PWAs con Web Bluetooth es brillante, ofreciendo un vasto lienzo para la innovación en el desarrollo móvil y IoT.
¡Anímate a experimentar y construir tu propia PWA con Web Bluetooth! Las posibilidades son infinitas.
Tutoriales relacionados
- Aprovechando la API de Acceso al Sistema de Archivos en PWAs: Persistencia Avanzadaintermediate20 min
- Aprovechando la API de Compartición Web en PWAs: Comparte Contenido de Forma Nativaintermediate15 min
- Aprovechando la API de Sincronización en Segundo Plano en PWAs: Datos Siempre al Díaintermediate18 min
- Explorando la Instalación en PWAs: Add to Home Screen (A2HS) y sus Mecanismosintermediate15 min
- Notificaciones Push en PWA: Re-engagement y Experiencia de Usuario Mejoradaintermediate15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!