incyclist-devices 1.4.61 → 1.4.64
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-peripheral.d.ts +1 -0
- package/lib/ble/ble-peripheral.js +34 -19
- package/lib/ble/fm.d.ts +2 -1
- package/lib/ble/fm.js +27 -6
- package/lib/ble/incyclist-protocol.js +6 -2
- package/lib/ble/pwr.js +3 -1
- 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 +4 -4
- 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) {
|
|
@@ -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,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;
|
|
@@ -101,9 +102,10 @@ class BlePeripheralConnector {
|
|
|
101
102
|
c.on('data', (data, _isNotification) => {
|
|
102
103
|
this.onData((0, ble_1.uuid)(c.uuid), data);
|
|
103
104
|
});
|
|
104
|
-
if (callback)
|
|
105
|
+
if (callback) {
|
|
105
106
|
this.on((0, ble_1.uuid)(c.uuid), callback);
|
|
106
|
-
|
|
107
|
+
}
|
|
108
|
+
this.logEvent({ message: 'subscribe', peripheral: this.peripheral.address, characteristic: c.uuid, uuid: (0, ble_1.uuid)(c.uuid) });
|
|
107
109
|
if (this.state.subscribed.find(uuid => uuid === c.uuid) === undefined) {
|
|
108
110
|
try {
|
|
109
111
|
yield this.subscribe(c.uuid);
|
|
@@ -117,7 +119,7 @@ class BlePeripheralConnector {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
catch (err) {
|
|
120
|
-
|
|
122
|
+
this.logEvent({ message: 'error', fn: 'subscribeAll()', error: err.message || err, stack: err.stack });
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
this.state.isSubscribing = false;
|
|
@@ -126,21 +128,34 @@ class BlePeripheralConnector {
|
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
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) })) });
|
|
129
132
|
return new Promise((resolve, reject) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 });
|
|
134
158
|
}
|
|
135
|
-
characteristic.on('data', (data, _isNotification) => {
|
|
136
|
-
this.onData(characteristicUuid, data);
|
|
137
|
-
});
|
|
138
|
-
characteristic.subscribe((err) => {
|
|
139
|
-
if (err)
|
|
140
|
-
reject(err);
|
|
141
|
-
else
|
|
142
|
-
resolve(true);
|
|
143
|
-
});
|
|
144
159
|
});
|
|
145
160
|
}
|
|
146
161
|
onData(characteristicUuid, data) {
|
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;
|
|
@@ -148,7 +149,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
148
149
|
}
|
|
149
150
|
catch (err) {
|
|
150
151
|
}
|
|
151
|
-
return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
|
|
152
|
+
return Object.assign(Object.assign({}, this.data), { raw: `2a37:${data.toString('hex')}` });
|
|
152
153
|
}
|
|
153
154
|
setCrr(crr) { this.crr = crr; }
|
|
154
155
|
getCrr() { return this.crr; }
|
|
@@ -215,7 +216,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
215
216
|
this.data.remainingTime = data.readUInt16LE(offset);
|
|
216
217
|
offset += 2;
|
|
217
218
|
}
|
|
218
|
-
return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
|
|
219
|
+
return Object.assign(Object.assign({}, this.data), { raw: `2ad2:${data.toString('hex')}` });
|
|
219
220
|
}
|
|
220
221
|
parseFitnessMachineStatus(_data) {
|
|
221
222
|
const data = Buffer.from(_data);
|
|
@@ -252,7 +253,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
252
253
|
default: break;
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
|
-
return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
|
|
256
|
+
return Object.assign(Object.assign({}, this.data), { raw: `2ada:${data.toString('hex')}` });
|
|
256
257
|
}
|
|
257
258
|
getFitnessMachineFeatures() {
|
|
258
259
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -286,6 +287,11 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
286
287
|
case '2ada':
|
|
287
288
|
res = this.parseFitnessMachineStatus(data);
|
|
288
289
|
break;
|
|
290
|
+
case '2a63':
|
|
291
|
+
case '2a5b':
|
|
292
|
+
case '347b0011-7635-408b-8918-8ff3949ce592':
|
|
293
|
+
this.logger.logEvent({ message: 'onBleData', raw: `uuid:${data.toString('hex')}` });
|
|
294
|
+
break;
|
|
289
295
|
default:
|
|
290
296
|
break;
|
|
291
297
|
}
|
|
@@ -295,6 +301,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
295
301
|
writeFtmsMessage(requestedOpCode, data) {
|
|
296
302
|
return __awaiter(this, void 0, void 0, function* () {
|
|
297
303
|
try {
|
|
304
|
+
this.logEvent({ message: 'fmts:write', data: data.toString('hex') });
|
|
298
305
|
const res = yield this.write(FTMS_CP, data);
|
|
299
306
|
const responseData = Buffer.from(res);
|
|
300
307
|
const opCode = responseData.readUInt8(0);
|
|
@@ -302,19 +309,25 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
302
309
|
const result = responseData.readUInt8(2);
|
|
303
310
|
if (opCode !== 128 || request !== requestedOpCode)
|
|
304
311
|
throw new Error('Illegal response ');
|
|
312
|
+
this.logEvent({ message: 'fmts:write result', res, result });
|
|
305
313
|
return result;
|
|
306
314
|
}
|
|
307
315
|
catch (err) {
|
|
308
|
-
this.logEvent({ message: '
|
|
316
|
+
this.logEvent({ message: 'fmts:write failed', opCode: requestedOpCode, reason: err.message });
|
|
309
317
|
return 4;
|
|
310
318
|
}
|
|
311
319
|
});
|
|
312
320
|
}
|
|
313
321
|
requestControl() {
|
|
314
322
|
return __awaiter(this, void 0, void 0, function* () {
|
|
323
|
+
let to = undefined;
|
|
324
|
+
if (this.isCheckingControl) {
|
|
325
|
+
to = setTimeout(() => { }, 3500);
|
|
326
|
+
}
|
|
315
327
|
if (this.hasControl)
|
|
316
328
|
return true;
|
|
317
329
|
this.logEvent({ message: 'requestControl' });
|
|
330
|
+
this.isCheckingControl = true;
|
|
318
331
|
const data = Buffer.alloc(1);
|
|
319
332
|
data.writeUInt8(0, 0);
|
|
320
333
|
const res = yield this.writeFtmsMessage(0, data);
|
|
@@ -324,6 +337,9 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
324
337
|
else {
|
|
325
338
|
this.logEvent({ message: 'requestControl failed' });
|
|
326
339
|
}
|
|
340
|
+
this.isCheckingControl = false;
|
|
341
|
+
if (to)
|
|
342
|
+
clearTimeout(to);
|
|
327
343
|
return this.hasControl;
|
|
328
344
|
});
|
|
329
345
|
}
|
|
@@ -335,6 +351,10 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
335
351
|
if (!this.hasControl)
|
|
336
352
|
return;
|
|
337
353
|
const hasControl = yield this.requestControl();
|
|
354
|
+
if (!hasControl) {
|
|
355
|
+
this.logEvent({ message: 'setTargetPower failed', reason: 'control is disabled' });
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
338
358
|
const data = Buffer.alloc(3);
|
|
339
359
|
data.writeUInt8(5, 0);
|
|
340
360
|
data.writeInt16LE(Math.round(power), 1);
|
|
@@ -345,7 +365,8 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
345
365
|
setSlope(slope) {
|
|
346
366
|
return __awaiter(this, void 0, void 0, function* () {
|
|
347
367
|
this.logEvent({ message: 'setSlope', slope });
|
|
348
|
-
|
|
368
|
+
const hasControl = yield this.requestControl();
|
|
369
|
+
if (!hasControl)
|
|
349
370
|
return;
|
|
350
371
|
const { windSpeed, crr, cw } = this;
|
|
351
372
|
return yield this.setIndoorBikeSimulation(windSpeed, slope, crr, cw);
|
|
@@ -577,7 +598,7 @@ class FmAdapter extends Device_1.default {
|
|
|
577
598
|
break;
|
|
578
599
|
}
|
|
579
600
|
}
|
|
580
|
-
this.device.requestControl();
|
|
601
|
+
yield this.device.requestControl();
|
|
581
602
|
const startRequest = this.getCyclingMode().getBikeInitRequest();
|
|
582
603
|
yield this.sendUpdate(startRequest);
|
|
583
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
|
}
|
package/lib/ble/pwr.js
CHANGED
|
@@ -111,6 +111,8 @@ class BleCyclingPowerDevice extends ble_device_1.BleDevice {
|
|
|
111
111
|
this.accTorque = data.readUInt16LE(offset);
|
|
112
112
|
offset += 2;
|
|
113
113
|
}
|
|
114
|
+
if (flags & 0x10) {
|
|
115
|
+
}
|
|
114
116
|
if (flags & 0x20) {
|
|
115
117
|
const crankData = {
|
|
116
118
|
revolutions: data.readUInt16LE(offset),
|
|
@@ -125,7 +127,7 @@ class BleCyclingPowerDevice extends ble_device_1.BleDevice {
|
|
|
125
127
|
catch (err) {
|
|
126
128
|
}
|
|
127
129
|
const { instantaneousPower, balance, accTorque, rpm, time } = this;
|
|
128
|
-
return { instantaneousPower, balance, accTorque, rpm, time, raw: data.toString('hex') };
|
|
130
|
+
return { instantaneousPower, balance, accTorque, rpm, time, raw: `2a63:${data.toString('hex')}` };
|
|
129
131
|
}
|
|
130
132
|
onData(characteristic, data) {
|
|
131
133
|
super.onData(characteristic, data);
|
|
@@ -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
|
@@ -49,8 +49,8 @@ class PowerMeterCyclingMode extends power_base_1.default {
|
|
|
49
49
|
let power = bikeData.power || 0;
|
|
50
50
|
const slope = prevData.slope || 0;
|
|
51
51
|
const distanceInternal = prevData.distanceInternal || 0;
|
|
52
|
-
if (
|
|
53
|
-
|
|
52
|
+
if (power > 0) {
|
|
53
|
+
data.isPedalling = true;
|
|
54
54
|
}
|
|
55
55
|
const m = this.getWeight();
|
|
56
56
|
const t = this.getTimeSinceLastUpdate();
|
|
@@ -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;
|