news 2026/9/8 15:47:48

Puppeteer `BluetoothEmulation.emulateAdapter()`:模拟蓝牙适配器以驱动 Web Bluetooth 仿真

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Puppeteer `BluetoothEmulation.emulateAdapter()`:模拟蓝牙适配器以驱动 Web Bluetooth 仿真

PuppeteerBluetoothEmulation.emulateAdapter():模拟蓝牙适配器以驱动 Web Bluetooth 仿真

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

本文围绕 Puppeteer 的page.bluetooth.emulateAdapter()方法展开:它对应 Web Bluetooth 规范中的bluetooth.simulateAdapter调试命令,是启用蓝牙仿真的前置条件。读完本文,你能掌握该方法的签名、参数取值(AdapterStateleSupported)、它在 CDP 与 BiDi 两种协议下的底层实现差异,以及如何结合simulatePreconnectedPeripheral()waitForDevicePrompt()完成一条完整的 Web Bluetooth 设备选择测试链路。

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

BluetoothEmulation接口暴露页面的蓝牙仿真能力,其中emulateAdapter()是三个方法中必须最先调用的一环——只有先把"虚拟蓝牙适配器"设置成指定状态,后续的simulatePreconnectedPeripheral()(模拟已连接外设)才能在页面上产生可感知的设备。

在 Page 抽象类 中,该方法通过page.bluetooth属性对外暴露:

// packages/puppeteer-core/src/api/Page.ts abstract get bluetooth(): BluetoothEmulation;

整个接口定义在 api/BluetoothEmulation.ts 中,并带有@experimental标记,说明它仍处于实验阶段,API 可能随版本演进。

方法签名与参数详解

文档给出的 TypeScript 签名为:

interface BluetoothEmulation { emulateAdapter(state: AdapterState, leSupported?: boolean): Promise<void>; }

参数:state(AdapterState)

state指定期望的蓝牙适配器状态,类型为 AdapterState,源码中是一个三值联合类型:

// packages/puppeteer-core/src/api/BluetoothEmulation.ts export type AdapterState = 'absent' | 'powered-off' | 'powered-on';
取值语义
'absent'设备上不存在蓝牙适配器
'powered-off'存在适配器但已关闭
'powered-on'适配器已开启,可被网页检测到

要测试依赖navigator.bluetooth的页面功能,通常使用'powered-on';要验证"用户机器没有蓝牙"时的降级 UI,则传入'absent''powered-off'

参数:leSupported(可选,boolean)

标记该适配器是否支持低功耗蓝牙(Bluetooth Low Energy, LE)。该参数是可选的;从源码实现看,CDP 与 BiDi 两种后端都将默认值处理为true

// packages/puppeteer-core/src/cdp/BluetoothEmulation.ts async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> { // ... }

也就是说,不显式传参时,模拟出的适配器默认支持 LE。如果你的被测页面依赖 LE 设备(如心率带、BLE 信标),可保持默认;若要模拟一个仅支持经典蓝牙的适配器,显式传入false

返回值Promise<void>,方法在浏览器确认状态切换完成后 resolve,调用方应await后再执行后续的蓝牙交互。

底层实现:CDP 与 BiDi 两条路径

Puppeteer 同时支持 CDP 和 WebDriver BiDi 两种协议,emulateAdapter()在两者下有各自实现,行为上却保持一致。

CDP 实现:先 disable 再 enable

CdpBluetoothEmulation 的实现值得注意:

// packages/puppeteer-core/src/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命令要求覆盖已存在的模拟适配器,因此在 CDP 层面必须分两步——先发送BluetoothEmulation.disable清掉旧的模拟状态,再发送BluetoothEmulation.enable携带{ state, leSupported }建立新状态。这解释了为什么emulateAdapter()是幂等且可反复调用的:每次调用都是"先卸载、再重装",而不是增量更新。

BiDi 实现:单条命令 + 浏览器上下文

BidiBluetoothEmulation 则直接发送单条bluetooth.simulateAdapter命令,并携带当前浏览器上下文 ID:

// packages/puppeteer-core/src/bidi/BluetoothEmulation.ts async emulateAdapter(state: AdapterState, leSupported = true): Promise<void> { await this.#session.send('bluetooth.simulateAdapter', { context: this.#contextId, state, leSupported, }); }

BiDi 后端将context显式作为命令参数传入,这是它与 CDP 实现结构上的主要差别。

需要注意的作用域限制

接口文档中的 Remarks 部分明确指出了一个隔离性限制:Web Bluetooth 规范要求模拟的适配器应按顶层可导航单元(top-level navigable)隔离,但目前 Chromium 的蓝牙仿真实现是绑定到浏览器上下文(browser context)而非页面的。这意味着同一 browser context 下的不同页面共享同一套蓝牙仿真状态,互相会相互干扰。

从 BidiBluetoothEmulation 的构造器看,BiDi 实现接收contextId并在每条命令中带上它,可以推断 BiDi 协议层是按 context 维度下发仿真命令的。基于此,从源码结构看,一个稳妥的实践是:对蓝牙仿真敏感的场景下,为需要不同蓝牙状态的测试使用独立的 browser context,避免跨页面状态污染。

完整实战:从 emulateAdapter 到设备选择

单独调用emulateAdapter()只会得到一个"开启的虚拟适配器",网页的navigator.bluetooth.requestDevice()此时仍无设备可选。典型的完整链路是三步:

  1. emulateAdapter('powered-on')—— 建立可用的虚拟适配器(本文主角);
  2. simulatePreconnectedPeripheral(...)—— 注入一台"已连接"的模拟外设,提供地址、名称、厂商数据和已知服务 UUID;
  3. disableEmulation()—— 测试结束后清除仿真状态。

接口文档给出的示例:

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

其中PreconnectedPeripheral的字段定义可参见 api/BluetoothEmulation.ts:address(MAC 地址)、namemanufacturerData(数组,每项含 Bluetooth SIG 公司标识key与 base64 编码的data)、knownServiceUuids

端到端验证:仓库测试中的真实用法

仓库的 bluetooth-emulation.test.ts 展示了该方法在生产级测试中的完整用法,值得直接借鉴:

// test/src/bluetooth-emulation.test.ts const state = setupSeparateTestBrowserHooks({ args: [ '--enable-features=WebBluetoothNewPermissionsBackend', '--enable-features=WebBluetooth', ], acceptInsecureCerts: true, }); it('can be selected', async function () { const {page, httpsServer} = state; 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( navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [], }), ); const devicePrompt = await devicePromptPromise; await devicePrompt.select(devicePrompt.devices[0]!); expect(await navigatorRequestDevicePromise).toEqual(DEVICE_NAME); });

这段测试揭示了几个关键前提,文档正文并未展开,实操时容易踩坑:

  • Feature flag:Chromium 需要以--enable-features=WebBluetoothNewPermissionsBackend--enable-features=WebBluetooth启动,Web Bluetooth API 才可用。Puppeteer 的launch()/connect()中通过args传入。
  • HTTPS 安全上下文:Web Bluetooth 仅在安全上下文可用,测试使用了httpsServer.EMPTY_PAGE(配合acceptInsecureCerts: true接受自签证书)。
  • 调用时序emulateAdapter()simulatePreconnectedPeripheral()都在触发requestDevice()之前完成,与"先建适配器、再放设备、后触发 UI"的链路一致。
  • 设备提示的接管:Puppeteer 提供page.waitForDevicePrompt()捕获设备选择提示,再通过devicePrompt.select(...)devicePrompt.cancel()程序化操作,分别验证"选中设备"与"取消后requestDevice()被 reject"两种分支。

模拟无蓝牙环境的写法

借助state参数的三态能力,同一接口也能模拟负面场景:

// 验证页面对"无蓝牙硬件"的降级表现 await page.bluetooth.emulateAdapter('absent'); // 或 await page.bluetooth.emulateAdapter('powered-off');

小结与适用边界

  • emulateAdapter(state, leSupported?)是 Web Bluetooth 仿真的入口,返回Promise<void>,必须await
  • state'absent' | 'powered-off' | 'powered-on'leSupported可选,两个后端默认均为true
  • CDP 路径通过"disable + enable"两条命令实现规范的"覆盖旧适配器"语义(见 cdp/BluetoothEmulation.ts);BiDi 路径单条bluetooth.simulateAdapter命令按 context 下发。
  • 注意仿真状态绑定 browser context 而非页面,同一 context 内多页面会共享/干扰蓝牙仿真状态。
  • 该方法标记为实验性(@experimental),且依赖 Chromium 的 Web Bluetooth feature flags 与 HTTPS 环境,BiDi/Firefox 一侧的可用性以当前仓库实现为准。

相关文档:BluetoothEmulation 接口、disableEmulation()、simulatePreconnectedPeripheral()。

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 15:45:14

BLE低功耗设计-第5章第2题-怎样在功耗和传输可靠性中权衡

蓝牙面试题解析:怎样在功耗和传输可靠性中权衡? 难度:⭐⭐⭐ 中等 | 场景:社招一面/二面、功率权衡 | 高频:🔥🔥🔥 标准答案 功耗与可靠性权衡靠动态功率控制(按 RSSI/链路质量调节):信号好/近距离降功率省电,信号差/远距离升功率保连接,非连接态降功率或关发射…

作者头像 李华
网站建设 2026/9/8 15:44:55

res-downloader:本地代理一开,资源嗅探下载变成勾选操作

res-downloader&#xff1a;本地代理一开&#xff0c;资源嗅探下载变成勾选操作 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader …

作者头像 李华
网站建设 2026/9/8 15:44:41

BLE低功耗设计-第5章第6题-如何实现射频占空比的动态优化

蓝牙面试题解析:如何实现射频占空比的动态优化? 难度:⭐⭐⭐⭐ 较难 | 场景:社招二面/三面、动态占空比 | 高频:🔥🔥🔥 标准答案 动态优化射频占空比靠实时采集状态(信号质量/数据量/功耗),通过软件算法或硬件控制动态调节广播/连接间隔与事件长度,在需要时增大占…

作者头像 李华
网站建设 2026/9/8 15:44:15

复合材料电击穿与电树枝:从随机失效到统计建模

做过一段时间高压绝缘材料实验的人&#xff0c;几乎都会被样品的击穿电压离散性搞得难受。复合材料中电击穿与电树枝现象研究&#xff0c;看标题挺学术&#xff0c;实际做得越深越像一个“结构决定命运”的故事。我最初被这个课题吸引&#xff0c;是因为同样厚度、同样配方的一…

作者头像 李华
网站建设 2026/9/8 15:40:57

Arduino并口直驱8路舵机控制板:寄存器操作实战与踩坑记录

最近在调试一台上位机与机械臂联动的项目&#xff0c;主控用的是Arduino Mega 2560&#xff0c;舵机一多就发现引脚不够用&#xff0c;看来看去把目光落在了8路舵机控制板上。研究过程中踩了不少坑&#xff0c;也搞明白了一些底层原理&#xff0c;特别是关于寄存器直接操作的细…

作者头像 李华