incyclist-devices 1.4.62 → 1.4.65
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/ble/ble-device.d.ts +0 -1
- package/lib/ble/ble-device.js +50 -27
- package/lib/ble/ble-interface.js +8 -6
- package/lib/ble/ble-peripheral.d.ts +2 -0
- package/lib/ble/ble-peripheral.js +33 -16
- package/lib/ble/fm.d.ts +2 -1
- package/lib/ble/fm.js +19 -3
- package/lib/ble/incyclist-protocol.js +6 -2
- package/lib/ble/tacx.d.ts +72 -0
- package/lib/ble/tacx.js +639 -0
- package/lib/daum/ERGCyclingMode.js +9 -2
- package/lib/modes/power-meter.js +2 -2
- package/lib/modes/simulator.js +9 -0
- package/lib/simulator/Simulator.js +5 -0
- package/package.json +1 -1
package/lib/ble/ble-device.d.ts
CHANGED
package/lib/ble/ble-device.js
CHANGED
|
@@ -47,9 +47,7 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
47
47
|
if (this.logger) {
|
|
48
48
|
this.logger.logEvent(event);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
console.log('~~~BLE:', event);
|
|
52
|
-
}
|
|
50
|
+
console.log('~~~BLE:', event);
|
|
53
51
|
}
|
|
54
52
|
setLogger(logger) {
|
|
55
53
|
this.logger = logger;
|
|
@@ -246,6 +244,7 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
246
244
|
if (writeIdx !== -1) {
|
|
247
245
|
const writeItem = this.writeQueue[writeIdx];
|
|
248
246
|
this.writeQueue.splice(writeIdx, 1);
|
|
247
|
+
console.log('~~~ write queue', this.writeQueue);
|
|
249
248
|
if (writeItem.resolve)
|
|
250
249
|
writeItem.resolve(data);
|
|
251
250
|
}
|
|
@@ -253,34 +252,58 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
253
252
|
}
|
|
254
253
|
write(characteristicUuid, data, withoutResponse = false) {
|
|
255
254
|
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
-
|
|
255
|
+
try {
|
|
257
256
|
const connector = this.ble.getConnector(this.peripheral);
|
|
258
|
-
connector.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (withoutResponse) {
|
|
271
|
-
characteristic.write(data, withoutResponse);
|
|
272
|
-
resolve(new ArrayBuffer(0));
|
|
273
|
-
return;
|
|
257
|
+
const isAlreadySubscribed = connector.isSubscribed(characteristicUuid);
|
|
258
|
+
console.log('~~~ write ', characteristicUuid, data.toString('hex'), isAlreadySubscribed, this.subscribedCharacteristics);
|
|
259
|
+
if (!withoutResponse && !isAlreadySubscribed) {
|
|
260
|
+
const connector = this.ble.getConnector(this.peripheral);
|
|
261
|
+
connector.on(characteristicUuid, (uuid, data) => {
|
|
262
|
+
this.onData(uuid, data);
|
|
263
|
+
});
|
|
264
|
+
this.logEvent({ message: 'write:subscribing ', characteristic: characteristicUuid });
|
|
265
|
+
yield connector.subscribe(characteristicUuid);
|
|
266
|
+
this.subscribedCharacteristics.push(characteristicUuid);
|
|
274
267
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const characteristic = this.characteristics.find(c => c.uuid === characteristicUuid || (0, ble_1.uuid)(c.uuid) === characteristicUuid);
|
|
270
|
+
if (!characteristic) {
|
|
271
|
+
reject(new Error('Characteristic not found'));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (withoutResponse) {
|
|
275
|
+
this.logEvent({ message: 'writing' });
|
|
276
|
+
characteristic.write(data, withoutResponse);
|
|
277
|
+
resolve(new ArrayBuffer(0));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const writeId = this.writeQueue.length;
|
|
282
|
+
let messageDeleted = false;
|
|
283
|
+
this.writeQueue.push({ uuid: characteristicUuid.toLocaleLowerCase(), data, resolve, reject });
|
|
284
|
+
const to = setTimeout(() => {
|
|
285
|
+
console.log('~~~ write timeout');
|
|
286
|
+
if (this.writeQueue.length > writeId && !messageDeleted)
|
|
287
|
+
this.writeQueue.splice(writeId, 1);
|
|
288
|
+
this.logEvent({ message: 'writing response', err: 'timeout' });
|
|
289
|
+
reject(new Error('timeout'));
|
|
290
|
+
}, 1000);
|
|
291
|
+
this.logEvent({ message: 'writing' });
|
|
292
|
+
characteristic.write(data, withoutResponse, (err) => {
|
|
293
|
+
clearTimeout(to);
|
|
294
|
+
this.logEvent({ message: 'writing response', err });
|
|
295
|
+
if (err) {
|
|
296
|
+
this.writeQueue.splice(writeId, 1);
|
|
297
|
+
messageDeleted = true;
|
|
298
|
+
reject(err);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
281
301
|
}
|
|
282
302
|
});
|
|
283
|
-
}
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
this.logEvent({ message: 'error', fn: '', error: err.message || err, stack: err.stack });
|
|
306
|
+
}
|
|
284
307
|
});
|
|
285
308
|
}
|
|
286
309
|
read(characteristicUuid) {
|
package/lib/ble/ble-interface.js
CHANGED
|
@@ -55,9 +55,7 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
55
55
|
if (this.logger) {
|
|
56
56
|
this.logger.logEvent(event);
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
console.log('~~BLE:', event);
|
|
60
|
-
}
|
|
58
|
+
console.log('~~BLE:', event);
|
|
61
59
|
}
|
|
62
60
|
onStateChange(state) {
|
|
63
61
|
if (state !== ble_1.BleState.POWERED_ON) {
|
|
@@ -540,8 +538,8 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
540
538
|
if (fromCache)
|
|
541
539
|
this.logEvent({ message: 'adding from Cache', peripheral: peripheral.address });
|
|
542
540
|
else {
|
|
543
|
-
const { id, name, address } = peripheral;
|
|
544
|
-
this.logEvent({ message: 'BLE scan: found device', peripheral: { id, name, address } });
|
|
541
|
+
const { id, name, address, advertisement = {} } = peripheral;
|
|
542
|
+
this.logEvent({ message: 'BLE scan: found device', peripheral: { id, name, address, services: advertisement.serviceUuids } });
|
|
545
543
|
}
|
|
546
544
|
if (!peripheral || !peripheral.advertisement || !peripheral.advertisement.serviceUuids || peripheral.advertisement.serviceUuids.length === 0)
|
|
547
545
|
return;
|
|
@@ -552,7 +550,11 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
552
550
|
return;
|
|
553
551
|
peripheralsProcessed.push(peripheral.address);
|
|
554
552
|
const characteristics = yield this.getCharacteristics(peripheral);
|
|
555
|
-
const
|
|
553
|
+
const connector = this.getConnector(peripheral);
|
|
554
|
+
const connectedPeripheral = connector.getPeripheral();
|
|
555
|
+
const { id, name, address, advertisement = {} } = connectedPeripheral;
|
|
556
|
+
const DeviceClasses = this.getDeviceClasses(connectedPeripheral, { profile });
|
|
557
|
+
this.logEvent({ message: 'BLE scan: device connected', peripheral: { id, name, address, services: advertisement.serviceUuids, classes: DeviceClasses.map(c => c.prototype.constructor.name) } });
|
|
556
558
|
let cntFound = 0;
|
|
557
559
|
DeviceClasses.forEach((DeviceClass) => __awaiter(this, void 0, void 0, function* () {
|
|
558
560
|
if (!DeviceClass)
|
|
@@ -22,6 +22,7 @@ export default class BlePeripheralConnector {
|
|
|
22
22
|
reconnect(): Promise<void>;
|
|
23
23
|
onDisconnect(): void;
|
|
24
24
|
initialize(enforce?: boolean): Promise<void>;
|
|
25
|
+
isSubscribed(characteristicUuid: string): boolean;
|
|
25
26
|
subscribeAll(callback: (characteristicUuid: string, data: any) => void): Promise<string[]>;
|
|
26
27
|
subscribe(characteristicUuid: string): Promise<boolean>;
|
|
27
28
|
onData(characteristicUuid: string, data: any): void;
|
|
@@ -31,4 +32,5 @@ export default class BlePeripheralConnector {
|
|
|
31
32
|
getState(): string;
|
|
32
33
|
getCharachteristics(): BleCharacteristic[];
|
|
33
34
|
getServices(): string[];
|
|
35
|
+
getPeripheral(): BlePeripheral;
|
|
34
36
|
}
|
|
@@ -31,9 +31,7 @@ class BlePeripheralConnector {
|
|
|
31
31
|
if (this.logger) {
|
|
32
32
|
this.logger.logEvent(event);
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
console.log('~~~BLE:', event);
|
|
36
|
-
}
|
|
34
|
+
console.log('~~~BLE:', event);
|
|
37
35
|
}
|
|
38
36
|
connect() {
|
|
39
37
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -86,6 +84,9 @@ class BlePeripheralConnector {
|
|
|
86
84
|
this.state.isInitialized = this.characteristics !== undefined && this.services !== undefined;
|
|
87
85
|
});
|
|
88
86
|
}
|
|
87
|
+
isSubscribed(characteristicUuid) {
|
|
88
|
+
return this.state.subscribed.find(c => c === characteristicUuid || (0, ble_1.uuid)(c) === characteristicUuid || c === (0, ble_1.uuid)(characteristicUuid)) !== undefined;
|
|
89
|
+
}
|
|
89
90
|
subscribeAll(callback) {
|
|
90
91
|
return __awaiter(this, void 0, void 0, function* () {
|
|
91
92
|
const cnt = this.characteristics.length;
|
|
@@ -127,21 +128,34 @@ class BlePeripheralConnector {
|
|
|
127
128
|
});
|
|
128
129
|
}
|
|
129
130
|
subscribe(characteristicUuid) {
|
|
131
|
+
this.logEvent({ message: 'subscribe', characteristic: characteristicUuid, characteristics: this.characteristics.map(c => ({ characteristic: c.uuid, uuid: (0, ble_1.uuid)(c.uuid) })) });
|
|
130
132
|
return new Promise((resolve, reject) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
try {
|
|
134
|
+
const characteristic = this.characteristics.find(c => c.uuid === characteristicUuid || (0, ble_1.uuid)(c.uuid) === characteristicUuid);
|
|
135
|
+
this.logEvent({ message: 'subscribe', peripheral: this.peripheral.address, characteristic: characteristic.uuid, uuid: (0, ble_1.uuid)(characteristic.uuid) });
|
|
136
|
+
if (!characteristic) {
|
|
137
|
+
reject(new Error('Characteristic not found'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
characteristic.on('data', (data, _isNotification) => {
|
|
141
|
+
this.onData(characteristicUuid, data);
|
|
142
|
+
});
|
|
143
|
+
const to = setTimeout(() => {
|
|
144
|
+
this.logEvent({ message: 'subscribe result', characteristic: characteristicUuid, error: 'timeout' });
|
|
145
|
+
reject(new Error('timeout'));
|
|
146
|
+
}, 3000);
|
|
147
|
+
characteristic.subscribe((err) => {
|
|
148
|
+
clearTimeout(to);
|
|
149
|
+
this.logEvent({ message: 'subscribe result', characteristic: characteristicUuid, error: err });
|
|
150
|
+
if (err)
|
|
151
|
+
reject(err);
|
|
152
|
+
else
|
|
153
|
+
resolve(true);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.logEvent({ message: 'error', error: err.message || err, stack: err.stack });
|
|
135
158
|
}
|
|
136
|
-
characteristic.on('data', (data, _isNotification) => {
|
|
137
|
-
this.onData(characteristicUuid, data);
|
|
138
|
-
});
|
|
139
|
-
characteristic.subscribe((err) => {
|
|
140
|
-
if (err)
|
|
141
|
-
reject(err);
|
|
142
|
-
else
|
|
143
|
-
resolve(true);
|
|
144
|
-
});
|
|
145
159
|
});
|
|
146
160
|
}
|
|
147
161
|
onData(characteristicUuid, data) {
|
|
@@ -167,5 +181,8 @@ class BlePeripheralConnector {
|
|
|
167
181
|
getServices() {
|
|
168
182
|
return this.services;
|
|
169
183
|
}
|
|
184
|
+
getPeripheral() {
|
|
185
|
+
return this.peripheral;
|
|
186
|
+
}
|
|
170
187
|
}
|
|
171
188
|
exports.default = BlePeripheralConnector;
|
package/lib/ble/fm.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ declare type PowerData = {
|
|
|
16
16
|
rpm: number;
|
|
17
17
|
raw?: string;
|
|
18
18
|
};
|
|
19
|
-
declare type IndoorBikeData = {
|
|
19
|
+
export declare type IndoorBikeData = {
|
|
20
20
|
speed?: number;
|
|
21
21
|
averageSpeed?: number;
|
|
22
22
|
cadence?: number;
|
|
@@ -45,6 +45,7 @@ export default class BleFitnessMachineDevice extends BleDevice {
|
|
|
45
45
|
data: IndoorBikeData;
|
|
46
46
|
features: IndoorBikeFeatures;
|
|
47
47
|
hasControl: boolean;
|
|
48
|
+
isCheckingControl: boolean;
|
|
48
49
|
isCPSubscribed: boolean;
|
|
49
50
|
crr: number;
|
|
50
51
|
cw: number;
|
package/lib/ble/fm.js
CHANGED
|
@@ -86,6 +86,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
86
86
|
super(props);
|
|
87
87
|
this.features = undefined;
|
|
88
88
|
this.hasControl = false;
|
|
89
|
+
this.isCheckingControl = false;
|
|
89
90
|
this.isCPSubscribed = false;
|
|
90
91
|
this.crr = 0.0033;
|
|
91
92
|
this.cw = 0.6;
|
|
@@ -300,6 +301,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
300
301
|
writeFtmsMessage(requestedOpCode, data) {
|
|
301
302
|
return __awaiter(this, void 0, void 0, function* () {
|
|
302
303
|
try {
|
|
304
|
+
this.logEvent({ message: 'fmts:write', data: data.toString('hex') });
|
|
303
305
|
const res = yield this.write(FTMS_CP, data);
|
|
304
306
|
const responseData = Buffer.from(res);
|
|
305
307
|
const opCode = responseData.readUInt8(0);
|
|
@@ -307,19 +309,25 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
307
309
|
const result = responseData.readUInt8(2);
|
|
308
310
|
if (opCode !== 128 || request !== requestedOpCode)
|
|
309
311
|
throw new Error('Illegal response ');
|
|
312
|
+
this.logEvent({ message: 'fmts:write result', res, result });
|
|
310
313
|
return result;
|
|
311
314
|
}
|
|
312
315
|
catch (err) {
|
|
313
|
-
this.logEvent({ message: '
|
|
316
|
+
this.logEvent({ message: 'fmts:write failed', opCode: requestedOpCode, reason: err.message });
|
|
314
317
|
return 4;
|
|
315
318
|
}
|
|
316
319
|
});
|
|
317
320
|
}
|
|
318
321
|
requestControl() {
|
|
319
322
|
return __awaiter(this, void 0, void 0, function* () {
|
|
323
|
+
let to = undefined;
|
|
324
|
+
if (this.isCheckingControl) {
|
|
325
|
+
to = setTimeout(() => { }, 3500);
|
|
326
|
+
}
|
|
320
327
|
if (this.hasControl)
|
|
321
328
|
return true;
|
|
322
329
|
this.logEvent({ message: 'requestControl' });
|
|
330
|
+
this.isCheckingControl = true;
|
|
323
331
|
const data = Buffer.alloc(1);
|
|
324
332
|
data.writeUInt8(0, 0);
|
|
325
333
|
const res = yield this.writeFtmsMessage(0, data);
|
|
@@ -329,6 +337,9 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
329
337
|
else {
|
|
330
338
|
this.logEvent({ message: 'requestControl failed' });
|
|
331
339
|
}
|
|
340
|
+
this.isCheckingControl = false;
|
|
341
|
+
if (to)
|
|
342
|
+
clearTimeout(to);
|
|
332
343
|
return this.hasControl;
|
|
333
344
|
});
|
|
334
345
|
}
|
|
@@ -340,6 +351,10 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
340
351
|
if (!this.hasControl)
|
|
341
352
|
return;
|
|
342
353
|
const hasControl = yield this.requestControl();
|
|
354
|
+
if (!hasControl) {
|
|
355
|
+
this.logEvent({ message: 'setTargetPower failed', reason: 'control is disabled' });
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
343
358
|
const data = Buffer.alloc(3);
|
|
344
359
|
data.writeUInt8(5, 0);
|
|
345
360
|
data.writeInt16LE(Math.round(power), 1);
|
|
@@ -350,7 +365,8 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
350
365
|
setSlope(slope) {
|
|
351
366
|
return __awaiter(this, void 0, void 0, function* () {
|
|
352
367
|
this.logEvent({ message: 'setSlope', slope });
|
|
353
|
-
|
|
368
|
+
const hasControl = yield this.requestControl();
|
|
369
|
+
if (!hasControl)
|
|
354
370
|
return;
|
|
355
371
|
const { windSpeed, crr, cw } = this;
|
|
356
372
|
return yield this.setIndoorBikeSimulation(windSpeed, slope, crr, cw);
|
|
@@ -582,7 +598,7 @@ class FmAdapter extends Device_1.default {
|
|
|
582
598
|
break;
|
|
583
599
|
}
|
|
584
600
|
}
|
|
585
|
-
this.device.requestControl();
|
|
601
|
+
yield this.device.requestControl();
|
|
586
602
|
const startRequest = this.getCyclingMode().getBikeInitRequest();
|
|
587
603
|
yield this.sendUpdate(startRequest);
|
|
588
604
|
bleDevice.on('data', (data) => {
|
|
@@ -40,6 +40,7 @@ const fm_1 = __importStar(require("./fm"));
|
|
|
40
40
|
const hrm_1 = __importStar(require("./hrm"));
|
|
41
41
|
const pwr_1 = __importStar(require("./pwr"));
|
|
42
42
|
const wahoo_kickr_1 = __importDefault(require("./wahoo-kickr"));
|
|
43
|
+
const tacx_1 = __importDefault(require("./tacx"));
|
|
43
44
|
class BleProtocol extends DeviceProtocol_1.default {
|
|
44
45
|
constructor(binding) {
|
|
45
46
|
super();
|
|
@@ -67,7 +68,7 @@ class BleProtocol extends DeviceProtocol_1.default {
|
|
|
67
68
|
const { id, name, address } = bleDevice;
|
|
68
69
|
return { id, name, address };
|
|
69
70
|
};
|
|
70
|
-
switch (profile.
|
|
71
|
+
switch (profile.toLowerCase()) {
|
|
71
72
|
case 'hr':
|
|
72
73
|
case 'heartrate monitor':
|
|
73
74
|
return new hrm_1.HrmAdapter(fromDevice ? bleDevice : new hrm_1.default(props()), this);
|
|
@@ -75,14 +76,17 @@ class BleProtocol extends DeviceProtocol_1.default {
|
|
|
75
76
|
case 'smart trainer':
|
|
76
77
|
case 'wahoo smart trainer':
|
|
77
78
|
case 'fitness machine':
|
|
79
|
+
case tacx_1.default.PROFILE.toLowerCase():
|
|
78
80
|
let device;
|
|
79
81
|
if (fromDevice)
|
|
80
82
|
device = bleDevice;
|
|
81
83
|
else {
|
|
82
84
|
device = this.ble.findDeviceInCache(Object.assign(Object.assign({}, props()), { profile }));
|
|
83
85
|
if (!device) {
|
|
84
|
-
if (profile.
|
|
86
|
+
if (profile.toLowerCase() === 'wahoo smart trainer')
|
|
85
87
|
device = new wahoo_kickr_1.default(props());
|
|
88
|
+
else if (profile === tacx_1.default.PROFILE)
|
|
89
|
+
device = new tacx_1.default(props());
|
|
86
90
|
else
|
|
87
91
|
device = new fm_1.default(props());
|
|
88
92
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import BleProtocol from './incyclist-protocol';
|
|
3
|
+
import { BleDeviceClass } from './ble';
|
|
4
|
+
import DeviceAdapter from '../Device';
|
|
5
|
+
import BleFitnessMachineDevice, { FmAdapter, IndoorBikeData } from './fm';
|
|
6
|
+
interface BleFeBikeData extends IndoorBikeData {
|
|
7
|
+
EquipmentType?: 'Treadmill' | 'Elliptical' | 'StationaryBike' | 'Rower' | 'Climber' | 'NordicSkier' | 'Trainer' | 'General';
|
|
8
|
+
RealSpeed?: number;
|
|
9
|
+
VirtualSpeed?: number;
|
|
10
|
+
HeartRateSource?: 'HandContact' | 'EM' | 'ANT+';
|
|
11
|
+
State?: 'OFF' | 'READY' | 'IN_USE' | 'FINISHED';
|
|
12
|
+
EventCount?: number;
|
|
13
|
+
AccumulatedPower?: number;
|
|
14
|
+
TrainerStatus?: number;
|
|
15
|
+
TargetStatus?: 'OnTarget' | 'LowSpeed' | 'HighSpeed';
|
|
16
|
+
}
|
|
17
|
+
declare type CrankData = {
|
|
18
|
+
revolutions?: number;
|
|
19
|
+
time?: number;
|
|
20
|
+
cntUpdateMissing?: number;
|
|
21
|
+
};
|
|
22
|
+
export default class TacxAdvancedFitnessMachineDevice extends BleFitnessMachineDevice {
|
|
23
|
+
static services: string[];
|
|
24
|
+
static characteristics: string[];
|
|
25
|
+
static PROFILE: string;
|
|
26
|
+
prevCrankData: CrankData;
|
|
27
|
+
currentCrankData: CrankData;
|
|
28
|
+
timeOffset: number;
|
|
29
|
+
tsPrevWrite: any;
|
|
30
|
+
data: BleFeBikeData;
|
|
31
|
+
hasFECData: boolean;
|
|
32
|
+
constructor(props?: any);
|
|
33
|
+
init(): Promise<boolean>;
|
|
34
|
+
getProfile(): string;
|
|
35
|
+
getServiceUUids(): string[];
|
|
36
|
+
isBike(): boolean;
|
|
37
|
+
isPower(): boolean;
|
|
38
|
+
isHrm(): boolean;
|
|
39
|
+
parseCrankData(crankData: any): {
|
|
40
|
+
rpm: number;
|
|
41
|
+
time: any;
|
|
42
|
+
};
|
|
43
|
+
parsePower(_data: Buffer): IndoorBikeData;
|
|
44
|
+
resetState(): void;
|
|
45
|
+
parseFEState(capStateBF: number): void;
|
|
46
|
+
parseGeneralFE(data: Buffer): BleFeBikeData;
|
|
47
|
+
parseTrainerData(data: Buffer): BleFeBikeData;
|
|
48
|
+
parseFECMessage(_data: Buffer): BleFeBikeData;
|
|
49
|
+
onData(characteristic: string, data: Buffer): void;
|
|
50
|
+
getChecksum(message: any[]): number;
|
|
51
|
+
buildMessage(payload?: number[], msgID?: number): Buffer;
|
|
52
|
+
sendMessage(message: Buffer): Promise<boolean>;
|
|
53
|
+
sendUserConfiguration(userWeight: any, bikeWeight: any, wheelDiameter: any, gearRatio: any): Promise<boolean>;
|
|
54
|
+
sendBasicResistance(resistance: any): Promise<boolean>;
|
|
55
|
+
sendTargetPower(power: any): Promise<boolean>;
|
|
56
|
+
sendWindResistance(windCoeff: any, windSpeed: any, draftFactor: any): Promise<boolean>;
|
|
57
|
+
sendTrackResistance(slope: any, rrCoeff?: any): Promise<boolean>;
|
|
58
|
+
setTargetPower(power: number): Promise<boolean>;
|
|
59
|
+
setSlope(slope: any): Promise<boolean>;
|
|
60
|
+
reset(): void;
|
|
61
|
+
}
|
|
62
|
+
export declare class TacxBleFEAdapter extends FmAdapter {
|
|
63
|
+
static PROFILE: string;
|
|
64
|
+
device: TacxAdvancedFitnessMachineDevice;
|
|
65
|
+
constructor(device: BleDeviceClass, protocol: BleProtocol);
|
|
66
|
+
isSame(device: DeviceAdapter): boolean;
|
|
67
|
+
getProfile(): string;
|
|
68
|
+
start(props?: any): Promise<any>;
|
|
69
|
+
pause(): Promise<boolean>;
|
|
70
|
+
resume(): Promise<boolean>;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
package/lib/ble/tacx.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
+
}) : function(o, v) {
|
|
12
|
+
o["default"] = v;
|
|
13
|
+
});
|
|
14
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
+
if (mod && mod.__esModule) return mod;
|
|
16
|
+
var result = {};
|
|
17
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
+
__setModuleDefault(result, mod);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
|
+
};
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.TacxBleFEAdapter = void 0;
|
|
35
|
+
const ble_interface_1 = __importDefault(require("./ble-interface"));
|
|
36
|
+
const Device_1 = require("../Device");
|
|
37
|
+
const gd_eventlog_1 = require("gd-eventlog");
|
|
38
|
+
const fm_1 = __importStar(require("./fm"));
|
|
39
|
+
const TACX_FE_C_BLE = '6e40fec1';
|
|
40
|
+
const TACX_FE_C_RX = '6e40fec2';
|
|
41
|
+
const TACX_FE_C_TX = '6e40fec3';
|
|
42
|
+
const SYNC_BYTE = 0xA4;
|
|
43
|
+
const DEFAULT_CHANNEL = 5;
|
|
44
|
+
const ACKNOWLEDGED_DATA = 0x4F;
|
|
45
|
+
const PROFILE_ID = 'Tacx FE-C over BLE';
|
|
46
|
+
const cwABike = {
|
|
47
|
+
race: 0.35,
|
|
48
|
+
triathlon: 0.29,
|
|
49
|
+
mountain: 0.57
|
|
50
|
+
};
|
|
51
|
+
const cRR = 0.0036;
|
|
52
|
+
var ANTMessages;
|
|
53
|
+
(function (ANTMessages) {
|
|
54
|
+
ANTMessages[ANTMessages["calibrationCommand"] = 1] = "calibrationCommand";
|
|
55
|
+
ANTMessages[ANTMessages["calibrationStatus"] = 2] = "calibrationStatus";
|
|
56
|
+
ANTMessages[ANTMessages["generalFE"] = 16] = "generalFE";
|
|
57
|
+
ANTMessages[ANTMessages["generalSettings"] = 17] = "generalSettings";
|
|
58
|
+
ANTMessages[ANTMessages["trainerData"] = 25] = "trainerData";
|
|
59
|
+
ANTMessages[ANTMessages["basicResistance"] = 48] = "basicResistance";
|
|
60
|
+
ANTMessages[ANTMessages["targetPower"] = 49] = "targetPower";
|
|
61
|
+
ANTMessages[ANTMessages["windResistance"] = 50] = "windResistance";
|
|
62
|
+
ANTMessages[ANTMessages["trackResistance"] = 51] = "trackResistance";
|
|
63
|
+
ANTMessages[ANTMessages["feCapabilities"] = 54] = "feCapabilities";
|
|
64
|
+
ANTMessages[ANTMessages["userConfiguration"] = 55] = "userConfiguration";
|
|
65
|
+
ANTMessages[ANTMessages["requestData"] = 70] = "requestData";
|
|
66
|
+
ANTMessages[ANTMessages["commandStatus"] = 71] = "commandStatus";
|
|
67
|
+
ANTMessages[ANTMessages["manufactererData"] = 80] = "manufactererData";
|
|
68
|
+
ANTMessages[ANTMessages["productInformation"] = 81] = "productInformation";
|
|
69
|
+
})(ANTMessages || (ANTMessages = {}));
|
|
70
|
+
class TacxAdvancedFitnessMachineDevice extends fm_1.default {
|
|
71
|
+
constructor(props) {
|
|
72
|
+
super(props);
|
|
73
|
+
this.prevCrankData = undefined;
|
|
74
|
+
this.currentCrankData = undefined;
|
|
75
|
+
this.timeOffset = 0;
|
|
76
|
+
this.tsPrevWrite = undefined;
|
|
77
|
+
this.data = {};
|
|
78
|
+
this.hasFECData = false;
|
|
79
|
+
}
|
|
80
|
+
init() {
|
|
81
|
+
const _super = Object.create(null, {
|
|
82
|
+
init: { get: () => super.init }
|
|
83
|
+
});
|
|
84
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
try {
|
|
86
|
+
this.logEvent({ message: 'get device info' });
|
|
87
|
+
yield _super.init.call(this);
|
|
88
|
+
this.logEvent({ message: 'device info', deviceInfo: this.deviceInfo, features: this.features });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return Promise.resolve(false);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
getProfile() {
|
|
96
|
+
return TacxAdvancedFitnessMachineDevice.PROFILE;
|
|
97
|
+
}
|
|
98
|
+
getServiceUUids() {
|
|
99
|
+
return TacxAdvancedFitnessMachineDevice.services;
|
|
100
|
+
}
|
|
101
|
+
isBike() {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
isPower() {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
isHrm() {
|
|
108
|
+
return this.hasService('180d');
|
|
109
|
+
}
|
|
110
|
+
parseCrankData(crankData) {
|
|
111
|
+
if (!this.prevCrankData)
|
|
112
|
+
this.prevCrankData = { revolutions: 0, time: 0, cntUpdateMissing: -1 };
|
|
113
|
+
const c = this.currentCrankData = crankData;
|
|
114
|
+
const p = this.prevCrankData;
|
|
115
|
+
let rpm = this.data.cadence;
|
|
116
|
+
let hasUpdate = c.time !== p.time;
|
|
117
|
+
if (hasUpdate) {
|
|
118
|
+
let time = c.time - p.time;
|
|
119
|
+
let revs = c.revolutions - p.revolutions;
|
|
120
|
+
if (c.time < p.time) {
|
|
121
|
+
time += 0x10000;
|
|
122
|
+
this.timeOffset += 0x10000;
|
|
123
|
+
}
|
|
124
|
+
if (c.revolutions < p.revolutions)
|
|
125
|
+
revs += 0x10000;
|
|
126
|
+
rpm = 1024 * 60 * revs / time;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
if (p.cntUpdateMissing < 0 || p.cntUpdateMissing > 2) {
|
|
130
|
+
rpm = 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const cntUpdateMissing = p.cntUpdateMissing;
|
|
134
|
+
this.prevCrankData = this.currentCrankData;
|
|
135
|
+
if (hasUpdate)
|
|
136
|
+
this.prevCrankData.cntUpdateMissing = 0;
|
|
137
|
+
else
|
|
138
|
+
this.prevCrankData.cntUpdateMissing = cntUpdateMissing + 1;
|
|
139
|
+
return { rpm, time: this.timeOffset + c.time };
|
|
140
|
+
}
|
|
141
|
+
parsePower(_data) {
|
|
142
|
+
const data = Buffer.from(_data);
|
|
143
|
+
try {
|
|
144
|
+
let offset = 4;
|
|
145
|
+
const flags = data.readUInt16LE(0);
|
|
146
|
+
this.data.instantaneousPower = data.readUInt16LE(2);
|
|
147
|
+
if (flags & 0x1)
|
|
148
|
+
data.readUInt8(offset++);
|
|
149
|
+
if (flags & 0x4) {
|
|
150
|
+
data.readUInt16LE(offset);
|
|
151
|
+
offset += 2;
|
|
152
|
+
}
|
|
153
|
+
if (flags & 0x20) {
|
|
154
|
+
const crankData = {
|
|
155
|
+
revolutions: data.readUInt16LE(offset),
|
|
156
|
+
time: data.readUInt16LE(offset + 2)
|
|
157
|
+
};
|
|
158
|
+
const { rpm, time } = this.parseCrankData(crankData);
|
|
159
|
+
this.data.cadence = rpm;
|
|
160
|
+
this.data.time = time;
|
|
161
|
+
offset += 4;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
}
|
|
166
|
+
const { instantaneousPower, cadence, time } = this.data;
|
|
167
|
+
return { instantaneousPower, cadence, time, raw: data.toString('hex') };
|
|
168
|
+
}
|
|
169
|
+
resetState() {
|
|
170
|
+
const state = this.data;
|
|
171
|
+
delete state.time;
|
|
172
|
+
delete state.totalDistance;
|
|
173
|
+
delete state.RealSpeed;
|
|
174
|
+
delete state.VirtualSpeed;
|
|
175
|
+
delete state.heartrate;
|
|
176
|
+
delete state.HeartRateSource;
|
|
177
|
+
delete state.EventCount;
|
|
178
|
+
delete state.cadence;
|
|
179
|
+
delete state.AccumulatedPower;
|
|
180
|
+
delete state.instantaneousPower;
|
|
181
|
+
delete state.averagePower;
|
|
182
|
+
delete state.TrainerStatus;
|
|
183
|
+
delete state.TargetStatus;
|
|
184
|
+
}
|
|
185
|
+
parseFEState(capStateBF) {
|
|
186
|
+
switch ((capStateBF & 0x70) >> 4) {
|
|
187
|
+
case 1:
|
|
188
|
+
this.data.State = 'OFF';
|
|
189
|
+
break;
|
|
190
|
+
case 2:
|
|
191
|
+
this.data.State = 'READY';
|
|
192
|
+
this.resetState();
|
|
193
|
+
break;
|
|
194
|
+
case 3:
|
|
195
|
+
this.data.State = 'IN_USE';
|
|
196
|
+
break;
|
|
197
|
+
case 4:
|
|
198
|
+
this.data.State = 'FINISHED';
|
|
199
|
+
break;
|
|
200
|
+
default:
|
|
201
|
+
delete this.data.State;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (capStateBF & 0x80) {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
parseGeneralFE(data) {
|
|
208
|
+
const equipmentTypeBF = data.readUInt8(1);
|
|
209
|
+
let elapsedTime = data.readUInt8(2);
|
|
210
|
+
let distance = data.readUInt8(3);
|
|
211
|
+
const speed = data.readUInt16LE(4);
|
|
212
|
+
const heartRate = data.readUInt8(6);
|
|
213
|
+
const capStateBF = data.readUInt8(7);
|
|
214
|
+
switch (equipmentTypeBF & 0x1F) {
|
|
215
|
+
case 19:
|
|
216
|
+
this.data.EquipmentType = 'Treadmill';
|
|
217
|
+
break;
|
|
218
|
+
case 20:
|
|
219
|
+
this.data.EquipmentType = 'Elliptical';
|
|
220
|
+
break;
|
|
221
|
+
case 21:
|
|
222
|
+
this.data.EquipmentType = 'StationaryBike';
|
|
223
|
+
break;
|
|
224
|
+
case 22:
|
|
225
|
+
this.data.EquipmentType = 'Rower';
|
|
226
|
+
break;
|
|
227
|
+
case 23:
|
|
228
|
+
this.data.EquipmentType = 'Climber';
|
|
229
|
+
break;
|
|
230
|
+
case 24:
|
|
231
|
+
this.data.EquipmentType = 'NordicSkier';
|
|
232
|
+
break;
|
|
233
|
+
case 25:
|
|
234
|
+
this.data.EquipmentType = 'Trainer';
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
this.data.EquipmentType = 'General';
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (heartRate !== 0xFF) {
|
|
241
|
+
switch (capStateBF & 0x03) {
|
|
242
|
+
case 3: {
|
|
243
|
+
this.data.heartrate = heartRate;
|
|
244
|
+
this.data.HeartRateSource = 'HandContact';
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case 2: {
|
|
248
|
+
this.data.heartrate = heartRate;
|
|
249
|
+
this.data.HeartRateSource = 'EM';
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 1: {
|
|
253
|
+
this.data.heartrate = heartRate;
|
|
254
|
+
this.data.HeartRateSource = 'ANT+';
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
default: {
|
|
258
|
+
delete this.data.heartrate;
|
|
259
|
+
delete this.data.HeartRateSource;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
elapsedTime /= 4;
|
|
265
|
+
const oldElapsedTime = (this.data.time || 0) % 64;
|
|
266
|
+
if (elapsedTime !== oldElapsedTime) {
|
|
267
|
+
if (oldElapsedTime > elapsedTime) {
|
|
268
|
+
elapsedTime += 64;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
this.data.time = (this.data.time || 0) + elapsedTime - oldElapsedTime;
|
|
272
|
+
if (capStateBF & 0x04) {
|
|
273
|
+
const oldDistance = (this.data.time || 0) % 256;
|
|
274
|
+
if (distance !== oldDistance) {
|
|
275
|
+
if (oldDistance > distance) {
|
|
276
|
+
distance += 256;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.data.totalDistance = (this.data.totalDistance || 0) + distance - oldDistance;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
delete this.data.totalDistance;
|
|
283
|
+
}
|
|
284
|
+
this.data.speed = speed / 1000;
|
|
285
|
+
if (capStateBF & 0x08) {
|
|
286
|
+
this.data.VirtualSpeed = speed / 1000;
|
|
287
|
+
delete this.data.RealSpeed;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
delete this.data.VirtualSpeed;
|
|
291
|
+
this.data.RealSpeed = speed / 1000;
|
|
292
|
+
}
|
|
293
|
+
this.parseFEState(capStateBF);
|
|
294
|
+
return this.data;
|
|
295
|
+
}
|
|
296
|
+
parseTrainerData(data) {
|
|
297
|
+
const oldEventCount = this.data.EventCount || 0;
|
|
298
|
+
let eventCount = data.readUInt8(1);
|
|
299
|
+
const cadence = data.readUInt8(2);
|
|
300
|
+
let accPower = data.readUInt16LE(3);
|
|
301
|
+
const power = data.readUInt16LE(5) & 0xFFF;
|
|
302
|
+
const trainerStatus = data.readUInt8(6) >> 4;
|
|
303
|
+
const flagStateBF = data.readUInt8(7);
|
|
304
|
+
if (eventCount !== oldEventCount) {
|
|
305
|
+
this.data.EventCount = eventCount;
|
|
306
|
+
if (oldEventCount > eventCount) {
|
|
307
|
+
eventCount += 255;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (cadence !== 0xFF) {
|
|
311
|
+
this.data.cadence = cadence;
|
|
312
|
+
}
|
|
313
|
+
if (power !== 0xFFF) {
|
|
314
|
+
this.data.instantaneousPower = power;
|
|
315
|
+
const oldAccPower = (this.data.AccumulatedPower || 0) % 65536;
|
|
316
|
+
if (accPower !== oldAccPower) {
|
|
317
|
+
if (oldAccPower > accPower) {
|
|
318
|
+
accPower += 65536;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.data.AccumulatedPower = (this.data.AccumulatedPower || 0) + accPower - oldAccPower;
|
|
322
|
+
this.data.averagePower = (accPower - oldAccPower) / (eventCount - oldEventCount);
|
|
323
|
+
}
|
|
324
|
+
this.data.TrainerStatus = trainerStatus;
|
|
325
|
+
switch (flagStateBF & 0x03) {
|
|
326
|
+
case 0:
|
|
327
|
+
this.data.TargetStatus = 'OnTarget';
|
|
328
|
+
break;
|
|
329
|
+
case 1:
|
|
330
|
+
this.data.TargetStatus = 'LowSpeed';
|
|
331
|
+
break;
|
|
332
|
+
case 2:
|
|
333
|
+
this.data.TargetStatus = 'HighSpeed';
|
|
334
|
+
break;
|
|
335
|
+
default:
|
|
336
|
+
delete this.data.TargetStatus;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
this.parseFEState(flagStateBF);
|
|
340
|
+
return this.data;
|
|
341
|
+
}
|
|
342
|
+
parseFECMessage(_data) {
|
|
343
|
+
const data = Buffer.from(_data);
|
|
344
|
+
const c = data.readUInt8(0);
|
|
345
|
+
if (c !== SYNC_BYTE) {
|
|
346
|
+
this.logEvent({ message: 'SYNC missing', raw: data.toString('hex') });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const len = data.readUInt8(1);
|
|
350
|
+
const messageId = data.readUInt8(4);
|
|
351
|
+
this.hasFECData = true;
|
|
352
|
+
let res;
|
|
353
|
+
switch (messageId) {
|
|
354
|
+
case ANTMessages.generalFE:
|
|
355
|
+
res = this.parseGeneralFE(data.slice(4, len + 2));
|
|
356
|
+
break;
|
|
357
|
+
case ANTMessages.trainerData:
|
|
358
|
+
res = this.parseTrainerData(data.slice(4, len + 2));
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
res.raw = data.toString('hex');
|
|
362
|
+
return res;
|
|
363
|
+
}
|
|
364
|
+
onData(characteristic, data) {
|
|
365
|
+
super.onData(characteristic, data);
|
|
366
|
+
const uuid = characteristic.toLocaleLowerCase();
|
|
367
|
+
let res = undefined;
|
|
368
|
+
if (uuid && uuid.startsWith(TACX_FE_C_RX)) {
|
|
369
|
+
res = this.parseFECMessage(data);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
switch (uuid) {
|
|
373
|
+
case '2a63':
|
|
374
|
+
if (!this.hasFECData)
|
|
375
|
+
res = this.parsePower(data);
|
|
376
|
+
break;
|
|
377
|
+
case '2ad2':
|
|
378
|
+
if (!this.hasFECData)
|
|
379
|
+
res = this.parseIndoorBikeData(data);
|
|
380
|
+
break;
|
|
381
|
+
case '2a37':
|
|
382
|
+
res = this.parseHrm(data);
|
|
383
|
+
break;
|
|
384
|
+
case '2ada':
|
|
385
|
+
if (!this.hasFECData)
|
|
386
|
+
res = this.parseFitnessMachineStatus(data);
|
|
387
|
+
break;
|
|
388
|
+
default:
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (res)
|
|
393
|
+
this.emit('data', res);
|
|
394
|
+
}
|
|
395
|
+
getChecksum(message) {
|
|
396
|
+
let checksum = 0;
|
|
397
|
+
message.forEach((byte) => {
|
|
398
|
+
checksum = (checksum ^ byte) % 0xFF;
|
|
399
|
+
});
|
|
400
|
+
return checksum;
|
|
401
|
+
}
|
|
402
|
+
buildMessage(payload = [], msgID = 0x00) {
|
|
403
|
+
const m = [];
|
|
404
|
+
m.push(SYNC_BYTE);
|
|
405
|
+
m.push(payload.length);
|
|
406
|
+
m.push(msgID);
|
|
407
|
+
payload.forEach((byte) => {
|
|
408
|
+
m.push(byte);
|
|
409
|
+
});
|
|
410
|
+
m.push(this.getChecksum(m));
|
|
411
|
+
return Buffer.from(m);
|
|
412
|
+
}
|
|
413
|
+
sendMessage(message) {
|
|
414
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
415
|
+
yield this.write(TACX_FE_C_TX, message, true);
|
|
416
|
+
return true;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
sendUserConfiguration(userWeight, bikeWeight, wheelDiameter, gearRatio) {
|
|
420
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
421
|
+
const logStr = `sendUserConfiguration(${userWeight},${bikeWeight},${wheelDiameter},${gearRatio})`;
|
|
422
|
+
this.logEvent({ message: logStr });
|
|
423
|
+
var m = userWeight === undefined ? 0xFFFF : userWeight;
|
|
424
|
+
var mb = bikeWeight === undefined ? 0xFFF : bikeWeight;
|
|
425
|
+
var d = wheelDiameter === undefined ? 0xFF : wheelDiameter;
|
|
426
|
+
var gr = gearRatio === undefined ? 0x00 : gearRatio;
|
|
427
|
+
var dOffset = 0xFF;
|
|
428
|
+
if (m !== 0xFFFF)
|
|
429
|
+
m = Math.trunc(m * 100);
|
|
430
|
+
if (mb !== 0xFFF)
|
|
431
|
+
mb = Math.trunc(mb * 20);
|
|
432
|
+
if (d !== 0xFF) {
|
|
433
|
+
d = d * 1000;
|
|
434
|
+
dOffset = d % 10;
|
|
435
|
+
d = Math.trunc(d / 10);
|
|
436
|
+
}
|
|
437
|
+
if (gr !== 0x00) {
|
|
438
|
+
gr = Math.trunc(gr / 0.03);
|
|
439
|
+
}
|
|
440
|
+
var payload = [];
|
|
441
|
+
payload.push(DEFAULT_CHANNEL);
|
|
442
|
+
payload.push(0x37);
|
|
443
|
+
payload.push(m & 0xFF);
|
|
444
|
+
payload.push((m >> 8) & 0xFF);
|
|
445
|
+
payload.push(0xFF);
|
|
446
|
+
payload.push(((mb & 0xF) << 4) | (dOffset & 0xF));
|
|
447
|
+
payload.push((mb >> 4) & 0xF);
|
|
448
|
+
payload.push(d & 0xFF);
|
|
449
|
+
payload.push(gr & 0xFF);
|
|
450
|
+
const data = this.buildMessage(payload, ACKNOWLEDGED_DATA);
|
|
451
|
+
return yield this.sendMessage(data);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
sendBasicResistance(resistance) {
|
|
455
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
456
|
+
const logStr = `sendBasicResistance(${resistance})`;
|
|
457
|
+
this.logEvent({ message: logStr });
|
|
458
|
+
var res = resistance === undefined ? 0 : resistance;
|
|
459
|
+
res = res / 0.5;
|
|
460
|
+
var payload = [];
|
|
461
|
+
payload.push(DEFAULT_CHANNEL);
|
|
462
|
+
payload.push(0x30);
|
|
463
|
+
payload.push(0xFF);
|
|
464
|
+
payload.push(0xFF);
|
|
465
|
+
payload.push(0xFF);
|
|
466
|
+
payload.push(0xFF);
|
|
467
|
+
payload.push(0xFF);
|
|
468
|
+
payload.push(0xFF);
|
|
469
|
+
payload.push(res & 0xFF);
|
|
470
|
+
const data = this.buildMessage(payload, ACKNOWLEDGED_DATA);
|
|
471
|
+
return yield this.sendMessage(data);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
sendTargetPower(power) {
|
|
475
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
476
|
+
const logStr = `sendTargetPower(${power})`;
|
|
477
|
+
this.logEvent({ message: logStr });
|
|
478
|
+
var p = power === undefined ? 0x00 : power;
|
|
479
|
+
p = p * 4;
|
|
480
|
+
var payload = [];
|
|
481
|
+
payload.push(DEFAULT_CHANNEL);
|
|
482
|
+
payload.push(0x31);
|
|
483
|
+
payload.push(0xFF);
|
|
484
|
+
payload.push(0xFF);
|
|
485
|
+
payload.push(0xFF);
|
|
486
|
+
payload.push(0xFF);
|
|
487
|
+
payload.push(0xFF);
|
|
488
|
+
payload.push(p & 0xFF);
|
|
489
|
+
payload.push((p >> 8) & 0xFF);
|
|
490
|
+
const data = this.buildMessage(payload, ACKNOWLEDGED_DATA);
|
|
491
|
+
return yield this.sendMessage(data);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
sendWindResistance(windCoeff, windSpeed, draftFactor) {
|
|
495
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
496
|
+
const logStr = `sendWindResistance(${windCoeff},${windSpeed},${draftFactor})`;
|
|
497
|
+
this.logEvent({ message: logStr });
|
|
498
|
+
var wc = windCoeff === undefined ? 0xFF : windCoeff;
|
|
499
|
+
var ws = windSpeed === undefined ? 0xFF : windSpeed;
|
|
500
|
+
var df = draftFactor === undefined ? 0xFF : draftFactor;
|
|
501
|
+
if (wc !== 0xFF) {
|
|
502
|
+
wc = Math.trunc(wc / 0.01);
|
|
503
|
+
}
|
|
504
|
+
if (ws !== 0xFF) {
|
|
505
|
+
ws = Math.trunc(ws + 127);
|
|
506
|
+
}
|
|
507
|
+
if (df !== 0xFF) {
|
|
508
|
+
df = Math.trunc(df / 0.01);
|
|
509
|
+
}
|
|
510
|
+
var payload = [];
|
|
511
|
+
payload.push(DEFAULT_CHANNEL);
|
|
512
|
+
payload.push(0x32);
|
|
513
|
+
payload.push(0xFF);
|
|
514
|
+
payload.push(0xFF);
|
|
515
|
+
payload.push(0xFF);
|
|
516
|
+
payload.push(0xFF);
|
|
517
|
+
payload.push(wc & 0xFF);
|
|
518
|
+
payload.push(ws & 0xFF);
|
|
519
|
+
payload.push(df & 0xFF);
|
|
520
|
+
const data = this.buildMessage(payload, ACKNOWLEDGED_DATA);
|
|
521
|
+
return yield this.sendMessage(data);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
sendTrackResistance(slope, rrCoeff) {
|
|
525
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
526
|
+
const logStr = `sendTrackResistance(${slope},${rrCoeff})`;
|
|
527
|
+
this.logEvent({ message: logStr });
|
|
528
|
+
var s = slope === undefined ? 0xFFFF : slope;
|
|
529
|
+
var rr = rrCoeff === undefined ? 0xFF : rrCoeff;
|
|
530
|
+
if (s !== 0xFFFF) {
|
|
531
|
+
s = Math.trunc((s + 200) / 0.01);
|
|
532
|
+
}
|
|
533
|
+
if (rr !== 0xFF) {
|
|
534
|
+
rr = Math.trunc(rr / 0.00005);
|
|
535
|
+
}
|
|
536
|
+
var payload = [];
|
|
537
|
+
payload.push(DEFAULT_CHANNEL);
|
|
538
|
+
payload.push(0x33);
|
|
539
|
+
payload.push(0xFF);
|
|
540
|
+
payload.push(0xFF);
|
|
541
|
+
payload.push(0xFF);
|
|
542
|
+
payload.push(0xFF);
|
|
543
|
+
payload.push(s & 0xFF);
|
|
544
|
+
payload.push((s >> 8) & 0xFF);
|
|
545
|
+
payload.push(rr & 0xFF);
|
|
546
|
+
const data = this.buildMessage(payload, ACKNOWLEDGED_DATA);
|
|
547
|
+
return yield this.sendMessage(data);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
setTargetPower(power) {
|
|
551
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
552
|
+
if (this.data.targetPower !== undefined && this.data.targetPower === power)
|
|
553
|
+
return true;
|
|
554
|
+
return yield this.sendTargetPower(power);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
setSlope(slope) {
|
|
558
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
559
|
+
return yield this.sendTrackResistance(slope, this.crr);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
reset() {
|
|
563
|
+
this.data = {};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
exports.default = TacxAdvancedFitnessMachineDevice;
|
|
567
|
+
TacxAdvancedFitnessMachineDevice.services = [TACX_FE_C_BLE];
|
|
568
|
+
TacxAdvancedFitnessMachineDevice.characteristics = ['2acc', '2ad2', '2ad6', '2ad8', '2ad9', '2ada', TACX_FE_C_RX, TACX_FE_C_TX];
|
|
569
|
+
TacxAdvancedFitnessMachineDevice.PROFILE = PROFILE_ID;
|
|
570
|
+
ble_interface_1.default.register('TacxBleFEDevice', 'tacx-ble-fec', TacxAdvancedFitnessMachineDevice, TacxAdvancedFitnessMachineDevice.services);
|
|
571
|
+
class TacxBleFEAdapter extends fm_1.FmAdapter {
|
|
572
|
+
constructor(device, protocol) {
|
|
573
|
+
super(device, protocol);
|
|
574
|
+
this.device = device;
|
|
575
|
+
this.ble = protocol.ble;
|
|
576
|
+
this.cyclingMode = this.getDefaultCyclingMode();
|
|
577
|
+
this.logger = new gd_eventlog_1.EventLogger('BLE-FEC-Tacx');
|
|
578
|
+
if (this.device)
|
|
579
|
+
this.device.setLogger(this.logger);
|
|
580
|
+
}
|
|
581
|
+
isSame(device) {
|
|
582
|
+
if (!(device instanceof TacxBleFEAdapter))
|
|
583
|
+
return false;
|
|
584
|
+
const adapter = device;
|
|
585
|
+
return (adapter.getName() === this.getName() && adapter.getProfile() === this.getProfile());
|
|
586
|
+
}
|
|
587
|
+
getProfile() {
|
|
588
|
+
return TacxBleFEAdapter.PROFILE;
|
|
589
|
+
}
|
|
590
|
+
start(props) {
|
|
591
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
592
|
+
this.logger.logEvent({ message: 'start requested', profile: this.getProfile(), props });
|
|
593
|
+
if (this.ble.isScanning())
|
|
594
|
+
yield this.ble.stopScan();
|
|
595
|
+
try {
|
|
596
|
+
const bleDevice = yield this.ble.connectDevice(this.device);
|
|
597
|
+
bleDevice.setLogger(this.logger);
|
|
598
|
+
if (bleDevice) {
|
|
599
|
+
this.device = bleDevice;
|
|
600
|
+
const mode = this.getCyclingMode();
|
|
601
|
+
if (mode && mode.getSetting('bikeType')) {
|
|
602
|
+
const bikeType = mode.getSetting('bikeType').toLowerCase();
|
|
603
|
+
this.device.setCrr(cRR);
|
|
604
|
+
switch (bikeType) {
|
|
605
|
+
case 'race':
|
|
606
|
+
this.device.setCw(cwABike.race);
|
|
607
|
+
break;
|
|
608
|
+
case 'triathlon':
|
|
609
|
+
this.device.setCw(cwABike.triathlon);
|
|
610
|
+
break;
|
|
611
|
+
case 'mountain':
|
|
612
|
+
this.device.setCw(cwABike.mountain);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const { user, wheelDiameter, gearRatio } = props || {};
|
|
617
|
+
const userWeight = (user && user.weight ? user.weight : Device_1.DEFAULT_USER_WEIGHT);
|
|
618
|
+
const bikeWeight = Device_1.DEFAULT_BIKE_WEIGHT;
|
|
619
|
+
this.device.sendTrackResistance(0.0);
|
|
620
|
+
this.device.sendUserConfiguration(userWeight, bikeWeight, wheelDiameter, gearRatio);
|
|
621
|
+
const startRequest = this.getCyclingMode().getBikeInitRequest();
|
|
622
|
+
yield this.sendUpdate(startRequest);
|
|
623
|
+
bleDevice.on('data', (data) => {
|
|
624
|
+
this.onDeviceData(data);
|
|
625
|
+
});
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
catch (err) {
|
|
630
|
+
this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
|
|
631
|
+
throw new Error(`could not start device, reason:${err.message}`);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
pause() { this.paused = true; return Promise.resolve(true); }
|
|
636
|
+
resume() { this.paused = false; return Promise.resolve(true); }
|
|
637
|
+
}
|
|
638
|
+
exports.TacxBleFEAdapter = TacxBleFEAdapter;
|
|
639
|
+
TacxBleFEAdapter.PROFILE = PROFILE_ID;
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const CyclingMode_1 = require("../CyclingMode");
|
|
7
7
|
const calculations_1 = __importDefault(require("../calculations"));
|
|
8
8
|
const power_base_1 = __importDefault(require("../modes/power-base"));
|
|
9
|
+
const MIN_SPEED = 10;
|
|
9
10
|
const config = {
|
|
10
11
|
name: "ERG",
|
|
11
12
|
description: "Calculates speed based on power and slope. Power is either set by workout or calculated based on gear and cadence",
|
|
@@ -147,9 +148,15 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
147
148
|
const m = this.getWeight();
|
|
148
149
|
const t = this.getTimeSinceLastUpdate();
|
|
149
150
|
const { speed, distance } = this.calculateSpeedAndDistance(power, slope, m, t, { bikeType });
|
|
150
|
-
|
|
151
|
+
if (power === 0 && speed < MIN_SPEED) {
|
|
152
|
+
data.speed = Math.round(prevData.speed - 1) < 0 ? 0 : Math.round(prevData.speed - 1);
|
|
153
|
+
data.distanceInternal = distanceInternal + data.speed / 3.6 * t;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
data.speed = speed;
|
|
157
|
+
data.distanceInternal = distanceInternal + distance;
|
|
158
|
+
}
|
|
151
159
|
data.power = Math.round(power);
|
|
152
|
-
data.distanceInternal = distanceInternal + distance;
|
|
153
160
|
data.slope = slope;
|
|
154
161
|
data.pedalRpm = rpm;
|
|
155
162
|
data.gear = gear;
|
package/lib/modes/power-meter.js
CHANGED
|
@@ -62,8 +62,8 @@ class PowerMeterCyclingMode extends power_base_1.default {
|
|
|
62
62
|
data.distanceInternal = distanceInternal + data.speed / 3.6 * t;
|
|
63
63
|
}
|
|
64
64
|
else {
|
|
65
|
-
data.speed =
|
|
66
|
-
data.distanceInternal =
|
|
65
|
+
data.speed = speed;
|
|
66
|
+
data.distanceInternal = distanceInternal + distance;
|
|
67
67
|
}
|
|
68
68
|
if (props.log)
|
|
69
69
|
this.logger.logEvent({ message: "updateData result", data, bikeData, prevSpeed: prevData.speed, stopped: speed < MIN_SPEED });
|
package/lib/modes/simulator.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const gd_eventlog_1 = require("gd-eventlog");
|
|
7
7
|
const CyclingMode_1 = require("../CyclingMode");
|
|
8
8
|
const power_base_1 = __importDefault(require("./power-base"));
|
|
9
|
+
const MIN_SPEED = 10;
|
|
9
10
|
const config = {
|
|
10
11
|
name: "Simulator",
|
|
11
12
|
description: "Simulates a ride with constant speed or power output",
|
|
@@ -106,6 +107,14 @@ class SimulatorCyclingMode extends power_base_1.default {
|
|
|
106
107
|
speed = res.speed;
|
|
107
108
|
distance = res.distance;
|
|
108
109
|
}
|
|
110
|
+
if (power === 0 && speed < MIN_SPEED) {
|
|
111
|
+
data.speed = Math.round(prevData.speed - 1) < 0 ? 0 : Math.round(prevData.speed - 1);
|
|
112
|
+
data.distanceInternal = distanceInternal + data.speed / 3.6 * t;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
data.speed = speed;
|
|
116
|
+
data.distanceInternal = distanceInternal + distance;
|
|
117
|
+
}
|
|
109
118
|
data.speed = speed;
|
|
110
119
|
data.power = Math.round(power);
|
|
111
120
|
data.distanceInternal = distanceInternal + distance;
|
|
@@ -218,6 +218,8 @@ class Simulator extends Device_1.default {
|
|
|
218
218
|
return;
|
|
219
219
|
}
|
|
220
220
|
const prevDist = this.data.distanceInternal;
|
|
221
|
+
const d = this.data;
|
|
222
|
+
const prevTime = d.deviceTime;
|
|
221
223
|
this.data = this.getCyclingMode().updateData(this.data);
|
|
222
224
|
let data = {
|
|
223
225
|
speed: this.data.speed,
|
|
@@ -230,6 +232,9 @@ class Simulator extends Device_1.default {
|
|
|
230
232
|
deviceTime: (Date.now() - this.startTS) / 1000,
|
|
231
233
|
deviceDistanceCounter: this.data.distanceInternal
|
|
232
234
|
};
|
|
235
|
+
if (this.isBot) {
|
|
236
|
+
this.logger.logEvent(Object.assign({ message: 'Coach update', prevDist, prevTime }, data));
|
|
237
|
+
}
|
|
233
238
|
this.paused = (this.data.speed === 0);
|
|
234
239
|
if (this.ignoreHrm)
|
|
235
240
|
delete data.heartrate;
|