incyclist-devices 3.0.26 → 3.0.27
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/lib/cjs/ble/base/adapter.js +35 -7
- package/lib/cjs/ble/base/peripheral.js +60 -9
- package/lib/cjs/ble/fm/adapter.js +15 -4
- package/lib/cjs/ble/fm/sensor.js +10 -4
- package/lib/cjs/utils/task.js +14 -1
- package/lib/esm/ble/base/adapter.js +35 -7
- package/lib/esm/ble/base/peripheral.js +60 -9
- package/lib/esm/ble/fm/adapter.js +15 -4
- package/lib/esm/ble/fm/sensor.js +10 -4
- package/lib/esm/utils/task.js +14 -1
- package/lib/types/ble/base/adapter.d.ts +4 -0
- package/lib/types/ble/base/peripheral.d.ts +3 -2
- package/lib/types/ble/fm/sensor.d.ts +1 -1
- package/lib/types/ble/types.d.ts +1 -0
- package/lib/types/utils/task.d.ts +5 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
202
|
-
await this.
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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();
|
|
@@ -379,9 +393,23 @@ class BleAdapter extends adpater_js_1.default {
|
|
|
379
393
|
return success;
|
|
380
394
|
}
|
|
381
395
|
async stop() {
|
|
396
|
+
if (this.stopPromise !== undefined) {
|
|
397
|
+
return this.stopPromise;
|
|
398
|
+
}
|
|
399
|
+
this.stopPromise = this.runStop();
|
|
400
|
+
try {
|
|
401
|
+
return await this.stopPromise;
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
this.stopPromise = undefined;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async runStop() {
|
|
382
408
|
this.logEvent({ message: 'stopping device', device: this.getName(), interface: this.getInterface() });
|
|
383
409
|
if (this.isStarting()) {
|
|
384
|
-
|
|
410
|
+
const task = this.startTask;
|
|
411
|
+
await task.stop();
|
|
412
|
+
await task.getUnderlyingPromise()?.catch(() => { });
|
|
385
413
|
}
|
|
386
414
|
this.started = false;
|
|
387
415
|
this.resetData();
|
|
@@ -158,8 +158,23 @@ class BlePeripheral {
|
|
|
158
158
|
const promise = this.discoverServicesPromise;
|
|
159
159
|
const res = await promise;
|
|
160
160
|
(0, utils_js_1.sleep)(0).then(() => { delete this.discoverServicesPromise; });
|
|
161
|
+
const isComplete = this.checkAnnouncedServices(res);
|
|
162
|
+
if (!isComplete) {
|
|
163
|
+
this.logEvent({ message: 'service data incomplete - disconnecting' });
|
|
164
|
+
}
|
|
161
165
|
return res;
|
|
162
166
|
}
|
|
167
|
+
checkAnnouncedServices(discovered) {
|
|
168
|
+
const announced = this.getAnnouncedServices().map(x => (0, utils_js_2.beautifyUUID)(x));
|
|
169
|
+
const toBeChecked = discovered.map(x => (0, utils_js_2.beautifyUUID)(x));
|
|
170
|
+
const cntAnnounced = announced.length;
|
|
171
|
+
let cntVerified = 0;
|
|
172
|
+
for (const s of announced) {
|
|
173
|
+
if (toBeChecked.includes(s))
|
|
174
|
+
cntVerified++;
|
|
175
|
+
}
|
|
176
|
+
return cntAnnounced === cntVerified;
|
|
177
|
+
}
|
|
163
178
|
async _discoverServices() {
|
|
164
179
|
if (!this.getPeripheral())
|
|
165
180
|
return [];
|
|
@@ -436,25 +451,61 @@ class BlePeripheral {
|
|
|
436
451
|
return Promise.reject(new Error('characteristic not found: ' + characteristicUUID));
|
|
437
452
|
}
|
|
438
453
|
}
|
|
454
|
+
const signal = options?.signal;
|
|
439
455
|
return new Promise((resolve, reject) => {
|
|
456
|
+
let settled = false;
|
|
457
|
+
let onData;
|
|
458
|
+
const cleanup = () => {
|
|
459
|
+
if (onData) {
|
|
460
|
+
c.off('data', onData);
|
|
461
|
+
onData = undefined;
|
|
462
|
+
}
|
|
463
|
+
signal?.removeEventListener('abort', onAbort);
|
|
464
|
+
};
|
|
465
|
+
const settleResolve = (result) => {
|
|
466
|
+
if (settled)
|
|
467
|
+
return;
|
|
468
|
+
settled = true;
|
|
469
|
+
cleanup();
|
|
470
|
+
resolve(result);
|
|
471
|
+
};
|
|
472
|
+
const settleReject = (err) => {
|
|
473
|
+
if (settled)
|
|
474
|
+
return;
|
|
475
|
+
settled = true;
|
|
476
|
+
cleanup();
|
|
477
|
+
reject(err);
|
|
478
|
+
};
|
|
479
|
+
const onAbort = () => {
|
|
480
|
+
settleReject(new Error('aborted'));
|
|
481
|
+
};
|
|
482
|
+
if (signal) {
|
|
483
|
+
if (signal.aborted) {
|
|
484
|
+
settleReject(new Error('aborted'));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
488
|
+
}
|
|
440
489
|
const write = () => {
|
|
441
|
-
if (this.disconnecting || !this.connected)
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
490
|
+
if (this.disconnecting || !this.connected) {
|
|
491
|
+
settleResolve(Buffer.from([]));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
onData = (responseData) => {
|
|
495
|
+
settleResolve(responseData);
|
|
496
|
+
};
|
|
497
|
+
c.on('data', onData);
|
|
447
498
|
this.logEvent({ message: 'write request', characteristic: uuid, data: data.toString('hex'), withoutResponse: options?.withoutResponse === true });
|
|
448
499
|
c.write(data, options?.withoutResponse === true, (err) => {
|
|
449
500
|
if (err)
|
|
450
|
-
|
|
501
|
+
settleReject(err);
|
|
451
502
|
});
|
|
452
503
|
if (options?.withoutResponse) {
|
|
453
|
-
|
|
504
|
+
settleResolve(Buffer.from([]));
|
|
454
505
|
}
|
|
455
506
|
};
|
|
456
507
|
if (!options?.withoutResponse) {
|
|
457
|
-
this.subscribe(characteristicUUID, null).then(
|
|
508
|
+
this.subscribe(characteristicUUID, null).then(() => {
|
|
458
509
|
write();
|
|
459
510
|
});
|
|
460
511
|
}
|
|
@@ -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
|
}
|
package/lib/cjs/ble/fm/sensor.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/lib/cjs/utils/task.js
CHANGED
|
@@ -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
|
-
|
|
197
|
-
await this.
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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();
|
|
@@ -374,9 +388,23 @@ export default class BleAdapter extends IncyclistDevice {
|
|
|
374
388
|
return success;
|
|
375
389
|
}
|
|
376
390
|
async stop() {
|
|
391
|
+
if (this.stopPromise !== undefined) {
|
|
392
|
+
return this.stopPromise;
|
|
393
|
+
}
|
|
394
|
+
this.stopPromise = this.runStop();
|
|
395
|
+
try {
|
|
396
|
+
return await this.stopPromise;
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
this.stopPromise = undefined;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async runStop() {
|
|
377
403
|
this.logEvent({ message: 'stopping device', device: this.getName(), interface: this.getInterface() });
|
|
378
404
|
if (this.isStarting()) {
|
|
379
|
-
|
|
405
|
+
const task = this.startTask;
|
|
406
|
+
await task.stop();
|
|
407
|
+
await task.getUnderlyingPromise()?.catch(() => { });
|
|
380
408
|
}
|
|
381
409
|
this.started = false;
|
|
382
410
|
this.resetData();
|
|
@@ -155,8 +155,23 @@ export class BlePeripheral {
|
|
|
155
155
|
const promise = this.discoverServicesPromise;
|
|
156
156
|
const res = await promise;
|
|
157
157
|
sleep(0).then(() => { delete this.discoverServicesPromise; });
|
|
158
|
+
const isComplete = this.checkAnnouncedServices(res);
|
|
159
|
+
if (!isComplete) {
|
|
160
|
+
this.logEvent({ message: 'service data incomplete - disconnecting' });
|
|
161
|
+
}
|
|
158
162
|
return res;
|
|
159
163
|
}
|
|
164
|
+
checkAnnouncedServices(discovered) {
|
|
165
|
+
const announced = this.getAnnouncedServices().map(x => beautifyUUID(x));
|
|
166
|
+
const toBeChecked = discovered.map(x => beautifyUUID(x));
|
|
167
|
+
const cntAnnounced = announced.length;
|
|
168
|
+
let cntVerified = 0;
|
|
169
|
+
for (const s of announced) {
|
|
170
|
+
if (toBeChecked.includes(s))
|
|
171
|
+
cntVerified++;
|
|
172
|
+
}
|
|
173
|
+
return cntAnnounced === cntVerified;
|
|
174
|
+
}
|
|
160
175
|
async _discoverServices() {
|
|
161
176
|
if (!this.getPeripheral())
|
|
162
177
|
return [];
|
|
@@ -433,25 +448,61 @@ export class BlePeripheral {
|
|
|
433
448
|
return Promise.reject(new Error('characteristic not found: ' + characteristicUUID));
|
|
434
449
|
}
|
|
435
450
|
}
|
|
451
|
+
const signal = options?.signal;
|
|
436
452
|
return new Promise((resolve, reject) => {
|
|
453
|
+
let settled = false;
|
|
454
|
+
let onData;
|
|
455
|
+
const cleanup = () => {
|
|
456
|
+
if (onData) {
|
|
457
|
+
c.off('data', onData);
|
|
458
|
+
onData = undefined;
|
|
459
|
+
}
|
|
460
|
+
signal?.removeEventListener('abort', onAbort);
|
|
461
|
+
};
|
|
462
|
+
const settleResolve = (result) => {
|
|
463
|
+
if (settled)
|
|
464
|
+
return;
|
|
465
|
+
settled = true;
|
|
466
|
+
cleanup();
|
|
467
|
+
resolve(result);
|
|
468
|
+
};
|
|
469
|
+
const settleReject = (err) => {
|
|
470
|
+
if (settled)
|
|
471
|
+
return;
|
|
472
|
+
settled = true;
|
|
473
|
+
cleanup();
|
|
474
|
+
reject(err);
|
|
475
|
+
};
|
|
476
|
+
const onAbort = () => {
|
|
477
|
+
settleReject(new Error('aborted'));
|
|
478
|
+
};
|
|
479
|
+
if (signal) {
|
|
480
|
+
if (signal.aborted) {
|
|
481
|
+
settleReject(new Error('aborted'));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
485
|
+
}
|
|
437
486
|
const write = () => {
|
|
438
|
-
if (this.disconnecting || !this.connected)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
487
|
+
if (this.disconnecting || !this.connected) {
|
|
488
|
+
settleResolve(Buffer.from([]));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
onData = (responseData) => {
|
|
492
|
+
settleResolve(responseData);
|
|
493
|
+
};
|
|
494
|
+
c.on('data', onData);
|
|
444
495
|
this.logEvent({ message: 'write request', characteristic: uuid, data: data.toString('hex'), withoutResponse: options?.withoutResponse === true });
|
|
445
496
|
c.write(data, options?.withoutResponse === true, (err) => {
|
|
446
497
|
if (err)
|
|
447
|
-
|
|
498
|
+
settleReject(err);
|
|
448
499
|
});
|
|
449
500
|
if (options?.withoutResponse) {
|
|
450
|
-
|
|
501
|
+
settleResolve(Buffer.from([]));
|
|
451
502
|
}
|
|
452
503
|
};
|
|
453
504
|
if (!options?.withoutResponse) {
|
|
454
|
-
this.subscribe(characteristicUUID, null).then(
|
|
505
|
+
this.subscribe(characteristicUUID, null).then(() => {
|
|
455
506
|
write();
|
|
456
507
|
});
|
|
457
508
|
}
|
|
@@ -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
|
}
|
package/lib/esm/ble/fm/sensor.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/lib/esm/utils/task.js
CHANGED
|
@@ -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>;
|
|
@@ -29,15 +29,16 @@ export declare class BlePeripheral implements IBlePeripheral {
|
|
|
29
29
|
isConnected(): boolean;
|
|
30
30
|
isConnecting(): boolean;
|
|
31
31
|
onDisconnect(callback: () => void): void;
|
|
32
|
-
getManufacturerData(): Buffer
|
|
32
|
+
getManufacturerData(): Buffer;
|
|
33
33
|
getServiceData(uuid: string): Buffer | undefined;
|
|
34
34
|
protected onPeripheralDisconnect(): Promise<void>;
|
|
35
35
|
protected onPeripheralError(err: Error): void;
|
|
36
36
|
discoverServices(): Promise<string[]>;
|
|
37
|
+
protected checkAnnouncedServices(discovered: string[]): boolean;
|
|
37
38
|
protected _discoverServices(): Promise<string[]>;
|
|
38
39
|
discoverCharacteristics(serviceUUID: string): Promise<BleCharacteristic[]>;
|
|
39
40
|
protected _discoverCharacteristics(serviceUUID: string): Promise<BleCharacteristic[]>;
|
|
40
|
-
subscribe(characteristicUUID: string, callback: (characteristicUuid: string, data: Buffer, isNotify?:
|
|
41
|
+
subscribe(characteristicUUID: string, callback: (characteristicUuid: string, data: Buffer, isNotify?: boolean) => void): Promise<boolean>;
|
|
41
42
|
unsubscribe(characteristicUUID: string): Promise<boolean>;
|
|
42
43
|
subscribeSelected(characteristics: string[], callback: (characteristicUuid: string, data: Buffer, isNotify?: boolean) => void): Promise<boolean>;
|
|
43
44
|
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>;
|
package/lib/types/ble/types.d.ts
CHANGED
|
@@ -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
|
}
|