incyclist-devices 3.0.19 → 3.0.21

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 (39) hide show
  1. package/lib/cjs/base/consts.js +2 -1
  2. package/lib/cjs/ble/base/adapter.js +5 -8
  3. package/lib/cjs/ble/base/interface.js +20 -2
  4. package/lib/cjs/ble/base/peripheral.js +22 -5
  5. package/lib/cjs/ble/base/sensor.js +12 -1
  6. package/lib/cjs/ble/characteristics/csc/measurement.js +6 -2
  7. package/lib/cjs/ble/csc/adapter.js +29 -2
  8. package/lib/cjs/ble/csc/sensor.js +9 -2
  9. package/lib/cjs/ble/fm/adapter.js +3 -0
  10. package/lib/cjs/ble/wahoo/adapter.js +10 -0
  11. package/lib/cjs/ble/wahoo/sensor.js +35 -3
  12. package/lib/cjs/modes/speed.js +133 -0
  13. package/lib/cjs/types/sensor.js +2 -0
  14. package/lib/esm/base/consts.js +1 -0
  15. package/lib/esm/ble/base/adapter.js +5 -8
  16. package/lib/esm/ble/base/interface.js +20 -2
  17. package/lib/esm/ble/base/peripheral.js +22 -5
  18. package/lib/esm/ble/base/sensor.js +12 -1
  19. package/lib/esm/ble/characteristics/csc/measurement.js +6 -2
  20. package/lib/esm/ble/csc/adapter.js +29 -2
  21. package/lib/esm/ble/csc/sensor.js +9 -2
  22. package/lib/esm/ble/fm/adapter.js +3 -0
  23. package/lib/esm/ble/wahoo/adapter.js +10 -0
  24. package/lib/esm/ble/wahoo/sensor.js +35 -3
  25. package/lib/esm/modes/speed.js +127 -0
  26. package/lib/esm/types/sensor.js +1 -0
  27. package/lib/types/base/consts.d.ts +1 -0
  28. package/lib/types/ble/base/interface.d.ts +2 -1
  29. package/lib/types/ble/base/sensor.d.ts +2 -0
  30. package/lib/types/ble/characteristics/csc/measurement.d.ts +1 -0
  31. package/lib/types/ble/csc/adapter.d.ts +5 -1
  32. package/lib/types/ble/csc/sensor.d.ts +6 -2
  33. package/lib/types/ble/types.d.ts +1 -0
  34. package/lib/types/ble/wahoo/adapter.d.ts +3 -1
  35. package/lib/types/ble/wahoo/sensor.d.ts +5 -0
  36. package/lib/types/modes/speed.d.ts +43 -0
  37. package/lib/types/types/index.d.ts +1 -0
  38. package/lib/types/types/sensor.d.ts +4 -0
  39. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_PROPS = exports.DEFAULT_USER_WEIGHT = exports.DEFAULT_BIKE_WEIGHT = void 0;
3
+ exports.DEFAULT_PROPS = exports.DEFAULT_WHEEL_CIRCUMFERENCE = exports.DEFAULT_USER_WEIGHT = exports.DEFAULT_BIKE_WEIGHT = void 0;
4
4
  exports.DEFAULT_BIKE_WEIGHT = 10;
5
5
  exports.DEFAULT_USER_WEIGHT = 75;
6
+ exports.DEFAULT_WHEEL_CIRCUMFERENCE = 2.118;
6
7
  exports.DEFAULT_PROPS = {
7
8
  userWeight: exports.DEFAULT_USER_WEIGHT,
8
9
  bikeWeight: exports.DEFAULT_BIKE_WEIGHT
@@ -359,17 +359,14 @@ class BleAdapter extends adpater_js_1.default {
359
359
  sensor.off('data', this.onDeviceDataHandler);
360
360
  let connected = false;
361
361
  if (sensor.isReconnectBusy()) {
362
- connected = await sensor.reconnectSensor(false);
363
- }
364
- if (connected) {
365
- sensor.on('data', this.onDeviceDataHandler);
366
- return true;
362
+ connected = await sensor.reconnectSensor(true);
363
+ if (connected) {
364
+ sensor.on('data', this.onDeviceDataHandler);
365
+ }
366
+ return connected;
367
367
  }
368
368
  await sensor.getPeripheral().disconnect();
369
369
  const success = await super.restart(pause);
370
- if (success) {
371
- sensor.on('data', this.onDeviceDataHandler);
372
- }
373
370
  return success;
374
371
  }
375
372
  async stop() {
@@ -405,7 +405,12 @@ class BleInterface extends node_events_1.EventEmitter {
405
405
  }
406
406
  const isWahoo = this.checkForWahooEnhancement(announcement);
407
407
  if (isWahoo) {
408
- this.processWahooAnnouncement(announcement);
408
+ this.processEnrichedAnnouncement(announcement);
409
+ return;
410
+ }
411
+ const isTacx = this.checkForTacxEnhancement(announcement);
412
+ if (isTacx) {
413
+ this.processEnrichedAnnouncement(announcement);
409
414
  return;
410
415
  }
411
416
  this.addService(announcement);
@@ -419,7 +424,7 @@ class BleInterface extends node_events_1.EventEmitter {
419
424
  }
420
425
  return false;
421
426
  }
422
- processWahooAnnouncement(announcement) {
427
+ processEnrichedAnnouncement(announcement) {
423
428
  if (this.isCompleting(announcement)) {
424
429
  return;
425
430
  }
@@ -429,6 +434,19 @@ class BleInterface extends node_events_1.EventEmitter {
429
434
  this.addService(announcement);
430
435
  });
431
436
  }
437
+ checkForTacxEnhancement(announcement) {
438
+ const name = announcement?.name?.toUpperCase();
439
+ if (name.startsWith('TACX')) {
440
+ const supported = announcement.serviceUUIDs.map(s => (0, utils_js_1.beautifyUUID)(s));
441
+ const tacxService = (0, utils_js_1.beautifyUUID)('6E40FEC1-B5A3-F393-E0A9-E50E24DCCA9E');
442
+ const cp = (0, utils_js_1.beautifyUUID)('1818');
443
+ const fe = (0, utils_js_1.beautifyUUID)('1826');
444
+ if (supported.includes(cp) && !supported.includes(fe) && !supported.includes(tacxService)) {
445
+ return true;
446
+ }
447
+ }
448
+ return false;
449
+ }
432
450
  buildAnnouncement(peripheral) {
433
451
  return {
434
452
  advertisement: peripheral.advertisement,
@@ -163,8 +163,9 @@ class BlePeripheral {
163
163
  async _discoverServices() {
164
164
  if (!this.getPeripheral())
165
165
  return [];
166
+ const { name, address } = this.getInfo();
166
167
  if (this.getPeripheral().discoverServicesAsync) {
167
- this.logEvent({ message: 'discover services', address: this.getPeripheral().address });
168
+ this.logEvent({ message: 'discover services', name, address });
168
169
  const peripheral = this.getPeripheral();
169
170
  let services = [];
170
171
  if (peripheral?.discoverServicesAsync) {
@@ -172,14 +173,14 @@ class BlePeripheral {
172
173
  .catch(() => []);
173
174
  }
174
175
  this.discoveredServiceUUIds = services.map(s => (0, utils_js_2.beautifyUUID)(s.uuid));
175
- this.logEvent({ message: 'discover services result', address: this.getPeripheral().address, services: this.discoveredServiceUUIds });
176
+ this.logEvent({ message: 'discover services result', name, address, services: this.discoveredServiceUUIds });
176
177
  return services.map(s => s.uuid);
177
178
  }
178
179
  else {
179
- this.logEvent({ message: 'discover services and characteristics', address: this.getPeripheral().address });
180
+ this.logEvent({ message: 'discover services and characteristics', name, address });
180
181
  const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], []);
181
182
  this.discoveredServiceUUIds = res.services.map(s => (0, utils_js_2.beautifyUUID)(s.uuid));
182
- this.logEvent({ message: 'discover services result', address: this.getPeripheral().address, services: this.discoveredServiceUUIds });
183
+ this.logEvent({ message: 'discover services result', name, address, services: this.discoveredServiceUUIds });
183
184
  return res.services.map(s => s.uuid);
184
185
  }
185
186
  }
@@ -193,10 +194,12 @@ class BlePeripheral {
193
194
  async _discoverCharacteristics(serviceUUID) {
194
195
  if (!this.getPeripheral())
195
196
  return [];
196
- this.logEvent({ message: 'discover services and characteristics', service: serviceUUID, address: this.getPeripheral().address });
197
+ const { name, address } = this.getInfo();
198
+ this.logEvent({ message: 'discover services and characteristics', name, address, service: serviceUUID });
197
199
  const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([serviceUUID], [])
198
200
  .catch(() => ({ services: [], characteristics: [] }));
199
201
  res.characteristics.forEach(c => this.characteristics[(0, utils_js_2.beautifyUUID)(c.uuid)] = c);
202
+ this.logEvent({ message: 'discover services and characteristics result', name, address, service: serviceUUID });
200
203
  return res.characteristics.map(c => {
201
204
  const { uuid, properties, name, _serviceUuid } = c;
202
205
  return { uuid, properties, name, _serviceUuid };
@@ -318,6 +321,15 @@ class BlePeripheral {
318
321
  if (!success)
319
322
  retry.push(c);
320
323
  }
324
+ else {
325
+ const uuid = (0, utils_js_2.beautifyUUID)(element);
326
+ if (c?.properties) {
327
+ this.logEvent({ message: 'cannot subscribe', uuid, reason: 'invalid type', properties: c.properties.join('|') });
328
+ }
329
+ else {
330
+ this.logEvent({ message: 'cannot subscribe', uuid, reason: 'not found' });
331
+ }
332
+ }
321
333
  }
322
334
  for (const element of retry) {
323
335
  const c = element;
@@ -332,12 +344,17 @@ class BlePeripheral {
332
344
  }
333
345
  async discoverAllCharacteristics() {
334
346
  try {
347
+ const { name, address } = this.getInfo();
348
+ this.logEvent({ message: 'discover all characteristics', name, address });
335
349
  const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], []);
336
350
  const found = [];
351
+ const uuids = [];
337
352
  res.characteristics.forEach(c => {
338
353
  this.characteristics[(0, utils_js_2.beautifyUUID)(c.uuid)] = c;
339
354
  found.push(c.uuid);
355
+ uuids.push((0, utils_js_2.beautifyUUID)(c.uuid));
340
356
  });
357
+ this.logEvent({ message: 'discover all characteristics result', name, address, uuids: uuids.join('|') });
341
358
  return found;
342
359
  }
343
360
  catch (err) {
@@ -12,6 +12,7 @@ class TBleSensor extends node_events_1.EventEmitter {
12
12
  stopRequested = false;
13
13
  subscribeSuccess = false;
14
14
  reconnectPromise;
15
+ connectPromise;
15
16
  onDataHandler;
16
17
  logEvent(event, ...args) {
17
18
  try {
@@ -76,7 +77,8 @@ class TBleSensor extends node_events_1.EventEmitter {
76
77
  this.logEvent({ message: 'no peripheral' });
77
78
  return false;
78
79
  }
79
- const connected = await this.peripheral.connect();
80
+ this.connectPromise = this.connectPromise ?? this.peripheral.connect();
81
+ const connected = await this.connectPromise;
80
82
  if (!connected)
81
83
  return false;
82
84
  if (!reconnect)
@@ -128,6 +130,9 @@ class TBleSensor extends node_events_1.EventEmitter {
128
130
  let connected = false;
129
131
  let subscribed = false;
130
132
  let success = false;
133
+ if (connectionLost) {
134
+ this.emit('connctionLost');
135
+ }
131
136
  await (0, utils_js_1.sleep)(500);
132
137
  do {
133
138
  try {
@@ -147,6 +152,9 @@ class TBleSensor extends node_events_1.EventEmitter {
147
152
  if (!this.stopRequested && !success)
148
153
  this.logEvent({ message: 'reconnect sensor retry', name, address });
149
154
  } while (!success || this.stopRequested);
155
+ if (connectionLost && success) {
156
+ this.emit('connctionRecovered');
157
+ }
150
158
  this.logEvent({ message: 'reconnect sensor completed', success, stopRequested: this.stopRequested });
151
159
  return success;
152
160
  }
@@ -156,6 +164,9 @@ class TBleSensor extends node_events_1.EventEmitter {
156
164
  isConnected() {
157
165
  return this.peripheral?.isConnected();
158
166
  }
167
+ isReady() {
168
+ return this.isConnected() && this.connectPromise === undefined && this.reconnectPromise === undefined;
169
+ }
159
170
  isSubscribed() {
160
171
  return this.subscribeSuccess;
161
172
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CscMeasurement = void 0;
4
+ const consts_js_1 = require("../../../base/consts.js");
4
5
  class CscMeasurement {
5
6
  data;
6
7
  prevCrankData = undefined;
@@ -8,13 +9,16 @@ class CscMeasurement {
8
9
  prevWheelData = undefined;
9
10
  currentWheelData = undefined;
10
11
  timeOffset = 0;
11
- cw = 2.1;
12
+ cw = consts_js_1.DEFAULT_WHEEL_CIRCUMFERENCE;
12
13
  constructor(data = {}) {
13
14
  this.data = data;
14
15
  }
15
16
  setWheelCircumference(wheelCircumference) {
16
17
  this.cw = wheelCircumference;
17
18
  }
19
+ getWheelCircumference() {
20
+ return this.cw;
21
+ }
18
22
  parse(buffer, features) {
19
23
  const data = Buffer.from(buffer);
20
24
  let offset = 0;
@@ -89,7 +93,7 @@ class CscMeasurement {
89
93
  }
90
94
  const seconds = time / 1024;
91
95
  const rps = revs / seconds;
92
- speed = rps * this.cw;
96
+ speed = rps * this.cw * 3.6;
93
97
  }
94
98
  else if (p.cntUpdateMissing < 0 || p.cntUpdateMissing > 2) {
95
99
  speed = 0;
@@ -8,9 +8,14 @@ const gd_eventlog_1 = require("gd-eventlog");
8
8
  const adapter_js_1 = __importDefault(require("../base/adapter.js"));
9
9
  const index_js_1 = require("../../types/index.js");
10
10
  const sensor_js_1 = require("./sensor.js");
11
+ const speed_js_1 = __importDefault(require("../../modes/speed.js"));
11
12
  class BleCSCAdapter extends adapter_js_1.default {
12
13
  static INCYCLIST_PROFILE_NAME = 'Speed + Cadence Sensor';
13
14
  static CAPABILITIES = [index_js_1.IncyclistCapability.Speed, index_js_1.IncyclistCapability.Cadence];
15
+ static controllers = {
16
+ modes: [speed_js_1.default],
17
+ default: speed_js_1.default
18
+ };
14
19
  constructor(settings, props) {
15
20
  super(settings, props);
16
21
  this.logger = new gd_eventlog_1.EventLogger('Ble-CSC');
@@ -48,8 +53,30 @@ class BleCSCAdapter extends adapter_js_1.default {
48
53
  return this.getName();
49
54
  }
50
55
  mapData(deviceData) {
51
- const { cadence, speed } = deviceData;
52
- return { cadence, speed: speed * 3.6 };
56
+ const { cadence, speed = 0 } = deviceData;
57
+ return { cadence, speed };
58
+ }
59
+ transformData(bikeData) {
60
+ if (bikeData === undefined)
61
+ return;
62
+ let data = {
63
+ speed: bikeData.speed,
64
+ power: bikeData.power,
65
+ cadence: bikeData.pedalRpm,
66
+ timestamp: Date.now()
67
+ };
68
+ if (bikeData.time)
69
+ data.deviceTime = bikeData.time;
70
+ this.data = data;
71
+ return data;
72
+ }
73
+ setCyclingMode(mode, settings, sendInitCommands) {
74
+ super.setCyclingMode(mode, settings, sendInitCommands);
75
+ try {
76
+ const wc = this.getCyclingMode().getSetting('wc');
77
+ this.getSensor().setWheelCircumference(wc / 1000);
78
+ }
79
+ catch { }
53
80
  }
54
81
  }
55
82
  exports.BleCSCAdapter = BleCSCAdapter;
@@ -15,12 +15,12 @@ class BleCyclingSpeedCadenceDevice extends sensor_js_1.TBleSensor {
15
15
  data;
16
16
  parsers = {};
17
17
  featureParser;
18
+ measurement = new measurement_js_1.CscMeasurement();
18
19
  constructor(peripheral, props) {
19
20
  super(peripheral, props);
20
21
  this.data = {};
21
- const measurement = new measurement_js_1.CscMeasurement();
22
22
  this.featureParser = new features_js_1.CscFeatures();
23
- this.parsers[(0, utils_js_1.beautifyUUID)(consts_js_1.CSC_MEASUREMENT)] = measurement;
23
+ this.parsers[(0, utils_js_1.beautifyUUID)(consts_js_1.CSC_MEASUREMENT)] = this.measurement;
24
24
  }
25
25
  getRequiredCharacteristics() {
26
26
  return [consts_js_1.CSC_MEASUREMENT];
@@ -34,6 +34,7 @@ class BleCyclingSpeedCadenceDevice extends sensor_js_1.TBleSensor {
34
34
  }
35
35
  catch (err) {
36
36
  this.logEvent({ message: 'read failed', characteristic: consts_js_1.CSC_FEATURE, reason: err.message });
37
+ return {};
37
38
  }
38
39
  }
39
40
  onData(characteristic, characteristicData) {
@@ -54,5 +55,11 @@ class BleCyclingSpeedCadenceDevice extends sensor_js_1.TBleSensor {
54
55
  reset() {
55
56
  this.data = {};
56
57
  }
58
+ async setWheelCircumference(wheelCircumference) {
59
+ this.measurement.setWheelCircumference(wheelCircumference);
60
+ }
61
+ getWheelCircumference() {
62
+ return this.measurement.getWheelCircumference();
63
+ }
57
64
  }
58
65
  exports.BleCyclingSpeedCadenceDevice = BleCyclingSpeedCadenceDevice;
@@ -327,6 +327,9 @@ class BleFmAdapter extends adapter_js_1.default {
327
327
  delete this.promiseSendUpdate;
328
328
  return confirmed;
329
329
  }
330
+ if (!device.isReady()) {
331
+ this.logEvent({ message: 'send bike update failed', reason: 'not connected' });
332
+ }
330
333
  }
331
334
  catch (error) {
332
335
  const err = error;
@@ -7,6 +7,7 @@ const gd_eventlog_1 = require("gd-eventlog");
7
7
  const index_js_1 = require("../fm/index.js");
8
8
  const sensor_js_1 = __importDefault(require("./sensor.js"));
9
9
  const index_js_2 = require("../../types/index.js");
10
+ const calculations_js_1 = __importDefault(require("../../utils/calculations.js"));
10
11
  class BleWahooAdapter extends index_js_1.BleFmAdapter {
11
12
  static INCYCLIST_PROFILE_NAME = 'Smart Trainer';
12
13
  constructor(settings, props) {
@@ -29,6 +30,15 @@ class BleWahooAdapter extends index_js_1.BleFmAdapter {
29
30
  updateSensor(peripheral) {
30
31
  this.device = new sensor_js_1.default(peripheral, { logger: this.logger });
31
32
  }
33
+ mapData(deviceData) {
34
+ const data = super.mapData(deviceData);
35
+ if (!deviceData.instantaneousPower && !deviceData.cadence && (deviceData.speed ?? 0) > 0) {
36
+ const m = this.getWeight();
37
+ data.isPedalling = true;
38
+ data.power = calculations_js_1.default.calculatePower(m, data.speed / 3.6, 0);
39
+ }
40
+ return data;
41
+ }
32
42
  async checkCapabilities() {
33
43
  this.logEvent({ message: 'device capabilities updated', name: this.getSettings().name, interface: this.getSettings().interface, capabilities: this.capabilities });
34
44
  }
@@ -23,6 +23,9 @@ class BleWahooDevice extends sensor_js_1.default {
23
23
  isSimMode;
24
24
  isRequestControlBusy = false;
25
25
  weight = consts_js_1.DEFAULT_BIKE_WEIGHT + consts_js_1.DEFAULT_USER_WEIGHT;
26
+ prevWheelRevolutions = undefined;
27
+ prevWheelEventTime = undefined;
28
+ wheelCircumference = 2.096;
26
29
  simModeSettings;
27
30
  constructor(peripheral, props) {
28
31
  super(peripheral, props);
@@ -163,17 +166,40 @@ class BleWahooDevice extends sensor_js_1.default {
163
166
  }
164
167
  parsePower(_data) {
165
168
  const data = Buffer.from(_data);
169
+ let balance;
170
+ let accTorque;
171
+ let speed;
172
+ let wheelRevolutions, wheelEventTime;
166
173
  try {
167
174
  let offset = 4;
168
175
  const flags = data.readUInt16LE(0);
169
176
  this.data.instantaneousPower = data.readUInt16LE(2);
170
177
  if (flags & 0x1)
171
- data.readUInt8(offset++);
178
+ balance = data.readUInt8(offset++);
172
179
  if (flags & 0x4) {
180
+ accTorque = data.readUInt16LE(offset);
173
181
  offset += 2;
174
182
  }
175
183
  if (flags & 0x10) {
184
+ wheelRevolutions = data.readUInt32LE(offset);
185
+ wheelEventTime = data.readUInt16LE(offset + 4);
176
186
  offset += 6;
187
+ if (this.prevWheelRevolutions !== undefined && this.prevWheelEventTime !== undefined) {
188
+ const wheelRevDelta = wheelRevolutions - this.prevWheelRevolutions;
189
+ const wheelTimeDelta = (wheelEventTime - this.prevWheelEventTime + 65536) % 65536;
190
+ const timeSeconds = (wheelTimeDelta / 1024);
191
+ if (timeSeconds > 0 && wheelRevDelta >= 0) {
192
+ const distance = wheelRevDelta * this.wheelCircumference;
193
+ speed = distance / timeSeconds * 3.6;
194
+ this.data.speed = speed;
195
+ }
196
+ else if (timeSeconds > 0 && wheelRevDelta === 0) {
197
+ speed = 0;
198
+ this.data.speed = speed;
199
+ }
200
+ }
201
+ this.prevWheelRevolutions = wheelRevolutions;
202
+ this.prevWheelEventTime = wheelEventTime;
177
203
  }
178
204
  if (flags & 0x20) {
179
205
  const crankData = {
@@ -188,8 +214,8 @@ class BleWahooDevice extends sensor_js_1.default {
188
214
  catch (err) {
189
215
  this.logEvent({ message: 'error', fn: 'parsePower', error: err.message, stack: err.stack });
190
216
  }
191
- const { instantaneousPower, cadence, time } = this.data;
192
- return { instantaneousPower, cadence, time, raw: data.toString('hex') };
217
+ const { instantaneousPower, cadence, speed: dataSpeed, time } = this.data;
218
+ return { instantaneousPower, speed: dataSpeed, cadence, time, raw: '2a63:' + data.toString('hex') };
193
219
  }
194
220
  supportsHeartRate() {
195
221
  try {
@@ -341,5 +367,11 @@ class BleWahooDevice extends sensor_js_1.default {
341
367
  return false;
342
368
  }
343
369
  }
370
+ async setWheelCircumference(wheelCircumference) {
371
+ this.wheelCircumference = wheelCircumference;
372
+ }
373
+ getWheelCircumference() {
374
+ return this.wheelCircumference;
375
+ }
344
376
  }
345
377
  exports.default = BleWahooDevice;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const base_1 = require("./base");
7
+ const types_1 = require("./types");
8
+ const calculations_js_1 = __importDefault(require("../utils/calculations.js"));
9
+ const consts_js_1 = require("../base/consts.js");
10
+ class SpeedCyclingMode extends base_1.CyclingModeBase {
11
+ data;
12
+ prevUpdateTS = 0;
13
+ prevRequest;
14
+ static config = {
15
+ name: "SpeedSensor",
16
+ description: "Calculates power based on speed and slope.",
17
+ properties: [
18
+ { key: 'bikeType', name: 'Bike Type', description: '', type: types_1.CyclingModeProperyType.SingleSelect, options: ['Race', 'Mountain', 'Triathlon'], default: 'Race' },
19
+ { key: 'wc', name: 'Wheel Circumference', description: 'Circumference in mm', type: types_1.CyclingModeProperyType.Integer, default: Math.ceil(consts_js_1.DEFAULT_WHEEL_CIRCUMFERENCE * 1000) },
20
+ ]
21
+ };
22
+ constructor(adapter, props) {
23
+ super(adapter, props);
24
+ this.data = { speed: 0, power: 0, distanceInternal: 0, pedalRpm: 0, isPedalling: false, heartrate: 0 };
25
+ }
26
+ getData() {
27
+ return this.data;
28
+ }
29
+ getSlope() {
30
+ const { slope } = this.data;
31
+ return slope || 0;
32
+ }
33
+ getWeight() {
34
+ const a = this.adapter;
35
+ const defaultWeight = consts_js_1.DEFAULT_BIKE_WEIGHT + consts_js_1.DEFAULT_USER_WEIGHT;
36
+ const m = (a) ? a.getWeight() || defaultWeight : defaultWeight;
37
+ return m;
38
+ }
39
+ getTimeSinceLastUpdate() {
40
+ const ts = Date.now();
41
+ const duration = this.prevUpdateTS === 0 ? 0 : ((ts - this.prevUpdateTS) / 1000);
42
+ return duration;
43
+ }
44
+ getBikeInitRequest() {
45
+ return {};
46
+ }
47
+ updateData(data, log = true) {
48
+ const prevData = this.data;
49
+ const prevRequest = this.prevRequest ?? {};
50
+ const slope = (prevRequest.slope !== undefined ? prevRequest.slope : prevData.slope ?? 0);
51
+ const { pedalRpm = 0 } = data ?? {};
52
+ const bikeType = this.getSetting('bikeType')?.toLowerCase();
53
+ if (data.isPedalling === false || data.speed === 0) {
54
+ this.data = {
55
+ ...data,
56
+ speed: 0,
57
+ isPedalling: false,
58
+ power: 0,
59
+ slope
60
+ };
61
+ this.prevUpdateTS = Date.now();
62
+ return this.data;
63
+ }
64
+ const m = this.getWeight();
65
+ const t = this.getTimeSinceLastUpdate();
66
+ const v = (data.speed ?? 0) / 3.6;
67
+ const power = calculations_js_1.default.calculatePower(m, v, 0, { bikeType });
68
+ const { speed: calcSpeed, distance } = this.calculateSpeedAndDistance(power, slope, m, t, { bikeType });
69
+ const prevTime = this.data.time ?? 0;
70
+ const prevDistance = this.data.distanceInternal ?? 0;
71
+ const updated = {
72
+ ...data,
73
+ time: prevTime + t,
74
+ isPedalling: data.isPedalling ?? (v > 0 || pedalRpm > 0),
75
+ slope,
76
+ power,
77
+ speed: calcSpeed,
78
+ distanceInternal: prevDistance + distance
79
+ };
80
+ if (log && this.logger)
81
+ this.logEvent({ message: "updateData result", mode: this.getName(), data: updated, bikeData: data });
82
+ this.data = updated;
83
+ this.prevUpdateTS = Date.now();
84
+ return updated;
85
+ }
86
+ sendBikeUpdate(request) {
87
+ if (request.slope !== undefined) {
88
+ this.data.slope = request.slope;
89
+ }
90
+ return {};
91
+ }
92
+ calculatePowerAndDistance(speed, slope, m, t, props = {}) {
93
+ const prevData = this.getData();
94
+ const vPrev = (prevData.speed || 0) / 3.6;
95
+ const EkinPrev = 1 / 2 * m * vPrev * vPrev;
96
+ const vTarget = (speed || 0) / 3.6;
97
+ const Ekin = 1 / 2 * m * vTarget * vTarget;
98
+ const powerDelta = t !== 0 ? (EkinPrev - Ekin) / t : 0;
99
+ const powerToMaintainSpeed = calculations_js_1.default.calculatePower(m, vPrev, slope, props);
100
+ const power = powerToMaintainSpeed - powerDelta;
101
+ const v = speed / 3.6;
102
+ const distance = v * t;
103
+ this.data.power = power;
104
+ return { power, distance };
105
+ }
106
+ calculateSpeedAndDistance(power, slope, m, t, props = {}) {
107
+ const prevData = this.getData();
108
+ const vPrev = (prevData.speed || 0) / 3.6;
109
+ const EkinPrev = 1 / 2 * m * vPrev * vPrev;
110
+ let powerToMaintainSpeed = calculations_js_1.default.calculatePower(m, vPrev, slope, props);
111
+ if (t >= 30) {
112
+ const speed = calculations_js_1.default.calculateSpeed(m, power, slope, props);
113
+ return { speed, distance: 0 };
114
+ }
115
+ const powerDelta = powerToMaintainSpeed - power;
116
+ const Ekin = EkinPrev - powerDelta * t;
117
+ if (Ekin > 0) {
118
+ const v = Math.sqrt(2 * Ekin / m);
119
+ const speed = v * 3.6;
120
+ const distance = v * t;
121
+ this.data.speed = speed;
122
+ return { speed, distance };
123
+ }
124
+ else {
125
+ const v = vPrev * 0.5;
126
+ const speed = v * 3.6;
127
+ const distance = v * t;
128
+ this.data.speed = speed;
129
+ return { speed, distance };
130
+ }
131
+ }
132
+ }
133
+ exports.default = SpeedCyclingMode;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,6 @@
1
1
  export const DEFAULT_BIKE_WEIGHT = 10;
2
2
  export const DEFAULT_USER_WEIGHT = 75;
3
+ export const DEFAULT_WHEEL_CIRCUMFERENCE = 2.118;
3
4
  export const DEFAULT_PROPS = {
4
5
  userWeight: DEFAULT_USER_WEIGHT,
5
6
  bikeWeight: DEFAULT_BIKE_WEIGHT
@@ -354,17 +354,14 @@ export default class BleAdapter extends IncyclistDevice {
354
354
  sensor.off('data', this.onDeviceDataHandler);
355
355
  let connected = false;
356
356
  if (sensor.isReconnectBusy()) {
357
- connected = await sensor.reconnectSensor(false);
358
- }
359
- if (connected) {
360
- sensor.on('data', this.onDeviceDataHandler);
361
- return true;
357
+ connected = await sensor.reconnectSensor(true);
358
+ if (connected) {
359
+ sensor.on('data', this.onDeviceDataHandler);
360
+ }
361
+ return connected;
362
362
  }
363
363
  await sensor.getPeripheral().disconnect();
364
364
  const success = await super.restart(pause);
365
- if (success) {
366
- sensor.on('data', this.onDeviceDataHandler);
367
- }
368
365
  return success;
369
366
  }
370
367
  async stop() {
@@ -402,7 +402,12 @@ export class BleInterface extends EventEmitter {
402
402
  }
403
403
  const isWahoo = this.checkForWahooEnhancement(announcement);
404
404
  if (isWahoo) {
405
- this.processWahooAnnouncement(announcement);
405
+ this.processEnrichedAnnouncement(announcement);
406
+ return;
407
+ }
408
+ const isTacx = this.checkForTacxEnhancement(announcement);
409
+ if (isTacx) {
410
+ this.processEnrichedAnnouncement(announcement);
406
411
  return;
407
412
  }
408
413
  this.addService(announcement);
@@ -416,7 +421,7 @@ export class BleInterface extends EventEmitter {
416
421
  }
417
422
  return false;
418
423
  }
419
- processWahooAnnouncement(announcement) {
424
+ processEnrichedAnnouncement(announcement) {
420
425
  if (this.isCompleting(announcement)) {
421
426
  return;
422
427
  }
@@ -426,6 +431,19 @@ export class BleInterface extends EventEmitter {
426
431
  this.addService(announcement);
427
432
  });
428
433
  }
434
+ checkForTacxEnhancement(announcement) {
435
+ const name = announcement?.name?.toUpperCase();
436
+ if (name.startsWith('TACX')) {
437
+ const supported = announcement.serviceUUIDs.map(s => beautifyUUID(s));
438
+ const tacxService = beautifyUUID('6E40FEC1-B5A3-F393-E0A9-E50E24DCCA9E');
439
+ const cp = beautifyUUID('1818');
440
+ const fe = beautifyUUID('1826');
441
+ if (supported.includes(cp) && !supported.includes(fe) && !supported.includes(tacxService)) {
442
+ return true;
443
+ }
444
+ }
445
+ return false;
446
+ }
429
447
  buildAnnouncement(peripheral) {
430
448
  return {
431
449
  advertisement: peripheral.advertisement,