incyclist-devices 1.5.11 → 1.5.12
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/LICENSE +0 -0
- package/lib/DeviceSupport.d.ts +36 -36
- package/lib/DeviceSupport.js +82 -82
- package/lib/ant/AntAdapter.d.ts +50 -50
- package/lib/ant/AntAdapter.js +109 -109
- package/lib/ant/AntScanner.d.ts +60 -60
- package/lib/ant/AntScanner.js +651 -651
- package/lib/ant/antfe/AntFEAdapter.d.ts +83 -83
- package/lib/ant/antfe/AntFEAdapter.js +652 -652
- package/lib/ant/antfe/ant-fe-adv-st-mode.d.ts +9 -9
- package/lib/ant/antfe/ant-fe-adv-st-mode.js +51 -51
- package/lib/ant/antfe/ant-fe-erg-mode.d.ts +6 -6
- package/lib/ant/antfe/ant-fe-erg-mode.js +14 -14
- package/lib/ant/antfe/ant-fe-st-mode.d.ts +5 -5
- package/lib/ant/antfe/ant-fe-st-mode.js +13 -13
- package/lib/ant/anthrm/AntHrmAdapter.d.ts +16 -16
- package/lib/ant/anthrm/AntHrmAdapter.js +130 -130
- package/lib/ant/antpwr/pwr-adapter.d.ts +49 -49
- package/lib/ant/antpwr/pwr-adapter.js +251 -251
- package/lib/ant/utils.d.ts +1 -1
- package/lib/ant/utils.js +23 -23
- package/lib/antv2/AntAdapter.d.ts +48 -0
- package/lib/antv2/AntAdapter.js +104 -0
- package/lib/antv2/adapter-factory.d.ts +11 -11
- package/lib/antv2/adapter-factory.js +40 -40
- package/lib/antv2/ant-binding.d.ts +13 -13
- package/lib/antv2/ant-binding.js +27 -27
- package/lib/antv2/ant-device.d.ts +51 -51
- package/lib/antv2/ant-device.js +115 -115
- package/lib/antv2/ant-interface.d.ts +37 -37
- package/lib/antv2/ant-interface.js +255 -255
- package/lib/antv2/fe.d.ts +29 -29
- package/lib/antv2/fe.js +262 -262
- package/lib/antv2/hr.d.ts +18 -18
- package/lib/antv2/hr.js +93 -93
- package/lib/antv2/incyclist-protocol.d.ts +37 -37
- package/lib/antv2/incyclist-protocol.js +126 -126
- package/lib/antv2/pwr.d.ts +28 -28
- package/lib/antv2/pwr.js +163 -163
- package/lib/antv2/sensor-factory.d.ts +5 -5
- package/lib/antv2/sensor-factory.js +20 -20
- package/lib/ble/ble-device.d.ts +63 -63
- package/lib/ble/ble-device.js +444 -444
- package/lib/ble/ble-erg-mode.d.ts +18 -18
- package/lib/ble/ble-erg-mode.js +132 -132
- package/lib/ble/ble-interface.d.ts +100 -100
- package/lib/ble/ble-interface.js +721 -721
- package/lib/ble/ble-peripheral.d.ts +36 -36
- package/lib/ble/ble-peripheral.js +200 -200
- package/lib/ble/ble-st-mode.d.ts +15 -15
- package/lib/ble/ble-st-mode.js +95 -95
- package/lib/ble/ble.d.ts +129 -129
- package/lib/ble/ble.js +86 -86
- package/lib/ble/consts.d.ts +14 -14
- package/lib/ble/consts.js +17 -17
- package/lib/ble/elite.d.ts +90 -90
- package/lib/ble/elite.js +322 -322
- package/lib/ble/fm.d.ts +125 -125
- package/lib/ble/fm.js +745 -745
- package/lib/ble/hrm.d.ts +48 -48
- package/lib/ble/hrm.js +134 -134
- package/lib/ble/incyclist-protocol.d.ts +31 -31
- package/lib/ble/incyclist-protocol.js +153 -153
- package/lib/ble/pwr.d.ts +89 -89
- package/lib/ble/pwr.js +321 -321
- package/lib/ble/tacx.d.ts +92 -90
- package/lib/ble/tacx.js +763 -731
- package/lib/ble/wahoo-kickr.d.ts +98 -98
- package/lib/ble/wahoo-kickr.js +496 -496
- package/lib/calculations.d.ts +13 -13
- package/lib/calculations.js +150 -150
- package/lib/cycling-mode.d.ts +76 -76
- package/lib/cycling-mode.js +79 -79
- package/lib/daum/DaumAdapter.d.ts +66 -66
- package/lib/daum/DaumAdapter.js +396 -396
- package/lib/daum/DaumPowerMeterCyclingMode.d.ts +8 -8
- package/lib/daum/DaumPowerMeterCyclingMode.js +21 -21
- package/lib/daum/ERGCyclingMode.d.ts +26 -26
- package/lib/daum/ERGCyclingMode.js +201 -201
- package/lib/daum/SmartTrainerCyclingMode.d.ts +41 -41
- package/lib/daum/SmartTrainerCyclingMode.js +344 -344
- package/lib/daum/classic/DaumClassicAdapter.d.ts +22 -22
- package/lib/daum/classic/DaumClassicAdapter.js +183 -183
- package/lib/daum/classic/DaumClassicCyclingMode.d.ts +13 -13
- package/lib/daum/classic/DaumClassicCyclingMode.js +97 -97
- package/lib/daum/classic/DaumClassicProtocol.d.ts +27 -27
- package/lib/daum/classic/DaumClassicProtocol.js +185 -185
- package/lib/daum/classic/bike.d.ts +68 -68
- package/lib/daum/classic/bike.js +467 -467
- package/lib/daum/classic/utils.d.ts +13 -13
- package/lib/daum/classic/utils.js +143 -143
- package/lib/daum/constants.d.ts +19 -19
- package/lib/daum/constants.js +22 -22
- package/lib/daum/premium/DaumClassicCyclingMode.d.ts +14 -14
- package/lib/daum/premium/DaumClassicCyclingMode.js +86 -86
- package/lib/daum/premium/DaumPremiumAdapter.d.ts +16 -16
- package/lib/daum/premium/DaumPremiumAdapter.js +163 -163
- package/lib/daum/premium/DaumPremiumProtocol.d.ts +32 -32
- package/lib/daum/premium/DaumPremiumProtocol.js +207 -207
- package/lib/daum/premium/bike.d.ts +127 -127
- package/lib/daum/premium/bike.js +904 -904
- package/lib/daum/premium/tcpserial.d.ts +33 -33
- package/lib/daum/premium/tcpserial.js +123 -123
- package/lib/daum/premium/utils.d.ts +62 -62
- package/lib/daum/premium/utils.js +376 -376
- package/lib/device.d.ts +92 -92
- package/lib/device.js +71 -71
- package/lib/kettler/comms.d.ts +59 -59
- package/lib/kettler/comms.js +242 -242
- package/lib/kettler/ergo-racer/ERGCyclingMode.d.ts +25 -25
- package/lib/kettler/ergo-racer/ERGCyclingMode.js +144 -144
- package/lib/kettler/ergo-racer/adapter.d.ts +101 -101
- package/lib/kettler/ergo-racer/adapter.js +639 -639
- package/lib/kettler/ergo-racer/protocol.d.ts +41 -41
- package/lib/kettler/ergo-racer/protocol.js +203 -203
- package/lib/modes/power-base.d.ts +20 -20
- package/lib/modes/power-base.js +70 -70
- package/lib/modes/power-meter.d.ts +20 -20
- package/lib/modes/power-meter.js +78 -78
- package/lib/modes/simulator.d.ts +29 -29
- package/lib/modes/simulator.js +140 -140
- package/lib/protocol.d.ts +74 -74
- package/lib/protocol.js +41 -41
- package/lib/registry.d.ts +8 -8
- package/lib/registry.js +33 -33
- package/lib/simulator/Simulator.d.ts +69 -69
- package/lib/simulator/Simulator.js +288 -288
- package/lib/types/command.d.ts +8 -8
- package/lib/types/command.js +2 -2
- package/lib/types/route.d.ts +24 -24
- package/lib/types/route.js +9 -9
- package/lib/types/user.d.ts +11 -11
- package/lib/types/user.js +9 -9
- package/lib/utils.d.ts +14 -14
- package/lib/utils.js +114 -114
- package/package.json +47 -47
package/lib/ble/fm.js
CHANGED
|
@@ -1,745 +1,745 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.FmAdapter = void 0;
|
|
16
|
-
const ble_device_1 = require("./ble-device");
|
|
17
|
-
const ble_interface_1 = __importDefault(require("./ble-interface"));
|
|
18
|
-
const device_1 = __importDefault(require("../device"));
|
|
19
|
-
const gd_eventlog_1 = require("gd-eventlog");
|
|
20
|
-
const power_meter_1 = __importDefault(require("../modes/power-meter"));
|
|
21
|
-
const ble_st_mode_1 = __importDefault(require("./ble-st-mode"));
|
|
22
|
-
const ble_erg_mode_1 = __importDefault(require("./ble-erg-mode"));
|
|
23
|
-
const consts_1 = require("./consts");
|
|
24
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
25
|
-
const cwABike = {
|
|
26
|
-
race: 0.35,
|
|
27
|
-
triathlon: 0.29,
|
|
28
|
-
mountain: 0.57
|
|
29
|
-
};
|
|
30
|
-
const cRR = 0.0036;
|
|
31
|
-
const bit = (nr) => (1 << nr);
|
|
32
|
-
const IndoorBikeDataFlag = {
|
|
33
|
-
MoreData: bit(0),
|
|
34
|
-
AverageSpeedPresent: bit(1),
|
|
35
|
-
InstantaneousCadence: bit(2),
|
|
36
|
-
AverageCadencePresent: bit(3),
|
|
37
|
-
TotalDistancePresent: bit(4),
|
|
38
|
-
ResistanceLevelPresent: bit(5),
|
|
39
|
-
InstantaneousPowerPresent: bit(6),
|
|
40
|
-
AveragePowerPresent: bit(7),
|
|
41
|
-
ExpendedEnergyPresent: bit(8),
|
|
42
|
-
HeartRatePresent: bit(9),
|
|
43
|
-
MetabolicEquivalentPresent: bit(10),
|
|
44
|
-
ElapsedTimePresent: bit(11),
|
|
45
|
-
RemainingTimePresent: bit(12)
|
|
46
|
-
};
|
|
47
|
-
const FitnessMachineFeatureFlag = {
|
|
48
|
-
AverageSpeedSupported: bit(0),
|
|
49
|
-
CadenceSupported: bit(1),
|
|
50
|
-
TotalDistanceSupported: bit(2),
|
|
51
|
-
InclinationSupported: bit(3),
|
|
52
|
-
ElevationGainSupported: bit(4),
|
|
53
|
-
PaceSupported: bit(5),
|
|
54
|
-
StepCountSupported: bit(6),
|
|
55
|
-
ResistanceLevelSupported: bit(7),
|
|
56
|
-
StrideCountSupported: bit(8),
|
|
57
|
-
ExpendedEnergySupported: bit(9),
|
|
58
|
-
HeartRateMeasurementSupported: bit(10),
|
|
59
|
-
MetabolicEquivalentSupported: bit(11),
|
|
60
|
-
ElapsedTimeSupported: bit(12),
|
|
61
|
-
RemainingTimeSupported: bit(13),
|
|
62
|
-
PowerMeasurementSupported: bit(14),
|
|
63
|
-
ForceOnBeltAndPowerOutputSupported: bit(15),
|
|
64
|
-
UserDataRetentionSupported: bit(16)
|
|
65
|
-
};
|
|
66
|
-
const TargetSettingFeatureFlag = {
|
|
67
|
-
SpeedTargetSettingSupported: bit(0),
|
|
68
|
-
InclinationTargetSettingSupported: bit(1),
|
|
69
|
-
ResistanceTargetSettingSupported: bit(2),
|
|
70
|
-
PowerTargetSettingSupported: bit(3),
|
|
71
|
-
HeartRateTargetSettingSupported: bit(4),
|
|
72
|
-
TargetedExpendedEnergyConfigurationSupported: bit(5),
|
|
73
|
-
TargetedStepNumberConfigurationSupported: bit(6),
|
|
74
|
-
TargetedStrideNumberConfigurationSupported: bit(7),
|
|
75
|
-
TargetedDistanceConfigurationSupported: bit(8),
|
|
76
|
-
TargetedTrainingTimeConfigurationSupported: bit(9),
|
|
77
|
-
TargetedTimeInTwoHeartRateZonesConfigurationSupported: bit(10),
|
|
78
|
-
TargetedTimeInThreeHeartRateZonesConfigurationSupported: bit(11),
|
|
79
|
-
TargetedTimeInFiveHeartRateZonesConfigurationSupported: bit(12),
|
|
80
|
-
IndoorBikeSimulationParametersSupported: bit(13),
|
|
81
|
-
WheelCircumferenceConfigurationSupported: bit(14),
|
|
82
|
-
SpinDownControlSupported: bit(15),
|
|
83
|
-
TargetedCadenceConfigurationSupported: bit(16)
|
|
84
|
-
};
|
|
85
|
-
class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
86
|
-
constructor(props) {
|
|
87
|
-
super(props);
|
|
88
|
-
this.features = undefined;
|
|
89
|
-
this.hasControl = false;
|
|
90
|
-
this.isCheckingControl = false;
|
|
91
|
-
this.isCPSubscribed = false;
|
|
92
|
-
this.crr = 0.0033;
|
|
93
|
-
this.cw = 0.6;
|
|
94
|
-
this.windSpeed = 0;
|
|
95
|
-
this.wheelSize = 2100;
|
|
96
|
-
this.data = {};
|
|
97
|
-
this.services = BleFitnessMachineDevice.services;
|
|
98
|
-
}
|
|
99
|
-
isMatching(characteristics) {
|
|
100
|
-
if (!characteristics)
|
|
101
|
-
return false;
|
|
102
|
-
const hasStatus = characteristics.find(c => c === consts_1.FTMS_STATUS) !== undefined;
|
|
103
|
-
const hasCP = characteristics.find(c => c === consts_1.FTMS_CP) !== undefined;
|
|
104
|
-
const hasIndoorBike = characteristics.find(c => c === consts_1.INDOOR_BIKE_DATA) !== undefined;
|
|
105
|
-
return hasStatus && hasCP && hasIndoorBike;
|
|
106
|
-
}
|
|
107
|
-
subscribeWriteResponse(cuuid) {
|
|
108
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
109
|
-
this.logEvent({ message: 'subscribe to CP response', characteristics: cuuid });
|
|
110
|
-
const connector = this.ble.getConnector(this.peripheral);
|
|
111
|
-
const isAlreadySubscribed = connector.isSubscribed(cuuid);
|
|
112
|
-
if (!isAlreadySubscribed) {
|
|
113
|
-
connector.removeAllListeners(cuuid);
|
|
114
|
-
let prev = undefined;
|
|
115
|
-
let prevTS = undefined;
|
|
116
|
-
connector.on(cuuid, (uuid, data) => {
|
|
117
|
-
const message = data.toString('hex');
|
|
118
|
-
if (prevTS && prev && message === prev && Date.now() - prevTS < 500) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
prevTS = Date.now();
|
|
122
|
-
prev = message;
|
|
123
|
-
this.onData(uuid, data);
|
|
124
|
-
});
|
|
125
|
-
yield connector.subscribe(cuuid);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
subscribeAll(conn) {
|
|
130
|
-
return new Promise(resolve => {
|
|
131
|
-
const characteristics = [consts_1.INDOOR_BIKE_DATA, consts_1.FTMS_STATUS, consts_1.FTMS_CP];
|
|
132
|
-
const timeout = Date.now() + 5500;
|
|
133
|
-
const iv = setInterval(() => {
|
|
134
|
-
const subscriptionStatus = characteristics.map(c => this.subscribedCharacteristics.find(s => s === c) !== undefined);
|
|
135
|
-
const done = subscriptionStatus.filter(s => s === true).length === characteristics.length;
|
|
136
|
-
if (done || Date.now() > timeout) {
|
|
137
|
-
clearInterval(iv);
|
|
138
|
-
resolve();
|
|
139
|
-
}
|
|
140
|
-
}, 100);
|
|
141
|
-
try {
|
|
142
|
-
const connector = conn || this.ble.getConnector(this.peripheral);
|
|
143
|
-
for (let i = 0; i < characteristics.length; i++) {
|
|
144
|
-
const c = characteristics[i];
|
|
145
|
-
const isAlreadySubscribed = connector.isSubscribed(c);
|
|
146
|
-
if (!isAlreadySubscribed) {
|
|
147
|
-
connector.removeAllListeners(c);
|
|
148
|
-
connector.on(c, (uuid, data) => {
|
|
149
|
-
this.onData(uuid, data);
|
|
150
|
-
});
|
|
151
|
-
connector.subscribe(c);
|
|
152
|
-
this.subscribedCharacteristics.push(c);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
catch (err) {
|
|
157
|
-
this.logEvent({ message: 'Error', fn: 'subscribeAll()', error: err.message, stack: err.stack });
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
init() {
|
|
162
|
-
const _super = Object.create(null, {
|
|
163
|
-
initDevice: { get: () => super.initDevice }
|
|
164
|
-
});
|
|
165
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
166
|
-
try {
|
|
167
|
-
yield _super.initDevice.call(this);
|
|
168
|
-
yield this.getFitnessMachineFeatures();
|
|
169
|
-
this.logEvent({ message: 'device info', deviceInfo: this.deviceInfo, features: this.features });
|
|
170
|
-
}
|
|
171
|
-
catch (err) {
|
|
172
|
-
this.logEvent({ message: 'error', fn: 'BleFitnessMachineDevice.init()', error: err.message || err, stack: err.stack });
|
|
173
|
-
return Promise.resolve(false);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
onDisconnect() {
|
|
178
|
-
super.onDisconnect();
|
|
179
|
-
this.hasControl = false;
|
|
180
|
-
}
|
|
181
|
-
getProfile() {
|
|
182
|
-
return 'Smart Trainer';
|
|
183
|
-
}
|
|
184
|
-
getServiceUUids() {
|
|
185
|
-
return BleFitnessMachineDevice.services;
|
|
186
|
-
}
|
|
187
|
-
isBike() {
|
|
188
|
-
return this.features === undefined ||
|
|
189
|
-
((this.features.targetSettings & TargetSettingFeatureFlag.IndoorBikeSimulationParametersSupported) !== 0);
|
|
190
|
-
}
|
|
191
|
-
isPower() {
|
|
192
|
-
if (this.hasService('1818'))
|
|
193
|
-
return true;
|
|
194
|
-
if (this.features === undefined)
|
|
195
|
-
return false;
|
|
196
|
-
const { fitnessMachine } = this.features;
|
|
197
|
-
if (fitnessMachine & FitnessMachineFeatureFlag.PowerMeasurementSupported)
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
isHrm() {
|
|
201
|
-
return this.hasService('180d') || (this.features && (this.features.fitnessMachine & FitnessMachineFeatureFlag.HeartRateMeasurementSupported) !== 0);
|
|
202
|
-
}
|
|
203
|
-
parseHrm(_data) {
|
|
204
|
-
const data = Buffer.from(_data);
|
|
205
|
-
try {
|
|
206
|
-
const flags = data.readUInt8(0);
|
|
207
|
-
if (flags % 1 === 0) {
|
|
208
|
-
this.data.heartrate = data.readUInt8(1);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
this.data.heartrate = data.readUInt16LE(1);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
this.logEvent({ message: 'error', fn: 'parseHrm()', error: err.message | err, stack: err.stack });
|
|
216
|
-
}
|
|
217
|
-
return Object.assign(Object.assign({}, this.data), { raw: `2a37:${data.toString('hex')}` });
|
|
218
|
-
}
|
|
219
|
-
setCrr(crr) { this.crr = crr; }
|
|
220
|
-
getCrr() { return this.crr; }
|
|
221
|
-
setCw(cw) { this.cw = cw; }
|
|
222
|
-
getCw() { return this.cw; }
|
|
223
|
-
setWindSpeed(windSpeed) { this.windSpeed = windSpeed; }
|
|
224
|
-
getWindSpeed() { return this.windSpeed; }
|
|
225
|
-
parseIndoorBikeData(_data) {
|
|
226
|
-
const data = Buffer.from(_data);
|
|
227
|
-
try {
|
|
228
|
-
const flags = data.readUInt16LE(0);
|
|
229
|
-
let offset = 2;
|
|
230
|
-
if ((flags & IndoorBikeDataFlag.MoreData) === 0) {
|
|
231
|
-
this.data.speed = data.readUInt16LE(offset) / 100;
|
|
232
|
-
offset += 2;
|
|
233
|
-
}
|
|
234
|
-
if (flags & IndoorBikeDataFlag.AverageSpeedPresent) {
|
|
235
|
-
this.data.averageSpeed = data.readUInt16LE(offset) / 100;
|
|
236
|
-
offset += 2;
|
|
237
|
-
}
|
|
238
|
-
if (flags & IndoorBikeDataFlag.InstantaneousCadence) {
|
|
239
|
-
this.data.cadence = data.readUInt16LE(offset) / 2;
|
|
240
|
-
offset += 2;
|
|
241
|
-
}
|
|
242
|
-
if (flags & IndoorBikeDataFlag.AverageCadencePresent) {
|
|
243
|
-
this.data.averageCadence = data.readUInt16LE(offset) / 2;
|
|
244
|
-
offset += 2;
|
|
245
|
-
}
|
|
246
|
-
if (flags & IndoorBikeDataFlag.TotalDistancePresent) {
|
|
247
|
-
const dvLow = data.readUInt8(offset);
|
|
248
|
-
offset += 1;
|
|
249
|
-
const dvHigh = data.readUInt16LE(offset);
|
|
250
|
-
offset += 2;
|
|
251
|
-
this.data.totalDistance = (dvHigh << 8) + dvLow;
|
|
252
|
-
}
|
|
253
|
-
if (flags & IndoorBikeDataFlag.ResistanceLevelPresent) {
|
|
254
|
-
this.data.resistanceLevel = data.readInt16LE(offset);
|
|
255
|
-
offset += 2;
|
|
256
|
-
}
|
|
257
|
-
if (flags & IndoorBikeDataFlag.InstantaneousPowerPresent) {
|
|
258
|
-
this.data.instantaneousPower = data.readInt16LE(offset);
|
|
259
|
-
offset += 2;
|
|
260
|
-
}
|
|
261
|
-
if (flags & IndoorBikeDataFlag.AveragePowerPresent) {
|
|
262
|
-
this.data.averagePower = data.readInt16LE(offset);
|
|
263
|
-
offset += 2;
|
|
264
|
-
}
|
|
265
|
-
if (flags & IndoorBikeDataFlag.ExpendedEnergyPresent) {
|
|
266
|
-
this.data.totalEnergy = data.readUInt16LE(offset);
|
|
267
|
-
offset += 2;
|
|
268
|
-
this.data.energyPerHour = data.readUInt16LE(offset);
|
|
269
|
-
offset += 2;
|
|
270
|
-
this.data.energyPerMinute = data.readUInt8(offset);
|
|
271
|
-
offset += 1;
|
|
272
|
-
}
|
|
273
|
-
if (flags & IndoorBikeDataFlag.HeartRatePresent) {
|
|
274
|
-
this.data.heartrate = data.readUInt8(offset);
|
|
275
|
-
offset += 1;
|
|
276
|
-
}
|
|
277
|
-
if (flags & IndoorBikeDataFlag.MetabolicEquivalentPresent) {
|
|
278
|
-
this.data.metabolicEquivalent = data.readUInt8(offset) / 10;
|
|
279
|
-
offset += 2;
|
|
280
|
-
}
|
|
281
|
-
if (flags & IndoorBikeDataFlag.ElapsedTimePresent) {
|
|
282
|
-
this.data.time = data.readUInt16LE(offset);
|
|
283
|
-
offset += 2;
|
|
284
|
-
}
|
|
285
|
-
if (flags & IndoorBikeDataFlag.RemainingTimePresent) {
|
|
286
|
-
this.data.remainingTime = data.readUInt16LE(offset);
|
|
287
|
-
offset += 2;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
this.logEvent({ message: 'error', fn: 'parseIndoorBikeData()', error: err.message | err, stack: err.stack });
|
|
292
|
-
}
|
|
293
|
-
return Object.assign(Object.assign({}, this.data), { raw: `2ad2:${data.toString('hex')}` });
|
|
294
|
-
}
|
|
295
|
-
parseFitnessMachineStatus(_data) {
|
|
296
|
-
const data = Buffer.from(_data);
|
|
297
|
-
try {
|
|
298
|
-
const OpCode = data.readUInt8(0);
|
|
299
|
-
switch (OpCode) {
|
|
300
|
-
case 8:
|
|
301
|
-
this.data.targetPower = data.readInt16LE(1);
|
|
302
|
-
break;
|
|
303
|
-
case 6:
|
|
304
|
-
this.data.targetInclination = data.readInt16LE(1) / 10;
|
|
305
|
-
break;
|
|
306
|
-
case 4:
|
|
307
|
-
this.data.status = "STARTED";
|
|
308
|
-
break;
|
|
309
|
-
case 3:
|
|
310
|
-
case 2:
|
|
311
|
-
this.data.status = "STOPPED";
|
|
312
|
-
break;
|
|
313
|
-
case 20:
|
|
314
|
-
const spinDownStatus = data.readUInt8(1);
|
|
315
|
-
switch (spinDownStatus) {
|
|
316
|
-
case 1:
|
|
317
|
-
this.data.status = "SPIN DOWN REQUESTED";
|
|
318
|
-
break;
|
|
319
|
-
case 2:
|
|
320
|
-
this.data.status = "SPIN DOWN SUCCESS";
|
|
321
|
-
break;
|
|
322
|
-
case 3:
|
|
323
|
-
this.data.status = "SPIN DOWN ERROR";
|
|
324
|
-
break;
|
|
325
|
-
case 4:
|
|
326
|
-
this.data.status = "STOP PEDALING";
|
|
327
|
-
break;
|
|
328
|
-
default: break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
catch (err) {
|
|
333
|
-
this.logEvent({ message: 'error', fn: 'parseFitnessMachineStatus()', error: err.message | err, stack: err.stack });
|
|
334
|
-
}
|
|
335
|
-
return Object.assign(Object.assign({}, this.data), { raw: `2ada:${data.toString('hex')}` });
|
|
336
|
-
}
|
|
337
|
-
getFitnessMachineFeatures() {
|
|
338
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
339
|
-
if (this.features)
|
|
340
|
-
return this.features;
|
|
341
|
-
try {
|
|
342
|
-
const data = yield this.read('2acc');
|
|
343
|
-
const buffer = data ? Buffer.from(data) : undefined;
|
|
344
|
-
if (buffer) {
|
|
345
|
-
const fitnessMachine = buffer.readUInt32LE(0);
|
|
346
|
-
const targetSettings = buffer.readUInt32LE(4);
|
|
347
|
-
this.features = { fitnessMachine, targetSettings };
|
|
348
|
-
this.logEvent({ message: 'supported Features: ', fatures: this.features });
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
catch (err) {
|
|
352
|
-
this.logEvent({ message: 'could not read FitnessMachineFeatures', error: err.message, stack: err.stack });
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
onData(characteristic, data) {
|
|
357
|
-
const hasData = super.onData(characteristic, data);
|
|
358
|
-
if (!hasData)
|
|
359
|
-
return false;
|
|
360
|
-
const uuid = characteristic.toLocaleLowerCase();
|
|
361
|
-
let res = undefined;
|
|
362
|
-
switch (uuid) {
|
|
363
|
-
case consts_1.INDOOR_BIKE_DATA:
|
|
364
|
-
res = this.parseIndoorBikeData(data);
|
|
365
|
-
break;
|
|
366
|
-
case '2a37':
|
|
367
|
-
res = this.parseHrm(data);
|
|
368
|
-
break;
|
|
369
|
-
case consts_1.FTMS_STATUS:
|
|
370
|
-
res = this.parseFitnessMachineStatus(data);
|
|
371
|
-
break;
|
|
372
|
-
case '2a63':
|
|
373
|
-
case '2a5b':
|
|
374
|
-
case '347b0011-7635-408b-8918-8ff3949ce592':
|
|
375
|
-
break;
|
|
376
|
-
default:
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
if (res) {
|
|
380
|
-
this.emit('data', res);
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
writeFtmsMessage(requestedOpCode, data, props) {
|
|
386
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
387
|
-
try {
|
|
388
|
-
this.logEvent({ message: 'fmts:write', data: data.toString('hex') });
|
|
389
|
-
const res = yield this.write(consts_1.FTMS_CP, data, props);
|
|
390
|
-
const responseData = Buffer.from(res);
|
|
391
|
-
const opCode = responseData.readUInt8(0);
|
|
392
|
-
const request = responseData.readUInt8(1);
|
|
393
|
-
const result = responseData.readUInt8(2);
|
|
394
|
-
if (opCode !== 128 || request !== requestedOpCode)
|
|
395
|
-
throw new Error('Illegal response ');
|
|
396
|
-
this.logEvent({ message: 'fmts:write result', res, result });
|
|
397
|
-
return result;
|
|
398
|
-
}
|
|
399
|
-
catch (err) {
|
|
400
|
-
this.logEvent({ message: 'fmts:write failed', opCode: requestedOpCode, reason: err.message });
|
|
401
|
-
return 4;
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
requestControl() {
|
|
406
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
407
|
-
let to = undefined;
|
|
408
|
-
if (this.isCheckingControl) {
|
|
409
|
-
to = setTimeout(() => { }, 3500);
|
|
410
|
-
}
|
|
411
|
-
if (this.hasControl)
|
|
412
|
-
return true;
|
|
413
|
-
this.logEvent({ message: 'requestControl' });
|
|
414
|
-
this.isCheckingControl = true;
|
|
415
|
-
const data = Buffer.alloc(1);
|
|
416
|
-
data.writeUInt8(0, 0);
|
|
417
|
-
const res = yield this.writeFtmsMessage(0, data, { timeout: 5000 });
|
|
418
|
-
if (res === 1) {
|
|
419
|
-
this.hasControl = true;
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
this.logEvent({ message: 'requestControl failed' });
|
|
423
|
-
}
|
|
424
|
-
this.isCheckingControl = false;
|
|
425
|
-
if (to)
|
|
426
|
-
clearTimeout(to);
|
|
427
|
-
return this.hasControl;
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
setTargetPower(power) {
|
|
431
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
432
|
-
this.logEvent({ message: 'setTargetPower', power, skip: (this.data.targetPower !== undefined && this.data.targetPower === power) });
|
|
433
|
-
if (this.data.targetPower !== undefined && this.data.targetPower === power)
|
|
434
|
-
return true;
|
|
435
|
-
if (!this.hasControl)
|
|
436
|
-
return;
|
|
437
|
-
const hasControl = yield this.requestControl();
|
|
438
|
-
if (!hasControl) {
|
|
439
|
-
this.logEvent({ message: 'setTargetPower failed', reason: 'control is disabled' });
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
const data = Buffer.alloc(3);
|
|
443
|
-
data.writeUInt8(5, 0);
|
|
444
|
-
data.writeInt16LE(Math.round(power), 1);
|
|
445
|
-
const res = yield this.writeFtmsMessage(5, data);
|
|
446
|
-
return (res === 1);
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
setSlope(slope) {
|
|
450
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
451
|
-
this.logEvent({ message: 'setSlope', slope });
|
|
452
|
-
const hasControl = yield this.requestControl();
|
|
453
|
-
if (!hasControl)
|
|
454
|
-
return;
|
|
455
|
-
const { windSpeed, crr, cw } = this;
|
|
456
|
-
return yield this.setIndoorBikeSimulation(windSpeed, slope, crr, cw);
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
setTargetInclination(inclination) {
|
|
460
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
461
|
-
if (this.data.targetInclination !== undefined && this.data.targetInclination === inclination)
|
|
462
|
-
return true;
|
|
463
|
-
if (!this.hasControl)
|
|
464
|
-
return;
|
|
465
|
-
const hasControl = yield this.requestControl();
|
|
466
|
-
if (!hasControl) {
|
|
467
|
-
this.logEvent({ message: 'setTargetInclination failed', reason: 'control is disabled' });
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
const data = Buffer.alloc(3);
|
|
471
|
-
data.writeUInt8(3, 0);
|
|
472
|
-
data.writeInt16LE(Math.round(inclination * 10), 1);
|
|
473
|
-
const res = yield this.writeFtmsMessage(3, data);
|
|
474
|
-
return (res === 1);
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
setIndoorBikeSimulation(windSpeed, gradient, crr, cw) {
|
|
478
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
479
|
-
const hasControl = yield this.requestControl();
|
|
480
|
-
if (!hasControl) {
|
|
481
|
-
this.logEvent({ message: 'setIndoorBikeSimulation failed', reason: 'control is disabled' });
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
const data = Buffer.alloc(7);
|
|
485
|
-
data.writeUInt8(17, 0);
|
|
486
|
-
data.writeInt16LE(Math.round(windSpeed * 1000), 1);
|
|
487
|
-
data.writeInt16LE(Math.round(gradient * 100), 3);
|
|
488
|
-
data.writeUInt8(Math.round(crr * 10000), 5);
|
|
489
|
-
data.writeUInt8(Math.round(cw * 100), 6);
|
|
490
|
-
const res = yield this.writeFtmsMessage(17, data);
|
|
491
|
-
return (res === 1);
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
startRequest() {
|
|
495
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
496
|
-
const hasControl = yield this.requestControl();
|
|
497
|
-
if (!hasControl) {
|
|
498
|
-
this.logEvent({ message: 'startRequest failed', reason: 'control is disabled' });
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
const data = Buffer.alloc(1);
|
|
502
|
-
data.writeUInt8(7, 0);
|
|
503
|
-
const res = yield this.writeFtmsMessage(7, data);
|
|
504
|
-
return (res === 1);
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
stopRequest() {
|
|
508
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
509
|
-
const hasControl = yield this.requestControl();
|
|
510
|
-
if (!hasControl) {
|
|
511
|
-
this.logEvent({ message: 'stopRequest failed', reason: 'control is disabled' });
|
|
512
|
-
return false;
|
|
513
|
-
}
|
|
514
|
-
const data = Buffer.alloc(2);
|
|
515
|
-
data.writeUInt8(8, 0);
|
|
516
|
-
data.writeUInt8(1, 1);
|
|
517
|
-
const res = yield this.writeFtmsMessage(8, data);
|
|
518
|
-
return (res === 1);
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
PauseRequest() {
|
|
522
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
523
|
-
const hasControl = yield this.requestControl();
|
|
524
|
-
if (!hasControl) {
|
|
525
|
-
this.logEvent({ message: 'PauseRequest failed', reason: 'control is disabled' });
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
const data = Buffer.alloc(2);
|
|
529
|
-
data.writeUInt8(8, 0);
|
|
530
|
-
data.writeUInt8(2, 1);
|
|
531
|
-
const res = yield this.writeFtmsMessage(8, data);
|
|
532
|
-
return (res === 1);
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
reset() {
|
|
536
|
-
this.data = {};
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
exports.default = BleFitnessMachineDevice;
|
|
540
|
-
BleFitnessMachineDevice.services = [consts_1.FTMS];
|
|
541
|
-
BleFitnessMachineDevice.characteristics = ['2acc', consts_1.INDOOR_BIKE_DATA, '2ad6', '2ad8', consts_1.FTMS_CP, consts_1.FTMS_STATUS];
|
|
542
|
-
BleFitnessMachineDevice.detectionPriority = 100;
|
|
543
|
-
ble_interface_1.default.register('BleFitnessMachineDevice', 'fm', BleFitnessMachineDevice, BleFitnessMachineDevice.services);
|
|
544
|
-
class FmAdapter extends device_1.default {
|
|
545
|
-
constructor(device, protocol) {
|
|
546
|
-
super(protocol);
|
|
547
|
-
this.ignore = false;
|
|
548
|
-
this.paused = false;
|
|
549
|
-
this.distanceInternal = 0;
|
|
550
|
-
this.device = device;
|
|
551
|
-
this.ble = protocol.ble;
|
|
552
|
-
this.cyclingMode = this.getDefaultCyclingMode();
|
|
553
|
-
this.logger = new gd_eventlog_1.EventLogger('BLE-FM');
|
|
554
|
-
if (this.device)
|
|
555
|
-
this.device.setLogger(this.logger);
|
|
556
|
-
}
|
|
557
|
-
isBike() { return this.device.isBike(); }
|
|
558
|
-
isHrm() { return this.device.isHrm(); }
|
|
559
|
-
isPower() { return this.device.isPower(); }
|
|
560
|
-
isSame(device) {
|
|
561
|
-
if (!(device instanceof FmAdapter))
|
|
562
|
-
return false;
|
|
563
|
-
const adapter = device;
|
|
564
|
-
return (adapter.getName() === this.getName() && adapter.getProfile() === this.getProfile());
|
|
565
|
-
}
|
|
566
|
-
getProfile() {
|
|
567
|
-
const profile = this.device ? this.device.getProfile() : undefined;
|
|
568
|
-
return profile || 'Smart Trainer';
|
|
569
|
-
}
|
|
570
|
-
getName() {
|
|
571
|
-
return `${this.device.name}`;
|
|
572
|
-
}
|
|
573
|
-
getDisplayName() {
|
|
574
|
-
return this.getName();
|
|
575
|
-
}
|
|
576
|
-
getSupportedCyclingModes() {
|
|
577
|
-
return [ble_st_mode_1.default, ble_erg_mode_1.default, power_meter_1.default];
|
|
578
|
-
}
|
|
579
|
-
setCyclingMode(mode, settings) {
|
|
580
|
-
let selectedMode;
|
|
581
|
-
if (typeof mode === 'string') {
|
|
582
|
-
const supported = this.getSupportedCyclingModes();
|
|
583
|
-
const CyclingModeClass = supported.find(M => { const m = new M(this); return m.getName() === mode; });
|
|
584
|
-
if (CyclingModeClass) {
|
|
585
|
-
this.cyclingMode = new CyclingModeClass(this, settings);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
selectedMode = this.getDefaultCyclingMode();
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
selectedMode = mode;
|
|
592
|
-
}
|
|
593
|
-
this.cyclingMode = selectedMode;
|
|
594
|
-
this.cyclingMode.setSettings(settings);
|
|
595
|
-
}
|
|
596
|
-
getCyclingMode() {
|
|
597
|
-
if (!this.cyclingMode)
|
|
598
|
-
this.cyclingMode = this.getDefaultCyclingMode();
|
|
599
|
-
return this.cyclingMode;
|
|
600
|
-
}
|
|
601
|
-
getDefaultCyclingMode() {
|
|
602
|
-
return new ble_st_mode_1.default(this);
|
|
603
|
-
}
|
|
604
|
-
getPort() {
|
|
605
|
-
return 'ble';
|
|
606
|
-
}
|
|
607
|
-
setIgnoreBike(ignore) {
|
|
608
|
-
this.ignore = ignore;
|
|
609
|
-
}
|
|
610
|
-
setIgnorePower(ignore) {
|
|
611
|
-
this.ignore = ignore;
|
|
612
|
-
}
|
|
613
|
-
onDeviceData(deviceData) {
|
|
614
|
-
if (this.prevDataTS && Date.now() - this.prevDataTS < 1000)
|
|
615
|
-
return;
|
|
616
|
-
this.prevDataTS = Date.now();
|
|
617
|
-
this.logger.logEvent({ message: 'onDeviceData', data: deviceData });
|
|
618
|
-
let incyclistData = this.mapData(deviceData);
|
|
619
|
-
incyclistData = this.getCyclingMode().updateData(incyclistData);
|
|
620
|
-
const data = this.transformData(incyclistData);
|
|
621
|
-
if (this.onDataFn && !this.ignore && !this.paused)
|
|
622
|
-
this.onDataFn(data);
|
|
623
|
-
}
|
|
624
|
-
mapData(deviceData) {
|
|
625
|
-
const data = {
|
|
626
|
-
isPedalling: false,
|
|
627
|
-
power: 0,
|
|
628
|
-
pedalRpm: undefined,
|
|
629
|
-
speed: 0,
|
|
630
|
-
heartrate: 0,
|
|
631
|
-
distanceInternal: 0,
|
|
632
|
-
slope: undefined,
|
|
633
|
-
time: undefined
|
|
634
|
-
};
|
|
635
|
-
data.power = (deviceData.instantaneousPower !== undefined ? deviceData.instantaneousPower : data.power);
|
|
636
|
-
data.pedalRpm = (deviceData.cadence !== undefined ? deviceData.cadence : data.pedalRpm);
|
|
637
|
-
data.time = (deviceData.time !== undefined ? deviceData.time : data.time);
|
|
638
|
-
data.isPedalling = data.pedalRpm > 0 || (data.pedalRpm === undefined && data.power > 0);
|
|
639
|
-
return data;
|
|
640
|
-
}
|
|
641
|
-
transformData(bikeData) {
|
|
642
|
-
if (this.ignore) {
|
|
643
|
-
return {};
|
|
644
|
-
}
|
|
645
|
-
if (bikeData === undefined)
|
|
646
|
-
return;
|
|
647
|
-
let distance = 0;
|
|
648
|
-
if (this.distanceInternal !== undefined && bikeData.distanceInternal !== undefined) {
|
|
649
|
-
distance = Math.round(bikeData.distanceInternal - this.distanceInternal);
|
|
650
|
-
}
|
|
651
|
-
if (bikeData.distanceInternal !== undefined)
|
|
652
|
-
this.distanceInternal = bikeData.distanceInternal;
|
|
653
|
-
let data = {
|
|
654
|
-
speed: bikeData.speed,
|
|
655
|
-
slope: bikeData.slope,
|
|
656
|
-
power: bikeData.power !== undefined ? Math.round(bikeData.power) : undefined,
|
|
657
|
-
cadence: bikeData.pedalRpm !== undefined ? Math.round(bikeData.pedalRpm) : undefined,
|
|
658
|
-
distance,
|
|
659
|
-
timestamp: Date.now()
|
|
660
|
-
};
|
|
661
|
-
return data;
|
|
662
|
-
}
|
|
663
|
-
start(props) {
|
|
664
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
665
|
-
this.logger.logEvent({ message: 'ftms: start requested', profile: this.getProfile(), props });
|
|
666
|
-
const { restart } = props || {};
|
|
667
|
-
if (!restart && this.ble.isScanning())
|
|
668
|
-
yield this.ble.stopScan();
|
|
669
|
-
try {
|
|
670
|
-
let bleDevice;
|
|
671
|
-
if (!this.device || !restart) {
|
|
672
|
-
bleDevice = (yield this.ble.connectDevice(this.device));
|
|
673
|
-
this.device = bleDevice;
|
|
674
|
-
}
|
|
675
|
-
else
|
|
676
|
-
bleDevice = this.device;
|
|
677
|
-
if (bleDevice) {
|
|
678
|
-
bleDevice.setLogger(this.logger);
|
|
679
|
-
const mode = this.getCyclingMode();
|
|
680
|
-
if (mode && mode.getSetting('bikeType')) {
|
|
681
|
-
const bikeType = mode.getSetting('bikeType').toLowerCase();
|
|
682
|
-
this.device.setCrr(cRR);
|
|
683
|
-
switch (bikeType) {
|
|
684
|
-
case 'race':
|
|
685
|
-
this.device.setCw(cwABike.race);
|
|
686
|
-
break;
|
|
687
|
-
case 'triathlon':
|
|
688
|
-
this.device.setCw(cwABike.triathlon);
|
|
689
|
-
break;
|
|
690
|
-
case 'mountain':
|
|
691
|
-
this.device.setCw(cwABike.mountain);
|
|
692
|
-
break;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
let hasControl = yield this.device.requestControl();
|
|
696
|
-
if (!hasControl) {
|
|
697
|
-
let retry = 1;
|
|
698
|
-
while (!hasControl && retry < 3) {
|
|
699
|
-
yield sleep(1000);
|
|
700
|
-
hasControl = yield this.device.requestControl();
|
|
701
|
-
retry++;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
if (!hasControl)
|
|
705
|
-
throw new Error('could not establish control');
|
|
706
|
-
const startRequest = this.getCyclingMode().getBikeInitRequest();
|
|
707
|
-
yield this.sendUpdate(startRequest);
|
|
708
|
-
bleDevice.on('data', (data) => {
|
|
709
|
-
this.onDeviceData(data);
|
|
710
|
-
});
|
|
711
|
-
return true;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
catch (err) {
|
|
715
|
-
this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
|
|
716
|
-
throw new Error(`could not start device, reason:${err.message}`);
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
stop() {
|
|
721
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
722
|
-
this.logger.logEvent({ message: 'stop requested', profile: this.getProfile() });
|
|
723
|
-
this.distanceInternal = 0;
|
|
724
|
-
this.device.reset();
|
|
725
|
-
return this.device.disconnect();
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
sendUpdate(request) {
|
|
729
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
730
|
-
if (this.paused || !this.device)
|
|
731
|
-
return;
|
|
732
|
-
const update = this.getCyclingMode().sendBikeUpdate(request);
|
|
733
|
-
this.logger.logEvent({ message: 'send bike update requested', profile: this.getProfile(), update, request });
|
|
734
|
-
if (update.slope !== undefined) {
|
|
735
|
-
yield this.device.setSlope(update.slope);
|
|
736
|
-
}
|
|
737
|
-
if (update.targetPower !== undefined) {
|
|
738
|
-
yield this.device.setTargetPower(update.targetPower);
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
pause() { this.paused = true; return Promise.resolve(true); }
|
|
743
|
-
resume() { this.paused = false; return Promise.resolve(true); }
|
|
744
|
-
}
|
|
745
|
-
exports.FmAdapter = FmAdapter;
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FmAdapter = void 0;
|
|
16
|
+
const ble_device_1 = require("./ble-device");
|
|
17
|
+
const ble_interface_1 = __importDefault(require("./ble-interface"));
|
|
18
|
+
const device_1 = __importDefault(require("../device"));
|
|
19
|
+
const gd_eventlog_1 = require("gd-eventlog");
|
|
20
|
+
const power_meter_1 = __importDefault(require("../modes/power-meter"));
|
|
21
|
+
const ble_st_mode_1 = __importDefault(require("./ble-st-mode"));
|
|
22
|
+
const ble_erg_mode_1 = __importDefault(require("./ble-erg-mode"));
|
|
23
|
+
const consts_1 = require("./consts");
|
|
24
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
25
|
+
const cwABike = {
|
|
26
|
+
race: 0.35,
|
|
27
|
+
triathlon: 0.29,
|
|
28
|
+
mountain: 0.57
|
|
29
|
+
};
|
|
30
|
+
const cRR = 0.0036;
|
|
31
|
+
const bit = (nr) => (1 << nr);
|
|
32
|
+
const IndoorBikeDataFlag = {
|
|
33
|
+
MoreData: bit(0),
|
|
34
|
+
AverageSpeedPresent: bit(1),
|
|
35
|
+
InstantaneousCadence: bit(2),
|
|
36
|
+
AverageCadencePresent: bit(3),
|
|
37
|
+
TotalDistancePresent: bit(4),
|
|
38
|
+
ResistanceLevelPresent: bit(5),
|
|
39
|
+
InstantaneousPowerPresent: bit(6),
|
|
40
|
+
AveragePowerPresent: bit(7),
|
|
41
|
+
ExpendedEnergyPresent: bit(8),
|
|
42
|
+
HeartRatePresent: bit(9),
|
|
43
|
+
MetabolicEquivalentPresent: bit(10),
|
|
44
|
+
ElapsedTimePresent: bit(11),
|
|
45
|
+
RemainingTimePresent: bit(12)
|
|
46
|
+
};
|
|
47
|
+
const FitnessMachineFeatureFlag = {
|
|
48
|
+
AverageSpeedSupported: bit(0),
|
|
49
|
+
CadenceSupported: bit(1),
|
|
50
|
+
TotalDistanceSupported: bit(2),
|
|
51
|
+
InclinationSupported: bit(3),
|
|
52
|
+
ElevationGainSupported: bit(4),
|
|
53
|
+
PaceSupported: bit(5),
|
|
54
|
+
StepCountSupported: bit(6),
|
|
55
|
+
ResistanceLevelSupported: bit(7),
|
|
56
|
+
StrideCountSupported: bit(8),
|
|
57
|
+
ExpendedEnergySupported: bit(9),
|
|
58
|
+
HeartRateMeasurementSupported: bit(10),
|
|
59
|
+
MetabolicEquivalentSupported: bit(11),
|
|
60
|
+
ElapsedTimeSupported: bit(12),
|
|
61
|
+
RemainingTimeSupported: bit(13),
|
|
62
|
+
PowerMeasurementSupported: bit(14),
|
|
63
|
+
ForceOnBeltAndPowerOutputSupported: bit(15),
|
|
64
|
+
UserDataRetentionSupported: bit(16)
|
|
65
|
+
};
|
|
66
|
+
const TargetSettingFeatureFlag = {
|
|
67
|
+
SpeedTargetSettingSupported: bit(0),
|
|
68
|
+
InclinationTargetSettingSupported: bit(1),
|
|
69
|
+
ResistanceTargetSettingSupported: bit(2),
|
|
70
|
+
PowerTargetSettingSupported: bit(3),
|
|
71
|
+
HeartRateTargetSettingSupported: bit(4),
|
|
72
|
+
TargetedExpendedEnergyConfigurationSupported: bit(5),
|
|
73
|
+
TargetedStepNumberConfigurationSupported: bit(6),
|
|
74
|
+
TargetedStrideNumberConfigurationSupported: bit(7),
|
|
75
|
+
TargetedDistanceConfigurationSupported: bit(8),
|
|
76
|
+
TargetedTrainingTimeConfigurationSupported: bit(9),
|
|
77
|
+
TargetedTimeInTwoHeartRateZonesConfigurationSupported: bit(10),
|
|
78
|
+
TargetedTimeInThreeHeartRateZonesConfigurationSupported: bit(11),
|
|
79
|
+
TargetedTimeInFiveHeartRateZonesConfigurationSupported: bit(12),
|
|
80
|
+
IndoorBikeSimulationParametersSupported: bit(13),
|
|
81
|
+
WheelCircumferenceConfigurationSupported: bit(14),
|
|
82
|
+
SpinDownControlSupported: bit(15),
|
|
83
|
+
TargetedCadenceConfigurationSupported: bit(16)
|
|
84
|
+
};
|
|
85
|
+
class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
86
|
+
constructor(props) {
|
|
87
|
+
super(props);
|
|
88
|
+
this.features = undefined;
|
|
89
|
+
this.hasControl = false;
|
|
90
|
+
this.isCheckingControl = false;
|
|
91
|
+
this.isCPSubscribed = false;
|
|
92
|
+
this.crr = 0.0033;
|
|
93
|
+
this.cw = 0.6;
|
|
94
|
+
this.windSpeed = 0;
|
|
95
|
+
this.wheelSize = 2100;
|
|
96
|
+
this.data = {};
|
|
97
|
+
this.services = BleFitnessMachineDevice.services;
|
|
98
|
+
}
|
|
99
|
+
isMatching(characteristics) {
|
|
100
|
+
if (!characteristics)
|
|
101
|
+
return false;
|
|
102
|
+
const hasStatus = characteristics.find(c => c === consts_1.FTMS_STATUS) !== undefined;
|
|
103
|
+
const hasCP = characteristics.find(c => c === consts_1.FTMS_CP) !== undefined;
|
|
104
|
+
const hasIndoorBike = characteristics.find(c => c === consts_1.INDOOR_BIKE_DATA) !== undefined;
|
|
105
|
+
return hasStatus && hasCP && hasIndoorBike;
|
|
106
|
+
}
|
|
107
|
+
subscribeWriteResponse(cuuid) {
|
|
108
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
this.logEvent({ message: 'subscribe to CP response', characteristics: cuuid });
|
|
110
|
+
const connector = this.ble.getConnector(this.peripheral);
|
|
111
|
+
const isAlreadySubscribed = connector.isSubscribed(cuuid);
|
|
112
|
+
if (!isAlreadySubscribed) {
|
|
113
|
+
connector.removeAllListeners(cuuid);
|
|
114
|
+
let prev = undefined;
|
|
115
|
+
let prevTS = undefined;
|
|
116
|
+
connector.on(cuuid, (uuid, data) => {
|
|
117
|
+
const message = data.toString('hex');
|
|
118
|
+
if (prevTS && prev && message === prev && Date.now() - prevTS < 500) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
prevTS = Date.now();
|
|
122
|
+
prev = message;
|
|
123
|
+
this.onData(uuid, data);
|
|
124
|
+
});
|
|
125
|
+
yield connector.subscribe(cuuid);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
subscribeAll(conn) {
|
|
130
|
+
return new Promise(resolve => {
|
|
131
|
+
const characteristics = [consts_1.INDOOR_BIKE_DATA, consts_1.FTMS_STATUS, consts_1.FTMS_CP];
|
|
132
|
+
const timeout = Date.now() + 5500;
|
|
133
|
+
const iv = setInterval(() => {
|
|
134
|
+
const subscriptionStatus = characteristics.map(c => this.subscribedCharacteristics.find(s => s === c) !== undefined);
|
|
135
|
+
const done = subscriptionStatus.filter(s => s === true).length === characteristics.length;
|
|
136
|
+
if (done || Date.now() > timeout) {
|
|
137
|
+
clearInterval(iv);
|
|
138
|
+
resolve();
|
|
139
|
+
}
|
|
140
|
+
}, 100);
|
|
141
|
+
try {
|
|
142
|
+
const connector = conn || this.ble.getConnector(this.peripheral);
|
|
143
|
+
for (let i = 0; i < characteristics.length; i++) {
|
|
144
|
+
const c = characteristics[i];
|
|
145
|
+
const isAlreadySubscribed = connector.isSubscribed(c);
|
|
146
|
+
if (!isAlreadySubscribed) {
|
|
147
|
+
connector.removeAllListeners(c);
|
|
148
|
+
connector.on(c, (uuid, data) => {
|
|
149
|
+
this.onData(uuid, data);
|
|
150
|
+
});
|
|
151
|
+
connector.subscribe(c);
|
|
152
|
+
this.subscribedCharacteristics.push(c);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.logEvent({ message: 'Error', fn: 'subscribeAll()', error: err.message, stack: err.stack });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
init() {
|
|
162
|
+
const _super = Object.create(null, {
|
|
163
|
+
initDevice: { get: () => super.initDevice }
|
|
164
|
+
});
|
|
165
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
166
|
+
try {
|
|
167
|
+
yield _super.initDevice.call(this);
|
|
168
|
+
yield this.getFitnessMachineFeatures();
|
|
169
|
+
this.logEvent({ message: 'device info', deviceInfo: this.deviceInfo, features: this.features });
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
this.logEvent({ message: 'error', fn: 'BleFitnessMachineDevice.init()', error: err.message || err, stack: err.stack });
|
|
173
|
+
return Promise.resolve(false);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
onDisconnect() {
|
|
178
|
+
super.onDisconnect();
|
|
179
|
+
this.hasControl = false;
|
|
180
|
+
}
|
|
181
|
+
getProfile() {
|
|
182
|
+
return 'Smart Trainer';
|
|
183
|
+
}
|
|
184
|
+
getServiceUUids() {
|
|
185
|
+
return BleFitnessMachineDevice.services;
|
|
186
|
+
}
|
|
187
|
+
isBike() {
|
|
188
|
+
return this.features === undefined ||
|
|
189
|
+
((this.features.targetSettings & TargetSettingFeatureFlag.IndoorBikeSimulationParametersSupported) !== 0);
|
|
190
|
+
}
|
|
191
|
+
isPower() {
|
|
192
|
+
if (this.hasService('1818'))
|
|
193
|
+
return true;
|
|
194
|
+
if (this.features === undefined)
|
|
195
|
+
return false;
|
|
196
|
+
const { fitnessMachine } = this.features;
|
|
197
|
+
if (fitnessMachine & FitnessMachineFeatureFlag.PowerMeasurementSupported)
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
isHrm() {
|
|
201
|
+
return this.hasService('180d') || (this.features && (this.features.fitnessMachine & FitnessMachineFeatureFlag.HeartRateMeasurementSupported) !== 0);
|
|
202
|
+
}
|
|
203
|
+
parseHrm(_data) {
|
|
204
|
+
const data = Buffer.from(_data);
|
|
205
|
+
try {
|
|
206
|
+
const flags = data.readUInt8(0);
|
|
207
|
+
if (flags % 1 === 0) {
|
|
208
|
+
this.data.heartrate = data.readUInt8(1);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.data.heartrate = data.readUInt16LE(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
this.logEvent({ message: 'error', fn: 'parseHrm()', error: err.message | err, stack: err.stack });
|
|
216
|
+
}
|
|
217
|
+
return Object.assign(Object.assign({}, this.data), { raw: `2a37:${data.toString('hex')}` });
|
|
218
|
+
}
|
|
219
|
+
setCrr(crr) { this.crr = crr; }
|
|
220
|
+
getCrr() { return this.crr; }
|
|
221
|
+
setCw(cw) { this.cw = cw; }
|
|
222
|
+
getCw() { return this.cw; }
|
|
223
|
+
setWindSpeed(windSpeed) { this.windSpeed = windSpeed; }
|
|
224
|
+
getWindSpeed() { return this.windSpeed; }
|
|
225
|
+
parseIndoorBikeData(_data) {
|
|
226
|
+
const data = Buffer.from(_data);
|
|
227
|
+
try {
|
|
228
|
+
const flags = data.readUInt16LE(0);
|
|
229
|
+
let offset = 2;
|
|
230
|
+
if ((flags & IndoorBikeDataFlag.MoreData) === 0) {
|
|
231
|
+
this.data.speed = data.readUInt16LE(offset) / 100;
|
|
232
|
+
offset += 2;
|
|
233
|
+
}
|
|
234
|
+
if (flags & IndoorBikeDataFlag.AverageSpeedPresent) {
|
|
235
|
+
this.data.averageSpeed = data.readUInt16LE(offset) / 100;
|
|
236
|
+
offset += 2;
|
|
237
|
+
}
|
|
238
|
+
if (flags & IndoorBikeDataFlag.InstantaneousCadence) {
|
|
239
|
+
this.data.cadence = data.readUInt16LE(offset) / 2;
|
|
240
|
+
offset += 2;
|
|
241
|
+
}
|
|
242
|
+
if (flags & IndoorBikeDataFlag.AverageCadencePresent) {
|
|
243
|
+
this.data.averageCadence = data.readUInt16LE(offset) / 2;
|
|
244
|
+
offset += 2;
|
|
245
|
+
}
|
|
246
|
+
if (flags & IndoorBikeDataFlag.TotalDistancePresent) {
|
|
247
|
+
const dvLow = data.readUInt8(offset);
|
|
248
|
+
offset += 1;
|
|
249
|
+
const dvHigh = data.readUInt16LE(offset);
|
|
250
|
+
offset += 2;
|
|
251
|
+
this.data.totalDistance = (dvHigh << 8) + dvLow;
|
|
252
|
+
}
|
|
253
|
+
if (flags & IndoorBikeDataFlag.ResistanceLevelPresent) {
|
|
254
|
+
this.data.resistanceLevel = data.readInt16LE(offset);
|
|
255
|
+
offset += 2;
|
|
256
|
+
}
|
|
257
|
+
if (flags & IndoorBikeDataFlag.InstantaneousPowerPresent) {
|
|
258
|
+
this.data.instantaneousPower = data.readInt16LE(offset);
|
|
259
|
+
offset += 2;
|
|
260
|
+
}
|
|
261
|
+
if (flags & IndoorBikeDataFlag.AveragePowerPresent) {
|
|
262
|
+
this.data.averagePower = data.readInt16LE(offset);
|
|
263
|
+
offset += 2;
|
|
264
|
+
}
|
|
265
|
+
if (flags & IndoorBikeDataFlag.ExpendedEnergyPresent) {
|
|
266
|
+
this.data.totalEnergy = data.readUInt16LE(offset);
|
|
267
|
+
offset += 2;
|
|
268
|
+
this.data.energyPerHour = data.readUInt16LE(offset);
|
|
269
|
+
offset += 2;
|
|
270
|
+
this.data.energyPerMinute = data.readUInt8(offset);
|
|
271
|
+
offset += 1;
|
|
272
|
+
}
|
|
273
|
+
if (flags & IndoorBikeDataFlag.HeartRatePresent) {
|
|
274
|
+
this.data.heartrate = data.readUInt8(offset);
|
|
275
|
+
offset += 1;
|
|
276
|
+
}
|
|
277
|
+
if (flags & IndoorBikeDataFlag.MetabolicEquivalentPresent) {
|
|
278
|
+
this.data.metabolicEquivalent = data.readUInt8(offset) / 10;
|
|
279
|
+
offset += 2;
|
|
280
|
+
}
|
|
281
|
+
if (flags & IndoorBikeDataFlag.ElapsedTimePresent) {
|
|
282
|
+
this.data.time = data.readUInt16LE(offset);
|
|
283
|
+
offset += 2;
|
|
284
|
+
}
|
|
285
|
+
if (flags & IndoorBikeDataFlag.RemainingTimePresent) {
|
|
286
|
+
this.data.remainingTime = data.readUInt16LE(offset);
|
|
287
|
+
offset += 2;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
this.logEvent({ message: 'error', fn: 'parseIndoorBikeData()', error: err.message | err, stack: err.stack });
|
|
292
|
+
}
|
|
293
|
+
return Object.assign(Object.assign({}, this.data), { raw: `2ad2:${data.toString('hex')}` });
|
|
294
|
+
}
|
|
295
|
+
parseFitnessMachineStatus(_data) {
|
|
296
|
+
const data = Buffer.from(_data);
|
|
297
|
+
try {
|
|
298
|
+
const OpCode = data.readUInt8(0);
|
|
299
|
+
switch (OpCode) {
|
|
300
|
+
case 8:
|
|
301
|
+
this.data.targetPower = data.readInt16LE(1);
|
|
302
|
+
break;
|
|
303
|
+
case 6:
|
|
304
|
+
this.data.targetInclination = data.readInt16LE(1) / 10;
|
|
305
|
+
break;
|
|
306
|
+
case 4:
|
|
307
|
+
this.data.status = "STARTED";
|
|
308
|
+
break;
|
|
309
|
+
case 3:
|
|
310
|
+
case 2:
|
|
311
|
+
this.data.status = "STOPPED";
|
|
312
|
+
break;
|
|
313
|
+
case 20:
|
|
314
|
+
const spinDownStatus = data.readUInt8(1);
|
|
315
|
+
switch (spinDownStatus) {
|
|
316
|
+
case 1:
|
|
317
|
+
this.data.status = "SPIN DOWN REQUESTED";
|
|
318
|
+
break;
|
|
319
|
+
case 2:
|
|
320
|
+
this.data.status = "SPIN DOWN SUCCESS";
|
|
321
|
+
break;
|
|
322
|
+
case 3:
|
|
323
|
+
this.data.status = "SPIN DOWN ERROR";
|
|
324
|
+
break;
|
|
325
|
+
case 4:
|
|
326
|
+
this.data.status = "STOP PEDALING";
|
|
327
|
+
break;
|
|
328
|
+
default: break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
this.logEvent({ message: 'error', fn: 'parseFitnessMachineStatus()', error: err.message | err, stack: err.stack });
|
|
334
|
+
}
|
|
335
|
+
return Object.assign(Object.assign({}, this.data), { raw: `2ada:${data.toString('hex')}` });
|
|
336
|
+
}
|
|
337
|
+
getFitnessMachineFeatures() {
|
|
338
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
339
|
+
if (this.features)
|
|
340
|
+
return this.features;
|
|
341
|
+
try {
|
|
342
|
+
const data = yield this.read('2acc');
|
|
343
|
+
const buffer = data ? Buffer.from(data) : undefined;
|
|
344
|
+
if (buffer) {
|
|
345
|
+
const fitnessMachine = buffer.readUInt32LE(0);
|
|
346
|
+
const targetSettings = buffer.readUInt32LE(4);
|
|
347
|
+
this.features = { fitnessMachine, targetSettings };
|
|
348
|
+
this.logEvent({ message: 'supported Features: ', fatures: this.features });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
this.logEvent({ message: 'could not read FitnessMachineFeatures', error: err.message, stack: err.stack });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
onData(characteristic, data) {
|
|
357
|
+
const hasData = super.onData(characteristic, data);
|
|
358
|
+
if (!hasData)
|
|
359
|
+
return false;
|
|
360
|
+
const uuid = characteristic.toLocaleLowerCase();
|
|
361
|
+
let res = undefined;
|
|
362
|
+
switch (uuid) {
|
|
363
|
+
case consts_1.INDOOR_BIKE_DATA:
|
|
364
|
+
res = this.parseIndoorBikeData(data);
|
|
365
|
+
break;
|
|
366
|
+
case '2a37':
|
|
367
|
+
res = this.parseHrm(data);
|
|
368
|
+
break;
|
|
369
|
+
case consts_1.FTMS_STATUS:
|
|
370
|
+
res = this.parseFitnessMachineStatus(data);
|
|
371
|
+
break;
|
|
372
|
+
case '2a63':
|
|
373
|
+
case '2a5b':
|
|
374
|
+
case '347b0011-7635-408b-8918-8ff3949ce592':
|
|
375
|
+
break;
|
|
376
|
+
default:
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
if (res) {
|
|
380
|
+
this.emit('data', res);
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
writeFtmsMessage(requestedOpCode, data, props) {
|
|
386
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
387
|
+
try {
|
|
388
|
+
this.logEvent({ message: 'fmts:write', data: data.toString('hex') });
|
|
389
|
+
const res = yield this.write(consts_1.FTMS_CP, data, props);
|
|
390
|
+
const responseData = Buffer.from(res);
|
|
391
|
+
const opCode = responseData.readUInt8(0);
|
|
392
|
+
const request = responseData.readUInt8(1);
|
|
393
|
+
const result = responseData.readUInt8(2);
|
|
394
|
+
if (opCode !== 128 || request !== requestedOpCode)
|
|
395
|
+
throw new Error('Illegal response ');
|
|
396
|
+
this.logEvent({ message: 'fmts:write result', res, result });
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
this.logEvent({ message: 'fmts:write failed', opCode: requestedOpCode, reason: err.message });
|
|
401
|
+
return 4;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
requestControl() {
|
|
406
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
407
|
+
let to = undefined;
|
|
408
|
+
if (this.isCheckingControl) {
|
|
409
|
+
to = setTimeout(() => { }, 3500);
|
|
410
|
+
}
|
|
411
|
+
if (this.hasControl)
|
|
412
|
+
return true;
|
|
413
|
+
this.logEvent({ message: 'requestControl' });
|
|
414
|
+
this.isCheckingControl = true;
|
|
415
|
+
const data = Buffer.alloc(1);
|
|
416
|
+
data.writeUInt8(0, 0);
|
|
417
|
+
const res = yield this.writeFtmsMessage(0, data, { timeout: 5000 });
|
|
418
|
+
if (res === 1) {
|
|
419
|
+
this.hasControl = true;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
this.logEvent({ message: 'requestControl failed' });
|
|
423
|
+
}
|
|
424
|
+
this.isCheckingControl = false;
|
|
425
|
+
if (to)
|
|
426
|
+
clearTimeout(to);
|
|
427
|
+
return this.hasControl;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
setTargetPower(power) {
|
|
431
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
432
|
+
this.logEvent({ message: 'setTargetPower', power, skip: (this.data.targetPower !== undefined && this.data.targetPower === power) });
|
|
433
|
+
if (this.data.targetPower !== undefined && this.data.targetPower === power)
|
|
434
|
+
return true;
|
|
435
|
+
if (!this.hasControl)
|
|
436
|
+
return;
|
|
437
|
+
const hasControl = yield this.requestControl();
|
|
438
|
+
if (!hasControl) {
|
|
439
|
+
this.logEvent({ message: 'setTargetPower failed', reason: 'control is disabled' });
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
const data = Buffer.alloc(3);
|
|
443
|
+
data.writeUInt8(5, 0);
|
|
444
|
+
data.writeInt16LE(Math.round(power), 1);
|
|
445
|
+
const res = yield this.writeFtmsMessage(5, data);
|
|
446
|
+
return (res === 1);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
setSlope(slope) {
|
|
450
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
451
|
+
this.logEvent({ message: 'setSlope', slope });
|
|
452
|
+
const hasControl = yield this.requestControl();
|
|
453
|
+
if (!hasControl)
|
|
454
|
+
return;
|
|
455
|
+
const { windSpeed, crr, cw } = this;
|
|
456
|
+
return yield this.setIndoorBikeSimulation(windSpeed, slope, crr, cw);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
setTargetInclination(inclination) {
|
|
460
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
461
|
+
if (this.data.targetInclination !== undefined && this.data.targetInclination === inclination)
|
|
462
|
+
return true;
|
|
463
|
+
if (!this.hasControl)
|
|
464
|
+
return;
|
|
465
|
+
const hasControl = yield this.requestControl();
|
|
466
|
+
if (!hasControl) {
|
|
467
|
+
this.logEvent({ message: 'setTargetInclination failed', reason: 'control is disabled' });
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const data = Buffer.alloc(3);
|
|
471
|
+
data.writeUInt8(3, 0);
|
|
472
|
+
data.writeInt16LE(Math.round(inclination * 10), 1);
|
|
473
|
+
const res = yield this.writeFtmsMessage(3, data);
|
|
474
|
+
return (res === 1);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
setIndoorBikeSimulation(windSpeed, gradient, crr, cw) {
|
|
478
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
479
|
+
const hasControl = yield this.requestControl();
|
|
480
|
+
if (!hasControl) {
|
|
481
|
+
this.logEvent({ message: 'setIndoorBikeSimulation failed', reason: 'control is disabled' });
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const data = Buffer.alloc(7);
|
|
485
|
+
data.writeUInt8(17, 0);
|
|
486
|
+
data.writeInt16LE(Math.round(windSpeed * 1000), 1);
|
|
487
|
+
data.writeInt16LE(Math.round(gradient * 100), 3);
|
|
488
|
+
data.writeUInt8(Math.round(crr * 10000), 5);
|
|
489
|
+
data.writeUInt8(Math.round(cw * 100), 6);
|
|
490
|
+
const res = yield this.writeFtmsMessage(17, data);
|
|
491
|
+
return (res === 1);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
startRequest() {
|
|
495
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
496
|
+
const hasControl = yield this.requestControl();
|
|
497
|
+
if (!hasControl) {
|
|
498
|
+
this.logEvent({ message: 'startRequest failed', reason: 'control is disabled' });
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const data = Buffer.alloc(1);
|
|
502
|
+
data.writeUInt8(7, 0);
|
|
503
|
+
const res = yield this.writeFtmsMessage(7, data);
|
|
504
|
+
return (res === 1);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
stopRequest() {
|
|
508
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
509
|
+
const hasControl = yield this.requestControl();
|
|
510
|
+
if (!hasControl) {
|
|
511
|
+
this.logEvent({ message: 'stopRequest failed', reason: 'control is disabled' });
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
const data = Buffer.alloc(2);
|
|
515
|
+
data.writeUInt8(8, 0);
|
|
516
|
+
data.writeUInt8(1, 1);
|
|
517
|
+
const res = yield this.writeFtmsMessage(8, data);
|
|
518
|
+
return (res === 1);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
PauseRequest() {
|
|
522
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
523
|
+
const hasControl = yield this.requestControl();
|
|
524
|
+
if (!hasControl) {
|
|
525
|
+
this.logEvent({ message: 'PauseRequest failed', reason: 'control is disabled' });
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
const data = Buffer.alloc(2);
|
|
529
|
+
data.writeUInt8(8, 0);
|
|
530
|
+
data.writeUInt8(2, 1);
|
|
531
|
+
const res = yield this.writeFtmsMessage(8, data);
|
|
532
|
+
return (res === 1);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
reset() {
|
|
536
|
+
this.data = {};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
exports.default = BleFitnessMachineDevice;
|
|
540
|
+
BleFitnessMachineDevice.services = [consts_1.FTMS];
|
|
541
|
+
BleFitnessMachineDevice.characteristics = ['2acc', consts_1.INDOOR_BIKE_DATA, '2ad6', '2ad8', consts_1.FTMS_CP, consts_1.FTMS_STATUS];
|
|
542
|
+
BleFitnessMachineDevice.detectionPriority = 100;
|
|
543
|
+
ble_interface_1.default.register('BleFitnessMachineDevice', 'fm', BleFitnessMachineDevice, BleFitnessMachineDevice.services);
|
|
544
|
+
class FmAdapter extends device_1.default {
|
|
545
|
+
constructor(device, protocol) {
|
|
546
|
+
super(protocol);
|
|
547
|
+
this.ignore = false;
|
|
548
|
+
this.paused = false;
|
|
549
|
+
this.distanceInternal = 0;
|
|
550
|
+
this.device = device;
|
|
551
|
+
this.ble = protocol.ble;
|
|
552
|
+
this.cyclingMode = this.getDefaultCyclingMode();
|
|
553
|
+
this.logger = new gd_eventlog_1.EventLogger('BLE-FM');
|
|
554
|
+
if (this.device)
|
|
555
|
+
this.device.setLogger(this.logger);
|
|
556
|
+
}
|
|
557
|
+
isBike() { return this.device.isBike(); }
|
|
558
|
+
isHrm() { return this.device.isHrm(); }
|
|
559
|
+
isPower() { return this.device.isPower(); }
|
|
560
|
+
isSame(device) {
|
|
561
|
+
if (!(device instanceof FmAdapter))
|
|
562
|
+
return false;
|
|
563
|
+
const adapter = device;
|
|
564
|
+
return (adapter.getName() === this.getName() && adapter.getProfile() === this.getProfile());
|
|
565
|
+
}
|
|
566
|
+
getProfile() {
|
|
567
|
+
const profile = this.device ? this.device.getProfile() : undefined;
|
|
568
|
+
return profile || 'Smart Trainer';
|
|
569
|
+
}
|
|
570
|
+
getName() {
|
|
571
|
+
return `${this.device.name}`;
|
|
572
|
+
}
|
|
573
|
+
getDisplayName() {
|
|
574
|
+
return this.getName();
|
|
575
|
+
}
|
|
576
|
+
getSupportedCyclingModes() {
|
|
577
|
+
return [ble_st_mode_1.default, ble_erg_mode_1.default, power_meter_1.default];
|
|
578
|
+
}
|
|
579
|
+
setCyclingMode(mode, settings) {
|
|
580
|
+
let selectedMode;
|
|
581
|
+
if (typeof mode === 'string') {
|
|
582
|
+
const supported = this.getSupportedCyclingModes();
|
|
583
|
+
const CyclingModeClass = supported.find(M => { const m = new M(this); return m.getName() === mode; });
|
|
584
|
+
if (CyclingModeClass) {
|
|
585
|
+
this.cyclingMode = new CyclingModeClass(this, settings);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
selectedMode = this.getDefaultCyclingMode();
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
selectedMode = mode;
|
|
592
|
+
}
|
|
593
|
+
this.cyclingMode = selectedMode;
|
|
594
|
+
this.cyclingMode.setSettings(settings);
|
|
595
|
+
}
|
|
596
|
+
getCyclingMode() {
|
|
597
|
+
if (!this.cyclingMode)
|
|
598
|
+
this.cyclingMode = this.getDefaultCyclingMode();
|
|
599
|
+
return this.cyclingMode;
|
|
600
|
+
}
|
|
601
|
+
getDefaultCyclingMode() {
|
|
602
|
+
return new ble_st_mode_1.default(this);
|
|
603
|
+
}
|
|
604
|
+
getPort() {
|
|
605
|
+
return 'ble';
|
|
606
|
+
}
|
|
607
|
+
setIgnoreBike(ignore) {
|
|
608
|
+
this.ignore = ignore;
|
|
609
|
+
}
|
|
610
|
+
setIgnorePower(ignore) {
|
|
611
|
+
this.ignore = ignore;
|
|
612
|
+
}
|
|
613
|
+
onDeviceData(deviceData) {
|
|
614
|
+
if (this.prevDataTS && Date.now() - this.prevDataTS < 1000)
|
|
615
|
+
return;
|
|
616
|
+
this.prevDataTS = Date.now();
|
|
617
|
+
this.logger.logEvent({ message: 'onDeviceData', data: deviceData });
|
|
618
|
+
let incyclistData = this.mapData(deviceData);
|
|
619
|
+
incyclistData = this.getCyclingMode().updateData(incyclistData);
|
|
620
|
+
const data = this.transformData(incyclistData);
|
|
621
|
+
if (this.onDataFn && !this.ignore && !this.paused)
|
|
622
|
+
this.onDataFn(data);
|
|
623
|
+
}
|
|
624
|
+
mapData(deviceData) {
|
|
625
|
+
const data = {
|
|
626
|
+
isPedalling: false,
|
|
627
|
+
power: 0,
|
|
628
|
+
pedalRpm: undefined,
|
|
629
|
+
speed: 0,
|
|
630
|
+
heartrate: 0,
|
|
631
|
+
distanceInternal: 0,
|
|
632
|
+
slope: undefined,
|
|
633
|
+
time: undefined
|
|
634
|
+
};
|
|
635
|
+
data.power = (deviceData.instantaneousPower !== undefined ? deviceData.instantaneousPower : data.power);
|
|
636
|
+
data.pedalRpm = (deviceData.cadence !== undefined ? deviceData.cadence : data.pedalRpm);
|
|
637
|
+
data.time = (deviceData.time !== undefined ? deviceData.time : data.time);
|
|
638
|
+
data.isPedalling = data.pedalRpm > 0 || (data.pedalRpm === undefined && data.power > 0);
|
|
639
|
+
return data;
|
|
640
|
+
}
|
|
641
|
+
transformData(bikeData) {
|
|
642
|
+
if (this.ignore) {
|
|
643
|
+
return {};
|
|
644
|
+
}
|
|
645
|
+
if (bikeData === undefined)
|
|
646
|
+
return;
|
|
647
|
+
let distance = 0;
|
|
648
|
+
if (this.distanceInternal !== undefined && bikeData.distanceInternal !== undefined) {
|
|
649
|
+
distance = Math.round(bikeData.distanceInternal - this.distanceInternal);
|
|
650
|
+
}
|
|
651
|
+
if (bikeData.distanceInternal !== undefined)
|
|
652
|
+
this.distanceInternal = bikeData.distanceInternal;
|
|
653
|
+
let data = {
|
|
654
|
+
speed: bikeData.speed,
|
|
655
|
+
slope: bikeData.slope,
|
|
656
|
+
power: bikeData.power !== undefined ? Math.round(bikeData.power) : undefined,
|
|
657
|
+
cadence: bikeData.pedalRpm !== undefined ? Math.round(bikeData.pedalRpm) : undefined,
|
|
658
|
+
distance,
|
|
659
|
+
timestamp: Date.now()
|
|
660
|
+
};
|
|
661
|
+
return data;
|
|
662
|
+
}
|
|
663
|
+
start(props) {
|
|
664
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
665
|
+
this.logger.logEvent({ message: 'ftms: start requested', profile: this.getProfile(), props });
|
|
666
|
+
const { restart } = props || {};
|
|
667
|
+
if (!restart && this.ble.isScanning())
|
|
668
|
+
yield this.ble.stopScan();
|
|
669
|
+
try {
|
|
670
|
+
let bleDevice;
|
|
671
|
+
if (!this.device || !restart) {
|
|
672
|
+
bleDevice = (yield this.ble.connectDevice(this.device));
|
|
673
|
+
this.device = bleDevice;
|
|
674
|
+
}
|
|
675
|
+
else
|
|
676
|
+
bleDevice = this.device;
|
|
677
|
+
if (bleDevice) {
|
|
678
|
+
bleDevice.setLogger(this.logger);
|
|
679
|
+
const mode = this.getCyclingMode();
|
|
680
|
+
if (mode && mode.getSetting('bikeType')) {
|
|
681
|
+
const bikeType = mode.getSetting('bikeType').toLowerCase();
|
|
682
|
+
this.device.setCrr(cRR);
|
|
683
|
+
switch (bikeType) {
|
|
684
|
+
case 'race':
|
|
685
|
+
this.device.setCw(cwABike.race);
|
|
686
|
+
break;
|
|
687
|
+
case 'triathlon':
|
|
688
|
+
this.device.setCw(cwABike.triathlon);
|
|
689
|
+
break;
|
|
690
|
+
case 'mountain':
|
|
691
|
+
this.device.setCw(cwABike.mountain);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
let hasControl = yield this.device.requestControl();
|
|
696
|
+
if (!hasControl) {
|
|
697
|
+
let retry = 1;
|
|
698
|
+
while (!hasControl && retry < 3) {
|
|
699
|
+
yield sleep(1000);
|
|
700
|
+
hasControl = yield this.device.requestControl();
|
|
701
|
+
retry++;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!hasControl)
|
|
705
|
+
throw new Error('could not establish control');
|
|
706
|
+
const startRequest = this.getCyclingMode().getBikeInitRequest();
|
|
707
|
+
yield this.sendUpdate(startRequest);
|
|
708
|
+
bleDevice.on('data', (data) => {
|
|
709
|
+
this.onDeviceData(data);
|
|
710
|
+
});
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
|
|
716
|
+
throw new Error(`could not start device, reason:${err.message}`);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
stop() {
|
|
721
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
722
|
+
this.logger.logEvent({ message: 'stop requested', profile: this.getProfile() });
|
|
723
|
+
this.distanceInternal = 0;
|
|
724
|
+
this.device.reset();
|
|
725
|
+
return this.device.disconnect();
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
sendUpdate(request) {
|
|
729
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
730
|
+
if (this.paused || !this.device)
|
|
731
|
+
return;
|
|
732
|
+
const update = this.getCyclingMode().sendBikeUpdate(request);
|
|
733
|
+
this.logger.logEvent({ message: 'send bike update requested', profile: this.getProfile(), update, request });
|
|
734
|
+
if (update.slope !== undefined) {
|
|
735
|
+
yield this.device.setSlope(update.slope);
|
|
736
|
+
}
|
|
737
|
+
if (update.targetPower !== undefined) {
|
|
738
|
+
yield this.device.setTargetPower(update.targetPower);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
pause() { this.paused = true; return Promise.resolve(true); }
|
|
743
|
+
resume() { this.paused = false; return Promise.resolve(true); }
|
|
744
|
+
}
|
|
745
|
+
exports.FmAdapter = FmAdapter;
|