JavaScript bridge API
Web Bluetooth
The navigator.bluetooth shape, answered natively so a page written for Chrome works unchanged.
wbConnect bridge#
window.WebToApk.wbConnect(requestId: String, deviceId: String)
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind device.gatt.connect(). Pages use navigator.bluetooth; do not call this directly.
Returns: (through gatt.connect()) a Promise of the BluetoothRemoteGATTServer, after the device's services are discovered. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions 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;
}
}Reconnect when the device drops. Calling connect() again on the same device works:
device.addEventListener('gattserverdisconnected', async function () {
showStatus('Reconnecting...');
try { await connectAndList(device); showStatus('Connected'); } catch (e) {}
});Notes: Raw signature wbConnect(requestId, deviceId). Connecting to a device that is already connected resolves again at once. getPrimaryService(s) rejects with NetworkError before connect() has finished. Several devices can be connected at the same time.
wbDisconnect bridge#
window.WebToApk.wbDisconnect(deviceId: String)
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind device.gatt.disconnect() (and device.forget()). Pages use navigator.bluetooth; do not call this directly.
Returns: nothing (disconnect() is synchronous, as in the web standard). The device then fires gattserverdisconnected. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions 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));
});
}Notes: Raw signature wbDisconnect(deviceId). After a disconnect the old service and characteristic objects stop receiving notifications; after the next connect(), get them again and call startNotifications() again. The app drops every connection when it closes.
wbGetCharacteristics bridge#
window.WebToApk.wbGetCharacteristics(requestId: String, deviceId: String, service: String)
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind service.getCharacteristic(uuid) and service.getCharacteristics(). Pages use navigator.bluetooth; do not call this directly.
Returns: (through the service) a Promise of one BluetoothRemoteGATTCharacteristic, or of an array of them, each with uuid and properties (read, write, writeWithoutResponse, notify, indicate, broadcast, authenticatedSignedWrites). Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions 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;
}Notes: Raw signature wbGetCharacteristics(requestId, deviceId, service). A UUID the service does not have rejects with NotFoundError; an unknown name throws TypeError (pass a full UUID for custom characteristics). The list is fetched once per service and then reused.
wbIsAvailable bridge#
window.WebToApk.wbIsAvailable(): Boolean
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind navigator.bluetooth.getAvailability(). Pages use navigator.bluetooth; do not call this directly.
Returns: (through getAvailability()) a Promise of true when the build has Bluetooth and the phone's Bluetooth is on, else false. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions 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();
});Notes: Raw signature wbIsAvailable(): Boolean (synchronous). It checks the adapter only - it does not ask for any permission. The availabilitychanged event of the web standard is not fired, so check again when the page becomes visible.
wbNotify bridge#
window.WebToApk.wbNotify(
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind characteristic.startNotifications() and stopNotifications(). Pages use navigator.bluetooth; do not call this directly.
Returns: (through the characteristic) a Promise of the characteristic itself. Each new value fires characteristicvaluechanged on it, with the value in event.target.value (a DataView). Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions 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) {}
}Notes: Raw signature wbNotify(requestId, deviceId, service, characteristic, enable). A characteristic that only supports indications is subscribed with indications automatically. A characteristic that cannot notify rejects with NotSupportedError. oncharacteristicvaluechanged = fn works too. After a disconnect, subscribe again.
wbRead bridge#
window.WebToApk.wbRead(requestId: String, deviceId: String, service: String, characteristic: String)
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind characteristic.readValue(). Pages use navigator.bluetooth; do not call this directly.
Returns: (through readValue()) a Promise of a DataView; characteristic.value is updated too. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions 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);
}
}Notes: Raw signature wbRead(requestId, deviceId, service, characteristic). Reads, writes and notification changes on one device run one after another in the order you call them, so you can fire several without waiting in between.
wbRequestDevice bridge#
window.WebToApk.wbRequestDevice(requestId: String, optionsJson: String)
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind navigator.bluetooth.requestDevice(options): it shows the app's device chooser. Pages use navigator.bluetooth.requestDevice; do not call this directly.
Returns: (through requestDevice) a Promise of a BluetoothDevice {id, name, gatt}. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth) and the Nearby devices permission, asked on first use.
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
}
});To list every nearby device instead of filtering: requestDevice({ acceptAllDevices: true }).
Notes: Raw signature wbRequestDevice(requestId, optionsJson); the answer comes back to the polyfill, not to the page. Call requestDevice from a tap. Filters use services (must be advertised), name (exact) and namePrefix; a filter matches when all its keys match, and the device matches when any filter matches. Neither filters nor acceptAllDevices → TypeError. Cancelling the chooser rejects with NotFoundError. The chooser scans for 30 seconds. device.id is the device's Bluetooth address.
wbWrite bridge#
window.WebToApk.wbWrite(
Raw transport for WebBluetoothPolyfill. A page never calls these directly - it calls navigator.bluetooth, exactly as it would in a browser.
Described by its group, Web Bluetooth (navigator.bluetooth), rather than on its own.
Example
Internal transport behind characteristic.writeValue(), writeValueWithResponse() and writeValueWithoutResponse(). Pages use navigator.bluetooth; do not call this directly.
Returns: (through the characteristic) a Promise of undefined. Needs: Bluetooth (turned on by a ZIP or HTML build when your code mentions navigator.bluetooth).
With response (writeValue is the same): resolves when the device confirms the write.
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);
}Without response: faster, for streams where a lost packet does not matter.
async function sendText(characteristic, text) {
if (!characteristic.properties.writeWithoutResponse) throw new Error('This characteristic needs writeValue');
await characteristic.writeValueWithoutResponse(new TextEncoder().encode(text));
}Notes: Raw signature wbWrite(requestId, deviceId, service, characteristic, valueBase64, withResponse). The value can be an ArrayBuffer, any typed array, a DataView or a plain array of bytes; anything else throws TypeError. Errors: NotSupportedError (write rejected), NetworkError (write failed or not connected), NotFoundError.