Создать APK бесплатно

JavaScript bridge API

Web Bluetooth

Интерфейс navigator.bluetooth, реализованный на уровне приложения, чтобы страница, написанная для Chrome, работала без изменений.

navigator.bluetooth веб-стандарт#

navigator.bluetooth — Web Bluetooth

Пример

Стандартный Web Bluetooth: тот же код navigator.bluetooth, который вы написали бы для Chrome, работает в установленном приложении для любого устройства Bluetooth Low Energy.

Возвращает: Promise, как в веб-стандарте. Значения — это DataView. Требуется: Bluetooth. Переключателя нет: при сборке из ZIP или HTML-страницы сборка включает его сама, если в вашем коде упоминается navigator.bluetooth (или BluetoothUUID). Сборка сайта (URL) включить его не может. Без него navigator.bluetooth не существует. Первый requestDevice() запрашивает разрешение «Устройства поблизости» (Геолокация на Android 11 и старше). Если Bluetooth выключен, приложение просит пользователя включить его и затем само открывает список устройств; на Android 6–11 то же происходит с настройкой «Геолокация», потому что эти версии не находят устройства Bluetooth при выключенной геолокации.

Нагрудный датчик пульса, по нажатию кнопки. navigator.bluetooth существует ещё до запуска вашего первого скрипта, поэтому проверка возможностей в начале страницы работает; проверка внутри обработчика, как здесь, всё равно остаётся самой надёжной привычкой:

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();
});

Запись. writeValue и writeValueWithResponse ждут подтверждения от устройства; writeValueWithoutResponse не ждёт:

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'));
}

Обозреватель устройств. Выберите любое устройство поблизости и выведите все его службы и характеристики с указанием, что умеет каждая:

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);
  }
});

Датчик скорости и каденса велосипеда. Один датчик (или отдельная пара датчиков скорости и каденса) на стандартной службе CSC. Значения — накопленные итоги, поэтому скорость и каденс вычисляются по разности двух уведомлений:

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();
}

Показ каждой ошибки на экране. Каждый шаг завершается отказом с именованной ошибкой, поэтому страница может подсказать пользователю, что делать, вместо того чтобы молчать:

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); });

Примечания: Окно выбора устройства рисует само приложение; оно сканирует 30 секунд, затем предлагает «Сканировать снова»; страница может обратиться только к устройству, которое выбрал пользователь. getAvailability() отвечает, есть ли в телефоне Bluetooth вообще (как в Chrome), а не включён ли он: requestDevice() предлагает его включить. Датчик, который присылает своё имя только в ответе на сканирование, всё равно показывается в списке по имени и подходит под namePrefix. Первое подключение, которое Android сразу разрывает, повторяется до того, как connect() вернёт отказ. Неверно написанное имя службы в filters или optionalServices завершается отказом с TypeError. Фильтр services совпадает только со службами, которые устройство объявляет. После connect() можно использовать все службы устройства, даже те, которых нет в optionalServices (всё равно оставляйте их, Chrome они нужны). Также доступны: getAvailability(), getPrimaryServices(), getCharacteristics(), stopNotifications(), characteristic.properties, device.forget(), BluetoothUUID.getService/getCharacteristic/canonicalUUID. getDevices(), requestLEScan(), watchAdvertisements() и дескрипторы завершаются отказом с NotSupportedError. Про стандартные фитнес-датчики с уже разобранными числами см. bleStartScan.

wbConnect мост#

window.WebToApk.wbConnect(requestId: String, deviceId: String)

Низкоуровневый транспорт для WebBluetoothPolyfill. Страница никогда не вызывает его напрямую — она вызывает navigator.bluetooth, точно так же, как в браузере.

Описано в своей группе, Web Bluetooth (navigator.bluetooth), а не отдельно.

Пример

Внутренний транспорт для device.gatt.connect(). Страницы используют navigator.bluetooth; не вызывайте это напрямую.

Возвращает: (через gatt.connect()) Promise с BluetoothRemoteGATTServer после обнаружения служб устройства. Требуется: Bluetooth (включается сборкой из ZIP или HTML, когда в вашем коде упоминается 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;
  }
}

Переподключайтесь, когда устройство отключается. Повторный вызов connect() для того же устройства работает:

device.addEventListener('gattserverdisconnected', async function () {
  showStatus('Reconnecting...');
  try { await connectAndList(device); showStatus('Connected'); } catch (e) {}
});

Подключитесь к двум датчикам одновременно, например к нагрудному датчику пульса и измерителю мощности:

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')
  };
}

Примечания: Прямая сигнатура wbConnect(requestId, deviceId). Подключение к уже подключённому устройству снова завершается сразу. getPrimaryService(s) отклоняется с NetworkError, пока connect() не завершился. К нескольким устройствам можно подключиться одновременно.

wbDisconnect мост#

window.WebToApk.wbDisconnect(deviceId: String)

Низкоуровневый транспорт для WebBluetoothPolyfill. Страница никогда не вызывает его напрямую — она вызывает navigator.bluetooth, точно так же, как в браузере.

Описано в своей группе, Web Bluetooth (navigator.bluetooth), а не отдельно.

Пример

Внутренний транспорт для device.gatt.disconnect() (и device.forget()). Страницы используют navigator.bluetooth; не вызывайте его напрямую.

Возвращает: ничего (disconnect() синхронный, как в веб-стандарте). Затем устройство вызывает gattserverdisconnected. Требуется: Bluetooth (его включает сборка из ZIP или HTML, когда в вашем коде упоминается 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));
  });
}

Отличайте вашу собственную остановку от выхода датчика из зоны действия и переподключайтесь только во втором случае:

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'); });
  });
}

Кнопка «Удалить датчик»: forget() отключает и забывает устройство, и событие 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.');
});

Примечания: Прямая сигнатура wbDisconnect(deviceId). После отключения старые объекты службы и характеристики перестают получать уведомления; после следующего connect() получите их заново и снова вызовите startNotifications(). Приложение разрывает все подключения при закрытии.

wbGetCharacteristics мост#

window.WebToApk.wbGetCharacteristics(requestId: String, deviceId: String, service: String)

Низкоуровневый транспорт для WebBluetoothPolyfill. Страница никогда не вызывает его напрямую — она вызывает navigator.bluetooth, точно так же, как в браузере.

Описано в своей группе, Web Bluetooth (navigator.bluetooth), а не отдельно.

Пример

Внутренний транспорт для service.getCharacteristic(uuid) и service.getCharacteristics(). Страницы используют navigator.bluetooth; не вызывайте это напрямую.

Возвращает: (через службу) Promise с одним BluetoothRemoteGATTCharacteristic или с массивом таких объектов, у каждого есть uuid и properties (read, write, writeWithoutResponse, notify, indicate, broadcast, authenticatedSignedWrites). Требуется: Bluetooth (включается сборкой из ZIP или HTML, когда в вашем коде упоминается 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;
}

Находите характеристики собственной службы по тому, что они умеют, вместо жёсткой привязки к их UUID:

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 };
}

Прочитайте необязательную характеристику и считайте NotFoundError признаком «у этого устройства её нет»:

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;
  }
}

Примечания: Низкоуровневая сигнатура wbGetCharacteristics(requestId, deviceId, service). UUID, которого нет у службы, завершается отказом с NotFoundError; неизвестное имя вызывает TypeError (для собственных характеристик передавайте полный UUID). Список запрашивается один раз для службы, а затем используется повторно.

wbIsAvailable мост#

window.WebToApk.wbIsAvailable(): Boolean

getAvailability(): есть ли в телефоне Bluetooth (ответ Chrome), а не включён ли он. requestDevice() просит пользователя его включить; ответ false, когда он просто выключен, заставлял страницы, которые проверяют заранее («if (!await getAvailability())»), писать «Bluetooth не поддерживается» и никогда не доходить до этого запроса (отчёт по фитнесу от 2026-10-03).

Пример

Внутренний транспорт для navigator.bluetooth.getAvailability(). Страницы используют navigator.bluetooth; не вызывайте это напрямую.

Возвращает: (через getAvailability()) Promise с true, если в сборке есть Bluetooth и он включён на телефоне, иначе false. Требуется: Bluetooth (включается сборкой из ZIP или HTML, когда в вашем коде упоминается 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();
});

Включайте кнопку «Подключиться» только пока Bluetooth включён, проверяя снова всякий раз, когда пользователь возвращается в приложение:

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();
});

Покажите понятное сообщение «Bluetooth выключен» перед открытием окна выбора, вместо NotFoundError после него:

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);
  }
});

Примечания: Прямая сигнатура wbIsAvailable(): Boolean (синхронно). Проверяется только адаптер — никакое разрешение не запрашивается. Событие availabilitychanged из веб-стандарта не срабатывает, поэтому проверяйте снова, когда страница становится видимой.

wbNotify мост#

window.WebToApk.wbNotify(

Низкоуровневый транспорт для WebBluetoothPolyfill. Страница никогда не вызывает его напрямую — она вызывает navigator.bluetooth, точно так же, как в браузере.

Описано в своей группе, Web Bluetooth (navigator.bluetooth), а не отдельно.

Пример

Внутренний транспорт для characteristic.startNotifications() и stopNotifications(). Страницы используют navigator.bluetooth; не вызывайте это напрямую.

Возвращает: (через характеристику) Promise с самой характеристикой. Каждое новое значение вызывает на ней characteristicvaluechanged со значением в event.target.value (это DataView). Требуется: Bluetooth (включается сборкой из ZIP или HTML, когда в вашем коде упоминается 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) {}
}

Термометр, измерение которого поддерживает только индикации, подписывается так же, здесь через свойство 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();
}

Получайте текстовые строки от устройства с последовательным интерфейсом, склеивая уведомления, которые разорвали строку посередине:

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;
}

Примечания: Прямая сигнатура wbNotify(requestId, deviceId, service, characteristic, enable). На характеристику, которая поддерживает только индикации, автоматически подписываются через индикации. Характеристика, которая не умеет уведомлять, отклоняется с NotSupportedError. oncharacteristicvaluechanged = fn тоже работает. После отключения подпишитесь заново.

wbRead мост#

window.WebToApk.wbRead(requestId: String, deviceId: String, service: String, characteristic: String)

Низкоуровневый транспорт для WebBluetoothPolyfill. Страница никогда не вызывает его напрямую — она вызывает navigator.bluetooth, точно так же, как в браузере.

Описано в своей группе, Web Bluetooth (navigator.bluetooth), а не отдельно.

Пример

Внутренний транспорт для characteristic.readValue(). Страницы используют navigator.bluetooth; не вызывайте это напрямую.

Возвращает: (через readValue()) Promise с DataView; characteristic.value тоже обновляется. Требуется: Bluetooth (включается сборкой из ZIP или HTML, когда в вашем коде упоминается 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);
  }
}

Опрашивайте датчик, который можно только читать, здесь — стандартная характеристика Temperature (0x2a6e, в сотых долях градуса):

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);
}

Заполните панель «Об этом устройстве», запустив сразу несколько чтений; они выполняются одно за другим автоматически:

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
  });
}

Примечания: Низкоуровневая сигнатура wbRead(requestId, deviceId, service, characteristic). Чтения, записи и изменения уведомлений на одном устройстве выполняются одно за другим в том порядке, в котором вы их вызываете, поэтому можно запустить несколько сразу, не дожидаясь между ними.

wbRequestDevice мост#

window.WebToApk.wbRequestDevice(requestId: String, optionsJson: String)

Низкоуровневый транспорт для WebBluetoothPolyfill. Страница никогда не вызывает его напрямую — она вызывает navigator.bluetooth, точно так же, как в браузере.

Описано в своей группе, Web Bluetooth (navigator.bluetooth), а не отдельно.

Пример

Внутренний транспорт для navigator.bluetooth.requestDevice(options): он показывает окно выбора устройства приложения. Страницы используют navigator.bluetooth.requestDevice; не вызывайте это напрямую.

Возвращает: (через requestDevice) Promise с BluetoothDevice {id, name, gatt}. Требуется: Bluetooth (включается сборкой из ZIP или HTML, когда в вашем коде упоминается navigator.bluetooth) и разрешение «Устройства поблизости», запрашиваемое при первом использовании.

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
  }
});

Чтобы показать все устройства поблизости, без фильтрации: requestDevice({ acceptAllDevices: true }).

Выберите собственное устройство по точному имени или префиксу имени, указав его собственную 128-битную службу, чтобы её можно было использовать после подключения:

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;
  }
}

Запрашивайте устройство один раз за сеанс и используйте его при следующих нажатиях (getDevices() недоступен, поэтому сохраняйте полученный объект):

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);
  }
});

Примечания: Прямая сигнатура wbRequestDevice(requestId, optionsJson); ответ возвращается полифилу, а не странице. Вызывайте requestDevice по нажатию. Фильтры используют services (должны объявляться в рекламных пакетах), name (точное совпадение) и namePrefix; фильтр совпадает, когда совпадают все его ключи, а устройство подходит, когда подходит любой фильтр. Если нет ни filters, ни acceptAllDevices → TypeError. Отмена окна выбора отклоняется с NotFoundError. Окно выбора сканирует 30 секунд. device.id — это Bluetooth-адрес устройства.

wbWrite мост#

window.WebToApk.wbWrite(

Низкоуровневый транспорт для WebBluetoothPolyfill. Страница никогда не вызывает его напрямую — она вызывает navigator.bluetooth, точно так же, как в браузере.

Описано в своей группе, Web Bluetooth (navigator.bluetooth), а не отдельно.

Пример

Внутренний транспорт для characteristic.writeValue(), writeValueWithResponse() и writeValueWithoutResponse(). Страницы используют navigator.bluetooth; не вызывайте это напрямую.

Возвращает: (через характеристику) Promise из undefined. Требуется: Bluetooth (его включает сборка из ZIP или HTML, когда в вашем коде упоминается navigator.bluetooth).

С ответом (writeValue — то же самое): завершается, когда устройство подтверждает запись.

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);
}

Без ответа: быстрее, для потоков, где потеря пакета не важна.

async function sendText(characteristic, text) {
  if (!characteristic.properties.writeWithoutResponse) throw new Error('This characteristic needs writeValue');
  await characteristic.writeValueWithoutResponse(new TextEncoder().encode(text));
}

Отправьте длинную текстовую команду кусочками по 20 байт. Приложение не просит устройство увеличить размер пакета, поэтому за одну запись помещается 20 байт:

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');

Примечания: Низкоуровневая сигнатура wbWrite(requestId, deviceId, service, characteristic, valueBase64, withResponse). Значением может быть ArrayBuffer, любой типизированный массив, DataView или обычный массив байтов; всё остальное вызывает TypeError. Ошибки: NotSupportedError (запись отклонена), NetworkError (запись не удалась или нет подключения), NotFoundError.