test.js
const noble = require('./index');
noble.startScanning(); // any service UUID, no duplicates
noble.startScanning([], true);
noble.startScanning([], true,()=>{});
lib/noble.js
function Noble (bindings) {
this.initialized = false;
this.address = 'unknown';
this._state = 'unknown';
this._bindings = bindings;
this._peripherals = {};
this._services = {};
this._characteristics = {};
this._descriptors = {};
this._discoveredPeripheralUUids = {};
this._bindings.on('stateChange', this.onStateChange.bind(this));
this._bindings.on('addressChange', this.onAddressChange.bind(this));
this._bindings.on('scanParametersSet', this.onScanParametersSet.bind(this));
this._bindings.on('scanStart', this.onScanStart.bind(this));
}
Noble.prototype.onScanStart = function (filterDuplicates) {
debug('scanStart');
this.emit('scanStart', filterDuplicates);
};
lib/noble.js
- startScanning函数
const startScanning = function (serviceUuids, allowDuplicates, callback) {
if (typeof serviceUuids === 'function') {
this.emit('warning', 'calling startScanning(callback) is deprecated');
}
if (typeof allowDuplicates === 'function') {
this.emit('warning', 'calling startScanning(serviceUuids, callback) is deprecated');
}
const scan = function (state) {
if (state !== 'poweredOn') {
const error = new Error(`Could not start scanning, state is ${state} (not poweredOn)`);
if (typeof callback === 'function') {
callback(error);
} else {
throw error;
}
} else {
if (callback) {
this.once('scanStart', filterDuplicates => {
callback(null, filterDuplicates);
});
}
this._discoveredPeripheralUUids = {};
this._allowDuplicates = allowDuplicates;
this._bindings.startScanning(serviceUuids, allowDuplicates);
}
};
// if bindings still not init, do it now
if (!this.initialized) {
this.initialized = true;
this._bindings.init();
this.once('stateChange', scan.bind(this));
} else {
scan.call(this, this._state);
}
};
Noble.prototype.startScanning = startScanning;
Noble.prototype.startScanningAsync = function (serviceUUIDs, allowDuplicates) {
return util.promisify((callback) => this.startScanning(serviceUUIDs, allowDuplicates, callback))();
};
- 其中 this._bindings.startScanning(serviceUuids, allowDuplicates);会执行lib/mac/src/noble_mac.mm文件中NobleMac::Scan方法,函数代码如下:
NobleMac::InstanceMethod("startScanning", &NobleMac::Scan);
Napi::Value NobleMac::Scan(const Napi::CallbackInfo& info) {。。。};
- 其中,this._bindings.init();
NobleMac::InstanceMethod("init", &NobleMac::Init),
。。。
Napi::Object Init(Napi::Env env, Napi::Object exports) {
// 创建一个新的字符串对象,内容为"NobleMac"
Napi::String name = Napi::String::New(env, "NobleMac");
// 将NobleMac类创建的对象设置到exports对象中,键名为"NobleMac"
exports.Set(name, NobleMac::GetClass(env));
// 返回修改后的exports对象
return exports;
}
lib/distributed/bindings.js ???
const NobleBindings = function () {
this._wss = new WebSocketServer({
port: 0xB1e
});
this._startScanCommand = null;
this._peripherals = {};
this._wss.on('connection', this._onConnection.bind(this));
this.on('close', this._onClose.bind(this));
this.on('message', this._onMessage.bind(this));
process.nextTick(() => {
this.emit('stateChange', 'poweredOff');
});
};
NobleBindings.prototype.startScanning = function (serviceUuids, allowDuplicates) {
this._startScanCommand = {
action: 'startScanning',
serviceUuids,
allowDuplicates
};
lib/mac/binding.gyp
-binding.gyp是交叉编译的打包配置
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'src/noble_mac.mm', 'src/napi_objc.mm', 'src/ble_manager.mm', 'src/objc_cpp.mm', 'src/callbacks.cc' ],
'include_dirs': ["<!@(node -p \"require('node-addon-api').include\")"],
'dependencies': ["<!(node -p \"require('node-addon-api').gyp\")"],
'cflags!': [ '-fno-exceptions' ],
'cflags_cc!': [ '-fno-exceptions' ],
'cflags+': ['-fvisibility=hidden'],
'xcode_settings': {
'GCC_SYMBOLS_PRIVATE_EXTERN': 'YES', # -fvisibility=hidden
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
'CLANG_CXX_LIBRARY': 'libc++',
'MACOSX_DEPLOYMENT_TARGET': '10.7',
'OTHER_CFLAGS': [
'-fobjc-arc',
],
},
'link_settings': {
'libraries': [
'$(SDKROOT)/System/Library/Frameworks/CoreBluetooth.framework',
]
},
'product_dir': '../lib/mac/native',
}
]
}
- 打包完成后生成文件 lib/mac/native/binding.node
lib/mac/bindings.js
const { EventEmitter } = require('events'); // 引入Node.js的EventEmitter模块,用于创建事件发射器
const { inherits } = require('util'); // 引入Node.js的util模块中的inherits方法,用于实现基于原型的继承
const { resolve } = require('path'); // 引入Node.js的path模块中的resolve方法,用于解析文件路径
// 解析当前文件所在目录的上两级目录,赋值给变量dir
// 项目的根目录 ,对于本机 = /Users/caiyunsun/vs2024proj/abandonware_noble
const dir = resolve(__dirname, '..', '..');
console.log(dir);
// 引入node-gyp-build模块,传入dir作为参数,并将返回值赋值给变量binding
const binding = require('node-gyp-build')(dir);
console.log(binding); // {NobleMac:f}
// 从binding对象中获取NobleMac属性,并将其赋值给变量NobleMac
const { NobleMac } = binding;
// 使用inherits方法实现NobleMac基于EventEmitter的继承,使NobleMac具有EventEmitter的属性和方法
inherits(NobleMac, EventEmitter);
console.log(NobleMac); // 2
// 将NobleMac导出为模块的公开接口,以便在其他文件中通过require引入并使用
module.exports = NobleMac;
-
打印binding如下:
-
打印console.log(NobleMac)如下:
lib/mac/src/noble_mac.h
- bindings.js指出实际导出NobleMac,而这里是NobleMac的声明文件。
#include <napi.h>
#include "ble_manager.h"
class NobleMac : public Napi::ObjectWrap<NobleMac>
{
public:
NobleMac(const Napi::CallbackInfo&);
Napi::Value Init(const Napi::CallbackInfo&);
Napi::Value Scan(const Napi::CallbackInfo&);
Napi::Value StopScan(const Napi::CallbackInfo&);
static Napi::Function GetClass(Napi::Env);
private:
BLEManager* manager;
};
lib/mac/src/noble_mac.mm
// NobleMac类的构造函数
NobleMac::NobleMac(const Napi::CallbackInfo& info) : ObjectWrap(info) {
}
// startScanning(serviceUuids, allowDuplicates)
Napi::Value NobleMac::Scan(const Napi::CallbackInfo& info) {
// 检查BLEManager对象是否存在
CHECK_MANAGER()
// 将传入的第一个参数转换为UUID数组
NSArray* array = getUuidArray(info[0]);
// 将传入的第二个参数转换为布尔值,如果没有传入,则使用默认值NO
// default value NO
auto duplicates = getBool(info[1], NO);
// 开始扫描,使用转换后的UUID数组和布尔值作为参数
[manager scan:array allowDuplicates:duplicates];
// 返回空值
return Napi::Value();
}
Napi::Function NobleMac::GetClass(Napi::Env env) {
// 定义一个名为"NobleMac"的类,并添加其方法
return DefineClass(env, "NobleMac", {
// 定义"init"实例方法,绑定到NobleMac::Init函数
NobleMac::InstanceMethod("init", &NobleMac::Init),
// 定义"startScanning"实例方法,绑定到NobleMac::Scan函数
NobleMac::InstanceMethod("startScanning", &NobleMac::Scan),
NobleMac::InstanceMethod("stopScanning", &NobleMac::StopScan),
});
}
关键是 :
// 开始扫描,使用转换后的UUID数组和布尔值作为参数
[manager scan:array allowDuplicates:duplicates];
执行BLEManager类的scan方法,传入参数1是UUID的数组array,参入参数2是allowDuplicates:duplicates。其中allowDuplicates是函数定义时的形参名称,duplicates是实参。
lib/mac/src/ble_manager.h
- BLEManager类声明文件。
- BLEManager类以组合方式使用Emit,用于回调函数。
// 导入Foundation和CoreBluetooth框架,它们是Apple提供的基础库和蓝牙通信库
#import <Foundation/Foundation.h>
#import <CoreBluetooth/CoreBluetooth.h>
// 导入dispatch库,用于多线程编程
#include <dispatch/dispatch.h>
// 导入自定义的callbacks.h头文件
#include "callbacks.h"
// 声明BLEManager类,并使其遵循CBCentralManagerDelegate和CBPeripheralDelegate协议
@interface BLEManager : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate>
{
// 定义Emit类型的成员变量emit,用于回调函数
Emit emit;
// 定义bool类型的成员变量pendingRead,表示是否有待处理的读取请求
bool pendingRead;
}
// 定义CBCentralManager类型的属性centralManager,用于管理蓝牙连接
@property(strong) CBCentralManager *centralManager;
// 定义dispatch_queue_t类型的属性dispatchQueue,用于处理并发操作
@property dispatch_queue_t dispatchQueue;
// 定义NSMutableDictionary类型的属性peripherals,用于存储已发现的蓝牙外设信息
@property NSMutableDictionary *peripherals;
// 初始化方法,接收两个参数:receiver为Napi::Value类型,表示JavaScript的接收对象;callback为Napi::Function类型,表示JavaScript的回调函数
- (instancetype)init:(const Napi::Value &)receiver with:(const Napi::Function &)callback;
// 扫描蓝牙外设的方法,接收两个参数:serviceUUIDs为NSString数组,表示需要扫描的服务UUID;allowDuplicates为BOOL类型,表示是否允许重复扫描
- (void)scan:(NSArray<NSString *> *)serviceUUIDs allowDuplicates:(BOOL)allowDuplicates;
// 停止扫描蓝牙外设的方法
- (void)stopScan;
@end
lib/mac/src/ble_manager.mm
- BLEManager类的scan方法具体实现,形参2个,第一个是NSArray<NSString*> *类型的,形参名是serviceUUIDs;第二个参数形参名是allowDuplicates,类型是BOOL。
- 在方法最后执行emit.ScanState(true) 执行回调函数ScanState,参数是true;
- (void)scan: (NSArray<NSString*> *)serviceUUIDs allowDuplicates: (BOOL)allowDuplicates {
NSMutableArray* advServicesUuid = [NSMutableArray arrayWithCapacity:[serviceUUIDs count]];
[serviceUUIDs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[advServicesUuid addObject:[CBUUID UUIDWithString:obj]];
}];
NSDictionary *options = @{CBCentralManagerScanOptionAllowDuplicatesKey:[NSNumber numberWithBool:allowDuplicates]};
[self.centralManager scanForPeripheralsWithServices:advServicesUuid options:options];
emit.ScanState(true);
}
- (void)stopScan {
[self.centralManager stopScan];
emit.ScanState(false);
}
lib/mac/src/callbacks.h
- 调用noble.startScanning之后,会用回调事件ScanState的方式通知回调结果。
- callbacks.h定义所有的回调事件。
class Emit
{
public:
// 定义一个Wrap函数,用于包装回调函数
void Wrap(const Napi::Value &receiver, const Napi::Function &callback);
// 定义一个RadioState函数,用于处理无线电状态
void RadioState(const std::string &status);
// 定义一个ScanState函数,用于控制扫描的开始和结束
void ScanState(bool start);
// 定义一个Scan函数,用于扫描到周边设备
void Scan(const std::string &uuid, int rssi, const Peripheral &peripheral);
protected:
// 成员变量为受保护的访问修饰符,只能在类内部和派生类中访问
std::shared_ptr<ThreadSafeCallback> mCallback;
// 指向线程安全回调对象的智能指针,用于处理回调事件
};
lib/mac/src/callbacks.cc
void Emit::ScanState(bool start) {
mCallback->call([start](Napi::Env env, std::vector<napi_value>& args) {
// emit('scanStart') emit('scanStop')
args = { _s(start ? "scanStart" : "scanStop") };
});
}
void Emit::Scan(const std::string& uuid, int rssi, const Peripheral& peripheral) {
。。。。
// emit('discover', deviceUuid, address, addressType, connectable, advertisement, rssi);
args = { _s("discover"), _u(uuid), _s(address), toAddressType(env, addressType), _b(connectable), advertisment, _n(rssi) };
});
}
日志打印
const noble = require('../index');
// noble
console.log(noble);
// 以下是日志内容
执行node ./examples/advertisement-discovery.js脚本,
控制台打印日志为:
Noble {
initialized: false,
address: 'unknown',
_state: 'unknown',
_bindings: NobleMac {
_events: [Object: null prototype] {
stateChange: [Function: bound ],
addressChange: [Function: bound ],
scanParametersSet: [Function: bound ],
scanStart: [Function: bound ],
scanStop: [Function: bound ],
discover: [Function: bound ],
connect: [Function: bound ],
disconnect: [Function: bound ],
rssiUpdate: [Function: bound ],
servicesDiscover: [Function: bound ],
servicesDiscovered: [Function: bound ],
includedServicesDiscover: [Function: bound ],
characteristicsDiscover: [Function: bound ],
characteristicsDiscovered: [Function: bound ],
read: [Function: bound ],
write: [Function: bound ],
broadcast: [Function: bound ],
notify: [Function: bound ],
descriptorsDiscover: [Function: bound ],
valueRead: [Function: bound ],
valueWrite: [Function: bound ],
handleRead: [Function: bound ],
handleWrite: [Function: bound ],
handleNotify: [Function: bound ],
onMtu: [Function: bound ]
},
_eventsCount: 25
},
_peripherals: {},
_services: {},
_characteristics: {},
_descriptors: {},
_discoveredPeripheralUUids: {},
_events: [Object: null prototype] {
warning: [Function (anonymous)],
newListener: [Function (anonymous)]
},
_eventsCount: 2
}
- 日志中Noble._bindings._events共25个函数,对应lib/noble.js中绑定的25个方法。
- 当lib/noble.js执行this._bindings.init();函数后,会在Node下增加6个函数。
build/gyp-mac-tool交叉编译配置
更改代码后执行编译指令
// 代码检查
npm run lint
// 代码检查
npm run lint-fix
// 构建清理
npm run clean
// 代码预构建
npm run prebuild
// 代码预构建 - mac系统
npm run prebuild-darwin
// 代码编译
npm run rebuild
// 测试用例代码测试 ,执行test/test.js
npm run test