incyclist-devices 1.4.97 → 1.4.100

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.
Files changed (128) hide show
  1. package/LICENSE +0 -0
  2. package/lib/CyclingMode.d.ts +76 -76
  3. package/lib/CyclingMode.js +79 -79
  4. package/lib/Device.d.ts +92 -92
  5. package/lib/Device.js +71 -71
  6. package/lib/DeviceProtocol.d.ts +74 -74
  7. package/lib/DeviceProtocol.js +41 -41
  8. package/lib/DeviceRegistry.d.ts +8 -8
  9. package/lib/DeviceRegistry.js +33 -33
  10. package/lib/DeviceSupport.d.ts +34 -34
  11. package/lib/DeviceSupport.js +78 -78
  12. package/lib/ant/AntAdapter.d.ts +50 -50
  13. package/lib/ant/AntAdapter.js +109 -109
  14. package/lib/ant/AntScanner.d.ts +60 -60
  15. package/lib/ant/AntScanner.js +651 -651
  16. package/lib/ant/antfe/AntFEAdapter.d.ts +83 -78
  17. package/lib/ant/antfe/AntFEAdapter.js +652 -611
  18. package/lib/ant/antfe/ant-fe-adv-st-mode.d.ts +9 -9
  19. package/lib/ant/antfe/ant-fe-adv-st-mode.js +51 -51
  20. package/lib/ant/antfe/ant-fe-erg-mode.d.ts +6 -6
  21. package/lib/ant/antfe/ant-fe-erg-mode.js +14 -14
  22. package/lib/ant/antfe/ant-fe-st-mode.d.ts +5 -5
  23. package/lib/ant/antfe/ant-fe-st-mode.js +13 -13
  24. package/lib/ant/anthrm/AntHrmAdapter.d.ts +16 -16
  25. package/lib/ant/anthrm/AntHrmAdapter.js +130 -130
  26. package/lib/ant/antpwr/pwr-adapter.d.ts +49 -49
  27. package/lib/ant/antpwr/pwr-adapter.js +251 -251
  28. package/lib/ant/utils.d.ts +1 -1
  29. package/lib/ant/utils.js +23 -23
  30. package/lib/ble/ble-device.d.ts +63 -63
  31. package/lib/ble/ble-device.js +442 -442
  32. package/lib/ble/ble-erg-mode.d.ts +18 -18
  33. package/lib/ble/ble-erg-mode.js +132 -127
  34. package/lib/ble/ble-interface.d.ts +100 -99
  35. package/lib/ble/ble-interface.js +717 -712
  36. package/lib/ble/ble-peripheral.d.ts +36 -36
  37. package/lib/ble/ble-peripheral.js +200 -200
  38. package/lib/ble/ble-st-mode.d.ts +15 -15
  39. package/lib/ble/ble-st-mode.js +102 -102
  40. package/lib/ble/ble.d.ts +129 -129
  41. package/lib/ble/ble.js +86 -86
  42. package/lib/ble/consts.d.ts +14 -14
  43. package/lib/ble/consts.js +17 -17
  44. package/lib/ble/fm.d.ts +125 -125
  45. package/lib/ble/fm.js +739 -739
  46. package/lib/ble/hrm.d.ts +48 -48
  47. package/lib/ble/hrm.js +134 -134
  48. package/lib/ble/incyclist-protocol.d.ts +31 -31
  49. package/lib/ble/incyclist-protocol.js +147 -147
  50. package/lib/ble/pwr.d.ts +89 -89
  51. package/lib/ble/pwr.js +321 -321
  52. package/lib/ble/tacx.d.ts +90 -90
  53. package/lib/ble/tacx.js +731 -730
  54. package/lib/ble/wahoo-kickr.d.ts +98 -98
  55. package/lib/ble/wahoo-kickr.js +496 -496
  56. package/lib/calculations.d.ts +13 -13
  57. package/lib/calculations.js +150 -150
  58. package/lib/daum/DaumAdapter.d.ts +66 -66
  59. package/lib/daum/DaumAdapter.js +396 -396
  60. package/lib/daum/DaumPowerMeterCyclingMode.d.ts +8 -8
  61. package/lib/daum/DaumPowerMeterCyclingMode.js +21 -21
  62. package/lib/daum/ERGCyclingMode.d.ts +26 -26
  63. package/lib/daum/ERGCyclingMode.js +201 -201
  64. package/lib/daum/SmartTrainerCyclingMode.d.ts +41 -41
  65. package/lib/daum/SmartTrainerCyclingMode.js +344 -344
  66. package/lib/daum/classic/DaumClassicAdapter.d.ts +18 -18
  67. package/lib/daum/classic/DaumClassicAdapter.js +146 -146
  68. package/lib/daum/classic/DaumClassicCyclingMode.d.ts +13 -13
  69. package/lib/daum/classic/DaumClassicCyclingMode.js +97 -97
  70. package/lib/daum/classic/DaumClassicProtocol.d.ts +27 -27
  71. package/lib/daum/classic/DaumClassicProtocol.js +185 -185
  72. package/lib/daum/classic/bike.d.ts +64 -64
  73. package/lib/daum/classic/bike.js +456 -456
  74. package/lib/daum/classic/utils.d.ts +13 -13
  75. package/lib/daum/classic/utils.js +143 -143
  76. package/lib/daum/constants.d.ts +19 -19
  77. package/lib/daum/constants.js +22 -22
  78. package/lib/daum/premium/DaumClassicCyclingMode.d.ts +14 -14
  79. package/lib/daum/premium/DaumClassicCyclingMode.js +86 -86
  80. package/lib/daum/premium/DaumPremiumAdapter.d.ts +12 -12
  81. package/lib/daum/premium/DaumPremiumAdapter.js +131 -131
  82. package/lib/daum/premium/DaumPremiumProtocol.d.ts +32 -32
  83. package/lib/daum/premium/DaumPremiumProtocol.js +207 -207
  84. package/lib/daum/premium/bike.d.ts +123 -123
  85. package/lib/daum/premium/bike.js +894 -894
  86. package/lib/daum/premium/tcpserial.d.ts +33 -33
  87. package/lib/daum/premium/tcpserial.js +123 -123
  88. package/lib/daum/premium/utils.d.ts +62 -62
  89. package/lib/daum/premium/utils.js +376 -376
  90. package/lib/kettler/comms.d.ts +59 -59
  91. package/lib/kettler/comms.js +242 -242
  92. package/lib/kettler/ergo-racer/ERGCyclingMode.d.ts +25 -25
  93. package/lib/kettler/ergo-racer/ERGCyclingMode.js +144 -145
  94. package/lib/kettler/ergo-racer/adapter.d.ts +101 -101
  95. package/lib/kettler/ergo-racer/adapter.js +639 -639
  96. package/lib/kettler/ergo-racer/protocol.d.ts +41 -41
  97. package/lib/kettler/ergo-racer/protocol.js +203 -203
  98. package/lib/modes/power-base.d.ts +20 -20
  99. package/lib/modes/power-base.js +70 -70
  100. package/lib/modes/power-meter.d.ts +20 -20
  101. package/lib/modes/power-meter.js +78 -78
  102. package/lib/modes/simulator.d.ts +29 -29
  103. package/lib/modes/simulator.js +140 -140
  104. package/lib/simulator/Simulator.d.ts +69 -69
  105. package/lib/simulator/Simulator.js +288 -288
  106. package/lib/types/command.d.ts +8 -8
  107. package/lib/types/command.js +2 -2
  108. package/lib/types/route.d.ts +24 -24
  109. package/lib/types/route.js +9 -9
  110. package/lib/types/user.d.ts +11 -11
  111. package/lib/types/user.js +9 -9
  112. package/lib/utils.d.ts +14 -14
  113. package/lib/utils.js +114 -114
  114. package/package.json +46 -46
  115. package/lib/ant/antfe/ant-fe-st-mode copy.d.ts +0 -7
  116. package/lib/ant/antfe/ant-fe-st-mode copy.js +0 -54
  117. package/lib/ant/antpwr/AntPWRAdapter.d.ts +0 -24
  118. package/lib/ant/antpwr/AntPWRAdapter.js +0 -252
  119. package/lib/daum/PowerMeterCyclingMode.d.ts +0 -18
  120. package/lib/daum/PowerMeterCyclingMode.js +0 -78
  121. package/lib/daum/classic/ERGCyclingMode.d.ts +0 -23
  122. package/lib/daum/classic/ERGCyclingMode.js +0 -171
  123. package/lib/daum/indoorbike.d.ts +0 -24
  124. package/lib/daum/indoorbike.js +0 -178
  125. package/lib/kettler/ergo-racer/modes/power-meter.d.ts +0 -18
  126. package/lib/kettler/ergo-racer/modes/power-meter.js +0 -86
  127. package/lib/simulator/simulator-mode.d.ts +0 -28
  128. package/lib/simulator/simulator-mode.js +0 -120
package/lib/ble/fm.js CHANGED
@@ -1,739 +1,739 @@
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.readInt8(offset);
255
- offset += 1;
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
- if (this.ble.isScanning())
667
- yield this.ble.stopScan();
668
- try {
669
- const bleDevice = yield this.ble.connectDevice(this.device);
670
- bleDevice.setLogger(this.logger);
671
- if (bleDevice) {
672
- this.device = bleDevice;
673
- const mode = this.getCyclingMode();
674
- if (mode && mode.getSetting('bikeType')) {
675
- const bikeType = mode.getSetting('bikeType').toLowerCase();
676
- this.device.setCrr(cRR);
677
- switch (bikeType) {
678
- case 'race':
679
- this.device.setCw(cwABike.race);
680
- break;
681
- case 'triathlon':
682
- this.device.setCw(cwABike.triathlon);
683
- break;
684
- case 'mountain':
685
- this.device.setCw(cwABike.mountain);
686
- break;
687
- }
688
- }
689
- let hasControl = yield this.device.requestControl();
690
- if (!hasControl) {
691
- let retry = 1;
692
- while (!hasControl && retry < 3) {
693
- yield sleep(1000);
694
- hasControl = yield this.device.requestControl();
695
- retry++;
696
- }
697
- }
698
- if (!hasControl)
699
- throw new Error('could not establish control');
700
- const startRequest = this.getCyclingMode().getBikeInitRequest();
701
- yield this.sendUpdate(startRequest);
702
- bleDevice.on('data', (data) => {
703
- this.onDeviceData(data);
704
- });
705
- return true;
706
- }
707
- }
708
- catch (err) {
709
- this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
710
- throw new Error(`could not start device, reason:${err.message}`);
711
- }
712
- });
713
- }
714
- stop() {
715
- return __awaiter(this, void 0, void 0, function* () {
716
- this.logger.logEvent({ message: 'stop requested', profile: this.getProfile() });
717
- this.distanceInternal = 0;
718
- this.device.reset();
719
- return this.device.disconnect();
720
- });
721
- }
722
- sendUpdate(request) {
723
- return __awaiter(this, void 0, void 0, function* () {
724
- if (this.paused || !this.device)
725
- return;
726
- const update = this.getCyclingMode().sendBikeUpdate(request);
727
- this.logger.logEvent({ message: 'send bike update requested', profile: this.getProfile(), update, request });
728
- if (update.slope !== undefined) {
729
- yield this.device.setSlope(update.slope);
730
- }
731
- if (update.targetPower !== undefined) {
732
- yield this.device.setTargetPower(update.targetPower);
733
- }
734
- });
735
- }
736
- pause() { this.paused = true; return Promise.resolve(true); }
737
- resume() { this.paused = false; return Promise.resolve(true); }
738
- }
739
- 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
+ if (this.ble.isScanning())
667
+ yield this.ble.stopScan();
668
+ try {
669
+ const bleDevice = yield this.ble.connectDevice(this.device);
670
+ bleDevice.setLogger(this.logger);
671
+ if (bleDevice) {
672
+ this.device = bleDevice;
673
+ const mode = this.getCyclingMode();
674
+ if (mode && mode.getSetting('bikeType')) {
675
+ const bikeType = mode.getSetting('bikeType').toLowerCase();
676
+ this.device.setCrr(cRR);
677
+ switch (bikeType) {
678
+ case 'race':
679
+ this.device.setCw(cwABike.race);
680
+ break;
681
+ case 'triathlon':
682
+ this.device.setCw(cwABike.triathlon);
683
+ break;
684
+ case 'mountain':
685
+ this.device.setCw(cwABike.mountain);
686
+ break;
687
+ }
688
+ }
689
+ let hasControl = yield this.device.requestControl();
690
+ if (!hasControl) {
691
+ let retry = 1;
692
+ while (!hasControl && retry < 3) {
693
+ yield sleep(1000);
694
+ hasControl = yield this.device.requestControl();
695
+ retry++;
696
+ }
697
+ }
698
+ if (!hasControl)
699
+ throw new Error('could not establish control');
700
+ const startRequest = this.getCyclingMode().getBikeInitRequest();
701
+ yield this.sendUpdate(startRequest);
702
+ bleDevice.on('data', (data) => {
703
+ this.onDeviceData(data);
704
+ });
705
+ return true;
706
+ }
707
+ }
708
+ catch (err) {
709
+ this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
710
+ throw new Error(`could not start device, reason:${err.message}`);
711
+ }
712
+ });
713
+ }
714
+ stop() {
715
+ return __awaiter(this, void 0, void 0, function* () {
716
+ this.logger.logEvent({ message: 'stop requested', profile: this.getProfile() });
717
+ this.distanceInternal = 0;
718
+ this.device.reset();
719
+ return this.device.disconnect();
720
+ });
721
+ }
722
+ sendUpdate(request) {
723
+ return __awaiter(this, void 0, void 0, function* () {
724
+ if (this.paused || !this.device)
725
+ return;
726
+ const update = this.getCyclingMode().sendBikeUpdate(request);
727
+ this.logger.logEvent({ message: 'send bike update requested', profile: this.getProfile(), update, request });
728
+ if (update.slope !== undefined) {
729
+ yield this.device.setSlope(update.slope);
730
+ }
731
+ if (update.targetPower !== undefined) {
732
+ yield this.device.setTargetPower(update.targetPower);
733
+ }
734
+ });
735
+ }
736
+ pause() { this.paused = true; return Promise.resolve(true); }
737
+ resume() { this.paused = false; return Promise.resolve(true); }
738
+ }
739
+ exports.FmAdapter = FmAdapter;