incyclist-devices 3.0.26 → 3.0.28

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.
@@ -17,6 +17,8 @@ class BleAdapter extends adpater_js_1.default {
17
17
  onDeviceDisconnectHandler = this.emit.bind(this);
18
18
  onDisconnectDoneHandler = this.onDisconnectDone.bind(this);
19
19
  startTask;
20
+ startPromise;
21
+ stopPromise;
20
22
  constructor(settings, props) {
21
23
  super(settings, props);
22
24
  this.deviceData = {};
@@ -198,8 +200,11 @@ class BleAdapter extends adpater_js_1.default {
198
200
  return 'connected';
199
201
  }
200
202
  async start(startProps) {
201
- if (this.isStarting()) {
202
- await this.stop();
203
+ while (this.stopPromise !== undefined) {
204
+ await this.stopPromise;
205
+ }
206
+ if (this.isStarting() && this.startPromise !== undefined) {
207
+ return this.startPromise;
203
208
  }
204
209
  const ble = this.getBle();
205
210
  ble.once('disconnect-done', this.onDisconnectDoneHandler);
@@ -209,11 +214,20 @@ class BleAdapter extends adpater_js_1.default {
209
214
  errorOnTimeout: false,
210
215
  log: this.logEvent.bind(this)
211
216
  });
212
- const res = await this.startTask.run();
213
- if (!res) {
214
- ble.removeListener('disconnect-done', this.onDisconnectDoneHandler);
217
+ this.startPromise = this.runStartTask(ble);
218
+ return this.startPromise;
219
+ }
220
+ async runStartTask(ble) {
221
+ try {
222
+ const res = await this.startTask.run();
223
+ if (!res) {
224
+ ble.removeListener('disconnect-done', this.onDisconnectDoneHandler);
225
+ }
226
+ return res;
227
+ }
228
+ finally {
229
+ this.startPromise = undefined;
215
230
  }
216
- return res;
217
231
  }
218
232
  isStarting() {
219
233
  return this.startTask?.isRunning();
@@ -331,7 +345,10 @@ class BleAdapter extends adpater_js_1.default {
331
345
  }
332
346
  const sensor = this.getSensor();
333
347
  let connected = await sensor.startSensor();
334
- await sensor.subscribe();
348
+ const subscribed = await sensor.subscribe();
349
+ if (connected && !subscribed) {
350
+ connected = false;
351
+ }
335
352
  if (connected) {
336
353
  sensor.on('data', this.onDeviceDataHandler);
337
354
  sensor.on('disconnected', this.onDeviceDisconnectHandler);
@@ -379,9 +396,23 @@ class BleAdapter extends adpater_js_1.default {
379
396
  return success;
380
397
  }
381
398
  async stop() {
399
+ if (this.stopPromise !== undefined) {
400
+ return this.stopPromise;
401
+ }
402
+ this.stopPromise = this.runStop();
403
+ try {
404
+ return await this.stopPromise;
405
+ }
406
+ finally {
407
+ this.stopPromise = undefined;
408
+ }
409
+ }
410
+ async runStop() {
382
411
  this.logEvent({ message: 'stopping device', device: this.getName(), interface: this.getInterface() });
383
412
  if (this.isStarting()) {
384
- await this.startTask.stop();
413
+ const task = this.startTask;
414
+ await task.stop();
415
+ await task.getUnderlyingPromise()?.catch(() => { });
385
416
  }
386
417
  this.started = false;
387
418
  this.resetData();
@@ -15,6 +15,7 @@ class BlePeripheral {
15
15
  disconnecting = false;
16
16
  disconnectedSignalled = false;
17
17
  discoveredServiceUUIds;
18
+ serviceDiscoveryIncomplete = false;
18
19
  discoverServicesPromise;
19
20
  discoverCharacteristicsPromise = {};
20
21
  onErrorHandler = this.onPeripheralError.bind(this);
@@ -158,8 +159,26 @@ class BlePeripheral {
158
159
  const promise = this.discoverServicesPromise;
159
160
  const res = await promise;
160
161
  (0, utils_js_1.sleep)(0).then(() => { delete this.discoverServicesPromise; });
162
+ const isComplete = this.checkAnnouncedServices(res);
163
+ this.serviceDiscoveryIncomplete = !isComplete;
164
+ if (!isComplete) {
165
+ const { name, address } = this.getInfo();
166
+ this.logEvent({ message: 'service data incomplete - disconnecting', name, address,
167
+ announced: this.getAnnouncedServices(), discovered: this.getDiscoveredServices() });
168
+ }
161
169
  return res;
162
170
  }
171
+ checkAnnouncedServices(discovered) {
172
+ const announced = this.getAnnouncedServices().map(x => (0, utils_js_2.beautifyUUID)(x));
173
+ const toBeChecked = discovered.map(x => (0, utils_js_2.beautifyUUID)(x));
174
+ const cntAnnounced = announced.length;
175
+ let cntVerified = 0;
176
+ for (const s of announced) {
177
+ if (toBeChecked.includes(s))
178
+ cntVerified++;
179
+ }
180
+ return cntAnnounced === cntVerified;
181
+ }
163
182
  async _discoverServices() {
164
183
  if (!this.getPeripheral())
165
184
  return [];
@@ -309,6 +328,14 @@ class BlePeripheral {
309
328
  }
310
329
  catch { }
311
330
  }
331
+ if (this.serviceDiscoveryIncomplete) {
332
+ this.logEvent({ message: 'peripheral subscribe selected failed', uuids, reason: 'service discovery incomplete',
333
+ announced: this.getAnnouncedServices(), discovered: this.getDiscoveredServices() });
334
+ this.discoveredServiceUUIds = undefined;
335
+ this.serviceDiscoveryIncomplete = false;
336
+ await this.disconnect().catch(() => { });
337
+ return false;
338
+ }
312
339
  try {
313
340
  if (Object.keys(this.characteristics).length === 0) {
314
341
  await this.discoverAllCharacteristics();
@@ -349,6 +376,11 @@ class BlePeripheral {
349
376
  const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], []);
350
377
  const found = [];
351
378
  const uuids = [];
379
+ const freshUuids = new Set(res.characteristics.map(c => (0, utils_js_2.beautifyUUID)(c.uuid)));
380
+ Object.keys(this.characteristics).forEach(existing => {
381
+ if (!freshUuids.has(existing))
382
+ delete this.characteristics[existing];
383
+ });
352
384
  res.characteristics.forEach(c => {
353
385
  this.characteristics[(0, utils_js_2.beautifyUUID)(c.uuid)] = c;
354
386
  found.push(c.uuid);
@@ -367,6 +399,12 @@ class BlePeripheral {
367
399
  const target = characteristics.map(c => (0, utils_js_2.fullUUID)(c));
368
400
  const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], target);
369
401
  const found = [];
402
+ const freshUuids = new Set(res.characteristics.map(c => (0, utils_js_2.beautifyUUID)(c.uuid)));
403
+ characteristics.forEach(requested => {
404
+ const requestedUuid = (0, utils_js_2.beautifyUUID)(requested);
405
+ if (!freshUuids.has(requestedUuid))
406
+ delete this.characteristics[requestedUuid];
407
+ });
370
408
  res.characteristics.forEach(c => {
371
409
  this.characteristics[(0, utils_js_2.beautifyUUID)(c.uuid)] = c;
372
410
  found.push(c.uuid);
@@ -436,25 +474,61 @@ class BlePeripheral {
436
474
  return Promise.reject(new Error('characteristic not found: ' + characteristicUUID));
437
475
  }
438
476
  }
477
+ const signal = options?.signal;
439
478
  return new Promise((resolve, reject) => {
479
+ let settled = false;
480
+ let onData;
481
+ const cleanup = () => {
482
+ if (onData) {
483
+ c.off('data', onData);
484
+ onData = undefined;
485
+ }
486
+ signal?.removeEventListener('abort', onAbort);
487
+ };
488
+ const settleResolve = (result) => {
489
+ if (settled)
490
+ return;
491
+ settled = true;
492
+ cleanup();
493
+ resolve(result);
494
+ };
495
+ const settleReject = (err) => {
496
+ if (settled)
497
+ return;
498
+ settled = true;
499
+ cleanup();
500
+ reject(err);
501
+ };
502
+ const onAbort = () => {
503
+ settleReject(new Error('aborted'));
504
+ };
505
+ if (signal) {
506
+ if (signal.aborted) {
507
+ settleReject(new Error('aborted'));
508
+ return;
509
+ }
510
+ signal.addEventListener('abort', onAbort, { once: true });
511
+ }
440
512
  const write = () => {
441
- if (this.disconnecting || !this.connected)
442
- return Promise.resolve(Buffer.from([]));
443
- c.on('data', (data) => {
444
- c.removeAllListeners('data');
445
- resolve(data);
446
- });
513
+ if (this.disconnecting || !this.connected) {
514
+ settleResolve(Buffer.from([]));
515
+ return;
516
+ }
517
+ onData = (responseData) => {
518
+ settleResolve(responseData);
519
+ };
520
+ c.on('data', onData);
447
521
  this.logEvent({ message: 'write request', characteristic: uuid, data: data.toString('hex'), withoutResponse: options?.withoutResponse === true });
448
522
  c.write(data, options?.withoutResponse === true, (err) => {
449
523
  if (err)
450
- reject(err);
524
+ settleReject(err);
451
525
  });
452
526
  if (options?.withoutResponse) {
453
- resolve(Buffer.from([]));
527
+ settleResolve(Buffer.from([]));
454
528
  }
455
529
  };
456
530
  if (!options?.withoutResponse) {
457
- this.subscribe(characteristicUUID, null).then(success => {
531
+ this.subscribe(characteristicUUID, null).then(() => {
458
532
  write();
459
533
  });
460
534
  }
@@ -79,6 +79,7 @@ class TBleSensor extends node_events_1.EventEmitter {
79
79
  }
80
80
  this.connectPromise = this.connectPromise ?? this.peripheral.connect();
81
81
  const connected = await this.connectPromise;
82
+ delete this.connectPromise;
82
83
  if (!connected)
83
84
  return false;
84
85
  if (!reconnect)
@@ -225,24 +225,33 @@ class BleFmAdapter extends adapter_js_1.default {
225
225
  let hasControl = false;
226
226
  let tryCnt = 0;
227
227
  const sensor = this.getSensor();
228
+ let cancelled = false;
229
+ const controlAbort = new AbortController();
230
+ const abandon = () => {
231
+ if (cancelled)
232
+ return;
233
+ cancelled = true;
234
+ controlAbort.abort();
235
+ };
228
236
  const controlPromise = new Promise((resolve) => {
229
237
  this.startTask.notifyOnStop(() => {
238
+ abandon();
230
239
  resolve(false);
231
240
  });
232
241
  const waitUntilControl = async () => {
233
242
  if (this.supportsVirtualShifting()) {
234
243
  await this.initVirtualShifting();
235
244
  }
236
- while (!hasControl && this.isStarting()) {
245
+ while (!hasControl && !cancelled && this.isStarting()) {
237
246
  if (tryCnt++ === 0) {
238
247
  this.logEvent({ message: 'requesting control', device: this.getName(), interface: this.getInterface() });
239
248
  }
240
- hasControl = await sensor.requestControl();
249
+ hasControl = await sensor.requestControl(controlAbort.signal);
241
250
  if (hasControl) {
242
251
  this.logEvent({ message: 'control granted', device: this.getName(), interface: this.getInterface() });
243
252
  resolve(this.isStarting());
244
253
  }
245
- else {
254
+ else if (!cancelled) {
246
255
  await (0, utils_js_1.sleep)(this.requestControlRetryDelay);
247
256
  }
248
257
  }
@@ -253,10 +262,12 @@ class BleFmAdapter extends adapter_js_1.default {
253
262
  timeout: this.requestControlTimeout,
254
263
  name: 'establishControl',
255
264
  errorOnTimeout: false,
256
- log: this.logEvent.bind(this)
265
+ log: this.logEvent.bind(this),
266
+ onCancel: abandon
257
267
  });
258
268
  const granted = await waitTask.run();
259
269
  if (!granted && this.isStarting()) {
270
+ abandon();
260
271
  this.logEvent({ message: 'could not establish control', device: this.getName(), interface: this.getInterface() });
261
272
  throw new Error('could not establish control');
262
273
  }
@@ -105,7 +105,7 @@ class BleFitnessMachineDevice extends sensor_js_1.TBleSensor {
105
105
  this.hasControl = false;
106
106
  this.ftmsServiceDataAttempts = 0;
107
107
  }
108
- async requestControl() {
108
+ async requestControl(signal) {
109
109
  if (this.hasControl) {
110
110
  return true;
111
111
  }
@@ -115,7 +115,7 @@ class BleFitnessMachineDevice extends sensor_js_1.TBleSensor {
115
115
  this.isCheckingControl = true;
116
116
  const data = Buffer.alloc(1);
117
117
  data.writeUInt8(0, 0);
118
- const res = await this.writeFtmsMessage(0, data, { timeout: 5000 });
118
+ const res = await this.writeFtmsMessage(0, data, { timeout: 5000, signal });
119
119
  if (res === 1) {
120
120
  this.hasControl = true;
121
121
  }
@@ -622,9 +622,15 @@ class BleFitnessMachineDevice extends sensor_js_1.TBleSensor {
622
622
  let res;
623
623
  let tsStart = Date.now();
624
624
  if (props?.timeout) {
625
- res = await new task_js_1.InteruptableTask(this.write(consts_js_1.FTMS_CP, data, props), {
625
+ const internalAbort = new AbortController();
626
+ const combined = new AbortController();
627
+ props.signal?.addEventListener('abort', () => combined.abort(), { once: true });
628
+ internalAbort.signal.addEventListener('abort', () => combined.abort(), { once: true });
629
+ const signal = combined.signal;
630
+ res = await new task_js_1.InteruptableTask(this.write(consts_js_1.FTMS_CP, data, { ...props, signal }), {
626
631
  timeout: props.timeout ?? 800,
627
- errorOnTimeout: true
632
+ errorOnTimeout: true,
633
+ onCancel: () => internalAbort.abort()
628
634
  }).run();
629
635
  }
630
636
  else {
@@ -10,6 +10,7 @@ class InteruptableTask {
10
10
  internalEvents = new node_events_1.EventEmitter();
11
11
  promise;
12
12
  onStopNotifiers = [];
13
+ cancelNotified = false;
13
14
  constructor(promise, props) {
14
15
  this.state = (props?.state ?? {});
15
16
  this.props = props;
@@ -20,6 +21,9 @@ class InteruptableTask {
20
21
  getPromise() {
21
22
  return this.internalState.promise;
22
23
  }
24
+ getUnderlyingPromise() {
25
+ return this.promise;
26
+ }
23
27
  getState() {
24
28
  return this.state;
25
29
  }
@@ -40,6 +44,7 @@ class InteruptableTask {
40
44
  const { timeout } = this.props;
41
45
  if (timeout) {
42
46
  this.internalState.tsTimeout = this.internalState.tsStart + timeout;
47
+ this.internalState.timeoutDuration = timeout;
43
48
  this.internalState.onTimeout = this.onTimeout.bind(this);
44
49
  this.internalState.timeout = setTimeout(() => { this.internalEvents.emit('timeout'); }, timeout);
45
50
  this.internalEvents.on('timeout', this.internalState.onTimeout);
@@ -53,6 +58,7 @@ class InteruptableTask {
53
58
  this.sendStopNotification();
54
59
  if (this.getState().result === 'completed' || this.getState().result === 'error')
55
60
  return;
61
+ this.notifyCancel();
56
62
  this.getState().result = 'stopped';
57
63
  if (this.props.onDone)
58
64
  resolve(this.props.onDone(this.getState()));
@@ -97,8 +103,9 @@ class InteruptableTask {
97
103
  if (!this.internalState.timeout)
98
104
  return;
99
105
  const message = this.props.name ? `${this.props.name} timeout` : 'timeout';
100
- this.logEvent({ message, active: this.isRunning() });
106
+ this.logEvent({ message, active: this.isRunning(), duration: this.internalState?.timeoutDuration });
101
107
  this.clearTimeout();
108
+ this.notifyCancel();
102
109
  this.getState().result = 'timeout';
103
110
  const resolve = this.internalState.onDone;
104
111
  const reject = this.internalState.onError;
@@ -111,6 +118,12 @@ class InteruptableTask {
111
118
  else
112
119
  resolve(null);
113
120
  }
121
+ notifyCancel() {
122
+ if (this.cancelNotified)
123
+ return;
124
+ this.cancelNotified = true;
125
+ this.props.onCancel?.();
126
+ }
114
127
  sendStopNotification() {
115
128
  this.onStopNotifiers.forEach((cb) => {
116
129
  if (typeof cb === 'function')
@@ -12,6 +12,8 @@ export default class BleAdapter extends IncyclistDevice {
12
12
  onDeviceDisconnectHandler = this.emit.bind(this);
13
13
  onDisconnectDoneHandler = this.onDisconnectDone.bind(this);
14
14
  startTask;
15
+ startPromise;
16
+ stopPromise;
15
17
  constructor(settings, props) {
16
18
  super(settings, props);
17
19
  this.deviceData = {};
@@ -193,8 +195,11 @@ export default class BleAdapter extends IncyclistDevice {
193
195
  return 'connected';
194
196
  }
195
197
  async start(startProps) {
196
- if (this.isStarting()) {
197
- await this.stop();
198
+ while (this.stopPromise !== undefined) {
199
+ await this.stopPromise;
200
+ }
201
+ if (this.isStarting() && this.startPromise !== undefined) {
202
+ return this.startPromise;
198
203
  }
199
204
  const ble = this.getBle();
200
205
  ble.once('disconnect-done', this.onDisconnectDoneHandler);
@@ -204,11 +209,20 @@ export default class BleAdapter extends IncyclistDevice {
204
209
  errorOnTimeout: false,
205
210
  log: this.logEvent.bind(this)
206
211
  });
207
- const res = await this.startTask.run();
208
- if (!res) {
209
- ble.removeListener('disconnect-done', this.onDisconnectDoneHandler);
212
+ this.startPromise = this.runStartTask(ble);
213
+ return this.startPromise;
214
+ }
215
+ async runStartTask(ble) {
216
+ try {
217
+ const res = await this.startTask.run();
218
+ if (!res) {
219
+ ble.removeListener('disconnect-done', this.onDisconnectDoneHandler);
220
+ }
221
+ return res;
222
+ }
223
+ finally {
224
+ this.startPromise = undefined;
210
225
  }
211
- return res;
212
226
  }
213
227
  isStarting() {
214
228
  return this.startTask?.isRunning();
@@ -326,7 +340,10 @@ export default class BleAdapter extends IncyclistDevice {
326
340
  }
327
341
  const sensor = this.getSensor();
328
342
  let connected = await sensor.startSensor();
329
- await sensor.subscribe();
343
+ const subscribed = await sensor.subscribe();
344
+ if (connected && !subscribed) {
345
+ connected = false;
346
+ }
330
347
  if (connected) {
331
348
  sensor.on('data', this.onDeviceDataHandler);
332
349
  sensor.on('disconnected', this.onDeviceDisconnectHandler);
@@ -374,9 +391,23 @@ export default class BleAdapter extends IncyclistDevice {
374
391
  return success;
375
392
  }
376
393
  async stop() {
394
+ if (this.stopPromise !== undefined) {
395
+ return this.stopPromise;
396
+ }
397
+ this.stopPromise = this.runStop();
398
+ try {
399
+ return await this.stopPromise;
400
+ }
401
+ finally {
402
+ this.stopPromise = undefined;
403
+ }
404
+ }
405
+ async runStop() {
377
406
  this.logEvent({ message: 'stopping device', device: this.getName(), interface: this.getInterface() });
378
407
  if (this.isStarting()) {
379
- await this.startTask.stop();
408
+ const task = this.startTask;
409
+ await task.stop();
410
+ await task.getUnderlyingPromise()?.catch(() => { });
380
411
  }
381
412
  this.started = false;
382
413
  this.resetData();
@@ -12,6 +12,7 @@ export class BlePeripheral {
12
12
  disconnecting = false;
13
13
  disconnectedSignalled = false;
14
14
  discoveredServiceUUIds;
15
+ serviceDiscoveryIncomplete = false;
15
16
  discoverServicesPromise;
16
17
  discoverCharacteristicsPromise = {};
17
18
  onErrorHandler = this.onPeripheralError.bind(this);
@@ -155,8 +156,26 @@ export class BlePeripheral {
155
156
  const promise = this.discoverServicesPromise;
156
157
  const res = await promise;
157
158
  sleep(0).then(() => { delete this.discoverServicesPromise; });
159
+ const isComplete = this.checkAnnouncedServices(res);
160
+ this.serviceDiscoveryIncomplete = !isComplete;
161
+ if (!isComplete) {
162
+ const { name, address } = this.getInfo();
163
+ this.logEvent({ message: 'service data incomplete - disconnecting', name, address,
164
+ announced: this.getAnnouncedServices(), discovered: this.getDiscoveredServices() });
165
+ }
158
166
  return res;
159
167
  }
168
+ checkAnnouncedServices(discovered) {
169
+ const announced = this.getAnnouncedServices().map(x => beautifyUUID(x));
170
+ const toBeChecked = discovered.map(x => beautifyUUID(x));
171
+ const cntAnnounced = announced.length;
172
+ let cntVerified = 0;
173
+ for (const s of announced) {
174
+ if (toBeChecked.includes(s))
175
+ cntVerified++;
176
+ }
177
+ return cntAnnounced === cntVerified;
178
+ }
160
179
  async _discoverServices() {
161
180
  if (!this.getPeripheral())
162
181
  return [];
@@ -306,6 +325,14 @@ export class BlePeripheral {
306
325
  }
307
326
  catch { }
308
327
  }
328
+ if (this.serviceDiscoveryIncomplete) {
329
+ this.logEvent({ message: 'peripheral subscribe selected failed', uuids, reason: 'service discovery incomplete',
330
+ announced: this.getAnnouncedServices(), discovered: this.getDiscoveredServices() });
331
+ this.discoveredServiceUUIds = undefined;
332
+ this.serviceDiscoveryIncomplete = false;
333
+ await this.disconnect().catch(() => { });
334
+ return false;
335
+ }
309
336
  try {
310
337
  if (Object.keys(this.characteristics).length === 0) {
311
338
  await this.discoverAllCharacteristics();
@@ -346,6 +373,11 @@ export class BlePeripheral {
346
373
  const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], []);
347
374
  const found = [];
348
375
  const uuids = [];
376
+ const freshUuids = new Set(res.characteristics.map(c => beautifyUUID(c.uuid)));
377
+ Object.keys(this.characteristics).forEach(existing => {
378
+ if (!freshUuids.has(existing))
379
+ delete this.characteristics[existing];
380
+ });
349
381
  res.characteristics.forEach(c => {
350
382
  this.characteristics[beautifyUUID(c.uuid)] = c;
351
383
  found.push(c.uuid);
@@ -364,6 +396,12 @@ export class BlePeripheral {
364
396
  const target = characteristics.map(c => fullUUID(c));
365
397
  const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], target);
366
398
  const found = [];
399
+ const freshUuids = new Set(res.characteristics.map(c => beautifyUUID(c.uuid)));
400
+ characteristics.forEach(requested => {
401
+ const requestedUuid = beautifyUUID(requested);
402
+ if (!freshUuids.has(requestedUuid))
403
+ delete this.characteristics[requestedUuid];
404
+ });
367
405
  res.characteristics.forEach(c => {
368
406
  this.characteristics[beautifyUUID(c.uuid)] = c;
369
407
  found.push(c.uuid);
@@ -433,25 +471,61 @@ export class BlePeripheral {
433
471
  return Promise.reject(new Error('characteristic not found: ' + characteristicUUID));
434
472
  }
435
473
  }
474
+ const signal = options?.signal;
436
475
  return new Promise((resolve, reject) => {
476
+ let settled = false;
477
+ let onData;
478
+ const cleanup = () => {
479
+ if (onData) {
480
+ c.off('data', onData);
481
+ onData = undefined;
482
+ }
483
+ signal?.removeEventListener('abort', onAbort);
484
+ };
485
+ const settleResolve = (result) => {
486
+ if (settled)
487
+ return;
488
+ settled = true;
489
+ cleanup();
490
+ resolve(result);
491
+ };
492
+ const settleReject = (err) => {
493
+ if (settled)
494
+ return;
495
+ settled = true;
496
+ cleanup();
497
+ reject(err);
498
+ };
499
+ const onAbort = () => {
500
+ settleReject(new Error('aborted'));
501
+ };
502
+ if (signal) {
503
+ if (signal.aborted) {
504
+ settleReject(new Error('aborted'));
505
+ return;
506
+ }
507
+ signal.addEventListener('abort', onAbort, { once: true });
508
+ }
437
509
  const write = () => {
438
- if (this.disconnecting || !this.connected)
439
- return Promise.resolve(Buffer.from([]));
440
- c.on('data', (data) => {
441
- c.removeAllListeners('data');
442
- resolve(data);
443
- });
510
+ if (this.disconnecting || !this.connected) {
511
+ settleResolve(Buffer.from([]));
512
+ return;
513
+ }
514
+ onData = (responseData) => {
515
+ settleResolve(responseData);
516
+ };
517
+ c.on('data', onData);
444
518
  this.logEvent({ message: 'write request', characteristic: uuid, data: data.toString('hex'), withoutResponse: options?.withoutResponse === true });
445
519
  c.write(data, options?.withoutResponse === true, (err) => {
446
520
  if (err)
447
- reject(err);
521
+ settleReject(err);
448
522
  });
449
523
  if (options?.withoutResponse) {
450
- resolve(Buffer.from([]));
524
+ settleResolve(Buffer.from([]));
451
525
  }
452
526
  };
453
527
  if (!options?.withoutResponse) {
454
- this.subscribe(characteristicUUID, null).then(success => {
528
+ this.subscribe(characteristicUUID, null).then(() => {
455
529
  write();
456
530
  });
457
531
  }
@@ -76,6 +76,7 @@ export class TBleSensor extends EventEmitter {
76
76
  }
77
77
  this.connectPromise = this.connectPromise ?? this.peripheral.connect();
78
78
  const connected = await this.connectPromise;
79
+ delete this.connectPromise;
79
80
  if (!connected)
80
81
  return false;
81
82
  if (!reconnect)
@@ -220,24 +220,33 @@ export default class BleFmAdapter extends BleAdapter {
220
220
  let hasControl = false;
221
221
  let tryCnt = 0;
222
222
  const sensor = this.getSensor();
223
+ let cancelled = false;
224
+ const controlAbort = new AbortController();
225
+ const abandon = () => {
226
+ if (cancelled)
227
+ return;
228
+ cancelled = true;
229
+ controlAbort.abort();
230
+ };
223
231
  const controlPromise = new Promise((resolve) => {
224
232
  this.startTask.notifyOnStop(() => {
233
+ abandon();
225
234
  resolve(false);
226
235
  });
227
236
  const waitUntilControl = async () => {
228
237
  if (this.supportsVirtualShifting()) {
229
238
  await this.initVirtualShifting();
230
239
  }
231
- while (!hasControl && this.isStarting()) {
240
+ while (!hasControl && !cancelled && this.isStarting()) {
232
241
  if (tryCnt++ === 0) {
233
242
  this.logEvent({ message: 'requesting control', device: this.getName(), interface: this.getInterface() });
234
243
  }
235
- hasControl = await sensor.requestControl();
244
+ hasControl = await sensor.requestControl(controlAbort.signal);
236
245
  if (hasControl) {
237
246
  this.logEvent({ message: 'control granted', device: this.getName(), interface: this.getInterface() });
238
247
  resolve(this.isStarting());
239
248
  }
240
- else {
249
+ else if (!cancelled) {
241
250
  await sleep(this.requestControlRetryDelay);
242
251
  }
243
252
  }
@@ -248,10 +257,12 @@ export default class BleFmAdapter extends BleAdapter {
248
257
  timeout: this.requestControlTimeout,
249
258
  name: 'establishControl',
250
259
  errorOnTimeout: false,
251
- log: this.logEvent.bind(this)
260
+ log: this.logEvent.bind(this),
261
+ onCancel: abandon
252
262
  });
253
263
  const granted = await waitTask.run();
254
264
  if (!granted && this.isStarting()) {
265
+ abandon();
255
266
  this.logEvent({ message: 'could not establish control', device: this.getName(), interface: this.getInterface() });
256
267
  throw new Error('could not establish control');
257
268
  }
@@ -103,7 +103,7 @@ export default class BleFitnessMachineDevice extends TBleSensor {
103
103
  this.hasControl = false;
104
104
  this.ftmsServiceDataAttempts = 0;
105
105
  }
106
- async requestControl() {
106
+ async requestControl(signal) {
107
107
  if (this.hasControl) {
108
108
  return true;
109
109
  }
@@ -113,7 +113,7 @@ export default class BleFitnessMachineDevice extends TBleSensor {
113
113
  this.isCheckingControl = true;
114
114
  const data = Buffer.alloc(1);
115
115
  data.writeUInt8(0, 0);
116
- const res = await this.writeFtmsMessage(0, data, { timeout: 5000 });
116
+ const res = await this.writeFtmsMessage(0, data, { timeout: 5000, signal });
117
117
  if (res === 1) {
118
118
  this.hasControl = true;
119
119
  }
@@ -620,9 +620,15 @@ export default class BleFitnessMachineDevice extends TBleSensor {
620
620
  let res;
621
621
  let tsStart = Date.now();
622
622
  if (props?.timeout) {
623
- res = await new InteruptableTask(this.write(FTMS_CP, data, props), {
623
+ const internalAbort = new AbortController();
624
+ const combined = new AbortController();
625
+ props.signal?.addEventListener('abort', () => combined.abort(), { once: true });
626
+ internalAbort.signal.addEventListener('abort', () => combined.abort(), { once: true });
627
+ const signal = combined.signal;
628
+ res = await new InteruptableTask(this.write(FTMS_CP, data, { ...props, signal }), {
624
629
  timeout: props.timeout ?? 800,
625
- errorOnTimeout: true
630
+ errorOnTimeout: true,
631
+ onCancel: () => internalAbort.abort()
626
632
  }).run();
627
633
  }
628
634
  else {
@@ -7,6 +7,7 @@ export class InteruptableTask {
7
7
  internalEvents = new EventEmitter();
8
8
  promise;
9
9
  onStopNotifiers = [];
10
+ cancelNotified = false;
10
11
  constructor(promise, props) {
11
12
  this.state = (props?.state ?? {});
12
13
  this.props = props;
@@ -17,6 +18,9 @@ export class InteruptableTask {
17
18
  getPromise() {
18
19
  return this.internalState.promise;
19
20
  }
21
+ getUnderlyingPromise() {
22
+ return this.promise;
23
+ }
20
24
  getState() {
21
25
  return this.state;
22
26
  }
@@ -37,6 +41,7 @@ export class InteruptableTask {
37
41
  const { timeout } = this.props;
38
42
  if (timeout) {
39
43
  this.internalState.tsTimeout = this.internalState.tsStart + timeout;
44
+ this.internalState.timeoutDuration = timeout;
40
45
  this.internalState.onTimeout = this.onTimeout.bind(this);
41
46
  this.internalState.timeout = setTimeout(() => { this.internalEvents.emit('timeout'); }, timeout);
42
47
  this.internalEvents.on('timeout', this.internalState.onTimeout);
@@ -50,6 +55,7 @@ export class InteruptableTask {
50
55
  this.sendStopNotification();
51
56
  if (this.getState().result === 'completed' || this.getState().result === 'error')
52
57
  return;
58
+ this.notifyCancel();
53
59
  this.getState().result = 'stopped';
54
60
  if (this.props.onDone)
55
61
  resolve(this.props.onDone(this.getState()));
@@ -94,8 +100,9 @@ export class InteruptableTask {
94
100
  if (!this.internalState.timeout)
95
101
  return;
96
102
  const message = this.props.name ? `${this.props.name} timeout` : 'timeout';
97
- this.logEvent({ message, active: this.isRunning() });
103
+ this.logEvent({ message, active: this.isRunning(), duration: this.internalState?.timeoutDuration });
98
104
  this.clearTimeout();
105
+ this.notifyCancel();
99
106
  this.getState().result = 'timeout';
100
107
  const resolve = this.internalState.onDone;
101
108
  const reject = this.internalState.onError;
@@ -108,6 +115,12 @@ export class InteruptableTask {
108
115
  else
109
116
  resolve(null);
110
117
  }
118
+ notifyCancel() {
119
+ if (this.cancelNotified)
120
+ return;
121
+ this.cancelNotified = true;
122
+ this.props.onCancel?.();
123
+ }
111
124
  sendStopNotification() {
112
125
  this.onStopNotifiers.forEach((cb) => {
113
126
  if (typeof cb === 'function')
@@ -15,6 +15,8 @@ export default class BleAdapter<TDeviceData extends BleDeviceData, TDevice exten
15
15
  protected onDeviceDisconnectHandler: any;
16
16
  protected onDisconnectDoneHandler: any;
17
17
  protected startTask: InteruptableTask<TaskState, boolean>;
18
+ protected startPromise?: Promise<boolean>;
19
+ protected stopPromise?: Promise<boolean>;
18
20
  constructor(settings: BleDeviceSettings, props?: DeviceProperties);
19
21
  getUniqueName(): string;
20
22
  connect(): Promise<boolean>;
@@ -43,6 +45,7 @@ export default class BleAdapter<TDeviceData extends BleDeviceData, TDevice exten
43
45
  getDefaultStartupTimeout(): number;
44
46
  startPreChecks(props: BleStartProperties): Promise<'done' | 'connected' | 'connection-failed'>;
45
47
  start(startProps?: BleStartProperties): Promise<boolean>;
48
+ protected runStartTask(ble: IBleInterface<any>): Promise<boolean>;
46
49
  protected isStarting(): boolean;
47
50
  protected hasData(): boolean;
48
51
  protected waitForInitialData(startupTimeout: any): Promise<void>;
@@ -55,6 +58,7 @@ export default class BleAdapter<TDeviceData extends BleDeviceData, TDevice exten
55
58
  protected onDisconnectDone(): Promise<void>;
56
59
  restart(pause?: number): Promise<boolean>;
57
60
  stop(): Promise<boolean>;
61
+ protected runStop(): Promise<boolean>;
58
62
  pause(): Promise<boolean>;
59
63
  resume(): Promise<boolean>;
60
64
  protected getBle(): IBleInterface<any>;
@@ -14,6 +14,7 @@ export declare class BlePeripheral implements IBlePeripheral {
14
14
  protected disconnecting: boolean;
15
15
  protected disconnectedSignalled: boolean;
16
16
  protected discoveredServiceUUIds: Array<string> | undefined;
17
+ protected serviceDiscoveryIncomplete: boolean;
17
18
  protected discoverServicesPromise: Promise<string[]> | undefined;
18
19
  protected discoverCharacteristicsPromise: Record<string, Promise<BleCharacteristic[]> | undefined>;
19
20
  protected onErrorHandler: any;
@@ -29,15 +30,16 @@ export declare class BlePeripheral implements IBlePeripheral {
29
30
  isConnected(): boolean;
30
31
  isConnecting(): boolean;
31
32
  onDisconnect(callback: () => void): void;
32
- getManufacturerData(): Buffer<ArrayBufferLike>;
33
+ getManufacturerData(): Buffer;
33
34
  getServiceData(uuid: string): Buffer | undefined;
34
35
  protected onPeripheralDisconnect(): Promise<void>;
35
36
  protected onPeripheralError(err: Error): void;
36
37
  discoverServices(): Promise<string[]>;
38
+ protected checkAnnouncedServices(discovered: string[]): boolean;
37
39
  protected _discoverServices(): Promise<string[]>;
38
40
  discoverCharacteristics(serviceUUID: string): Promise<BleCharacteristic[]>;
39
41
  protected _discoverCharacteristics(serviceUUID: string): Promise<BleCharacteristic[]>;
40
- subscribe(characteristicUUID: string, callback: (characteristicUuid: string, data: Buffer, isNotify?: any) => void): Promise<boolean>;
42
+ subscribe(characteristicUUID: string, callback: (characteristicUuid: string, data: Buffer, isNotify?: boolean) => void): Promise<boolean>;
41
43
  unsubscribe(characteristicUUID: string): Promise<boolean>;
42
44
  subscribeSelected(characteristics: string[], callback: (characteristicUuid: string, data: Buffer, isNotify?: boolean) => void): Promise<boolean>;
43
45
  discoverAllCharacteristics(): Promise<string[]>;
@@ -35,7 +35,7 @@ export default class BleFitnessMachineDevice extends TBleSensor {
35
35
  getWindSpeed(): number;
36
36
  getSupportedSports(): Array<Sport>;
37
37
  protected onDisconnect(): void;
38
- requestControl(): Promise<boolean>;
38
+ requestControl(signal?: AbortSignal): Promise<boolean>;
39
39
  setTargetPower(power: number): Promise<boolean>;
40
40
  setTargetResistanceLevel(resistanceLevel: number): Promise<boolean>;
41
41
  setSlope(slope: number): Promise<boolean>;
@@ -123,6 +123,7 @@ export type BleCommsConnectProps = {
123
123
  export interface BleWriteProps {
124
124
  withoutResponse?: boolean;
125
125
  timeout?: number;
126
+ signal?: AbortSignal;
126
127
  }
127
128
  export interface ConnectState {
128
129
  isConnecting: boolean;
@@ -13,10 +13,12 @@ export interface TaskProps<T, P> {
13
13
  errorOnTimeout?: boolean;
14
14
  log?: (event: any) => void;
15
15
  onDone?: (state: T) => P;
16
+ onCancel?: () => void;
16
17
  }
17
18
  interface InternalTaskState<P> {
18
19
  tsStart?: number;
19
20
  tsTimeout?: number;
21
+ timeoutDuration?: number;
20
22
  isRunning: boolean;
21
23
  timeout?: NodeJS.Timeout;
22
24
  promise?: Promise<P>;
@@ -31,8 +33,10 @@ export declare class InteruptableTask<T extends TaskState, P> {
31
33
  protected internalEvents: EventEmitter<any>;
32
34
  protected promise?: Promise<P>;
33
35
  protected onStopNotifiers: Array<() => void>;
36
+ protected cancelNotified: boolean;
34
37
  constructor(promise: Promise<any>, props?: TaskProps<T, P>);
35
38
  getPromise(): Promise<P>;
39
+ getUnderlyingPromise(): Promise<any> | undefined;
36
40
  getState(): T;
37
41
  run(): Promise<P>;
38
42
  notifyOnStop(cb: () => void): void;
@@ -41,6 +45,7 @@ export declare class InteruptableTask<T extends TaskState, P> {
41
45
  isRunning(): boolean;
42
46
  protected clearTimeout(): void;
43
47
  protected onTimeout(): void;
48
+ protected notifyCancel(): void;
44
49
  protected sendStopNotification(): void;
45
50
  protected logEvent(event: any): void;
46
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-devices",
3
- "version": "3.0.26",
3
+ "version": "3.0.28",
4
4
  "scripts": {
5
5
  "lint": "eslint . --ext .ts",
6
6
  "build": "npm run build:esm && npm run build:cjs",