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.
- package/lib/cjs/base/consts.js +2 -1
- package/lib/cjs/ble/base/adapter.js +5 -8
- package/lib/cjs/ble/base/interface.js +20 -2
- package/lib/cjs/ble/base/peripheral.js +22 -5
- package/lib/cjs/ble/base/sensor.js +12 -1
- package/lib/cjs/ble/characteristics/csc/measurement.js +6 -2
- package/lib/cjs/ble/csc/adapter.js +29 -2
- package/lib/cjs/ble/csc/sensor.js +9 -2
- package/lib/cjs/ble/fm/adapter.js +3 -0
- package/lib/cjs/ble/wahoo/adapter.js +10 -0
- package/lib/cjs/ble/wahoo/sensor.js +35 -3
- package/lib/cjs/modes/speed.js +133 -0
- package/lib/cjs/types/sensor.js +2 -0
- package/lib/esm/base/consts.js +1 -0
- package/lib/esm/ble/base/adapter.js +5 -8
- package/lib/esm/ble/base/interface.js +20 -2
- package/lib/esm/ble/base/peripheral.js +22 -5
- package/lib/esm/ble/base/sensor.js +12 -1
- package/lib/esm/ble/characteristics/csc/measurement.js +6 -2
- package/lib/esm/ble/csc/adapter.js +29 -2
- package/lib/esm/ble/csc/sensor.js +9 -2
- package/lib/esm/ble/fm/adapter.js +3 -0
- package/lib/esm/ble/wahoo/adapter.js +10 -0
- package/lib/esm/ble/wahoo/sensor.js +35 -3
- package/lib/esm/modes/speed.js +127 -0
- package/lib/esm/types/sensor.js +1 -0
- package/lib/types/base/consts.d.ts +1 -0
- package/lib/types/ble/base/interface.d.ts +2 -1
- package/lib/types/ble/base/sensor.d.ts +2 -0
- package/lib/types/ble/characteristics/csc/measurement.d.ts +1 -0
- package/lib/types/ble/csc/adapter.d.ts +5 -1
- package/lib/types/ble/csc/sensor.d.ts +6 -2
- package/lib/types/ble/types.d.ts +1 -0
- package/lib/types/ble/wahoo/adapter.d.ts +3 -1
- package/lib/types/ble/wahoo/sensor.d.ts +5 -0
- package/lib/types/modes/speed.d.ts +43 -0
- package/lib/types/types/index.d.ts +1 -0
- package/lib/types/types/sensor.d.ts +4 -0
- package/package.json +1 -1
package/lib/cjs/base/consts.js
CHANGED
|
@@ -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(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return
|
|
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.
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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;
|
package/lib/esm/base/consts.js
CHANGED
|
@@ -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(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return
|
|
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.
|
|
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
|
-
|
|
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,
|