# Web Bluetooth API

> navigator.bluetooth.requestDevice() 弹出选择器让用户选一台蓝牙低功耗设备，并暴露其 GATT 服务。本页列出过滤器成员、每个异常，以及带回退分支的示例。

`navigator.bluetooth.requestDevice()` 弹出一个由浏览器托管的选择器，列出附近匹配所传过滤器的蓝牙低功耗设备，并以用户选中的那一个 `BluetoothDevice` 兑现。页面随后通过该对象连接设备的 GATT 服务器，读取、写入或订阅特征值，所以 PWA 不需要原生伴侣应用就能与心率带、打印机或灯具通信。

该 API 仅在 Chromium 中实现。Chrome 56 在 Android、ChromeOS 和 macOS 上支持，Chrome 70 加入 Windows，Linux 仍需开启 `chrome://flags/#enable-experimental-web-platform-features`（[实现状态](https://github.com/whatwg/bluetooth/blob/main/implementation-status.md)，github.com）；Edge 79、Samsung Internet 6.0 与 Opera 43 跟随 Chrome。Firefox 有一个未关闭的跟踪 bug（[674737](https://bugzilla.mozilla.org/show_bug.cgi?id=674737)），WebKit 记录了正式的「反对」立场（[standards-positions #570](https://github.com/WebKit/standards-positions/issues/570)），Android WebView 不暴露 `navigator.bluetooth`。

## 语法

```js
navigator.bluetooth.requestDevice(options)
navigator.bluetooth.getDevices()
navigator.bluetooth.getAvailability()

device.gatt.connect()
server.getPrimaryService(service)
service.getCharacteristic(characteristic)
characteristic.readValue()
characteristic.writeValueWithResponse(value)
characteristic.startNotifications()
```

`requestDevice()` 返回 `Promise<BluetoothDevice>`；`getDevices()` 返回 `Promise<sequence<BluetoothDevice>>`，列出此前已授权给本源的设备且不弹窗，自 Chrome 85 起位于 `chrome://flags/#enable-web-bluetooth-new-permissions-backend` 之后（BCD `api.Bluetooth.getDevices`）。`getAvailability()` 在存在适配器时兑现为 `true`。`Bluetooth` 接口标注为 `[Exposed=Window, SecureContext]`：在 worker 中不可用，非安全源上 `navigator.bluetooth` 为 `undefined`。

## 参数

`requestDevice()` 接受一个 `RequestDeviceOptions` 字典。`filters` 与 `acceptAllDevices: true` 必须恰好给出一个；选择器只列出至少匹配一个过滤器且不匹配任何排除过滤器的设备。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `filters` | `sequence<BluetoothLEScanFilterInit>` | 与 `acceptAllDevices` 二选一 | 匹配任一过滤器的设备会被列出。给出时必须非空。 |
| `exclusionFilters` | `sequence<BluetoothLEScanFilterInit>` | 否 | 匹配其中任一项的设备即使匹配 `filters` 也会被隐藏；需要同时给出 `filters`。Chrome 114。 |
| `optionalServices` | `sequence<BluetoothServiceUUID>` | 否，默认 `[]` | 连接后页面可以访问、但未在某个 `services` 过滤器中列出的服务。两处都没有的服务会以 `SecurityError` 拒绝访问。 |
| `optionalManufacturerData` | `sequence<unsigned short>` | 否，默认 `[]` | 页面可以从广播中读取其厂商数据的公司标识符。 |
| `acceptAllDevices` | `boolean` | 与 `filters` 二选一 | 列出附近所有设备而不过滤。 |

每个 `BluetoothLEScanFilterInit` 至少需要一个成员。

| 成员 | 类型 | 说明 |
|---|---|---|
| `services` | `sequence<BluetoothServiceUUID>` | 广播的服务；可写 16 位别名（`0x180f`）、GATT 名称（`'battery_service'`）或 128 位 UUID 字符串。必须非空。 |
| `name` | `DOMString` | 精确的设备名，UTF-8 编码不超过 248 字节。 |
| `namePrefix` | `DOMString` | 设备名前缀，非空且 UTF-8 编码不超过 248 字节。 |
| `manufacturerData` | `sequence<BluetoothManufacturerDataFilterInit>` | `{ companyIdentifier, dataPrefix?, mask? }`；同一过滤器内标识符不得重复。Chrome 92。 |
| `serviceData` | `sequence<BluetoothServiceDataFilterInit>` | `{ service, dataPrefix?, mask? }`。 |

`getPrimaryService()`、`getCharacteristic()` 及其复数形式接受同样三种写法的 `BluetoothServiceUUID` 或 `BluetoothCharacteristicUUID`。

## 异常

`requestDevice()` 以下列之一拒绝；`TypeError` 检查最先执行，在任何无线电活动之前。

| 异常 | 条件 |
|---|---|
| `TypeError` | 给了 `exclusionFilters` 但没有 `filters`；`filters` 与 `acceptAllDevices: true` 同时给出或都没给；`filters` 或 `exclusionFilters` 为空数组；某个过滤器没有任何成员；`services`、`manufacturerData` 或 `serviceData` 为空数组；`name` 或 `namePrefix` 超过 248 个 UTF-8 字节，或 `namePrefix` 为空；`companyIdentifier` 重复；`dataPrefix` 为空；`mask` 长度与 `dataPrefix` 不一致。 |
| `SecurityError` | 文档不被允许使用 `bluetooth` Permissions Policy 特性；调用时没有瞬时用户激活；或过滤器或 `optionalServices` 中出现被封禁的服务 UUID。 |
| `NotFoundError` | 选择器在未选中设备的情况下关闭，或没有适配器、没有匹配的设备。 |

GATT 方法的拒绝方式不同。`device.gatt.connect()` 在设备超出范围或链路失败时以 `NetworkError` 拒绝。`getPrimaryService()` 与 `getCharacteristic()` 对未经 `filters` 或 `optionalServices` 授权的服务或被封禁的 UUID 以 `SecurityError` 拒绝，`gatt.connected` 为 `false` 时以 `NetworkError` 拒绝，设备没有所请求项时以 `NotFoundError` 拒绝。`readValue()` 与 `writeValueWithResponse()` 对禁止读或写的 UUID 以 `SecurityError` 拒绝，对超过 512 字节的值以 `InvalidModificationError` 拒绝，服务或特征值已不存在时以 `InvalidStateError` 拒绝，设备拒绝该操作时以 `NotSupportedError` 拒绝，操作中途断连时以 `NetworkError` 拒绝。这些都列在「另请参阅」所链规范的各方法步骤中。

:::observed
在 Chrome 中于用户激活处理函数之外调用 `navigator.bluetooth.requestDevice()`，以 `SecurityError: Must be handling a user gesture to show a permission request.` 拒绝；不选设备直接关闭选择器，以 `NotFoundError: User cancelled the requestDevice() chooser.` 拒绝；`filters` 与 `acceptAllDevices` 都不传则抛出 `TypeError: Failed to execute 'requestDevice' on 'Bluetooth': Either 'filters' should be present or 'acceptAllDevices' should be true, but not both.`；设备在 `gatt.connect()` 期间走出范围，以 `NetworkError: Bluetooth Device is no longer in range.` 拒绝。这些字符串定义在 Chromium 的 [`bluetooth.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/bluetooth/bluetooth.cc) 与 [`bluetooth_error.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/bluetooth/bluetooth_error.cc)（chromium.googlesource.com）。
:::

## 示例

每个示例都先检测 `navigator.bluetooth`，并给出没有它时页面的行为；前两个因激活规则必须由 `click` 之类的处理函数触发。

### 读取电量并给出回退提示

按标准的 Battery Service 过滤，选择器只显示广播该服务的设备；连接后读取单字节的 Battery Level 特征值。API 缺失时函数返回 `null`，界面说明该浏览器无法访问蓝牙设备。

```js
async function readBatteryLevel() {
  if (!('bluetooth' in navigator)) return null;

  const device = await navigator.bluetooth.requestDevice({
    filters: [{ services: ['battery_service'] }],
  });
  const server = await device.gatt.connect();
  const service = await server.getPrimaryService('battery_service');
  const characteristic = await service.getCharacteristic('battery_level');
  const value = await characteristic.readValue();
  return value.getUint8(0);
}

document.querySelector('#battery').addEventListener('click', async () => {
  const output = document.querySelector('#battery-output');
  try {
    const level = await readBatteryLevel();
    output.textContent = level === null
      ? '此浏览器无法访问蓝牙设备，请直接在设备上查看。'
      : `电量：${level}%`;
  } catch (err) {
    if (err.name === 'NotFoundError') return; // 选择器被关闭
    output.textContent = `${err.name}: ${err.message}`;
  }
});
```

`'battery_service'` 与 `'battery_level'` 是 GATT 分配名称，浏览器将其映射为 `0x180F` 与 `0x2A19`；厂商自定义服务用 128 位 UUID 字符串，用法相同。

### 订阅心率通知并在断连后恢复

`startNotifications()` 之后，通知以 `characteristicvaluechanged` 事件到达。设备随时可能走出范围，所以在设备上监听 `gattserverdisconnected`，并提供重连：对同一个对象再次调用 `gatt.connect()`，不需要重新弹出选择器。

```js
async function watchHeartRate(onBeat, onLost) {
  if (!('bluetooth' in navigator)) {
    onLost('此浏览器不提供蓝牙访问');
    return;
  }

  const device = await navigator.bluetooth.requestDevice({
    filters: [{ services: ['heart_rate'] }],
  });
  device.addEventListener('gattserverdisconnected', () => onLost('设备已断开'));

  const server = await device.gatt.connect();
  const service = await server.getPrimaryService('heart_rate');
  const measurement = await service.getCharacteristic('heart_rate_measurement');

  measurement.addEventListener('characteristicvaluechanged', (event) => {
    const data = event.target.value;
    const is16Bit = data.getUint8(0) & 0x1;
    onBeat(is16Bit ? data.getUint16(1, true) : data.getUint8(1));
  });
  await measurement.startNotifications();
}
```

Heart Rate Measurement 的第一个字节是标志位；bit 0 表示随后的心率值是 8 位还是 16 位，所以处理函数在读取前要分支。

### 显示配对按钮前先检查有没有适配器

`getAvailability()` 告诉页面这台机器是否有蓝牙无线电，这样可以在没有适配器的桌面机上隐藏「连接」按钮，而不是让用户撞上 `NotFoundError`。它不弹窗，也不需要用户激活。

```js
async function bluetoothUiState() {
  if (!('bluetooth' in navigator)) return 'unsupported';
  const available = await navigator.bluetooth.getAvailability();
  return available ? 'ready' : 'no-adapter';
}

const state = await bluetoothUiState();
document.querySelector('#connect').hidden = state !== 'ready';
document.querySelector('#bluetooth-note').textContent = {
  unsupported: '请在桌面或 Android 设备上使用 Chrome 或 Edge 进行蓝牙连接。',
  'no-adapter': '此设备上未找到蓝牙适配器。',
  ready: '',
}[state];
```

页面打开期间可用性可能变化（插入 USB 适配器、关闭蓝牙）；`navigator.bluetooth` 上的 `availabilitychanged` 事件会报告新值。

## 另请参阅

- [Web Serial API](/zh/reference/capabilities/web-serial/)，对串口采用同一套选择器模型
- [WebUSB API](/zh/reference/capabilities/web-usb/)
- [WebHID API](/zh/reference/capabilities/web-hid/)
- [Web Bluetooth: requestDevice() method](https://bluetooth.spec.whatwg.org/#dom-bluetooth-requestdevice)（bluetooth.spec.whatwg.org）
- [Web Bluetooth: getDevices() method](https://bluetooth.spec.whatwg.org/#dom-bluetooth-getdevices)（bluetooth.spec.whatwg.org）
- [WebKit standards position: Web Bluetooth](https://github.com/WebKit/standards-positions/issues/570)（github.com）
- [Mozilla bug 674737: Implement Web Bluetooth](https://bugzilla.mozilla.org/show_bug.cgi?id=674737)（bugzilla.mozilla.org）
- [Communicating with Bluetooth devices over JavaScript](https://developer.chrome.com/docs/capabilities/bluetooth)（developer.chrome.com）