API del puente de JavaScript
Web Bluetooth
La estructura de navigator.bluetooth, respondida de forma nativa para que una página escrita para Chrome funcione sin cambios.
wbConnect puente#
window.WebToApk.wbConnect(requestId: String, deviceId: String)
Transporte sin procesar para WebBluetoothPolyfill. Una página nunca llama a esto directamente: llama a navigator.bluetooth, exactamente como lo haría en un navegador.
Descrito por su grupo, Web Bluetooth (navigator.bluetooth), en lugar de por separado.
Ejemplo
Transporte interno detrás de device.gatt.connect(). Las páginas usan navigator.bluetooth; no llames a esto directamente.
Devuelve: (mediante gatt.connect()) una Promise del BluetoothRemoteGATTServer, después de descubrir los servicios del dispositivo. Requiere: Bluetooth (activado por una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth).
async function connectAndList(device) {
try {
var server = await device.gatt.connect();
var services = await server.getPrimaryServices();
services.forEach(function (s) { console.log('service', s.uuid); });
return server;
} catch (err) {
showStatus('Could not connect: ' + err.message); // NetworkError, NotFoundError
return null;
}
}Vuelve a conectar cuando el dispositivo se desconecta. Llamar a connect() de nuevo sobre el mismo dispositivo funciona:
device.addEventListener('gattserverdisconnected', async function () {
showStatus('Reconnecting...');
try { await connectAndList(device); showStatus('Connected'); } catch (e) {}
});Conéctate a dos sensores al mismo tiempo, por ejemplo una banda de frecuencia cardíaca y un medidor de potencia:
async function connectBoth(hrDevice, powerDevice) {
var servers = await Promise.all([hrDevice.gatt.connect(), powerDevice.gatt.connect()]);
var hr = await servers[0].getPrimaryService('heart_rate');
var power = await servers[1].getPrimaryService('cycling_power');
return {
heartRate: await hr.getCharacteristic('heart_rate_measurement'),
power: await power.getCharacteristic('cycling_power_measurement')
};
}Notas: Firma directa wbConnect(requestId, deviceId). Conectar a un dispositivo que ya está conectado se resuelve de nuevo al instante. getPrimaryService(s) se rechaza con NetworkError antes de que connect() haya terminado. Se pueden conectar varios dispositivos al mismo tiempo.
wbDisconnect puente#
window.WebToApk.wbDisconnect(deviceId: String)
Transporte sin procesar para WebBluetoothPolyfill. Una página nunca llama a esto directamente: llama a navigator.bluetooth, exactamente como lo haría en un navegador.
Descrito por su grupo, Web Bluetooth (navigator.bluetooth), en lugar de por separado.
Ejemplo
Transporte interno detrás de device.gatt.disconnect() (y de device.forget()). Las páginas usan navigator.bluetooth; no llames a esto directamente.
Devuelve: nada (disconnect() es síncrono, como en el estándar web). Después el dispositivo dispara gattserverdisconnected. Requiere: Bluetooth (se activa en una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth).
var device = null; // from navigator.bluetooth.requestDevice(...)
document.getElementById('stop').addEventListener('click', function () {
if (device && device.gatt.connected) device.gatt.disconnect();
});
// Fires for your own disconnect() and when the device goes away by itself.
function watch(dev) {
dev.addEventListener('gattserverdisconnected', function () {
showStatus('Disconnected from ' + (dev.name || dev.id));
});
}Distingue tu propio Stop de que el sensor salga del alcance, y reconecta solo en el segundo caso:
var userStopped = false;
function stopSession(device) {
userStopped = true;
if (device.gatt.connected) device.gatt.disconnect();
}
function watchDrops(device) {
device.addEventListener('gattserverdisconnected', function () {
if (userStopped) { userStopped = false; showStatus('Session ended'); return; }
showStatus('Sensor out of range, reconnecting...');
device.gatt.connect()
.then(resubscribe) // your code: get the characteristics, startNotifications()
.catch(function () { showStatus('Could not reconnect'); });
});
}Un botón «Quitar sensor»: forget() desconecta y descarta el dispositivo, y no sigue ningún gattserverdisconnected:
var device = null; // from navigator.bluetooth.requestDevice(...)
document.getElementById('forget').addEventListener('click', async function () {
if (!device) return;
await device.forget();
device = null;
showStatus('Sensor removed. Tap Connect to pick one again.');
});Notas: Firma directa wbDisconnect(deviceId). Tras una desconexión, los objetos de servicio y característica antiguos dejan de recibir notificaciones; después del siguiente connect(), vuelve a obtenerlos y llama a startNotifications() de nuevo. La app corta todas las conexiones al cerrarse.
wbGetCharacteristics puente#
window.WebToApk.wbGetCharacteristics(requestId: String, deviceId: String, service: String)
Transporte sin procesar para WebBluetoothPolyfill. Una página nunca llama a esto directamente: llama a navigator.bluetooth, exactamente como lo haría en un navegador.
Descrito por su grupo, Web Bluetooth (navigator.bluetooth), en lugar de por separado.
Ejemplo
Transporte interno detrás de service.getCharacteristic(uuid) y service.getCharacteristics(). Las páginas usan navigator.bluetooth; no llames a esto directamente.
Devuelve: (mediante el servicio) una Promise de un BluetoothRemoteGATTCharacteristic, o de una matriz de ellos, cada uno con uuid y properties (read, write, writeWithoutResponse, notify, indicate, broadcast, authenticatedSignedWrites). Requiere: Bluetooth (activado por una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth).
async function describe(server) {
var service = await server.getPrimaryService('heart_rate');
// One characteristic, by standard name, 16-bit number or full UUID:
var measurement = await service.getCharacteristic('heart_rate_measurement'); // same as 0x2a37
// All of them:
var list = await service.getCharacteristics();
list.forEach(function (c) {
var p = c.properties;
console.log(c.uuid, p.read ? 'read' : '', p.write ? 'write' : '', p.notify ? 'notify' : '');
});
return measurement;
}Encuentra las características de un servicio personalizado por lo que pueden hacer, en lugar de escribir sus UUID a mano en el código:
async function findSerialPipes(server) {
var svc = await server.getPrimaryService('0000ffe0-0000-1000-8000-00805f9b34fb');
var list = await svc.getCharacteristics();
var incoming = list.find(function (c) { return c.properties.notify || c.properties.indicate; });
var outgoing = list.find(function (c) { return c.properties.write || c.properties.writeWithoutResponse; });
if (!incoming || !outgoing) throw new Error('This device does not look like a serial adapter');
return { incoming: incoming, outgoing: outgoing };
}Lee una característica opcional y trata NotFoundError como "este dispositivo no la tiene":
var LOCATIONS = ['Other', 'Chest', 'Wrist', 'Finger', 'Hand', 'Ear lobe', 'Foot'];
async function sensorLocation(server) {
var hr = await server.getPrimaryService('heart_rate');
try {
var chr = await hr.getCharacteristic('body_sensor_location'); // 0x2a38, optional in the spec
var v = await chr.readValue();
return LOCATIONS[v.getUint8(0)] || 'Unknown';
} catch (err) {
if (err.name === 'NotFoundError') return null; // this strap does not report it
throw err;
}
}Notas: Firma sin procesar wbGetCharacteristics(requestId, deviceId, service). Un UUID que el servicio no tiene se rechaza con NotFoundError; un nombre desconocido lanza TypeError (pasa un UUID completo para características personalizadas). La lista se obtiene una vez por servicio y luego se reutiliza.
wbIsAvailable puente#
window.WebToApk.wbIsAvailable(): Boolean
getAvailability(): si el teléfono TIENE Bluetooth (la respuesta de Chrome), no si está encendido. requestDevice() pide al usuario que lo encienda; responder false mientras estaba simplemente apagado hacía que las páginas que comprueban primero ("if (!await getAvailability())") dijeran «Bluetooth no compatible» y nunca llegaran a ese aviso (informe de fitness 2026-10-03).
Ejemplo
Transporte interno detrás de navigator.bluetooth.getAvailability(). Las páginas usan navigator.bluetooth; no llames a esto directamente.
Devuelve: (mediante getAvailability()) una Promise de true cuando la compilación tiene Bluetooth y el Bluetooth del teléfono está activado, y false en caso contrario. Requiere: Bluetooth (activado por una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth).
async function updateBluetoothHint() {
var hint = document.getElementById('bt-hint');
if (!navigator.bluetooth) { hint.textContent = 'Bluetooth is not available in this app.'; return; }
var on = await navigator.bluetooth.getAvailability();
hint.textContent = on ? '' : 'Turn on Bluetooth to connect your sensor.';
}
document.addEventListener('visibilitychange', function () {
if (!document.hidden) updateBluetoothHint();
});Activa el botón Conectar solo mientras el Bluetooth esté encendido, comprobando de nuevo cada vez que el usuario vuelva a la app:
async function refreshConnectButton() {
var btn = document.getElementById('connect');
if (!navigator.bluetooth) { btn.hidden = true; return; }
btn.disabled = !(await navigator.bluetooth.getAvailability());
}
window.addEventListener('load', refreshConnectButton);
document.addEventListener('visibilitychange', function () {
if (!document.hidden) refreshConnectButton();
});Muestra un mensaje claro de «El Bluetooth está apagado» antes de abrir el selector, en lugar de un NotFoundError después:
document.getElementById('scan').addEventListener('click', async function () {
if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
if (!(await navigator.bluetooth.getAvailability())) {
showStatus('Bluetooth is off. Turn it on, then tap Scan again.');
return;
}
try {
var device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true });
showStatus('Picked ' + (device.name || device.id));
} catch (err) {
showStatus(err.name === 'NotFoundError' ? 'No device chosen.' : err.message);
}
});Notas: Firma directa wbIsAvailable(): Boolean (síncrona). Solo comprueba el adaptador: no pide ningún permiso. El evento availabilitychanged del estándar web no se activa, así que comprueba de nuevo cuando la página se vuelva visible.
wbNotify puente#
window.WebToApk.wbNotify(
Transporte sin procesar para WebBluetoothPolyfill. Una página nunca llama a esto directamente: llama a navigator.bluetooth, exactamente como lo haría en un navegador.
Descrito por su grupo, Web Bluetooth (navigator.bluetooth), en lugar de por separado.
Ejemplo
Transporte interno detrás de characteristic.startNotifications() y stopNotifications(). Las páginas usan navigator.bluetooth; no llames a esto directamente.
Devuelve: (mediante la característica) una Promise de la propia característica. Cada nuevo valor activa characteristicvaluechanged en ella, con el valor en event.target.value (un DataView). Requiere: Bluetooth (activado por una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth).
var cadenceChr = null;
async function startCadence(server) {
var svc = await server.getPrimaryService('running_speed_and_cadence');
cadenceChr = await svc.getCharacteristic('rsc_measurement');
cadenceChr.addEventListener('characteristicvaluechanged', onRsc);
await cadenceChr.startNotifications();
}
function onRsc(e) {
var v = e.target.value;
var kmh = v.getUint16(1, true) / 256 * 3.6;
var spm = v.getUint8(3);
show('run', kmh.toFixed(1) + ' km/h, ' + spm + ' steps/min');
}
async function stopCadence() {
if (!cadenceChr) return;
cadenceChr.removeEventListener('characteristicvaluechanged', onRsc);
try { await cadenceChr.stopNotifications(); } catch (e) {}
}Un termómetro cuya medición solo admite indicaciones se suscribe de la misma manera, aquí mediante la propiedad oncharacteristicvaluechanged:
async function watchThermometer(server) {
var svc = await server.getPrimaryService('health_thermometer');
var chr = await svc.getCharacteristic('temperature_measurement');
chr.oncharacteristicvaluechanged = function (e) {
var v = e.target.value;
var mantissa = v.getUint32(1, true) & 0x00ffffff; // IEEE-11073 FLOAT
if (mantissa & 0x800000) mantissa -= 0x1000000;
var fahrenheit = v.getUint8(0) & 1;
show('temp', (mantissa * Math.pow(10, v.getInt8(4))).toFixed(1) + (fahrenheit ? ' °F' : ' °C'));
};
await chr.startNotifications();
}Recibe líneas de texto de un dispositivo de tipo serie, uniendo las notificaciones que parten una línea por la mitad:
async function listenForLines(server, onLine) {
var uart = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e');
var tx = await uart.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e');
if (!tx.properties.notify && !tx.properties.indicate) throw new Error('This device does not stream data');
var decoder = new TextDecoder(), buffer = '';
tx.addEventListener('characteristicvaluechanged', function (e) {
buffer += decoder.decode(e.target.value, { stream: true });
var lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(onLine);
});
await tx.startNotifications();
return tx;
}Notas: Firma directa wbNotify(requestId, deviceId, service, characteristic, enable). Una característica que solo admite indicaciones se suscribe automáticamente con indicaciones. Una característica que no puede notificar se rechaza con NotSupportedError. oncharacteristicvaluechanged = fn también funciona. Tras una desconexión, vuelve a suscribirte.
wbRead puente#
window.WebToApk.wbRead(requestId: String, deviceId: String, service: String, characteristic: String)
Transporte sin procesar para WebBluetoothPolyfill. Una página nunca llama a esto directamente: llama a navigator.bluetooth, exactamente como lo haría en un navegador.
Descrito por su grupo, Web Bluetooth (navigator.bluetooth), en lugar de por separado.
Ejemplo
Transporte interno detrás de characteristic.readValue(). Las páginas usan navigator.bluetooth; no llames a esto directamente.
Devuelve: (mediante readValue()) una Promise de un DataView; characteristic.value también se actualiza. Requiere: Bluetooth (activado por una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth).
async function readInfo(server) {
try {
var battery = await server.getPrimaryService('battery_service');
var level = await (await battery.getCharacteristic('battery_level')).readValue();
show('battery', level.getUint8(0) + '%');
var info = await server.getPrimaryService('device_information');
var maker = await (await info.getCharacteristic('manufacturer_name_string')).readValue();
show('maker', new TextDecoder().decode(maker));
} catch (err) {
// NotFoundError: the device has no such service/characteristic
// NotSupportedError: read rejected by the device NetworkError: not connected / read failed
show('info', err.message);
}
}Consulta periódicamente un sensor que solo se puede leer, aquí la característica estándar Temperature (0x2a6e, en centésimas de grado):
var pollTimer = null;
async function startTemperaturePolling(server) {
var env = await server.getPrimaryService('environmental_sensing');
var temp = await env.getCharacteristic(0x2a6e);
pollTimer = setInterval(async function () {
try {
var v = await temp.readValue();
show('temp', (v.getInt16(0, true) / 100).toFixed(1) + ' °C');
} catch (err) {
clearInterval(pollTimer); // NetworkError once the sensor is gone
show('temp', err.message);
}
}, 5000);
}Rellena un panel "Acerca de este dispositivo" iniciando varias lecturas a la vez; se ejecutan una tras otra por ti:
async function readDeviceInfo(server) {
var info = await server.getPrimaryService('device_information');
var names = ['model_number_string', 'serial_number_string', 'firmware_revision_string'];
var decoder = new TextDecoder();
var results = await Promise.allSettled(names.map(async function (name) {
var chr = await info.getCharacteristic(name);
return decoder.decode(await chr.readValue());
}));
results.forEach(function (r, i) {
show(names[i], r.status === 'fulfilled' ? r.value : '-'); // missing ones reject with NotFoundError
});
}Notas: Firma sin procesar wbRead(requestId, deviceId, service, characteristic). Las lecturas, escrituras y cambios de notificaciones en un dispositivo se ejecutan una tras otra en el orden en que las llamas, así que puedes lanzar varias sin esperar entre ellas.
wbRequestDevice puente#
window.WebToApk.wbRequestDevice(requestId: String, optionsJson: String)
Transporte sin procesar para WebBluetoothPolyfill. Una página nunca llama a esto directamente: llama a navigator.bluetooth, exactamente como lo haría en un navegador.
Descrito por su grupo, Web Bluetooth (navigator.bluetooth), en lugar de por separado.
Ejemplo
Transporte interno detrás de navigator.bluetooth.requestDevice(options): muestra el selector de dispositivos de la app. Las páginas usan navigator.bluetooth.requestDevice; no llames a esto directamente.
Devuelve: (mediante requestDevice) una Promise de un BluetoothDevice {id, name, gatt}. Requiere: Bluetooth (activado por una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth) y el permiso de Dispositivos cercanos, que se pide en el primer uso.
document.getElementById('pick').addEventListener('click', async function () {
if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
try {
var device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['cycling_power'] }, { namePrefix: 'KICKR' }],
optionalServices: ['battery_service']
});
showStatus('Picked ' + (device.name || device.id));
} catch (err) {
if (err.name === 'NotFoundError') showStatus('No device chosen.'); // also: Bluetooth off
else showStatus(err.name + ': ' + err.message); // SecurityError = permission
}
});Para listar todos los dispositivos cercanos en lugar de filtrar: requestDevice({ acceptAllDevices: true }).
Elige un dispositivo personalizado por su nombre exacto o por un prefijo del nombre, indicando su propio servicio de 128 bits para poder usarlo después de conectar:
async function pickLamp() {
try {
return await navigator.bluetooth.requestDevice({
filters: [{ name: 'Desk Lamp' }, { namePrefix: 'LAMP-' }], // either one matches
optionalServices: ['0000ffe0-0000-1000-8000-00805f9b34fb']
});
} catch (err) {
if (err.name === 'SecurityError') showStatus('Allow Nearby devices to find your lamp.');
else if (err.name !== 'NotFoundError') showStatus(err.message); // NotFoundError = cancelled
return null;
}
}Pide el dispositivo una vez por sesión y reutilízalo en los toques siguientes (getDevices() no está disponible, así que conserva el objeto que obtuviste):
var scale = null;
document.getElementById('weigh').addEventListener('click', async function () {
if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
try {
if (!scale) {
scale = await navigator.bluetooth.requestDevice({
filters: [{ services: ['weight_scale'], namePrefix: 'MI' }] // both keys must match
});
}
var server = await scale.gatt.connect();
var svc = await server.getPrimaryService('weight_scale');
var chr = await svc.getCharacteristic('weight_measurement');
chr.addEventListener('characteristicvaluechanged', onWeight);
await chr.startNotifications();
} catch (err) {
showStatus(err.name + ': ' + err.message);
}
});Notas: Firma directa wbRequestDevice(requestId, optionsJson); la respuesta vuelve al polyfill, no a la página. Llama a requestDevice desde un toque. Los filtros usan services (deben estar anunciados), name (exacto) y namePrefix; un filtro coincide cuando coinciden todas sus claves, y el dispositivo coincide cuando coincide cualquier filtro. Ni filters ni acceptAllDevices → TypeError. Cancelar el selector se rechaza con NotFoundError. El selector busca durante 30 segundos. device.id es la dirección Bluetooth del dispositivo.
wbWrite puente#
window.WebToApk.wbWrite(
Transporte sin procesar para WebBluetoothPolyfill. Una página nunca llama a esto directamente: llama a navigator.bluetooth, exactamente como lo haría en un navegador.
Descrito por su grupo, Web Bluetooth (navigator.bluetooth), en lugar de por separado.
Ejemplo
Transporte interno detrás de characteristic.writeValue(), writeValueWithResponse() y writeValueWithoutResponse(). Las páginas usan navigator.bluetooth; no llames a esto directamente.
Devuelve: (a través de la característica) una Promise de undefined. Requiere: Bluetooth (se activa en una compilación ZIP o HTML cuando tu código menciona navigator.bluetooth).
Con respuesta (writeValue es lo mismo): se resuelve cuando el dispositivo confirma la escritura.
async function setTargetPower(server, watts) {
var ftms = await server.getPrimaryService('fitness_machine');
var cp = await ftms.getCharacteristic('00002ad9-0000-1000-8000-00805f9b34fb'); // FTMS control point
await cp.writeValueWithResponse(new Uint8Array([0x00])); // request control
var cmd = new DataView(new ArrayBuffer(3));
cmd.setUint8(0, 0x05); // set target power
cmd.setInt16(1, watts, true);
await cp.writeValueWithResponse(cmd);
}Sin respuesta: más rápido, para flujos donde un paquete perdido no importa.
async function sendText(characteristic, text) {
if (!characteristic.properties.writeWithoutResponse) throw new Error('This characteristic needs writeValue');
await characteristic.writeValueWithoutResponse(new TextEncoder().encode(text));
}Envía un comando de texto largo en trozos de 20 bytes. La app no pide al dispositivo un tamaño de paquete mayor, así que 20 bytes es lo que cabe en una escritura:
async function sendLine(rx, text) {
var bytes = new TextEncoder().encode(text + '\n');
for (var i = 0; i < bytes.length; i += 20) {
await rx.writeValueWithoutResponse(bytes.subarray(i, i + 20));
}
}
// sendLine(rxCharacteristic, 'SET COLOR 255 120 0 BRIGHTNESS 80');Notas: Firma sin procesar wbWrite(requestId, deviceId, service, characteristic, valueBase64, withResponse). El valor puede ser un ArrayBuffer, cualquier arreglo tipado, un DataView o un arreglo simple de bytes; cualquier otra cosa lanza TypeError. Errores: NotSupportedError (escritura rechazada), NetworkError (la escritura falló o no hay conexión), NotFoundError.