incyclist-devices 1.4.61 → 1.4.64

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