incyclist-devices 3.0.20 → 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/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/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/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() {
|
|
@@ -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() {
|
|
@@ -9,6 +9,7 @@ export class TBleSensor extends EventEmitter {
|
|
|
9
9
|
stopRequested = false;
|
|
10
10
|
subscribeSuccess = false;
|
|
11
11
|
reconnectPromise;
|
|
12
|
+
connectPromise;
|
|
12
13
|
onDataHandler;
|
|
13
14
|
logEvent(event, ...args) {
|
|
14
15
|
try {
|
|
@@ -73,7 +74,8 @@ export class TBleSensor extends EventEmitter {
|
|
|
73
74
|
this.logEvent({ message: 'no peripheral' });
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
this.connectPromise = this.connectPromise ?? this.peripheral.connect();
|
|
78
|
+
const connected = await this.connectPromise;
|
|
77
79
|
if (!connected)
|
|
78
80
|
return false;
|
|
79
81
|
if (!reconnect)
|
|
@@ -125,6 +127,9 @@ export class TBleSensor extends EventEmitter {
|
|
|
125
127
|
let connected = false;
|
|
126
128
|
let subscribed = false;
|
|
127
129
|
let success = false;
|
|
130
|
+
if (connectionLost) {
|
|
131
|
+
this.emit('connctionLost');
|
|
132
|
+
}
|
|
128
133
|
await sleep(500);
|
|
129
134
|
do {
|
|
130
135
|
try {
|
|
@@ -144,6 +149,9 @@ export class TBleSensor extends EventEmitter {
|
|
|
144
149
|
if (!this.stopRequested && !success)
|
|
145
150
|
this.logEvent({ message: 'reconnect sensor retry', name, address });
|
|
146
151
|
} while (!success || this.stopRequested);
|
|
152
|
+
if (connectionLost && success) {
|
|
153
|
+
this.emit('connctionRecovered');
|
|
154
|
+
}
|
|
147
155
|
this.logEvent({ message: 'reconnect sensor completed', success, stopRequested: this.stopRequested });
|
|
148
156
|
return success;
|
|
149
157
|
}
|
|
@@ -153,6 +161,9 @@ export class TBleSensor extends EventEmitter {
|
|
|
153
161
|
isConnected() {
|
|
154
162
|
return this.peripheral?.isConnected();
|
|
155
163
|
}
|
|
164
|
+
isReady() {
|
|
165
|
+
return this.isConnected() && this.connectPromise === undefined && this.reconnectPromise === undefined;
|
|
166
|
+
}
|
|
156
167
|
isSubscribed() {
|
|
157
168
|
return this.subscribeSuccess;
|
|
158
169
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_WHEEL_CIRCUMFERENCE } from "../../../base/consts.js";
|
|
1
2
|
export class CscMeasurement {
|
|
2
3
|
data;
|
|
3
4
|
prevCrankData = undefined;
|
|
@@ -5,13 +6,16 @@ export class CscMeasurement {
|
|
|
5
6
|
prevWheelData = undefined;
|
|
6
7
|
currentWheelData = undefined;
|
|
7
8
|
timeOffset = 0;
|
|
8
|
-
cw =
|
|
9
|
+
cw = DEFAULT_WHEEL_CIRCUMFERENCE;
|
|
9
10
|
constructor(data = {}) {
|
|
10
11
|
this.data = data;
|
|
11
12
|
}
|
|
12
13
|
setWheelCircumference(wheelCircumference) {
|
|
13
14
|
this.cw = wheelCircumference;
|
|
14
15
|
}
|
|
16
|
+
getWheelCircumference() {
|
|
17
|
+
return this.cw;
|
|
18
|
+
}
|
|
15
19
|
parse(buffer, features) {
|
|
16
20
|
const data = Buffer.from(buffer);
|
|
17
21
|
let offset = 0;
|
|
@@ -86,7 +90,7 @@ export class CscMeasurement {
|
|
|
86
90
|
}
|
|
87
91
|
const seconds = time / 1024;
|
|
88
92
|
const rps = revs / seconds;
|
|
89
|
-
speed = rps * this.cw;
|
|
93
|
+
speed = rps * this.cw * 3.6;
|
|
90
94
|
}
|
|
91
95
|
else if (p.cntUpdateMissing < 0 || p.cntUpdateMissing > 2) {
|
|
92
96
|
speed = 0;
|
|
@@ -2,9 +2,14 @@ import { EventLogger } from 'gd-eventlog';
|
|
|
2
2
|
import BleAdapter from '../base/adapter.js';
|
|
3
3
|
import { IncyclistCapability } from '../../types/index.js';
|
|
4
4
|
import { BleCyclingSpeedCadenceDevice } from './sensor.js';
|
|
5
|
+
import SpeedCyclingMode from '../../modes/speed.js';
|
|
5
6
|
export class BleCSCAdapter extends BleAdapter {
|
|
6
7
|
static INCYCLIST_PROFILE_NAME = 'Speed + Cadence Sensor';
|
|
7
8
|
static CAPABILITIES = [IncyclistCapability.Speed, IncyclistCapability.Cadence];
|
|
9
|
+
static controllers = {
|
|
10
|
+
modes: [SpeedCyclingMode],
|
|
11
|
+
default: SpeedCyclingMode
|
|
12
|
+
};
|
|
8
13
|
constructor(settings, props) {
|
|
9
14
|
super(settings, props);
|
|
10
15
|
this.logger = new EventLogger('Ble-CSC');
|
|
@@ -42,7 +47,29 @@ export class BleCSCAdapter extends BleAdapter {
|
|
|
42
47
|
return this.getName();
|
|
43
48
|
}
|
|
44
49
|
mapData(deviceData) {
|
|
45
|
-
const { cadence, speed } = deviceData;
|
|
46
|
-
return { cadence, speed
|
|
50
|
+
const { cadence, speed = 0 } = deviceData;
|
|
51
|
+
return { cadence, speed };
|
|
52
|
+
}
|
|
53
|
+
transformData(bikeData) {
|
|
54
|
+
if (bikeData === undefined)
|
|
55
|
+
return;
|
|
56
|
+
let data = {
|
|
57
|
+
speed: bikeData.speed,
|
|
58
|
+
power: bikeData.power,
|
|
59
|
+
cadence: bikeData.pedalRpm,
|
|
60
|
+
timestamp: Date.now()
|
|
61
|
+
};
|
|
62
|
+
if (bikeData.time)
|
|
63
|
+
data.deviceTime = bikeData.time;
|
|
64
|
+
this.data = data;
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
setCyclingMode(mode, settings, sendInitCommands) {
|
|
68
|
+
super.setCyclingMode(mode, settings, sendInitCommands);
|
|
69
|
+
try {
|
|
70
|
+
const wc = this.getCyclingMode().getSetting('wc');
|
|
71
|
+
this.getSensor().setWheelCircumference(wc / 1000);
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
47
74
|
}
|
|
48
75
|
}
|
|
@@ -12,12 +12,12 @@ export class BleCyclingSpeedCadenceDevice extends TBleSensor {
|
|
|
12
12
|
data;
|
|
13
13
|
parsers = {};
|
|
14
14
|
featureParser;
|
|
15
|
+
measurement = new CscMeasurement();
|
|
15
16
|
constructor(peripheral, props) {
|
|
16
17
|
super(peripheral, props);
|
|
17
18
|
this.data = {};
|
|
18
|
-
const measurement = new CscMeasurement();
|
|
19
19
|
this.featureParser = new CscFeatures();
|
|
20
|
-
this.parsers[beautifyUUID(CSC_MEASUREMENT)] = measurement;
|
|
20
|
+
this.parsers[beautifyUUID(CSC_MEASUREMENT)] = this.measurement;
|
|
21
21
|
}
|
|
22
22
|
getRequiredCharacteristics() {
|
|
23
23
|
return [CSC_MEASUREMENT];
|
|
@@ -31,6 +31,7 @@ export class BleCyclingSpeedCadenceDevice extends TBleSensor {
|
|
|
31
31
|
}
|
|
32
32
|
catch (err) {
|
|
33
33
|
this.logEvent({ message: 'read failed', characteristic: CSC_FEATURE, reason: err.message });
|
|
34
|
+
return {};
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
onData(characteristic, characteristicData) {
|
|
@@ -51,4 +52,10 @@ export class BleCyclingSpeedCadenceDevice extends TBleSensor {
|
|
|
51
52
|
reset() {
|
|
52
53
|
this.data = {};
|
|
53
54
|
}
|
|
55
|
+
async setWheelCircumference(wheelCircumference) {
|
|
56
|
+
this.measurement.setWheelCircumference(wheelCircumference);
|
|
57
|
+
}
|
|
58
|
+
getWheelCircumference() {
|
|
59
|
+
return this.measurement.getWheelCircumference();
|
|
60
|
+
}
|
|
54
61
|
}
|
|
@@ -322,6 +322,9 @@ export default class BleFmAdapter extends BleAdapter {
|
|
|
322
322
|
delete this.promiseSendUpdate;
|
|
323
323
|
return confirmed;
|
|
324
324
|
}
|
|
325
|
+
if (!device.isReady()) {
|
|
326
|
+
this.logEvent({ message: 'send bike update failed', reason: 'not connected' });
|
|
327
|
+
}
|
|
325
328
|
}
|
|
326
329
|
catch (error) {
|
|
327
330
|
const err = error;
|
|
@@ -2,6 +2,7 @@ import { EventLogger } from 'gd-eventlog';
|
|
|
2
2
|
import { BleFmAdapter } from '../fm/index.js';
|
|
3
3
|
import BleWahooDevice from './sensor.js';
|
|
4
4
|
import { IncyclistCapability } from '../../types/index.js';
|
|
5
|
+
import calc from '../../utils/calculations.js';
|
|
5
6
|
export default class BleWahooAdapter extends BleFmAdapter {
|
|
6
7
|
static INCYCLIST_PROFILE_NAME = 'Smart Trainer';
|
|
7
8
|
constructor(settings, props) {
|
|
@@ -24,6 +25,15 @@ export default class BleWahooAdapter extends BleFmAdapter {
|
|
|
24
25
|
updateSensor(peripheral) {
|
|
25
26
|
this.device = new BleWahooDevice(peripheral, { logger: this.logger });
|
|
26
27
|
}
|
|
28
|
+
mapData(deviceData) {
|
|
29
|
+
const data = super.mapData(deviceData);
|
|
30
|
+
if (!deviceData.instantaneousPower && !deviceData.cadence && (deviceData.speed ?? 0) > 0) {
|
|
31
|
+
const m = this.getWeight();
|
|
32
|
+
data.isPedalling = true;
|
|
33
|
+
data.power = calc.calculatePower(m, data.speed / 3.6, 0);
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
27
37
|
async checkCapabilities() {
|
|
28
38
|
this.logEvent({ message: 'device capabilities updated', name: this.getSettings().name, interface: this.getSettings().interface, capabilities: this.capabilities });
|
|
29
39
|
}
|
|
@@ -18,6 +18,9 @@ export default class BleWahooDevice extends BleFitnessMachineDevice {
|
|
|
18
18
|
isSimMode;
|
|
19
19
|
isRequestControlBusy = false;
|
|
20
20
|
weight = DEFAULT_BIKE_WEIGHT + DEFAULT_USER_WEIGHT;
|
|
21
|
+
prevWheelRevolutions = undefined;
|
|
22
|
+
prevWheelEventTime = undefined;
|
|
23
|
+
wheelCircumference = 2.096;
|
|
21
24
|
simModeSettings;
|
|
22
25
|
constructor(peripheral, props) {
|
|
23
26
|
super(peripheral, props);
|
|
@@ -158,17 +161,40 @@ export default class BleWahooDevice extends BleFitnessMachineDevice {
|
|
|
158
161
|
}
|
|
159
162
|
parsePower(_data) {
|
|
160
163
|
const data = Buffer.from(_data);
|
|
164
|
+
let balance;
|
|
165
|
+
let accTorque;
|
|
166
|
+
let speed;
|
|
167
|
+
let wheelRevolutions, wheelEventTime;
|
|
161
168
|
try {
|
|
162
169
|
let offset = 4;
|
|
163
170
|
const flags = data.readUInt16LE(0);
|
|
164
171
|
this.data.instantaneousPower = data.readUInt16LE(2);
|
|
165
172
|
if (flags & 0x1)
|
|
166
|
-
data.readUInt8(offset++);
|
|
173
|
+
balance = data.readUInt8(offset++);
|
|
167
174
|
if (flags & 0x4) {
|
|
175
|
+
accTorque = data.readUInt16LE(offset);
|
|
168
176
|
offset += 2;
|
|
169
177
|
}
|
|
170
178
|
if (flags & 0x10) {
|
|
179
|
+
wheelRevolutions = data.readUInt32LE(offset);
|
|
180
|
+
wheelEventTime = data.readUInt16LE(offset + 4);
|
|
171
181
|
offset += 6;
|
|
182
|
+
if (this.prevWheelRevolutions !== undefined && this.prevWheelEventTime !== undefined) {
|
|
183
|
+
const wheelRevDelta = wheelRevolutions - this.prevWheelRevolutions;
|
|
184
|
+
const wheelTimeDelta = (wheelEventTime - this.prevWheelEventTime + 65536) % 65536;
|
|
185
|
+
const timeSeconds = (wheelTimeDelta / 1024);
|
|
186
|
+
if (timeSeconds > 0 && wheelRevDelta >= 0) {
|
|
187
|
+
const distance = wheelRevDelta * this.wheelCircumference;
|
|
188
|
+
speed = distance / timeSeconds * 3.6;
|
|
189
|
+
this.data.speed = speed;
|
|
190
|
+
}
|
|
191
|
+
else if (timeSeconds > 0 && wheelRevDelta === 0) {
|
|
192
|
+
speed = 0;
|
|
193
|
+
this.data.speed = speed;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.prevWheelRevolutions = wheelRevolutions;
|
|
197
|
+
this.prevWheelEventTime = wheelEventTime;
|
|
172
198
|
}
|
|
173
199
|
if (flags & 0x20) {
|
|
174
200
|
const crankData = {
|
|
@@ -183,8 +209,8 @@ export default class BleWahooDevice extends BleFitnessMachineDevice {
|
|
|
183
209
|
catch (err) {
|
|
184
210
|
this.logEvent({ message: 'error', fn: 'parsePower', error: err.message, stack: err.stack });
|
|
185
211
|
}
|
|
186
|
-
const { instantaneousPower, cadence, time } = this.data;
|
|
187
|
-
return { instantaneousPower, cadence, time, raw: data.toString('hex') };
|
|
212
|
+
const { instantaneousPower, cadence, speed: dataSpeed, time } = this.data;
|
|
213
|
+
return { instantaneousPower, speed: dataSpeed, cadence, time, raw: '2a63:' + data.toString('hex') };
|
|
188
214
|
}
|
|
189
215
|
supportsHeartRate() {
|
|
190
216
|
try {
|
|
@@ -336,4 +362,10 @@ export default class BleWahooDevice extends BleFitnessMachineDevice {
|
|
|
336
362
|
return false;
|
|
337
363
|
}
|
|
338
364
|
}
|
|
365
|
+
async setWheelCircumference(wheelCircumference) {
|
|
366
|
+
this.wheelCircumference = wheelCircumference;
|
|
367
|
+
}
|
|
368
|
+
getWheelCircumference() {
|
|
369
|
+
return this.wheelCircumference;
|
|
370
|
+
}
|
|
339
371
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { CyclingModeBase } from "./base";
|
|
2
|
+
import { CyclingModeProperyType } from "./types";
|
|
3
|
+
import calc from '../utils/calculations.js';
|
|
4
|
+
import { DEFAULT_BIKE_WEIGHT, DEFAULT_USER_WEIGHT, DEFAULT_WHEEL_CIRCUMFERENCE } from "../base/consts.js";
|
|
5
|
+
export default class SpeedCyclingMode extends CyclingModeBase {
|
|
6
|
+
data;
|
|
7
|
+
prevUpdateTS = 0;
|
|
8
|
+
prevRequest;
|
|
9
|
+
static config = {
|
|
10
|
+
name: "SpeedSensor",
|
|
11
|
+
description: "Calculates power based on speed and slope.",
|
|
12
|
+
properties: [
|
|
13
|
+
{ key: 'bikeType', name: 'Bike Type', description: '', type: CyclingModeProperyType.SingleSelect, options: ['Race', 'Mountain', 'Triathlon'], default: 'Race' },
|
|
14
|
+
{ key: 'wc', name: 'Wheel Circumference', description: 'Circumference in mm', type: CyclingModeProperyType.Integer, default: Math.ceil(DEFAULT_WHEEL_CIRCUMFERENCE * 1000) },
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
constructor(adapter, props) {
|
|
18
|
+
super(adapter, props);
|
|
19
|
+
this.data = { speed: 0, power: 0, distanceInternal: 0, pedalRpm: 0, isPedalling: false, heartrate: 0 };
|
|
20
|
+
}
|
|
21
|
+
getData() {
|
|
22
|
+
return this.data;
|
|
23
|
+
}
|
|
24
|
+
getSlope() {
|
|
25
|
+
const { slope } = this.data;
|
|
26
|
+
return slope || 0;
|
|
27
|
+
}
|
|
28
|
+
getWeight() {
|
|
29
|
+
const a = this.adapter;
|
|
30
|
+
const defaultWeight = DEFAULT_BIKE_WEIGHT + DEFAULT_USER_WEIGHT;
|
|
31
|
+
const m = (a) ? a.getWeight() || defaultWeight : defaultWeight;
|
|
32
|
+
return m;
|
|
33
|
+
}
|
|
34
|
+
getTimeSinceLastUpdate() {
|
|
35
|
+
const ts = Date.now();
|
|
36
|
+
const duration = this.prevUpdateTS === 0 ? 0 : ((ts - this.prevUpdateTS) / 1000);
|
|
37
|
+
return duration;
|
|
38
|
+
}
|
|
39
|
+
getBikeInitRequest() {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
updateData(data, log = true) {
|
|
43
|
+
const prevData = this.data;
|
|
44
|
+
const prevRequest = this.prevRequest ?? {};
|
|
45
|
+
const slope = (prevRequest.slope !== undefined ? prevRequest.slope : prevData.slope ?? 0);
|
|
46
|
+
const { pedalRpm = 0 } = data ?? {};
|
|
47
|
+
const bikeType = this.getSetting('bikeType')?.toLowerCase();
|
|
48
|
+
if (data.isPedalling === false || data.speed === 0) {
|
|
49
|
+
this.data = {
|
|
50
|
+
...data,
|
|
51
|
+
speed: 0,
|
|
52
|
+
isPedalling: false,
|
|
53
|
+
power: 0,
|
|
54
|
+
slope
|
|
55
|
+
};
|
|
56
|
+
this.prevUpdateTS = Date.now();
|
|
57
|
+
return this.data;
|
|
58
|
+
}
|
|
59
|
+
const m = this.getWeight();
|
|
60
|
+
const t = this.getTimeSinceLastUpdate();
|
|
61
|
+
const v = (data.speed ?? 0) / 3.6;
|
|
62
|
+
const power = calc.calculatePower(m, v, 0, { bikeType });
|
|
63
|
+
const { speed: calcSpeed, distance } = this.calculateSpeedAndDistance(power, slope, m, t, { bikeType });
|
|
64
|
+
const prevTime = this.data.time ?? 0;
|
|
65
|
+
const prevDistance = this.data.distanceInternal ?? 0;
|
|
66
|
+
const updated = {
|
|
67
|
+
...data,
|
|
68
|
+
time: prevTime + t,
|
|
69
|
+
isPedalling: data.isPedalling ?? (v > 0 || pedalRpm > 0),
|
|
70
|
+
slope,
|
|
71
|
+
power,
|
|
72
|
+
speed: calcSpeed,
|
|
73
|
+
distanceInternal: prevDistance + distance
|
|
74
|
+
};
|
|
75
|
+
if (log && this.logger)
|
|
76
|
+
this.logEvent({ message: "updateData result", mode: this.getName(), data: updated, bikeData: data });
|
|
77
|
+
this.data = updated;
|
|
78
|
+
this.prevUpdateTS = Date.now();
|
|
79
|
+
return updated;
|
|
80
|
+
}
|
|
81
|
+
sendBikeUpdate(request) {
|
|
82
|
+
if (request.slope !== undefined) {
|
|
83
|
+
this.data.slope = request.slope;
|
|
84
|
+
}
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
calculatePowerAndDistance(speed, slope, m, t, props = {}) {
|
|
88
|
+
const prevData = this.getData();
|
|
89
|
+
const vPrev = (prevData.speed || 0) / 3.6;
|
|
90
|
+
const EkinPrev = 1 / 2 * m * vPrev * vPrev;
|
|
91
|
+
const vTarget = (speed || 0) / 3.6;
|
|
92
|
+
const Ekin = 1 / 2 * m * vTarget * vTarget;
|
|
93
|
+
const powerDelta = t !== 0 ? (EkinPrev - Ekin) / t : 0;
|
|
94
|
+
const powerToMaintainSpeed = calc.calculatePower(m, vPrev, slope, props);
|
|
95
|
+
const power = powerToMaintainSpeed - powerDelta;
|
|
96
|
+
const v = speed / 3.6;
|
|
97
|
+
const distance = v * t;
|
|
98
|
+
this.data.power = power;
|
|
99
|
+
return { power, distance };
|
|
100
|
+
}
|
|
101
|
+
calculateSpeedAndDistance(power, slope, m, t, props = {}) {
|
|
102
|
+
const prevData = this.getData();
|
|
103
|
+
const vPrev = (prevData.speed || 0) / 3.6;
|
|
104
|
+
const EkinPrev = 1 / 2 * m * vPrev * vPrev;
|
|
105
|
+
let powerToMaintainSpeed = calc.calculatePower(m, vPrev, slope, props);
|
|
106
|
+
if (t >= 30) {
|
|
107
|
+
const speed = calc.calculateSpeed(m, power, slope, props);
|
|
108
|
+
return { speed, distance: 0 };
|
|
109
|
+
}
|
|
110
|
+
const powerDelta = powerToMaintainSpeed - power;
|
|
111
|
+
const Ekin = EkinPrev - powerDelta * t;
|
|
112
|
+
if (Ekin > 0) {
|
|
113
|
+
const v = Math.sqrt(2 * Ekin / m);
|
|
114
|
+
const speed = v * 3.6;
|
|
115
|
+
const distance = v * t;
|
|
116
|
+
this.data.speed = speed;
|
|
117
|
+
return { speed, distance };
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const v = vPrev * 0.5;
|
|
121
|
+
const speed = v * 3.6;
|
|
122
|
+
const distance = v * t;
|
|
123
|
+
this.data.speed = speed;
|
|
124
|
+
return { speed, distance };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -9,6 +9,7 @@ export declare class TBleSensor extends EventEmitter implements IBleSensor {
|
|
|
9
9
|
protected stopRequested: boolean;
|
|
10
10
|
protected subscribeSuccess: boolean;
|
|
11
11
|
protected reconnectPromise: Promise<boolean> | undefined;
|
|
12
|
+
protected connectPromise: Promise<boolean> | undefined;
|
|
12
13
|
protected onDataHandler: any;
|
|
13
14
|
logEvent(event: any, ...args: any): void;
|
|
14
15
|
constructor(peripheral: IBlePeripheral, props?: {
|
|
@@ -32,6 +33,7 @@ export declare class TBleSensor extends EventEmitter implements IBleSensor {
|
|
|
32
33
|
doReconnectSensor(connectionLost?: boolean): Promise<boolean>;
|
|
33
34
|
reset(): void;
|
|
34
35
|
isConnected(): boolean;
|
|
36
|
+
isReady(): boolean;
|
|
35
37
|
isSubscribed(): boolean;
|
|
36
38
|
protected onDisconnect(): void;
|
|
37
39
|
read(characteristicUUID: string): Promise<Buffer>;
|
|
@@ -14,6 +14,7 @@ export declare class CscMeasurement implements CharacteristicParser<CyclingCaden
|
|
|
14
14
|
protected cw: number;
|
|
15
15
|
constructor(data?: CyclingCadenceAndSpeed);
|
|
16
16
|
setWheelCircumference(wheelCircumference: any): void;
|
|
17
|
+
getWheelCircumference(): number;
|
|
17
18
|
parse(buffer: Buffer, features?: Feature): CyclingCadenceAndSpeed;
|
|
18
19
|
protected parseCrankData(crankData: any): {
|
|
19
20
|
rpm?: undefined;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import BleAdapter from '../base/adapter.js';
|
|
2
2
|
import { BleDeviceSettings, IBlePeripheral } from '../types.js';
|
|
3
|
-
import { DeviceProperties, IncyclistCapability, IAdapter, IncyclistAdapterData } from '../../types/index.js';
|
|
3
|
+
import { DeviceProperties, IncyclistCapability, IAdapter, IncyclistAdapterData, ControllerConfig, IncyclistBikeData } from '../../types/index.js';
|
|
4
4
|
import { LegacyProfile } from '../../antv2/types.js';
|
|
5
5
|
import { BleCyclingSpeedCadenceDevice } from './sensor.js';
|
|
6
6
|
import { CSCData } from './types.js';
|
|
7
|
+
import ICyclingMode from '../../modes/types.js';
|
|
7
8
|
export declare class BleCSCAdapter extends BleAdapter<CSCData, BleCyclingSpeedCadenceDevice> {
|
|
8
9
|
protected static INCYCLIST_PROFILE_NAME: LegacyProfile;
|
|
9
10
|
protected static CAPABILITIES: IncyclistCapability[];
|
|
11
|
+
protected static controllers: ControllerConfig;
|
|
10
12
|
constructor(settings: BleDeviceSettings, props?: DeviceProperties);
|
|
11
13
|
protected checkCapabilities(): Promise<void>;
|
|
12
14
|
isSame(device: IAdapter): boolean;
|
|
@@ -14,4 +16,6 @@ export declare class BleCSCAdapter extends BleAdapter<CSCData, BleCyclingSpeedCa
|
|
|
14
16
|
getProfile(): LegacyProfile;
|
|
15
17
|
getDisplayName(): string;
|
|
16
18
|
mapData(deviceData: CSCData): IncyclistAdapterData;
|
|
19
|
+
transformData(bikeData: IncyclistBikeData): IncyclistAdapterData;
|
|
20
|
+
setCyclingMode(mode: string | ICyclingMode, settings?: any, sendInitCommands?: boolean): void;
|
|
17
21
|
}
|
|
@@ -2,9 +2,10 @@ import { LegacyProfile } from '../../antv2/types.js';
|
|
|
2
2
|
import { BleProtocol } from '../types.js';
|
|
3
3
|
import { TBleSensor } from '../base/sensor.js';
|
|
4
4
|
import { CharacteristicParser } from '../characteristics/types.js';
|
|
5
|
-
import { CyclingCadenceAndSpeed } from '../characteristics/csc/measurement.js';
|
|
5
|
+
import { CscMeasurement, CyclingCadenceAndSpeed } from '../characteristics/csc/measurement.js';
|
|
6
6
|
import { BleCSCFeatures, CscFeatures } from '../characteristics/csc/features.js';
|
|
7
|
-
|
|
7
|
+
import { ISpeedSensor } from '../../types/sensor.js';
|
|
8
|
+
export declare class BleCyclingSpeedCadenceDevice extends TBleSensor implements ISpeedSensor {
|
|
8
9
|
static readonly profile: LegacyProfile;
|
|
9
10
|
static readonly protocol: BleProtocol;
|
|
10
11
|
static readonly services: string[];
|
|
@@ -13,9 +14,12 @@ export declare class BleCyclingSpeedCadenceDevice extends TBleSensor {
|
|
|
13
14
|
protected data: CyclingCadenceAndSpeed;
|
|
14
15
|
protected parsers: Record<string, CharacteristicParser<any>>;
|
|
15
16
|
protected featureParser: CscFeatures;
|
|
17
|
+
protected measurement: CscMeasurement;
|
|
16
18
|
constructor(peripheral: any, props?: any);
|
|
17
19
|
protected getRequiredCharacteristics(): Array<string>;
|
|
18
20
|
getFeatures(): Promise<BleCSCFeatures>;
|
|
19
21
|
onData(characteristic: string, characteristicData: Buffer): boolean;
|
|
20
22
|
reset(): void;
|
|
23
|
+
setWheelCircumference(wheelCircumference: number): Promise<void>;
|
|
24
|
+
getWheelCircumference(): number;
|
|
21
25
|
}
|
package/lib/types/ble/types.d.ts
CHANGED
|
@@ -175,6 +175,7 @@ export interface IBleSensor extends EventEmitter {
|
|
|
175
175
|
stopSensor(): Promise<boolean>;
|
|
176
176
|
reset(): void;
|
|
177
177
|
isConnected(): boolean;
|
|
178
|
+
isReady(): boolean;
|
|
178
179
|
read(characteristicUUID: string): Promise<Buffer>;
|
|
179
180
|
write(characteristicUUID: string, data: Buffer, options?: BleWriteProps): Promise<Buffer>;
|
|
180
181
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { BleFmAdapter } from '../fm/index.js';
|
|
2
2
|
import { BleDeviceProperties, BleDeviceSettings, IBlePeripheral } from '../types.js';
|
|
3
|
-
import { IAdapter } from '../../types/index.js';
|
|
3
|
+
import { IAdapter, IncyclistBikeData } from '../../types/index.js';
|
|
4
4
|
import { LegacyProfile } from '../../antv2/types.js';
|
|
5
|
+
import { IndoorBikeData } from './types.js';
|
|
5
6
|
export default class BleWahooAdapter extends BleFmAdapter {
|
|
6
7
|
protected static INCYCLIST_PROFILE_NAME: LegacyProfile;
|
|
7
8
|
constructor(settings: BleDeviceSettings, props?: BleDeviceProperties);
|
|
8
9
|
isSame(device: IAdapter): boolean;
|
|
9
10
|
getProfile(): LegacyProfile;
|
|
10
11
|
updateSensor(peripheral: IBlePeripheral): void;
|
|
12
|
+
mapData(deviceData: IndoorBikeData): IncyclistBikeData;
|
|
11
13
|
protected checkCapabilities(): Promise<void>;
|
|
12
14
|
}
|
|
@@ -18,6 +18,9 @@ export default class BleWahooDevice extends BleFitnessMachineDevice {
|
|
|
18
18
|
isSimMode: boolean;
|
|
19
19
|
isRequestControlBusy: boolean;
|
|
20
20
|
weight: number;
|
|
21
|
+
prevWheelRevolutions: number | undefined;
|
|
22
|
+
prevWheelEventTime: number | undefined;
|
|
23
|
+
wheelCircumference: number;
|
|
21
24
|
simModeSettings: {
|
|
22
25
|
weight: number;
|
|
23
26
|
crr: number;
|
|
@@ -46,4 +49,6 @@ export default class BleWahooDevice extends BleFitnessMachineDevice {
|
|
|
46
49
|
setSimWindResistance(cw: number): Promise<boolean>;
|
|
47
50
|
setSimGrade(slope: number): Promise<boolean>;
|
|
48
51
|
setSimWindSpeed(v: number): Promise<boolean>;
|
|
52
|
+
setWheelCircumference(wheelCircumference: number): Promise<void>;
|
|
53
|
+
getWheelCircumference(): number;
|
|
49
54
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { IAdapter, IncyclistBikeData } from "../types";
|
|
2
|
+
import { CyclingModeBase } from "./base";
|
|
3
|
+
import ICyclingMode, { CyclingModeProperyType, Settings, UpdateRequest } from "./types";
|
|
4
|
+
export default class SpeedCyclingMode extends CyclingModeBase implements ICyclingMode {
|
|
5
|
+
data: IncyclistBikeData;
|
|
6
|
+
prevUpdateTS: number;
|
|
7
|
+
prevRequest: UpdateRequest | undefined;
|
|
8
|
+
protected static config: {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
properties: ({
|
|
12
|
+
key: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
type: CyclingModeProperyType;
|
|
16
|
+
options: string[];
|
|
17
|
+
default: string;
|
|
18
|
+
} | {
|
|
19
|
+
key: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
type: CyclingModeProperyType;
|
|
23
|
+
default: number;
|
|
24
|
+
options?: undefined;
|
|
25
|
+
})[];
|
|
26
|
+
};
|
|
27
|
+
constructor(adapter: IAdapter, props?: Settings);
|
|
28
|
+
getData(): Partial<IncyclistBikeData>;
|
|
29
|
+
getSlope(): number;
|
|
30
|
+
getWeight(): number;
|
|
31
|
+
getTimeSinceLastUpdate(): number;
|
|
32
|
+
getBikeInitRequest(): UpdateRequest;
|
|
33
|
+
updateData(data: IncyclistBikeData, log?: boolean): IncyclistBikeData;
|
|
34
|
+
sendBikeUpdate(request: UpdateRequest): UpdateRequest;
|
|
35
|
+
protected calculatePowerAndDistance(speed: number, slope: number, m: number, t: number, props?: {}): {
|
|
36
|
+
power: number;
|
|
37
|
+
distance: number;
|
|
38
|
+
};
|
|
39
|
+
protected calculateSpeedAndDistance(power: number, slope: number, m: number, t: number, props?: {}): {
|
|
40
|
+
speed: number;
|
|
41
|
+
distance: number;
|
|
42
|
+
};
|
|
43
|
+
}
|