incyclist-devices 1.4.48 → 1.4.49
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/ble/ble-device.d.ts +10 -2
- package/lib/ble/ble-device.js +24 -4
- package/lib/ble/ble-erg-mode.d.ts +1 -8
- package/lib/ble/ble-erg-mode.js +9 -28
- package/lib/ble/ble-st-mode.d.ts +1 -10
- package/lib/ble/ble-st-mode.js +22 -71
- package/lib/ble/fm.d.ts +25 -2
- package/lib/ble/fm.js +232 -19
- package/lib/ble/hrm.js +1 -0
- package/lib/ble/pwr.js +1 -0
- package/lib/modes/power-base.js +0 -1
- package/package.json +1 -1
package/lib/ble/ble-device.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ interface BleDeviceConstructProps extends BleDeviceProps {
|
|
|
6
6
|
log?: boolean;
|
|
7
7
|
logger?: EventLogger;
|
|
8
8
|
}
|
|
9
|
+
declare type CommandQueueItem = {
|
|
10
|
+
uuid: string;
|
|
11
|
+
data: Buffer;
|
|
12
|
+
resolve: any;
|
|
13
|
+
reject: any;
|
|
14
|
+
timeout: any;
|
|
15
|
+
};
|
|
9
16
|
export declare abstract class BleDevice extends BleDeviceClass {
|
|
10
17
|
id: string;
|
|
11
18
|
address: string;
|
|
@@ -19,6 +26,7 @@ export declare abstract class BleDevice extends BleDeviceClass {
|
|
|
19
26
|
deviceInfo: BleDeviceInfo;
|
|
20
27
|
isInitialized: boolean;
|
|
21
28
|
subscribedCharacteristics: string[];
|
|
29
|
+
writeQueue: CommandQueueItem[];
|
|
22
30
|
constructor(props?: BleDeviceConstructProps);
|
|
23
31
|
logEvent(event: any): void;
|
|
24
32
|
setInterface(ble: BleInterfaceClass): void;
|
|
@@ -32,8 +40,8 @@ export declare abstract class BleDevice extends BleDeviceClass {
|
|
|
32
40
|
connect(props?: ConnectProps): Promise<boolean>;
|
|
33
41
|
disconnect(): Promise<boolean>;
|
|
34
42
|
abstract getProfile(): string;
|
|
35
|
-
|
|
36
|
-
write(characteristicUuid: string, data: Buffer, withoutResponse?: boolean): Promise<
|
|
43
|
+
onData(characteristic: string, data: Buffer): void;
|
|
44
|
+
write(characteristicUuid: string, data: Buffer, withoutResponse?: boolean): Promise<ArrayBuffer>;
|
|
37
45
|
read(characteristicUuid: string): Promise<Uint8Array>;
|
|
38
46
|
getDeviceInfo(): Promise<BleDeviceInfo>;
|
|
39
47
|
}
|
package/lib/ble/ble-device.js
CHANGED
|
@@ -26,6 +26,7 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
26
26
|
this.characteristics = [];
|
|
27
27
|
this.subscribedCharacteristics = [];
|
|
28
28
|
this.isInitialized = false;
|
|
29
|
+
this.writeQueue = [];
|
|
29
30
|
if (props.peripheral) {
|
|
30
31
|
const { id, address, advertisement, state } = props.peripheral;
|
|
31
32
|
this.peripheral = props.peripheral;
|
|
@@ -103,6 +104,7 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
103
104
|
return Promise.resolve(true);
|
|
104
105
|
return this.getDeviceInfo().then(() => {
|
|
105
106
|
this.emit('deviceInfo', this.deviceInfo);
|
|
107
|
+
this.logEvent(Object.assign({ message: 'ftms device init done' }, this.deviceInfo));
|
|
106
108
|
this.isInitialized = true;
|
|
107
109
|
return true;
|
|
108
110
|
});
|
|
@@ -235,9 +237,20 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
235
237
|
}
|
|
236
238
|
});
|
|
237
239
|
}
|
|
240
|
+
onData(characteristic, data) {
|
|
241
|
+
if (this.writeQueue.length > 0) {
|
|
242
|
+
const writeIdx = this.writeQueue.findIndex(i => i.uuid === characteristic.toLocaleLowerCase());
|
|
243
|
+
if (writeIdx !== -1) {
|
|
244
|
+
const writeItem = this.writeQueue[writeIdx];
|
|
245
|
+
this.writeQueue.splice(writeIdx, 1);
|
|
246
|
+
if (writeItem.resolve)
|
|
247
|
+
writeItem.resolve(data);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
238
251
|
write(characteristicUuid, data, withoutResponse = false) {
|
|
239
252
|
return __awaiter(this, void 0, void 0, function* () {
|
|
240
|
-
if (this.subscribedCharacteristics.find(c => c === characteristicUuid) === undefined) {
|
|
253
|
+
if (!withoutResponse && this.subscribedCharacteristics.find(c => c === characteristicUuid) === undefined) {
|
|
241
254
|
const connector = this.ble.getConnector(this.peripheral);
|
|
242
255
|
connector.on(characteristicUuid, (uuid, data) => {
|
|
243
256
|
this.onData(uuid, data);
|
|
@@ -251,11 +264,18 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
251
264
|
reject(new Error('Characteristic not found'));
|
|
252
265
|
return;
|
|
253
266
|
}
|
|
267
|
+
if (withoutResponse) {
|
|
268
|
+
characteristic.write(data, withoutResponse);
|
|
269
|
+
resolve(new ArrayBuffer(0));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const writeId = this.writeQueue.length;
|
|
273
|
+
this.writeQueue.push({ uuid: characteristicUuid.toLocaleLowerCase(), data, resolve, reject, timeout: Date.now() + 1000 });
|
|
254
274
|
characteristic.write(data, withoutResponse, (err) => {
|
|
255
|
-
if (err)
|
|
275
|
+
if (err) {
|
|
276
|
+
this.writeQueue.splice(writeId, 1);
|
|
256
277
|
reject(err);
|
|
257
|
-
|
|
258
|
-
resolve(true);
|
|
278
|
+
}
|
|
259
279
|
});
|
|
260
280
|
});
|
|
261
281
|
});
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import CyclingMode, { CyclingModeProperty, IncyclistBikeData, UpdateRequest } from "../CyclingMode";
|
|
2
2
|
import PowerBasedCyclingModeBase from "../modes/power-base";
|
|
3
3
|
import { FmAdapter } from "./fm";
|
|
4
|
-
export
|
|
5
|
-
rpmUpdated?: boolean;
|
|
6
|
-
gearUpdated?: boolean;
|
|
7
|
-
starting?: boolean;
|
|
8
|
-
tsStart?: number;
|
|
9
|
-
};
|
|
10
|
-
export default class ERGCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
|
|
4
|
+
export default class BleERGCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
|
|
11
5
|
prevRequest: UpdateRequest;
|
|
12
6
|
hasBikeUpdate: boolean;
|
|
13
7
|
chain: number[];
|
|
14
8
|
cassette: number[];
|
|
15
|
-
event: ERGEvent;
|
|
16
9
|
constructor(adapter: FmAdapter, props?: any);
|
|
17
10
|
getName(): string;
|
|
18
11
|
getDescription(): string;
|
package/lib/ble/ble-erg-mode.js
CHANGED
|
@@ -13,11 +13,10 @@ const config = {
|
|
|
13
13
|
{ key: 'startPower', name: 'Starting Power', description: 'Initial power in Watts at start of training', type: CyclingMode_1.CyclingModeProperyType.Integer, default: 50, min: 25, max: 800 },
|
|
14
14
|
]
|
|
15
15
|
};
|
|
16
|
-
class
|
|
16
|
+
class BleERGCyclingMode extends power_base_1.default {
|
|
17
17
|
constructor(adapter, props) {
|
|
18
18
|
super(adapter, props);
|
|
19
19
|
this.hasBikeUpdate = false;
|
|
20
|
-
this.event = {};
|
|
21
20
|
this.initLogger('ERGMode');
|
|
22
21
|
}
|
|
23
22
|
getName() {
|
|
@@ -34,7 +33,7 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
34
33
|
}
|
|
35
34
|
getBikeInitRequest() {
|
|
36
35
|
const startPower = this.getSetting('startPower');
|
|
37
|
-
return { targetPower: startPower };
|
|
36
|
+
return { slope: 0, targetPower: startPower };
|
|
38
37
|
}
|
|
39
38
|
sendBikeUpdate(request) {
|
|
40
39
|
const getData = () => {
|
|
@@ -43,25 +42,18 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
43
42
|
const { pedalRpm, slope, power, speed } = this.data;
|
|
44
43
|
return { pedalRpm, slope, power, speed };
|
|
45
44
|
};
|
|
46
|
-
this.logger.logEvent({ message: "processing update request", request, prev: this.prevRequest, data: getData()
|
|
45
|
+
this.logger.logEvent({ message: "processing update request", request, prev: this.prevRequest, data: getData() });
|
|
47
46
|
let newRequest = {};
|
|
48
47
|
try {
|
|
49
48
|
if (!request || request.reset || Object.keys(request).length === 0) {
|
|
50
49
|
this.prevRequest = {};
|
|
51
|
-
return request
|
|
50
|
+
return request.reset ? { reset: true } : {};
|
|
52
51
|
}
|
|
53
52
|
const prevData = this.data || {};
|
|
54
53
|
if (request.targetPower !== undefined) {
|
|
55
54
|
delete request.slope;
|
|
56
55
|
delete request.refresh;
|
|
57
56
|
}
|
|
58
|
-
if (this.event.starting && request.targetPower === undefined) {
|
|
59
|
-
newRequest.targetPower = this.getSetting('startPower');
|
|
60
|
-
if (this.event.tsStart && Date.now() - this.event.tsStart > 5000) {
|
|
61
|
-
delete this.event.starting;
|
|
62
|
-
delete this.event.tsStart;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
57
|
if (request.refresh) {
|
|
66
58
|
delete request.refresh;
|
|
67
59
|
newRequest.targetPower = this.prevRequest.targetPower;
|
|
@@ -70,14 +62,12 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
70
62
|
if (!this.data)
|
|
71
63
|
this.data = {};
|
|
72
64
|
this.data.slope = request.slope;
|
|
65
|
+
delete request.slope;
|
|
73
66
|
}
|
|
74
67
|
if (request.maxPower !== undefined && request.minPower !== undefined && request.maxPower === request.minPower) {
|
|
75
68
|
request.targetPower = request.maxPower;
|
|
76
|
-
}
|
|
77
|
-
if (request.targetPower !== undefined) {
|
|
78
69
|
newRequest.targetPower = request.targetPower;
|
|
79
70
|
}
|
|
80
|
-
delete request.slope;
|
|
81
71
|
if (request.maxPower !== undefined) {
|
|
82
72
|
if (newRequest.targetPower !== undefined && newRequest.targetPower > request.maxPower) {
|
|
83
73
|
newRequest.targetPower = request.maxPower;
|
|
@@ -89,9 +79,8 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
89
79
|
newRequest.targetPower = request.minPower;
|
|
90
80
|
}
|
|
91
81
|
newRequest.minPower = request.minPower;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
delete newRequest.targetPower;
|
|
82
|
+
if (prevData.power && prevData.power < request.minPower)
|
|
83
|
+
newRequest.targetPower = request.minPower;
|
|
95
84
|
}
|
|
96
85
|
this.prevRequest = JSON.parse(JSON.stringify(request));
|
|
97
86
|
}
|
|
@@ -106,11 +95,6 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
106
95
|
const prevRequest = this.prevRequest || {};
|
|
107
96
|
const data = this.data || {};
|
|
108
97
|
const bikeType = this.getSetting('bikeType').toLowerCase();
|
|
109
|
-
delete this.event.rpmUpdated;
|
|
110
|
-
if (prevData === {} || prevData.speed === undefined || prevData.speed === 0) {
|
|
111
|
-
this.event.starting = true;
|
|
112
|
-
this.event.tsStart = Date.now();
|
|
113
|
-
}
|
|
114
98
|
try {
|
|
115
99
|
const rpm = bikeData.pedalRpm || 0;
|
|
116
100
|
let power = bikeData.power || 0;
|
|
@@ -127,15 +111,12 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
127
111
|
data.distanceInternal = Math.round(distanceInternal + distance);
|
|
128
112
|
data.slope = slope;
|
|
129
113
|
data.pedalRpm = rpm;
|
|
130
|
-
if (data.time !== undefined &&
|
|
114
|
+
if (data.time !== undefined && data.speed > 0)
|
|
131
115
|
data.time += t;
|
|
132
116
|
else
|
|
133
117
|
data.time = 0;
|
|
134
118
|
data.heartrate = bikeData.heartrate;
|
|
135
119
|
data.isPedalling = bikeData.isPedalling;
|
|
136
|
-
if (rpm && rpm !== prevData.pedalRpm) {
|
|
137
|
-
this.event.rpmUpdated = true;
|
|
138
|
-
}
|
|
139
120
|
}
|
|
140
121
|
catch (err) {
|
|
141
122
|
this.logger.logEvent({ message: 'error', fn: 'updateData()', error: err.message || err });
|
|
@@ -145,4 +126,4 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
145
126
|
return data;
|
|
146
127
|
}
|
|
147
128
|
}
|
|
148
|
-
exports.default =
|
|
129
|
+
exports.default = BleERGCyclingMode;
|
package/lib/ble/ble-st-mode.d.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import CyclingMode, { CyclingModeProperty, IncyclistBikeData, UpdateRequest } from "../CyclingMode";
|
|
2
2
|
import PowerBasedCyclingModeBase from "../modes/power-base";
|
|
3
3
|
import { FmAdapter } from "./fm";
|
|
4
|
-
export
|
|
5
|
-
rpmUpdated?: boolean;
|
|
6
|
-
gearUpdated?: boolean;
|
|
7
|
-
starting?: boolean;
|
|
8
|
-
tsStart?: number;
|
|
9
|
-
};
|
|
10
|
-
export default class ERGCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
|
|
4
|
+
export default class FtmsCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
|
|
11
5
|
prevRequest: UpdateRequest;
|
|
12
6
|
hasBikeUpdate: boolean;
|
|
13
|
-
chain: number[];
|
|
14
|
-
cassette: number[];
|
|
15
|
-
event: ERGEvent;
|
|
16
7
|
constructor(adapter: FmAdapter, props?: any);
|
|
17
8
|
getName(): string;
|
|
18
9
|
getDescription(): string;
|
package/lib/ble/ble-st-mode.js
CHANGED
|
@@ -9,16 +9,14 @@ const config = {
|
|
|
9
9
|
name: "Smart Trainer",
|
|
10
10
|
description: "Calculates speed based on power and slope. Slope is set to the device",
|
|
11
11
|
properties: [
|
|
12
|
-
{ key: 'bikeType', name: 'Bike Type', description: '', type: CyclingMode_1.CyclingModeProperyType.SingleSelect, options: ['Race', 'Mountain', 'Triathlon'], default: 'Race' }
|
|
13
|
-
{ key: 'startPower', name: 'Starting Power', description: 'Initial power in Watts at start of training', type: CyclingMode_1.CyclingModeProperyType.Integer, default: 50, min: 25, max: 800 },
|
|
12
|
+
{ key: 'bikeType', name: 'Bike Type', description: '', type: CyclingMode_1.CyclingModeProperyType.SingleSelect, options: ['Race', 'Mountain', 'Triathlon'], default: 'Race' }
|
|
14
13
|
]
|
|
15
14
|
};
|
|
16
|
-
class
|
|
15
|
+
class FtmsCyclingMode extends power_base_1.default {
|
|
17
16
|
constructor(adapter, props) {
|
|
18
17
|
super(adapter, props);
|
|
19
18
|
this.hasBikeUpdate = false;
|
|
20
|
-
this.
|
|
21
|
-
this.initLogger('ERGMode');
|
|
19
|
+
this.initLogger('FtmsMode');
|
|
22
20
|
}
|
|
23
21
|
getName() {
|
|
24
22
|
return config.name;
|
|
@@ -33,71 +31,32 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
33
31
|
return config.properties.find(p => p.name === name);
|
|
34
32
|
}
|
|
35
33
|
getBikeInitRequest() {
|
|
36
|
-
|
|
37
|
-
return { targetPower: startPower };
|
|
34
|
+
return { slope: 0 };
|
|
38
35
|
}
|
|
39
36
|
sendBikeUpdate(request) {
|
|
40
37
|
const getData = () => {
|
|
41
38
|
if (!this.data)
|
|
42
39
|
return {};
|
|
43
|
-
const { pedalRpm, slope, power, speed } = this.data;
|
|
44
|
-
return { pedalRpm, slope, power, speed };
|
|
40
|
+
const { gear, pedalRpm, slope, power, speed } = this.data;
|
|
41
|
+
return { gear, pedalRpm, slope, power, speed };
|
|
45
42
|
};
|
|
46
|
-
|
|
43
|
+
const event = {};
|
|
44
|
+
if (this.data === undefined)
|
|
45
|
+
event.noData = true;
|
|
46
|
+
if (request.slope !== undefined && (event.noData || Math.abs(request.slope - this.data.slope) >= 0.1))
|
|
47
|
+
event.slopeUpdate = true;
|
|
48
|
+
if (this.prevRequest === undefined)
|
|
49
|
+
event.initialCall = true;
|
|
50
|
+
this.logger.logEvent({ message: "processing update request", request, prev: this.prevRequest, data: getData(), event });
|
|
47
51
|
let newRequest = {};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.prevRequest = {};
|
|
51
|
-
return request || {};
|
|
52
|
-
}
|
|
53
|
-
const prevData = this.data || {};
|
|
54
|
-
if (request.targetPower !== undefined) {
|
|
55
|
-
delete request.slope;
|
|
56
|
-
delete request.refresh;
|
|
57
|
-
}
|
|
58
|
-
if (this.event.starting && request.targetPower === undefined) {
|
|
59
|
-
newRequest.targetPower = this.getSetting('startPower');
|
|
60
|
-
if (this.event.tsStart && Date.now() - this.event.tsStart > 5000) {
|
|
61
|
-
delete this.event.starting;
|
|
62
|
-
delete this.event.tsStart;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (request.refresh) {
|
|
66
|
-
delete request.refresh;
|
|
67
|
-
newRequest.targetPower = this.prevRequest.targetPower;
|
|
68
|
-
}
|
|
69
|
-
if (request.slope !== undefined) {
|
|
70
|
-
if (!this.data)
|
|
71
|
-
this.data = {};
|
|
72
|
-
this.data.slope = request.slope;
|
|
73
|
-
}
|
|
74
|
-
if (request.maxPower !== undefined && request.minPower !== undefined && request.maxPower === request.minPower) {
|
|
75
|
-
request.targetPower = request.maxPower;
|
|
76
|
-
}
|
|
77
|
-
if (request.targetPower !== undefined) {
|
|
78
|
-
newRequest.targetPower = request.targetPower;
|
|
79
|
-
}
|
|
80
|
-
delete request.slope;
|
|
81
|
-
if (request.maxPower !== undefined) {
|
|
82
|
-
if (newRequest.targetPower !== undefined && newRequest.targetPower > request.maxPower) {
|
|
83
|
-
newRequest.targetPower = request.maxPower;
|
|
84
|
-
}
|
|
85
|
-
newRequest.maxPower = request.maxPower;
|
|
86
|
-
}
|
|
87
|
-
if (request.minPower !== undefined) {
|
|
88
|
-
if (newRequest.targetPower !== undefined && newRequest.targetPower < request.minPower) {
|
|
89
|
-
newRequest.targetPower = request.minPower;
|
|
90
|
-
}
|
|
91
|
-
newRequest.minPower = request.minPower;
|
|
92
|
-
}
|
|
93
|
-
if (newRequest.targetPower !== undefined && prevData.power !== undefined && newRequest.targetPower === prevData.power) {
|
|
94
|
-
delete newRequest.targetPower;
|
|
95
|
-
}
|
|
96
|
-
this.prevRequest = JSON.parse(JSON.stringify(request));
|
|
52
|
+
if (request.slope === undefined && request.refresh && this.prevRequest) {
|
|
53
|
+
return this.prevRequest;
|
|
97
54
|
}
|
|
98
|
-
|
|
99
|
-
|
|
55
|
+
if (request.slope !== undefined) {
|
|
56
|
+
newRequest.slope = parseFloat(request.slope.toFixed(1));
|
|
57
|
+
this.data.slope = newRequest.slope;
|
|
100
58
|
}
|
|
59
|
+
this.prevRequest = JSON.parse(JSON.stringify(newRequest));
|
|
101
60
|
return newRequest;
|
|
102
61
|
}
|
|
103
62
|
updateData(bikeData) {
|
|
@@ -106,11 +65,6 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
106
65
|
const prevRequest = this.prevRequest || {};
|
|
107
66
|
const data = this.data || {};
|
|
108
67
|
const bikeType = this.getSetting('bikeType').toLowerCase();
|
|
109
|
-
delete this.event.rpmUpdated;
|
|
110
|
-
if (prevData === {} || prevData.speed === undefined || prevData.speed === 0) {
|
|
111
|
-
this.event.starting = true;
|
|
112
|
-
this.event.tsStart = Date.now();
|
|
113
|
-
}
|
|
114
68
|
try {
|
|
115
69
|
const rpm = bikeData.pedalRpm || 0;
|
|
116
70
|
let power = bikeData.power || 0;
|
|
@@ -127,15 +81,12 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
127
81
|
data.distanceInternal = Math.round(distanceInternal + distance);
|
|
128
82
|
data.slope = slope;
|
|
129
83
|
data.pedalRpm = rpm;
|
|
130
|
-
if (data.time !== undefined
|
|
84
|
+
if (data.time !== undefined)
|
|
131
85
|
data.time += t;
|
|
132
86
|
else
|
|
133
87
|
data.time = 0;
|
|
134
88
|
data.heartrate = bikeData.heartrate;
|
|
135
89
|
data.isPedalling = bikeData.isPedalling;
|
|
136
|
-
if (rpm && rpm !== prevData.pedalRpm) {
|
|
137
|
-
this.event.rpmUpdated = true;
|
|
138
|
-
}
|
|
139
90
|
}
|
|
140
91
|
catch (err) {
|
|
141
92
|
this.logger.logEvent({ message: 'error', fn: 'updateData()', error: err.message || err });
|
|
@@ -145,4 +96,4 @@ class ERGCyclingMode extends power_base_1.default {
|
|
|
145
96
|
return data;
|
|
146
97
|
}
|
|
147
98
|
}
|
|
148
|
-
exports.default =
|
|
99
|
+
exports.default = FtmsCyclingMode;
|
package/lib/ble/fm.d.ts
CHANGED
|
@@ -31,6 +31,9 @@ declare type IndoorBikeData = {
|
|
|
31
31
|
time?: number;
|
|
32
32
|
remainingTime?: number;
|
|
33
33
|
raw?: string;
|
|
34
|
+
targetPower?: number;
|
|
35
|
+
targetInclination?: number;
|
|
36
|
+
status?: string;
|
|
34
37
|
};
|
|
35
38
|
declare type IndoorBikeFeatures = {
|
|
36
39
|
fitnessMachine: number;
|
|
@@ -43,6 +46,10 @@ export default class BleFitnessMachineDevice extends BleDevice {
|
|
|
43
46
|
features: IndoorBikeFeatures;
|
|
44
47
|
hasControl: boolean;
|
|
45
48
|
isCPSubscribed: boolean;
|
|
49
|
+
crr: number;
|
|
50
|
+
cw: number;
|
|
51
|
+
windSpeed: number;
|
|
52
|
+
wheelSize: number;
|
|
46
53
|
constructor(props?: any);
|
|
47
54
|
init(): Promise<boolean>;
|
|
48
55
|
onDisconnect(): void;
|
|
@@ -52,11 +59,25 @@ export default class BleFitnessMachineDevice extends BleDevice {
|
|
|
52
59
|
isPower(): boolean;
|
|
53
60
|
isHrm(): boolean;
|
|
54
61
|
parseHrm(_data: Uint8Array): IndoorBikeData;
|
|
62
|
+
setCrr(crr: number): void;
|
|
63
|
+
getCrr(): number;
|
|
64
|
+
setCw(cw: number): void;
|
|
65
|
+
getCw(): number;
|
|
66
|
+
setWindSpeed(windSpeed: number): void;
|
|
67
|
+
getWindSpeed(): number;
|
|
55
68
|
parseIndoorBikeData(_data: Uint8Array): IndoorBikeData;
|
|
69
|
+
parseFitnessMachineStatus(_data: Uint8Array): IndoorBikeData;
|
|
56
70
|
getFitnessMachineFeatures(): Promise<IndoorBikeFeatures>;
|
|
57
71
|
onData(characteristic: string, data: Buffer): void;
|
|
72
|
+
writeFtmsMessage(requestedOpCode: any, data: any): Promise<number>;
|
|
58
73
|
requestControl(): Promise<boolean>;
|
|
59
|
-
setTargetPower(power: number): Promise<
|
|
74
|
+
setTargetPower(power: number): Promise<boolean>;
|
|
75
|
+
setSlope(slope: any): Promise<boolean>;
|
|
76
|
+
setTargetInclination(inclination: number): Promise<boolean>;
|
|
77
|
+
setIndoorBikeSimulation(windSpeed: number, gradient: number, crr: number, cw: number): Promise<boolean>;
|
|
78
|
+
startRequest(): Promise<boolean>;
|
|
79
|
+
stopRequest(): Promise<boolean>;
|
|
80
|
+
PauseRequest(): Promise<boolean>;
|
|
60
81
|
reset(): void;
|
|
61
82
|
}
|
|
62
83
|
export declare class FmAdapter extends DeviceAdapter {
|
|
@@ -66,7 +87,7 @@ export declare class FmAdapter extends DeviceAdapter {
|
|
|
66
87
|
protocol: DeviceProtocol;
|
|
67
88
|
paused: boolean;
|
|
68
89
|
logger: EventLogger;
|
|
69
|
-
|
|
90
|
+
cyclingMode: CyclingMode;
|
|
70
91
|
distanceInternal: number;
|
|
71
92
|
prevDataTS: number;
|
|
72
93
|
constructor(device: BleDeviceClass, protocol: BleProtocol);
|
|
@@ -77,6 +98,8 @@ export declare class FmAdapter extends DeviceAdapter {
|
|
|
77
98
|
getProfile(): string;
|
|
78
99
|
getName(): string;
|
|
79
100
|
getDisplayName(): string;
|
|
101
|
+
getSupportedCyclingModes(): Array<any>;
|
|
102
|
+
setCyclingMode(mode: string | CyclingMode, settings?: any): void;
|
|
80
103
|
getCyclingMode(): CyclingMode;
|
|
81
104
|
getDefaultCyclingMode(): CyclingMode;
|
|
82
105
|
getPort(): string;
|
package/lib/ble/fm.js
CHANGED
|
@@ -18,7 +18,15 @@ const ble_interface_1 = __importDefault(require("./ble-interface"));
|
|
|
18
18
|
const Device_1 = __importDefault(require("../Device"));
|
|
19
19
|
const gd_eventlog_1 = require("gd-eventlog");
|
|
20
20
|
const power_meter_1 = __importDefault(require("../modes/power-meter"));
|
|
21
|
+
const ble_st_mode_1 = __importDefault(require("./ble-st-mode"));
|
|
22
|
+
const ble_erg_mode_1 = __importDefault(require("./ble-erg-mode"));
|
|
21
23
|
const FTMS_CP = '2ad9';
|
|
24
|
+
const cwABike = {
|
|
25
|
+
race: 0.35,
|
|
26
|
+
triathlon: 0.29,
|
|
27
|
+
mountain: 0.57
|
|
28
|
+
};
|
|
29
|
+
const cRR = 0.0036;
|
|
22
30
|
const bit = (nr) => (1 << nr);
|
|
23
31
|
const IndoorBikeDataFlag = {
|
|
24
32
|
MoreData: bit(0),
|
|
@@ -79,6 +87,10 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
79
87
|
this.features = undefined;
|
|
80
88
|
this.hasControl = false;
|
|
81
89
|
this.isCPSubscribed = false;
|
|
90
|
+
this.crr = 0.0033;
|
|
91
|
+
this.cw = 0.6;
|
|
92
|
+
this.windSpeed = 0;
|
|
93
|
+
this.wheelSize = 2100;
|
|
82
94
|
this.data = {};
|
|
83
95
|
}
|
|
84
96
|
init() {
|
|
@@ -138,6 +150,12 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
138
150
|
}
|
|
139
151
|
return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
|
|
140
152
|
}
|
|
153
|
+
setCrr(crr) { this.crr = crr; }
|
|
154
|
+
getCrr() { return this.crr; }
|
|
155
|
+
setCw(cw) { this.cw = cw; }
|
|
156
|
+
getCw() { return this.cw; }
|
|
157
|
+
setWindSpeed(windSpeed) { this.windSpeed = windSpeed; }
|
|
158
|
+
getWindSpeed() { return this.windSpeed; }
|
|
141
159
|
parseIndoorBikeData(_data) {
|
|
142
160
|
const data = Buffer.from(_data);
|
|
143
161
|
const flags = data.readUInt16LE(0);
|
|
@@ -163,7 +181,7 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
163
181
|
offset += 1;
|
|
164
182
|
const dvHigh = data.readUInt16LE(offset);
|
|
165
183
|
offset += 2;
|
|
166
|
-
this.data.totalDistance = dvHigh << 8 + dvLow;
|
|
184
|
+
this.data.totalDistance = (dvHigh << 8) + dvLow;
|
|
167
185
|
}
|
|
168
186
|
if (flags & IndoorBikeDataFlag.ResistanceLevelPresent) {
|
|
169
187
|
this.data.resistanceLevel = data.readInt16LE(offset);
|
|
@@ -199,6 +217,43 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
199
217
|
}
|
|
200
218
|
return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
|
|
201
219
|
}
|
|
220
|
+
parseFitnessMachineStatus(_data) {
|
|
221
|
+
const data = Buffer.from(_data);
|
|
222
|
+
const OpCode = data.readUInt8(0);
|
|
223
|
+
switch (OpCode) {
|
|
224
|
+
case 8:
|
|
225
|
+
this.data.targetPower = data.readInt16LE(1);
|
|
226
|
+
break;
|
|
227
|
+
case 6:
|
|
228
|
+
this.data.targetInclination = data.readInt16LE(1) / 10;
|
|
229
|
+
break;
|
|
230
|
+
case 4:
|
|
231
|
+
this.data.status = "STARTED";
|
|
232
|
+
break;
|
|
233
|
+
case 3:
|
|
234
|
+
case 2:
|
|
235
|
+
this.data.status = "STOPPED";
|
|
236
|
+
break;
|
|
237
|
+
case 20:
|
|
238
|
+
const spinDownStatus = data.readUInt8(1);
|
|
239
|
+
switch (spinDownStatus) {
|
|
240
|
+
case 1:
|
|
241
|
+
this.data.status = "SPIN DOWN REQUESTED";
|
|
242
|
+
break;
|
|
243
|
+
case 2:
|
|
244
|
+
this.data.status = "SPIN DOWN SUCCESS";
|
|
245
|
+
break;
|
|
246
|
+
case 3:
|
|
247
|
+
this.data.status = "SPIN DOWN ERROR";
|
|
248
|
+
break;
|
|
249
|
+
case 4:
|
|
250
|
+
this.data.status = "STOP PEDALING";
|
|
251
|
+
break;
|
|
252
|
+
default: break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
|
|
256
|
+
}
|
|
202
257
|
getFitnessMachineFeatures() {
|
|
203
258
|
return __awaiter(this, void 0, void 0, function* () {
|
|
204
259
|
if (this.features)
|
|
@@ -218,14 +273,42 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
218
273
|
});
|
|
219
274
|
}
|
|
220
275
|
onData(characteristic, data) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
276
|
+
super.onData(characteristic, data);
|
|
277
|
+
const uuid = characteristic.toLocaleLowerCase();
|
|
278
|
+
let res = undefined;
|
|
279
|
+
switch (uuid) {
|
|
280
|
+
case '2ad2':
|
|
281
|
+
res = this.parseIndoorBikeData(data);
|
|
282
|
+
break;
|
|
283
|
+
case '2a37':
|
|
284
|
+
res = this.parseHrm(data);
|
|
285
|
+
break;
|
|
286
|
+
case '2ada':
|
|
287
|
+
res = this.parseFitnessMachineStatus(data);
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
break;
|
|
224
291
|
}
|
|
225
|
-
if (
|
|
226
|
-
const res = this.parseHrm(data);
|
|
292
|
+
if (res)
|
|
227
293
|
this.emit('data', res);
|
|
228
|
-
|
|
294
|
+
}
|
|
295
|
+
writeFtmsMessage(requestedOpCode, data) {
|
|
296
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
297
|
+
try {
|
|
298
|
+
const res = yield this.write(FTMS_CP, data);
|
|
299
|
+
const responseData = Buffer.from(res);
|
|
300
|
+
const opCode = responseData.readUInt8(0);
|
|
301
|
+
const request = responseData.readUInt8(1);
|
|
302
|
+
const result = responseData.readUInt8(2);
|
|
303
|
+
if (opCode !== 128 || request !== requestedOpCode)
|
|
304
|
+
throw new Error('Illegal response ');
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
this.logEvent({ message: 'writeFtmsMessage failed', opCode: requestedOpCode, reason: err.message });
|
|
309
|
+
return 4;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
229
312
|
}
|
|
230
313
|
requestControl() {
|
|
231
314
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -233,21 +316,109 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
|
233
316
|
return true;
|
|
234
317
|
const data = Buffer.alloc(1);
|
|
235
318
|
data.writeUInt8(0, 0);
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
319
|
+
const res = yield this.writeFtmsMessage(0, data);
|
|
320
|
+
if (res === 1) {
|
|
238
321
|
this.hasControl = true;
|
|
322
|
+
}
|
|
239
323
|
return this.hasControl;
|
|
240
324
|
});
|
|
241
325
|
}
|
|
242
326
|
setTargetPower(power) {
|
|
243
327
|
return __awaiter(this, void 0, void 0, function* () {
|
|
328
|
+
this.logEvent({ message: 'setTargetPower', power, skip: (this.data.targetPower !== undefined && this.data.targetPower === power) });
|
|
329
|
+
if (this.data.targetPower !== undefined && this.data.targetPower === power)
|
|
330
|
+
return true;
|
|
244
331
|
const hasControl = yield this.requestControl();
|
|
245
|
-
if (!hasControl)
|
|
246
|
-
|
|
332
|
+
if (!hasControl) {
|
|
333
|
+
this.logEvent({ message: 'setTargetPower failed', reason: 'control is disabled' });
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
247
336
|
const data = Buffer.alloc(3);
|
|
248
337
|
data.writeUInt8(5, 0);
|
|
249
338
|
data.writeInt16LE(Math.round(power), 1);
|
|
250
|
-
const res = yield this.
|
|
339
|
+
const res = yield this.writeFtmsMessage(5, data);
|
|
340
|
+
return (res === 1);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
setSlope(slope) {
|
|
344
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
345
|
+
this.logEvent({ message: 'setSlope', slope });
|
|
346
|
+
const { windSpeed, crr, cw } = this;
|
|
347
|
+
return yield this.setIndoorBikeSimulation(windSpeed, slope, crr, cw);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
setTargetInclination(inclination) {
|
|
351
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
352
|
+
if (this.data.targetInclination !== undefined && this.data.targetInclination === inclination)
|
|
353
|
+
return true;
|
|
354
|
+
const hasControl = yield this.requestControl();
|
|
355
|
+
if (!hasControl) {
|
|
356
|
+
this.logEvent({ message: 'setTargetInclination failed', reason: 'control is disabled' });
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const data = Buffer.alloc(3);
|
|
360
|
+
data.writeUInt8(3, 0);
|
|
361
|
+
data.writeInt16LE(Math.round(inclination * 10), 1);
|
|
362
|
+
const res = yield this.writeFtmsMessage(3, data);
|
|
363
|
+
return (res === 1);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
setIndoorBikeSimulation(windSpeed, gradient, crr, cw) {
|
|
367
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
368
|
+
const hasControl = yield this.requestControl();
|
|
369
|
+
if (!hasControl) {
|
|
370
|
+
this.logEvent({ message: 'setTargetInclination failed', reason: 'control is disabled' });
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const data = Buffer.alloc(7);
|
|
374
|
+
data.writeUInt8(17, 0);
|
|
375
|
+
data.writeInt16LE(Math.round(windSpeed * 1000), 1);
|
|
376
|
+
data.writeInt16LE(Math.round(gradient * 100), 3);
|
|
377
|
+
data.writeUInt8(Math.round(crr * 10000), 5);
|
|
378
|
+
data.writeUInt8(Math.round(cw * 100), 6);
|
|
379
|
+
const res = yield this.writeFtmsMessage(17, data);
|
|
380
|
+
return (res === 1);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
startRequest() {
|
|
384
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
385
|
+
const hasControl = yield this.requestControl();
|
|
386
|
+
if (!hasControl) {
|
|
387
|
+
this.logEvent({ message: 'startRequest failed', reason: 'control is disabled' });
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const data = Buffer.alloc(1);
|
|
391
|
+
data.writeUInt8(7, 0);
|
|
392
|
+
const res = yield this.writeFtmsMessage(7, data);
|
|
393
|
+
return (res === 1);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
stopRequest() {
|
|
397
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
398
|
+
const hasControl = yield this.requestControl();
|
|
399
|
+
if (!hasControl) {
|
|
400
|
+
this.logEvent({ message: 'stopRequest failed', reason: 'control is disabled' });
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
const data = Buffer.alloc(2);
|
|
404
|
+
data.writeUInt8(8, 0);
|
|
405
|
+
data.writeUInt8(1, 1);
|
|
406
|
+
const res = yield this.writeFtmsMessage(8, data);
|
|
407
|
+
return (res === 1);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
PauseRequest() {
|
|
411
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
412
|
+
const hasControl = yield this.requestControl();
|
|
413
|
+
if (!hasControl) {
|
|
414
|
+
this.logEvent({ message: 'PauseRequest failed', reason: 'control is disabled' });
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const data = Buffer.alloc(2);
|
|
418
|
+
data.writeUInt8(8, 0);
|
|
419
|
+
data.writeUInt8(2, 1);
|
|
420
|
+
const res = yield this.writeFtmsMessage(8, data);
|
|
421
|
+
return (res === 1);
|
|
251
422
|
});
|
|
252
423
|
}
|
|
253
424
|
reset() {
|
|
@@ -266,7 +437,7 @@ class FmAdapter extends Device_1.default {
|
|
|
266
437
|
this.distanceInternal = 0;
|
|
267
438
|
this.device = device;
|
|
268
439
|
this.ble = protocol.ble;
|
|
269
|
-
this.
|
|
440
|
+
this.cyclingMode = this.getDefaultCyclingMode();
|
|
270
441
|
this.logger = new gd_eventlog_1.EventLogger('BLE-FM');
|
|
271
442
|
}
|
|
272
443
|
isBike() { return this.device.isBike(); }
|
|
@@ -287,13 +458,33 @@ class FmAdapter extends Device_1.default {
|
|
|
287
458
|
getDisplayName() {
|
|
288
459
|
return this.getName();
|
|
289
460
|
}
|
|
461
|
+
getSupportedCyclingModes() {
|
|
462
|
+
return [ble_st_mode_1.default, ble_erg_mode_1.default, power_meter_1.default];
|
|
463
|
+
}
|
|
464
|
+
setCyclingMode(mode, settings) {
|
|
465
|
+
let selectedMode;
|
|
466
|
+
if (typeof mode === 'string') {
|
|
467
|
+
const supported = this.getSupportedCyclingModes();
|
|
468
|
+
const CyclingModeClass = supported.find(M => { const m = new M(this); return m.getName() === mode; });
|
|
469
|
+
if (CyclingModeClass) {
|
|
470
|
+
this.cyclingMode = new CyclingModeClass(this, settings);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
selectedMode = this.getDefaultCyclingMode();
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
selectedMode = mode;
|
|
477
|
+
}
|
|
478
|
+
this.cyclingMode = selectedMode;
|
|
479
|
+
this.cyclingMode.setSettings(settings);
|
|
480
|
+
}
|
|
290
481
|
getCyclingMode() {
|
|
291
|
-
if (!this.
|
|
292
|
-
this.
|
|
293
|
-
return this.
|
|
482
|
+
if (!this.cyclingMode)
|
|
483
|
+
this.cyclingMode = this.getDefaultCyclingMode();
|
|
484
|
+
return this.cyclingMode;
|
|
294
485
|
}
|
|
295
486
|
getDefaultCyclingMode() {
|
|
296
|
-
return new
|
|
487
|
+
return new ble_st_mode_1.default(this);
|
|
297
488
|
}
|
|
298
489
|
getPort() {
|
|
299
490
|
return 'ble';
|
|
@@ -363,6 +554,22 @@ class FmAdapter extends Device_1.default {
|
|
|
363
554
|
const bleDevice = yield this.ble.connectDevice(this.device);
|
|
364
555
|
if (bleDevice) {
|
|
365
556
|
this.device = bleDevice;
|
|
557
|
+
const mode = this.getCyclingMode();
|
|
558
|
+
if (mode && mode.getSetting('bikeType')) {
|
|
559
|
+
const bikeType = mode.getSetting('bikeType').toLowerCase();
|
|
560
|
+
this.device.setCrr(cRR);
|
|
561
|
+
switch (bikeType) {
|
|
562
|
+
case 'race':
|
|
563
|
+
this.device.setCw(cwABike.race);
|
|
564
|
+
break;
|
|
565
|
+
case 'triathlon':
|
|
566
|
+
this.device.setCw(cwABike.triathlon);
|
|
567
|
+
break;
|
|
568
|
+
case 'mountain':
|
|
569
|
+
this.device.setCw(cwABike.mountain);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
366
573
|
bleDevice.on('data', (data) => {
|
|
367
574
|
this.onDeviceData(data);
|
|
368
575
|
});
|
|
@@ -385,9 +592,15 @@ class FmAdapter extends Device_1.default {
|
|
|
385
592
|
}
|
|
386
593
|
sendUpdate(request) {
|
|
387
594
|
return __awaiter(this, void 0, void 0, function* () {
|
|
388
|
-
if (this.paused)
|
|
595
|
+
if (this.paused || !this.device)
|
|
389
596
|
return;
|
|
390
|
-
this.getCyclingMode().sendBikeUpdate(request);
|
|
597
|
+
const requested = this.getCyclingMode().sendBikeUpdate(request);
|
|
598
|
+
if (requested.slope !== undefined) {
|
|
599
|
+
yield this.device.setSlope(requested.slope);
|
|
600
|
+
}
|
|
601
|
+
if (requested.targetPower !== undefined) {
|
|
602
|
+
yield this.device.setTargetPower(requested.targetPower);
|
|
603
|
+
}
|
|
391
604
|
});
|
|
392
605
|
}
|
|
393
606
|
pause() { this.paused = true; return Promise.resolve(true); }
|
package/lib/ble/hrm.js
CHANGED
|
@@ -51,6 +51,7 @@ class BleHrmDevice extends ble_device_1.BleDevice {
|
|
|
51
51
|
return { heartrate, rr, raw: data.toString('hex') };
|
|
52
52
|
}
|
|
53
53
|
onData(characteristic, data) {
|
|
54
|
+
super.onData(characteristic, data);
|
|
54
55
|
if (characteristic.toLocaleLowerCase() === '2a37') {
|
|
55
56
|
const res = this.parseHrm(data);
|
|
56
57
|
this.emit('data', res);
|
package/lib/ble/pwr.js
CHANGED
|
@@ -109,6 +109,7 @@ class BleCyclingPowerDevice extends ble_device_1.BleDevice {
|
|
|
109
109
|
return { instantaneousPower, balance, accTorque, rpm, time, raw: data.toString('hex') };
|
|
110
110
|
}
|
|
111
111
|
onData(characteristic, data) {
|
|
112
|
+
super.onData(characteristic, data);
|
|
112
113
|
if (characteristic.toLocaleLowerCase() === '2a63') {
|
|
113
114
|
const res = this.parsePower(data);
|
|
114
115
|
this.emit('data', res);
|
package/lib/modes/power-base.js
CHANGED
|
@@ -37,7 +37,6 @@ class PowerBasedCyclingModeBase extends CyclingMode_1.CyclingModeBase {
|
|
|
37
37
|
let powerToMaintainSpeed = calculations_1.default.calculatePower(m, vPrev, slope, props);
|
|
38
38
|
const powerDelta = powerToMaintainSpeed - power;
|
|
39
39
|
const Ekin = EkinPrev - powerDelta * t;
|
|
40
|
-
console.log('~~~~ calculateSpeedAndDistance', Ekin, powerToMaintainSpeed, power, m, vPrev, slope);
|
|
41
40
|
if (Ekin > 0) {
|
|
42
41
|
const v = Math.sqrt(2 * Ekin / m);
|
|
43
42
|
const speed = v * 3.6;
|