首页
/ Puppeteer 蓝牙模拟入门到源码:BluetoothEmulation.emulateAdapter() 完整解析

Puppeteer 蓝牙模拟入门到源码:BluetoothEmulation.emulateAdapter() 完整解析

2026-09-03 21:20:13作者:滕妙奇

本文围绕 Puppeteer 的 BluetoothEmulation.emulateAdapter() API 展开:它是 Web Bluetooth 模拟的前提调用,负责在浏览器中"点亮"一个虚拟蓝牙适配器。读完本篇,你能掌握该方法的签名与参数取值、它与 simulatePreconnectedPeripheral() / disableEmulation() 的完整调用链,并能从源码层面理解 CDP 与 WebDriver BiDi 两条协议路径下 emulateAdapter 的底层实现差异。

方法定位:为什么必须先 emulateAdapter

在浏览器中测试 Web Bluetooth API(如 navigator.bluetooth.requestDevice())时,无头(headless)环境通常没有真实蓝牙硬件,页面请求设备会直接失败。Puppeteer 的解决方案是为每个页面暴露一个 bluetooth 对象(BluetoothEmulation 接口实例),其 emulateAdapter() 方法对应 CDP 命令 BluetoothEmulation.enable(Web Bluetooth 规范中的 bluetooth.simulateAdapter 命令),用于在浏览器中创建一个状态可控的模拟适配器。

从源码结构看,Page 通过抽象 getter 暴露该接口,声明位于 Page.ts

abstract get bluetooth(): BluetoothEmulation;

emulateAdapter() 是整个蓝牙模拟链路的第一步:不先模拟出"存在且开机"的适配器,后续的外设(peripheral)模拟就没有作用对象。该接口整体标记为 @experimental,接口定义与 JSDoc 位于 BluetoothEmulation.ts

方法签名与参数说明

官方 API 文档给出的 TypeScript 签名如下(见 emulateAdapter API 文档):

interface BluetoothEmulation {
  emulateAdapter(state: AdapterState, leSupported?: boolean): Promise<void>;
}
参数 类型 说明
state AdapterState 期望的蓝牙适配器状态
leSupported boolean(可选) 标记该适配器是否支持低功耗蓝牙(BLE)。从两个协议实现源码看,省略时默认值为 true

返回值:Promise<void>,resolve 表示模拟适配器已在浏览器端生效。

AdapterState 是一个三值联合类型(AdapterState 文档),类型定义见 BluetoothEmulation.ts

export type AdapterState = 'absent' | 'powered-off' | 'powered-on';
取值 语义 页面可见效果
'absent' 适配器不存在 表现为设备没有蓝牙硬件
'powered-off' 适配器存在但已关闭 蓝牙开关处于关闭状态
'powered-on' 适配器存在且开机 可正常发起 Web Bluetooth 请求(最常用)

CDP 实现:先 disable 再 enable 的覆盖语义

CDP 协议路径的实现类是 CdpBluetoothEmulation,位于 cdp/BluetoothEmulation.ts

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.
  // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
  await this.#connection.send('BluetoothEmulation.disable');
  await this.#connection.send('BluetoothEmulation.enable', {
    state,
    leSupported,
  });
}

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

  1. 覆盖式语义:源码注释指出,Web Bluetooth 规范要求 simulateAdapter 命令"覆盖已有适配器"(step 6)。由于 CDP 没有原生的覆盖命令,Puppeteer 的做法是先发送 BluetoothEmulation.disable 清掉旧模拟,再发送 BluetoothEmulation.enable 携带 stateleSupported。因此连续两次调用 emulateAdapter() 不会产生叠加状态,后者完全取代前者。
  2. leSupported 默认 true:函数参数默认值 leSupported = trueBiDi 实现保持一致——即不显式传参时,模拟适配器默认声明支持低功耗蓝牙,这对使用 BLE 的 Web 应用测试是更贴近真实设备的默认行为。

BiDi 实现:按 context 隔离的 simulateAdapter

WebDriver BiDi 路径的实现类是 BidiBluetoothEmulation,位于 bidi/BluetoothEmulation.ts

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

与 CDP 版本"先 disable 再 enable"的两步走不同,BiDi 路径直接下发单条 bluetooth.simulateAdapter 命令,并在参数中携带 context(浏览器上下文 ID)。这提示从 BiDi 协议结构上,模拟作用域是以 browser context 为单位传递的——这也呼应了下面"隔离性说明"中讨论的作用域行为。

完整调用链:从点亮适配器到清理

BluetoothEmulation 接口共三个方法,emulateAdapter() 之后通常配合外设模拟与禁用模拟完成一次完整测试。官方文档给出的端到端示例如下(见 BluetoothEmulation 接口文档):

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

三步的职责划分:

步骤 方法 作用 底层命令(CDP / BiDi)
1. 点亮适配器 emulateAdapter('powered-on') 创建开机状态的模拟适配器 BluetoothEmulation.disable + BluetoothEmulation.enable / bluetooth.simulateAdapter
2. 模拟已连接外设 simulatePreconnectedPeripheral() 向页面提供一台"已连接"的虚拟设备(见 PreconnectedPeripheral 文档 BluetoothEmulation.simulatePreconnectedPeripheral / bluetooth.simulatePreconnectedPeripheral
3. 清理模拟 disableEmulation() 关闭模拟适配器,恢复真实环境行为 BluetoothEmulation.disable / bluetooth.disableSimulation

第 2 步的 PreconnectedPeripheral 对象结构定义在 BluetoothEmulation.tsaddress(设备地址)、name(设备名)、manufacturerData(厂商数据数组,key 为蓝牙 SIG 分配的公司标识符、data 为 base64 字符串)、knownServiceUuids(已知服务 UUID 列表)。这些字段会被原样透传给浏览器端的模拟外设(见 CDP 实现)。

关于 disableEmulation()disableEmulation API 文档):它返回 Promise<void>,CDP 路径下仅发送单条 BluetoothEmulation.disable实现)。建议在 afterEach 等钩子中调用它,避免模拟状态在测试用例之间残留。

隔离性说明:模拟状态绑定在 browser context 上

BluetoothEmulation 接口文档 中明确记录了一个重要的行为限制:Web Bluetooth 规范要求模拟适配器按顶层可导航对象(top-level navigable)隔离,但当前 Chromium 的蓝牙模拟实现是绑定在 browser context 而非页面上的。这意味着:

  • 同一个 browserContext 内打开的多个页面,共享同一套蓝牙模拟状态;
  • 在一个页面上执行 emulateAdapter()simulatePreconnectedPeripheral(),会直接影响该 context 内其他页面的可见蓝牙环境(相互干扰);
  • 测试多个"蓝牙场景"时,可以推断更稳妥的做法是为不同场景分别创建独立的 browser context,或严格按"模拟 → 断言 → disableEmulation() 清理"的单场景流程执行。

BiDi 实现中 emulateAdapter 携带 context ID 下发命令,与上述"按 context 隔离"的作用域描述相吻合。

测试用例印证

仓库内的蓝牙模拟测试套件位于 bluetooth-emulation.test.ts,其中验证了 emulateAdapter('powered-on') 之后页面中 navigator.bluetooth 相关 API 的行为变化:

// test/src/bluetooth-emulation.test.ts
await page.bluetooth.emulateAdapter('powered-on');

该文件同时覆盖了"点亮适配器"与"外设模拟"两类断言,可作为编写自己 Web Bluetooth 测试时的参考起点。

适用前提与小结

  • 浏览器支持:蓝牙模拟依赖 Chromium 侧实现(CDP 或 BiDi 命令),Firefox 等无对应命令的引擎不可用;接口整体处于 @experimental 阶段,后续版本签名可能调整。
  • 调用顺序emulateAdapter() 是前置步骤,leSupported 省略时默认 true;重复调用会以"先禁用再启用"的方式覆盖旧状态。
  • 清理:测试结束后调用 disableEmulation() 恢复真实行为。

至此,emulateAdapter() 的用法可以概括为一条链路:emulateAdapter('powered-on') 点亮适配器 → simulatePreconnectedPeripheral() 提供虚拟外设 → 断言页面行为 → disableEmulation() 清理,而 CDP(cdp/BluetoothEmulation.ts)与 BiDi(bidi/BluetoothEmulation.ts)两套实现确保了同一套公共 API 在不同协议下的等价语义。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
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
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384