Skip to content

Commit

Permalink
feat(noble): add direct device connection capability without prior sc…
Browse files Browse the repository at this point in the history
…anning
  • Loading branch information
stoprocent committed May 28, 2024
1 parent e3cd3e4 commit 77c0210
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 8 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<Peripheral>;
```

##### 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_

Expand Down
40 changes: 40 additions & 0 deletions examples/connect-address.js
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export declare function startScanning(serviceUUIDs?: string[], allowDuplicates?:
export declare function startScanningAsync(serviceUUIDs?: string[], allowDuplicates?: boolean): Promise<void>;
export declare function stopScanning(callback?: () => void): void;
export declare function stopScanningAsync(): Promise<void>;
export declare function connect(peripheralUuid: string, options?: object, callback?: (error?: Error, peripheral: Peripheral) => void): void;
export declare function connectAsync(peripheralUuid: string, options?: object): Promise<Peripheral>;
export declare function cancelConnect(peripheralUuid: string, options?: object): void;
export declare function reset(): void;

Expand Down
37 changes: 34 additions & 3 deletions lib/hci-socket/bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
};
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/mac/src/ble_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString*> *)serviceUUIDs allowDuplicates: (BOOL)allowDuplicates;
Expand Down
19 changes: 18 additions & 1 deletion lib/mac/src/ble_manager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,11 +41,13 @@ - (void)scan: (NSArray<NSString*> *)serviceUUIDs allowDuplicates: (BOOL)allowDup

- (void)stopScan {
[self.centralManager stopScan];
[self.discovered removeAllObjects];
emit.ScanState(false);
}

- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
std::string uuid = getUuid(peripheral);
[self.discovered addObject:getNSUuid(peripheral)];

Peripheral p;
p.address = getAddress(uuid, &p.addressType);
Expand Down Expand Up @@ -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;
Expand All @@ -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<NSString *, id> *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, "");
}
Expand Down
21 changes: 20 additions & 1 deletion lib/noble.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!`);
Expand Down
6 changes: 3 additions & 3 deletions test/lib/hci-socket/bindings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit 77c0210

Please sign in to comment.