darwin-corebluetooth-connected-peripherals-recovery
v0.1.1
Published
Enumerate the CoreBluetooth peripherals already connected to macOS by service UUID — the non-scan path for devices that stopped advertising. Node-API v8, prebuilt for darwin-arm64 and darwin-x64.
Maintainers
Readme
darwin-corebluetooth-connected-peripherals-recovery
查询 macOS 上系统已连接的 CoreBluetooth 外设(按 Service UUID 过滤)。Node-API v8,随包提供 darwin-arm64 / darwin-x64 prebuild。
const { findConnectedPeripherals } = require("darwin-corebluetooth-connected-peripherals-recovery");
const peripherals = await findConnectedPeripherals(["AB5E0001-5A21-4F05-BC7D-AF01F617B664"]);
// [ { id: 'FCC65679-E122-7B92-1EDD-0214EB717DFE', name: '小米蓝牙语音遥控器' } ]为什么需要它
BLE 外设一旦被连接通常就停止广播,此时主动扫描永远扫不到它(已连接的 HID 遥控器、手环都常见)。CoreBluetooth 为此提供了非扫描入口:
[self.central retrieveConnectedPeripheralsWithServices:@[serviceUUID]];问题是不是每个 BLE 抽象层都会用到它。例如 unified-ble-manager 4.0.27 的 CoreBluetooth backend 刻意不注册 address targeting(Apple 平台无法从蓝牙地址发起连接),也不暴露 connected-peripheral 查询,于是"设备已连接 → 不再广播 → 扫描扫不到"就成了一条死路。这个包把那一个调用单独暴露出来,供这类抽象层补一条合成 advertisement 或做诊断。
对照:Linux 的 BlueZ 不需要这层(Device1 对象在 ObjectManager 里天然可见,UBM 已注册 peer:address-targeting);Windows 也有同类缺口(WinRT 有 FromBluetoothAddressAsync,但同样取决于抽象层有没有用)。
平台支持
| 平台 | 状态 |
| --- | --- |
| macOS (darwin-arm64 / darwin-x64) | ✅ 已实机验证 |
| 其它平台 | ❌ findConnectedPeripherals() 抛错(fail-closed,不做降级) |
包本身可以安装在任何平台(npm install 在非 macOS 上是干净的 no-op),但只有 macOS 上有实现。
安装
npm install darwin-corebluetooth-connected-peripherals-recovery发布版带 prebuild,安装时不会编译任何东西,也不会落到源码构建:发布包不含 binding.gyp / src/(见下方"为什么发布包不带 binding.gyp")。因此第三方工具也不会替它编译——@electron/rebuild(即 electron-builder install-app-deps、electron-rebuild)只按"包的根目录里有没有 binding.gyp"判定原生模块,看不到它就直接跳过。源码构建只存在于源码检出(需要 Xcode Command Line Tools)。
首次调用会弹出系统蓝牙授权;未授权时调用以明确错误失败,不会静默返回空数组。
API
findConnectedPeripherals(serviceUuids, options?)
返回系统当前已连接、且在广播里声明过任一给定 Service 的外设。
findConnectedPeripherals(
serviceUuids: readonly string[], // 至少一个 CBUUID 字符串(128 位或 16/32 位短形式)
options?: { timeoutMs?: number }, // 等待 CoreBluetooth 就绪的上限,默认 5000;0 表示不设上限
): Promise<readonly { id: string, name: string | null }[]>id是CBPeripheral.identifier.UUIDString:由 host 与设备共同派生、只在当前这台 Mac 上有意义的不透明标识。它不是蓝牙硬件地址,两者之间没有可换算关系(Apple 不提供该 API)。name在外设没有可用名称时为null。timeoutMs是必需的健壮性上限:适配器可能长时间停在非终态(例如未授权后回调不再到来),没有它 Promise 会永远 pending。超时以CoreBluetooth did not report a usable adapter state within N ms拒绝。- 参数非法(空数组、非字符串元素、负数 timeout)抛
TypeError,且先于原生模块加载——非 macOS 上也能拿到参数错误。 - 返回的数组和每个元素都是冻结的。
capabilities
const { capabilities } = require("darwin-corebluetooth-connected-peripherals-recovery");
// darwin → { platform: 'darwin', status: 'supported', backend: 'corebluetooth', reason: null }不加载原生模块,可在任何平台上安全地做特性判断。
resolveArtifactPath()
返回当前会被加载的 .node 路径(没有则为 null)。只做路径判断、不加载模块,用于诊断"到底加载了哪个产物"。设 DARWIN_CB_CONNECTED_PERIPHERALS_SOURCE=1 可让本地 build/ 产物优先于 prebuild。
接到一个 BLE 抽象层上
典型用法是补一条合成 advertisement —— 外设本来就不在广播,所以把非扫描查询的结果伪装成一次广播事件,交给上层原有的 connect/GATT 流程。以 unified-ble-manager 的 CoreBluetooth boundary 为例:
const { createConnectedPeripheralRecoveryBoundary } = require("./your-boundary-adapter.cjs");
// CoreBluetooth 的 advertisement 形状是 { nativePeerId, localName, serviceUuids, rssi, ... }
// nativePeerId 必须填入上面拿到的 id,UBM 之后会用它 connect注意各家抽象层的 advertisement 形状并不通用:WinRT 的必需字段里有 scanToken,且没有 manufacturerData/serviceData。适配层要按目标平台单独写。
构建与发布
npm install # 安装 devDependencies(node-addon-api、node-gyp)
npm run build:prebuilds # 依次构建 darwin-arm64 / darwin-x64 并用 lipo 复核架构
npm test # 需要真实已连接的设备;无设备时相关用例只断言结构产物布局遵循 Node 原生模块的 prebuild 约定:
prebuilds/darwin-arm64/corebluetooth_connected_peripherals.node
prebuilds/darwin-x64/corebluetooth_connected_peripherals.nodeprebuilds/ 会提交进仓库并随包发布;build/ 是本地中间产物,已忽略。
为什么发布包不带 binding.gyp
binding.gyp 与 src/ 只存在于源码检出,不随 npm 包发布(package.json 的 files 是白名单,没列进去就不会打包)。
原因是 @electron/rebuild 判定"这个模块需要重建"的唯一依据,就是模块根目录里有没有 binding.gyp:
// electron/rebuild → Rebuilder.rebuild()
const nativeModulePaths = candidatePaths.filter((p) =>
fs.existsSync(path.resolve(p, "binding.gyp")),
);它不看平台、不看 os 字段,也不看本包的 install 脚本。所以只要发布包里带着 binding.gyp,消费者在 Linux / Windows 上做 Electron 打包时就会被强行 node-gyp rebuild,撞上 src/addon_stub.cc 的 #error,把对方的 CI 打挂——而本包在非 macOS 上本来就没有可编译的东西。darwin-arm64 / darwin-x64 是 Node 在 macOS 上的全部目标,两者都随包发布,因此"安装时源码构建"对消费者不可达;去掉它换来的是"任何平台上都没有工具会去编译这个包",顺带 macOS 上也不再有重复编译。
同理,Node-API v8 的产物与运行时 ABI 无关,Electron 直接复用随包的 .node,不需要按运行时/ABI 铺矩阵。⚠️ 打包 Electron 时 .node 必须保持在 asar 之外,并纳入签名/公证流程。
边界
这个包只做查询:不配对、不连接、不做 GATT 服务发现与订阅、不读取任何特征值。拿到 id 之后的一切都由调用方的 BLE 栈负责。
License
MIT
