incyclist-devices 3.0.25 → 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 +44 -9
- package/lib/cjs/ble/base/peripheral.js +60 -9
- package/lib/cjs/ble/fm/adapter.js +57 -4
- package/lib/cjs/ble/fm/sensor.js +10 -7
- package/lib/cjs/utils/task.js +14 -1
- package/lib/esm/ble/base/adapter.js +44 -9
- package/lib/esm/ble/base/peripheral.js +60 -9
- package/lib/esm/ble/fm/adapter.js +57 -4
- package/lib/esm/ble/fm/sensor.js +10 -7
- package/lib/esm/utils/task.js +14 -1
- package/lib/types/ble/base/adapter.d.ts +5 -0
- package/lib/types/ble/base/peripheral.d.ts +3 -2
- package/lib/types/ble/fm/adapter.d.ts +3 -0
- 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();
|
|
@@ -253,6 +267,8 @@ class BleAdapter extends adpater_js_1.default {
|
|
|
253
267
|
}
|
|
254
268
|
async initControl(_props) {
|
|
255
269
|
}
|
|
270
|
+
async initControlBestEffort(_props) {
|
|
271
|
+
}
|
|
256
272
|
getStartLogProps(props) {
|
|
257
273
|
const capabilities = this.props?.capabilities;
|
|
258
274
|
const { user, userWeight, bikeWeight, timeout, wheelDiameter, restart, scanOnly } = props ?? {};
|
|
@@ -288,11 +304,16 @@ class BleAdapter extends adpater_js_1.default {
|
|
|
288
304
|
this.stopped = true;
|
|
289
305
|
return false;
|
|
290
306
|
}
|
|
291
|
-
await this.waitForInitialData(timeout);
|
|
292
307
|
await this.checkCapabilities();
|
|
293
308
|
const skipControl = this.props.capabilities && !this.props.capabilities.includes(index_js_1.IncyclistCapability.Control);
|
|
294
|
-
|
|
309
|
+
const isControllable = this.hasCapability(index_js_1.IncyclistCapability.Control) && !skipControl;
|
|
310
|
+
if (isControllable) {
|
|
295
311
|
await this.initControl(startProps);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
await this.initControlBestEffort(startProps);
|
|
315
|
+
}
|
|
316
|
+
await this.waitForInitialData(timeout);
|
|
296
317
|
this.stopped = false;
|
|
297
318
|
this.started = true;
|
|
298
319
|
if (wasPaused)
|
|
@@ -372,9 +393,23 @@ class BleAdapter extends adpater_js_1.default {
|
|
|
372
393
|
return success;
|
|
373
394
|
}
|
|
374
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() {
|
|
375
408
|
this.logEvent({ message: 'stopping device', device: this.getName(), interface: this.getInterface() });
|
|
376
409
|
if (this.isStarting()) {
|
|
377
|
-
|
|
410
|
+
const task = this.startTask;
|
|
411
|
+
await task.stop();
|
|
412
|
+
await task.getUnderlyingPromise()?.catch(() => { });
|
|
378
413
|
}
|
|
379
414
|
this.started = false;
|
|
380
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
|
}
|
|
@@ -12,6 +12,7 @@ const adapter_js_1 = __importDefault(require("../base/adapter.js"));
|
|
|
12
12
|
const consts_js_1 = require("./consts.js");
|
|
13
13
|
const index_js_1 = require("../../types/index.js");
|
|
14
14
|
const utils_js_1 = require("../../utils/utils.js");
|
|
15
|
+
const task_js_1 = require("../../utils/task.js");
|
|
15
16
|
const index_js_2 = require("../zwift/play/index.js");
|
|
16
17
|
const index_js_3 = require("../../features/index.js");
|
|
17
18
|
const fm_resistance_js_1 = __importDefault(require("../../modes/fm-resistance.js"));
|
|
@@ -21,6 +22,7 @@ class BleFmAdapter extends adapter_js_1.default {
|
|
|
21
22
|
distanceInternal = 0;
|
|
22
23
|
connectPromise;
|
|
23
24
|
requestControlRetryDelay = 1000;
|
|
25
|
+
requestControlTimeout = 10000;
|
|
24
26
|
promiseSendUpdate;
|
|
25
27
|
zwiftPlay;
|
|
26
28
|
isHubInitialized = false;
|
|
@@ -168,8 +170,36 @@ class BleFmAdapter extends adapter_js_1.default {
|
|
|
168
170
|
if (!this.hasCapability(index_js_1.IncyclistCapability.Control))
|
|
169
171
|
return;
|
|
170
172
|
await this.establishControl();
|
|
173
|
+
await this.attemptStartRequest();
|
|
171
174
|
await this.sendInitialRequest();
|
|
172
175
|
}
|
|
176
|
+
async initControlBestEffort(_startProps) {
|
|
177
|
+
if (!this.isStarting())
|
|
178
|
+
return;
|
|
179
|
+
this.setConstants();
|
|
180
|
+
try {
|
|
181
|
+
const hasControl = await this.getSensor().requestControl();
|
|
182
|
+
if (hasControl) {
|
|
183
|
+
this.logEvent({ message: 'control granted (downgraded device)', device: this.getName(), interface: this.getInterface() });
|
|
184
|
+
await this.attemptStartRequest();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
this.logEvent({ message: 'could not establish control (downgraded device, non-fatal)', device: this.getName(), interface: this.getInterface() });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
this.logEvent({ message: 'could not establish control (downgraded device, non-fatal)', device: this.getName(), interface: this.getInterface(), error: err.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async attemptStartRequest() {
|
|
195
|
+
try {
|
|
196
|
+
const started = await this.getSensor().startRequest();
|
|
197
|
+
this.logEvent({ message: started ? 'start request acknowledged' : 'start request not acknowledged (non-fatal)', device: this.getName(), interface: this.getInterface() });
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
this.logEvent({ message: 'start request failed (non-fatal)', device: this.getName(), interface: this.getInterface(), error: err.message });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
173
203
|
setConstants() {
|
|
174
204
|
const mode = this.getCyclingMode();
|
|
175
205
|
const sensor = this.getSensor();
|
|
@@ -195,30 +225,53 @@ class BleFmAdapter extends adapter_js_1.default {
|
|
|
195
225
|
let hasControl = false;
|
|
196
226
|
let tryCnt = 0;
|
|
197
227
|
const sensor = this.getSensor();
|
|
198
|
-
|
|
228
|
+
let cancelled = false;
|
|
229
|
+
const controlAbort = new AbortController();
|
|
230
|
+
const abandon = () => {
|
|
231
|
+
if (cancelled)
|
|
232
|
+
return;
|
|
233
|
+
cancelled = true;
|
|
234
|
+
controlAbort.abort();
|
|
235
|
+
};
|
|
236
|
+
const controlPromise = new Promise((resolve) => {
|
|
199
237
|
this.startTask.notifyOnStop(() => {
|
|
238
|
+
abandon();
|
|
200
239
|
resolve(false);
|
|
201
240
|
});
|
|
202
241
|
const waitUntilControl = async () => {
|
|
203
242
|
if (this.supportsVirtualShifting()) {
|
|
204
243
|
await this.initVirtualShifting();
|
|
205
244
|
}
|
|
206
|
-
while (!hasControl && this.isStarting()) {
|
|
245
|
+
while (!hasControl && !cancelled && this.isStarting()) {
|
|
207
246
|
if (tryCnt++ === 0) {
|
|
208
247
|
this.logEvent({ message: 'requesting control', device: this.getName(), interface: this.getInterface() });
|
|
209
248
|
}
|
|
210
|
-
hasControl = await sensor.requestControl();
|
|
249
|
+
hasControl = await sensor.requestControl(controlAbort.signal);
|
|
211
250
|
if (hasControl) {
|
|
212
251
|
this.logEvent({ message: 'control granted', device: this.getName(), interface: this.getInterface() });
|
|
213
252
|
resolve(this.isStarting());
|
|
214
253
|
}
|
|
215
|
-
else {
|
|
254
|
+
else if (!cancelled) {
|
|
216
255
|
await (0, utils_js_1.sleep)(this.requestControlRetryDelay);
|
|
217
256
|
}
|
|
218
257
|
}
|
|
219
258
|
};
|
|
220
259
|
waitUntilControl();
|
|
221
260
|
});
|
|
261
|
+
const waitTask = new task_js_1.InteruptableTask(controlPromise, {
|
|
262
|
+
timeout: this.requestControlTimeout,
|
|
263
|
+
name: 'establishControl',
|
|
264
|
+
errorOnTimeout: false,
|
|
265
|
+
log: this.logEvent.bind(this),
|
|
266
|
+
onCancel: abandon
|
|
267
|
+
});
|
|
268
|
+
const granted = await waitTask.run();
|
|
269
|
+
if (!granted && this.isStarting()) {
|
|
270
|
+
abandon();
|
|
271
|
+
this.logEvent({ message: 'could not establish control', device: this.getName(), interface: this.getInterface() });
|
|
272
|
+
throw new Error('could not establish control');
|
|
273
|
+
}
|
|
274
|
+
return granted;
|
|
222
275
|
}
|
|
223
276
|
async sendInitialRequest() {
|
|
224
277
|
const startRequest = this.getCyclingMode().getBikeInitRequest();
|
package/lib/cjs/ble/fm/sensor.js
CHANGED
|
@@ -105,20 +105,17 @@ 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
|
}
|
|
112
112
|
if (!this.isSubscribed())
|
|
113
113
|
return false;
|
|
114
|
-
if (this.features?.setPower === false && this.features?.setSlope === false && this.features?.setResistance === false) {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
114
|
this.logEvent({ message: 'requestControl' });
|
|
118
115
|
this.isCheckingControl = true;
|
|
119
116
|
const data = Buffer.alloc(1);
|
|
120
117
|
data.writeUInt8(0, 0);
|
|
121
|
-
const res = await this.writeFtmsMessage(0, data, { timeout: 5000 });
|
|
118
|
+
const res = await this.writeFtmsMessage(0, data, { timeout: 5000, signal });
|
|
122
119
|
if (res === 1) {
|
|
123
120
|
this.hasControl = true;
|
|
124
121
|
}
|
|
@@ -625,9 +622,15 @@ class BleFitnessMachineDevice extends sensor_js_1.TBleSensor {
|
|
|
625
622
|
let res;
|
|
626
623
|
let tsStart = Date.now();
|
|
627
624
|
if (props?.timeout) {
|
|
628
|
-
|
|
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 }), {
|
|
629
631
|
timeout: props.timeout ?? 800,
|
|
630
|
-
errorOnTimeout: true
|
|
632
|
+
errorOnTimeout: true,
|
|
633
|
+
onCancel: () => internalAbort.abort()
|
|
631
634
|
}).run();
|
|
632
635
|
}
|
|
633
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();
|
|
@@ -248,6 +262,8 @@ export default class BleAdapter extends IncyclistDevice {
|
|
|
248
262
|
}
|
|
249
263
|
async initControl(_props) {
|
|
250
264
|
}
|
|
265
|
+
async initControlBestEffort(_props) {
|
|
266
|
+
}
|
|
251
267
|
getStartLogProps(props) {
|
|
252
268
|
const capabilities = this.props?.capabilities;
|
|
253
269
|
const { user, userWeight, bikeWeight, timeout, wheelDiameter, restart, scanOnly } = props ?? {};
|
|
@@ -283,11 +299,16 @@ export default class BleAdapter extends IncyclistDevice {
|
|
|
283
299
|
this.stopped = true;
|
|
284
300
|
return false;
|
|
285
301
|
}
|
|
286
|
-
await this.waitForInitialData(timeout);
|
|
287
302
|
await this.checkCapabilities();
|
|
288
303
|
const skipControl = this.props.capabilities && !this.props.capabilities.includes(IncyclistCapability.Control);
|
|
289
|
-
|
|
304
|
+
const isControllable = this.hasCapability(IncyclistCapability.Control) && !skipControl;
|
|
305
|
+
if (isControllable) {
|
|
290
306
|
await this.initControl(startProps);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
await this.initControlBestEffort(startProps);
|
|
310
|
+
}
|
|
311
|
+
await this.waitForInitialData(timeout);
|
|
291
312
|
this.stopped = false;
|
|
292
313
|
this.started = true;
|
|
293
314
|
if (wasPaused)
|
|
@@ -367,9 +388,23 @@ export default class BleAdapter extends IncyclistDevice {
|
|
|
367
388
|
return success;
|
|
368
389
|
}
|
|
369
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() {
|
|
370
403
|
this.logEvent({ message: 'stopping device', device: this.getName(), interface: this.getInterface() });
|
|
371
404
|
if (this.isStarting()) {
|
|
372
|
-
|
|
405
|
+
const task = this.startTask;
|
|
406
|
+
await task.stop();
|
|
407
|
+
await task.getUnderlyingPromise()?.catch(() => { });
|
|
373
408
|
}
|
|
374
409
|
this.started = false;
|
|
375
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
|
}
|
|
@@ -7,6 +7,7 @@ import BleAdapter from '../base/adapter.js';
|
|
|
7
7
|
import { cRR, cwABike } from './consts.js';
|
|
8
8
|
import { IncyclistCapability } from '../../types/index.js';
|
|
9
9
|
import { sleep } from '../../utils/utils.js';
|
|
10
|
+
import { InteruptableTask } from '../../utils/task.js';
|
|
10
11
|
import { BleZwiftPlaySensor } from '../zwift/play/index.js';
|
|
11
12
|
import { useFeatureToggle } from '../../features/index.js';
|
|
12
13
|
import FMResistanceMode from '../../modes/fm-resistance.js';
|
|
@@ -16,6 +17,7 @@ export default class BleFmAdapter extends BleAdapter {
|
|
|
16
17
|
distanceInternal = 0;
|
|
17
18
|
connectPromise;
|
|
18
19
|
requestControlRetryDelay = 1000;
|
|
20
|
+
requestControlTimeout = 10000;
|
|
19
21
|
promiseSendUpdate;
|
|
20
22
|
zwiftPlay;
|
|
21
23
|
isHubInitialized = false;
|
|
@@ -163,8 +165,36 @@ export default class BleFmAdapter extends BleAdapter {
|
|
|
163
165
|
if (!this.hasCapability(IncyclistCapability.Control))
|
|
164
166
|
return;
|
|
165
167
|
await this.establishControl();
|
|
168
|
+
await this.attemptStartRequest();
|
|
166
169
|
await this.sendInitialRequest();
|
|
167
170
|
}
|
|
171
|
+
async initControlBestEffort(_startProps) {
|
|
172
|
+
if (!this.isStarting())
|
|
173
|
+
return;
|
|
174
|
+
this.setConstants();
|
|
175
|
+
try {
|
|
176
|
+
const hasControl = await this.getSensor().requestControl();
|
|
177
|
+
if (hasControl) {
|
|
178
|
+
this.logEvent({ message: 'control granted (downgraded device)', device: this.getName(), interface: this.getInterface() });
|
|
179
|
+
await this.attemptStartRequest();
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.logEvent({ message: 'could not establish control (downgraded device, non-fatal)', device: this.getName(), interface: this.getInterface() });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
this.logEvent({ message: 'could not establish control (downgraded device, non-fatal)', device: this.getName(), interface: this.getInterface(), error: err.message });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async attemptStartRequest() {
|
|
190
|
+
try {
|
|
191
|
+
const started = await this.getSensor().startRequest();
|
|
192
|
+
this.logEvent({ message: started ? 'start request acknowledged' : 'start request not acknowledged (non-fatal)', device: this.getName(), interface: this.getInterface() });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
this.logEvent({ message: 'start request failed (non-fatal)', device: this.getName(), interface: this.getInterface(), error: err.message });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
168
198
|
setConstants() {
|
|
169
199
|
const mode = this.getCyclingMode();
|
|
170
200
|
const sensor = this.getSensor();
|
|
@@ -190,30 +220,53 @@ export default class BleFmAdapter extends BleAdapter {
|
|
|
190
220
|
let hasControl = false;
|
|
191
221
|
let tryCnt = 0;
|
|
192
222
|
const sensor = this.getSensor();
|
|
193
|
-
|
|
223
|
+
let cancelled = false;
|
|
224
|
+
const controlAbort = new AbortController();
|
|
225
|
+
const abandon = () => {
|
|
226
|
+
if (cancelled)
|
|
227
|
+
return;
|
|
228
|
+
cancelled = true;
|
|
229
|
+
controlAbort.abort();
|
|
230
|
+
};
|
|
231
|
+
const controlPromise = new Promise((resolve) => {
|
|
194
232
|
this.startTask.notifyOnStop(() => {
|
|
233
|
+
abandon();
|
|
195
234
|
resolve(false);
|
|
196
235
|
});
|
|
197
236
|
const waitUntilControl = async () => {
|
|
198
237
|
if (this.supportsVirtualShifting()) {
|
|
199
238
|
await this.initVirtualShifting();
|
|
200
239
|
}
|
|
201
|
-
while (!hasControl && this.isStarting()) {
|
|
240
|
+
while (!hasControl && !cancelled && this.isStarting()) {
|
|
202
241
|
if (tryCnt++ === 0) {
|
|
203
242
|
this.logEvent({ message: 'requesting control', device: this.getName(), interface: this.getInterface() });
|
|
204
243
|
}
|
|
205
|
-
hasControl = await sensor.requestControl();
|
|
244
|
+
hasControl = await sensor.requestControl(controlAbort.signal);
|
|
206
245
|
if (hasControl) {
|
|
207
246
|
this.logEvent({ message: 'control granted', device: this.getName(), interface: this.getInterface() });
|
|
208
247
|
resolve(this.isStarting());
|
|
209
248
|
}
|
|
210
|
-
else {
|
|
249
|
+
else if (!cancelled) {
|
|
211
250
|
await sleep(this.requestControlRetryDelay);
|
|
212
251
|
}
|
|
213
252
|
}
|
|
214
253
|
};
|
|
215
254
|
waitUntilControl();
|
|
216
255
|
});
|
|
256
|
+
const waitTask = new InteruptableTask(controlPromise, {
|
|
257
|
+
timeout: this.requestControlTimeout,
|
|
258
|
+
name: 'establishControl',
|
|
259
|
+
errorOnTimeout: false,
|
|
260
|
+
log: this.logEvent.bind(this),
|
|
261
|
+
onCancel: abandon
|
|
262
|
+
});
|
|
263
|
+
const granted = await waitTask.run();
|
|
264
|
+
if (!granted && this.isStarting()) {
|
|
265
|
+
abandon();
|
|
266
|
+
this.logEvent({ message: 'could not establish control', device: this.getName(), interface: this.getInterface() });
|
|
267
|
+
throw new Error('could not establish control');
|
|
268
|
+
}
|
|
269
|
+
return granted;
|
|
217
270
|
}
|
|
218
271
|
async sendInitialRequest() {
|
|
219
272
|
const startRequest = this.getCyclingMode().getBikeInitRequest();
|
package/lib/esm/ble/fm/sensor.js
CHANGED
|
@@ -103,20 +103,17 @@ 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
|
}
|
|
110
110
|
if (!this.isSubscribed())
|
|
111
111
|
return false;
|
|
112
|
-
if (this.features?.setPower === false && this.features?.setSlope === false && this.features?.setResistance === false) {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
112
|
this.logEvent({ message: 'requestControl' });
|
|
116
113
|
this.isCheckingControl = true;
|
|
117
114
|
const data = Buffer.alloc(1);
|
|
118
115
|
data.writeUInt8(0, 0);
|
|
119
|
-
const res = await this.writeFtmsMessage(0, data, { timeout: 5000 });
|
|
116
|
+
const res = await this.writeFtmsMessage(0, data, { timeout: 5000, signal });
|
|
120
117
|
if (res === 1) {
|
|
121
118
|
this.hasControl = true;
|
|
122
119
|
}
|
|
@@ -623,9 +620,15 @@ export default class BleFitnessMachineDevice extends TBleSensor {
|
|
|
623
620
|
let res;
|
|
624
621
|
let tsStart = Date.now();
|
|
625
622
|
if (props?.timeout) {
|
|
626
|
-
|
|
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 }), {
|
|
627
629
|
timeout: props.timeout ?? 800,
|
|
628
|
-
errorOnTimeout: true
|
|
630
|
+
errorOnTimeout: true,
|
|
631
|
+
onCancel: () => internalAbort.abort()
|
|
629
632
|
}).run();
|
|
630
633
|
}
|
|
631
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,17 +45,20 @@ 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>;
|
|
49
52
|
protected checkCapabilities(): Promise<void>;
|
|
50
53
|
protected initControl(_props?: BleStartProperties): Promise<void>;
|
|
54
|
+
protected initControlBestEffort(_props?: BleStartProperties): Promise<void>;
|
|
51
55
|
protected getStartLogProps(props: BleStartProperties): BleStartProperties;
|
|
52
56
|
protected startAdapter(startProps?: BleStartProperties): Promise<boolean>;
|
|
53
57
|
startSensor(): Promise<boolean>;
|
|
54
58
|
protected onDisconnectDone(): Promise<void>;
|
|
55
59
|
restart(pause?: number): Promise<boolean>;
|
|
56
60
|
stop(): Promise<boolean>;
|
|
61
|
+
protected runStop(): Promise<boolean>;
|
|
57
62
|
pause(): Promise<boolean>;
|
|
58
63
|
resume(): Promise<boolean>;
|
|
59
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[]>;
|
|
@@ -12,6 +12,7 @@ export default class BleFmAdapter extends BleAdapter<IndoorBikeData, BleFitnessM
|
|
|
12
12
|
protected distanceInternal: number;
|
|
13
13
|
protected connectPromise: Promise<boolean> | undefined;
|
|
14
14
|
protected requestControlRetryDelay: number;
|
|
15
|
+
protected requestControlTimeout: number;
|
|
15
16
|
protected promiseSendUpdate: Promise<UpdateRequest | void> | undefined;
|
|
16
17
|
protected zwiftPlay: BleZwiftPlaySensor | undefined;
|
|
17
18
|
protected isHubInitialized: boolean;
|
|
@@ -28,6 +29,8 @@ export default class BleFmAdapter extends BleAdapter<IndoorBikeData, BleFitnessM
|
|
|
28
29
|
protected checkResume(): boolean[];
|
|
29
30
|
protected initVirtualShifting(initHub?: boolean): Promise<void>;
|
|
30
31
|
protected initControl(_startProps?: BleStartProperties): Promise<void>;
|
|
32
|
+
protected initControlBestEffort(_startProps?: BleStartProperties): Promise<void>;
|
|
33
|
+
protected attemptStartRequest(): Promise<void>;
|
|
31
34
|
protected setConstants(): void;
|
|
32
35
|
protected establishControl(): Promise<boolean>;
|
|
33
36
|
protected sendInitialRequest(): Promise<void>;
|
|
@@ -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
|
}
|