incyclist-devices 1.4.60 → 1.4.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -462,7 +462,7 @@ class BleInterface extends ble_1.BleInterfaceClass {
462
462
  }
463
463
  scan(props) {
464
464
  return __awaiter(this, void 0, void 0, function* () {
465
- const { timeout = DEFAULT_SCAN_TIMEOUT, deviceTypes = [], requested } = props;
465
+ const { timeout = DEFAULT_SCAN_TIMEOUT, deviceTypes = [], requested, isBackgroundScan } = props;
466
466
  let profile;
467
467
  if (requested)
468
468
  profile = requested instanceof ble_1.BleDeviceClass ?
@@ -479,7 +479,7 @@ class BleInterface extends ble_1.BleInterfaceClass {
479
479
  }
480
480
  const peripheralsProcessed = [];
481
481
  const devicesProcessed = [];
482
- this.logEvent({ message: 'scan()', props, scanState: this.scanState,
482
+ this.logEvent({ message: 'scan()', props: { timeout, isBackgroundScan }, scanState: this.scanState,
483
483
  peripheralCache: this.peripheralCache.map(i => ({ address: i.address, ts: i.ts, name: i.peripheral ? i.peripheral.advertisement.localName : '' })),
484
484
  deviceCache: this.devices.map(i => ({ address: i.device.address, profile: i.device.getProfile(), isConnected: i.isConnected }))
485
485
  });
@@ -101,9 +101,10 @@ class BlePeripheralConnector {
101
101
  c.on('data', (data, _isNotification) => {
102
102
  this.onData((0, ble_1.uuid)(c.uuid), data);
103
103
  });
104
- if (callback)
104
+ if (callback) {
105
105
  this.on((0, ble_1.uuid)(c.uuid), callback);
106
- this.logEvent({ message: 'subscribe', peripheral: this.peripheral.address, characteristic: c.uuid });
106
+ }
107
+ this.logEvent({ message: 'subscribe', peripheral: this.peripheral.address, characteristic: c.uuid, uuid: (0, ble_1.uuid)(c.uuid) });
107
108
  if (this.state.subscribed.find(uuid => uuid === c.uuid) === undefined) {
108
109
  try {
109
110
  yield this.subscribe(c.uuid);
@@ -117,7 +118,7 @@ class BlePeripheralConnector {
117
118
  }
118
119
  }
119
120
  catch (err) {
120
- console.log('~~~ error', err);
121
+ this.logEvent({ message: 'error', fn: 'subscribeAll()', error: err.message || err, stack: err.stack });
121
122
  }
122
123
  }
123
124
  this.state.isSubscribing = false;
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;
package/lib/ble/fm.js CHANGED
@@ -148,7 +148,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
148
148
  }
149
149
  catch (err) {
150
150
  }
151
- return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
151
+ return Object.assign(Object.assign({}, this.data), { raw: `2a37:${data.toString('hex')}` });
152
152
  }
153
153
  setCrr(crr) { this.crr = crr; }
154
154
  getCrr() { return this.crr; }
@@ -215,7 +215,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
215
215
  this.data.remainingTime = data.readUInt16LE(offset);
216
216
  offset += 2;
217
217
  }
218
- return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
218
+ return Object.assign(Object.assign({}, this.data), { raw: `2ad2:${data.toString('hex')}` });
219
219
  }
220
220
  parseFitnessMachineStatus(_data) {
221
221
  const data = Buffer.from(_data);
@@ -252,7 +252,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
252
252
  default: break;
253
253
  }
254
254
  }
255
- return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
255
+ return Object.assign(Object.assign({}, this.data), { raw: `2ada:${data.toString('hex')}` });
256
256
  }
257
257
  getFitnessMachineFeatures() {
258
258
  return __awaiter(this, void 0, void 0, function* () {
@@ -286,6 +286,11 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
286
286
  case '2ada':
287
287
  res = this.parseFitnessMachineStatus(data);
288
288
  break;
289
+ case '2a63':
290
+ case '2a5b':
291
+ case '347b0011-7635-408b-8918-8ff3949ce592':
292
+ this.logger.logEvent({ message: 'onBleData', raw: `uuid:${data.toString('hex')}` });
293
+ break;
289
294
  default:
290
295
  break;
291
296
  }
@@ -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;
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-devices",
3
- "version": "1.4.60",
3
+ "version": "1.4.63",
4
4
  "dependencies": {
5
5
  "@serialport/parser-byte-length": "^9.0.1",
6
6
  "@serialport/parser-delimiter": "^9.0.1",