diff --git a/README.md b/README.md index f8f2a100d..c0924f3c7 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,7 @@ API structure: * [_Event: Scanning started_](#event-scanning-started) * [Stop scanning](#stop-scanning) * [_Event: Scanning stopped_](#event-scanning-stopped) + * [Connect by UUID / Address](#connect-by-uuid) * [_Event: Peripheral discovered_](#event-peripheral-discovered) * [_Event: Warning raised_](#event-warning-raised) * [Reset device](#reset-device) @@ -355,6 +356,54 @@ The event is emitted when: * Scanning is stopped * Another application stops scanning +#### Connect by UUID + +The `connect` function is used to establish a Bluetooth Low Energy connection to a peripheral device using its UUID. It provides both callback-based and Promise-based interfaces. + +##### Usage + +```typescript +// Callback-based usage +connect(peripheralUuid: string, options?: object, callback?: (error?: Error, peripheral: Peripheral) => void): void; + +// Promise-based usage +connectAsync(peripheralUuid: string, options?: object): Promise; +``` + +##### Parameters +- `peripheralUuid`: The UUID of the peripheral to connect to. +- `options`: Optional parameters for the connection (this may include connection interval, latency, supervision timeout, etc.). +- `callback`: An optional callback that returns an error or the connected peripheral object. + +##### Description +The `connect` function initiates a connection to a BLE peripheral. The function immediately returns, and the actual connection result is provided asynchronously via the callback or Promise. If the peripheral is successfully connected, a `Peripheral` object representing the connected device is provided. + +##### Example + +```javascript +const noble = require('noble'); + +// Using callback +noble.connect('1234567890abcdef', {}, (error, peripheral) => { + if (error) { + console.error('Connection error:', error); + } else { + console.log('Connected to:', peripheral.uuid); + } +}); + +// Using async/await +async function connectPeripheral() { + try { + const peripheral = await noble.connectAsync('1234567890abcdef'); + console.log('Connected to:', peripheral.uuid); + } catch (error) { + console.error('Connection error:', error); + } +} +connectPeripheral(); +``` + #### _Event: Peripheral discovered_ diff --git a/examples/connect-address.js b/examples/connect-address.js new file mode 100644 index 000000000..fdda24367 --- /dev/null +++ b/examples/connect-address.js @@ -0,0 +1,40 @@ +const noble = require('../index'); +const direct = require('debug')('connection/direct'); +const scan = require('debug')('connection/scan'); + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function run () { + noble.on('stateChange', async function (state) { + if (state === 'poweredOn') { + try { + direct('connecting'); + // const uuid = 'f1:36:1c:ab:94:cc'.split(':').join(''); // HCI Address UUID + const uuid = '2561b846d6f83ee27580bca8ed6ec079'; // MacOS UUID + const peripheral = await noble.connectAsync(uuid); + direct(`connected ${peripheral.uuid}`); + await peripheral.disconnectAsync(); + direct('disconnected'); + console.log('sleeping for 2000ms'); + await sleep(2000); + scan('connecting by scan'); + await noble.startScanningAsync(); + noble.on('discover', async peripheral => { + if (peripheral.uuid === uuid) { + await noble.stopScanningAsync(); + await peripheral.connectAsync(); + scan(`connected ${peripheral.uuid}`); + await peripheral.disconnectAsync(); + scan('disconnected'); + } + }); + } catch (error) { + console.log(error); + } + } + }); +} + +run(); diff --git a/index.d.ts b/index.d.ts index 7b5a23143..3c8cc2305 100644 --- a/index.d.ts +++ b/index.d.ts @@ -25,6 +25,8 @@ export declare function startScanning(serviceUUIDs?: string[], allowDuplicates?: export declare function startScanningAsync(serviceUUIDs?: string[], allowDuplicates?: boolean): Promise; export declare function stopScanning(callback?: () => void): void; export declare function stopScanningAsync(): Promise; +export declare function connect(peripheralUuid: string, options?: object, callback?: (error?: Error, peripheral: Peripheral) => void): void; +export declare function connectAsync(peripheralUuid: string, options?: object): Promise; export declare function cancelConnect(peripheralUuid: string, options?: object): void; export declare function reset(): void; diff --git a/lib/hci-socket/bindings.js b/lib/hci-socket/bindings.js index adba8b009..a0a3fe9b9 100644 --- a/lib/hci-socket/bindings.js +++ b/lib/hci-socket/bindings.js @@ -52,14 +52,20 @@ NobleBindings.prototype.stopScanning = function () { }; NobleBindings.prototype.connect = function (peripheralUuid, parameters) { - const address = this._addresses[peripheralUuid]; - const addressType = this._addresseTypes[peripheralUuid]; + let address = this._addresses[peripheralUuid]; + const addressType = this._addresseTypes[peripheralUuid] || 'random'; // Default to 'random' if type is not defined + // If address is not available, generate it from the UUID using the transformation logic inline + if (!address) { + address = peripheralUuid.match(/.{1,2}/g).join(':'); // Converts UUID back to MAC address format + } + + // Manage connection attempts if (!this._pendingConnectionUuid) { this._pendingConnectionUuid = peripheralUuid; - this._hci.createLeConn(address, addressType, parameters); } else { + // If there is already a pending connection, queue this one this._connectionQueue.push({ id: peripheralUuid, params: parameters }); } }; @@ -249,6 +255,31 @@ NobleBindings.prototype.onLeConnComplete = function ( if (status === 0) { uuid = address.split(':').join('').toLowerCase(); + // Check if address is already known + if (!this._addresses[uuid]) { + // Simulate discovery if address is not known + const advertisement = { // Assume structure, adjust as necessary + serviceUuids: [], // Actual service UUID data needed here + serviceData: [] // Actual service data needed here + }; + const rssi = 127; + const connectable = true; // Assuming the device is connectable + const scannable = false; // Assuming the device is not scannable + + this._scanServiceUuids = []; // We have to set this to fake scan + + // Call onDiscover to simulate device discovery + this.onDiscover( + status, + address, + addressType, + connectable, + advertisement, + rssi, + scannable + ); + } + const aclStream = new AclStream( this._hci, handle, diff --git a/lib/mac/src/ble_manager.h b/lib/mac/src/ble_manager.h index 3ea5b7725..b7881f6d1 100644 --- a/lib/mac/src/ble_manager.h +++ b/lib/mac/src/ble_manager.h @@ -20,6 +20,7 @@ @property (strong) CBCentralManager *centralManager; @property dispatch_queue_t dispatchQueue; @property NSMutableDictionary *peripherals; +@property NSMutableSet *discovered; - (instancetype)init: (const Napi::Value&) receiver with: (const Napi::Function&) callback; - (void)scan: (NSArray *)serviceUUIDs allowDuplicates: (BOOL)allowDuplicates; diff --git a/lib/mac/src/ble_manager.mm b/lib/mac/src/ble_manager.mm index 030fe5c50..0d8a6ce7c 100644 --- a/lib/mac/src/ble_manager.mm +++ b/lib/mac/src/ble_manager.mm @@ -18,6 +18,7 @@ - (instancetype)init: (const Napi::Value&) receiver with: (const Napi::Function& self->emit.Wrap(receiver, callback); self.dispatchQueue = dispatch_queue_create("CBqueue", 0); self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:self.dispatchQueue]; + self.discovered = [NSMutableSet set]; self.peripherals = [NSMutableDictionary dictionaryWithCapacity:10]; } return self; @@ -40,11 +41,13 @@ - (void)scan: (NSArray *)serviceUUIDs allowDuplicates: (BOOL)allowDup - (void)stopScan { [self.centralManager stopScan]; + [self.discovered removeAllObjects]; emit.ScanState(false); } - (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { std::string uuid = getUuid(peripheral); + [self.discovered addObject:getNSUuid(peripheral)]; Peripheral p; p.address = getAddress(uuid, &p.addressType); @@ -93,7 +96,11 @@ - (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPer - (BOOL)connect:(NSString*) uuid { CBPeripheral *peripheral = [self.peripherals objectForKey:uuid]; if(!peripheral) { - NSArray* peripherals = [self.centralManager retrievePeripheralsWithIdentifiers:@[[[NSUUID alloc] initWithUUIDString:uuid]]]; + NSUUID *identifier = [[NSUUID alloc] initWithUUIDString:uuid]; + if (!identifier) { + return NO; + } + NSArray* peripherals = [self.centralManager retrievePeripheralsWithIdentifiers:@[identifier]]; peripheral = [peripherals firstObject]; if(peripheral) { peripheral.delegate = self; @@ -108,6 +115,16 @@ - (BOOL)connect:(NSString*) uuid { } - (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { + // Check if peripheral was known + if ([self.discovered containsObject:getNSUuid(peripheral)] == false) { + // The peripheral was connected without being discovered by this app instance + // Optionally simulate discovery using dummy or last known advertisement data and RSSI + NSDictionary *advertisementData = @{ }; // Placeholder, use actual last known data if available + NSNumber *RSSI = @127; // Placeholder RSSI, use actual last known value if available + + // Simulate discovery handling + [self centralManager:central didDiscoverPeripheral:peripheral advertisementData:advertisementData RSSI:RSSI]; + } std::string uuid = getUuid(peripheral); emit.Connected(uuid, ""); } diff --git a/lib/noble.js b/lib/noble.js index 6fd57a0ae..6b3a0894f 100644 --- a/lib/noble.js +++ b/lib/noble.js @@ -227,15 +227,34 @@ Noble.prototype.onDiscover = function (uuid, address, addressType, connectable, } }; -Noble.prototype.connect = function (peripheralUuid, parameters) { +Noble.prototype.connect = function (peripheralUuid, parameters, callback) { + // Check if callback is a function + if (typeof callback === 'function') { + // Create a unique event name using the peripheral UUID + const eventName = `connect:${peripheralUuid}`; + + // Add a one-time listener for this specific event + this.once(eventName, (error) => { + callback(error, this._peripherals[peripheralUuid]); + }); + } + + // Proceed to initiate the connection this._bindings.connect(peripheralUuid, parameters); }; +Noble.prototype.connectAsync = function (peripheralUuid, parameters) { + return util.promisify((callback) => this.connect(peripheralUuid, parameters, callback))(); +}; Noble.prototype.onConnect = function (peripheralUuid, error) { const peripheral = this._peripherals[peripheralUuid]; if (peripheral) { + // Emit a unique connect event for the specific peripheral + this.emit(`connect:${peripheralUuid}`, error); + peripheral.state = error ? 'error' : 'connected'; + // Also emit the general 'connect' event for a peripheral peripheral.emit('connect', error); } else { this.emit('warning', `unknown peripheral ${peripheralUuid} connected!`); diff --git a/test/lib/hci-socket/bindings.test.js b/test/lib/hci-socket/bindings.test.js index c74df92cb..64c3fd00a 100644 --- a/test/lib/hci-socket/bindings.test.js +++ b/test/lib/hci-socket/bindings.test.js @@ -135,12 +135,12 @@ describe('hci-socket bindings', () => { it('missing peripheral, no queue', () => { bindings._hci.createLeConn = fake.resolves(null); - bindings.connect('peripheralUuid', 'parameters'); + bindings.connect('112233445566', 'parameters'); - should(bindings._pendingConnectionUuid).eql('peripheralUuid'); + should(bindings._pendingConnectionUuid).eql('112233445566'); assert.calledOnce(bindings._hci.createLeConn); - assert.calledWith(bindings._hci.createLeConn, undefined, undefined, 'parameters'); + assert.calledWith(bindings._hci.createLeConn, '11:22:33:44:55:66', 'random', 'parameters'); }); it('existing peripheral, no queue', () => {