@stoprocent/noble 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/examples/echo.js +16 -18
- package/index.d.ts +15 -14
- package/lib/characteristic.js +117 -42
- package/lib/noble.js +64 -16
- package/package.json +1 -1
- package/prebuilds/darwin-x64+arm64/@stoprocent+noble.node +0 -0
- package/prebuilds/win32-ia32/@stoprocent+noble.node +0 -0
- package/prebuilds/win32-x64/@stoprocent+noble.node +0 -0
- package/test/lib/characteristic.test.js +205 -49
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A Node.js BLE (Bluetooth Low Energy) central module.
|
|
|
8
8
|
|
|
9
9
|
Want to implement a peripheral? Check out [@stoprocent/bleno](https://github.com/stoprocent/bleno).
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
> **Note:** Currently, running both noble (central) and bleno (peripheral) together only works with macOS bindings or when using separate HCI/UART dongles. Support for running both on a single HCI adapter (e.g., on Linux systems) will be added in future releases.
|
|
12
12
|
|
|
13
13
|
## About This Fork
|
|
14
14
|
|
|
@@ -360,6 +360,8 @@ const characteristics = await service.discoverCharacteristicsAsync([characterist
|
|
|
360
360
|
|
|
361
361
|
### Characteristic Methods
|
|
362
362
|
|
|
363
|
+
> **Note:** The `data` event is the primary event for handling both read responses and notifications. When using the event-based approach, you can differentiate between read responses and notifications using the `isNotification` parameter. The previously used `read` event **has been deprecated and removed**. Instead, use the `data` event with `isNotification=false` to identify read responses.
|
|
364
|
+
|
|
363
365
|
```typescript
|
|
364
366
|
// Read characteristic value
|
|
365
367
|
const data = await characteristic.readAsync();
|
|
@@ -373,10 +375,34 @@ await characteristic.subscribeAsync();
|
|
|
373
375
|
// Unsubscribe from notifications
|
|
374
376
|
await characteristic.unsubscribeAsync();
|
|
375
377
|
|
|
378
|
+
// Receive notifications using async iterator
|
|
379
|
+
for await (const data of characteristic.notificationsAsync()) {
|
|
380
|
+
console.log(`Received notification: ${data}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
376
383
|
// Discover descriptors
|
|
377
384
|
const descriptors = await characteristic.discoverDescriptorsAsync();
|
|
378
385
|
```
|
|
379
386
|
|
|
387
|
+
### Characteristic Events
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// Receive data (both read responses and notifications)
|
|
391
|
+
characteristic.on('data', (data: Buffer, isNotification: boolean) => {
|
|
392
|
+
console.log(`Received ${isNotification ? 'notification' : 'read response'}: ${data}`);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Write completion
|
|
396
|
+
characteristic.on('write', (error: Error | undefined) => {
|
|
397
|
+
console.log('Write completed');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Descriptor discovery
|
|
401
|
+
characteristic.on('descriptorsDiscover', (descriptors: Descriptor[]) => {
|
|
402
|
+
console.log('Descriptors discovered');
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
380
406
|
### Descriptor Methods
|
|
381
407
|
|
|
382
408
|
```typescript
|
|
@@ -604,4 +630,4 @@ The following environment variables can configure noble's behavior:
|
|
|
604
630
|
| BLUETOOTH_HCI_SOCKET_UART_PORT | UART port for HCI communication | none | `export BLUETOOTH_HCI_SOCKET_UART_PORT=/dev/ttyUSB0` |
|
|
605
631
|
| BLUETOOTH_HCI_SOCKET_UART_BAUDRATE | UART baudrate | 1000000 | `export BLUETOOTH_HCI_SOCKET_UART_BAUDRATE=1000000` |
|
|
606
632
|
|
|
607
|
-
**Note:** The preferred method for configuration is now using the `withBindings()` API rather than environment variables.
|
|
633
|
+
> **Note:** The preferred method for configuration is now using the `withBindings()` API rather than environment variables.
|
package/examples/echo.js
CHANGED
|
@@ -48,34 +48,32 @@ async function connectAndSetUp (peripheral) {
|
|
|
48
48
|
console.log('Discovered services and characteristics');
|
|
49
49
|
const echoCharacteristic = characteristics[0];
|
|
50
50
|
|
|
51
|
-
// data callback receives notifications
|
|
52
|
-
echoCharacteristic.on('read', (data, isNotification) => {
|
|
53
|
-
console.log(`Received: "${data}"`);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// subscribe to be notified whenever the peripheral update the characteristic
|
|
57
|
-
try {
|
|
58
|
-
await echoCharacteristic.subscribeAsync();
|
|
59
|
-
console.log('Subscribed for echoCharacteristic notifications');
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error('Error subscribing to echoCharacteristic:', error);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
51
|
// create an interval to send data to the service
|
|
66
52
|
let count = 0;
|
|
67
|
-
setInterval(async () => {
|
|
53
|
+
const interval = setInterval(async () => {
|
|
68
54
|
count++;
|
|
69
55
|
const message = Buffer.from(`hello, ble ${count}`, 'utf-8');
|
|
70
56
|
console.log(`Sending: '${message}'`);
|
|
71
57
|
await echoCharacteristic.writeAsync(message, false);
|
|
72
|
-
},
|
|
58
|
+
}, 500);
|
|
59
|
+
|
|
60
|
+
// subscribe to be notified whenever the peripheral update the characteristic
|
|
61
|
+
try {
|
|
62
|
+
for await (const data of echoCharacteristic.notificationsAsync()) {
|
|
63
|
+
console.log(`Received: "${data}"`);
|
|
64
|
+
if (count >= 15) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
clearInterval(interval);
|
|
70
|
+
await peripheral.disconnectAsync();
|
|
71
|
+
noble.stop();
|
|
72
|
+
}
|
|
73
73
|
|
|
74
74
|
} catch (error) {
|
|
75
75
|
console.error('Error during connection setup:', error);
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
peripheral.on('disconnect', () => console.log('disconnected'));
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
// Handle process termination
|
package/index.d.ts
CHANGED
|
@@ -174,36 +174,37 @@ declare module '@stoprocent/noble' {
|
|
|
174
174
|
|
|
175
175
|
readAsync(): Promise<Buffer>;
|
|
176
176
|
writeAsync(data: Buffer, withoutResponse: boolean): Promise<void>;
|
|
177
|
-
broadcastAsync(broadcast: boolean): Promise<void>;
|
|
178
|
-
notifyAsync(notify: boolean): Promise<void>;
|
|
179
|
-
discoverDescriptorsAsync(): Promise<Descriptor[]>;
|
|
180
177
|
subscribeAsync(): Promise<void>;
|
|
181
178
|
unsubscribeAsync(): Promise<void>;
|
|
179
|
+
discoverDescriptorsAsync(): Promise<Descriptor[]>;
|
|
180
|
+
broadcastAsync(broadcast: boolean): Promise<void>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Async iterator for receiving notifications from the characteristic.
|
|
184
|
+
* Automatically handles subscription and cleanup when finished.
|
|
185
|
+
* @returns AsyncGenerator that yields notification data
|
|
186
|
+
*/
|
|
187
|
+
notificationsAsync(): AsyncGenerator<Buffer, void, unknown>;
|
|
182
188
|
|
|
183
189
|
read(callback?: (error: Error | undefined, data: Buffer) => void): void;
|
|
184
190
|
write(data: Buffer, withoutResponse: boolean, callback?: (error: Error | undefined) => void): void;
|
|
185
|
-
broadcast(broadcast: boolean, callback?: (error: Error | undefined) => void): void;
|
|
186
|
-
notify(notify: boolean, callback?: (error: Error | undefined) => void): void;
|
|
187
|
-
discoverDescriptors(callback?: (error: Error | undefined, descriptors: Descriptor[]) => void): void;
|
|
188
191
|
subscribe(callback?: (error: Error | undefined) => void): void;
|
|
189
192
|
unsubscribe(callback?: (error: Error | undefined) => void): void;
|
|
193
|
+
discoverDescriptors(callback?: (error: Error | undefined, descriptors: Descriptor[]) => void): void;
|
|
194
|
+
broadcast(broadcast: boolean, callback?: (error: Error | undefined) => void): void;
|
|
190
195
|
|
|
191
196
|
toString(): string;
|
|
192
197
|
|
|
193
|
-
on(event: "read", listener: (data: Buffer, isNotification: boolean) => void): this;
|
|
194
|
-
on(event: "write", withoutResponse: boolean, listener: (error: Error | undefined) => void): this;
|
|
195
|
-
on(event: "broadcast", listener: (state: string) => void): this;
|
|
196
|
-
on(event: "notify", listener: (state: string) => void): this;
|
|
197
198
|
on(event: "data", listener: (data: Buffer, isNotification: boolean) => void): this;
|
|
199
|
+
on(event: "write", listener: (error: Error | undefined) => void): this;
|
|
198
200
|
on(event: "descriptorsDiscover", listener: (descriptors: Descriptor[]) => void): this;
|
|
201
|
+
on(event: "broadcast", listener: (state: string) => void): this;
|
|
199
202
|
on(event: string, listener: Function): this;
|
|
200
203
|
|
|
201
|
-
once(event: "read", listener: (data: Buffer, isNotification: boolean) => void): this;
|
|
202
|
-
once(event: "write", withoutResponse: boolean, listener: (error: Error | undefined) => void): this;
|
|
203
|
-
once(event: "broadcast", listener: (state: string) => void): this;
|
|
204
|
-
once(event: "notify", listener: (state: string) => void): this;
|
|
205
204
|
once(event: "data", listener: (data: Buffer, isNotification: boolean) => void): this;
|
|
205
|
+
once(event: "write", listener: (error: Error | undefined) => void): this;
|
|
206
206
|
once(event: "descriptorsDiscover", listener: (descriptors: Descriptor[]) => void): this;
|
|
207
|
+
once(event: "broadcast", listener: (state: string) => void): this;
|
|
207
208
|
once(event: string, listener: Function): this;
|
|
208
209
|
}
|
|
209
210
|
|
package/lib/characteristic.js
CHANGED
|
@@ -9,6 +9,7 @@ class Characteristic extends EventEmitter {
|
|
|
9
9
|
this._noble = noble;
|
|
10
10
|
this._peripheralId = peripheralId;
|
|
11
11
|
this._serviceUuid = serviceUuid;
|
|
12
|
+
this._isNotifying = false;
|
|
12
13
|
|
|
13
14
|
this.uuid = uuid;
|
|
14
15
|
this.name = null;
|
|
@@ -21,6 +22,9 @@ class Characteristic extends EventEmitter {
|
|
|
21
22
|
this.name = characteristic.name;
|
|
22
23
|
this.type = characteristic.type;
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
// set the isNotifying state
|
|
27
|
+
this.on('notify', (state, error) => !error ? this._isNotifying = state : null);
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
toString () {
|
|
@@ -39,13 +43,13 @@ class Characteristic extends EventEmitter {
|
|
|
39
43
|
// 'read' for non-notifications is only present for backwards compatbility
|
|
40
44
|
if (!isNotification) {
|
|
41
45
|
// remove the listener
|
|
42
|
-
this.removeListener('
|
|
46
|
+
this.removeListener('data', onRead);
|
|
43
47
|
// call the callback
|
|
44
48
|
callback(error, data);
|
|
45
49
|
}
|
|
46
50
|
};
|
|
47
51
|
|
|
48
|
-
this.on('
|
|
52
|
+
this.on('data', onRead);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
this._noble.read(
|
|
@@ -97,28 +101,38 @@ class Characteristic extends EventEmitter {
|
|
|
97
101
|
});
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
+
subscribe (callback) {
|
|
105
|
+
this._notify(true, callback);
|
|
106
|
+
}
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
);
|
|
108
|
+
async subscribeAsync () {
|
|
109
|
+
return this._noble._withDisconnectHandler(this._peripheralId, () => {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
this.subscribe((error, state) => error ? reject(error) : resolve(state));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
unsubscribe (callback) {
|
|
117
|
+
this._notify(false, callback);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async unsubscribeAsync () {
|
|
114
121
|
return this._noble._withDisconnectHandler(this._peripheralId, () => {
|
|
115
122
|
return new Promise((resolve, reject) => {
|
|
116
|
-
this.
|
|
123
|
+
this.unsubscribe(error => error ? reject(error) : resolve());
|
|
117
124
|
});
|
|
118
125
|
});
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
|
|
128
|
+
_notify (notify, callback) {
|
|
129
|
+
if (notify === this._isNotifying) {
|
|
130
|
+
if (callback) {
|
|
131
|
+
callback(null, this._isNotifying);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
if (callback) {
|
|
123
137
|
this.once('notify', (state, error) => callback(error, state));
|
|
124
138
|
}
|
|
@@ -131,54 +145,115 @@ class Characteristic extends EventEmitter {
|
|
|
131
145
|
);
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
async
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
async *notificationsAsync () {
|
|
149
|
+
const notifications = [];
|
|
150
|
+
let notifying = true;
|
|
151
|
+
|
|
152
|
+
// Main data listener that populates the notifications array
|
|
153
|
+
const dataListener = (data, isNotification, error) => {
|
|
154
|
+
if (error) {
|
|
155
|
+
notifying = false;
|
|
156
|
+
}
|
|
157
|
+
if (isNotification) {
|
|
158
|
+
notifications.push(data);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Notify state listener
|
|
163
|
+
const notifyListener = (state) => {
|
|
164
|
+
notifying = state;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Set up listeners
|
|
168
|
+
this.on('data', dataListener);
|
|
169
|
+
this.on('notify', notifyListener);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Start subscribing
|
|
173
|
+
await this.subscribeAsync();
|
|
174
|
+
|
|
175
|
+
// Process notifications
|
|
176
|
+
while (notifying || notifications.length > 0) {
|
|
177
|
+
if (notifications.length > 0) {
|
|
178
|
+
// If we have notifications, yield them
|
|
179
|
+
yield notifications.shift();
|
|
180
|
+
} else if (notifying) {
|
|
181
|
+
// Wait for more data or notify=false
|
|
182
|
+
await new Promise(resolve => {
|
|
183
|
+
// Create listeners that automatically remove themselves
|
|
184
|
+
const tempDataListener = (...args) => {
|
|
185
|
+
this.removeListener('data', tempDataListener);
|
|
186
|
+
this.removeListener('notify', tempNotifyListener);
|
|
187
|
+
resolve();
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const tempNotifyListener = (state) => {
|
|
191
|
+
if (state === false) {
|
|
192
|
+
this.removeListener('data', tempDataListener);
|
|
193
|
+
this.removeListener('notify', tempNotifyListener);
|
|
194
|
+
resolve();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Set up temporary listeners
|
|
199
|
+
this.once('data', tempDataListener);
|
|
200
|
+
this.once('notify', tempNotifyListener);
|
|
201
|
+
|
|
202
|
+
// Clean up if we already have notifications (race condition)
|
|
203
|
+
if (notifications.length > 0) {
|
|
204
|
+
this.removeListener('data', tempDataListener);
|
|
205
|
+
this.removeListener('notify', tempNotifyListener);
|
|
206
|
+
resolve();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
// Clean up all listeners
|
|
213
|
+
this.removeListener('data', dataListener);
|
|
214
|
+
this.removeListener('notify', notifyListener);
|
|
215
|
+
// Unsubscribe
|
|
216
|
+
await this.unsubscribeAsync();
|
|
217
|
+
}
|
|
144
218
|
}
|
|
145
219
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
}
|
|
220
|
+
discoverDescriptors (callback) {
|
|
221
|
+
if (callback) {
|
|
222
|
+
this.once('descriptorsDiscover', (descriptors, error) => callback(error, descriptors));
|
|
223
|
+
}
|
|
153
224
|
|
|
154
|
-
|
|
155
|
-
|
|
225
|
+
this._noble.discoverDescriptors(
|
|
226
|
+
this._peripheralId,
|
|
227
|
+
this._serviceUuid,
|
|
228
|
+
this.uuid
|
|
229
|
+
);
|
|
156
230
|
}
|
|
157
231
|
|
|
158
|
-
async
|
|
232
|
+
async discoverDescriptorsAsync () {
|
|
159
233
|
return this._noble._withDisconnectHandler(this._peripheralId, () => {
|
|
160
234
|
return new Promise((resolve, reject) => {
|
|
161
|
-
this.
|
|
235
|
+
this.discoverDescriptors((error, descriptors) => error ? reject(error) : resolve(descriptors));
|
|
162
236
|
});
|
|
163
237
|
});
|
|
164
238
|
}
|
|
165
239
|
|
|
166
|
-
|
|
240
|
+
broadcast (broadcast, callback) {
|
|
167
241
|
if (callback) {
|
|
168
|
-
this.once('
|
|
242
|
+
this.once('broadcast', error => callback(error));
|
|
169
243
|
}
|
|
170
244
|
|
|
171
|
-
this._noble.
|
|
245
|
+
this._noble.broadcast(
|
|
172
246
|
this._peripheralId,
|
|
173
247
|
this._serviceUuid,
|
|
174
|
-
this.uuid
|
|
248
|
+
this.uuid,
|
|
249
|
+
broadcast
|
|
175
250
|
);
|
|
176
251
|
}
|
|
177
252
|
|
|
178
|
-
async
|
|
253
|
+
async broadcastAsync (broadcast) {
|
|
179
254
|
return this._noble._withDisconnectHandler(this._peripheralId, () => {
|
|
180
255
|
return new Promise((resolve, reject) => {
|
|
181
|
-
this.
|
|
256
|
+
this.broadcast(broadcast, (state, error) => error ? reject(error) : resolve(state));
|
|
182
257
|
});
|
|
183
258
|
});
|
|
184
259
|
}
|
package/lib/noble.js
CHANGED
|
@@ -247,26 +247,74 @@ class Noble extends EventEmitter {
|
|
|
247
247
|
const deviceQueue = [];
|
|
248
248
|
let scanning = true;
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
// Main discover listener to add devices to the queue
|
|
251
|
+
const discoverListener = peripheral => deviceQueue.push(peripheral);
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
// State change listener
|
|
254
|
+
const scanStopListener = () => scanning = false;
|
|
254
255
|
|
|
255
|
-
|
|
256
|
+
// Set up listeners
|
|
257
|
+
this.on('discover', discoverListener);
|
|
258
|
+
this.once('scanStop', scanStopListener);
|
|
256
259
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
260
|
+
try {
|
|
261
|
+
// Start the scanning process
|
|
262
|
+
await this.startScanningAsync();
|
|
263
|
+
|
|
264
|
+
// Process discovered devices
|
|
265
|
+
while (scanning || deviceQueue.length > 0) {
|
|
266
|
+
if (deviceQueue.length > 0) {
|
|
267
|
+
// If we have devices in the queue, yield them
|
|
268
|
+
yield deviceQueue.shift();
|
|
269
|
+
} else if (scanning) {
|
|
270
|
+
// Wait for either a new device or scan stop
|
|
271
|
+
await new Promise(resolve => {
|
|
272
|
+
const tempDiscoverListener = () => resolve();
|
|
273
|
+
|
|
274
|
+
// Set up a temporary discover listener
|
|
275
|
+
this.once('discover', tempDiscoverListener);
|
|
276
|
+
|
|
277
|
+
// Set up a cleanup for when scanning stops
|
|
278
|
+
const tempScanStopListener = () => {
|
|
279
|
+
this.removeListener('discover', tempDiscoverListener);
|
|
280
|
+
resolve();
|
|
281
|
+
};
|
|
282
|
+
this.once('scanStop', tempScanStopListener);
|
|
283
|
+
|
|
284
|
+
// Handle race condition where a device might arrive during promise setup
|
|
285
|
+
if (deviceQueue.length > 0) {
|
|
286
|
+
this.removeListener('discover', tempDiscoverListener);
|
|
287
|
+
this.removeListener('scanStop', tempScanStopListener);
|
|
288
|
+
resolve();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Optional: Add a maximum wait time, but with proper cleanup
|
|
292
|
+
// This can be removed to eliminate timer dependency
|
|
293
|
+
if (scanning) {
|
|
294
|
+
const timeoutId = setTimeout(() => {
|
|
295
|
+
this.removeListener('discover', tempDiscoverListener);
|
|
296
|
+
this.removeListener('scanStop', tempScanStopListener);
|
|
297
|
+
resolve();
|
|
298
|
+
}, 1000);
|
|
299
|
+
|
|
300
|
+
// Make sure we clear the timeout if we resolve before timeout
|
|
301
|
+
const clearTimeoutFn = () => clearTimeout(timeoutId);
|
|
302
|
+
this.once('discover', clearTimeoutFn);
|
|
303
|
+
this.once('scanStop', clearTimeoutFn);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} finally {
|
|
309
|
+
// Clean up listeners
|
|
310
|
+
this.removeListener('discover', discoverListener);
|
|
311
|
+
this.removeListener('scanStop', scanStopListener);
|
|
312
|
+
|
|
313
|
+
// Ensure scanning is stopped
|
|
314
|
+
if (scanning) {
|
|
315
|
+
await this.stopScanningAsync();
|
|
266
316
|
}
|
|
267
317
|
}
|
|
268
|
-
|
|
269
|
-
await this.stopScanningAsync();
|
|
270
318
|
}
|
|
271
319
|
|
|
272
320
|
_onScanStop () {
|
|
@@ -578,7 +626,7 @@ class Noble extends EventEmitter {
|
|
|
578
626
|
const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];
|
|
579
627
|
|
|
580
628
|
if (characteristic) {
|
|
581
|
-
characteristic.emit('
|
|
629
|
+
characteristic.emit('data', data, isNotification, error);
|
|
582
630
|
} else {
|
|
583
631
|
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} read!`);
|
|
584
632
|
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -80,9 +80,9 @@ describe('characteristic', () => {
|
|
|
80
80
|
const callback = jest.fn();
|
|
81
81
|
|
|
82
82
|
characteristic.read(callback);
|
|
83
|
-
characteristic.emit('
|
|
83
|
+
characteristic.emit('data');
|
|
84
84
|
// Check for single callback
|
|
85
|
-
characteristic.emit('
|
|
85
|
+
characteristic.emit('data');
|
|
86
86
|
|
|
87
87
|
expect(callback).toHaveBeenCalledWith(undefined, undefined);
|
|
88
88
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
@@ -99,9 +99,9 @@ describe('characteristic', () => {
|
|
|
99
99
|
const data = 'data';
|
|
100
100
|
|
|
101
101
|
characteristic.read(callback);
|
|
102
|
-
characteristic.emit('
|
|
102
|
+
characteristic.emit('data', data);
|
|
103
103
|
// Check for single callback
|
|
104
|
-
characteristic.emit('
|
|
104
|
+
characteristic.emit('data', data);
|
|
105
105
|
|
|
106
106
|
expect(callback).toHaveBeenCalledWith(undefined, data);
|
|
107
107
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
@@ -118,9 +118,9 @@ describe('characteristic', () => {
|
|
|
118
118
|
const data = 'data';
|
|
119
119
|
|
|
120
120
|
characteristic.read(callback);
|
|
121
|
-
characteristic.emit('
|
|
121
|
+
characteristic.emit('data', data, true);
|
|
122
122
|
// Check for single callback
|
|
123
|
-
characteristic.emit('
|
|
123
|
+
characteristic.emit('data', data, true);
|
|
124
124
|
|
|
125
125
|
expect(callback).not.toHaveBeenCalled();
|
|
126
126
|
expect(mockNoble.read).toHaveBeenCalledWith(
|
|
@@ -135,7 +135,7 @@ describe('characteristic', () => {
|
|
|
135
135
|
describe('readAsync', () => {
|
|
136
136
|
test('should delegate to noble', async () => {
|
|
137
137
|
const promise = characteristic.readAsync();
|
|
138
|
-
characteristic.emit('
|
|
138
|
+
characteristic.emit('data');
|
|
139
139
|
|
|
140
140
|
await expect(promise).resolves.toBeUndefined();
|
|
141
141
|
expect(mockNoble.read).toHaveBeenCalledWith(
|
|
@@ -148,7 +148,7 @@ describe('characteristic', () => {
|
|
|
148
148
|
|
|
149
149
|
test('should returns without data', async () => {
|
|
150
150
|
const promise = characteristic.readAsync();
|
|
151
|
-
characteristic.emit('
|
|
151
|
+
characteristic.emit('data');
|
|
152
152
|
|
|
153
153
|
await expect(promise).resolves.toBeUndefined();
|
|
154
154
|
expect(mockNoble.read).toHaveBeenCalledWith(
|
|
@@ -163,7 +163,7 @@ describe('characteristic', () => {
|
|
|
163
163
|
const data = 'data';
|
|
164
164
|
|
|
165
165
|
const promise = characteristic.readAsync();
|
|
166
|
-
characteristic.emit('
|
|
166
|
+
characteristic.emit('data', data);
|
|
167
167
|
|
|
168
168
|
await expect(promise).resolves.toEqual(data);
|
|
169
169
|
expect(mockNoble.read).toHaveBeenCalledWith(
|
|
@@ -179,7 +179,7 @@ describe('characteristic', () => {
|
|
|
179
179
|
const data = 'data';
|
|
180
180
|
|
|
181
181
|
const promise = characteristic.readAsync();
|
|
182
|
-
characteristic.emit('
|
|
182
|
+
characteristic.emit('data', data, true);
|
|
183
183
|
|
|
184
184
|
await expect(promise).resolves.toBeUndefined();
|
|
185
185
|
expect(mockNoble.read).toHaveBeenCalledWith(
|
|
@@ -418,7 +418,7 @@ describe('characteristic', () => {
|
|
|
418
418
|
|
|
419
419
|
describe('notify', () => {
|
|
420
420
|
test('should delegate to noble, true', () => {
|
|
421
|
-
characteristic.
|
|
421
|
+
characteristic._notify(true);
|
|
422
422
|
|
|
423
423
|
expect(mockNoble.notify).toHaveBeenCalledWith(
|
|
424
424
|
mockPeripheralId,
|
|
@@ -430,7 +430,10 @@ describe('characteristic', () => {
|
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
test('should delegate to noble, false', () => {
|
|
433
|
-
|
|
433
|
+
|
|
434
|
+
characteristic._isNotifying = true;
|
|
435
|
+
|
|
436
|
+
characteristic._notify(false);
|
|
434
437
|
|
|
435
438
|
expect(mockNoble.notify).toHaveBeenCalledWith(
|
|
436
439
|
mockPeripheralId,
|
|
@@ -444,7 +447,7 @@ describe('characteristic', () => {
|
|
|
444
447
|
test('should callback', () => {
|
|
445
448
|
const callback = jest.fn();
|
|
446
449
|
|
|
447
|
-
characteristic.
|
|
450
|
+
characteristic._notify(true, callback);
|
|
448
451
|
characteristic.emit('notify');
|
|
449
452
|
// Check for single callback
|
|
450
453
|
characteristic.emit('notify');
|
|
@@ -461,36 +464,6 @@ describe('characteristic', () => {
|
|
|
461
464
|
});
|
|
462
465
|
});
|
|
463
466
|
|
|
464
|
-
describe('notifyAsync', () => {
|
|
465
|
-
test('should delegate to noble, true', async () => {
|
|
466
|
-
const promise = characteristic.notifyAsync(true);
|
|
467
|
-
characteristic.emit('notify');
|
|
468
|
-
|
|
469
|
-
await expect(promise).resolves.toBeUndefined();
|
|
470
|
-
expect(mockNoble.notify).toHaveBeenCalledWith(
|
|
471
|
-
mockPeripheralId,
|
|
472
|
-
mockServiceUuid,
|
|
473
|
-
mockUuid,
|
|
474
|
-
true
|
|
475
|
-
);
|
|
476
|
-
expect(mockNoble.notify).toHaveBeenCalledTimes(1);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test('should delegate to noble, false', async () => {
|
|
480
|
-
const promise = characteristic.notifyAsync(false);
|
|
481
|
-
characteristic.emit('notify');
|
|
482
|
-
|
|
483
|
-
await expect(promise).resolves.toBeUndefined();
|
|
484
|
-
expect(mockNoble.notify).toHaveBeenCalledWith(
|
|
485
|
-
mockPeripheralId,
|
|
486
|
-
mockServiceUuid,
|
|
487
|
-
mockUuid,
|
|
488
|
-
false
|
|
489
|
-
);
|
|
490
|
-
expect(mockNoble.notify).toHaveBeenCalledTimes(1);
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
|
-
|
|
494
467
|
describe('subscribe', () => {
|
|
495
468
|
test('should delegate to noble notify, true', () => {
|
|
496
469
|
characteristic.subscribe();
|
|
@@ -542,6 +515,7 @@ describe('characteristic', () => {
|
|
|
542
515
|
|
|
543
516
|
describe('unsubscribe', () => {
|
|
544
517
|
test('should delegate to noble notify, false', () => {
|
|
518
|
+
characteristic._isNotifying = true;
|
|
545
519
|
characteristic.unsubscribe();
|
|
546
520
|
|
|
547
521
|
expect(mockNoble.notify).toHaveBeenCalledWith(
|
|
@@ -555,13 +529,13 @@ describe('characteristic', () => {
|
|
|
555
529
|
|
|
556
530
|
test('should callback', () => {
|
|
557
531
|
const callback = jest.fn();
|
|
558
|
-
|
|
532
|
+
characteristic._isNotifying = true;
|
|
559
533
|
characteristic.unsubscribe(callback);
|
|
560
|
-
characteristic.emit('notify');
|
|
534
|
+
characteristic.emit('notify', false);
|
|
561
535
|
// Check for single callback
|
|
562
|
-
characteristic.emit('notify');
|
|
536
|
+
characteristic.emit('notify', false);
|
|
563
537
|
|
|
564
|
-
expect(callback).toHaveBeenCalledWith(undefined,
|
|
538
|
+
expect(callback).toHaveBeenCalledWith(undefined, false);
|
|
565
539
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
566
540
|
expect(mockNoble.notify).toHaveBeenCalledWith(
|
|
567
541
|
mockPeripheralId,
|
|
@@ -574,9 +548,10 @@ describe('characteristic', () => {
|
|
|
574
548
|
});
|
|
575
549
|
|
|
576
550
|
describe('unsubscribeAsync', () => {
|
|
577
|
-
test('should delegate to noble notify, false', async () => {
|
|
551
|
+
test('should delegate to noble notify, false', async () => {
|
|
552
|
+
characteristic._isNotifying = true;
|
|
578
553
|
const promise = characteristic.unsubscribeAsync();
|
|
579
|
-
characteristic.emit('notify');
|
|
554
|
+
characteristic.emit('notify', false);
|
|
580
555
|
|
|
581
556
|
await expect(promise).resolves.toBeUndefined();
|
|
582
557
|
expect(mockNoble.notify).toHaveBeenCalledWith(
|
|
@@ -589,6 +564,187 @@ describe('characteristic', () => {
|
|
|
589
564
|
});
|
|
590
565
|
});
|
|
591
566
|
|
|
567
|
+
describe('notificationsAsync', () => {
|
|
568
|
+
test('should call subscribeAsync and unsubscribeAsync', async () => {
|
|
569
|
+
// Spy on subscribeAsync and unsubscribeAsync
|
|
570
|
+
jest.spyOn(characteristic, 'subscribeAsync');
|
|
571
|
+
jest.spyOn(characteristic, 'unsubscribeAsync');
|
|
572
|
+
|
|
573
|
+
// Create an async generator
|
|
574
|
+
const iterator = characteristic.notificationsAsync();
|
|
575
|
+
|
|
576
|
+
// Wait for iterator setup with setTimeout
|
|
577
|
+
setTimeout(() => {
|
|
578
|
+
// Emit a data event with non-notification
|
|
579
|
+
characteristic.emit('notify', true);
|
|
580
|
+
characteristic.emit('data', 'test-data-1', true);
|
|
581
|
+
}, 10);
|
|
582
|
+
|
|
583
|
+
// Get the value from the iterator
|
|
584
|
+
const result = await iterator.next();
|
|
585
|
+
|
|
586
|
+
// Stop the iteration with timeout to ensure proper sequence
|
|
587
|
+
setTimeout(() => {
|
|
588
|
+
characteristic.emit('notify', false);
|
|
589
|
+
}, 10);
|
|
590
|
+
|
|
591
|
+
// We need to consume until the end to ensure unsubscribeAsync is called
|
|
592
|
+
await iterator.next();
|
|
593
|
+
|
|
594
|
+
// Verify that subscribeAsync was called
|
|
595
|
+
expect(characteristic.subscribeAsync).toHaveBeenCalledTimes(1);
|
|
596
|
+
expect(characteristic.unsubscribeAsync).toHaveBeenCalledTimes(1);
|
|
597
|
+
|
|
598
|
+
// Check the yielded data
|
|
599
|
+
expect(result.value).toEqual('test-data-1');
|
|
600
|
+
expect(result.done).toBe(false);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test('should yield multiple data values', async () => {
|
|
604
|
+
const data1 = 'test-data-1';
|
|
605
|
+
const data2 = 'test-data-2';
|
|
606
|
+
|
|
607
|
+
// Assume that the characteristic is already notifying
|
|
608
|
+
characteristic._isNotifying = true;
|
|
609
|
+
|
|
610
|
+
// Setup the async iterator
|
|
611
|
+
const iterator = characteristic.notificationsAsync();
|
|
612
|
+
|
|
613
|
+
// Emit events with setTimeout to ensure proper sequence
|
|
614
|
+
setTimeout(() => {
|
|
615
|
+
// Emit first data event (non-notification)
|
|
616
|
+
characteristic.emit('data', data1, true);
|
|
617
|
+
}, 10);
|
|
618
|
+
|
|
619
|
+
// Get first value
|
|
620
|
+
const result1 = await iterator.next();
|
|
621
|
+
|
|
622
|
+
// Emit second data event with timeout
|
|
623
|
+
setTimeout(() => {
|
|
624
|
+
characteristic.emit('data', data2, true);
|
|
625
|
+
}, 10);
|
|
626
|
+
|
|
627
|
+
// Get second value
|
|
628
|
+
const result2 = await iterator.next();
|
|
629
|
+
|
|
630
|
+
// End the notifications
|
|
631
|
+
setTimeout(() => {
|
|
632
|
+
characteristic.emit('notify', false);
|
|
633
|
+
}, 10);
|
|
634
|
+
|
|
635
|
+
// Consume until complete
|
|
636
|
+
await iterator.next();
|
|
637
|
+
|
|
638
|
+
// Check the yielded values
|
|
639
|
+
expect(result1.value).toEqual(data1);
|
|
640
|
+
expect(result1.done).toBe(false);
|
|
641
|
+
expect(result2.value).toEqual(data2);
|
|
642
|
+
expect(result2.done).toBe(false);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test('should ignore notification data', async () => {
|
|
646
|
+
// Setup the async iterator
|
|
647
|
+
const iterator = characteristic.notificationsAsync();
|
|
648
|
+
|
|
649
|
+
// Assume that the characteristic is already notifying
|
|
650
|
+
characteristic._isNotifying = true;
|
|
651
|
+
|
|
652
|
+
setTimeout(() => {
|
|
653
|
+
// Emit real data
|
|
654
|
+
characteristic.emit('data', 'actual-data', true);
|
|
655
|
+
|
|
656
|
+
// Emit notification data that should be ignored
|
|
657
|
+
characteristic.emit('data', 'ignored-data', true);
|
|
658
|
+
}, 10);
|
|
659
|
+
|
|
660
|
+
// Get the value
|
|
661
|
+
const result = await iterator.next();
|
|
662
|
+
|
|
663
|
+
// End the notifications
|
|
664
|
+
setTimeout(() => {
|
|
665
|
+
characteristic.emit('notify', false);
|
|
666
|
+
}, 10);
|
|
667
|
+
|
|
668
|
+
// Consume until complete
|
|
669
|
+
await iterator.next();
|
|
670
|
+
|
|
671
|
+
// Check that notification data was ignored
|
|
672
|
+
expect(result.value).toEqual('actual-data');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test('should end iteration on error', async () => {
|
|
676
|
+
// Setup the async iterator
|
|
677
|
+
const iterator = characteristic.notificationsAsync();
|
|
678
|
+
|
|
679
|
+
setTimeout(() => {
|
|
680
|
+
// Emit an error
|
|
681
|
+
characteristic.emit('data', null, false, new Error('test error'));
|
|
682
|
+
}, 10);
|
|
683
|
+
|
|
684
|
+
// Get the next value - should complete the iterator
|
|
685
|
+
const result = await iterator.next();
|
|
686
|
+
const doneResult = await iterator.next();
|
|
687
|
+
|
|
688
|
+
// Check that the iteration is complete
|
|
689
|
+
expect(doneResult.done).toBe(true);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test('should end iteration when notify is false', async () => {
|
|
693
|
+
// Setup the async iterator
|
|
694
|
+
const iterator = characteristic.notificationsAsync();
|
|
695
|
+
|
|
696
|
+
setTimeout(() => {
|
|
697
|
+
// End the notifications
|
|
698
|
+
characteristic.emit('notify', false);
|
|
699
|
+
}, 10);
|
|
700
|
+
|
|
701
|
+
// Get the next value - should complete the iterator
|
|
702
|
+
const result = await iterator.next();
|
|
703
|
+
|
|
704
|
+
// Check that the iteration is complete
|
|
705
|
+
expect(result.done).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Your working test (already correct)
|
|
709
|
+
test('should handle notify event states correctly', async () => {
|
|
710
|
+
// Setup the async iterator
|
|
711
|
+
const iterator = characteristic.notificationsAsync();
|
|
712
|
+
|
|
713
|
+
setTimeout(() => {
|
|
714
|
+
// subscribeAsync would trigger a notify event with true
|
|
715
|
+
characteristic.emit('notify', true);
|
|
716
|
+
// Emit some data
|
|
717
|
+
characteristic.emit('data', 'test-data-1', true);
|
|
718
|
+
characteristic.emit('data', 'test-data-2', true);
|
|
719
|
+
characteristic.emit('data', 'test-data-3', true);
|
|
720
|
+
}, 10);
|
|
721
|
+
|
|
722
|
+
// Get the value
|
|
723
|
+
const result = await iterator.next();
|
|
724
|
+
expect(result.value).toEqual('test-data-1');
|
|
725
|
+
expect(result.done).toBe(false);
|
|
726
|
+
|
|
727
|
+
const result2 = await iterator.next();
|
|
728
|
+
expect(result2.value).toEqual('test-data-2');
|
|
729
|
+
expect(result2.done).toBe(false);
|
|
730
|
+
|
|
731
|
+
const result3 = await iterator.next();
|
|
732
|
+
expect(result3.value).toEqual('test-data-3');
|
|
733
|
+
expect(result3.done).toBe(false);
|
|
734
|
+
|
|
735
|
+
setTimeout(() => {
|
|
736
|
+
// Now unsubscribe by emitting notify with false
|
|
737
|
+
characteristic.emit('notify', false);
|
|
738
|
+
}, 10);
|
|
739
|
+
|
|
740
|
+
// The iterator should complete
|
|
741
|
+
const doneResult = await iterator.next();
|
|
742
|
+
|
|
743
|
+
// Check that the iteration is complete
|
|
744
|
+
expect(doneResult.done).toBe(true);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
592
748
|
describe('discoverDescriptors', () => {
|
|
593
749
|
test('should delegate to noble', () => {
|
|
594
750
|
characteristic.discoverDescriptors();
|