首页
/ Puppeteer BluetoothEmulation 接口详解:page.bluetooth 模拟蓝牙适配器与外设的完整指南

Puppeteer BluetoothEmulation 接口详解:page.bluetooth 模拟蓝牙适配器与外设的完整指南

2026-09-05 10:22:23作者:魏侃纯Zoe

本文基于 Puppeteer 的 BluetoothEmulation 接口文档展开,系统讲解如何通过 page.bluetooth 模拟蓝牙适配器状态、注入预连接外设,并结合 CDP 与 WebDriver BiDi 两套底层实现、以及仓库内的端到端测试,说明其调用链路与使用限制。读完本文,你将能够理解 emulateAdaptersimulatePreconnectedPeripheraldisableEmulation 三个方法的确切语义,掌握配合 waitForDevicePrompt 完成 Web Bluetooth 设备请求自动化的完整流程,并明确该功能在 Chromium 中的隔离边界与实验性约束。

一、BluetoothEmulation 接口概述

BluetoothEmulation 是 Puppeteer 暴露的蓝牙模拟能力接口,官方文档描述其为 "Exposes the bluetooth emulation abilities"(暴露蓝牙模拟能力),通过 page.bluetooth 属性访问。接口签名如下:

export interface BluetoothEmulation

该接口定义了三个方法,全部标记为 Experimental(实验性):

方法 说明
disableEmulation() 禁用已模拟的蓝牙适配器。对应 Web Bluetooth 规范中的 bluetooth.disableSimulation 命令
emulateAdapter(state, leSupported) 模拟蓝牙适配器,是所有蓝牙模拟操作的前提。对应规范中的 bluetooth.simulateAdapter 命令
simulatePreconnectedPeripheral(preconnectedPeripheral) 模拟一个预连接的蓝牙外设。对应规范中的 bluetooth.simulatePreconnectedPeripheral 命令

由于三个方法均为实验性 API,接口在源码中同样以 @experimental 标注(见 接口定义文件)。这意味着 API 签名可能随版本演进调整,生产环境使用时应关注版本变更。

作用域限制:浏览器上下文级而非页面级

接口文档中最重要的 Remarks 指出:

Web Bluetooth specification requires the emulated adapters should be isolated per top-level navigable. However, at the moment Chromium's bluetooth emulation implementation is tight to the browser context, not the page. This means the bluetooth emulation exposed from different pages of the same browser context would interfere their states.

即规范要求模拟器按顶层可导航对象(页面)隔离,但当前 Chromium 的实现将模拟绑定在浏览器上下文(browser context) 层面。同一个浏览器上下文中的不同页面会互相干扰蓝牙模拟状态。

这一限制在 CDP 实现中有直接体现。从 CdpPage 构造函数 可以看到:

// Use browser context's connection, as current Bluetooth emulation in Chromium is
// implemented on the browser context level, and not tight to the specific tab.
this.#cdpBluetoothEmulation = new CdpBluetoothEmulation(
  this.#primaryTargetClient.connection(),
);

源码注释明确说明:CDP 蓝牙模拟命令通过浏览器上下文级别的连接(而非标签页会话)发送。因此编写测试时,若要避免状态串扰,应使用独立浏览器上下文(browser.createBrowserContext())隔离各用例。

二、核心类型定义

接口涉及三个公共类型,全部定义在 packages/puppeteer-core/src/api/BluetoothEmulation.ts

AdapterState:适配器状态

export type AdapterState = 'absent' | 'powered-off' | 'powered-on';

模拟的蓝牙适配器支持三种状态(详见 AdapterState 文档):

取值 含义
absent 设备不存在蓝牙适配器
powered-off 适配器存在但已关闭
powered-on 适配器存在且已开启(进行蓝牙模拟的前提)

BluetoothManufacturerData:厂商数据

export interface BluetoothManufacturerData {
  /**
   * The company identifier, as defined by the Bluetooth SIG.
   */
  key: number;
  /**
   * The manufacturer-specific data as a base64-encoded string.
   */
  data: string;
}
  • key:蓝牙 SIG 定义的公司标识符(company identifier);
  • data:厂商特定数据,必须以 base64 编码字符串传入。

类型定义见 BluetoothManufacturerData 文档

PreconnectedPeripheral:预连接外设

export interface PreconnectedPeripheral {
  address: string;
  name: string;
  manufacturerData: BluetoothManufacturerData[];
  knownServiceUuids: string[];
}

四个字段均有实际类型约束(见 PreconnectedPeripheral 文档):

字段 说明
address 外设蓝牙地址,如 '09:09:09:09:09:09'
name 外设名称,如 'SOME_NAME'
manufacturerData 厂商数据数组,每项含 key/data
knownServiceUuids 已知服务 UUID 列表,如 ['12345678-1234-5678-9abc-def123456789']

三、三个方法的签名与参数

emulateAdapter(state, leSupported)

interface BluetoothEmulation {
  emulateAdapter(state: AdapterState, leSupported?: boolean): Promise<void>;
}
  • stateAdapterState):期望的适配器状态;
  • leSupportedboolean,可选):标记该适配器是否支持低功耗蓝牙(LE)。从源码签名 emulateAdapter(state: AdapterState, leSupported = true) 可见,默认值为 true
  • 返回 Promise<void>

该方法是所有蓝牙模拟操作的前置条件:必须先让"适配器"处于开启状态,页面中的 navigator.bluetooth API 才能发现设备。

simulatePreconnectedPeripheral(preconnectedPeripheral)

interface BluetoothEmulation {
  simulatePreconnectedPeripheral(
    preconnectedPeripheral: PreconnectedPeripheral,
  ): Promise<void>;
}

调用后,该外设会以"已发现"状态出现在页面的设备选择提示中,可供 DeviceRequestPrompt.select() 选中。

disableEmulation()

interface BluetoothEmulation {
  disableEmulation(): Promise<void>;
}

无参数,返回 Promise<void>,用于结束模拟、恢复浏览器真实蓝牙状态,避免影响后续用例。

四、完整使用示例

文档给出的标准用法(与源码 JSDoc 中 @example 一致):

await page.bluetooth.emulateAdapter('powered-on');
await page.bluetooth.simulatePreconnectedPeripheral({
  address: '09:09:09:09:09:09',
  name: 'SOME_NAME',
  manufacturerData: [
    {
      key: 17,
      data: 'AP8BAX8=',
    },
  ],
  knownServiceUuids: ['12345678-1234-5678-9abc-def123456789'],
});
await page.bluetooth.disableEmulation();

调用顺序即完整生命周期:开启适配器 → 注入预连接外设 → 在页面中触发 navigator.bluetooth.requestDevice() 完成断言 → 关闭模拟。示例中 key: 17data: 'AP8BAX8=' 分别演示了公司标识符数字和 base64 编码数据两种取值形态。

五、底层实现:CDP 与 WebDriver BiDi 双通道

Puppeteer 对该接口提供了两套实现,均位于 packages/puppeteer-core/src/ 下:

CDP 实现:CdpBluetoothEmulation

CdpBluetoothEmulation 通过 Connection 直接发送 CDP 命令:

async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> {
  // Bluetooth spec requires overriding the existing adapter (step 6). From the CDP
  // perspective, it means disabling the emulation first.
  await this.#connection.send('BluetoothEmulation.disable');
  await this.#connection.send('BluetoothEmulation.enable', {
    state,
    leSupported,
  });
}

async disableEmulation(): Promise<void> {
  await this.#connection.send('BluetoothEmulation.disable');
}

async simulatePreconnectedPeripheral(
  preconnectedPeripheral: PreconnectedPeripheral,
): Promise<void> {
  await this.#connection.send(
    'BluetoothEmulation.simulatePreconnectedPeripheral',
    preconnectedPeripheral,
  );
}

两个值得注意的实现细节:

  1. emulateAdapter 会先发送 BluetoothEmulation.disable 再发送 enable。源码注释解释这是规范要求的行为——Web Bluetooth 规范的 simulateAdapter 命令第 6 步要求覆盖(override)已存在的适配器,因此在 CDP 层面需要先禁用再启用。这也意味着连续调用 emulateAdapter 是幂等安全的。
  2. 构造函数接收的是浏览器上下文级连接(见上文 CdpPage 中的构造位置),这解释了文档 Remarks 中"上下文级隔离"的限制来源。

BiDi 实现:BidiBluetoothEmulation

BidiBluetoothEmulation 面向 WebDriver BiDi 协议,所有命令都显式携带 context(上下文 ID),将模拟作用域限定在指定浏览器上下文:

async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> {
  await this.#session.send('bluetooth.simulateAdapter', {
    context: this.#contextId,
    state,
    leSupported,
  });
}

BiDi 版 simulatePreconnectedPeripheral 会显式解构外设对象,将 addressnamemanufacturerDataknownServiceUuids 逐字段展平后发送 bluetooth.simulatePreconnectedPeripheral 命令。对比两套实现可以看到:CDP 版整体透传 preconnectedPeripheral,BiDi 版按协议 schema 逐字段映射——接口抽象层(Page.bluetooth 抽象 getter)保证了用户代码在两种协议间无需改动。

六、端到端实战:配合 waitForDevicePrompt 完成设备选择

仓库测试 test/src/bluetooth-emulation.test.ts 展示了模拟蓝牙后与页面交互的完整闭环,其中包含几个文档示例未覆盖的关键前提:

1. 浏览器启动参数。测试通过 setupSeparateTestBrowserHooks 指定:

args: [
  '--enable-features=WebBluetoothNewPermissionsBackend',
  '--enable-features=WebBluetooth',
],
acceptInsecureCerts: true,

即需要 WebBluetoothNewPermissionsBackendWebBluetooth 两个 feature flag,并且页面必须运行在安全上下文(测试使用 httpsServer.EMPTY_PAGE)。

2. 标准交互流程。以"选择设备"用例为例:

await page.goto(httpsServer.EMPTY_PAGE);

await page.bluetooth.emulateAdapter('powered-on');
await page.bluetooth.simulatePreconnectedPeripheral(SIMULATED_PERIPHERAL);

const devicePromptPromise = page.waitForDevicePrompt();
const navigatorRequestDevicePromise = page.evaluate(
  triggerBluetoothDevicePrompt, // 内部调用 navigator.bluetooth.requestDevice
);

// 等待设备提示出现,然后选中模拟设备
const devicePrompt = await devicePromptPromise;
await devicePrompt.select(devicePrompt.devices[0]!);

// 断言:requestDevice 解析为模拟外设名称
expect(await navigatorRequestDevicePromise).toEqual(DEVICE_NAME);

其中 DeviceRequestPromptpage.waitForDevicePrompt() 返回(定义见 DeviceRequestPrompt 文档),核心成员有:

  • devicesreadonly):当前可选设备列表;
  • select(device):选中提示列表中的某个设备;
  • cancel():取消提示(测试用例验证了取消后 requestDevice 会 reject);
  • waitForDevice(filter, options):等待并解析第一个匹配过滤条件的设备。

典型写法是将 waitForDevicePrompt() 与触发请求的动作用 Promise.all 配对,例如点击页面上的"连接蓝牙"按钮:

const [devicePrompt] = Promise.all([
  page.waitForDevicePrompt(),
  page.click('#connect-bluetooth'),
]);
await devicePrompt.select(
  await devicePrompt.waitForDevice(({name}) => name.includes('My Device')),
);

Page 类对 waitForDevicePrompt 的注释特别提醒:该方法必须在设备请求发起之前调用,否则无法返回提示对象。

3. 模拟数据的复用。测试中的 SIMULATED_PERIPHERAL 常量与文档示例逐字段一致:地址 09:09:09:09:09:09、名称 SOME_NAME、厂商数据 {key: 17, data: 'AP8BAX8='}、服务 UUID 12345678-1234-5678-9abc-def123456789,可直接作为复制模板。

七、小结

BluetoothEmulation 接口以三个实验性方法提供了完整的 Web Bluetooth 模拟生命周期:用 emulateAdapter 设定 absent / powered-off / powered-on 三态适配器(LE 支持默认开启),用 simulatePreconnectedPeripheral 注入含地址、名称、base64 厂商数据与服务 UUID 的虚拟外设,再用 disableEmulation 收尾。理解它需要抓住三层事实:接口层定义于 packages/puppeteer-core/src/api/BluetoothEmulation.ts;实现层由 CDP 通道(先 disable 后 enable 覆盖旧适配器)与 BiDi 通道(按 context 限定作用域)分别落地;使用层则需配合 waitForDevicePrompt 与浏览器 feature flag 完成端到端断言。同时务必记住文档强调的约束——Chromium 当前的模拟绑定在浏览器上下文而非页面,同上下文多页面间的模拟状态会互相干扰,测试编排时宜用独立上下文隔离。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384