Puppeteer 详解 BluetoothManufacturerData:Web Bluetooth 模拟中的厂商数据接口
本篇围绕 Puppeteer API 文档中的 BluetoothManufacturerData 接口展开,讲解这一"模拟蓝牙外设厂商数据"类型的两个核心字段(key 与 data)的含义、取值来源,以及它如何被组合进 PreconnectedPeripheral,经由 page.bluetooth.simulatePreconnectedPeripheral() 下发到 Chrome(CDP)或 BiDi 会话,最终驱动 navigator.bluetooth.requestDevice() 设备选择提示框。读完本文,你将能够独立构造合法的蓝牙外设模拟配置,并理解其底层 CDP/BiDi 指令链路与浏览器上下文级别的状态限制。
接口定位:BluetoothManufacturerData 是什么
BluetoothManufacturerData 是一个用于模拟(模拟出的)蓝牙外设(peripheral)厂商数据的 TypeScript 接口。官方定义如下(见 BluetoothEmulation.ts 源码):
/**
* @public
* Represents the simulated bluetooth peripheral's manufacturer data.
*/
export interface BluetoothManufacturerData {
/**
* The company identifier, as defined by the
* {@link https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/|Bluetooth SIG}.
*/
key: number;
/**
* The manufacturer-specific data as a base64-encoded string.
*/
data: string;
}
在真实的 BLE(低功耗蓝牙)广播数据中,"厂商特定数据(Manufacturer Specific Data)"由两部分组成:一个 2 字节的公司标识符(Company Identifier),以及紧随其后的厂商自定义负载。Puppeteer 的该接口正是按这一结构建模:key 对应公司标识符,data 对应厂商自定义负载。整个接口属于 BluetoothEmulation 能力族(emulateAdapter / simulatePreconnectedPeripheral / disableEmulation),并在源码注释中标记为 @public 且 @experimental(实验性 API)。
属性详解:key 与 data
对应官方文档 puppeteer.bluetoothmanufacturerdata.md 中的属性表,完整继承其字段定义如下:
| 属性 | 修饰符 | 类型 | 说明 | 默认值 |
|---|---|---|---|---|
data |
string |
厂商特定数据,以 base64 编码的字符串表示 | ||
key |
number |
公司标识符,按 Bluetooth SIG 的 Assigned Numbers(Company Identifiers)注册表定义 |
key:Bluetooth SIG 公司标识符
key 必须取 Bluetooth SIG 官方"Assigned Numbers — Company Identifiers"注册表中的编号(例如注册表中 0x004C/76 对应 Apple、0x0005/5 对应 Intel 等公开条目,完整清单以注册表为准)。它是 number 类型,即十进制整数;测试用例 bluetooth-emulation.test.ts 中使用的示例值为 17。在 Web Bluetooth 语义下,页面代码可以通过 device.gatt 或广播数据过滤匹配该标识来识别特定厂商的外设,因此模拟时填写正确的 key 对行为保真很重要。
data:base64 编码的厂商负载
data 是厂商自定义负载的 base64 编码字符串,而非原始字节或十六进制串。这一点在写测试或模拟配置时容易踩坑:需要先把二进制负载编码为 base64 再传入。
以仓库测试与官方文档示例中反复出现的 'AP8BAX8=' 为例,其 base64 解码后恰好是 5 个字节:
'AP8BAX8=' -> 0x00 0xFF 0x01 0x01 0x5F
这是一个典型的小型厂商数据负载(末尾 0x5F 对应 ASCII 字符 "Some Name" 片段中常见的演示数据)。这提示读者:data 字段的长度与内容完全由被测 Web 应用对该厂商数据的解析逻辑决定,模拟时应以页面期望的字节序列为准,base64 只是传输编码形式。
使用位置:作为 PreconnectedPeripheral 的组成部分
BluetoothManufacturerData 并不是直接暴露给 page 的,而是作为 PreconnectedPeripheral(待模拟的蓝牙外设)的一个字段数组存在。完整的周边设备类型定义见 BluetoothEmulation.ts:
/**
* @public
* A bluetooth peripheral to be simulated.
*/
export interface PreconnectedPeripheral {
address: string;
name: string;
manufacturerData: BluetoothManufacturerData[];
knownServiceUuids: string[];
}
各字段的语义可参照 PreconnectedPeripheral 接口文档:
| 字段 | 类型 | 说明 |
|---|---|---|
address |
string |
模拟外设的蓝牙地址,示例为 '09:09:09:09:09:09' |
name |
string |
设备名称,示例为 'SOME_NAME' |
manufacturerData |
BluetoothManufacturerData[] |
一个或多个厂商数据条目,即本文主角接口 |
knownServiceUuids |
string[] |
该外设已知的 GATT 服务 UUID 列表 |
其中 manufacturerData 是数组类型,意味着同一台模拟外设可以携带多条不同公司标识(key)的厂商数据,逐条独立编码 data。
完整实战流程:从 emulateAdapter 到 disableEmulation
官方 BluetoothEmulation 文档 与 BluetoothEmulation.ts 源码注释中给出了同一套端到端示例,完整保留如下:
// 1. 先把模拟的蓝牙适配器置为"开机"状态(模拟的前提)
await page.bluetooth.emulateAdapter('powered-on');
// 2. 注入一台"已预连接"的模拟外设,携带厂商数据
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'],
});
// 3. 用完后关闭模拟,恢复真实行为
await page.bluetooth.disableEmulation();
关键说明:
-
emulateAdapter(state, leSupported?)是模拟的前置条件(Required for bluetooth simulations),对应 Web Bluetooth 规范的bluetooth.simulateAdapter命令。state的合法取值由AdapterState类型约束(源码定义):export type AdapterState = 'absent' | 'powered-off' | 'powered-on';第二参数
leSupported标记适配器是否支持低功耗蓝牙,两个实现中默认值均为true。 -
simulatePreconnectedPeripheral()对应规范中的bluetooth.simulatePreconnectedPeripheral(见方法文档),把上表四字段的外设"预连接"进模拟环境。 -
disableEmulation()对应bluetooth.disableSimulation,用于清理模拟状态。
这三个方法均挂载在 Page 实例上:从 Page.ts 源码结构看,Page 通过抽象访问器 abstract get bluetooth(): BluetoothEmulation 暴露该能力,CDP 与 BiDi 两套后端各自提供实现。
底层实现:CDP 与 BiDi 双后端的指令链路
Puppeteer 对 BluetoothManufacturerData 的消费并不在 JS 层做校验或转换,而是整体透传给浏览器协议。两个后端的实现可以对照阅读:
CDP 后端(Chromium DevTools Protocol)
CdpBluetoothEmulation 通过 Connection.send 发送 CDP 命令。值得注意的是 emulateAdapter 的实现细节:
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,
});
}
源码注释指出:Web Bluetooth 规范(bluetooth.simulateAdapter 命令的第 6 步)要求覆盖已存在的模拟适配器,因此在 CDP 侧必须先发 BluetoothEmulation.disable 再发 BluetoothEmulation.enable。而 simulatePreconnectedPeripheral 则是把整个 preconnectedPeripheral 对象(含 manufacturerData 数组)原样作为 BluetoothEmulation.simulatePreconnectedPeripheral 命令的参数发送——key 与 data 保持 JS 侧的类型不做任何变换。
BiDi 后端(WebDriver BiDi)
BidiBluetoothEmulation 发送的是 BiDi 域命令,且显式携带 context(浏览器上下文)标识:
await this.#session.send('bluetooth.simulatePreconnectedPeripheral', {
context: this.#contextId,
address: preconnectedPeripheral.address,
name: preconnectedPeripheral.name,
manufacturerData: preconnectedPeripheral.manufacturerData,
knownServiceUuids: preconnectedPeripheral.knownServiceUuids,
});
从源码结构看,两个后端对 manufacturerData 都是零加工透传,协议层面的 base64 编码与 key 取值合法性校验由浏览器端负责。这提示使用者:如果 data 不是合法 base64 或字段结构不完整,错误会在协议层/浏览器侧暴露,而不是在 Puppeteer 侧提前拦截。
测试佐证:厂商数据如何驱动真实设备选择提示
仓库内置的 bluetooth-emulation.test.ts 提供了该接口在真实浏览器中的完整验证链路,其模拟外设配置与官方示例完全一致:
const SIMULATED_PERIPHERAL = {
address: '09:09:09:09:09:09',
name: DEVICE_NAME, // 'SOME_NAME'
manufacturerData: [
{
key: 17,
data: 'AP8BAX8=',
},
],
knownServiceUuids: ['12345678-1234-5678-9abc-def123456789'],
};
测试的关键要点:
- 启动参数:通过
setupSeparateTestBrowserHooks传入--enable-features=WebBluetoothNewPermissionsBackend与--enable-features=WebBluetooth,并设置acceptInsecureCerts: true,在 HTTPS 测试服务器页面上执行(Web Bluetooth 要求安全上下文)。 - 触发页面 API:在页面内调用
navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [] }),浏览器弹出设备选择提示框。 - Puppeteer 侧接管:
page.waitForDevicePrompt()等待提示框,然后可以cancel()(此时页面侧的requestDevice应 reject),或select(devicePrompt.devices[0])选中模拟设备(此时页面拿到device.name === 'SOME_NAME')。
这说明携带 manufacturerData 的模拟外设会真实出现在 Web Bluetooth 的设备选择列表中,key/data 构成了页面端过滤与识别依据。
作用域与注意事项
- 实验性 API:
BluetoothManufacturerData及其所属的BluetoothEmulation三个方法在源码中均标注@experimental,接口签名可能随版本演进变化,用于生产自动化前建议锁定版本并关注变更日志。 - 模拟状态绑定在浏览器上下文而非页面:官方文档(BluetoothEmulation Remarks)明确说明——Web Bluetooth 规范要求模拟适配器按顶层可导航单元(top-level navigable)隔离,但 Chromium 当前的实现把蓝牙模拟绑定到**浏览器上下文(browser context)**上。因此,同一浏览器上下文内的不同页面暴露出的蓝牙模拟状态会相互干扰。从源码结构看,BiDi 实现按
contextId发送命令,CDP 实现则直接走浏览器级Connection,两种路径都体现了这一上下文级作用域。若需要相互独立的模拟状态,建议使用独立的浏览器上下文。 - 执行顺序:先
emulateAdapter('powered-on')再simulatePreconnectedPeripheral,最后disableEmulation清理;跳过适配器模拟直接注入外设的行为未受支持。
相关文件索引
| 内容 | 路径 |
|---|---|
| 本接口官方 API 文档 | docs/api/puppeteer.bluetoothmanufacturerdata.md |
接口类型定义(BluetoothManufacturerData / PreconnectedPeripheral / BluetoothEmulation) |
packages/puppeteer-core/src/api/BluetoothEmulation.ts |
| CDP 后端实现 | packages/puppeteer-core/src/cdp/BluetoothEmulation.ts |
| BiDi 后端实现 | packages/puppeteer-core/src/bidi/BluetoothEmulation.ts |
| 端到端测试用例 | test/src/bluetooth-emulation.test.ts |
| 外设接口文档 | docs/api/puppeteer.preconnectedperipheral.md |
| BluetoothEmulation 能力文档(含完整示例) | docs/api/puppeteer.bluetoothemulation.md |
小结
BluetoothManufacturerData 虽只是一个双字段接口(key: number + data: string),却是 Puppeteer 蓝牙模拟中承载"厂商身份"的关键载体:key 对齐 Bluetooth SIG 公司标识符注册表,data 以 base64 承载厂商自定义字节。它与 address、name、knownServiceUuids 共同构成 PreconnectedPeripheral,经 CDP 的 BluetoothEmulation.simulatePreconnectedPeripheral 或 BiDi 的 bluetooth.simulatePreconnectedPeripheral 命令透传给浏览器,使无硬件环境的自动化测试也能完整演练 requestDevice() 的设备选择、取消与选中流程。
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 StartedRust0623
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