Crear APK gratis

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.

navigator.bluetooth estándar web#

navigator.bluetooth — Web Bluetooth

Ejemplo

Web Bluetooth estándar: el mismo código navigator.bluetooth que escribirías para Chrome funciona en la app instalada, con cualquier dispositivo Bluetooth Low Energy.

Devuelve: Promises, como en el estándar web. Los valores son DataViews. Requiere: Bluetooth. No hay interruptor: cuando compilas desde un ZIP o una página HTML, la compilación lo activa por sí sola si tu código menciona navigator.bluetooth (o BluetoothUUID). Una compilación de sitio web (URL) no puede activarlo. Sin él, navigator.bluetooth no existe. El primer requestDevice() pide el permiso de Dispositivos cercanos (Ubicación en Android 11 y anteriores). Si el Bluetooth está apagado, la app pide al usuario que lo encienda y luego abre la lista de dispositivos por sí sola; en Android 6 a 11 hace lo mismo con el ajuste de Ubicación, porque esas versiones no encuentran ningún dispositivo Bluetooth mientras la Ubicación está desactivada.

Banda de frecuencia cardíaca, desde un toque en un botón. navigator.bluetooth existe antes de que se ejecute tu primer script, así que la detección de funciones al principio de la página funciona; comprobar dentro del controlador, como aquí, sigue siendo el hábito más seguro:

var hrDevice = null;

document.getElementById('connect').addEventListener('click', async function () {
  if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
  try {
    hrDevice = await navigator.bluetooth.requestDevice({
      filters: [{ services: ['heart_rate'] }],        // or { name: 'Polar H10' } / { namePrefix: 'Polar' }
      optionalServices: ['battery_service']
    });
    hrDevice.addEventListener('gattserverdisconnected', function () {
      showStatus('Strap disconnected');
    });
    var server = await hrDevice.gatt.connect();
    var hr = await server.getPrimaryService('heart_rate');
    var chr = await hr.getCharacteristic('heart_rate_measurement');
    chr.addEventListener('characteristicvaluechanged', function (e) {
      var v = e.target.value;                                   // DataView
      var bpm = (v.getUint8(0) & 1) ? v.getUint16(1, true) : v.getUint8(1);
      document.getElementById('bpm').textContent = bpm;
    });
    await chr.startNotifications();

    var battery = await server.getPrimaryService('battery_service');
    var level = await (await battery.getCharacteristic('battery_level')).readValue();
    document.getElementById('battery').textContent = level.getUint8(0) + '%';
  } catch (err) {
    // NotFoundError: user cancelled the chooser, Bluetooth is off, or no such service.
    // SecurityError: permission denied.  NetworkError: connection lost.
    showStatus(err.name + ': ' + err.message);
  }
});

document.getElementById('disconnect').addEventListener('click', function () {
  if (hrDevice && hrDevice.gatt.connected) hrDevice.gatt.disconnect();
});

Escritura. writeValue y writeValueWithResponse esperan a que el dispositivo confirme; writeValueWithoutResponse no:

async function sendCommands(server) {
  // Heart Rate Control Point: 0x01 = reset energy expended
  var hr = await server.getPrimaryService('heart_rate');
  var ctrl = await hr.getCharacteristic('heart_rate_control_point');
  await ctrl.writeValueWithResponse(new Uint8Array([0x01]));

  // A custom UART-style service (full 128-bit UUIDs work everywhere)
  var uart = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e');
  var rx = await uart.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e');
  await rx.writeValueWithoutResponse(new TextEncoder().encode('LED ON\n'));
}

Explorador de dispositivos. Elige cualquier dispositivo cercano y lista todos sus servicios y características, con lo que puede hacer cada una:

document.getElementById('explore').addEventListener('click', async function () {
  if (!navigator.bluetooth) { showStatus('Bluetooth is not available in this app.'); return; }
  try {
    var device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true });
    var server = await device.gatt.connect();
    var lines = [];
    for (var service of await server.getPrimaryServices()) {
      lines.push(service.uuid === BluetoothUUID.getService('battery_service') ? 'Battery' : service.uuid);
      var chars = await service.getCharacteristics().catch(function () { return []; });
      chars.forEach(function (c) {
        var can = Object.keys(c.properties).filter(function (k) { return c.properties[k]; });
        lines.push('  ' + c.uuid + '  ' + can.join(', '));
      });
    }
    document.getElementById('tree').textContent = lines.join('\n');
    device.gatt.disconnect();
  } catch (err) {
    showStatus(err.name + ': ' + err.message);
  }
});

Sensor de velocidad y cadencia de ciclismo. Un sensor (o un par separado de velocidad y cadencia) en el servicio estándar CSC. Los valores son totales acumulados, así que la velocidad y la cadencia salen de la diferencia entre dos notificaciones:

var WHEEL_MM = 2105;                       // 700x25c; set your own wheel circumference
var last = null;

async function connectCsc() {
  var device = await navigator.bluetooth.requestDevice({
    filters: [{ services: ['cycling_speed_and_cadence'] }]      // same as { services: [0x1816] }
  });
  device.addEventListener('gattserverdisconnected', function () { showStatus('Sensor disconnected'); });
  var server = await device.gatt.connect();
  var csc = await server.getPrimaryService('cycling_speed_and_cadence');
  var chr = await csc.getCharacteristic('csc_measurement');
  chr.addEventListener('characteristicvaluechanged', function (e) {
    var v = e.target.value, flags = v.getUint8(0), i = 1, now = {};
    if (flags & 1) { now.wheels = v.getUint32(i, true); now.wheelTime = v.getUint16(i + 4, true); i += 6; }
    if (flags & 2) { now.cranks = v.getUint16(i, true); now.crankTime = v.getUint16(i + 2, true); }
    if (last) {
      if (now.wheels !== undefined && now.wheelTime !== last.wheelTime) {
        var dt = ((now.wheelTime - last.wheelTime) & 0xffff) / 1024;          // seconds
        var kmh = (now.wheels - last.wheels) * WHEEL_MM / 1e6 / (dt / 3600);
        document.getElementById('speed').textContent = kmh.toFixed(1) + ' km/h';
      }
      if (now.cranks !== undefined && now.crankTime !== last.crankTime) {
        var ct = ((now.crankTime - last.crankTime) & 0xffff) / 1024;
        var rpm = ((now.cranks - last.cranks) & 0xffff) / ct * 60;
        document.getElementById('cadence').textContent = Math.round(rpm) + ' rpm';
      }
    }
    last = Object.assign({}, last, now);
  });
  await chr.startNotifications();
}

Mostrar cada fallo en pantalla. Cada paso se rechaza con un error con nombre, para que la página pueda decirle al usuario qué hacer en lugar de quedarse en silencio:

async function connectWithMessage(connect) {
  try {
    await connect();
    showStatus('Connected');
  } catch (err) {
    var msg = {
      NotFoundError: 'No sensor chosen, Bluetooth was left off, or the sensor lacks that service.',
      SecurityError: 'Bluetooth permission was refused. Allow "Nearby devices" in the app settings.',
      NetworkError: 'The sensor went out of range or is connected to another app or watch. Try again.',
      TypeError: 'Programming error: ' + err.message
    }[err.name] || err.message;
    showStatus(err.name + ': ' + msg);
  }
}
document.getElementById('scan-hr').addEventListener('click', function () { connectWithMessage(connectHeartRate); });
document.getElementById('scan-csc').addEventListener('click', function () { connectWithMessage(connectCsc); });

Notas: El selector de dispositivos lo dibuja la app y busca durante 30 segundos, luego ofrece "Buscar de nuevo"; una página solo puede acceder a un dispositivo que el usuario haya elegido. getAvailability() responde si el teléfono tiene Bluetooth en absoluto (como en Chrome), no si está encendido: requestDevice() pide encenderlo. Un sensor que envía su nombre solo en su respuesta de escaneo aparece igualmente con su nombre y coincide con namePrefix. Una primera conexión que Android corta de inmediato se reintenta antes de que connect() la rechace. Un nombre de servicio mal escrito en filters o optionalServices se rechaza con TypeError. Un filtro services solo coincide con los servicios que el dispositivo anuncia. Después de connect() se puede usar cualquier servicio que tenga el dispositivo, incluso los que no están en optionalServices (déjalos de todos modos, Chrome los necesita). También disponibles: getAvailability(), getPrimaryServices(), getCharacteristics(), stopNotifications(), characteristic.properties, device.forget(), BluetoothUUID.getService/getCharacteristic/canonicalUUID. getDevices(), requestLEScan(), watchAdvertisements() y los descriptores se rechazan con NotSupportedError. Para los sensores deportivos estándar con los números ya interpretados, consulta bleStartScan.

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.

Generado a partir del runtime de la app y sus archivos de ejemplo en cada compilación de la documentación. Léelo como Markdown · Todas las familias.

Comprobado con el puente publicado el 2026-10-22.