Puppeteer 蓝牙模拟入门到源码:BluetoothEmulation.emulateAdapter() 完整解析
本文围绕 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,
});
}
这里有两个值得注意的实现细节:
- 覆盖式语义:源码注释指出,Web Bluetooth 规范要求
simulateAdapter命令"覆盖已有适配器"(step 6)。由于 CDP 没有原生的覆盖命令,Puppeteer 的做法是先发送BluetoothEmulation.disable清掉旧模拟,再发送BluetoothEmulation.enable携带state与leSupported。因此连续两次调用emulateAdapter()不会产生叠加状态,后者完全取代前者。 leSupported默认true:函数参数默认值leSupported = true与 BiDi 实现保持一致——即不显式传参时,模拟适配器默认声明支持低功耗蓝牙,这对使用 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.ts:address(设备地址)、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 在不同协议下的等价语义。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00