因为上篇只介绍了noble-winrt只支持win10/win11,不支持其他系统,如linux(银河麒麟),macOS。那么我们要兼容多系统怎么办呢,办法有的朋友,我在我的项目中已经实战过了,同样抽取蓝牙部分拿出来做一个demo讲解,手把手喂饭给没有做过或正在为node蓝牙多系统适配焦头烂额的朋友,看的有用的,帮我github点个星星!!!!
https://github.com/Jadeite2/electron-vite--abandonware-noble
node.js: 20.x.x
Python 3.11.9
node-gyp v9.4.1
VisualStudioSetup(选择使用c++的桌面开发)
windows安装依赖要注意,因为这次要用的node蓝牙属于是c++的开发扩展,所以要安装node-gyp、VisualStudioSetup并选择使用c++的桌面开发安装后,才能完成node_moudel的编译和安装。
本文的node蓝牙api设备发现、连接、服务/特征发现、数据收发流程与上一篇 noble 基本一致,如下。 本次我们使用@abandonware/noble来作为蓝牙模块来开发功能。
使用electron-egg v4官方结构
electorn-egg V4版本官方结构
project
├── package.json npm包配置
├── bulid 打包用的资源和脚本
├── icons 软件图标(打包用到)
├── extraResources 额外资源目录
├── cmd 脚本/打包 命令配置
├── bin.js 开发环境配置
├── builder-xxx.json 打包配置
├── electron 主进程服务
├── main.js 入口文件
├── config 配置文件
├── config.default.js 默认配置,都会加载
├── config.local.js dev环境加载
├── config.prod.js 生产环境加载
├── controller 控制器
├── service 业务层
├── preload 预加载
├── index.js 入口文件,在程序启动时加载,如托盘、自动升级等功能要提前加载代码
├── bridge.js 桥接文件
├── lifecycle.js 生命周期函数
├── jobs 任务
├── frontend 前端目录(demo是用vue编写的)
├── go go目录(可选)
├── out 打包后生成的可执行文件
├── latest.yml 自动升级文件
├── xxx.exe window应用安装包
├── xxx.exe.blockmap window应用增量升级包
├── xxx.dmg mac应用安装包
├── xxx.deb linux应用安装包后缀有多种
├── logs 日志
├── public 资源目录
├── dist 前端资源会移动到这里,生产环境加载
├── electron 主进程代码,生产环境加载
├── html 一些模板
├── images 一些图片
├── data 内置数据库文件
├── sqlite-demo.db 示例sqlite数据库
我们主要关注electron 主进程服务controller 控制器 和 service 业务层,frontend 前端目录的view的indx.vue


在蓝牙通信中这三个uuid是最重要的,NOTIFY_UUID负责订阅消息,WRITE_UUID负责向蓝牙设备发送消息,NOTIFY_UUID 和WRITE_UUID 是服务下的特征(Characteristic),必须归属于某个 SERVICE_UUID。
有个办法是使用2个手机,都安装上面的BLE调试助手,一个手机选从机模式,另一个手机搜索连接蓝牙名AK62的从机或者是自己手机蓝牙名的从机,可以查看到fff0,fff1,fff2特征,替换到frontend 前端目录的view的indx.vue的86-89行

运行项目 npm run dev,重新扫描和连接手机上的BLE调试助手,选择从机模式,就可以通信了。切换到设备模式就是让手机变成蓝牙从机
本次需要注意蓝牙模块api返回的characteristics返回的是4位16进制UUID!!









Electron的ipc通信不在本文讲解范围内,请自行阅读electron文档及electron-egg框架文档。

noble.on('stateChange', callback) 监听蓝牙适配器状态变化
状态:`poweredOn`, `poweredOff`, `resetting`, `unauthorized`, `unsupported`, `unknown`
主要用到poweredOn,判断当前电脑蓝牙是否可用
noble.on('stateChange', (state) => {
// logger.info('BLE状态变化:', state);
if (state === 'poweredOn') {
// logger.info('蓝牙已启用,可以开始扫描');
} else {
// logger.warn('蓝牙不可用:', state);
this.stopScan();
}
});
noble.on('discover', callback) 发现蓝牙设备时触发。回调参数为 `peripheral` 设备对象。
noble.on('discover', (peripheral) => {
console.log(`发现设备: ${peripheral}`);
// 简化设备名称处理,只使用localName或默认名称
const deviceName = peripheral.advertisement.localName || '--';
const deviceInfo = {
id: peripheral.id,
name: deviceName,
rssi: peripheral.rssi,
connectable: peripheral.connectable,
state: peripheral.state,
advertisement: {
localName: peripheral.advertisement.localName,
txPowerLevel: peripheral.advertisement.txPowerLevel,
manufacturerData: peripheral.advertisement.manufacturerData ? peripheral.advertisement.manufacturerData.toString('hex') : null,
serviceData: peripheral.advertisement.serviceData,
serviceUuids: peripheral.advertisement.serviceUuids || [],
solicitationServiceUuids: peripheral.advertisement.solicitationServiceUuids || []
},
peripheral: peripheral // 保存完整的peripheral对象以便后续连接使用
};
this.devices.set(peripheral.id, deviceInfo);
});
noble.startScanning([serviceUUIDs], allowDuplicates) 开始扫描设备。
- `serviceUUIDs`:要扫描的服务 UUID 数组,空数组表示扫描所有设备。(这个参数我发填入了会扫描不到,所以我都不填)
- `allowDuplicates`:是否允许重复发现同一设备。
noble.startScanning([], true);
noble.stopScanning([callback]) 停止扫描设备。
noble.stopScanning();
peripheral.id 设备唯一标识符。
peripheral.advertisement 设备广播信息对象,包含:
- `localName`
- `txPowerLevel`
- `manufacturerData`
- `serviceData`
- `serviceUuids`
- `solicitationServiceUuids`
peripheral.connect(callback) 连接到设备。(ble.js第188行)
peripheral.connect((error) => { ... });
peripheral.discoverServices([serviceUUIDs], callback) 发现设备的服务。(ble.js第195行)
- `serviceUUIDs`:要发现的服务 UUID 数组
- 回调参数:`(error, services)`
peripheral.discoverServices([serviceUuid], (error, services) => { ... });
peripheral.disconnect(callback)- 断开设备连接。
peripheral.disconnect((error) => { ... });
service.uuid 服务的 UUID
service.discoverCharacteristics([characteristicUUIDs], callback) 发现服务下的特征(ble.js第208行)
- `characteristicUUIDs`:要发现的特征 UUID 数组。
- 回调参数:`(error, characteristics)`
service.discoverCharacteristics([writeUuid, notifyUuid], (error, characteristics) => { ... });
characteristic.uuid 特征的 UUID。
characteristic.write(data, withoutResponse, callback) 向特征写入数据。
- `data`:用Buffer 类型数据。
- `withoutResponse`:布尔值,是否不需要响应。
characteristic.write(Buffer.from([0x01, 0x02]), false, (error) => { ... });
characteristic.subscribe(callback) 订阅特征的通知(notify),订阅后才能监听蓝牙数据返回。
characteristic.subscribe((error) => { ... });
characteristic.on('data', callback)监听特征数据变化(notify)。
characteristic.on('data', (data) => {
// data为Buffer
});
characteristic.unsubscribe(callback) 取消订阅通知。
characteristic.unsubscribe((error) => { ... });
characteristic.removeAllListeners('data') 移除所有数据监听器。
参考流程(结合本项目代码)
1. 监听 `stateChange`,`poweredOn` 时允许扫描。
2. 调用 `noble.startScanning()` 开始扫描。
3. 监听 `discover` 事件,获取 `peripheral`。
4. 通过 `peripheral.connect()` 连接设备,连接的时候对特征做一个存储,读写的时候方便用,不要每次重新查找 ,如下。
service.discoverCharacteristics([].filter(Boolean), (error, characteristics) => {
logger.info(characteristics, '11111111111')
if (error) {
reject(error);
return;
}
// 缓存 writeUuid 特征
const writeChar = characteristics.find(item => item.uuid === writeUuid);
if (writeChar) {
this.characteristics.set(writeUuid, writeChar);
// logger.info('设备连接成功并缓存writeUuid特征:', writeUuid);
}
// 缓存 notifyUuid 特征(如果有)
if (notifyUuid) {
const notifyChar = characteristics.find(item => item.uuid === notifyUuid);
if (notifyChar) {
this.characteristics.set(notifyUuid, notifyChar);
// logger.info('设备连接成功并缓存notifyUuid特征:', notifyUuid);
}
}
// logger.info(this.characteristics, 'this.characteristicsthis.characteristicsthis.characteristics', this.characteristics.size)
resolve({
success: true,
message: '设备连接成功',
device: { id: device.id, name: device.name },
services: services.length
});
});
5. 通过 `peripheral.discoverServices()` 获取服务。
6. 通过 `service.discoverCharacteristics()` 获取特征。
7. 通过 `characteristic.write()` 写数据,`characteristic.subscribe()` 订阅通知,`characteristic.on('data')` 监听数据。
启动奔溃:
解决方案:系统设置-隐私与安全性-蓝牙,为终端添加权限

打包设置:需要在electron打包配置中写入获取mac系统权限的字段,以及在根目录下新建
entitlements.mac.plist文件:
"mac": {
// ...
"extendInfo": {
"NSBluetoothAlwaysUsageDescription": "This app uses Bluetooth to communicate with nearby devices."
},
"entitlements": "entitlements.mac.plist"
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.device.bluetooth</key>
<true/>
</dict>
</plist>
这样macOS intel架构的打包就没问题了。
- 不要直接套用@abandonware/noble里面的api,用我项目里的api,都是经过我自己踩坑看源码测试出来的。
看的有用的,麻烦帮我github点个星星, 一键999连
github地址:https://github.com/Jadeite2/electron-vite--abandonware-noble