Web Bluetooth

社区组报告草案,

关于本文档的更多详细信息
此版本:
https://webbluetoothcg.github.io/web-bluetooth/
问题跟踪:
GitHub
规范内联
编辑:
(Google LLC)
(Google LLC)
在 GitHub 上查看贡献者
翻译 (非规范性):
日本語
参与:
加入 W3C 社区组
通过 GitHub 修正文档
public-web-bluetooth@w3.org(存档)
IRC:W3C 的 IRC 上的 #web-bluetooth

摘要

本文档描述了一个 API,用于使用通用属性配置文件(GATT) 通过 Bluetooth 4 无线标准发现设备并与设备通信。

本文档状态

本规范由 Web Bluetooth 社区组发布。 它不是 W3C 标准,也不在 W3C 标准轨道上。 请注意,根据 W3C 社区贡献者许可协议 (CLA), 存在有限的退出权,并且适用其他条件。 了解更多关于 W3C 社区组和业务组的信息。

对本文档的更改可在 https://github.com/WebBluetoothCG/web-bluetooth/commits/gh-pages 跟踪。

如果你希望就本文档发表评论,请发送至 public-web-bluetooth@w3.org (订阅, 存档)。

1. 引言

本节为非规范性内容。

蓝牙是用于 设备之间短距离无线通信的标准。蓝牙“经典”模式(BR/EDR) 定义了一组二进制协议,并支持最高约 24Mbps 的速度。 蓝牙 4.0 引入了一种称为“Bluetooth Smart”的新“低功耗”模式, 即 BLE,也简称为 LE, 其速度限制在约 1Mbps,但允许设备在大部分时间关闭其发射器。 BLE 的大部分功能通过由通用属性 配置文件(GATT)提供的键/值 对实现。

BLE 定义了设备可承担的多种角色。广播者和 观察者角色分别用于仅发射和 仅接收的应用。 以外围设备角色运行的设备可以接收 连接,而以中心设备角色运行的设备可以连接到 外围设备。

以外围设备或中心设备角色运行的设备均可托管 一个GATT 服务器,该服务器公开由 服务、 特征和描述符构成的层次结构。有关 此层次结构的更多详细信息,请参见§ 6.1 GATT 信息模型。尽管 GATT 协议被设计为支持 BLE 传输,但它也可以在 BR/EDR 传输上运行。

本规范的第一个版本允许网页在以 中心设备角色运行的 UA 上,通过 BR/EDR 或 LE 连接来连接GATT 服务器。虽然本规范引用了[BLUETOOTH42] 规范, 但其目标也包括支持仅实现 蓝牙 4.0 或 4.1 的设备之间的通信。

1.1. 示例

要发现标准心率监测器并从中检索数据, 网站可使用如下代码:
let chosenHeartRateService = null;

navigator.bluetooth.requestDevice({
  filters: [{
    services: ['heart_rate'],
  }]
}).then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => {
  chosenHeartRateService = service;
  return Promise.all([
    service.getCharacteristic('body_sensor_location')
      .then(handleBodySensorLocationCharacteristic),
    service.getCharacteristic('heart_rate_measurement')
      .then(handleHeartRateMeasurementCharacteristic),
  ]);
});

function handleBodySensorLocationCharacteristic(characteristic) {
  if (characteristic === null) {
    console.log("未知的传感器位置。");
    return Promise.resolve();
  }
  return characteristic.readValue()
  .then(sensorLocationData => {
    const sensorLocation = sensorLocationData.getUint8(0);
    switch (sensorLocation) {
      case 0: return '其他';
      case 1: return '胸部';
      case 2: return '手腕';
      case 3: return '手指';
      case 4: return '手部';
      case 5: return '耳垂';
      case 6: return '足部';
      default: return '未知';
    }
  }).then(location => console.log(location));
}

function handleHeartRateMeasurementCharacteristic(characteristic) {
  return characteristic.startNotifications()
  .then(char => {
    characteristic.addEventListener('characteristicvaluechanged',
                                    onHeartRateChanged);
  });
}

function onHeartRateChanged(event) {
  const characteristic = event.target;
  console.log(parseHeartRate(characteristic.value));
}

parseHeartRate() 将根据 heart_rate_measurement 文档 进行定义,以读取存储在 DataView 中的 BluetoothRemoteGATTCharacteristic 的 value 字段。

function parseHeartRate(data) {
  const flags = data.getUint8(0);
  const rate16Bits = flags & 0x1;
  const result = {};
  let index = 1;
  if (rate16Bits) {
    result.heartRate = data.getUint16(index, /*小端序=*/true);
    index += 2;
  } else {
    result.heartRate = data.getUint8(index);
    index += 1;
  }
  const contactDetected = flags & 0x2;
  const contactSensorPresent = flags & 0x4;
  if (contactSensorPresent) {
    result.contactDetected = !!contactDetected;
  }
  const energyPresent = flags & 0x8;
  if (energyPresent) {
    result.energyExpended = data.getUint16(index, /*小端序=*/true);
    index += 2;
  }
  const rrIntervalPresent = flags & 0x10;
  if (rrIntervalPresent) {
    const rrIntervals = [];
    for (; index + 1 < data.byteLength; index += 2) {
      rrIntervals.push(data.getUint16(index, /*小端序=*/true));
    }
    result.rrIntervals = rrIntervals;
  }
  return result;
}

onHeartRateChanged() 可能记录如下对象

{
  heartRate: 70,
  contactDetected: true,
  energyExpended: 750,     // 表示 750kJ。
  rrIntervals: [890, 870]  // 表示 .87s 和 .85s。
}

如果心率传感器报告 energyExpended 字段,Web 应用可以通过向 heart_rate_control_point 特征写入数据,将其值重置为 0:

function resetEnergyExpended() {
  if (!chosenHeartRateService) {
    return Promise.reject(new Error('尚未选择心率传感器。'));
  }
  return chosenHeartRateService.getCharacteristic('heart_rate_control_point')
  .then(controlPoint => {
    const resetEnergyExpended = new Uint8Array([1]);
    return controlPoint.writeValue(resetEnergyExpended);
  });
}

2. 安全考虑

请参见§ 3 隐私考虑一节。[Issue #575]

3. 隐私考虑

3.1. 设备访问能力强大

当网站使用 requestDevice() 请求访问设备时,它会获得访问该调用中提到的所有 GATT 服务的能力。UA 必须在询问用户要信任哪些设备之前,告知用户这些服务会赋予网站哪些能力。 如果列表中的任何服务对 UA 而言是未知的,UA 必须假定这些服务会赋予网站对 设备的完全控制权,并告知用户此风险。UA 还必须允许用户 查看哪些网站可以访问哪些设备,并撤销这些配对。

UA 不得允许用户将整个设备类别与网站配对。 可以构造这样一种设备类别,其中每个单独的设备 都发送相同的蓝牙层标识信息。UA 无须 尝试检测这种伪造,并且可以允许用户将此 伪设备与网站配对。

为了帮助确保只有用户批准访问的实体才能真正获得 访问权限,本规范要求只有安全上下文才能访问 蓝牙设备。

3.2. 受信任的服务器可能提供恶意代码

本节为非规范性内容。

即使用户信任某个源,该源的服务器或开发者仍可能被 攻破,或者该源的网站可能容易受到 XSS 攻击。这两种情况 都可能导致用户授予恶意代码访问重要设备的权限。源 应定义内容安全策略([CSP3])以降低 XSS 攻击的风险,但这无法防范服务器或开发者被攻破的情况。

§ 4.1 Permission API 集成提供的在页面重新加载后检索已授权设备的能力,会使这种风险更加严重。攻击者 不必在网站被攻破期间诱使用户授予访问权限, 只要用户在网站被攻破期间访问该网站,攻击者就可以利用 先前已授权的设备。另一方面,当网站可以在页面重新加载之间保持 对设备的访问权限时,总体上就不必显示那么多 权限提示,从而使用户更有可能关注 他们实际看到的提示。

3.3. 针对设备的攻击

本节为非规范性内容。

来自网站的通信可能破坏某些设备的安全模型,这些设备 假定自己只会从远程设备受信任的操作系统接收消息。 人机接口设备就是一个典型例子,在这种情况下,允许 网站进行通信将使该网站能够记录按键。本 规范包含一个此类易受攻击的服务、 特征和描述符的 GATT 阻止列表,以防止网站利用 它们。

我们预计许多设备容易受到发送到其 无线电接口的意外数据的影响。过去,这些设备必须逐个利用,但此 API 使进行大规模攻击成为可能。本规范采用 多种方法来增加此类攻击的难度:

UA 还可以采取进一步措施来保护用户:

3.4. 蓝牙设备标识符

本节为非规范性内容。

每个蓝牙 BR/EDR 设备都有一个称为 BD_ADDR 的唯一 48 位 MAC 地址。每个蓝牙 LE 设备至少具有 公共设备 地址和静态设备地址之一。公共设备地址 是一个 MAC 地址。静态设备地址可能会在每次 重启时重新生成。BR/EDR/LE 设备会对 BD_ADDR 和 公共设备 地址使用相同的值(在Read BD_ADDR 命令中规定)。

LE 设备还可能具有唯一的 128 位身份解析密钥, 该密钥会在绑定过程中发送给受信任的设备。为了避免泄露 持久标识符,LE 设备在扫描和广播时可以使用随机 的可解析或不可解析私有地址,而不是其静态或 公共地址。这些地址会定期重新生成(大约每 15 分钟一次),但已绑定的设备可以使用IRK 检查其存储的某个 IRK 是否与给定的可解析私有地址匹配,方法是使用可解析私有 地址解析过程。

每个蓝牙设备还有一个人类可读的蓝牙设备名称。 这些名称不保证唯一,但根据设备 类型的不同,它们很可能是唯一的。

3.4.1. 远程蓝牙设备的标识符

本节为非规范性内容。

如果网站能够检索任何持久设备 ID,则这些 ID 可与 对周围设备进行大规模编目的工作结合使用,以确定 用户的位置。设备 ID 还可用于识别将 两个不同网站与同一蓝牙设备配对的用户是同一用户。另一方面, 许多可用的 GATT 服务都可用于对 设备进行指纹识别,并且设备可以轻易公开自定义 GATT 服务,使此操作 更加容易。

本规范建议 UA 在用户无意让脚本知道它们是同一设备时, 对同一设备使用不同的设备 ID,从而使 网站更难以这种方式滥用设备地址。设备制造商仍然可以设计 帮助跟踪用户的设备,但这需要额外工作。

3.4.2. UA 的蓝牙地址

本节为非规范性内容。

在 BR/EDR 模式下,或者在未使用隐私 功能进行主动扫描的 LE 模式下,UA 会向附近的任何蓝牙无线电广播其持久 ID。 这使攻击者很容易在某个区域散布恶意设备并跟踪 UA。截至 2014-08,几乎没有平台记录其实现了隐私 功能,因此尽管本规范推荐使用该功能,但很少有 UA 可能会使用它。 本规范确实要求用户手势 才能让网站触发扫描,这在一定程度上降低了扫描频率,但 如果有更多平台公开隐私 功能,效果会更好。

3.5. 暴露蓝牙可用性

本节为非规范性内容。

navigator.bluetooth.getAvailability() 会暴露用户系统上是否有 蓝牙无线电可用,而不考虑其是否已开启。如果用户已配置 UA 阻止 Web Bluetooth,可用性也会受到影响。一些用户可能认为这是隐私信息, 尽管很难想象暴露它会造成什么损害。这些 信息也会略微增加 UA 的指纹识别面。此 函数返回一个 Promise, 因此 UA 可以选择询问用户希望返回什么 值,但我们预计增加的风险足够小,以至于 UA 会选择不提示用户。

4. 设备发现

dictionary BluetoothDataFilterInit {
  BufferSource dataPrefix;
  BufferSource mask;
};

dictionary BluetoothManufacturerDataFilterInit : BluetoothDataFilterInit {
  required [EnforceRange] unsigned short companyIdentifier;
};

dictionary BluetoothServiceDataFilterInit : BluetoothDataFilterInit {
  required BluetoothServiceUUID service;
};

dictionary BluetoothLEScanFilterInit {
  sequence<BluetoothServiceUUID> services;
  DOMString name;
  DOMString namePrefix;
  sequence<BluetoothManufacturerDataFilterInit> manufacturerData;
  sequence<BluetoothServiceDataFilterInit> serviceData;
};

dictionary RequestDeviceOptions {
  sequence<BluetoothLEScanFilterInit> filters;
  sequence<BluetoothLEScanFilterInit> exclusionFilters;
  sequence<BluetoothServiceUUID> optionalServices = [];
  sequence<unsigned short> optionalManufacturerData = [];
  boolean acceptAllDevices = false;
};

[Exposed=Window, SecureContext]
interface Bluetooth : EventTarget {
  Promise<boolean> getAvailability();
  attribute EventHandler onavailabilitychanged;
  [SameObject]
  readonly attribute BluetoothDevice? referringDevice;
  Promise<sequence<BluetoothDevice>> getDevices();
  Promise<BluetoothDevice> requestDevice(optional RequestDeviceOptions options = {});
};

Bluetooth includes BluetoothDeviceEventHandlers;
Bluetooth includes CharacteristicEventHandlers;
Bluetooth includes ServiceEventHandlers;

本规范中定义的方法通常以异步方式完成, 并将工作排入一个 蓝牙 任务源。

注:Bluetooth 成员
注:getAvailability() 会告知页面蓝牙是否 完全可用。通过软件禁用的适配器仍应视为 可用。可用性的变化,例如用户物理地 连接或断开适配器时,会通过 availabilitychanged 事件报告。

referringDevice 提供对用户从中 打开此页面的设备的访问(如果存在)。例如,Eddystone 信标 可能会广播一个 URL,UA 允许用户打开该 URL。表示该信标的 BluetoothDevice 可通过 navigator.bluetooth.referringDevice 获得。

getDevices() 使页面能够检索用户已授予访问权限的 蓝牙设备。

requestDevice(options) 请求用户授予此源访问某个设备的权限,该设备 匹配 任一过滤器,这些过滤器位于 options.filters 中,但不匹配任何 过滤器 于 options.exclusionFilters 中。 要匹配过滤器, 设备必须:

制造商特定数据和服务数据都会将一个键映射到一个 字节数组。BluetoothDataFilterInit 会过滤这些数组。如果某个数组具有一个 prefix,使得 prefix & mask 等于 dataPrefix & mask, 则该数组匹配。

请注意,如果设备在连接时显著改变其行为,例如 不再广播用于标识自身的制造商数据,而是 让客户端发现某些用于标识的 GATT 服务,则网站可能需要 为两种行为都包含过滤器。

在少数情况下,设备可能不会广播足够的区分信息,以便 站点过滤掉无关设备。在这种情况下,站点可以将 acceptAllDevices 设置为 true,并省略 所有 filters 和 exclusionFilters。 这样一来,选择正确设备的责任将完全落在站点用户身上。 如果站点使用 acceptAllDevices, 它将只能 使用 optionalServices 中列出的服务。

用户选择要与此源配对的设备后,该源被允许 访问 UUID 列在 services 列表中的任何服务,该列表可以位于 options.filters 的任一元素中,或位于 options.optionalServices 中。 该源还 被允许访问设备广播数据中制造商代码在 options.optionalManufacturerData 中定义的任何制造商数据。

这意味着,如果开发者仅按名称过滤,则他们必须使用 optionalServices 才能访问任何服务。

假设 UA 靠近以下设备:
设备 广播的服务
D1 A, B, C, D
D2 A, B, E
D3 C, D
D4 E
D5 <无>

如果网站调用

navigator.bluetooth.requestDevice({
  filters: [ {services: [A, B]} ]
});

用户将看到一个包含设备 D1 和 D2 的对话框。如果用户 选择 D1,则网站将无法访问服务 C 或 D。如果用户 选择 D2,则网站将无法访问服务 E。

另一方面,如果网站调用

navigator.bluetooth.requestDevice({
  filters: [
    {services: [A, B]},
    {services: [C, D]}
  ]
});

该对话框将包含设备 D1、D2 和 D3,如果用户选择 D1, 网站将能够访问服务 A、B、C 和 D。

如果网站随后调用

navigator.bluetooth.getDevices();

得到的 Promise 将兑现为包含设备 D1 的数组,并且 网站将能够访问服务 A、B、C 和 D。

optionalServices 列表不会向用户看到的对话框 添加任何设备,但它会影响网站能够从用户 选择的设备中使用哪些服务。

navigator.bluetooth.requestDevice({
  filters: [ {services: [A, B]} ],
  optionalServices: [E]
});

显示一个包含 D1 和 D2、但不包含 D4 的对话框,因为 D4 不包含 所需服务。如果用户选择 D2,与第一个示例不同, 网站将能够访问服务 A、B 和 E。

如果网站再次调用

navigator.bluetooth.getDevices();

则得到的 Promise 将兑现为包含 设备 D1 和 D2 的数组。设备 D1 上可访问 A、B、C 和 D 服务, 而设备 D2 上可访问 A、B 和 E 服务。

允许的服务也适用于用户授予访问权限后设备发生变化的情况。 例如,如果用户在前一个 requestDevice() 调用中选择 D1,而 D1 随后添加了新的 E 服务,则会 触发 serviceadded 事件,并且网页将能够访问服务 E。

假设前一个 示例中的设备还广播如下名称:
设备 广播的设备名称
D1 First De…
D2 <无>
D3 Device Third
D4 Device Fourth
D5 Unique Name

下表显示,对于传递给 navigator.bluetooth.requestDevice({filters: filters}) 的若干 filters 值,用户可以在哪些设备之间进行选择。

filters 设备 注释
[{name: "Unique Name"}]
D5
[{namePrefix: "Device"}]
D3, D4
[{name: "First De"},
  {name: "First Device"}]
<无> D1 只广播其名称的前缀,因此尝试匹配其完整名称 会失败。
[{namePrefix: "First"},
  {name: "Unique Name"}]
D1, D5
[{services: [C],
  namePrefix: "Device"},
  {name: "Unique Name"}]
D3, D5

下表显示,对于传递给 navigator.bluetooth.requestDevice({filters: filters, exclusionFilters: exclusionFilters}) 的若干 filters 和 exclusionFilters 值,用户可以在哪些设备之间进行选择。

filters exclusionFilters 设备
[{namePrefix: "Device"}]  // D3, D4
[{name: "Device Third"}]   // D3
D4
[{namePrefix: "Device"}]  // D3, D4
[{namePrefix: "Device F"}] // D4
D3
[{services: [C]},         // D1, D3
  {namePrefix: "Device"}] // D3, D4
[{services: [A]},          // D1
  {name: "Device Fourth"}] // D4
D3
假设前一个 示例中的设备还广播如下制造商数据或服务数据:
设备 制造商数据 服务数据
D1 17: 01 02 03
D2 A: 01 02 03

下表显示,对于传递给 navigator.bluetooth.requestDevice({filters: filters}) 的若干 filters 值,用户可以在哪些设备之间进行选择。

filters 设备
[{ manufacturerData: [{ companyIdentifier: 17 }] }]
D1
[{ serviceData: [{ service: "A" }] }]
D2
[
  { manufacturerData: [{ companyIdentifier: 17 }] },
  { serviceData: [{ service: "A" }] },
]
D1, D2
[
  {
    manufacturerData: [{ companyIdentifier: 17 }],
    serviceData: [{ service: "A" }],
  },
]
<无>
[
  {
    manufacturerData: [
      {
        companyIdentifier: 17,
        dataPrefix: new Uint8Array([1, 2, 3])
      },
    ],
  },
]
D1
[
  {
    manufacturerData: [
      {
        companyIdentifier: 17,
        dataPrefix: new Uint8Array([1, 2, 3, 4])
      },
    ],
  },
]
<无>
[
  {
    manufacturerData: [
      {
        companyIdentifier: 17,
        dataPrefix: new Uint8Array([1])
      },
    ],
  },
]
D1
[
  {
    manufacturerData: [
      {
        companyIdentifier: 17,
        dataPrefix: new Uint8Array([0x91, 0xAA]),
        mask: new Uint8Array([0x0f, 0x57]),
      },
    ],
  },
]
D1
[
  {
    manufacturerData: [
      { companyIdentifier: 17 },
      { companyIdentifier: 18 },
    ]
  }
]
<无>
接受或拒绝所有可能设备的过滤器会导致 TypeError。 要接受所有设备,请改用 acceptAllDevices。
调用 注释
requestDevice({})
无效:缺少过滤器列表不会接受任何设备。
requestDevice({filters:[]})
无效:空的过滤器列表不会接受任何设备。
requestDevice({filters:[{}]})
无效:空过滤器会接受所有设备,因此也不被允许。
requestDevice({
  acceptAllDevices:true
})
有效:使用 acceptAllDevices 显式接受所有设备。
requestDevice({
  filters: [...],
  acceptAllDevices:true
})
无效:acceptAllDevices 会覆盖任何 filters。
requestDevice({
  exclusionFilters: [...],
  acceptAllDevices:true
})
无效:acceptAllDevices 会覆盖任何 exclusionFilters。
requestDevice({
  exclusionFilters: [...]
})
无效:exclusionFilters 要求存在 filters。
requestDevice({
  filters: [...],
  exclusionFilters: []
})
无效:exclusionFilters 必须为非空才能排除设备。
requestDevice({
  filters: [{namePrefix: ""}]
})
无效:如果存在 namePrefix,则其必须为非空才能过滤设备。
requestDevice({
  filters: [{manufacturerData: []}]
})
无效:如果 manufacturerData 存在, 则其必须为非空才能过滤设备。
requestDevice({
  filters: [{serviceData: []}]
})
无效:如果 serviceData 存在, 则其必须为非空才能过滤设备。

Bluetooth 的实例 使用下表所述的内部槽 创建:

内部槽 初始值 描述(非规范性)
[[deviceInstanceMap]] 一个从蓝牙设备 到 BluetoothDevice 实例的空映射。 确保在单个全局对象中,每个蓝牙 设备仅由一个 BluetoothDevice 实例表示。
[[attributeInstanceMap]] 一个从蓝牙缓存条目到 Promise 的空映射。 这些 Promise 会兑现为 BluetoothRemoteGATTService、 BluetoothRemoteGATTCharacteristic 或 BluetoothRemoteGATTDescriptor 实例。
[[referringDevice]] null 如果 Document 是从设备打开的,则在初始化 Document 对象时设置为一个 BluetoothDevice。
获取 navigator.bluetooth.referringDevice 必须返回 [[referringDevice]]。
某些 UA 可能允许用户使某个浏览上下文响应某个蓝牙设备而导航。
注:例如,如果 Eddystone 信标广播一个 URL,则 UA 可能允许用户导航到此 URL。

如果发生这种情况,则在初始化 Document 对象的过程中, UA 必须运行以下步骤:

  1. 令 referringDevice 为导致此次导航的设备。

  2. 获取表示 referringDevice 且位于 navigator.bluetooth 内的 BluetoothDevice,并令 referringDeviceObj 为结果。

  3. 如果上一步抛出了异常,则中止这些步骤。

    注:这意味着 UA 没有推断出用户有意授予 当前realm 对 referringDevice 的访问权限。例如, 用户可能已全局拒绝 GATT 访问。
  4. 将 navigator.bluetooth.[[referringDevice]] 设置为 referringDeviceObj。

如果以下步骤返回 match,则蓝牙设备 device 匹配过滤器 filter:
  1. 如果 filter.name 存在,则如果 device 的蓝牙设备名称不完整或不等于 filter.name,则返回 mismatch。

  2. 如果 filter.namePrefix 存在,则如果 device 的蓝牙设备名称不存在或不以 filter.namePrefix 开头, 则返回 mismatch。

  3. 对于 filter.services 中的每个 uuid,如果 UA 尚未接收到表明设备支持 UUID 为 uuid 的主服务(而非被包含服务)的广播数据、扩展查询 响应或服务发现响应,则返回 mismatch。

  4. 对于 filter["manufacturerData"] 中的每个 manufacturerData,如果 device 尚未广播制造商特定数据,其 公司标识符代码等于 manufacturerData["companyIdentifier"] 且其中的数据匹配 manufacturerData,则返回 mismatch。

  5. 对于 filter["serviceData"] 中的每个 serviceData,如果 device 尚未广播服务数据,其 UUID 的 128 位形式为 serviceData["service"] 且其中的数据匹配 serviceData,则返回 mismatch。

  6. 返回 match。

如果以下步骤返回 match,则字节数组 data 匹配 一个 BluetoothDataFilterInit filter。
注:此算法假定 filter 已经被规范化。
  1. 令 expectedPrefix 为 filter.dataPrefix 所持字节的副本。

  2. 令 mask 为 filter.mask 所持字节的副本。

  3. 如果 data 的字节数少于 expectedPrefix,则返回 mismatch。

  4. 对于 mask 中的每个 1 位,如果 data 中对应的位 不等于 expectedPrefix 中对应的位,则返回 mismatch。

  5. 返回 match。

如果以下步骤返回 true,则一个 BluetoothDataFilterInit filter1 是另一个 BluetoothDataFilterInit filter2 的 真子集:
  1. 如果 filter1 的长度小于 filter2 的长度,则返回 false。

  2. 令 byteIndex 为 0。

  3. 当 byteIndex 小于 filter2 的长度时,执行以下 子步骤:

    1. 如果 filter1.mask[byteIndex] & filter2.mask[byteIndex] 不等于 filter2.mask[byteIndex], 则返回 false。

    2. 如果 filter1.dataPrefix[byteIndex] & filter2.mask[byteIndex] 不等于 filter2.dataPrefix[byteIndex] & filter2.mask[byteIndex], 则返回 false。

    3. 将 byteIndex 设置为 byteIndex + 1。

  4. 返回 true。

设备广播的服务 UUID 列表可能并不包含设备支持的所有 UUID。广播数据会指明该列表是否 完整。如果网站过滤某个附近设备支持但 未广播的 UUID,则该设备可能不会被包含在 向用户呈现的设备列表中。UA 需要连接到该设备才能发现 完整的受支持服务列表,这可能会降低无线电性能并 导致延迟,因此本规范不要求这样做。
getDevices() 方法在被调用并给定一个 BluetoothPermissionStorage storage 时,必须运行 以下步骤:
  1. 令 global 为this 的相关全局对象。

  2. 如果 global 的关联 Document 不被允许使用 策略控制功能,其名称为 "bluetooth",则返回 一个被拒绝的 promise,其拒绝原因为一个 SecurityError。

  3. 令 promise 为一个新的 promise。

  4. 并行运行以下步骤:

    1. 令 devices 为一个新的空 Array。

    2. 对于 storage.allowedDevices 中的每个 allowedDevice, 将表示 allowedDevice@[[device]] 的 BluetoothDevice 对象添加到 devices。

    3. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用 devices 兑现 promise。

    注:devices 中的 BluetoothDevice 可能不在蓝牙无线电的 范围内。对于 devices 中的给定 device, 可使用 watchAdvertisements() 方法观察 device 何时处于范围内并广播广播数据包。当 advertisementreceived 事件 event 在 device 上触发时,它可能 表明该设备已经足够接近,可以通过调用 event.device.gatt.connect() 建立连接。
  5. 返回 promise。

requestDevice(options) 方法在被调用时,必须运行以下步骤:
  1. 如果 options.exclusionFilters 存在 ,并且 options.filters 不存在, 则返回 一个被拒绝的 promise,其拒绝原因为一个 TypeError。

  2. 如果 options.filters 存在,并且 options.acceptAllDevices 为 true, 或者如果 options.filters 不存在 并且 options.acceptAllDevices 为 false,则返回 一个被拒绝的 promise,其拒绝原因为一个 TypeError。

    注:这会强制要求 filters 或 acceptAllDevices:true 恰好存在其中一个。
  3. 令 promise 为一个新的 promise。

  4. 并行运行以下步骤:

    1. 请求蓝牙设备,如果 options.acceptAllDevices 为 false,则传入 options.filters, 否则传入 null;如果 options.exclusionFilters 存在则传入它,否则传入 null;并传入 options.optionalServices 和 options.optionalManufacturerData, 并令 devices 为结果。

    2. 在蓝牙任务源上排入一个全局任务,给定 this 的 相关全局对象,以运行以下 步骤:

      1. 如果上一步抛出异常,则使用该 异常拒绝 promise 并中止这些步骤。

      2. 如果 devices 是空序列,则以 NotFoundError 拒绝 promise 并中止这些步骤。

      3. 使用 devices[0] 兑现 promise。

  5. 返回 promise。

要请求 蓝牙设备,给定一个 BluetoothPermissionStorage storage 和一个 BluetoothLEScanFilterInit 序列 filters(可以为 null,表示 所有设备均可匹配)、一个 BluetoothLEScanFilterInit 序列 exclusionFilters(如果没有设置排除过滤器,可以为 null)、 一个 BluetoothServiceUUID 序列 optionalServices,以及一个 unsigned short 序列 optionalManufacturerData,UA 必须运行以下 步骤:
注:这些步骤可能会阻塞,因此使用此算法的地方必须并行 执行。
注:对此算法的调用最终将能够请求多个 设备,但目前它始终只返回一个设备。
  1. 令 global 为 storage 的相关全局对象。

  2. 令 document 为 global 的关联 Document。

  3. 如果 document 不被允许使用名为 “bluetooth”的策略控制功能, 则抛出 SecurityError 并中止这些步骤。

  4. 检查该算法是否在其相关全局对象具有 瞬态 激活时触发,否则抛出 SecurityError 并中止这些 步骤。

  5. 为了将参数从服务名称和别名转换为纯 UUID,执行以下子步骤:

    1. 如果 filters !== null && filters.length === 0, 则抛出 TypeError 并中止这些步骤。

    2. 如果 exclusionFilters !== null && exclusionFilters.length === 0, 则抛出 TypeError 并中止这些步骤。

    3. 令 uuidFilters 为一个新的 Array, uuidExclusionFilters 为一个新的 Array, 并令 requiredServiceUUIDs 为一个新的 Set。

    4. 如果 filters 为 null,则将 requiredServiceUUIDs 设置为 所有 UUID 的集合。

    5. 如果 filters 不为 null,则对于 filters 中的每个 filter,执行以下步骤:

      1. 令 canonicalFilter 为 filter 经规范化 的结果。

      2. 将 canonicalFilter 追加到 uuidFilters。

      3. 将 canonicalFilter.services 的内容添加到 requiredServiceUUIDs。

    6. 如果 exclusionFilters 不为 null,则对于 exclusionFilters 中的每个 exclusionFilter,执行以下步骤:

      1. 令 canonicalExclusionFilter 为 exclusionFilter 经规范化 的结果。

      2. 将 canonicalExclusionFilter 追加到 uuidExclusionFilters。

    7. 令 optionalServiceUUIDs 为 Array.prototype.map.call(optionalServices, BluetoothUUID.getService)。

    8. 如果任何 BluetoothUUID.getService() 调用抛出异常,则抛出该异常并中止这些步骤。

    9. 从 optionalServiceUUIDs 中移除任何 被阻止的 UUID。

  6. 令 descriptor 为

    {
      name: "bluetooth",
      filters: uuidFilters
      optionalServices: optionalServiceUUIDs,
      optionalManufacturerData: optionalManufacturerData
      acceptAllDevices: filters !== null,
    }
    
  7. 令 state 为 descriptor 的权限状态。

    注:在非安全上下文中,state 将为“denied”,因为 强大功能不能用于非安全上下文。
  8. 如果 state 为“denied”, 则返回 [] 并中止这些步骤。

  9. 如果 UA 可以证明下一步不可能找到任何设备, 例如因为没有可用于扫描的蓝牙适配器,或者 因为过滤器无法由任何可能的广播数据包匹配,则 UA 可以返回 [] 并中止这些步骤。

  10. 令 scanResult 为使用 global 和 requiredServiceUUIDs 调用扫描设备的结果。

  11. 如果 filters 不为 null,则执行以下子步骤:

    1. 如果 scanResult 中的设备 不匹配 uuidFilters 中的过滤器,则将其移除。

    2. 如果 exclusionFilters 不为 null,则从 scanResult 中移除那些匹配 uuidExclusionFilters 中过滤器的设备。

  12. 令 navigable 为 document 的可导航对象。

  13. 令 promptId 为一个新的唯一不透明字符串。

实际上, 在提示打开期间,设备列表会动态更新。当前规范文本 未反映这一点,但此事件可能会使用相同的 promptId 和新的设备列表触发多次。参见 https://github.com/WebBluetoothCG/web-bluetooth/issues/621。

  1. 给定 navigable、promptId 和 scanResult, 触发提示更新事件。

  2. 即使 scanResult 为空, 也提示用户选择 scanResult 中与 descriptor 关联的设备之一,并令 device 为结果。

    UA 可以允许用户选择附近不匹配 uuidFilters 的设备。

    注:UA 应向用户显示每个设备的人类可读名称。 如果该名称不可用,例如因为 UA 的蓝牙 系统不支持启用隐私的扫描,则 UA 应允许用户 表示兴趣,然后执行禁用隐私的扫描以检索 名称。
  3. 从可导航对象到设备 提示的映射[navigable 的可导航对象 ID] 中移除。

  4. UA 可以将 device 添加到 storage。

    注:选择 device 很可能表明用户 希望该设备至少在当前设置 对象的 "bluetooth" 额外权限数据的 allowedDevices 列表中出现,使其 mayUseGATT 字段为 true,使 requiredServiceUUIDs 和 optionalServiceUUIDs 并集中的所有服务都出现在其 allowedServices 列表中,此外 还保留已经存在的任何服务,并使 optionalManufacturerData 中的制造商代码出现在其 allowedManufacturerData 列表中。
  5. 如果 device 为“denied”, 则返回 [] 并中止这些步骤。

  6. UA 可以使用 device 内的所有服务填充蓝牙缓存。 忽略此步骤产生的任何错误。

  7. 在 this 内获取表示 device 的 BluetoothDevice, 传播任何异常,并令 deviceObj 为结果。

  8. 返回 [deviceObj]。

对 BluetoothLEScanFilterInit filter 进行规范化的结果, 是以下步骤返回的 BluetoothLEScanFilterInit:
  1. 如果 filter 的成员均不存在,则抛出 TypeError 并中止这些步骤。

  2. 令 canonicalizedFilter 为 {}。

  3. 如果 filter.services 存在,则执行以下子步骤:

    1. 如果 filter.services.length === 0,则抛出 TypeError 并中止这些步骤。

    2. 令 services 为 Array.prototype.map.call(filter.services, BluetoothUUID.getService)。

    3. 如果任何 BluetoothUUID.getService() 调用抛出异常,则抛出该异常并中止这些步骤。

    4. 如果 services 中的任何服务被阻止,则抛出 SecurityError 并中止这些步骤。

    5. 将 canonicalizedFilter.services 设置为 services。

  4. 如果 filter.name 存在,则执行以下子步骤。

    1. 如果 filter.name 的UTF-8 编码超过 248 字节,则抛出 TypeError 并中止这些步骤。

      注:248 是蓝牙 设备名称中 UTF-8 码元的最大数量。
    2. 将 canonicalizedFilter.name 设置为 filter.name。

  5. 如果 filter.namePrefix 存在,则执行以下子步骤。

    1. 如果 filter.namePrefix.length === 0,或者 filter.namePrefix 的UTF-8 编码超过 248 字节, 则抛出 TypeError 并中止这些步骤。

      注:248 是蓝牙 设备名称中 UTF-8 码元的最大数量。
    2. 将 canonicalizedFilter.namePrefix 设置为 filter.namePrefix。

  6. 将 canonicalizedFilter["manufacturerData"] 设置为 []。

  7. 如果 filter["manufacturerData"] 存在且 filter["manufacturerData"].length === 0, 则抛出 TypeError 并中止这些步骤。

  8. 对于 filter["manufacturerData"] 中的每个 manufacturerData, 执行以下子步骤:

    1. 如果 manufacturerData 是被阻止的制造商数据 过滤器,则抛出 SecurityError 并中止这些步骤。

    2. 如果 canonicalizedFilter["manufacturerData"] 中存在一个对象 existing,其中 existing["companyIdentifier"] === manufacturerData["companyIdentifier"], 则抛出 TypeError 并中止这些步骤。

    3. 令 canonicalizedManufacturerDataFilter 为 manufacturerData 经规范化并 转换为 ECMAScript 值后的结果。如果这抛出异常, 则传播该异常并中止这些步骤。

    4. 将 canonicalizedManufacturerDataFilter["companyIdentifier"] 设置为 manufacturerData["companyIdentifier"]。

    5. 将 canonicalizedManufacturerDataFilter 追加到 canonicalizedFilter["manufacturerData"]。

  9. 将 canonicalizedFilter.serviceData 设置为 []。

  10. 如果 filter["serviceData"] 存在且 filter["serviceData"].length === 0, 则抛出 TypeError 并中止这些步骤。

  11. 对于 filter["serviceData"] 中的每个 serviceData, 执行以下子步骤:

    1. 令 service 为 BluetoothUUID.getService(serviceData["service"])。 如果这抛出异常,则传播该异常并中止这些 步骤。

    2. 如果 service 被阻止,则抛出 SecurityError 并中止 这些步骤。

    3. 令 canonicalizedServiceDataFilter 为 serviceData 经规范化并 转换为 ECMAScript 值后的结果。如果这抛出异常, 则传播该异常并中止这些步骤。

    4. 将 canonicalizedServiceDataFilter["service"] 设置为 service。

    5. 将 canonicalizedServiceDataFilter 追加到 canonicalizedFilter["serviceData"]。

  12. 返回 canonicalizedFilter。

对 BluetoothDataFilterInit filter 进行规范化的结果,是以下步骤返回的 BluetoothDataFilterInit:
  1. 如果 filter.dataPrefix 不存在,则令 dataPrefix 为空字节序列。否则, 执行以下子步骤:

    1. 令 dataPrefix 为 filter.dataPrefix 所持字节的副本。

    2. 如果 dataPrefix 的长度为零,则抛出 TypeError 并中止 这些步骤。

  2. 如果 filter.mask 存在,则令 mask 为 filter.mask 所持字节的副本。 否则,令 mask 为一个由 0xFF 字节组成、长度与 dataPrefix 相同的序列。

  3. 如果 mask 与 dataPrefix 的长度不同,则抛出 TypeError 并中止这些步骤。

  4. 返回 {dataPrefix: new Uint8Array(|dataPrefix|), mask: new Uint8Array(|mask|)}。

要使用参数 global 和可选的 服务 UUID 集合(默认为所有 UUID 的集合)扫描设备, UA 必须执行 以下步骤:
  1. 如果 UA 最近使用一个 UUID 集合扫描过设备,而该集合是 当前扫描 UUID 的超集,则 UA 可以返回该 扫描的结果并中止这些步骤。

    TODO:确定 时间长度。

  2. 令 nearbyDevices 为一个蓝牙设备集合,其初始值 等于当前已连接到 UA(具有ATT 承载)的设备集合。

  3. 令 topLevelTraversable 为 global 的可导航对象的 顶级可遍历对象。

  4. 令 simulatedBluetoothDevices 为一个空列表。

  5. 如果 topLevelTraversable 具有模拟 蓝牙适配器,则令 simulatedBluetoothDevices 为其模拟蓝牙设备映射的值。

    支持 异步设备发现。

  6. 如果 UA 支持 LE 传输,则执行通用 发现 过程,但 UA 可以包含未设置 可发现 模式标志的设备,并将发现的蓝牙 设备添加到 nearbyDevices。UA 应启用隐私 功能。

    被动 扫描和隐私功能都能避免 泄露唯一且不可变的设备 ID。我们应当要求 UA 使用 两者之一,但似乎没有任何操作系统 API 暴露其中任一种。蓝牙还 使得被动扫描难以使用,因为它并不要求 中心设备支持 观察过程。

  7. 如果 UA 支持 BR/EDR 传输,则执行设备发现 过程,并将发现的蓝牙设备添加到 nearbyDevices。

    所有形式的 BR/EDR 查询/发现似乎都会泄露唯一且 不可变的设备地址。

  8. 令 result 为一个初始为空的蓝牙设备集合。

  9. 对于 nearbyDevices 和 simulatedBluetoothDevices 中的每个 蓝牙设备 device,执行以下子步骤:

    1. 如果 device 的支持的物理传输包含 LE 且其蓝牙设备名称是部分名称或不存在, 则 UA 应执行名称发现过程以获取 完整名称。

    2. 如果 device 广播的服务 UUID与 服务 UUID 集合存在非空 交集,则将 device 添加到 result 并中止这些子步骤。

      注:对于 BR/EDR 设备,在扩展查询响应中无法区分 GATT 服务和非 GATT 服务。如果站点 使用非 GATT 服务的 UUID 进行过滤,则用户可能能够 为 requestDevice 的结果选择一个设备,而此 API 没有提供与该设备交互的方法。
    3. UA 可以连接到 device,并使用 UUID 位于 服务 UUID 集合中的所有服务填充蓝牙 缓存。如果 device 的支持的物理 传输包含 BR/EDR,则除了标准 GATT 过程外,UA 在填充缓存时还可以使用服务发现协议(搜索 服务)。

      注:连接到每个附近设备以发现服务会消耗 电力,并可能减慢蓝牙无线电的其他使用。UA 应 仅在有理由预期某个设备可能值得关注时,才在该设备上 发现额外服务。

      UA 还应帮助开发者避免依赖这种额外发现 行为。例如,假设开发者此前已连接到某个 设备,因此 UA 知道该设备支持的完整服务集合。 如果该开发者随后使用一个未广播的 UUID 进行过滤,则其看到的对话框 可能包含该设备,即使该过滤器在用户的机器上很可能 会排除该设备。UA 可以提供一个 开发者选项,在发生这种情况时发出警告,或在匹配过滤器时仅包含 已广播的服务。

    4. 如果蓝牙缓存包含 device 内已知存在且 UUID 位于 服务 UUID 集合中的服务,则 UA 可以将 device 添加到 result。

  10. 从扫描中返回 result。

我们需要一种方式,让站点 注册以便在感兴趣的设备进入范围时接收事件。

要将一个被允许的 蓝牙 设备 device 添加到 BluetoothPermissionStorage storage, 给定一组 requiredServiceUUIDs 和一组 optionalServiceUUIDs,UA 必须运行以下步骤:
  1. 令 grantedServiceUUIDs 为一个新的 Set。

  2. 将 requiredServiceUUIDs 的内容添加到 grantedServiceUUIDs。

  3. 将 optionalServiceUUIDs 的内容添加到 grantedServiceUUIDs。

  4. 在 storage.allowedDevices 中搜索一个元素 allowedDevice,其中 device 等于 allowedDevice@[[device]]。 如果找到一个,则执行以下子步骤:

    1. 将 allowedDevice.allowedServices 的内容添加到 grantedServiceUUIDs。

    如果未找到,则执行以下子步骤:

    1. 令 allowedDevice.deviceId 为一个唯一 ID,其唯一性的范围取决于 UA 能够确定两个蓝牙连接属于 同一设备的程度,以及 用户希望向 脚本暴露这一事实的程度。

  5. 将 allowedDevice.allowedServices 设置为 grantedServiceUUIDs。

  6. 将 allowedDevice.mayUseGATT 设置为 true。

要移除一个被允许的蓝牙 设备 device,给定一个 BluetoothPermissionStorage storage, UA 必须运行以下步骤:

  1. 在 storage.allowedDevices 中搜索一个元素 allowedDevice,其中 device 等于 allowedDevice@[[device]]。 如果不存在这样的 元素,则中止这些步骤。

  2. 从 storage.allowedDevices 中移除 allowedDevice。

4.1. Permission API 集成

[permissions] API 为网站提供了一种统一的方式来查询它们拥有哪些 权限。

一旦站点被授予对一组设备的访问权限,它便可以使用 navigator.permissions.query({name: "bluetooth", ...}) 在重新加载后检索这些 设备。
navigator.permissions.query({
  name: "bluetooth",
  deviceId: sessionStorage.lastDevice,
}).then(result => {
  if (result.devices.length == 1) {
    return result.devices[0];
  } else {
    throw new DOMException("权限已丢失", "NotFoundError");
  }
}).then(...);

Web Bluetooth API 是一个由 强大功能,其 名称为 "bluetooth"。其 与权限相关的算法和类型定义如下:

权限描述符类型
dictionary BluetoothPermissionDescriptor : PermissionDescriptor {
  DOMString deviceId;
  // 这些与 RequestDeviceOptions 匹配。
  sequence<BluetoothLEScanFilterInit> filters;
  sequence<BluetoothServiceUUID> optionalServices = [];
  sequence<unsigned short> optionalManufacturerData = [];
  boolean acceptAllDevices = false;
};
额外权限数据类型
BluetoothPermissionStorage, 定义如下:
dictionary AllowedBluetoothDevice {
  required DOMString deviceId;
  required boolean mayUseGATT;
  // allowedServices 为 "all" 表示允许所有服务。
  required (DOMString or sequence<UUID>) allowedServices;
  required sequence<unsigned short> allowedManufacturerData;
};
dictionary BluetoothPermissionStorage {
  required sequence<AllowedBluetoothDevice> allowedDevices;
};

AllowedBluetoothDevice 实例具有一个内部槽 [[device]],其中 保存一个 蓝牙 设备。

额外权限数据约束
allowedDevices 中不同的元素必须具有 不同的 [[device]] 和不同的 deviceId。

如果 mayUseGATT 为 false, 则 allowedServices 和 allowedManufacturerData 必须都为 []。

注:deviceId 允许站点跟踪某个时刻看到的 BluetoothDevice 实例与另一个时刻看到的另一个 BluetoothDevice 实例是否表示同一设备,而后者可能位于 不同的realm 中。在返回 "bluetooth" 的 额外权限数据时,UA 应考虑其用户是否希望 发生或不发生这种跟踪。

例如,用户通常不希望两个不同的源知道 它们正在与同一设备交互,也通常不希望 在清除某个源的 cookie 后唯一标识符仍然持续存在。

权限结果类型
[Exposed=Window]
interface BluetoothPermissionResult : PermissionStatus {
  attribute FrozenArray<BluetoothDevice> devices;
};
权限查询算法
要使用一个 BluetoothPermissionDescriptor desc 和一个 BluetoothPermissionResult status 查询 "bluetooth" 权限,UA 必须:
  1. 令 global 为 status 的相关全局对象。

  2. 将 status.state 设置为 desc 的 权限状态。

  3. 如果 status.state 为“denied”, 则将 status.devices 设置为空的 FrozenArray 并中止 这些步骤。

  4. 令 matchingDevices 为一个新的 Array。

  5. 令 storage(一个 BluetoothPermissionStorage)为 "bluetooth" 对当前设置对象的 额外权限数据。

  6. 对于 storage.allowedDevices 中的每个 allowedDevice,运行 以下子步骤:

    1. 如果设置了 desc.deviceId 且 allowedDevice.deviceId != desc.deviceId,则继续处理下一个 allowedDevice。

    2. 如果设置了 desc.filters,则执行以下子步骤:

      1. 将 desc.filters 中的每个过滤器替换为 对其进行规范化 的结果。如果这些规范化中的任何一个抛出错误,则返回 该错误并中止这些步骤。

      2. 如果 allowedDevice.[[device]] 不 匹配 desc.filters 中的过滤器,则继续 处理下一个 allowedDevice。

    3. 在 global.navigator.bluetooth 中获取表示 allowedDevice.[[device]] 的 BluetoothDevice,并将 结果添加到 matchingDevices。

    注:desc.optionalServices 和 desc.optionalManufacturerData 字段不会影响 结果。
  7. 将 status.devices 设置为一个新的 FrozenArray, 其 内容为 matchingDevices。

权限撤销算法
要撤销 蓝牙访问权限,使设备不再暴露给用户无意继续暴露的站点, UA 必须运行以下步骤:
  1. 令 storage(一个 BluetoothPermissionStorage)为 "bluetooth" 对当前设置对象的 额外权限数据。

  2. 对于当前 realm 中的每个 BluetoothDevice 实例 deviceObj,运行以下子步骤:

    1. 如果 storage.allowedDevices 中存在一个 AllowedBluetoothDevice allowedDevice,满足:

      则将 deviceObj.[[allowedServices]] 更新为 allowedDevice.allowedServices, 并继续处理 下一个 deviceObj。

    2. 否则,通过运行其余步骤将 deviceObj 与其设备分离。

    3. 调用 deviceObj.gatt.disconnect()。

      注:这会在 deviceObj 上触发 gattserverdisconnected 事件。
    4. 将 deviceObj.[[representedDevice]] 设置为 null。

4.2. 整体蓝牙可用性

UA 可能运行在没有蓝牙无线电的计算机上。 requestDevice() 通过无法发现任何设备来处理这种情况,这会 导致 NotFoundError, 但网站也许能够以更 友好的方式进行处理。

仅向拥有蓝牙适配器的用户显示蓝牙 UI:
const bluetoothUI = document.querySelector('#bluetoothUI');
navigator.bluetooth.getAvailability().then(isAvailable => {
  bluetoothUI.hidden = !isAvailable;
});
navigator.bluetooth.addEventListener('availabilitychanged', e => {
  bluetoothUI.hidden = !e.value;
});
getAvailability() 方法 在被调用时,必须返回一个新的 promise promise,并并行运行以下 步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 如果 global 的关联 Document 不被允许使用名为 “bluetooth”的 策略控制功能,则在给定 global 的蓝牙任务源上排入一个全局任务,以使用 false 兑现 promise, 并中止这些步骤。

  3. 如果用户已将 UA 配置为让此函数针对当前源返回 特定答案,则排入一个 任务,以使用所配置的答案兑现 promise,并中止 这些步骤。

    注:如果用户已阻止 Web Bluetooth 权限,UA 可以使用 false 兑现 promise。
  4. 令 simulatedBluetoothAdapter 为 this 的可导航对象的 顶级可遍历对象的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 不为空,

    1. 如果 simulatedBluetoothAdapter 的适配器状态为 "absent",则在给定 global 的蓝牙任务源上排入一个全局任务,以使用 false 兑现 promise。

    2. 如果 simulatedBluetoothAdapter 的LE 支持状态为 false,则在给定 global 的蓝牙任务源上排入一个全局任务,以使用 false 兑现 promise。

    3. 否则,在给定 global 的蓝牙任务源上排入一个全局任务,以使用 true 兑现 promise。

    4. 中止这些步骤。

  6. 如果 UA 正在具有蓝牙无线电的系统上运行,则无论蓝牙 无线电是否开启,都在给定 global 的蓝牙任务源上排入一个全局任务,以使用 true 兑现 promise。

  7. 否则,在给定 global 的蓝牙任务源上排入一个全局任务,以使用 false 兑现 promise。

    注:promise 会被并行兑现,以允许 UA 调用 其他系统来确定蓝牙是否可用。
如果用户已阻止该权限,并且 UA 使用 false 兑现 getAvailability promise,则可以使用以下代码 检测蓝牙何时再次可用,以显示蓝牙 UI:
function checkAvailability() {
  const bluetoothUI = document.querySelector('#bluetoothUI');
  navigator.bluetooth.getAvailability().then(isAvailable => {
    bluetoothUI.hidden = !isAvailable;
  });
}

navigator.permissions.query({name: "bluetooth"}).then(status => {
  if (status.state !== 'denied') checkAvailability();

  // 蓝牙已被阻止,监听 PermissionStatus 的变化。
  status.onchange = () => {
    if (this.state !== 'denied') checkAvailability();
  };
});
如果 UA 变得能够或无法使用蓝牙,例如因为无线电 被物理连接或断开,或者用户更改了从 getAvailability() 返回答案的配置, 则 UA 必须对每个 全局对象 global,在给定该对象的蓝牙任务源上排入一个全局任务,以运行以下步骤:
  1. 令 oldAvailability 为变化前 getAvailability() 本会返回的值。

  2. 令 newAvailability 为变化后 getAvailability() 会返回的值。

  3. 如果 oldAvailability 与 newAvailability 不同,

    1. 令 navigator 为 global 的关联 Navigator。

    2. 令 bluetooth 为 navigator 的关联 Bluetooth。

    3. 在 bluetooth 上使用 ValueEvent 接口触发一个名为 availabilitychanged 的事件,并将其 value 属性 初始化为 newAvailability。

[
  Exposed=Window,
  SecureContext
]
interface ValueEvent : Event {
  constructor(DOMString type, optional ValueEventInit initDict = {});
  readonly attribute any value;
};

dictionary ValueEventInit : EventInit {
  any value = null;
};

ValueEvent 实例按照 DOM § 2.5 构造事件 中的规定进行构造。

value 属性必须返回其 初始化时的值。

这样一种通用事件类型 应属于 [HTML] 或 [DOM],而不应定义在这里。

5. 设备表示

UA 需要在多个层级跟踪蓝牙设备属性:全局、 每个源以及每个全局对象。

5.1. 全局蓝牙设备属性

物理蓝牙设备可能保证具有某些 UA 尚未接收到的属性。这些属性在此被描述为可选属性。

一个蓝牙设备具有 以下属性。除非另有说明,否则可选 属性不存在,序列和映射属性为空。 其他属性具有指定的默认值,或者 在引入设备时指定。

当且仅当两个蓝牙设备具有相同的 公共 蓝牙地址、静态地址、私有 地址或身份解析密钥,或者使用一个设备的 IRK 和另一个设备的 可解析私有 地址成功执行可解析私有 地址解析过程时,UA 应确定它们是同一蓝牙 设备。但是,由于平台 API 并未记录 它们如何确定设备身份,UA 可以使用其他过程。

5.2. BluetoothDevice

一个 BluetoothDevice 实例表示特定全局对象(或者等价地,特定的Realm 或 Bluetooth 实例)的一个蓝牙设备。

[Exposed=Window, SecureContext]
interface BluetoothDevice : EventTarget {
  readonly attribute DOMString id;
  readonly attribute DOMString? name;
  readonly attribute BluetoothRemoteGATTServer? gatt;

  Promise<undefined> forget();
  Promise<undefined> watchAdvertisements(
      optional WatchAdvertisementsOptions options = {});
  readonly attribute boolean watchingAdvertisements;
};
BluetoothDevice includes BluetoothDeviceEventHandlers;
BluetoothDevice includes CharacteristicEventHandlers;
BluetoothDevice includes ServiceEventHandlers;

dictionary WatchAdvertisementsOptions {
  AbortSignal signal;
};
注:BluetoothDevice 属性
id 在 UA 能够确定两个蓝牙连接属于同一设备的范围内,以及 用户希望向脚本暴露这一 事实的范围内,唯一标识设备。

name 是设备的人类可读名称。

gatt 在站点具有相应权限时,提供一种与此设备的 GATT 服务器进行交互的方式。

forget() 使页面能够撤销用户已授予的 对该设备的访问权限。

如果 UA 当前正在扫描来自此设备的 广播并为其触发事件,则 watchingAdvertisements 为 true。

BluetoothDevice 实例使用下表所述的内部槽 创建:

内部槽 初始值 描述(非规范性)
[[context]] <始终在正文中设置> 返回此 BluetoothDevice 的 Bluetooth 对象。
[[representedDevice]] <始终在正文中设置> 此对象所表示的蓝牙 设备; 如果访问权限已被撤销,则为 null。
[[gatt]] 一个新的 BluetoothRemoteGATTServer 实例,其 device 属性初始化为 this,并且 其 connected 属性初始化为 false。 不会改变。
[[allowedServices]] <始终在正文中设置> 此设备针对该 源的 allowedServices 列表;如果允许所有服务,则为 "all"。例如,UA 可以授予 某个源访问 referringDevice 上所有服务的权限,该设备曾广播 此源上的 URL。
[[allowedManufacturerData]] <始终在正文中设置> 此设备针对 该源的 allowedManufacturerData 列表。
[[watchAdvertisementsState]] 'not-watching' 一个描述 watchAdvertisements() 操作当前状态的字符串枚举。可能的 枚举值为:
  • 'not-watching'

  • 'pending-watch'

  • 'watching'

要在一个 Bluetooth 实例 context 中获取 表示一个蓝牙设备 device 的 BluetoothDevice,UA 必须运行以下步骤:
  1. 令 storage(一个 BluetoothPermissionStorage)为 "bluetooth" 对当前设置对象的 额外权限数据。

  2. 在 storage.allowedDevices 中查找 allowedDevice,其 allowedDevice.[[device]] 与 device 是同一设备。如果不存在这样的对象,则抛出 SecurityError 并中止这些步骤。

  3. 如果 context.[[deviceInstanceMap]] 中不存在与 device 属于同一设备的键,则运行以下 子步骤:

    1. 令 result 为一个新的 BluetoothDevice 实例。

    2. 将 result 的所有可选字段初始化为 null。

    3. 将 result.[[context]] 初始化为 context。

    4. 将 result.[[representedDevice]] 初始化为 device。

    5. 将 result.id 初始化为 allowedDevice.deviceId, 并将 result.[[allowedServices]] 初始化为 allowedDevice.allowedServices。

    6. 如果 device 具有部分或完整蓝牙设备 名称,则将 result.name 设置为该字符串。

    7. 将 result.watchingAdvertisements 初始化为 false。

    8. 在 context.[[deviceInstanceMap]] 中添加从 device 到 result 的映射。

  4. 返回 context.[[deviceInstanceMap]] 中键与 device 属于同一设备的值。

获取 gatt 属性必须执行以下步骤:
  1. 如果 "bluetooth" 对 this 的相关 设置对象的额外权限数据在其 allowedDevices 列表中具有一个 AllowedBluetoothDevice allowedDevice,其中 allowedDevice.[[device]] 与 this.[[representedDevice]] 是同一 设备,并且 allowedDevice.mayUseGATT 等于 true,则返回 this.[[gatt]]。

  2. 否则,返回 null。

forget() 方法在 被调用时,必须返回一个新的 promise promise,并运行以下步骤:
  1. 令 device 为目标 BluetoothDevice 对象。

  2. 令 storage 为当前 脚本执行环境中的 BluetoothPermissionStorage 对象。

  3. 使用 storage 从存储中移除 device。

  4. 兑现 promise。

用户代理具有一个关联的广播监听管理器,它是 启动一个新的并行队列的结果。

watchAdvertisements(options) 方法在被调用时,必须返回一个新的 promise promise,并 运行以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 如果 options.signal 存在,则执行 以下子步骤:

    1. 如果 options.signal 已 中止,则使用 this 中止 watchAdvertisements 并中止这些步骤。

    2. 向 options.signal 添加以下中止步骤:

      1. 使用 this 中止 watchAdvertisements。

      2. 以 AbortError 拒绝 promise。

  3. 如果 this.[[watchAdvertisementsState]] 为:

    'not-watching'
    1. 将 this.[[watchAdvertisementsState]] 设置为 'pending-watch'。

    2. 将以下步骤排入 广播监听管理器, 但当 this.[[watchAdvertisementsState]] 变为 not-watching 时中止:

      1. 确保 UA 正在扫描此设备的 广播。UA 不应过滤掉同一设备的“重复” 广播。

      2. 如果 UA 无法启用扫描,则在给定 global 的蓝牙任务源上排入一个全局任务以执行 以下步骤,并中止这些步骤:

        1. 将 this.[[watchAdvertisementsState]] 设置为 'not-watching'。

        2. 以下列错误之一拒绝 promise:

          UA 不支持扫描广播

          NotSupportedError

          蓝牙已关闭

          InvalidStateError

          其他原因

          UnknownError

      3. 在给定 global 的蓝牙任务源上排入一个全局任务以执行以下步骤,但当 this.[[watchAdvertisementsState]] 变为 not-watching 时中止:

        1. 将 this.[[watchAdvertisementsState]] 设置为 watching。

        2. 将 this.watchingAdvertisements 设置为 true。

        3. 以 undefined 兑现 promise。

    'pending-watch'
    1. 以 InvalidStateError 拒绝 promise。

    'watching'
    1. 以 undefined 兑现 promise。

  4. 如果已中止,则以 AbortError 拒绝 promise。

注:扫描会消耗电力,因此网站应避免 不必要地监听广播, 并应使用其 AbortController 尽快停止电力 消耗。

要为一个 BluetoothDevice device 中止 watchAdvertisements, 运行以下步骤:
  1. 将 this.[[watchAdvertisementsState]] 设置为 'not-watching'。

  2. 将 device.watchingAdvertisements 设置为 false。

  3. 将以下步骤排入广播监听管理器:

    1. 如果整个 UA 中不再有任何 BluetoothDevice 的 watchingAdvertisements 被设置为 true,UA 应停止扫描 广播。否则,如果不再有任何表示与 this 相同设备的 BluetoothDevice 的 watchingAdvertisements 被设置为 true,UA 应重新配置扫描,以避免接收 此设备的报告。

给定一个 Bluetooth bluetooth,要中止所有活动的 watchAdvertisements 操作,运行以下步骤:
  1. 对于 bluetooth.[[deviceInstanceMap]] 中的每个 device, 执行以下步骤:

    1. 如果 device.[[watchAdvertisementsState]] 为 pending-watch 或 watching,则使用 device 运行中止 watchAdvertisements。

5.2.1. 处理可见性变化

发起蓝牙设备扫描的操作只能在可见的 文档中运行。当可见性状态不再为 "visible" 时,需要中止该文档 的扫描操作。

本规范定义了以下给定 visibilityState 和 document 的页面可见性变化步骤:
  1. 令 global 为 document 的相关全局对象。

  2. 在给定 global 的蓝牙任务源上排入一个全局任务, 以 执行以下步骤:

    1. 令 navigator 为 global 的关联 Navigator。

    2. 令 bluetooth 为 navigator 的关联 Bluetooth。

    3. 如果 visibilityState 不为 "visible",则在 bluetooth 上中止所有活动的 watchAdvertisements 操作。

5.2.2. 处理 Document 失去完全活动状态

发起蓝牙设备扫描的操作只能在完全活动的文档中运行。 当失去完全活动状态时,需要中止该文档的扫描 操作。

当用户代理确定一个 Document 不再完全活动时,必须运行以下步骤:
  1. 令 document 为不再完全活动的 Document。

  2. 令 global 为 document 的相关全局对象。

  3. 在给定 global 的蓝牙任务源上排入一个全局任务, 以 执行以下步骤:

    1. 令 navigator 为 global 的关联 Navigator。

    2. 令 bluetooth 为 navigator 的关联 Bluetooth。

    3. 在 bluetooth 上运行中止所有活动的 watchAdvertisements 操作。

5.2.3. 响应广播事件

当一个广播 事件到达某个 BluetoothDevice, 且其 watchingAdvertisements 已设置时, UA 会分发一个“advertisementreceived” 事件。

[Exposed=Window, SecureContext]
interface BluetoothManufacturerDataMap {
  readonly maplike<unsigned short, DataView>;
};
[Exposed=Window, SecureContext]
interface BluetoothServiceDataMap {
  readonly maplike<UUID, DataView>;
};
[
  Exposed=Window,
  SecureContext
]
interface BluetoothAdvertisingEvent : Event {
  constructor(DOMString type, BluetoothAdvertisingEventInit init);
  [SameObject]
  readonly attribute BluetoothDevice device;
  readonly attribute FrozenArray<UUID> uuids;
  readonly attribute DOMString? name;
  readonly attribute unsigned short? appearance;
  readonly attribute byte? txPower;
  readonly attribute byte? rssi;
  [SameObject]
  readonly attribute BluetoothManufacturerDataMap manufacturerData;
  [SameObject]
  readonly attribute BluetoothServiceDataMap serviceData;
};
dictionary BluetoothAdvertisingEventInit : EventInit {
  required BluetoothDevice device;
  sequence<(DOMString or unsigned long)> uuids;
  DOMString name;
  unsigned short appearance;
  byte txPower;
  byte rssi;
  BluetoothManufacturerDataMap manufacturerData;
  BluetoothServiceDataMap serviceData;
};
device 是发送此广播的 BluetoothDevice。

uuids 列出此广播所表明 device 的 GATT 服务器支持的服务 UUID。

name 是 device 的 本地名称或其前缀。

appearance 是一个外观值,即 gap.appearance 特征定义的值之一。

txPower 是设备进行 广播时的发射功率,以 dBm 为单位。它用于按 this.txPower - this.rssi 计算路径损耗。

rssi 是接收到 广播时的功率,以 dBm 为单位。它用于按 this.txPower - this.rssi 计算路径损耗。

manufacturerData 将 unsigned short 公司标识符 代码映射到 DataView。

serviceData 将 UUID 映射到 DataView。

要检索设备并从中读取 iBeacon 数据,开发者可以使用 以下代码。请注意,此 API 目前并未提供一种 请求具有特定制造商数据的设备的方法,因此 iBeacon 需要 轮换其广播,以包含一个已知服务,从而让用户能够 在 requestDevice 对话框中选择此设备。
var known_service = "A service in the iBeacon's GATT server";
return navigator.bluetooth.requestDevice({
  filters: [{services: [known_service]}]
}).then(device => {
  device.watchAdvertisements();
  device.addEventListener('advertisementreceived', interpretIBeacon);
});

function interpretIBeacon(event) {
  var rssi = event.rssi;
  var appleData = event.manufacturerData.get(0x004C);
  if (appleData.byteLength != 23 ||
    appleData.getUint16(0, false) !== 0x0215) {
    console.log({isBeacon: false});
  }
  var uuidArray = new Uint8Array(appleData.buffer, 2, 16);
  var major = appleData.getUint16(18, false);
  var minor = appleData.getUint16(20, false);
  var txPowerAt1m = -appleData.getInt8(22);
  console.log({
      isBeacon: true,
      uuidArray,
      major,
      minor,
      pathLossVs1m: txPowerAt1m - rssi});
});

iBeacon 广播的格式源自 Adam Warski 的iBeacon 如何工作?。

当 UA 接收到一个广播事件(由一个广播 数据包和一个可选的扫描响应组成)时,必须运行以下步骤:
  1. 令 device 为发送该 广播事件的蓝牙设备。

  2. 对于 UA 中每个 BluetoothDevice deviceObj,如果 device 与 deviceObj.[[representedDevice]] 是同一设备, 则在 deviceObj 的相关设置对象的 负责的事件循环上排入一个 任务,以执行以下 子步骤:

    1. 如果 deviceObj.watchingAdvertisements 为 false,则中止这些子步骤。

    2. 针对该广播事件,在 deviceObj 上触发一个 advertisementreceived 事件。

要针对广播事件 adv 在一个 BluetoothDevice deviceObj 上触发一个 advertisementreceived 事件, UA 必须执行以下步骤:
  1. 令 event 为

    {
      bubbles: true,
      device: deviceObj,
      uuids: [],
      manufacturerData: new Map(),
      serviceData: new Map()
    }
    
  2. 如果 adv 中任何数据包的接收信号强度 可用,则将 event.rssi 设置为该信号强度(以 dBm 为单位)。

  3. 对于 adv 的广播数据包和扫描响应中的每个AD 结构,根据 AD 类型从以下步骤中进行选择:

    16 位服务 UUID的不完整列表
    16 位服务 UUID的完整列表
    32 位服务 UUID的不完整列表
    32 位服务 UUID的完整列表
    128 位服务 UUID的不完整列表
    128 位服务 UUID的完整列表
    对列出的 UUID 中的每个 uuid,如果它位于 this.device.[[allowedServices]] 中,则 将 uuid 追加到 event.uuids。
    缩短的本地名称
    完整的本地名称
    对 AD 数据执行无 BOM 的 UTF-8 解码,并 将 event.name 设置为结果。
    注:我们不会暴露名称是否完整,因为现有 API 需要读取原始广播才能获取此信息, 而在向 API 添加字段之前,我们希望获得更多证据来证明它有用。
    制造商特定数据
    对于每个 16 位公司标识符代码 manufacturerCode,如果它位于 this.device.[[allowedManufacturerData]] 中,并且制造商数据不是被阻止的制造商数据, 则向 event.manufacturerData 添加从 manufacturerCode 到一个包含制造商特定数据的 ArrayBuffer 的映射。
    TX 功率 电平
    将 event.txPower 设置为 AD 数据。
    服务数据 - 16 位 UUID
    服务数据 - 32 位 UUID
    服务数据 - 128 位 UUID
    对于服务数据中的每个UUID uuid,如果它 位于 this.device.[[allowedServices]] 中,则向 event.serviceData 添加从 uuid 到一个 包含 服务数据的 ArrayBuffer 的映射。
    外观
    将 event.appearance 设置为 AD 数据。
    其他情况
    跳到下一个 AD 结构。
  4. 在 deviceObj 上,使用 BluetoothAdvertisingEvent 并以 event 初始化,触发一个名为“advertisementreceived”的事件, 并将其 isTrusted 属性初始化 为 true。

BluetoothAdvertisingEvent 中的所有字段都会返回其最后一次 被初始化或设置为的值。

BluetoothAdvertisingEvent(type, init) 构造函数必须执行以下步骤:
  1. 令 event 为运行 DOM § 2.5 构造事件中的步骤所得的结果,但不包括 uuids、 manufacturerData 和 serviceData 成员。

  2. 如果设置了 init.uuids,则将 event.uuids 初始化为一个新的 FrozenArray, 其中包含 init.uuids.map( BluetoothUUID.getService) 的元素。 否则,将 event.uuids 初始化为空的 FrozenArray。

  3. 对于 init.manufacturerData 中的每个映射:

    1. 令 code 为转换成 unsigned short 的键。

    2. 令 value 为该值。

    3. 如果 value 不是 BufferSource, 则抛出 TypeError。

    4. 令 bytes 为一个新的只读 ArrayBuffer,其中包含 value 所持字节的副本。

    5. 在 event.manufacturerData.[[BackingMap]] 中添加从 code 到 new DataView(bytes) 的映射。

  4. 对于 init.serviceData 中的每个映射:

    1. 令 key 为该键。

    2. 令 service 为调用 BluetoothUUID.getService(key). 所得的结果。

    3. 令 value 为该值。

    4. 如果 value 不是 BufferSource, 则抛出 TypeError。

    5. 令 bytes 为一个新的只读 ArrayBuffer,其中包含 value 所持字节的副本。

    6. 在 event.serviceData.[[BackingMap]] 中添加从 service 到 new DataView(bytes) 的映射。

  5. 返回 event。

5.2.3.1. BluetoothManufacturerDataMap

BluetoothManufacturerDataMap 实例具有一个 [[BackingMap]] 槽,因为它们是 maplike,它将制造商代码映射到 制造商的数据,并将其转换为 DataView。

5.2.3.2. BluetoothServiceDataMap

BluetoothServiceDataMap 实例具有一个 [[BackingMap]] 槽,因为它们是 maplike,它将服务 UUID 映射到 服务的数据,并将其转换为 DataView。

6. GATT 交互

6.1. GATT 信息模型

GATT 配置文件 层次结构描述了 GATT 服务器如何包含由 配置文件、主服务、包含服务、 特征和描述符组成的层次结构。

配置文件纯粹是逻辑上的:配置文件规范描述了 该配置文件所包含的其他 GATT 实体之间的预期交互,但 无法查询设备支持哪些配置文件。

GATT 客户端可以使用一组GATT 过程发现设备上的服务、 特征和描述符并与其交互。本规范将服务、特征和描述符 统称为属性。所有属性都有一个由 UUID 标识的类型。每个属性还 有一个 16 位属性 句柄,用于将其与同一 GATT 服务器上相同类型的其他属性区分开来。属性 在概念上按照其属性句柄在其GATT 服务器内排序,但虽然平台接口 会以某种顺序提供属性,却不能保证该顺序与属性 句柄顺序一致。

一个服务包含一组 包含服务和 特征。包含 服务是对其他服务的引用,并且单个服务可以被多个 其他服务包含。如果服务直接出现在 GATT 服务器下,则称为主 服务;如果仅被其他服务包含,则称为次级服务,不过主服务也可以被 包含。

一个特征包含一个 值(即字节数组)以及一组 描述符。根据 特征的属性, GATT 客户端 可以读取或写入其值,或者注册以便 在值发生变化时收到通知。

最后,一个描述符包含一个 值(同样是字节数组),用于描述或配置其 特征。

6.1.1. 跨连接持久化

蓝牙属性 缓存系统允许已绑定的客户端 保存从一次连接到下一次连接的属性引用。Web Bluetooth 将网站视为与其获准 访问的设备未绑定:BluetoothRemoteGATTService、 BluetoothRemoteGATTCharacteristic 和 BluetoothRemoteGATTDescriptor 对象会在 断开连接时失效,并且站点在重新连接时必须再次检索 它们。

6.1.2. 蓝牙缓存

UA 必须维护一个蓝牙缓存,其中保存其在 设备上发现的服务、特征和描述符的层次结构。UA 可以在访问同一 设备的多个源之间共享此缓存。缓存中的每个潜在条目都处于已知存在、已知不存在 或未知状态。缓存不得包含两个属于同一 属性的条目。缓存中每个已知存在的条目都与每个 Bluetooth 实例的一个可选 Promise<BluetoothRemoteGATTService>、 Promise<BluetoothRemoteGATTCharacteristic> 或 Promise<BluetoothRemoteGATTDescriptor> 实例相关联。

注:例如,如果用户在 蓝牙缓存初始为空时调用 serviceA.getCharacteristic(uuid1) 函数,UA 会使用按 UUID 发现特征 过程来填充所需的缓存条目,并且 UA 会提前结束该过程, 因为它只需要一个特征来兑现返回的 Promise, 那么 serviceA 内第一个 UUID 为 uuid1 的特征 为已知存在,而之后所有具有该 UUID 的特征仍为未知。如果用户之后调用 serviceA.getCharacteristics(uuid1),UA 需要恢复或 重新启动按 UUID 发现特征过程。如果最终发现 serviceA 只有一个 UUID 为 uuid1 的特征,那么后续特征将变为已知不存在。

蓝牙缓存中的已知存在条目是有序的:主 服务在设备中以特定顺序出现,包含服务和 特征在服务中以特定顺序出现,而描述符 在特征中以特定顺序出现。该顺序应与设备上属性 句柄的顺序一致,但如果设备的顺序不可用,UA 可以使用其他顺序。

要使用与某个 描述匹配的条目填充 蓝牙缓存,UA 必须运行以下步骤。
注:这些步骤可能会阻塞,因此此算法的使用必须并行 运行。
  1. 尝试使缓存中所有匹配条目变为已知存在或 已知不存在,可以使用[BLUETOOTH42]所规定能够返回足够 信息的任意GATT 过程序列。按照 § 6.7 错误处理中的说明处理错误。

  2. 如果上一步返回错误,则从此算法返回该错误。

要在 BluetoothDevice 实例 deviceObj 中查询 蓝牙缓存以查找与某个描述匹配的条目,UA 必须返回一个包裹 新的 promise promise 的 deviceObj.gatt-连接检查包装器,并并行运行以下步骤:
  1. 令 global 为 deviceObj 的相关全局对象。

  2. 使用与描述 匹配的条目填充蓝牙缓存。

  3. 如果上一步返回错误,则在给定 global 的 蓝牙任务源上排入一个全局任务,以使用该 错误拒绝 promise,并中止这些步骤。

  4. 令 entries 为与该 描述匹配的已知存在缓存条目的序列。

  5. 令 context 为 deviceObj.[[context]]。

  6. 令 result 为一个新序列。

  7. 对于 entries 中的每个 entry:

    1. 如果 entry 没有关联的 Promise<BluetoothGATT*> 实例位于 context.[[attributeInstanceMap]] 中, 创建一个 BluetoothRemoteGATTService,表示 entry, 创建一个 BluetoothRemoteGATTCharacteristic 表示 entry,或创建一个 BluetoothRemoteGATTDescriptor,表示 entry, 具体取决于 entry 是 Service、Characteristic 还是 Descriptor,并添加从 entry 到所得 Promise 的映射,位于 context.[[attributeInstanceMap]] 中。

    2. 将 context.[[attributeInstanceMap]] 中与 entry 关联的 Promise<BluetoothGATT*> 实例追加到 result。

  8. 排入一个全局任务 到 蓝牙任务源 (给定 global),以 兑现 promise,使用等待全部 result 元素所得的结果。

Represented(obj: 设备或 GATT 属性) 根据 obj 的类型返回:
BluetoothDevice
obj.[[representedDevice]]
BluetoothRemoteGATTService
obj.[[representedService]]
BluetoothRemoteGATTCharacteristic
obj.[[representedCharacteristic]]
BluetoothRemoteGATTDescriptor
obj.[[representedDescriptor]]
要执行 GetGATTChildren( attribute: GATT 属性,
single: boolean,
uuidCanonicalizer: function,
uuid: 可选的 (DOMString or unsigned int),
allowedUuids: 可选的 ("all" or Array<DOMString>),
child type: GATT 声明类型),

UA 必须执行以下步骤:
  1. 如果存在 uuid,则将其设置为 uuidCanonicalizer(uuid)。如果 uuidCanonicalizer 抛出异常,则返回以该异常 拒绝的 promise并中止这些步骤。

  2. 如果 uuid 存在且被阻止,则返回一个 被拒绝的 promise,拒绝原因为一个 SecurityError 并中止这些步骤。

  3. 根据 attribute 的类型,令 deviceObj 为:

    BluetoothDevice
    attribute
    BluetoothRemoteGATTService
    attribute.device
    BluetoothRemoteGATTCharacteristic
    attribute.service.device
  4. 如果 deviceObj.gatt.connected 为 false,则返回一个被拒绝的 promise,其拒绝原因为一个 NetworkError 并中止这些步骤。

  5. 如果 Represented(attribute) 为 null,则返回 一个被拒绝的 promise,其拒绝原因为一个 InvalidStateError 并中止这些步骤。

    注:当服务或 特征从设备中移除或因断开连接而失效,随后再次使用其对象时,就会发生这种情况。

  6. 在 deviceObj 中查询蓝牙缓存以查找 满足以下条件的条目:

    • 位于 Represented(attribute) 内,

    • 具有由 child type 描述的类型,

    • 具有未被阻止的 UUID,

    • 如果存在 uuid,则具有 UUID uuid,

    • 如果存在 allowedUuids 且其不为 "all",则具有 allowedUuids 中的 UUID,并且

    • 如果设置了 single 标志,则是其中第一个。

    令 promise 为结果。

  7. 兑现后,对于 promise 以 result 兑现的情况,运行 以下步骤:

    • 如果 result 为空,则抛出 NotFoundError。

    • 否则,如果设置了 single 标志,则返回 result 的第一个(也是唯一的) 元素。

    • 否则,返回 result。

6.1.4. 识别服务、特征和描述符

在检查两个服务、特征或描述符 a 和 b 是否为同一 属性时,如果 a 和 b 位于同一 设备内并具有相同的属性句柄,则 UA 应确定 它们相同,但 UA 可以使用任意算法, 限制条件是,如果 a 和 b 符合以下任一 条件,则不得将其视为同一 属性:

注:该定义较为宽松,因为平台 API 暴露了其自身的 标识概念,但并未记录它是否基于属性句柄 相等性。
注:对于表示 服务、特征或描述符的两个 Javascript 对象 x 和 y,x === y 会返回这些对象是否表示同一 属性,这是由于查询蓝牙缓存算法 创建和缓存新对象的方式。

6.2. BluetoothRemoteGATTServer

BluetoothRemoteGATTServer 表示远程设备上的GATT 服务器。

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTServer {
  [SameObject]
  readonly attribute BluetoothDevice device;
  readonly attribute boolean connected;
  Promise<BluetoothRemoteGATTServer> connect();
  undefined disconnect();
  Promise<BluetoothRemoteGATTService> getPrimaryService(BluetoothServiceUUID service);
  Promise<sequence<BluetoothRemoteGATTService>>
    getPrimaryServices(optional BluetoothServiceUUID service);
};
device 是运行 此服务器的设备。

当此实例连接到 this.device 时,connected 为 true。即使 UA 在物理上已连接, 它也可能为 false,例如当其他全局对象具有其他已连接的 BluetoothRemoteGATTServer 实例时。

当任何 ECMAScript 代码都无法再 观察到 BluetoothRemoteGATTServer 实例 server 时, UA 应运行 server.disconnect()。

注:因为 BluetoothDevice 实例存储在 navigator.bluetooth.[[deviceInstanceMap]] 中, 所以至少在导航释放全局对象,或关闭标签页 或窗口销毁浏览上下文之前,这种情况不会 发生。
注:在垃圾回收时断开连接可确保 UA 不会继续不必要地消耗 远程设备上的资源。

BluetoothRemoteGATTServer 实例使用下表所述的内部 槽创建:

内部槽 初始值 描述(非规范性)
[[activeAlgorithms]] new Set() 包含与使用此服务器 连接的每个算法对应的 Promise。disconnect() 会清空此集合,使算法能够 判断其realm 在运行期间是否曾经断开连接。
[[automatedGATTConnectionResponse]] "not-expected" GATT 连接尝试的模拟 GATT 连接响应代码。
connect() 方法在被调用时,必须执行以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 如果 this.device.[[representedDevice]] 为 null,则返回一个以“NetworkError” DOMException 拒绝的 promise。

  3. 如果 UA 当前正在使用蓝牙系统,则可以返回一个以“NetworkError” DOMException 拒绝的 promise。

    实现可能能够避免此 NetworkError, 但目前站点需要将其对此 API 的使用串行化, 和/或为用户提供重试失败操作的方式。[Issue #188]

  4. 令 promise 为一个新的 promise。

  5. 如果 this.connected 为 true, 则使用 this 兑现 promise 并返回 promise。

  6. 将 promise 添加到 this.[[activeAlgorithms]]。

  7. 并行运行以下步骤:

    1. 如果 global 的可导航对象的 顶级可遍历对象的模拟蓝牙适配器 不为空,则运行以下步骤:

      1. 给定 global 的 可导航对象和 this.device,触发一个 GATT 连接尝试事件。

      2. 如果 this.[[automatedGATTConnectionResponse]] 为 "not-expected", 则将其设置为 "expected"。

      3. 如果 this.[[automatedGATTConnectionResponse]] 为 "expected", 则等待它发生变化。

      4. 令 response 为 this.[[automatedGATTConnectionResponse]]。

      5. 将 this.[[automatedGATTConnectionResponse]] 设置为 "not-expected"。

      6. 如果 response 不为 0,则执行 以下子步骤:

        1. 从 this.[[activeAlgorithms]] 中移除 promise。

        2. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止这些步骤。

    2. 否则,运行以下步骤:

      1. 如果 this.device.[[representedDevice]] 没有ATT 承载,则执行以下子步骤:

        1. 尝试使用 GAP 互操作性 要求中“连接建立”所述的过程创建ATT 承载。 如果 promise 从 this.[[activeAlgorithms]] 中移除,则中止此尝试。

          注:如果未收到可连接的广播, 这些过程可能会永远等待。 如果网站不再希望连接,则应调用 disconnect()。
        2. 如果此尝试因为 promise 从 this.[[activeAlgorithms]] 中移除而中止,则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“AbortError” DOMException 拒绝 promise 并中止这些 步骤。

        3. 如果此尝试因其他原因失败,则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止 这些步骤。

        4. 使用交换 MTU过程协商 支持的最大 MTU。忽略此步骤产生的任何错误。

        5. UA 可以尝试使用BR/EDR 绑定过程或LE 绑定过程与远程设备绑定。

          注:通常我们 更希望让网站控制 是否以及何时进行绑定,但 Core Bluetooth 平台 API 并未提供让 UA 实现这种控制项的方法。 已绑定比未绑定更安全,因此本 规范允许 UA 在可行的平台上伺机创建绑定。 这可能会导致在创建连接时出现用户可见的 配对对话框,而不是在访问受限特征时出现。

    3. 在给定 global 的蓝牙任务源上排入一个全局任务, 以执行以下子步骤:

      1. 如果 promise 不在 this.[[activeAlgorithms]] 中, 拒绝 promise,拒绝原因为一个 "AbortError" DOMException, 对 连接进行垃圾回收,连接属于 this.device.[[representedDevice]], 并中止这些步骤。

      2. 从 this.[[activeAlgorithms]] 中移除 promise。

      3. 如果 this.device.[[representedDevice]] 为 null,则拒绝 promise,拒绝原因为一个 "NetworkError" DOMException, 对 连接进行垃圾回收,该连接属于 this.device.[[representedDevice]], 并中止这些步骤。

      4. 将 this.connected 设置为 true。

      5. 使用 this 兑现 promise。

  8. 返回 promise。

disconnect() 方法在被调用时,必须执行以下步骤:
  1. 清空 this.[[activeAlgorithms]] 以中止任何活动的 connect() 调用。

  2. 如果 this.connected 为 false,则中止这些步骤。

  3. 清理已断开连接的设备 this.device。

  4. 令 device 为 this.device.[[representedDevice]]。

  5. 对连接进行垃圾回收,该连接属于 device。

要垃圾回收 device 的连接,UA 必须并行执行 以下步骤:
  1. 如果未使用此 API 的系统(无论位于 UA 内部还是外部) 正在使用 device 的ATT 承载,则中止此算法。

  2. 对于整个 UA 中的所有 BluetoothDevice deviceObj:

    1. 如果 deviceObj.[[representedDevice]] 与 device 不是 同一设备,则继续处理下一个 deviceObj。

    2. 如果 deviceObj.gatt.connected 为 true,则中止此算法。

    3. 如果 deviceObj.gatt.[[activeAlgorithms]] 包含调用 connect() 所产生的 Promise, 则中止此 算法。

  3. 销毁 device 的ATT 承载。

注:如果算法运行期间其 BluetoothRemoteGATTServer 被断开连接,则算法需要失败,即使 UA 整个 期间始终保持连接,并且 BluetoothRemoteGATTServer 随后在算法完成之前重新连接也是如此。我们包装返回的 Promise 来实现这一点。

要围绕一个 Promise promise 创建一个 gattServer-连接检查包装器, UA 必须:

  1. 如果 gattServer.connected 为 true,则将 promise 添加到 gattServer.[[activeAlgorithms]]。

  2. 对 promise 作出反应:

    • 如果 promise 以值 result 兑现,则:

      1. 如果 promise 位于 gattServer.[[activeAlgorithms]] 中,则将其移除并返回 result。

      2. 否则,抛出 NetworkError。

        注:抛出此错误是因为 gattServer 在 主算法执行期间断开了连接。
    • 如果 promise 以原因 error 被拒绝,则:

      1. 如果 promise 位于 gattServer.[[activeAlgorithms]] 中,则将其移除并抛出 error。

      2. 否则,抛出 NetworkError。

        注:抛出此错误是因为 gattServer 在 主算法执行期间断开了连接。
getPrimaryService(service) 方法在被调用时,必须执行以下步骤:
  1. 如果 this.device.[[allowedServices]] 不为 "all" 且 service 不在 this.device.[[allowedServices]] 中,则返回 一个以 SecurityError 拒绝的 promise 并中止这些步骤。

  2. 返回 GetGATTChildren(attribute=this.device,
    single=true,
    uuidCanonicalizer=BluetoothUUID.getService,
    uuid=service,
    allowedUuids=this.device.[[allowedServices]],
    child type="GATT Primary Service")

getPrimaryServices(service) 方法在被调用时,必须执行以下步骤:
  1. 如果 this.device.[[allowedServices]] 不为 "all",并且存在 service 且其不在 this.device.[[allowedServices]] 中,则返回 一个以 SecurityError 拒绝的 promise 并中止这些步骤。

  2. 返回 GetGATTChildren(attribute=this.device,
    single=false,
    uuidCanonicalizer=BluetoothUUID.getService,
    uuid=service,
    allowedUuids=this.device.[[allowedServices]],
    child type="GATT Primary Service")

6.3. BluetoothRemoteGATTService

BluetoothRemoteGATTService 表示一个 GATT 服务,即一组 特征以及与其他服务的关系,它们封装了 设备某一部分的行为。

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTService : EventTarget {
  [SameObject]
  readonly attribute BluetoothDevice device;
  readonly attribute UUID uuid;
  readonly attribute boolean isPrimary;
  Promise<BluetoothRemoteGATTCharacteristic>
    getCharacteristic(BluetoothCharacteristicUUID characteristic);
  Promise<sequence<BluetoothRemoteGATTCharacteristic>>
    getCharacteristics(optional BluetoothCharacteristicUUID characteristic);
  Promise<BluetoothRemoteGATTService>
    getIncludedService(BluetoothServiceUUID service);
  Promise<sequence<BluetoothRemoteGATTService>>
    getIncludedServices(optional BluetoothServiceUUID service);
};
BluetoothRemoteGATTService includes CharacteristicEventHandlers;
BluetoothRemoteGATTService includes ServiceEventHandlers;
device 是表示该 GATT 服务所属 远程外围设备的 BluetoothDevice。

uuid 是该 服务的 UUID,例如,心率服务的 UUID 为 '0000180d-0000-1000-8000-00805f9b34fb'。

isPrimary 指示 此服务的类型是主服务还是 次级服务。

BluetoothRemoteGATTService 实例使用下表所述的内部 槽创建:

内部槽 初始值 描述(非规范性)
[[representedService]] <始终在正文中设置> 此对象所表示的服务, 如果该服务已被移除或以其他方式失效,则为 null。
要创建一个表示 服务 service 的 BluetoothRemoteGATTService,UA 必须运行以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 令 promise 为一个新的 promise。

  3. 并行运行以下步骤:

    1. 令 result 为一个新的 BluetoothRemoteGATTService 实例,并将其 [[representedService]] 槽初始化为 service。

    2. 获取表示 service 所在设备的 BluetoothDevice,并令 device 为结果。

    3. 如果上一步抛出错误,则在给定 global 的蓝牙任务源上排入一个全局任务,以使用 该错误拒绝 promise 并中止这些步骤。

    4. 使用 device 初始化 result.device。

    5. 使用 service 的 UUID 初始化 result.uuid。

    6. 如果 service 是主服务,则将 result.isPrimary 初始化为 true。否则, 将 result.isPrimary 初始化为 false。

    7. 在给定 global 的蓝牙任务源上排入一个全局任务, 以使用 result 兑现 promise。

  4. 返回 promise。

getCharacteristic(characteristic) 方法检索此 服务内的一个特征。调用时,它必须返回

GetGATTChildren(attribute=this,
single=true,
uuidCanonicalizer=BluetoothUUID.getCharacteristic,
uuid=characteristic,
allowedUuids=undefined,
child type="GATT Characteristic")

getCharacteristics(characteristic) 方法检索此服务内的 特征列表。调用时,它必须返回

GetGATTChildren(attribute=this,
single=false,
uuidCanonicalizer=BluetoothUUID.getCharacteristic,
uuid=characteristic,
allowedUuids=undefined,
child type="GATT Characteristic")

getIncludedService(service) 方法检索此服务内的一个 包含服务。调用时,它必须返回

GetGATTChildren(attribute=this,
single=true,
uuidCanonicalizer=BluetoothUUID.getService,
uuid=service,
allowedUuids=undefined,
child type="GATT Included Service")

getIncludedServices(service) 方法检索此服务内的 包含服务列表。调用时,它必须返回

GetGATTChildren(attribute=this,
single=false,
uuidCanonicalizer=BluetoothUUID.getService,
uuid=service,
allowedUuids=undefined,
child type="GATT Included Service")

6.4. BluetoothRemoteGATTCharacteristic

BluetoothRemoteGATTCharacteristic 表示一个 GATT 特征, 它是一个基本数据元素,可提供有关 外围设备服务的更多信息。

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTCharacteristic : EventTarget {
  [SameObject]
  readonly attribute BluetoothRemoteGATTService service;
  readonly attribute UUID uuid;
  readonly attribute BluetoothCharacteristicProperties properties;
  readonly attribute DataView? value;
  Promise<BluetoothRemoteGATTDescriptor> getDescriptor(BluetoothDescriptorUUID descriptor);
  Promise<sequence<BluetoothRemoteGATTDescriptor>>
    getDescriptors(optional BluetoothDescriptorUUID descriptor);
  Promise<DataView> readValue();
  Promise<undefined> writeValue(BufferSource value);
  Promise<undefined> writeValueWithResponse(BufferSource value);
  Promise<undefined> writeValueWithoutResponse(BufferSource value);
  Promise<BluetoothRemoteGATTCharacteristic> startNotifications();
  Promise<BluetoothRemoteGATTCharacteristic> stopNotifications();
};
BluetoothRemoteGATTCharacteristic includes CharacteristicEventHandlers;
service 是此特征所属的 GATT 服务。

uuid 是该 特征的 UUID,例如 心率测量特征的 UUID 为 '00002a37-0000-1000-8000-00805f9b34fb'。

properties 保存此 特征的属性。

value 是当前缓存的 特征值。当读取该特征的值,或者通过 通知或指示更新该值时,此值会被更新。

BluetoothRemoteGATTCharacteristic 实例使用下表所述的 内部槽创建:

内部槽 初始值 描述(非规范性)
[[representedCharacteristic]] <始终在正文中设置> 此对象所表示的特征,如果该 特征已被移除或以其他方式失效,则为 null。
[[automatedCharacteristicReadResponse]] "not-expected" GATT 特征读取尝试的模拟 GATT 特征响应代码。
[[automatedCharacteristicReadResponseData]] 空字节序列 GATT 特征读取尝试的模拟 GATT 特征响应数据。
[[automatedCharacteristicWriteResponse]] "not-expected" GATT 特征写入尝试的模拟 GATT 特征响应代码。
[[automatedCharacteristicSubscribeToNotificationsResponse]] "not-expected" 尝试订阅 GATT 特征通知时的模拟 GATT 特征响应代码。
[[automatedCharacteristicUnsubscribeFromNotificationsResponse]] "not-expected" 尝试取消订阅 GATT 特征通知时的模拟 GATT 特征响应代码。
要创建一个 BluetoothRemoteGATTCharacteristic 表示特征 characteristic,UA 必须 运行以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 令 promise 为一个新的 promise。

  3. 并行运行以下步骤:

    1. 令 result 为一个新的 BluetoothRemoteGATTCharacteristic 实例,并将其 [[representedCharacteristic]] 槽初始化为 characteristic。

    2. 使用 characteristic 所在服务的 BluetoothRemoteGATTService 实例初始化 result.service<。

    3. 使用 characteristic 的 UUID 初始化 result.uuid。

    4. 创建 一个 BluetoothCharacteristicProperties 实例,该实例来自 特征 characteristic,并令 properties 为 结果。

    5. 如果上一步返回错误,则在给定 global 的蓝牙任务源上排入一个全局任务,以使用 该错误拒绝 promise 并中止这些步骤。

    6. 将 result.properties 初始化为 properties。

    7. 将 result.value 初始化为 null。如果 characteristic 最近读取的值可用,UA 可以将 result.value 初始化为一个新的 DataView,该对象包装一个包含 characteristic 最近读取值的新的 ArrayBuffer。

    8. 在给定 global 的蓝牙任务源上排入一个全局任务, 以使用 result 兑现 promise。

  4. 返回 promise。

getDescriptor(descriptor) 方法检索此 特征内的一个描述符。调用时,它必须返回

GetGATTChildren(attribute=this,
single=true,
uuidCanonicalizer=BluetoothUUID.getDescriptor,
uuid=descriptor,
allowedUuids=undefined,
child type="GATT Descriptor")

getDescriptors(descriptor) 方法检索此 特征内的描述符列表。调用时,它必须返回

GetGATTChildren(attribute=this,
single=false,
uuidCanonicalizer=BluetoothUUID.getDescriptor,
uuid=descriptor,
allowedUuids=undefined,
child type="GATT Descriptor")

readValue() 方法在被调用时,必须运行以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 令 gatt 为 this.service.device.gatt。

  3. 如果 this.uuid 被阻止读取,则返回一个以“SecurityError” DOMException 拒绝的 promise 并中止这些步骤。

  4. 如果 gatt.connected 为 false,则返回一个以“NetworkError” DOMException 拒绝的 promise 并中止这些步骤。

  5. 令 characteristic 为 this.[[representedCharacteristic]]。

  6. 如果 characteristic 为 null,则返回一个以 “InvalidStateError” DOMException 拒绝的 promise 并中止这些步骤。

  7. 返回一个包裹新的 promise promise 的 gatt-连接检查包装器,并并行运行以下步骤:

    1. 如果 characteristic 的 属性中未设置 Read 位,则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NotSupportedError” DOMException 拒绝 promise 并中止 这些步骤。

    2. 如果 global 的可导航对象的 顶级可遍历对象的模拟蓝牙适配器 不为空,则运行以下步骤:

      1. 如果 this.[[automatedCharacteristicReadResponse]] 不为 "not-expected",则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“InvalidStateError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 给定 global 的 可导航对象、this.device、 characteristic 和 read,触发一个模拟 特征事件。

      3. 将 this.[[automatedCharacteristicReadResponse]] 设置为 "expected", 并等待它发生变化。

      4. 令 response 为 this.[[automatedCharacteristicReadResponse]]。

      5. 将 this.[[automatedCharacteristicReadResponse]] 设置为 "not-expected"。

      6. 如果 response 不为 0,则执行以下子步骤:

        1. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止这些步骤。

      7. 否则,令 buffer 为一个新的 ArrayBuffer, 其中包含 this.[[automatedCharacteristicReadResponseData]]。

    3. 否则,运行以下步骤:

      1. 如果 UA 当前正在使用蓝牙系统,则可以在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并 中止这些步骤。

        实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化,和/或为 用户提供重试失败操作的方式。[Issue #188]

      2. 使用特征值读取 过程中的任意子过程组合来检索 characteristic 的值,并令 buffer 为一个保存所检索值的新的 ArrayBuffer。 按照§ 6.7 错误 处理中的说明处理错误。

    4. 在给定 global 的蓝牙任务源上排入一个全局任务, 以执行以下步骤:

      1. 如果 promise 不在 gatt.[[activeAlgorithms]] 中,则以“NetworkError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 如果上述子过程返回错误,则使用该错误拒绝 promise 并中止这些步骤。

      3. 将使用 buffer 创建的一个新的 DataView 赋给 this.value。

      4. 在 this 上触发一个名为“characteristicvaluechanged”的事件, 并将其 bubbles 属性初始化为 true。

      5. 使用 this.value 兑现 promise。

要执行 WriteCharacteristicValue( this: BluetoothRemoteGATTCharacteristic,
value: BufferSource,
response: string),

UA 必须执行以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 令 gatt 为 this.service.device.gatt。

  3. 如果 this.uuid 被阻止写入,则返回一个以“SecurityError” DOMException 拒绝的 promise 并中止这些步骤。

  4. 令 bytes 为 value 所持字节的副本。

  5. 如果 bytes 的长度超过 512 字节(根据长属性值,这是属性 值的最大长度),则返回一个以 “InvalidModificationError” DOMException 拒绝的 promise 并中止这些步骤。

  6. 如果 gatt.connected 为 false,则返回一个以“NetworkError” DOMException 拒绝的 promise 并中止 这些步骤。

  7. 令 characteristic 为 this.[[representedCharacteristic]]。

  8. 如果 characteristic 为 null,则返回一个以 “InvalidStateError” DOMException 拒绝的 promise 并中止这些步骤。

  9. 返回一个包裹新的 promise promise 的 gatt-连接检查包装器,并并行运行以下步骤。

    1. 断言:response 是 "required"、"never" 或 "optional" 之一。

    2. 如果 global 的可导航对象的 顶级可遍历对象的模拟蓝牙适配器 不为空,则运行以下步骤:

      1. 如果 this.[[automatedCharacteristicWriteResponse]] 不为 "not-expected",则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“InvalidStateError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 给定 global 的 可导航对象、this.device、 characteristic、write 和 bytes,触发一个模拟 特征事件。

      3. 将 this.[[automatedCharacteristicWriteResponse]] 设置为 "expected", 并等待它发生变化。

      4. 令 response 为 this.[[automatedCharacteristicWriteResponse]]。

      5. 将 this.[[automatedCharacteristicWriteResponse]] 设置为 "not-expected"。

      6. 如果 response 不为 0,则执行以下子步骤:

        1. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止这些步骤。

    3. 否则,运行以下步骤:

      1. 如果 UA 当前正在使用蓝牙系统,则可以在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并 中止这些步骤。

        实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化, 和/或为用户提供重试失败操作的方式。[Issue #188]

      2. 通过执行 以下步骤将 bytes 写入 characteristic:

        如果 response 为 "required"
        使用写入特征值 过程。
        如果 response 为 "never"
        使用无响应写入 过程。
        其他情况
        使用特征值写入 过程中的任意子过程组合。
        按照§ 6.7 错误处理中的说明处理错误。
    4. 使用蓝牙任务源在 global 上排入一个全局任务,以执行 以下步骤:

      1. 如果 promise 不在 gatt.[[activeAlgorithms]] 中,则以“NetworkError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 如果上述过程返回错误,则使用该错误拒绝 promise 并中止这些步骤。

      3. 将 this.value 设置为一个 新的 DataView, 它包装一个包含 bytes 的新的 ArrayBuffer。

      4. 使用 undefined 兑现 promise。

已弃用。请改用 writeValueWithResponse() 和 writeValueWithoutResponse()。

writeValue(value) 方法在被调用时,必须返回

WriteCharacteristicValue( this=this,
value=value,
response="optional")

此方法仅用于 向后兼容。新的实现 不应实现此方法。[Issue #238]

writeValueWithResponse(value) 方法在被调用时,必须 返回

WriteCharacteristicValue( this=this,
value=value,
response="required")

writeValueWithoutResponse(value) 方法在被调用时, 必须返回

WriteCharacteristicValue( this=this,
value=value,
response="never")

UA 必须维护一个映射,将每个已知的 GATT 特征映射到一组 Bluetooth 对象,该集合称为该特征的活动 通知 上下文集合。

注:给定特征的集合保存已 注册通知的每个 Realm 的 navigator.bluetooth 对象。当设备断开连接时,所有通知都会变为非活动状态。希望在 重新连接后继续接收通知的站点需要再次调用 startNotifications(), 而且在 startNotifications() 生效之前的间隙中,不可避免地存在遗漏某些通知的风险。
startNotifications() 方法在被调用时,必须运行以下 步骤。有关接收 通知的详细信息,请参见§ 6.6.4 响应通知和指示。
  1. 令 global 为 this 的相关全局对象。

  2. 令 gatt 为 this.service.device.gatt。

  3. 如果 this.uuid 被阻止读取,则返回一个以“SecurityError” DOMException 拒绝的 promise。

  4. 如果 gatt.connected 为 false,则返回一个以“NetworkError” DOMException 拒绝的 promise。

  5. 令 characteristic 为 this.[[representedCharacteristic]]。

  6. 如果 characteristic 为 null,则返回一个以 “InvalidStateError” DOMException 拒绝的 promise。

  7. 返回一个包裹新的 promise promise 的 gatt-连接检查包装器,并并行运行以下步骤。

    1. 如果 characteristic 的属性中既未设置 Notify 位,也未设置 Indicate 位, 则在给定 global 的蓝牙任务源上排入一个全局任务, 以使用 NotSupportedError 拒绝 promise 并中止这些 步骤。

    2. 如果 characteristic 的活动通知上下文集合 包含 navigator.bluetooth, 则在给定 global 的蓝牙任务源上排入一个全局任务, 以使用 this 兑现 promise 并中止 这些步骤。

    3. 如果 global 的可导航对象的 顶级可遍历对象的模拟蓝牙适配器 不为空,则运行以下步骤:

      1. 如果 this.[[automatedCharacteristicSubscribeToNotificationsResponse]] 不为 "not-expected",则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“InvalidStateError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 给定 global 的 可导航对象、this.device、 characteristic 和 subscribe-to-notifications,触发一个模拟 特征事件。

      3. 将 this.[[automatedCharacteristicSubscribeToNotificationsResponse]] 设置为 "expected", 并等待它发生变化。

      4. 令 response 为 this.[[automatedCharacteristicSubscribeToNotificationsResponse]]。

      5. 将 this.[[automatedCharacteristicSubscribeToNotificationsResponse]] 设置为 "not-expected"。

      6. 如果 response 不为 0,则执行以下子步骤:

        1. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止这些步骤。

      7. 否则,令 success 为 true。

    4. 否则,运行以下步骤:

      1. 如果 UA 当前正在使用蓝牙系统,则可以在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并 中止这些步骤。

        实现可能能够避免此 NetworkError, 但 目前站点需要将对此 API 的使用串行化,和/或为用户提供 重试失败操作的方式。[Issue #188]

      2. 如果该特征具有客户端特征 配置 描述符,则使用任意特征描述符 过程,以 确保 characteristic 的客户端特征 配置 描述符中的 Notification 或 Indication 位之一已设置,并符合 characteristic 的 属性约束。UA 应避免 同时设置两个位,并且如果两个位均已 设置,则必须对值变化事件去重。按照§ 6.7 错误 处理中的说明处理错误。

        注:某些设备具有这样的特征,其属性包含 Notify 或 Indicate 位,但没有客户端特征 配置描述符。这些不符合标准的特征 往往会无条件发送通知或指示,因此本 规范允许应用程序直接订阅其消息。
      3. 如果这些过程成功,则令 success 为 true。

    5. 如果 success 为 true,则将 navigator.bluetooth 添加到 characteristic 的活动通知上下文集合。

    6. 在给定 global 的蓝牙任务源上排入一个全局任务, 以执行以下步骤:

      1. 如果 promise 不在 gatt.[[activeAlgorithms]] 中,则以“NetworkError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 如果上述过程返回错误,则使用 该错误拒绝 promise 并中止这些步骤。

      3. 使用 this 兑现 promise。

注:启用通知后,所得的值变化 事件要到当前微任务 检查点之后才会分发。这允许开发者在结果 promise 的 .then 处理器中设置处理器。
stopNotifications() 方法在被调用时,必须返回一个新的 promise promise,并并行运行以下步骤:
  1. 令 characteristic 为 this.[[representedCharacteristic]]。

  2. 如果 characteristic 为 null,则返回一个以 InvalidStateError 拒绝的 promise 并中止这些步骤。

  3. 如果 characteristic 的活动通知上下文集合 包含 navigator.bluetooth, 则将其移除。

    1. 如果 global 的可导航对象的 顶级可遍历对象的模拟蓝牙适配器 不为空,则运行以下步骤:

      1. 如果 this.[[automatedCharacteristicUnsubscribeFromNotificationsResponse]] 不为 "not-expected",则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“InvalidStateError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 给定 global 的 可导航对象、this.device、 characteristic 和 unsubscribe-from-notifications,触发一个模拟 特征事件。

      3. 将 this.[[automatedCharacteristicUnsubscribeFromNotificationsResponse]] 设置为 "expected", 并等待它发生变化。

      4. 令 response 为 this.[[automatedCharacteristicUnsubscribeFromNotificationsResponse]]。

      5. 将 this.[[automatedCharacteristicUnsubscribeFromNotificationsResponse]] 设置为 "not-expected"。

      6. 如果 response 不为 0,则执行以下子步骤:

        1. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止这些步骤。。

    2. 否则,运行以下步骤:

      1. 如果 characteristic 的活动通知上下文 集合变为 空,并且该特征具有客户端特征 配置描述符,则 UA 应使用任意特征 描述符过程来清除 characteristic 的客户端 特征配置描述符中的 Notification 和 Indication 位。

  4. 在给定 this 的 相关全局对象的蓝牙任务源上排入一个全局任务,以使用 this 兑现 promise。

注:排入一个任务来兑现 promise,可确保不会有 由通知引起的值变化事件 在 promise 兑现之后到达。

6.4.1. BluetoothCharacteristicProperties

每个 BluetoothRemoteGATTCharacteristic 都通过 BluetoothCharacteristicProperties 对象公开其特征 属性。这些 属性表示哪些操作对该特征有效。

[Exposed=Window, SecureContext]
interface BluetoothCharacteristicProperties {
  readonly attribute boolean broadcast;
  readonly attribute boolean read;
  readonly attribute boolean writeWithoutResponse;
  readonly attribute boolean write;
  readonly attribute boolean notify;
  readonly attribute boolean indicate;
  readonly attribute boolean authenticatedSignedWrites;
  readonly attribute boolean reliableWrite;
  readonly attribute boolean writableAuxiliaries;
};
要从 特征创建一个 BluetoothCharacteristicProperties 实例 characteristic,UA 必须运行 以下步骤:
  1. 令 propertiesObj 为一个新的 BluetoothCharacteristicProperties 实例。

  2. 令 properties 为 characteristic 的特征属性。

  3. 根据 properties 中对应的 位初始化 propertiesObj 的属性:

    属性 位
    broadcast 广播
    read 读取
    writeWithoutResponse 无响应写入
    write 写入
    notify 通知
    indicate 指示
    authenticatedSignedWrites 已认证签名写入
  4. 如果特征属性的 Extended Properties 位未 设置,则将 propertiesObj.reliableWrite 和 propertiesObj.writableAuxiliaries 初始化为 false。否则,运行以下步骤:

    1. 发现 characteristic 的特征扩展 属性描述符,并读取其 值到 extendedProperties 中。 按照§ 6.7 错误处理中的说明处理错误。

      特征扩展 属性并未明确说明 对于给定特征,扩展属性是否不可变。 如果它们不可变,则应允许 UA 缓存它们。

    2. 如果上一步返回错误,则返回该错误。

    3. 根据 extendedProperties 的 Reliable Write 位初始化 propertiesObj.reliableWrite。

    4. 根据 extendedProperties 的 Writable Auxiliaries 位初始化 propertiesObj.writableAuxiliaries。

  5. 返回 propertiesObj。

6.5. BluetoothRemoteGATTDescriptor

BluetoothRemoteGATTDescriptor 表示一个 GATT 描述符,它 提供有关特征值的更多信息。

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTDescriptor {
  [SameObject]
  readonly attribute BluetoothRemoteGATTCharacteristic characteristic;
  readonly attribute UUID uuid;
  readonly attribute DataView? value;
  Promise<DataView> readValue();
  Promise<undefined> writeValue(BufferSource value);
};
characteristic 是 此描述符所属的 GATT 特征。

uuid 是 特征描述符的 UUID,例如 客户端特征配置描述符的 UUID 为 '00002902-0000-1000-8000-00805f9b34fb'。

value 是当前 缓存的描述符值。当读取描述符的值时, 此值会被更新。

BluetoothRemoteGATTDescriptor 实例使用下表所述的内部 槽创建:

内部槽 初始值 描述(非规范性)
[[representedDescriptor]] <始终在正文中设置> 此对象所表示的描述符, 如果该描述符 已被移除或以其他方式失效,则为 null。
[[automatedDescriptorReadResponse]] "not-expected" GATT 描述符读取尝试的模拟 GATT 描述符 响应代码。
[[automatedDescriptorReadResponseData]] 空字节序列 GATT 描述符读取尝试的模拟 GATT 描述符 响应数据。
[[automatedDescriptorWriteResponse]] "not-expected" GATT 描述符写入尝试的模拟 GATT 描述符 响应代码。
要创建一个表示 BluetoothRemoteGATTDescriptor 描述符 descriptor, UA 必须运行以下步骤。
  1. 令 promise 为一个新的 promise。

  2. 并行运行以下步骤:

    1. 令 result 为一个新的 BluetoothRemoteGATTDescriptor 实例,并将其 [[representedDescriptor]] 槽初始化为 descriptor。

    2. 使用表示 descriptor 所在特征的 BluetoothRemoteGATTCharacteristic 实例初始化 result.characteristic。

    3. 使用 descriptor 的 UUID 初始化 result.uuid。

    4. 将 result.value 初始化为 null。 如果 descriptor 最近读取的值 可用,UA 可以将 result.value 初始化为一个新的 DataView, 它包装一个新的 ArrayBuffer, 其中包含 descriptor 最近读取的值。

    5. 在给定 this 的相关全局对象的蓝牙任务源上排入一个全局任务,以使用 result 兑现 promise。

  3. 返回 promise。

readValue() 方法在被调用时,必须运行以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 令 gatt 为 this.characteristic.service.device.gatt。

  3. 如果 this.uuid 被阻止读取,则返回一个以“SecurityError” DOMException 拒绝的 promise。

  4. 如果 gatt.connected 为 false,则返回一个以“NetworkError” DOMException 拒绝的 promise。

  5. 令 descriptor 为 this.[[representedDescriptor]]。

  6. 如果 descriptor 为 null,则返回一个以 “InvalidStateError” DOMException 拒绝的 promise。

  7. 返回一个包裹新的 promise promise 的 gatt-连接检查包装器,并并行运行以下步骤:

    1. 如果 global 的可导航对象的 顶级可遍历对象的模拟蓝牙适配器 不为空,则运行以下步骤:

      1. 如果 this.[[automatedDescriptorReadResponse]] 不为 "not-expected",则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“InvalidStateError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 给定 global 的 可导航对象、this.device、 descriptor 和 read,触发一个模拟 描述符事件。

      3. 将 this.[[automatedDescriptorReadResponse]] 设置为 "expected", 并等待它发生变化。

      4. 令 response 为 this.[[automatedDescriptorReadResponse]]。

      5. 将 this.[[automatedDescriptorReadResponse]] 设置为 "not-expected"。

      6. 如果 response 不为 0,则执行以下子步骤:

        1. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止这些步骤。

      7. 否则,令 buffer 为一个新的 ArrayBuffer, 其中包含 this.[[automatedDescriptorReadResponseData]]。

    2. 否则,运行以下步骤:

      1. 如果 UA 当前正在使用蓝牙系统,则可以在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并 中止这些步骤。

        实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化, 和/或为用户提供重试失败操作的方式。[Issue #188]

      2. 使用读取特征 描述符或读取长特征 描述符子过程来检索 descriptor 的值,并令 buffer 为一个新的 ArrayBuffer, 其中保存 检索到的值。按照 § 6.7 错误处理中的说明处理错误。

    3. 在给定 global 的蓝牙任务源上排入一个全局任务, 以执行以下步骤:

      1. 如果 promise 不在 gatt.[[activeAlgorithms]] 中,则以“NetworkError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 如果上述子过程返回错误,则使用该错误拒绝 promise 并中止这些步骤。

      3. 将使用 buffer 创建的一个新的 DataView 赋给 this.value。

      4. 使用 this.value 兑现 promise。

writeValue(value) 方法在被调用时,必须运行 以下步骤:
  1. 令 global 为 this 的相关全局对象。

  2. 令 gatt 为 this.characteristic.service.device.gatt。

  3. 如果 this.uuid 被阻止写入,则返回一个以“SecurityError” DOMException 拒绝的 promise。

  4. 令 bytes 为 value 所持字节的副本。

  5. 如果 bytes 的长度超过 512 字节(根据长属性值,这是属性 值的最大长度),则返回一个以 “InvalidModificationError” DOMException 拒绝的 promise。

  6. 如果 gatt.connected 为 false,则返回一个以“NetworkError” DOMException 拒绝的 promise。

  7. 令 descriptor 为 this.[[representedDescriptor]]。

  8. 如果 descriptor 为 null,则返回一个以 “InvalidStateError” DOMException 拒绝的 promise。

  9. 返回一个包裹新的 promise promise 的 gatt-连接检查包装器,并并行运行以下步骤。

    1. 如果 global 的可导航对象的 顶级可遍历对象的模拟蓝牙适配器 不为空,则运行以下步骤:

      1. 如果 this.[[automatedDescriptorWriteResponse]] 不为 "not-expected",则在给定 global 的蓝牙任务源上排入一个全局任务,以使用“InvalidStateError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 给定 global 的 可导航对象、this.device、 descriptor、write 和 bytes,触发一个模拟 描述符事件。

      3. 将 this.[[automatedDescriptorWriteResponse]] 设置为 "expected", 并等待它发生变化。

      4. 令 response 为 this.[[automatedDescriptorWriteResponse]]。

      5. 将 this.[[automatedDescriptorWriteResponse]] 设置为 "not-expected"。

      6. 如果 response 不为 0,则执行以下子步骤:

        1. 在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并中止这些步骤。

    2. 否则,运行以下步骤:

      1. 如果 UA 当前正在使用蓝牙系统,则可以在给定 global 的蓝牙任务源上排入一个全局任务,以使用“NetworkError” DOMException 拒绝 promise 并 中止这些步骤。

        实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化, 和/或为用户提供重试失败操作的方式。[Issue #188]

      2. 使用写入特征 描述符或写入长特征 描述符子过程,将 bytes 写入 descriptor。按照 § 6.7 错误处理中的说明处理错误。

    3. 在给定 global 的蓝牙任务源上排入一个全局任务, 以执行以下步骤:

      1. 如果 promise 不在 gatt.[[activeAlgorithms]] 中,则以“NetworkError” DOMException 拒绝 promise 并中止 这些步骤。

      2. 如果上述子过程返回错误,则使用该错误拒绝 promise 并中止这些步骤。

      3. 将 this.value 设置为一个新的 DataView, 它包装一个新的 ArrayBuffer, 其中包含 bytes。

      4. 使用 undefined 兑现 promise。

6.6. 事件

6.6.1. 蓝牙树

蓝牙树是指 navigator.bluetooth 和实现 BluetoothDevice、 BluetoothRemoteGATTService、 BluetoothRemoteGATTCharacteristic 或 BluetoothRemoteGATTDescriptor 接口的对象参与树时所构成的结构。

6.6.2. 事件类型

advertisementreceived
当从某个设备接收到 广播事件时,在该 BluetoothDevice 上触发。
availabilitychanged
当 整个蓝牙系统对 UA 变为可用或不可用时,在 navigator.bluetooth 上触发。
characteristicvaluechanged
当一个 BluetoothRemoteGATTCharacteristic 的值发生变化时触发, 其变化可能源于 读取请求,或者 值变化通知/指示。
gattserverdisconnected
当 活动 GATT 连接丢失时,在一个 BluetoothDevice 上触发。
serviceadded
当在远程 设备上发现一个新的服务时,就在其被添加到蓝牙树之后,在新的 BluetoothRemoteGATTService 上触发。
servicechanged
当一个 BluetoothRemoteGATTService 的状态发生变化时触发。 这包括向服务添加或 从服务中移除任何特征和/或描述符,以及来自 远程设备的服务已更改指示。
serviceremoved
当一个 BluetoothRemoteGATTService 从其设备中移除时,就在其从蓝牙 树中移除之前触发。

6.6.3. 响应断开连接

当一个蓝牙设备 device 的ATT 承载 丢失 (例如因为远程设备移出范围,或用户使用平台 功能将其断开连接)时,对于每个 BluetoothDevice deviceObj,UA 必须在给定 deviceObj 的相关全局对象的蓝牙任务源上排入一个全局任务,以执行以下步骤:
  1. 如果 deviceObj.[[representedDevice]] 与 device 不是同一设备,则中止这些步骤。

  2. 如果 !deviceObj.gatt.connected, 则中止这些步骤。

  3. 清理已断开连接的设备 deviceObj。

要清理 已断开连接的设备 deviceObj,UA 必须:
  1. 将 deviceObj.gatt.connected 设置为 false。

  2. 清空 deviceObj.gatt.[[activeAlgorithms]]。

  3. 将 deviceObj.gatt.[[automatedGATTConnectionResponse]] 设置为 "not-expected"。

  4. 令 context 为 deviceObj.[[context]]。

  5. 从 context.[[attributeInstanceMap]] 中移除键位于 deviceObj.[[representedDevice]] 内的所有条目。

  6. 对于 deviceObj 的 realm 中的每个 BluetoothRemoteGATTService service,将 service.[[representedService]] 设置为 null。

  7. 对于 deviceObj 的realm 中的每个 BluetoothRemoteGATTCharacteristic characteristic,执行以下子步骤:

    1. 令 notificationContexts 为 characteristic.[[representedCharacteristic]] 的 活动通知上下文集合。

    2. 从 notificationContexts 中移除 context。

    3. 如果 notificationContexts 变为空,并且仍存在一个指向 deviceObj.[[representedDevice]] 的ATT 承载,并且 characteristic 具有客户端特征 配置描述符,则 UA 应使用任意 特征描述符过程 清除 characteristic 的客户端特征 配置 描述符中的 Notification 和 Indication 位。

    4. 将 characteristic.[[representedCharacteristic]] 设置为 null。

  8. 对于 deviceObj 的 realm 中的每个 BluetoothRemoteGATTDescriptor descriptor,将 descriptor.[[representedDescriptor]] 设置为 null。

  9. 在 deviceObj 上触发一个名为 gattserverdisconnected 的事件,并将其 bubbles 属性初始化为 true。

    注:此事件不会在 BluetoothRemoteGATTServer 上触发。

6.6.4. 响应通知和指示

当 UA 接收到蓝牙特征值通知 或指示时, 必须执行以下步骤:
  1. 对于特征的活动 通知上下文集合中的每个 bluetoothGlobal,在给定 bluetoothGlobal 的相关全局对象的蓝牙任务源上排入一个全局任务,以执行以下 子步骤:

    1. 令 characteristicObject 为 以 bluetoothGlobal 为根的蓝牙树中表示该 特征的 BluetoothRemoteGATTCharacteristic。

    2. 如果 characteristicObject .service.device.gatt.connected 为 false,则中止这些子步骤。

    3. 将 characteristicObject.value 设置为一个新的 DataView, 它包装一个新的 ArrayBuffer, 其中保存 特征的新值。

    4. 在 characteristicObject 上触发一个名为 characteristicvaluechanged 的事件,并将其 bubbles 属性初始化为 true。

6.6.5. 响应服务变更

蓝牙属性缓存系统允许客户端跟踪 服务、特征和描述符的变化。在 为向网页公开这些属性而发现其中任何属性之前,UA 必须订阅 服务已更改特征的指示(如果该特征存在)。 当 UA 收到服务已更改特征上的指示时,必须执行以下步骤。
  1. 令 removedAttributes 为 UA 在收到指示 之前已发现的、位于服务已更改特征所指示范围内的属性列表。

  2. 使用主服务发现、关系发现、 特征发现和特征描述符 发现过程,重新发现服务已更改特征所指示范围内的属性。 如果 UA 能证明发现结果 不会影响下方触发的事件,则可以跳过发现所指示范围的全部或一部分。

  3. 令 addedAttributes 为上一步中发现的属性列表。

  4. 如果一个具有相同定义(参见服务互操作性 要求)的属性(忽略特征值和描述符值)同时出现在 removedAttributes 和 addedAttributes 中,则将其 从两者中移除。

    给定以下设备状态:
    状态 1
    • 服务 A
      • 特征 C:值 [1, 2, 3]
    • 服务 B
    状态 2
    • 服务 A
      • 特征 C:值 [3, 2, 1]
    • 服务 B
    状态 3
    • 服务 A
      • 特征 D:值 [3, 2, 1]
    • 服务 B
    状态 4
    • 服务 A
      • 特征 C:值 [1, 2, 3]
    • 服务 B
      • 包含服务 A

    从状态 1 转换到状态 2 后,服务 A 仍具有“相同定义, 忽略特征值和描述符值”,这意味着它会同时从 removedAttributes 和 addedAttributes 中移除,并且不会 出现在任何 servicechanged 事件中。

    从状态 1 转换到状态 3 后,服务 A 具有不同的 定义,因为服务定义包含其 特征定义,因此它会同时保留在 removedAttributes 和 addedAttributes 中。然后在步骤 8中,该服务被移至 changedServices,这会使其触发 servicechanged 事件,而不是同时触发 serviceadded 和 serviceremoved。 步骤 9还会将服务 A 添加到 changedServices,因为特征 C 被移除,而特征 D 被添加。

    从状态 1 转换到状态 4 与 1->3 的转换类似。服务 B 在步骤 8中被移至 changedServices,但没有任何特征 或描述符发生变化,因此不会在 步骤 9中被重复添加。

  5. 令 invalidatedAttributes 为位于 removedAttributes 中但 不在 addedAttributes 中的属性。

  6. 对于 UA 中的每个环境设置对象 settings,在其负责的事件循环上排入一个 任务,以执行以下 子步骤:

    1. 对于每个 BluetoothRemoteGATTService service,如果其相关 设置对象为 settings,并且 service.[[representedService]] 位于 invalidatedAttributes 中,则将 service.[[representedService]] 设置为 null。

    2. 对于每个 BluetoothRemoteGATTCharacteristic characteristic,如果其 相关设置对象为 settings,并且 characteristic.[[representedCharacteristic]] 位于 invalidatedAttributes 中,则将 characteristic.[[representedCharacteristic]] 设置为 null。

    3. 对于每个 BluetoothRemoteGATTDescriptor descriptor,如果其相关 设置对象为 settings,并且 descriptor.[[representedDescriptor]] 位于 invalidatedAttributes 中,则将 descriptor.[[representedDescriptor]] 设置为 null。

    4. 令 global 为 settings 的 全局对象。

    5. 从 global.navigator.bluetooth.[[attributeInstanceMap]] 中移除表示 invalidatedAttributes 中属性的 每个条目。

  7. 令 changedServices 为一个初始为空的服务集合。

  8. 如果同一 服务同时出现在 removedAttributes 和 addedAttributes 中,则将其从两者中移除,并添加到 changedServices 中。

  9. 对于 removedAttributes 或 addedAttributes 中的每个特征和描述符,将其从原始列表中移除,并将其 父服务添加到 changedServices 中。

    注:从此处开始,removedAttributes 和 addedAttributes 仅包含服务。
  10. 如果 addedAttributes 中的一个服务在此前任何一次 getPrimaryService、getPrimaryServices、 getIncludedService 或 getIncludedServices 调用时即使 已经存在也不会被返回,则 UA 可以从 addedAttributes 中移除该服务。

  11. 令 changedDevices 为包含 removedAttributes、 addedAttributes 和 changedServices 中任一服务的蓝牙设备集合。

  12. 对于连接到 changedDevices 中某个设备的每个 BluetoothDevice deviceObj,在给定 deviceObj 的相关全局对象的蓝牙任务源上排入一个全局任务,以执行以下 步骤:

    1. 对于 removedAttributes 中的每个服务 service:

      1. 如果 deviceObj.[[allowedServices]] 为 "all" 或包含该服务的 UUID,则在 表示该服务的 BluetoothRemoteGATTService 上触发一个事件, 该事件名为 serviceremoved, 并将其 bubbles 属性 初始化为 true。

      2. 从蓝牙 树中移除此 BluetoothRemoteGATTService。

    2. 对于 addedAttributes 中的每个服务,如果 deviceObj.[[allowedServices]] 为 "all" 或包含该服务的 UUID,则将表示此服务的 BluetoothRemoteGATTService 添加到 蓝牙树,然后在该 BluetoothRemoteGATTService 上触发一个名为 serviceadded 的事件,并将其 bubbles 属性初始化为 true。

    3. 对于 changedServices 中的每个服务,如果 deviceObj.[[allowedServices]] 为 "all" 或包含该服务的 UUID,则在表示 该服务的 BluetoothRemoteGATTService 上触发一个名为 servicechanged 的事件,并将其 bubbles 属性初始化 为 true。

6.6.6. IDL 事件处理器

[SecureContext]
interface mixin CharacteristicEventHandlers {
  attribute EventHandler oncharacteristicvaluechanged;
};

oncharacteristicvaluechanged 是用于 characteristicvaluechanged 事件类型的事件处理器 IDL 属性。

[SecureContext]
interface mixin BluetoothDeviceEventHandlers {
  attribute EventHandler onadvertisementreceived;
  attribute EventHandler ongattserverdisconnected;
};

onadvertisementreceived 是用于 advertisementreceived 事件 类型的事件处理器 IDL 属性。

ongattserverdisconnected 是用于 gattserverdisconnected 事件类型的事件处理器 IDL 属性。

[SecureContext]
interface mixin ServiceEventHandlers {
  attribute EventHandler onserviceadded;
  attribute EventHandler onservicechanged;
  attribute EventHandler onserviceremoved;
};

onserviceadded 是用于 serviceadded 事件类型的事件 处理器 IDL 属性。

onservicechanged 是用于 servicechanged 事件类型的事件 处理器 IDL 属性。

onserviceremoved 是用于 serviceremoved 事件类型的事件 处理器 IDL 属性。

6.7. 错误处理

注:本节主要定义从系统错误到 JavaScript 错误名称的映射,并允许 UA 重试某些操作。重试逻辑和 可能的错误区分受到操作系统的严格限制,因此 这些要求不符合实际情况的地方很可能是 规范错误 而不是浏览器错误。
当 UA 使用一个GATT 过程执行算法中的某个步骤 或处理对蓝牙缓存的查询(此处两者均称为 “步骤”),并且 GATT 过程返回一个 错误 响应时,UA 必须执行以下步骤:
  1. 如果过程超时,或 ATT 承载(见配置文件 基础)不存在或因任何原因终止,则从该步骤返回一个 NetworkError 并中止这些步骤。

  2. 根据 Error Code 执行以下操作:

    Invalid PDU
    Invalid Offset
    Attribute Not Found
    Unsupported Group Type
    这些错误代码表示协议层发生了意外情况, 很可能是由于 UA 或设备中的错误。返回一个 NotSupportedError 作为该步骤的结果。
    Invalid Handle
    返回一个 InvalidStateError 作为该步骤的结果。
    Invalid Attribute Value Length
    返回一个 InvalidModificationError 作为该步骤的结果。
    Attribute Not Long

    如果在未使用“Long” 子过程的情况下收到此错误代码,则这可能表示设备存在错误。返回一个 NotSupportedError 作为该步骤的结果。

    否则,在不使用“Long”子过程的情况下重试该步骤。 如果由于正在写入的值长度而无法这样做, 则返回一个 InvalidModificationError 作为该步骤的结果。

    Insufficient Authentication
    Insufficient Encryption
    Insufficient Encryption Key Size
    UA 应尝试提高连接的安全级别。 如果此尝试失败或 UA 不支持更高的安全级别, 则从该步骤返回一个 SecurityError。 否则,以新的更高安全级别重试该步骤。
    Insufficient Authorization
    从该步骤返回一个 SecurityError。
    Application Error
    如果 GATT 过程为写入, 则从该步骤返回一个 InvalidModificationError。 否则,从该步骤返回一个 NotSupportedError。
    Read Not Permitted
    Write Not Permitted
    Request Not Supported
    Prepare Queue Full
    Insufficient Resources
    Unlikely Error
    其他任何情况
    从该步骤返回一个 NotSupportedError。

7. UUID

typedef DOMString UUID;

一个 UUID 字符串表示一个 128 位的 [RFC4122] UUID。一个有效 UUID 是一个与 [ECMAScript] 正则表达式 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ 匹配的字符串。 也就是说,有效 UUID 使用小写 并且不使用蓝牙标准定义的 16 位或 32 位 缩写。本规范中的函数和属性返回的所有 UUID 都必须是有效 UUID。如果本 规范中的函数接受一个类型为 UUID 的参数或一个 包含 UUID 属性的字典,并且传入任何 UUID 槽的参数不是有效 UUID,则该函数必须返回一个 被拒绝的 promise,其拒绝原因为一个 TypeError,并中止其其他步骤。

注:本标准提供了 BluetoothUUID.canonicalUUID(alias) 函数,用于将 16 位或 32 位蓝牙UUID 别名 映射为其 128 位形式。
注:蓝牙设备需要在比较 16 位和 32 位 UUID 之前将其转换为 128 位 UUID(如属性类型中所述), 但并非所有设备都会这样做。为了与这些设备互操作,如果 UA 已 从设备接收到某种形式(16 位、32 位或 128 位)的 UUID,则应 以相同形式将该 UUID 的其他别名发送回设备。

7.1. 标准化 UUID

Bluetooth SIG 在 [BLUETOOTH-ASSIGNED] 中维护一个 UUID 注册表,用于 标识服务、特征、描述符和其他实体。本 节提供了一种让脚本按名称查找这些 UUID 的方式,使其无须 在每个应用程序中重复这些 UUID。

一个有效名称是与 [ECMAScript] 正则表达式 /^[a-z0-9_-.]+$/ 匹配的字符串。

[Exposed=Window]
interface BluetoothUUID {
  static UUID getService((DOMString or unsigned long) name);
  static UUID getCharacteristic((DOMString or unsigned long) name);
  static UUID getDescriptor((DOMString or unsigned long) name);

  static UUID canonicalUUID([EnforceRange] unsigned long alias);
};

typedef (DOMString or unsigned long) BluetoothServiceUUID;
typedef (DOMString or unsigned long) BluetoothCharacteristicUUID;
typedef (DOMString or unsigned long) BluetoothDescriptorUUID;

静态 BluetoothUUID. canonicalUUID(alias) 方法在被调用时,必须返回由 16 位或 32 位 UUID 别名 alias 所表示的 128 位 UUID。

注:此算法通过将 “00000000-0000-1000-8000-00805f9b34fb”的最高 32 位替换为 别名的位来实现。例如,canonicalUUID(0xDEADBEEF) 返回 "deadbeef-0000-1000-8000-00805f9b34fb"。

BluetoothServiceUUID 表示 16 位和 32 位 UUID 别名、有效 UUID,以及来自 GATT 分配 服务键的有效名称,或者等价地, 表示使 BluetoothUUID.getService() 不会抛出 异常的值。

BluetoothCharacteristicUUID 表示 16 位和 32 位 UUID 别名、有效 UUID,以及来自GATT 分配 特征键的有效名称,或者等价地, 表示使 BluetoothUUID.getCharacteristic() 不会 抛出异常的值。

BluetoothDescriptorUUID 表示 16 位和 32 位 UUID 别名、有效 UUID,以及来自GATT 分配 描述符键的有效名称,或者等价地, 表示使 BluetoothUUID.getDescriptor() 不会抛出 异常的值。

要执行 ResolveUUIDName(name, GATT 分配 编号),UA 必须执行以下步骤:
  1. 如果 name 是一个 unsigned long,则返回 BluetoothUUID.canonicalUUID(name) 并中止这些步骤。

  2. 如果 name 是一个有效 UUID,则返回 name 并中止 这些步骤。

  3. 如果 name 是一个有效 名称,并且在 GATT assigned numbers 中映射到一个有效 UUID,则令 alias 为其分配 编号,并返回 BluetoothUUID.canonicalUUID(alias)。

  4. 否则,抛出一个 TypeError。

静态 BluetoothUUID. getService(name) 方法在被调用时,必须返回 ResolveUUIDName(name, GATT 分配 服务)。

静态 BluetoothUUID. getCharacteristic(name) 方法在被调用时,必须 返回 ResolveUUIDName(name, GATT 分配 特征)。

静态 BluetoothUUID. getDescriptor(name) 方法在被调用时,必须返回 ResolveUUIDName(name, GATT 分配 描述符)。

BluetoothUUID.getService(" cycling_power") 返回 "00001818-0000-1000-8000-00805f9b34fb"。

BluetoothUUID.getService("00001801-0000-1000-8000-00805f9b34fb") 返回 "00001801-0000-1000-8000-00805f9b34fb"。

BluetoothUUID.getService("unknown-service") 抛出一个 TypeError。

BluetoothUUID.getCharacteristic("ieee_11073-20601_regulatory_certification_data_list") 返回 "00002a2a-0000-1000-8000-00805f9b34fb"。

BluetoothUUID.getDescriptor("gatt.characteristic_presentation_format") 返回 "00002904-0000-1000-8000-00805f9b34fb"。

7.2. GATT 分配编号

本规范为 GATT 分配编号提供人类可读名称, 以提高使用标准化 GATT 服务、特征 和描述符的开发者的可读性。GATT 分配编号文件位于 https://github.com/WebBluetoothCG/registries 仓库中。

本规范此前使用由 Bluetooth SIG 提供的映射表来定义这些人类可读名称。当发现该映射表不再 维护时,本规范中直接规定了一个映射表。
在 URL url 处解析 GATT 分配编号的结果,是一个从有效 名称到有效 UUID的映射,或一个 错误,由以下算法产生:
  1. 获取 url,并令 contents 为其正文,按 UTF-8 解码。

  2. 令 lines 为在 '\n' 处分割 contents 的结果。

  3. 令 result 为一个空映射。

  4. 对于 lines 中的每个 line,执行以下子步骤:

    1. 如果 line 为空,或其第一个字符为 '#', 则继续处理下一行。

    2. 如果 line 由一个有效名称、一个空格(U+0020)和 一个有效 UUID组成, 则令 name 为该名称,并令 uuid 为该 UUID。

    3. 否则,返回错误并中止这些步骤。

    4. 如果 name 已存在于 result 中,则返回错误并 中止这些步骤。

    5. 在 result 中添加一个从 name 到 uuid 的映射。

  5. 返回 result。

GATT 分配 服务是在 https://github.com/WebBluetoothCG/registries/blob/master/gatt_assigned_services.txt 处解析 GATT 分配编号的结果。 UA 应定期重新获取此文件,但未规定具体频率。

GATT 分配特征是在 https://github.com/WebBluetoothCG/registries/blob/master/gatt_assigned_characteristics.txt 处解析 GATT 分配编号的结果。 UA 应定期重新获取此文件,但未规定具体频率。

GATT 分配 描述符是在 https://github.com/WebBluetoothCG/registries/blob/master/gatt_assigned_descriptors.txt 处解析 GATT 分配编号的结果。 UA 应定期重新获取此文件,但未规定具体频率。

8. 广播数据过滤器

广播数据过滤器表示一种匹配制造商数据或服务数据的方式。

要从一个字符串 input 解析 广播数据过滤器,执行 以下步骤:
  1. 令 words 为在 / 处对 input 进行严格 分割的结果。

  2. 如果 words 的长度不等于 2,则返回错误并 中止这些步骤。

  3. 如果 words[0] 的长度不等于 words[1] 的长度,则返回 错误并 中止这些步骤。

  4. 令 prefixData 为 words[0]。

  5. 令 prefixMask 为 words[1]。

  6. 如果 prefixData 或 prefixMask 不是ASCII 小写十六进制数字序列,则返回错误。

  7. 令 prefixIndex 为 0。

  8. 令 dataList 为空列表。

  9. 令 maskList 为空列表。

  10. 当 prefixIndex 小于 prefixData 的长度时,执行以下 子步骤:

    1. 令 data 为将 prefixData 中索引 prefixIndex 和 prefixIndex + 1 处的字符解释为十六进制数所得的结果。

    2. 令 mask 为将 prefixMask 中索引 prefixIndex 和 prefixIndex + 1 处的字符解释为十六进制数所得的结果。

    3. 将 data 追加到 dataList。

    4. 将 mask 追加到 maskList。

    5. 将 prefixIndex 设置为 |prefixIndex| + 2。

  11. 令 result 为一个新的 BluetoothDataFilterInit 字典。

  12. 将 result[dataPrefix] 设置为一个使用 dataList 构造的 Uint8Array。

  13. 将 result[mask] 设置为一个使用 maskList 构造的 Uint8Array。

  14. 返回 result。

9. 阻止列表

本规范依赖 https://github.com/WebBluetoothCG/registries 仓库中的阻止列表文件, 以限制网站可以访问的 GATT 属性和制造商数据集合。

一个有效 公司标识符字符串是一个ASCII 小写十六进制数字序列,其长度 大于 0 且小于 5。公司标识符的官方列表可在 蓝牙分配编号网站上找到。

在 URL url 处解析制造商数据阻止列表的结果是一个 从公司标识符代码到 BluetoothDataFilterInit 列表的映射, 或一个错误, 由以下算法产生:
  1. 获取 url,并令 contents 为其按 UTF-8 解码的正文。

  2. 令 lines 为在 contents 上以分隔符 '\n' 调用 split(separator, limit) 所得的结果。

  3. 令 result 为一个空映射。

  4. 对于 lines 中的每个 line,执行以下子步骤:

    1. 如果 line 为空,或其第一个字符为 '#',则继续处理 下一行。

    2. 令 regExp 为一个以 'manufacturer\ ([0-9a-f]+)\ ([0-9a-f]+\/[0-9a-f]+)' 构造的 RegExp。

    3. 令 matchResult 为在 line 上调用 regExp.exec(string) 所得的结果;如果 matchResult 为 null,或 matchResult 的长度不等于 3,则返回错误。

    4. 如果 matchResult[1] 是 有效公司标识符字符串, 则令 companyIdentifierStr 为 matchResult[1], 否则返回错误。

    5. 令 companyIdentifier 为将 companyIdentifierStr 解释为十六进制数所得的结果。

    6. 令 dataPrefixStr 为 matchResult[2]。

    7. 如果 companyIdentifier 不在 result 中,则将 result[companyIdentifier] 设置为空列表。

    8. 如果结果不是错误,则令 dataFilter 为在 dataPrefixStr 处解析广播数据过滤器的结果, 否则返回错误。

    9. 将 dataFilter 追加到 result[companyIdentifier]。

  5. 返回 result。

在 URL url 处解析 GATT 阻止列表的结果,是一个 从有效 UUID到标记的映射,或一个 错误,由以下 算法产生:
  1. 获取 url,并令 contents 为其正文,按 UTF-8 解码。

  2. 令 lines 为在 '\n' 处分割 contents 的结果。

  3. 令 result 为一个空映射。

  4. 对于 lines 中的每个 line,执行以下子步骤:

    1. 如果 line 为空,或其第一个字符为 '#', 则继续处理下一行。

    2. 如果 line 仅由一个有效 UUID组成,则令 uuid 为该 UUID,并令 token 为 “exclude”。

    3. 如果 line 由一个有效 UUID、一个空格(U+0020)以及 标记“exclude-reads”或 “exclude-writes”之一组成,则令 uuid 为该 UUID,并令 token 为该标记。

    4. 否则,返回错误并中止这些步骤。

    5. 如果 uuid 已存在于 result 中,则返回错误并 中止这些步骤。

    6. 在 result 中添加一个从 uuid 到 token 的映射。

  5. 返回 result。

GATT 阻止列表是在 https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt 处解析 GATT 阻止列表的结果。 制造商 数据阻止列表是在 https://github.com/WebBluetoothCG/registries/blob/master/manufacturer_data_blocklist.txt 处解析制造商数据阻止列表的结果。 UA 应定期重新获取阻止列表,但未规定具体频率。

如果GATT 阻止列表的 值为错误,或 UUID 在GATT 阻止列表中映射到“exclude”,则一个 UUID 被阻止。

如果GATT 阻止列表的值为错误,或 UUID 在GATT 阻止列表中映射到 “exclude”或“exclude-reads”之一,则一个 UUID 被阻止读取。

如果GATT 阻止列表的值为错误,或 UUID 在GATT 阻止列表中映射到 “exclude”或“exclude-writes”之一,则一个 UUID 被阻止写入。

如果以下 步骤返回 blocked,则制造商数据 manufacturerData 是被阻止的制造商数据:
  1. 如果制造商数据阻止列表的值为 错误,则返回 blocked。

  2. 令 manufacturerBlocklist 为制造商 数据阻止列表的值。

  3. 令 companyIdentifier 为 manufacturerData 的公司标识符。

  4. 如果 companyIdentifier 不在 manufacturerBlocklist 中,则返回 unblocked。

  5. 对于 manufacturerBlocklist[companyIdentifier] 中的每个 dataFilter, 执行以下子步骤:

    1. 如果 manufacturerData 的广播数据匹配 dataFilter, 则返回 blocked。

  6. 返回 unblocked。

如果以下步骤返回 blocked,则制造商数据过滤器 manufacturerDataFilter 是被阻止的制造商 数据过滤器:
  1. 如果制造商数据阻止列表的值为 错误,则返回 blocked。

  2. 令 manufacturerBlocklist 为制造商 数据阻止列表的值。

  3. 令 companyIdentifier 为 manufacturerDataFilter["companyIdentifier"]。

  4. 如果 companyIdentifier 不在 manufacturerBlocklist 中,则返回 unblocked。

  5. 对于 manufacturerBlocklist[companyIdentifier] 中的每个 dataFilter, 执行以下子步骤:

    1. 如果 manufacturerDataFilter 是 dataFilter 的真子集, 则返回 blocked。

  6. 返回 unblocked。

[SecureContext]
partial interface Navigator {
  [SameObject]
  readonly attribute Bluetooth bluetooth;
};

每个 Navigator 都有一个关联的 Bluetooth,它是一个 Bluetooth 对象。创建 Navigator 对象时,其 关联的 Bluetooth 必须设置为一个新的 Bluetooth 对象, 该对象在 Navigator 对象的相关 realm中创建。

Navigator 的 bluetooth getter 步骤是返回 this 的关联的 Bluetooth。

11. 集成

11.1. 权限策略

本规范定义了一个由标记“bluetooth”标识的策略控制功能, 该功能控制是否可以使用 Navigator 对象上的 bluetooth 属性所公开的方法。

此功能的默认允许列表为 ["self"]。

12. 自动化测试

出于用户代理自动化和应用程序测试的目的,本文档定义了对 [WebDriver-BiDi] 规范的扩展。

Web Bluetooth API 及其扩展规范给测试作者带来了挑战,因为要完整测试 这些接口,需要能够以可预测方式响应的物理硬件设备。为解决此 挑战,本文档定义了多个 WebDriver-BiDi 扩展命令,用于定义和 控制行为类似于物理外围设备及其广播的模拟外围设备和广播。 这些模拟外围设备和广播表示具有特定属性的设备, 其读数可完全由用户定义。

每个顶级可遍历对象都可以具有一个模拟蓝牙适配器,它是一个软件 定义的蓝牙适配器,具有一组已发现的模拟蓝牙 设备,并且可以承担诸如中心设备之类的角色。

每个模拟蓝牙适配器都有一个模拟蓝牙设备 映射,它是一个从蓝牙地址 字符串 到模拟蓝牙设备的有序映射。

每个模拟蓝牙适配器都有一个适配器状态,它是一个 描述适配器当前状态的字符串枚举。可能的枚举值为:

每个模拟蓝牙适配器都有一个低功耗支持状态,它是一个布尔值,用于描述适配器 是否支持低功耗蓝牙。

一个模拟 蓝牙设备是一个软件定义的蓝牙设备,其 行为类似于物理设备,可以附加到一个模拟蓝牙 适配器, 可以具有诸如制造商特定数据和服务 UUID之类的关联属性, 并具有一个模拟 GATT 服务映射,它是一个 从蓝牙 UUID 字符串到模拟 GATT 服务的有序 映射。

一个模拟 GATT 服务是一个软件定义的服务,它属于一个 模拟蓝牙设备,具有一个 UUID 属性,在 蓝牙缓存中为已知存在,并具有一个模拟 GATT 特征映射,它 是一个从蓝牙 UUID 字符串到模拟 GATT 特征的有序映射。

一个模拟 GATT 特征是一个软件定义的特征,它属于一个 模拟 GATT 服务,具有一个 UUID 属性和一个 特征属性属性, 在蓝牙缓存中为已知存在,并具有一个模拟 GATT 描述符 映射,它 是一个从蓝牙 UUID 字符串到模拟 GATT 描述符的有序映射。

模拟 GATT 特征属性是 软件定义的特征属性,它们属于一个 模拟 GATT 特征,并在 蓝牙缓存中为已知存在。

一个模拟 GATT 描述符是一个软件定义的描述符,它属于一个 模拟 GATT 特征,具有一个 UUID 属性,并在蓝牙缓存中为已知存在。

CDDL 片段使用 “text”类型,而不是 “browsingContext.BrowsingContext”,以允许独立地以编程方式 处理 CDDL 片段。目前无法引用其他模块。

12.1. 定义

bluetooth.BluetoothUuid = text;
bluetooth.BluetoothManufacturerData = { key: uint, data: tstr };
bluetooth.CharacteristicProperties = {
  ? broadcast: bool,
  ? read: bool,
  ? writeWithoutResponse: bool,
  ? write: bool,
  ? notify: bool,
  ? indicate: bool,
  ? authenticatedSignedWrites: bool,
  ? extendedProperties: bool
}
key
是公司标识符代码。
data
是经过 base64 编码的制造商数据字节序列。

12.2. bluetooth 模块

bluetooth 模块包含用于管理远程端蓝牙行为的命令。

12.2.1. 类型

12.2.1.1. bluetooth.RequestDevice 类型
bluetooth.RequestDevice = text

bluetooth.RequestDevice 是请求设备提示中单个设备的标识符。

一个设备提示是一个元组,由 一个设备 提示 ID(一个字符串)和一个设备集合组成,后者是一个由 BluetoothDevice 对象组成的集合。 它表示一个允许用户选择一个蓝牙设备的提示。

12.2.1.2. bluetooth.RequestDeviceInfo 类型
bluetooth.RequestDeviceInfo = {
   id: bluetooth.RequestDevice,
   name: text / null,
}

bluetooth.RequestDeviceInfo 表示请求设备提示中的单个设备。

给定一个 BluetoothDevice device,要序列化 设备:
  1. 令 id 为 device.id。

  2. 令 name 为 device.name。

  3. 返回一个与 bluetooth.RequestDeviceInfo 产生式匹配的映射,其中 "id" 设置为 id,"name" 设置为 name。

12.2.1.3. bluetooth.RequestDevicePrompt 类型
bluetooth.RequestDevicePrompt = text

bluetooth.RequestDevicePrompt 是单个提示的标识符。

远程端具有一个可导航对象到设备提示的映射,它是一个映射, 其键为可导航对象 ID,值为设备提示。

给定 navigableId 和 promptId,要获取提示:
  1. 令 promptMap 为可导航对象到设备提示的映射。

  2. 如果 promptMap[navigableId] 不存在:

    1. 返回带有no such prompt 错误代码的错误。

  3. 令 prompt 为可导航对象到设备 提示的映射[navigableId]。

  4. 如果 prompt 的设备提示 ID不是 promptId:

    1. 返回带有no such prompt 错误代码的错误。

  5. 返回以 prompt 为数据的成功。

给定设备 提示 prompt 和 deviceId,要匹配提示中的设备:
  1. 对于 prompt 的设备集合中的每个 device:

    1. 如果 device.id 是 deviceId,则返回以 device 为数据的成功。

  2. 否则:

    1. 返回带有no such device 错误代码的错误。

给定设备 提示 prompt,要序列化提示 设备:
  1. 令 devices 为一个空列表。

  2. 对于 prompt 的设备集合中的每个 device。

    1. 将序列化 device 的结果追加到 devices。

  3. 返回 devices。

12.2.1.4. bluetooth.ScanRecord 类型
bluetooth.ScanRecord = {
  ? name: text,
  ? uuids: [ * bluetooth.BluetoothUuid ],
  ? appearance: number,
  ? manufacturerData: [ * bluetooth.BluetoothManufacturerData ],
}

一个 bluetooth.ScanRecord 表示蓝牙设备发送的广播数据包的数据。

name
是蓝牙 设备的本地名称或其前缀。
uuids
列出此扫描记录表明蓝牙设备的 GATT 服务器支持的服务 UUID。
appearance
是一个外观值,即 gap.appearance 特征定义的值之一。
manufacturerData
一个 BluetoothManufacturerData 列表,它将 unsigned short 公司标识符代码映射到经过 base64 编码的制造商数据字节序列。

12.2.2. 错误

本规范通过以下附加代码扩展了 WebDriver BiDi 中的错误代码集合:

no such device
尝试引用未知的 BluetoothDevice。
no such prompt
尝试引用未知的设备提示。

12.2.3. 命令

BluetoothCommand = (
  bluetooth.HandleRequestDevicePrompt //
  bluetooth.SimulateAdapter //
  bluetooth.DisableSimulation //
  bluetooth.SimulatePreconnectedPeripheral //
  bluetooth.SimulateAdvertisement //
  bluetooth.SimulateGattConnectionResponse //
  bluetooth.SimulateGattDisconnection //
  bluetooth.SimulateService //
  bluetooth.SimulateCharacteristic //
  bluetooth.SimulateCharacteristicResponse //
  bluetooth.SimulateDescriptor //
  bluetooth.SimulateDescriptorResponse
)
12.2.3.1. bluetooth.handleRequestDevicePrompt 命令
bluetooth.HandleRequestDevicePrompt = (
   method: "bluetooth.handleRequestDevicePrompt",
   params: bluetooth.HandleRequestDevicePromptParameters,
)

bluetooth.HandleRequestDevicePromptParameters = {
   context: text,
   prompt: bluetooth.RequestDevicePrompt,
   (
       bluetooth.HandleRequestDevicePromptAcceptParameters //
       bluetooth.HandleRequestDevicePromptCancelParameters
   )
}

bluetooth.HandleRequestDevicePromptAcceptParameters = (
   accept: true,
   device: bluetooth.RequestDevice,
)

bluetooth.HandleRequestDevicePromptCancelParameters = (
   accept: false,
)
给定 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 promptId 为 params["prompt"]。

  3. 令 prompt 为使用 contextId 和 promptId 获取提示的尝试结果。

  4. 令 accept 为 params["accept"] 的值。

  5. 如果 accept 为 true:

    1. 令 deviceId 为 params["device"] 的值。

    2. 令 device 为给定 prompt 和 deviceId 匹配提示中的设备的尝试结果。

    3. 使用 device 确认 prompt。

  6. 否则:

    1. 关闭 prompt。

  7. 返回以 null 为数据的成功。

一个本地端可以通过发送以下消息关闭提示:
{
  "method": "bluetooth.handleRequestDevicePrompt",
  "params": {
    "context": "cxt-d03fdd81",
    "prompt": "pmt-e0a234b",
    "accept": true,
    "device": "dvc-9b3b872"
  }
}
12.2.3.2. bluetooth.simulateAdapter 命令
bluetooth.SimulateAdapter = (
   method: "bluetooth.simulateAdapter",
   params: bluetooth.SimulateAdapterParameters,
)

bluetooth.SimulateAdapterParameters = {
   context: text,
   ? leSupported: bool,
   state: "absent" / "powered-off" / "powered-on"
}
带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 如果 navigable 不是顶级可遍历对象,则返回带有invalid argument 错误代码的错误。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则运行以下步骤:

    1. 如果 params["leSupported"] 不存在,则将 params["leSupported"] 设置为 true。

    2. 令 simulatedBluetoothAdapter 为一个新的模拟蓝牙适配器。

    3. 将 simulatedBluetoothAdapter 的LE 支持状态 设置为 params["leSupported"]。

    4. 将 simulatedBluetoothAdapter 的适配器状态设置为 params["state"]。

    5. 将 navigable 的模拟蓝牙适配器设置为 simulatedBluetoothAdapter。

    6. 返回以 null 为数据的成功。

  6. 如果 simulatedBluetoothAdapter 不为空,则运行以下步骤:

    1. 如果 params["leSupported"] 存在,则返回带有invalid argument 错误代码的错误。

    2. 将 simulatedBluetoothAdapter 的适配器状态设置为 params["state"]。

    3. 返回以 null 为数据的成功。

一个本地端可以通过发送以下消息模拟支持低功耗蓝牙的适配器:
{
  "method": "bluetooth.simulateAdapter",
  "params": {
    "context": "cxt-d03fdd81",
    "leSupported": true,
    "state": "powered-on",
  }
}
一个本地端可以通过发送以下消息更新现有适配器的适配器状态:
{
  "method": "bluetooth.simulateAdapter",
  "params": {
    "context": "cxt-d03fdd81",
    "state": "powered-off",
  }
}
12.2.3.3. bluetooth.disableSimulation 命令
bluetooth.DisableSimulation = (
   method: "bluetooth.disableSimulation",
   params: bluetooth.DisableSimulationParameters,
)

bluetooth.DisableSimulationParameters = {
   context: text
}
带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 如果 navigable 不是顶级可遍历对象,则返回带有invalid argument 错误代码的错误。

  4. 将 navigable 的模拟蓝牙适配器设置为空。

  5. 返回以 null 为数据的成功。

一个本地端可以通过发送以下消息禁用现有模拟:
{
  "method": "bluetooth.disableSimulation",
  "params": {
    "context": "cxt-d03fdd81"
  }
}
12.2.3.4. bluetooth.simulatePreconnectedPeripheral 命令
bluetooth.SimulatePreconnectedPeripheral = (
   method: "bluetooth.simulatePreconnectedPeripheral",
   params: bluetooth.SimulatePreconnectedPeripheralParameters,
)

bluetooth.SimulatePreconnectedPeripheralParameters = {
   context: text,
   address: text,
   name: text,
   manufacturerData: [ * bluetooth.BluetoothManufacturerData ],
   knownServiceUuids: [ * bluetooth.BluetoothUuid ]
}
带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 如果 navigable 不是顶级可遍历对象,则返回带有invalid argument 错误代码的错误。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有invalid argument 错误代码的错误。

  6. 令 deviceAddress 为 params["address"]。

  7. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  8. 如果 deviceMapping[deviceAddress] 存在,则返回 带有invalid argument 错误 代码的错误。

  9. 令 simulatedBluetoothDevice 为一个新的模拟 蓝牙设备。

  10. 将 simulatedBluetoothDevice 的名称设置为 params["name"]。

  11. 将 simulatedBluetoothDevice 的地址设置为 params["address"]。

  12. 将 simulatedBluetoothDevice 的制造商 特定数据设置为对 params["manufacturerData"] 执行宽容 base64 解码的输出。

  13. 将 simulatedBluetoothDevice 的服务 UUID设置为 params["knownServiceUuids"]。

  14. 将 deviceMapping[deviceAddress] 设置为 simulatedBluetoothDevice。

  15. 返回以 null 为数据的成功。

一个本地端可以通过发送以下消息模拟一个预连接的外围设备:
{
  "method": "bluetooth.simulatePreconnectedPeripheral",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "name": "某个设备",
    "manufacturerData": [ { key: 17, data: "AP8BAX8=" } ],
    "knownServiceUuids": [
      "12345678-1234-5678-9abc-def123456789",
    ],
  }
}
12.2.3.5. bluetooth.simulateAdvertisement 命令
bluetooth.SimulateAdvertisement = (
   method: "bluetooth.simulateAdvertisement",
   params: bluetooth.SimulateAdvertisementParameters,
)

bluetooth.SimulateAdvertisementParameters = {
   context: text,
   scanEntry: bluetooth.SimulateAdvertisementScanEntryParameters
}

bluetooth.SimulateAdvertisementScanEntryParameters = {
   deviceAddress: text,
   rssi: number,
   scanRecord: bluetooth.ScanRecord
}

带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 topLevelNavigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 如果 topLevelNavigable 不是顶级可遍历对象,则返回带有invalid argument 错误代码的错误。

  4. 令 scanEntry 为 params["scanEntry"]。

  5. 令 deviceAddress 为 scanEntry["deviceAddress"]。

  6. 令 simulatedBluetoothAdapter 为 topLevelNavigable 的模拟蓝牙适配器。

  7. 如果 simulatedBluetoothAdapter 为空,则返回带有invalid argument 错误代码的错误。

  8. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  9. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。否则,令 simulatedDevice 为一个带有 deviceAddress 的新的模拟蓝牙设备,并将 deviceMapping[deviceAddress] 设置为 simulatedDevice。

  10. 如果 topLevelNavigable 当前正在执行扫描设备算法, 则将 simulatedDevice 插入该算法中的 simulatedBluetoothDevices 变量。

    将数据 插入另一个算法的变量中并没有明确定义。扫描设备算法需要 定义异步设备发现,以便与实现相匹配。

  11. 令 navigables 为 topLevelNavigable 的活动文档的包含自身的后代可导航对象。

  12. 对于 navigables 中的每个 navigable:

    1. 令 document 为 navigable 的活动文档。

    2. 在 document 的相关设置对象的负责的事件循环上排入一个任务,以执行以下 子步骤:

      1. 令 simulatedDeviceInstance 为获取表示 BluetoothDevice 的对象 simulatedDevice 的结果, 该对象位于 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth 中。

      2. 如果 simulatedDeviceInstance.[[watchAdvertisementsState]] 为 not-watching,则中止这些子步骤。

      3. 在 simulatedDeviceInstance 上,针对由 scanEntry["scanRecord"] 表示的广播事件,触发一个 advertisementreceived 事件。

  13. 返回以 null 为数据的成功。

一个本地端可以通过发送以下消息模拟设备广播:
{
  "method": "bluetooth.simulateAdvertisement",
  "params": {
    "context": "cxt-d03fdd81",
    "scanEntry": {
      "deviceAddress": "08:08:08:08:08:08",
      "rssi": -10,
      "scanRecord": {
        "name": "心率",
        "uuids": ["0000180d-0000-1000-8000-00805f9b34fb"],
        "manufacturerData": [ { key: 17, data: "AP8BAX8=" } ],
        "appearance": 1,
        "txPower": 1
      }
    }
  }
}
12.2.3.6. bluetooth.simulateGattConnectionResponse 命令
bluetooth.SimulateGattConnectionResponse = (
   method: "bluetooth.simulateGattConnectionResponse",
   params: bluetooth.SimulateGattConnectionResponseParameters,
)

bluetooth.SimulateGattConnectionResponseParameters = {
   context: text,
   address: text,
   code: uint
}

带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 令 deviceAddress 为 params["address"]。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有无效参数错误代码的错误。

  6. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  7. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。 否则,返回带有无效参数错误 代码的错误。

  8. 令 simulatedDeviceInstance 为在 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth中获取表示 BluetoothDevice simulatedDevice 的结果。

  9. 如果 simulatedDeviceInstance.[[gatt]].[[automatedGATTConnectionResponse]] 为 "expected", 则将 simulatedDeviceInstance.[[gatt]].[[automatedGATTConnectionResponse]] 设置为 params["code"]。

  10. 否则,返回带有无效元素状态错误 代码的错误。

一个本地端可以通过发送以下消息模拟设备 GATT 连接成功响应 (根据错误代码列表,错误代码为 0x00):
{
  "method": "bluetooth.simulateGattConnectionResponse",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "code": 0
  }
}
12.2.3.7. bluetooth.simulateGattDisconnection 命令
bluetooth.SimulateGattDisconnection = (
   method: "bluetooth.simulateGattDisconnection",
   params: bluetooth.SimulateGattDisconnectionParameters,
)

bluetooth.SimulateGattDisconnectionParameters = {
   context: text,
   address: text,
}

带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 令 deviceAddress 为 params["address"]。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有无效参数错误代码的错误。

  6. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  7. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。 否则,返回带有无效参数错误 代码的错误。

  8. 令 simulatedDeviceInstance 为在 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth中获取表示 BluetoothDevice simulatedDevice 的结果。

  9. 如果 simulatedDeviceInstance.[[gatt]].[[automatedGATTConnectionResponse]] 为 "expected", 则将 simulatedDeviceInstance.[[gatt]].[[automatedGATTConnectionResponse]] 设置为 0x15。

    根据 错误代码 列表,0x15 表示 "Remote Device Terminated Connection due to Power Off"。 这模拟了蓝牙设备无法 响应 GATT 连接尝试的场景。
  10. 否则,清理已断开连接的设备 simulatedDeviceInstance。

一个本地端可以通过发送 以下消息模拟设备 GATT 断开连接:
{
  "method": "bluetooth.simulateGattDisconnection",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
  }
}
12.2.3.8. bluetooth.simulateService 命令
bluetooth.SimulateService = (
   method: "bluetooth.simulateService",
   params: bluetooth.SimulateServiceParameters,
)

bluetooth.SimulateServiceParameters = {
   context: text,
   address: text,
   uuid: bluetooth.BluetoothUuid,
   type: "add" / "remove",
}
带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 令 deviceAddress 为 params["address"]。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有无效参数错误代码的错误。

  6. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  7. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。

  8. 否则,返回带有无效参数错误 代码的错误。

  9. 令 simulatedDeviceInstance 为在 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth中获取表示 BluetoothDevice simulatedDevice 的结果。

  10. 令 serviceMapping 为 simulatedDevice 的模拟 GATT 服务映射。

  11. 令 uuid 为 params["uuid"]。

  12. 如果 params["type"] 为 "add":

    1. 如果 serviceMapping[uuid] 存在,则返回带有无效元素状态错误代码的错误。

    2. 令 simulatedGattService 为一个新的模拟 GATT 服务。

    3. 将 simulatedGattService 的 UUID 设置为 uuid。

    4. 将 serviceMapping[uuid] 设置为 simulatedGattService。

    5. 创建一个表示 BluetoothRemoteGATTService simulatedGattService 的对象,并在 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]] 中添加从 simulatedGattService 到所得 Promise 的映射。

    6. 返回以 null 为数据的成功。

  13. 如果 params["type"] 为 "remove":

    1. 如果 serviceMapping[uuid] 存在,则令 simulatedGattService 为 serviceMapping[uuid]。

    2. 否则,返回带有无效元素状态错误代码的错误。

    3. 从 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]] 中移除 simulatedGattService。

    4. 从 serviceMapping 中移除 uuid。

    5. 返回以 null 为数据的成功。

  14. 返回带有无效参数错误 代码的错误。

一个本地端可以通过发送以下 消息模拟添加 GATT 服务:
{
  "method": "bluetooth.simulateService",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "uuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "type": "add"
  }
}
一个本地端可以通过发送以下 消息模拟移除 GATT 服务:
{
  "method": "bluetooth.simulateService",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "uuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "type": "remove"
  }
}
12.2.3.9. bluetooth.simulateCharacteristic 命令
bluetooth.SimulateCharacteristic = (
   method: "bluetooth.simulateCharacteristic",
   params: bluetooth.SimulateCharacteristicParameters,
)

bluetooth.SimulateCharacteristicParameters = {
   context: text,
   address: text,
   serviceUuid: bluetooth.BluetoothUuid,
   characteristicUuid: bluetooth.BluetoothUuid,
   ? characteristicProperties: bluetooth.CharacteristicProperties,
   type: "add" / "remove"
}
带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 令 deviceAddress 为 params["address"]。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有无效参数错误代码的错误。

  6. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  7. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。

  8. 否则,返回带有无效参数错误 代码的错误。

  9. 令 simulatedDeviceInstance 为在 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth中获取表示 BluetoothDevice simulatedDevice 的结果。

  10. 令 serviceMapping 为 simulatedDevice 的模拟 GATT 服务映射。

  11. 令 serviceUuid 为 params["serviceUuid"]。

  12. 如果 serviceMapping[serviceUuid] 存在,则令 simulatedService 为 serviceMapping[serviceUuid]。

  13. 否则,返回带有无效参数错误 代码的错误。

  14. 令 characteristicMapping 为 simulatedService 的模拟 GATT 特征 映射。

  15. 令 characteristicUuid 为 params["characteristicUuid"]。

  16. 如果 params["type"] 为 "add":

    1. 如果 characteristicMapping[characteristicUuid] 存在,则返回带有 无效元素状态错误代码的错误。

    2. 如果 params["characteristicProperties"] 不存在,则返回带有无效参数错误代码的错误。

    3. 令 simulatedGattCharacteristicProperties 为新的模拟 GATT 特征 属性,并运行以下步骤:

      1. 令 properties 为 params["characteristicProperties"]。

      2. 如果 properties["broadcast"] 存在,并且 properties["broadcast"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Broadcast 位。

      3. 如果 properties["read"] 存在,并且 properties["read"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Read 位。

      4. 如果 properties["writeWithoutResponse"] 存在,并且 properties["writeWithoutResponse"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Write Without Response 位。

      5. 如果 properties["write"] 存在,并且 properties["write"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Write 位。

      6. 如果 properties["notify"] 存在,并且 properties["notify"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Notify 位。

      7. 如果 properties["indicate"] 存在,并且 properties["indicate"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Indicate 位。

      8. 如果 properties["authenticatedSignedWrites"] 存在,并且 properties["authenticatedSignedWrites"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Authenticated Signed Writes 位。

      9. 如果 properties["extendedProperties"] 存在,并且 properties["extendedProperties"] 为 true,则设置 simulatedGattCharacteristicProperties 的 Extended Properties 位。

    4. 令 simulatedGattCharacteristic 为一个新的模拟 GATT 特征。

    5. 将 simulatedGattCharacteristic 的 UUID 设置为 characteristicUuid。

    6. 将 simulatedGattCharacteristic 的特征属性设置为 simulatedGattCharacteristicProperties。

    7. 将 characteristicMapping[characteristicUuid] 设置为 simulatedGattCharacteristic。

    8. 创建一个表示 BluetoothRemoteGATTCharacteristic simulatedGattCharacteristic 的对象,并在 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]] 中添加从 simulatedGattCharacteristic 到所得 Promise 的映射。

    9. 返回以 null 为数据的成功。

  17. 如果 params["type"] 为 "remove":

    1. 如果 params["characteristicProperties"] 存在,则返回带有无效参数错误代码的错误。

    2. 如果 characteristicMapping[characteristicUuid] 存在,则令 simulatedGattCharacteristic 为 characteristicMapping[characteristicUuid]。

    3. 否则,返回带有无效元素状态错误代码的错误。

    4. 从 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]] 中移除 simulatedGattCharacteristic。

    5. 从 characteristicMapping 中移除 characteristicUuid。

    6. 返回以 null 为数据的成功。

  18. 返回带有无效参数错误 代码的错误。

一个本地端可以模拟添加具有读取、 写入和通知属性的 GATT 特征, 方法是发送以下消息:
{
  "method": "bluetooth.simulateCharacteristic",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb",
    "characteristicProperties": {
      "read": true,
      "write": true,
      "notify": true
    },
    "type": "add"
  }
}
一个本地端可以通过发送以下消息模拟移除 GATT 特征:
{
  "method": "bluetooth.simulateCharacteristic",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb",
    "type": "remove"
  }
}
12.2.3.10. bluetooth.simulateCharacteristicResponse 命令
bluetooth.SimulateCharacteristicResponse = (
   method: "bluetooth.simulateCharacteristicResponse",
   params: bluetooth.SimulateCharacteristicResponseParameters,
)

bluetooth.SimulateCharacteristicResponseParameters = {
   context: text,
   address: text,
   serviceUuid: bluetooth.BluetoothUuid,
   characteristicUuid: bluetooth.BluetoothUuid,
   type: "read" / "write" / "subscribe-to-notifications" / "unsubscribe-from-notifications",
   code: uint,
   ? data: [ * uint ]
}
带命令参数 params 的远程端步骤为:
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 令 deviceAddress 为 params["address"]。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有无效参数错误代码的错误。

  6. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  7. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。 否则,返回带有无效参数错误 代码的错误。

  8. 令 serviceMapping 为 simulatedDevice 的模拟 GATT 服务映射。

  9. 令 serviceUuid 为 params["serviceUuid"]。

  10. 如果 serviceMapping[serviceUuid] 存在,则令 simulatedService 为 serviceMapping[serviceUuid]。

  11. 否则,返回带有无效参数错误 代码的错误。

  12. 令 characteristicMapping 为 simulatedService 的模拟 GATT 特征 映射。

  13. 令 characteristicUuid 为 params["characteristicUuid"]。

  14. 如果 characteristicMapping[characteristicUuid] 存在,则令 simulatedGattCharacteristic 为 characteristicMapping[characteristicUuid]。

  15. 否则,返回带有无效元素状态错误 代码的错误。

  16. 令 simulatedDeviceInstance 为在 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth中获取表示 BluetoothDevice simulatedDevice 的结果。

  17. 令 promise 为 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]][simulatedGattCharacteristic]。

  18. 兑现后,promise 以 characteristic 兑现时,运行以下步骤:

    1. 如果 params["type"] 为 "read", 则运行以下步骤:

      1. 如果 characteristic.[[automatedCharacteristicReadResponse]] 为 expected, 则将 characteristic.[[automatedCharacteristicReadResponse]] 设置为 params["code"] 并将 characteristic.[[automatedCharacteristicReadResponseData]] 设置为 params["data"] 所持字节的副本。

      2. 否则,返回带有无效元素状态错误代码的错误。

    2. 如果 params["type"] 为 "write", 则运行以下步骤:

      1. 如果 characteristic.[[automatedCharacteristicWriteResponse]] 为 expected, 则将 characteristic.[[automatedCharacteristicWriteResponse]] 设置为 params["code"]。

      2. 否则,返回带有无效元素状态错误代码的错误。

    3. 如果 params["type"] 为 "subscribe-to-notifications", 则运行以下步骤:

      1. 如果 characteristic.[[automatedCharacteristicSubscribeToNotificationsResponse]] 为 expected, 则将 characteristic.[[automatedCharacteristicSubscribeToNotificationsResponse]] 设置为 params["code"]。

      2. 否则,返回带有无效元素状态错误代码的错误。

    4. 如果 params["type"] 为 "unsubscribe-from-notifications", 则运行以下步骤:

      1. 如果 characteristic.[[automatedCharacteristicUnsubscribeFromNotificationsResponse]] 为 expected, 则将 characteristic.[[automatedCharacteristicUnsubscribeFromNotificationsResponse]] 设置为 params["code"]。

      2. 否则,返回带有无效元素状态错误代码的错误。

    5. 否则,返回带有无效参数错误代码的错误。

一个本地端可以通过发送以下消息模拟特征读取操作带有数据的成功响应(根据 错误响应,错误代码为 0x00):
{
  "method": "bluetooth.simulateCharacteristicResponse",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb",
    "type": "read",
    "code": 0,
    "data": [1, 2]
  }
}
12.2.3.11. bluetooth.simulateDescriptor 命令
bluetooth.SimulateDescriptor = (
   method: "bluetooth.simulateDescriptor",
   params: bluetooth.SimulateDescriptorParameters,
)

bluetooth.SimulateDescriptorParameters = {
   context: text,
   address: text,
   serviceUuid: bluetooth.BluetoothUuid,
   characteristicUuid: bluetooth.BluetoothUuid,
   descriptorUuid: bluetooth.BluetoothUuid,
   type: "add" / "remove"
}
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 令 deviceAddress 为 params["address"]。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有无效参数错误代码的错误。

  6. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  7. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。

  8. 否则,返回带有无效参数错误 代码的错误。

  9. 令 simulatedDeviceInstance 为在 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth中获取表示 BluetoothDevice simulatedDevice 的结果。

  10. 令 serviceMapping 为 simulatedDevice 的模拟 GATT 服务映射。

  11. 令 serviceUuid 为 params["serviceUuid"]。

  12. 如果 serviceMapping[serviceUuid] 存在,则令 simulatedService 为 serviceMapping[serviceUuid]。

  13. 否则,返回带有无效参数错误 代码的错误。

  14. 令 characteristicMapping 为 simulatedService 的模拟 GATT 特征 映射。

  15. 令 characteristicUuid 为 params["characteristicUuid"]。

  16. 如果 characteristicMapping[characteristicUuid] 存在,则令 simulatedCharacteristic 为 characteristicMapping[characteristicUuid]。

  17. 否则,返回带有无效参数错误 代码的错误。

  18. 令 descriptorMapping 为 simulatedCharacteristic 的模拟 GATT 描述符映射。

  19. 令 descriptorUuid 为 params["descriptorUuid"]。

  20. 如果 params["type"] 为 "add":

    1. 如果 descriptorMapping[descriptorUuid] 存在,则返回带有 无效元素状态错误代码的错误。

    2. 令 simulatedGattDescriptor 为一个新的模拟 GATT 描述符。

    3. 将 simulatedGattDescriptor 的 UUID 设置为 descriptorUuid。

    4. 将 descriptorMapping[descriptorUuid] 设置为 simulatedGattDescriptor。

    5. 创建一个表示 BluetoothRemoteGATTDescriptor simulatedGattDescriptor 的对象,并在 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]] 中添加从 simulatedGattDescriptor 到所得 Promise 的映射。

    6. 返回以 null 为数据的成功。

  21. 如果 params["type"] 为 "remove":

    1. 如果 descriptorMapping[descriptorUuid] 存在,则令 simulatedGattDescriptor 为 descriptorMapping[descriptorUuid]。

    2. 否则,返回带有无效元素状态错误代码的错误。

    3. 从 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]] 中移除 simulatedGattDescriptor。

    4. 从 descriptorMapping 中移除 descriptorUuid。

    5. 返回以 null 为数据的成功。

  22. 返回带有无效参数错误 代码的错误。

一个本地端可以通过发送以下 消息模拟添加 GATT 描述符:
{
  "method": "bluetooth.simulateDescriptor",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb",
    "descriptorUuid": "00002901-0000-1000-8000-00805f9b34fb",
    "type": "add"
  }
}
一个本地端可以通过发送以下 消息模拟移除 GATT 描述符:
{
  "method": "bluetooth.simulateDescriptor",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb",
    "descriptorUuid": "00002901-0000-1000-8000-00805f9b34fb",
    "type": "remove"
  }
}
12.2.3.12. bluetooth.simulateDescriptorResponse 命令
bluetooth.SimulateDescriptorResponse = (
   method: "bluetooth.simulateDescriptorResponse",
   params: bluetooth.SimulateDescriptorResponseParameters,
)

bluetooth.SimulateDescriptorResponseParameters = {
   context: text,
   address: text,
   serviceUuid: bluetooth.BluetoothUuid,
   characteristicUuid: bluetooth.BluetoothUuid,
   descriptorUuid: bluetooth.BluetoothUuid,
   type: "read" / "write",
   code: uint,
   ? data: [ * uint ]
}
  1. 令 contextId 为 params["context"]。

  2. 令 navigable 为使用 contextId 获取可导航对象的尝试结果。

  3. 令 deviceAddress 为 params["address"]。

  4. 令 simulatedBluetoothAdapter 为 navigable 的模拟 蓝牙适配器。

  5. 如果 simulatedBluetoothAdapter 为空,则返回带有无效参数错误代码的错误。

  6. 令 deviceMapping 为 simulatedBluetoothAdapter 的模拟蓝牙设备映射。

  7. 如果 deviceMapping[deviceAddress] 存在,则令 simulatedDevice 为 deviceMapping[deviceAddress]。 否则,返回带有无效参数错误 代码的错误。

  8. 令 serviceMapping 为 simulatedDevice 的模拟 GATT 服务映射。

  9. 令 serviceUuid 为 params["serviceUuid"]。

  10. 如果 serviceMapping[serviceUuid] 存在,则令 simulatedService 为 serviceMapping[serviceUuid]。

  11. 否则,返回带有无效参数错误 代码的错误。

  12. 令 characteristicMapping 为 simulatedService 的模拟 GATT 特征 映射。

  13. 令 characteristicUuid 为 params["characteristicUuid"]。

  14. 如果 characteristicMapping[characteristicUuid] 存在,则令 simulatedCharacteristic 为 characteristicMapping[characteristicUuid]。

  15. 否则,返回带有无效元素状态错误 代码的错误。

  16. 令 descriptorMapping 为 simulatedCharacteristic 的模拟 GATT 描述符映射。

  17. 令 descriptorUuid 为 params["descriptorUuid"]。

  18. 如果 descriptorMapping[descriptorUuid] 存在,则令 simulatedDescriptor 为 descriptorMapping[descriptorUuid]。

  19. 否则,返回带有无效元素状态错误 代码的错误。

  20. 令 simulatedDeviceInstance 为在 navigable 的活动窗口的关联 Navigator的 关联 Bluetooth中获取表示 BluetoothDevice simulatedDevice 的结果。

  21. 令 promise 为 simulatedDeviceInstance.[[context]].[[attributeInstanceMap]][simulatedDescriptor]。

  22. 兑现后,promise 以 descriptor 兑现时,运行以下步骤:

    1. 如果 params["type"] 为 "read", 则运行以下步骤:

      1. 如果 descriptor.[[automatedDescriptorReadResponse]] 为 expected, 则将 descriptor.[[automatedDescriptorReadResponse]] 设置为 params["code"] 并将 descriptor.[[automatedDescriptorReadResponseData]] 设置为 params["data"] 所持字节的副本。

      2. 否则,返回带有无效元素状态错误代码的错误。

    2. 如果 params["type"] 为 "write", 则运行以下步骤:

      1. 如果 characteristic.[[automatedDescriptorWriteResponse]] 为 expected, 则将 characteristic.[[automatedDescriptorWriteResponse]] 设置为 params["code"]。

      2. 否则,返回带有无效元素状态错误代码的错误。

    3. 否则,返回带有无效参数错误代码的错误。

一个本地端可以通过发送以下消息模拟描述符读取操作带有数据的成功响应(根据 错误响应,错误代码为 0x00):
{
  "method": "bluetooth.simulateDescriptorResponse",
  "params": {
    "context": "cxt-d03fdd81",
    "address": "09:09:09:09:09:09",
    "serviceUuid": "0000180d-0000-1000-8000-00805f9b34fb",
    "characteristicUuid": "00002a21-0000-1000-8000-00805f9b34fb",
    "descriptorUuid": "00002901-0000-1000-8000-00805f9b34fb",
    "type": "read",
    "code": 0,
    "data": [1, 2]
  }
}

12.2.4. 事件

BluetoothEvent = (
  bluetooth.RequestDevicePromptUpdated //
  bluetooth.GattConnectionAttempted
)
12.2.4.1. bluetooth.requestDevicePromptUpdated 事件
bluetooth.RequestDevicePromptUpdated = (
   method: "bluetooth.requestDevicePromptUpdated",
   params: bluetooth.RequestDevicePromptUpdatedParameters
)

bluetooth.RequestDevicePromptUpdatedParameters = {
   context: text,
   prompt: bluetooth.RequestDevicePrompt,
   devices: [* bluetooth.RequestDeviceInfo],
}
给定一个可导航对象 navigable、一个字符串 promptId 和一个 集合 devices(由蓝牙设备组成),要触发 提示已更新事件:
  1. 令 navigableId 为 navigable 的可导航对象 ID。

  2. 令 prompt 为设备提示(promptId, devices)。

  3. 令 serialized devices 为使用 prompt 序列化提示 设备的结果。

  4. 将可导航对象到设备 提示的映射[navigableId] 设置为 prompt。

  5. 令 params 为一个与 bluetooth.RequestDevicePromptUpdatedParameters 产生式匹配的映射,其中 "context" 字段设置为 navigableId,"prompt" 字段设置为 promptId,并且 "devices" 字段设置为 serialized devices。

  6. 令 body 为一个与 bluetooth.RequestDevicePromptUpdated 产生式匹配的映射,其中 "params" 字段设置为 params。

  7. 令 relatedNavigables 为一个包含 navigable 的集合。

  8. 对于给定 "bluetooth.requestDevicePromptUpdated" 和 relatedNavigables 的启用了事件的会话 集合中的每个 session:

    1. 使用 session 和 body 发出事件。

12.2.4.2. bluetooth.gattConnectionAttempted 事件
bluetooth.GattConnectionAttempted = (
   method: "bluetooth.gattConnectionAttempted",
   params: bluetooth.GattConnectionAttemptedParameters
)

bluetooth.GattConnectionAttemptedParameters = {
   context: text,
   address: text
}
给定一个 可导航对象 navigable 和一个 BluetoothDevice device,要触发 GATT 连接尝试事件:
  1. 令 navigableId 为 navigable 的可导航对象 ID。

  2. 令 params 为一个与 bluetooth.GattConnectionAttemptedParameters 产生式匹配的映射,其中 "context" 字段设置为 navigableId,"address" 字段设置为 device.[[representedDevice]] 的 地址。

  3. 令 body 为一个与 bluetooth.GattConnectionAttempted 产生式匹配的映射,其中 "params" 字段设置为 params。

  4. 令 relatedNavigables 为一个包含 navigable 的集合。

  5. 对于给定 "bluetooth.gattConnectionAttempted" 和 relatedNavigables 的启用了事件的会话集合中的每个 session:

    1. 使用 session 和 body 发出事件。

12.2.4.3. bluetooth.characteristicEventGenerated 事件
bluetooth.CharacteristicEventGenerated = (
   method: "bluetooth.characteristicEventGenerated",
   params: bluetooth.CharacteristicEventGeneratedParameters
)

bluetooth.CharacteristicEventGeneratedParameters = {
  context: text,
  address: text,
  serviceUuid: bluetooth.BluetoothUuid,
  characteristicUuid: bluetooth.BluetoothUuid,
  type: "read" / "write-with-response" / "write-without-response" / "subscribe-to-notifications" / "unsubscribe-from-notifications",
  ? data: [ * uint ]
}

给定一个可导航对象 navigable、一个 BluetoothDevice device、一个 模拟 GATT 特征 characteristic、一个字符串 type,以及一个可选的字节序列 bytes,要触发模拟特征事件 :

  1. 令 navigableId 为 navigable 的可导航对象 ID。

  2. 令 params 为一个与 bluetooth.CharacteristicEventGeneratedParameters 产生式匹配的映射,并运行 以下步骤:

    1. 将 params["context"] 设置为 navigableId。

    2. 将 params["address"] 设置为 device.[[representedDevice]] 的 地址。

    3. 令 service 为包含 characteristic 的模拟 GATT 服务。

    4. 将 params["serviceUuid"] 设置为 service 的 UUID。

    5. 将 params["characteristicUuid"] 设置为 characteristic 的 UUID。

    6. 将 params["type"] 设置为 type。

    7. 如果 type 为 "write-with-response" 或 "write-without-response", 则运行以下步骤:

      1. 令 data 为一个空列表。

      2. 对于 bytes 中的每个 byte:

        1. 将 byte 的值追加到 data。

      3. 将 params["data"] 设置为 data。

  3. 令 body 为一个与 bluetooth.CharacteristicEventGenerated 产生式匹配的映射,其中 "params" 字段设置为 params。

  4. 令 relatedNavigables 为一个包含 navigable 的集合。

  5. 对于给定 "bluetooth.characteristicEventGenerated" 和 relatedNavigables 的启用了事件的会话集合中的每个 session:

    1. 使用 session 和 body 发出事件。

12.2.4.4. bluetooth.descriptorEventGenerated 事件
bluetooth.DescriptorEventGenerated = (
   method: "bluetooth.descriptorEventGenerated",
   params: bluetooth.DescriptorEventGeneratedParameters
)

bluetooth.DescriptorEventGeneratedParameters = {
  context: text,
  address: text,
  serviceUuid: bluetooth.BluetoothUuid,
  characteristicUuid: bluetooth.BluetoothUuid,
  descriptorUuid: bluetooth.BluetoothUuid,
  type: "read" / "write",
  ? data: [ * uint ]
}

给定一个可导航对象 navigable、一个 BluetoothDevice device、一个 模拟 GATT 描述符 descriptor、一个字符串 type,以及一个可选的字节序列 bytes,要触发模拟描述符事件 :

  1. 令 navigableId 为 navigable 的可导航对象 ID。

  2. 令 params 为一个与 bluetooth.DescriptorEventGeneratedParameters 产生式匹配的映射,并运行 以下步骤:

    1. 将 params["context"] 设置为 navigableId。

    2. 将 params["address"] 设置为 device.[[representedDevice]] 的 地址。

    3. 令 characteristic 为包含 descriptor 的模拟 GATT 特征。

    4. 令 service 为包含 characteristic 的模拟 GATT 服务。

    5. 将 params["serviceUuid"] 设置为 service 的 UUID。

    6. 将 params["characteristicUuid"] 设置为 characteristic 的 UUID。

    7. 将 params["descriptorUuid"] 设置为 descriptor 的 UUID。

    8. 将 params["type"] 设置为 type。

    9. 如果 type 为 "write", 则运行以下步骤:

      1. 令 data 为一个空列表。

      2. 对于 bytes 中的每个 byte:

        1. 将 byte 的值追加到 data。

      3. 将 params["data"] 设置为 data。

  3. 令 body 为一个与 bluetooth.DescriptorEventGenerated 产生式匹配的映射,其中 "params" 字段设置为 params。

  4. 令 relatedNavigables 为一个包含 navigable 的集合。

  5. 对于给定 "bluetooth.descriptorEventGenerated" 和 relatedNavigables 的启用了事件的会话集合中的每个 session:

    1. 使用 session 和 body 发出事件。

13. 术语和约定

本规范使用一些约定以及来自其他 规范的若干术语。本节列出这些内容并链接到其主要 定义。

当本规范中的算法使用在本规范或其他 规范中定义的名称时,该名称必须解析为其初始值,并忽略当前执行环境中 对该名称所做的任何更改。例如,当 requestDevice() 算法要求调用 Array.prototype.map.call(filter.services, BluetoothUUID.getService) 时, 这必须应用 [ECMAScript] 中定义的 Array.prototype.map 算法,其中 filter.services 作为其 this 参数, 而§ 7.1 标准化 UUID中为 BluetoothUUID.getService 定义的算法作为其 callbackfn 参数,而不论对 window、Array、 Array.prototype、Array.prototype.map、 Function、Function.prototype、 BluetoothUUID、BluetoothUUID.getService 或其他 对象做过任何修改。

本规范使用一种类似于 WebIDL 的 FrozenArray 的只读类型。

[BLUETOOTH42]
  1. 架构与术语概述
    1. 一般说明
      1. 低功耗蓝牙操作概述 (定义广播事件)
    2. 通信拓扑和操作
      1. 操作过程和模式
        1. BR/EDR 过程
          1. 查询(发现)过程
            1. 扩展查询 响应
  2. 核心系统包 [BR/EDR 控制器卷]
    1. 错误代码
      1. 错误代码概述
        1. 错误代码列表
    2. 主机控制器接口功能规范
      1. HCI 命令和事件
        1. 信息参数
          1. 读取 BD_ADDR 命令
        2. 状态参数
          1. 读取 RSSI 命令
  3. 核心系统包 [主机卷]
    1. 服务发现协议(SDP)规范
      1. 概述
        1. 搜索服务
          1. UUID (定义UUID 别名以及 根据 UUID 别名计算所表示的 128 位 UUID 的算法)
    2. 通用访问配置文件
      1. 配置文件概述
        1. 配置文件角色
          1. 在 LE 物理传输上运行时的角色
            1. 广播者 角色
            2. 观察者角色
            3. 外围设备角色
            4. 中心设备角色
      2. 用户界面方面
        1. 蓝牙参数的表示
          1. 蓝牙设备 名称(用户友好名称)
      3. 空闲模式过程 — BR/EDR 物理传输
        1. 设备发现过程
        2. BR/EDR 绑定过程
      4. 操作模式和过程 — LE 物理传输
        1. 广播模式和观察过程
          1. 观察 过程
        2. 发现模式和过程
          1. 通用 发现过程
          2. 名称发现 过程
        3. 连接模式和过程
        4. 绑定模式和过程
          1. LE 绑定 过程
      5. 安全方面 — LE 物理传输
        1. 隐私功能
        2. 随机设备地址
          1. 静态地址
          2. 私有地址
            1. 可解析 私有地址解析过程
      6. 广播数据和扫描响应数据格式 (定义AD 结构)
      7. 蓝牙设备要求
        1. 蓝牙设备地址(定义BD_ADDR)
          1. 蓝牙设备地址类型
            1. 公共 蓝牙地址
      8. 定义 (定义绑定)
    3. 属性协议(ATT)
      1. 协议要求
        1. 基本概念
          1. 属性类型
          2. 属性句柄
          3. 长属性 值
        2. 属性协议 PDU
          1. 错误处理
            1. 错误 响应
    4. 通用属性配置文件(GATT)
      1. 配置文件概述
        1. 配置和角色 (定义GATT 客户端和GATT 服务器)
        2. 配置文件基础, 定义ATT 承载
        3. 属性协议
          1. 属性缓存
        4. GATT 配置文件层次结构
          1. 服务
          2. 包含服务
          3. 特征
      2. 服务互操作性 要求
        1. 服务定义
        2. 特征定义
          1. 特征声明
            1. 特征 属性
          2. 特征描述符声明
            1. 特征 扩展属性
            2. 客户端 特征配置
      3. GATT 功能要求 — 定义 GATT 过程。
        1. 服务器配置
          1. 交换 MTU
        2. 主服务发现
          1. 发现所有主服务
          2. 按服务 UUID 发现 主服务
        3. 关系发现
          1. 查找包含服务
        4. 特征发现
          1. 发现服务的所有特征
          2. 按 UUID 发现 特征
        5. 特征描述符 发现
          1. 发现所有 特征描述符
        6. 特征值读取
        7. 特征值写入
          1. 无响应 写入
          2. 写入 特征值
        8. 特征值 通知
        9. 特征值 指示
        10. 特征描述符
          1. 读取 特征描述符
          2. 读取 长特征描述符
          3. 写入 特征描述符
          4. 写入长 特征描述符
        11. 过程超时
      4. GAP 互操作性 要求
        1. BR/EDR GAP 互操作性要求
          1. 连接建立
        2. LE GAP 互操作性要求
          1. 连接建立
      5. 已定义的通用属性配置文件服务
        1. 服务已更改
    5. 安全管理器规范
      1. 安全管理器
        1. 低功耗蓝牙中的安全
          1. 密钥 和值的定义, 定义 身份解析密钥 (IRK)
  4. 核心系统包 [低功耗控制器卷]
    1. 链路层规范
      1. 一般说明
        1. 设备地址
          1. 公共设备 地址
          2. 随机设备地址
            1. 静态 设备地址
      2. 空中接口协议
        1. 非连接状态
          1. 扫描状态
            1. 被动 扫描
[BLUETOOTH-SUPPLEMENT6]
  1. 数据类型规范
    1. 数据类型定义和格式
      1. 服务 UUID 数据类型
      2. 本地名称数据类型
      3. 标志数据类型 (定义可发现模式标志)
      4. 制造商特定数据
      5. TX 功率级别
      6. 服务数据
      7. 外观

一致性

文档 约定

一致性要求通过 描述性断言 与 RFC 2119 术语的组合来表达。 本文档规范性部分中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、 “MAY”和“OPTIONAL” 应按照 RFC 2119 中的说明进行解释。 但是,为了可读性, 本规范中这些词并不都以全大写字母出现。

除明确标记为非规范性的章节、示例和注释之外, 本规范的所有文本均为规范性内容。[RFC2119]

本规范中的示例以“例如”一词引入, 或通过 class="example" 与规范性文本分隔, 如下所示:

这是一个信息性示例的例子。

信息性注释以“注”一词开头, 并通过 class="note" 与规范性文本分隔, 如下所示:

注:这是一个信息性注释。

索引

本规范定义的 术语

通过引用定义的 术语

参考文献

规范性参考文献

[BLUETOOTH-ASSIGNED]
分配编号。 现行标准。URL:https://www.bluetooth.com/specifications/assigned-numbers/
[BLUETOOTH-SUPPLEMENT6]
蓝牙核心规范版本 6 补充。2015 年 7 月 14 日。URL:https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=302735
[BLUETOOTH42]
蓝牙 规范版本 4.2。2014 年 12 月 2 日。URL:https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=286439
[DOM]
Anne van Kesteren。DOM 标准。现行标准。 URL:https://dom.spec.whatwg.org/
[ECMAScript]
ECMAScript 语言规范。URL:https://tc39.es/ecma262/multipage/
[ENCODING]
Anne van Kesteren。编码标准。现行 标准。URL:https://encoding.spec.whatwg.org/
[FINGERPRINTING-GUIDANCE]
Nick Doty;Tom Ritter。缓解 Web 规范中的浏览器 指纹识别。URL:https://w3c.github.io/fingerprinting-guidance/
[HTML]
Anne van Kesteren;等。HTML 标准。 现行标准。URL:https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren;Domenic Denicola。Infra 标准。现行标准。URL:https://infra.spec.whatwg.org/
[PAGE-VISIBILITY]
Jatinder Mann;Arvind Jain。页面可见性(第二 版)。2013 年 10 月 29 日。REC。URL:https://www.w3.org/TR/page-visibility/
[PERMISSIONS]
Marcos Caceres;Mike Taylor。权限。URL: https://w3c.github.io/permissions/
[PERMISSIONS-POLICY-1]
Ian Clelland。权限 策略。URL:https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner。用于 RFC 中 指示要求级别的关键词。1997 年 3 月。最佳当前实践。URL:https://datatracker.ietf.org/doc/html/rfc2119
[RFC4122]
P. Leach;M. Mealling;R. Salz。通用唯一 标识符(UUID)URN 命名空间。2005 年 7 月。提议标准。URL:https://www.rfc-editor.org/rfc/rfc4122
[RFC8610]
H. Birkholz;C. Vigano;C. Bormann。简明数据 定义语言(CDDL):用于表达简明二进制对象表示 (CBOR)和 JSON 数据结构的符号约定。2019 年 6 月。提议标准。URL:https://www.rfc-editor.org/rfc/rfc8610
[WEBDRIVER-BIDI]
James Graham;Alex Rudenko;Maksim Sadym。WebDriver BiDi。URL:https://w3c.github.io/webdriver-bidi/
[WEBDRIVER2]
Simon Stewart;David Burns。WebDriver。URL:https://w3c.github.io/webdriver/
[WEBHID]
WebHID API。社区组报告草案。URL:https://wicg.github.io/webhid/
[WEBIDL]
Edgar Chen;Timothy Gu。Web IDL 标准。现行 标准。URL:https://webidl.spec.whatwg.org/

非规范性参考文献

[CSP3]
Mike West;Antonio Sartori。内容安全策略 第 3 级。URL:https://w3c.github.io/webappsec-csp/

IDL 索引

dictionary BluetoothDataFilterInit {
  BufferSource dataPrefix;
  BufferSource mask;
};

dictionary BluetoothManufacturerDataFilterInit : BluetoothDataFilterInit {
  required [EnforceRange] unsigned short companyIdentifier;
};

dictionary BluetoothServiceDataFilterInit : BluetoothDataFilterInit {
  required BluetoothServiceUUID service;
};

dictionary BluetoothLEScanFilterInit {
  sequence<BluetoothServiceUUID> services;
  DOMString name;
  DOMString namePrefix;
  sequence<BluetoothManufacturerDataFilterInit> manufacturerData;
  sequence<BluetoothServiceDataFilterInit> serviceData;
};

dictionary RequestDeviceOptions {
  sequence<BluetoothLEScanFilterInit> filters;
  sequence<BluetoothLEScanFilterInit> exclusionFilters;
  sequence<BluetoothServiceUUID> optionalServices = [];
  sequence<unsigned short> optionalManufacturerData = [];
  boolean acceptAllDevices = false;
};

[Exposed=Window, SecureContext]
interface Bluetooth : EventTarget {
  Promise<boolean> getAvailability();
  attribute EventHandler onavailabilitychanged;
  [SameObject]
  readonly attribute BluetoothDevice? referringDevice;
  Promise<sequence<BluetoothDevice>> getDevices();
  Promise<BluetoothDevice> requestDevice(optional RequestDeviceOptions options = {});
};

Bluetooth includes BluetoothDeviceEventHandlers;
Bluetooth includes CharacteristicEventHandlers;
Bluetooth includes ServiceEventHandlers;

dictionary BluetoothPermissionDescriptor : PermissionDescriptor {
  DOMString deviceId;
  // These match RequestDeviceOptions.
  sequence<BluetoothLEScanFilterInit> filters;
  sequence<BluetoothServiceUUID> optionalServices = [];
  sequence<unsigned short> optionalManufacturerData = [];
  boolean acceptAllDevices = false;
};

dictionary AllowedBluetoothDevice {
  required DOMString deviceId;
  required boolean mayUseGATT;
  // An allowedServices of "all" means all services are allowed.
  required (DOMString or sequence<UUID>) allowedServices;
  required sequence<unsigned short> allowedManufacturerData;
};
dictionary BluetoothPermissionStorage {
  required sequence<AllowedBluetoothDevice> allowedDevices;
};

[Exposed=Window]
interface BluetoothPermissionResult : PermissionStatus {
  attribute FrozenArray<BluetoothDevice> devices;
};

[
  Exposed=Window,
  SecureContext
]
interface ValueEvent : Event {
  constructor(DOMString type, optional ValueEventInit initDict = {});
  readonly attribute any value;
};

dictionary ValueEventInit : EventInit {
  any value = null;
};

[Exposed=Window, SecureContext]
interface BluetoothDevice : EventTarget {
  readonly attribute DOMString id;
  readonly attribute DOMString? name;
  readonly attribute BluetoothRemoteGATTServer? gatt;

  Promise<undefined> forget();
  Promise<undefined> watchAdvertisements(
      optional WatchAdvertisementsOptions options = {});
  readonly attribute boolean watchingAdvertisements;
};
BluetoothDevice includes BluetoothDeviceEventHandlers;
BluetoothDevice includes CharacteristicEventHandlers;
BluetoothDevice includes ServiceEventHandlers;

dictionary WatchAdvertisementsOptions {
  AbortSignal signal;
};

[Exposed=Window, SecureContext]
interface BluetoothManufacturerDataMap {
  readonly maplike<unsigned short, DataView>;
};
[Exposed=Window, SecureContext]
interface BluetoothServiceDataMap {
  readonly maplike<UUID, DataView>;
};
[
  Exposed=Window,
  SecureContext
]
interface BluetoothAdvertisingEvent : Event {
  constructor(DOMString type, BluetoothAdvertisingEventInit init);
  [SameObject]
  readonly attribute BluetoothDevice device;
  readonly attribute FrozenArray<UUID> uuids;
  readonly attribute DOMString? name;
  readonly attribute unsigned short? appearance;
  readonly attribute byte? txPower;
  readonly attribute byte? rssi;
  [SameObject]
  readonly attribute BluetoothManufacturerDataMap manufacturerData;
  [SameObject]
  readonly attribute BluetoothServiceDataMap serviceData;
};
dictionary BluetoothAdvertisingEventInit : EventInit {
  required BluetoothDevice device;
  sequence<(DOMString or unsigned long)> uuids;
  DOMString name;
  unsigned short appearance;
  byte txPower;
  byte rssi;
  BluetoothManufacturerDataMap manufacturerData;
  BluetoothServiceDataMap serviceData;
};

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTServer {
  [SameObject]
  readonly attribute BluetoothDevice device;
  readonly attribute boolean connected;
  Promise<BluetoothRemoteGATTServer> connect();
  undefined disconnect();
  Promise<BluetoothRemoteGATTService> getPrimaryService(BluetoothServiceUUID service);
  Promise<sequence<BluetoothRemoteGATTService>>
    getPrimaryServices(optional BluetoothServiceUUID service);
};

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTService : EventTarget {
  [SameObject]
  readonly attribute BluetoothDevice device;
  readonly attribute UUID uuid;
  readonly attribute boolean isPrimary;
  Promise<BluetoothRemoteGATTCharacteristic>
    getCharacteristic(BluetoothCharacteristicUUID characteristic);
  Promise<sequence<BluetoothRemoteGATTCharacteristic>>
    getCharacteristics(optional BluetoothCharacteristicUUID characteristic);
  Promise<BluetoothRemoteGATTService>
    getIncludedService(BluetoothServiceUUID service);
  Promise<sequence<BluetoothRemoteGATTService>>
    getIncludedServices(optional BluetoothServiceUUID service);
};
BluetoothRemoteGATTService includes CharacteristicEventHandlers;
BluetoothRemoteGATTService includes ServiceEventHandlers;

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTCharacteristic : EventTarget {
  [SameObject]
  readonly attribute BluetoothRemoteGATTService service;
  readonly attribute UUID uuid;
  readonly attribute BluetoothCharacteristicProperties properties;
  readonly attribute DataView? value;
  Promise<BluetoothRemoteGATTDescriptor> getDescriptor(BluetoothDescriptorUUID descriptor);
  Promise<sequence<BluetoothRemoteGATTDescriptor>>
    getDescriptors(optional BluetoothDescriptorUUID descriptor);
  Promise<DataView> readValue();
  Promise<undefined> writeValue(BufferSource value);
  Promise<undefined> writeValueWithResponse(BufferSource value);
  Promise<undefined> writeValueWithoutResponse(BufferSource value);
  Promise<BluetoothRemoteGATTCharacteristic> startNotifications();
  Promise<BluetoothRemoteGATTCharacteristic> stopNotifications();
};
BluetoothRemoteGATTCharacteristic includes CharacteristicEventHandlers;

[Exposed=Window, SecureContext]
interface BluetoothCharacteristicProperties {
  readonly attribute boolean broadcast;
  readonly attribute boolean read;
  readonly attribute boolean writeWithoutResponse;
  readonly attribute boolean write;
  readonly attribute boolean notify;
  readonly attribute boolean indicate;
  readonly attribute boolean authenticatedSignedWrites;
  readonly attribute boolean reliableWrite;
  readonly attribute boolean writableAuxiliaries;
};

[Exposed=Window, SecureContext]
interface BluetoothRemoteGATTDescriptor {
  [SameObject]
  readonly attribute BluetoothRemoteGATTCharacteristic characteristic;
  readonly attribute UUID uuid;
  readonly attribute DataView? value;
  Promise<DataView> readValue();
  Promise<undefined> writeValue(BufferSource value);
};

[SecureContext]
interface mixin CharacteristicEventHandlers {
  attribute EventHandler oncharacteristicvaluechanged;
};

[SecureContext]
interface mixin BluetoothDeviceEventHandlers {
  attribute EventHandler onadvertisementreceived;
  attribute EventHandler ongattserverdisconnected;
};

[SecureContext]
interface mixin ServiceEventHandlers {
  attribute EventHandler onserviceadded;
  attribute EventHandler onservicechanged;
  attribute EventHandler onserviceremoved;
};

typedef DOMString UUID;

[Exposed=Window]
interface BluetoothUUID {
  static UUID getService((DOMString or unsigned long) name);
  static UUID getCharacteristic((DOMString or unsigned long) name);
  static UUID getDescriptor((DOMString or unsigned long) name);

  static UUID canonicalUUID([EnforceRange] unsigned long alias);
};

typedef (DOMString or unsigned long) BluetoothServiceUUID;
typedef (DOMString or unsigned long) BluetoothCharacteristicUUID;
typedef (DOMString or unsigned long) BluetoothDescriptorUUID;

[SecureContext]
partial interface Navigator {
  [SameObject]
  readonly attribute Bluetooth bluetooth;
};

CDDL 索引

问题索引

请参见§ 3 隐私考虑一节。[问题 #575] ↵
实际上,在提示打开期间,设备列表会动态更新。 当前的规范文本没有反映这一点,但此 事件可能会使用相同的 promptId 和 最新的设备列表多次发出。参见 https://github.com/WebBluetoothCG/web-bluetooth/issues/621。↵
TODO:确定具体的时间长度。↵
支持异步设备发现。↵
被动扫描和隐私功能都可避免 泄露唯一且不可变的设备 ID。我们应该要求 UA 使用 其中一种方式,但似乎没有任何 OS API 暴露这两者中的任意一种。蓝牙还 使得被动扫描难以使用,因为它并不 要求 中心设备支持观察过程。↵
所有形式的 BR/EDR 查询/发现似乎都会泄露唯一且 不可变的设备地址。↵
我们需要一种方式,让站点可以注册,以便在感兴趣的 设备进入范围时接收事件。↵
这种通用事件类型应属于 [HTML] 或 [DOM],而不是这里。↵
实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化 和/或为用户提供重试失败操作的方式。[问题 #188] ↵
实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化和/或为 用户提供重试失败操作的方式。[问题 #188] ↵
实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化 和/或为用户提供重试失败操作的方式。[问题 #188] ↵
此方法仅用于向后兼容。新的实现 不应实现此方法。[问题 #238] ↵
实现可能能够避免此 NetworkError, 但 目前站点需要将对此 API 的使用串行化和/或为用户提供 重试失败操作的方式。[问题 #188] ↵
特征扩展 属性并未明确说明 对于给定特征,扩展属性是否不可变。 如果它们不可变,则应允许 UA 缓存它们。↵
实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化 和/或为用户提供重试失败操作的方式。[问题 #188] ↵
实现可能能够避免此 NetworkError, 但目前站点需要将对此 API 的使用串行化 和/或为用户提供重试失败操作的方式。[问题 #188] ↵
CDDL 片段使用 "text" 类型,而不是 "browsingContext.BrowsingContext",以允许独立地以编程方式 处理 CDDL 片段。目前无法 引用其他模块。↵
将数据插入另一个算法的变量中并没有明确定义。扫描设备算法需要定义 异步设备发现,以便与实现相匹配。↵
⚠MDN

Bluetooth/availabilitychanged_event

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Bluetooth/availabilitychanged_event

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for AndroidNoneAndroid WebView?Samsung Internet?Opera Mobile?
⚠MDN

Bluetooth/getAvailability

In only one current engine.

FirefoxNoneSafariNoneChrome78+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

Bluetooth/getDevices

In only one current engine.

FirefoxNoneSafariNoneChrome🔰 85+
Opera?Edge🔰 85+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
⚠MDN

Bluetooth/requestDevice

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

Bluetooth

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android56+Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/authenticatedSignedWrites

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/broadcast

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/indicate

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/notify

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/read

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/reliableWrite

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/writableAuxiliaries

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/write

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties/writeWithoutResponse

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothCharacteristicProperties

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android56+Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothDevice/gatt

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothDevice/id

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothDevice/name

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothDevice

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android56+Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/getDescriptor

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/getDescriptors

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/properties

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/readValue

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/service

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/startNotifications

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/stopNotifications

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/uuid

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/value

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/writeValueWithoutResponse

In only one current engine.

FirefoxNoneSafariNoneChrome85+
Opera?Edge85+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic/writeValueWithResponse

In only one current engine.

FirefoxNoneSafariNoneChrome85+
Opera?Edge85+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTCharacteristic

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android56+Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTDescriptor/characteristic

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile44+
⚠MDN

BluetoothRemoteGATTDescriptor/readValue

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile44+
⚠MDN

BluetoothRemoteGATTDescriptor/uuid

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile44+
⚠MDN

BluetoothRemoteGATTDescriptor/value

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile44+
⚠MDN

BluetoothRemoteGATTDescriptor/writeValue

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile44+
⚠MDN

BluetoothRemoteGATTDescriptor

In only one current engine.

FirefoxNoneSafariNoneChrome57+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android57+Android WebViewNoneSamsung Internet?Opera Mobile44+
⚠MDN

BluetoothRemoteGATTServer/connect

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTServer/connected

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTServer/device

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTServer/disconnect

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTServer/getPrimaryService

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTServer/getPrimaryServices

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTServer

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android56+Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTService/device

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTService/getCharacteristic

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTService/getCharacteristics

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTService/isPrimary

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTService/uuid

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothRemoteGATTService

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android56+Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothUUID/canonicalUUID_static

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothUUID/getCharacteristic_static

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothUUID/getDescriptor_static

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothUUID/getService_static

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)?IENone
Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
⚠MDN

BluetoothUUID

In only one current engine.

FirefoxNoneSafariNoneChrome56+
Opera?Edge79+
Edge (Legacy)NoneIENone
Firefox for Android?iOS Safari?Chrome for Android56+Android WebViewNoneSamsung Internet?Opera Mobile?