@stoprocent/noble 2.0.1 → 2.1.1

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 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
- __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.
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
- }, 2500);
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
 
@@ -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('read', onRead);
46
+ this.removeListener('data', onRead);
43
47
  // call the callback
44
48
  callback(error, data);
45
49
  }
46
50
  };
47
51
 
48
- this.on('read', onRead);
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
- broadcast (broadcast, callback) {
101
- if (callback) {
102
- this.once('broadcast', error => callback(error));
103
- }
104
+ subscribe (callback) {
105
+ this._notify(true, callback);
106
+ }
104
107
 
105
- this._noble.broadcast(
106
- this._peripheralId,
107
- this._serviceUuid,
108
- this.uuid,
109
- broadcast
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
- async broadcastAsync (broadcast) {
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.broadcast(broadcast, (state, error) => error ? reject(error) : resolve(state));
123
+ this.unsubscribe(error => error ? reject(error) : resolve());
117
124
  });
118
125
  });
119
126
  }
120
127
 
121
- notify (notify, callback) {
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 notifyAsync (notify) {
135
- return this._noble._withDisconnectHandler(this._peripheralId, () => {
136
- return new Promise((resolve, reject) => {
137
- this.notify(notify, (error, state) => error ? reject(error) : resolve(state));
138
- });
139
- });
140
- }
141
-
142
- subscribe (callback) {
143
- this.notify(true, callback);
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
- async subscribeAsync () {
147
- return this._noble._withDisconnectHandler(this._peripheralId, () => {
148
- return new Promise((resolve, reject) => {
149
- this.subscribe((error, state) => error ? reject(error) : resolve(state));
150
- });
151
- });
152
- }
220
+ discoverDescriptors (callback) {
221
+ if (callback) {
222
+ this.once('descriptorsDiscover', (descriptors, error) => callback(error, descriptors));
223
+ }
153
224
 
154
- unsubscribe (callback) {
155
- this.notify(false, callback);
225
+ this._noble.discoverDescriptors(
226
+ this._peripheralId,
227
+ this._serviceUuid,
228
+ this.uuid
229
+ );
156
230
  }
157
231
 
158
- async unsubscribeAsync () {
232
+ async discoverDescriptorsAsync () {
159
233
  return this._noble._withDisconnectHandler(this._peripheralId, () => {
160
234
  return new Promise((resolve, reject) => {
161
- this.unsubscribe(error => error ? reject(error) : resolve());
235
+ this.discoverDescriptors((error, descriptors) => error ? reject(error) : resolve(descriptors));
162
236
  });
163
237
  });
164
238
  }
165
239
 
166
- discoverDescriptors (callback) {
240
+ broadcast (broadcast, callback) {
167
241
  if (callback) {
168
- this.once('descriptorsDiscover', (descriptors, error) => callback(error, descriptors));
242
+ this.once('broadcast', error => callback(error));
169
243
  }
170
244
 
171
- this._noble.discoverDescriptors(
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 discoverDescriptorsAsync () {
253
+ async broadcastAsync (broadcast) {
179
254
  return this._noble._withDisconnectHandler(this._peripheralId, () => {
180
255
  return new Promise((resolve, reject) => {
181
- this.discoverDescriptors((error, descriptors) => error ? reject(error) : resolve(descriptors));
256
+ this.broadcast(broadcast, (state, error) => error ? reject(error) : resolve(state));
182
257
  });
183
258
  });
184
259
  }
@@ -11,6 +11,7 @@
11
11
  bool pendingRead;
12
12
  }
13
13
  @property (strong) CBCentralManager *centralManager;
14
+ @property (assign) CBManagerState lastState;
14
15
  @property dispatch_queue_t dispatchQueue;
15
16
  @property NSMutableDictionary *peripherals;
16
17
  @property NSMutableSet *discovered;
@@ -22,6 +22,15 @@
22
22
 
23
23
  - (void)centralManagerDidUpdateState:(CBCentralManager *)central
24
24
  {
25
+ if (central.state != self.lastState && self.lastState == CBManagerStatePoweredOff && central.state == CBManagerStatePoweredOn) {
26
+ [self.peripherals enumerateKeysAndObjectsUsingBlock:^(id key, CBPeripheral* peripheral, BOOL *stop) {
27
+ if (peripheral.state == CBPeripheralStateConnected) {
28
+ [self.centralManager cancelPeripheralConnection:peripheral];
29
+ }
30
+ }];
31
+ }
32
+
33
+ self.lastState = central.state;
25
34
  auto state = stateToString(central.state);
26
35
  emit.RadioState(state);
27
36
  }
@@ -142,6 +151,7 @@
142
151
  // Simulate discovery handling
143
152
  [self centralManager:central didDiscoverPeripheral:peripheral advertisementData:advertisementData RSSI:RSSI];
144
153
  }
154
+
145
155
  std::string uuid = getUuid(peripheral);
146
156
  emit.Connected(uuid, "");
147
157
  }
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
- this.once('scanStop', () => scanning = false);
250
+ // Main discover listener to add devices to the queue
251
+ const discoverListener = peripheral => deviceQueue.push(peripheral);
251
252
 
252
- const listener = peripheral => deviceQueue.push(peripheral);
253
- this.on('discover', listener);
253
+ // State change listener
254
+ const scanStopListener = () => scanning = false;
254
255
 
255
- await this.startScanningAsync();
256
+ // Set up listeners
257
+ this.on('discover', discoverListener);
258
+ this.once('scanStop', scanStopListener);
256
259
 
257
- while (scanning || deviceQueue.length > 0) {
258
- if (deviceQueue.length > 0) {
259
- yield deviceQueue.shift();
260
- } else {
261
- await new Promise(resolve => {
262
- const tempListener = () => resolve();
263
- this.once('discover', tempListener);
264
- setTimeout(tempListener, 1000);
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('read', data, isNotification, error);
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
@@ -6,7 +6,7 @@
6
6
  "license": "MIT",
7
7
  "name": "@stoprocent/noble",
8
8
  "description": "A Node.js BLE (Bluetooth Low Energy) central library.",
9
- "version": "2.0.1",
9
+ "version": "2.1.1",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "https://github.com/stoprocent/noble.git"
@@ -80,9 +80,9 @@ describe('characteristic', () => {
80
80
  const callback = jest.fn();
81
81
 
82
82
  characteristic.read(callback);
83
- characteristic.emit('read');
83
+ characteristic.emit('data');
84
84
  // Check for single callback
85
- characteristic.emit('read');
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('read', data);
102
+ characteristic.emit('data', data);
103
103
  // Check for single callback
104
- characteristic.emit('read', data);
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('read', data, true);
121
+ characteristic.emit('data', data, true);
122
122
  // Check for single callback
123
- characteristic.emit('read', data, true);
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('read');
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('read');
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('read', data);
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('read', data, true);
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.notify(true);
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
- characteristic.notify(false);
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.notify(true, callback);
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, 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();