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
|
@@ -160,8 +160,9 @@ export class BlePeripheral {
|
|
|
160
160
|
async _discoverServices() {
|
|
161
161
|
if (!this.getPeripheral())
|
|
162
162
|
return [];
|
|
163
|
+
const { name, address } = this.getInfo();
|
|
163
164
|
if (this.getPeripheral().discoverServicesAsync) {
|
|
164
|
-
this.logEvent({ message: 'discover services',
|
|
165
|
+
this.logEvent({ message: 'discover services', name, address });
|
|
165
166
|
const peripheral = this.getPeripheral();
|
|
166
167
|
let services = [];
|
|
167
168
|
if (peripheral?.discoverServicesAsync) {
|
|
@@ -169,14 +170,14 @@ export class BlePeripheral {
|
|
|
169
170
|
.catch(() => []);
|
|
170
171
|
}
|
|
171
172
|
this.discoveredServiceUUIds = services.map(s => beautifyUUID(s.uuid));
|
|
172
|
-
this.logEvent({ message: 'discover services result',
|
|
173
|
+
this.logEvent({ message: 'discover services result', name, address, services: this.discoveredServiceUUIds });
|
|
173
174
|
return services.map(s => s.uuid);
|
|
174
175
|
}
|
|
175
176
|
else {
|
|
176
|
-
this.logEvent({ message: 'discover services and characteristics',
|
|
177
|
+
this.logEvent({ message: 'discover services and characteristics', name, address });
|
|
177
178
|
const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], []);
|
|
178
179
|
this.discoveredServiceUUIds = res.services.map(s => beautifyUUID(s.uuid));
|
|
179
|
-
this.logEvent({ message: 'discover services result',
|
|
180
|
+
this.logEvent({ message: 'discover services result', name, address, services: this.discoveredServiceUUIds });
|
|
180
181
|
return res.services.map(s => s.uuid);
|
|
181
182
|
}
|
|
182
183
|
}
|
|
@@ -190,10 +191,12 @@ export class BlePeripheral {
|
|
|
190
191
|
async _discoverCharacteristics(serviceUUID) {
|
|
191
192
|
if (!this.getPeripheral())
|
|
192
193
|
return [];
|
|
193
|
-
|
|
194
|
+
const { name, address } = this.getInfo();
|
|
195
|
+
this.logEvent({ message: 'discover services and characteristics', name, address, service: serviceUUID });
|
|
194
196
|
const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([serviceUUID], [])
|
|
195
197
|
.catch(() => ({ services: [], characteristics: [] }));
|
|
196
198
|
res.characteristics.forEach(c => this.characteristics[beautifyUUID(c.uuid)] = c);
|
|
199
|
+
this.logEvent({ message: 'discover services and characteristics result', name, address, service: serviceUUID });
|
|
197
200
|
return res.characteristics.map(c => {
|
|
198
201
|
const { uuid, properties, name, _serviceUuid } = c;
|
|
199
202
|
return { uuid, properties, name, _serviceUuid };
|
|
@@ -315,6 +318,15 @@ export class BlePeripheral {
|
|
|
315
318
|
if (!success)
|
|
316
319
|
retry.push(c);
|
|
317
320
|
}
|
|
321
|
+
else {
|
|
322
|
+
const uuid = beautifyUUID(element);
|
|
323
|
+
if (c?.properties) {
|
|
324
|
+
this.logEvent({ message: 'cannot subscribe', uuid, reason: 'invalid type', properties: c.properties.join('|') });
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
this.logEvent({ message: 'cannot subscribe', uuid, reason: 'not found' });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
318
330
|
}
|
|
319
331
|
for (const element of retry) {
|
|
320
332
|
const c = element;
|
|
@@ -329,12 +341,17 @@ export class BlePeripheral {
|
|
|
329
341
|
}
|
|
330
342
|
async discoverAllCharacteristics() {
|
|
331
343
|
try {
|
|
344
|
+
const { name, address } = this.getInfo();
|
|
345
|
+
this.logEvent({ message: 'discover all characteristics', name, address });
|
|
332
346
|
const res = await this.getPeripheral().discoverSomeServicesAndCharacteristicsAsync([], []);
|
|
333
347
|
const found = [];
|
|
348
|
+
const uuids = [];
|
|
334
349
|
res.characteristics.forEach(c => {
|
|
335
350
|
this.characteristics[beautifyUUID(c.uuid)] = c;
|
|
336
351
|
found.push(c.uuid);
|
|
352
|
+
uuids.push(beautifyUUID(c.uuid));
|
|
337
353
|
});
|
|
354
|
+
this.logEvent({ message: 'discover all characteristics result', name, address, uuids: uuids.join('|') });
|
|
338
355
|
return found;
|
|
339
356
|
}
|
|
340
357
|
catch (err) {
|
|
@@ -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 {};
|
|
@@ -79,7 +79,8 @@ export declare class BleInterface extends EventEmitter implements IBleInterface<
|
|
|
79
79
|
protected discoverPeripherals(): Promise<void>;
|
|
80
80
|
protected onPeripheralFound(peripheral: BleRawPeripheral): void;
|
|
81
81
|
protected checkForWahooEnhancement(announcement: BlePeripheralAnnouncement): boolean;
|
|
82
|
-
protected
|
|
82
|
+
protected processEnrichedAnnouncement(announcement: BlePeripheralAnnouncement): void;
|
|
83
|
+
protected checkForTacxEnhancement(announcement: BlePeripheralAnnouncement): boolean;
|
|
83
84
|
protected buildAnnouncement(peripheral: BleRawPeripheral): BlePeripheralAnnouncement;
|
|
84
85
|
protected updateWithServices(announcement: BlePeripheralAnnouncement): Promise<BlePeripheralAnnouncement>;
|
|
85
86
|
protected discoverServices(announcement: BlePeripheralAnnouncement): Promise<string[]>;
|
|
@@ -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
|
+
}
|