incyclist-devices 1.4.62 → 1.4.63
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/fm.d.ts +1 -1
- package/lib/ble/incyclist-protocol.js +6 -2
- package/lib/ble/tacx.d.ts +72 -0
- package/lib/ble/tacx.js +639 -0
- package/package.json +1 -1
package/lib/ble/fm.d.ts
CHANGED
|
@@ -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;
|