incyclist-devices 3.0.14 → 3.0.16
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/ble/base/interface.js +9 -2
- package/lib/cjs/ble/fm/adapter.js +5 -0
- package/lib/cjs/ble/zwift/play/sensor.js +52 -0
- package/lib/cjs/modes/antble-smarttrainer.js +15 -8
- package/lib/cjs/modes/types.js +2 -0
- package/lib/cjs/proto/zwift_hub.js +6 -5
- package/lib/esm/ble/base/interface.js +10 -3
- package/lib/esm/ble/fm/adapter.js +5 -0
- package/lib/esm/ble/zwift/play/sensor.js +53 -1
- package/lib/esm/modes/antble-smarttrainer.js +15 -8
- package/lib/esm/modes/types.js +2 -0
- package/lib/esm/proto/zwift_hub.js +6 -5
- package/lib/types/ble/zwift/play/sensor.d.ts +2 -0
- package/lib/types/modes/antble-smarttrainer.d.ts +9 -9
- package/lib/types/modes/base.d.ts +1 -1
- package/lib/types/modes/types.d.ts +2 -1
- package/lib/types/proto/zwift_hub.d.ts +1 -1
- package/lib/types/types/interface.d.ts +1 -1
- package/package.json +2 -1
|
@@ -98,10 +98,10 @@ class BleInterface extends node_events_1.EventEmitter {
|
|
|
98
98
|
await this.connectInternal();
|
|
99
99
|
}
|
|
100
100
|
async connect(reconnect) {
|
|
101
|
-
if (this.isAutoStart)
|
|
101
|
+
if (this.isAutoStart && !reconnect)
|
|
102
102
|
return this.isConnected();
|
|
103
103
|
else
|
|
104
|
-
return this.connectInternal(reconnect);
|
|
104
|
+
return await this.connectInternal(reconnect);
|
|
105
105
|
}
|
|
106
106
|
async connectInternal(reconnect) {
|
|
107
107
|
if (this.isConnected())
|
|
@@ -298,6 +298,11 @@ class BleInterface extends node_events_1.EventEmitter {
|
|
|
298
298
|
const { peripheral } = service;
|
|
299
299
|
if (peripheral.address === undefined || peripheral.address === '')
|
|
300
300
|
peripheral.address = peripheral.id || peripheral.name;
|
|
301
|
+
if (service.name === 'Zwift Ride' && service.serviceUUIDs.some(uuid => (0, utils_js_1.matches)(uuid, 'FC82'))) {
|
|
302
|
+
const protocol = 'zwift-play';
|
|
303
|
+
const { id, name, address } = (0, utils_js_1.getPeripheralInfo)(peripheral);
|
|
304
|
+
return { interface: BleInterface.INTERFACE_NAME, protocol, id, name, address };
|
|
305
|
+
}
|
|
301
306
|
const protocol = this.getAdapterFactory().getProtocol(service.serviceUUIDs);
|
|
302
307
|
const { id, name, address } = (0, utils_js_1.getPeripheralInfo)(peripheral);
|
|
303
308
|
return { interface: BleInterface.INTERFACE_NAME, protocol, id, name, address };
|
|
@@ -573,6 +578,8 @@ class BleInterface extends node_events_1.EventEmitter {
|
|
|
573
578
|
return false;
|
|
574
579
|
const found = service.serviceUUIDs.map(utils_js_1.parseUUID);
|
|
575
580
|
const expected = this.expectedServices.map(utils_js_1.parseUUID);
|
|
581
|
+
if (service.name.startsWith('Zwift'))
|
|
582
|
+
return true;
|
|
576
583
|
const supported = found.filter(uuid => expected.includes(uuid)) ?? [];
|
|
577
584
|
if (!supported.length) {
|
|
578
585
|
this.logEvent({ message: 'peripheral not supported', name: service.name, uuids: service.serviceUUIDs });
|
|
@@ -235,9 +235,11 @@ class BleFmAdapter extends adapter_js_1.default {
|
|
|
235
235
|
async checkCapabilities() {
|
|
236
236
|
const before = this.capabilities.join(',');
|
|
237
237
|
const sensor = this.getSensor();
|
|
238
|
+
let updateRequired = false;
|
|
238
239
|
if (!sensor.features) {
|
|
239
240
|
try {
|
|
240
241
|
await sensor.getFitnessMachineFeatures();
|
|
242
|
+
updateRequired = true;
|
|
241
243
|
}
|
|
242
244
|
catch (err) {
|
|
243
245
|
this.logEvent({ message: 'error getting fitness machine features', device: this.getName(), interface: this.getInterface(), error: err });
|
|
@@ -250,6 +252,9 @@ class BleFmAdapter extends adapter_js_1.default {
|
|
|
250
252
|
if (before !== after) {
|
|
251
253
|
this.logEvent({ message: 'device capabilities updated', name: this.getSettings().name, interface: this.getSettings().interface, capabilities: this.capabilities });
|
|
252
254
|
this.emit('device-info', this.getSettings(), { capabilities: this.capabilities });
|
|
255
|
+
updateRequired = true;
|
|
256
|
+
}
|
|
257
|
+
if (updateRequired) {
|
|
253
258
|
this.updateCyclingModeConfig();
|
|
254
259
|
}
|
|
255
260
|
}
|
|
@@ -19,6 +19,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
|
|
|
19
19
|
prevClickMessage;
|
|
20
20
|
upState;
|
|
21
21
|
downState;
|
|
22
|
+
rideKeyPadStates;
|
|
22
23
|
deviceType;
|
|
23
24
|
publicKey;
|
|
24
25
|
privateKey;
|
|
@@ -172,6 +173,9 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
|
|
|
172
173
|
else if (type === 0x03) {
|
|
173
174
|
this.onRidingData(message);
|
|
174
175
|
}
|
|
176
|
+
else if (type === 0x23) {
|
|
177
|
+
this.onRideKeyPadStatus(message);
|
|
178
|
+
}
|
|
175
179
|
else if (type === 0x2A) {
|
|
176
180
|
this.onTrainerResponse(message);
|
|
177
181
|
}
|
|
@@ -285,6 +289,53 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
|
|
|
285
289
|
this.logEvent({ message: 'Error', fn: 'onRidingData', error: err.message, stack: err.stack });
|
|
286
290
|
}
|
|
287
291
|
}
|
|
292
|
+
onRideKeyPadStatus(m) {
|
|
293
|
+
try {
|
|
294
|
+
const data = zwift_hub_js_1.RideKeyPadStatus.fromBinary(m);
|
|
295
|
+
const buttonNames = new Map([
|
|
296
|
+
[0x10, 'a'],
|
|
297
|
+
[0x20, 'b'],
|
|
298
|
+
[0x40, 'y'],
|
|
299
|
+
[0x80, 'z'],
|
|
300
|
+
[0x1000, 'r-shift-up'],
|
|
301
|
+
[0x2000, 'r-shift-down'],
|
|
302
|
+
[0x4000, 'ride-on'],
|
|
303
|
+
[0x8000, 'r-power']
|
|
304
|
+
]);
|
|
305
|
+
const buttonMap = data.buttonMap ?? 0;
|
|
306
|
+
const currentPresses = new Set();
|
|
307
|
+
buttonNames.forEach((name, bit) => {
|
|
308
|
+
const isPressed = (buttonMap & bit) === 0;
|
|
309
|
+
if (isPressed) {
|
|
310
|
+
currentPresses.add(bit);
|
|
311
|
+
const prevState = this.rideKeyPadStates.get(bit);
|
|
312
|
+
if (!prevState || !prevState.pressed) {
|
|
313
|
+
this.rideKeyPadStates.set(bit, { pressed: true, timestamp: Date.now() });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
this.rideKeyPadStates.forEach((state, bit) => {
|
|
318
|
+
if (!currentPresses.has(bit) && state.pressed) {
|
|
319
|
+
const keyName = buttonNames.get(bit);
|
|
320
|
+
const duration = Date.now() - state.timestamp;
|
|
321
|
+
this.emit('key-pressed', { key: keyName, duration, deviceType: this.deviceType });
|
|
322
|
+
this.rideKeyPadStates.set(bit, { pressed: false, timestamp: Date.now() });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
const pressedButtons = Array.from(currentPresses).map(bit => ({
|
|
326
|
+
bit: `0x${bit.toString(16).padStart(8, '0')}`,
|
|
327
|
+
name: buttonNames.get(bit)
|
|
328
|
+
}));
|
|
329
|
+
this.logEvent({
|
|
330
|
+
message: 'ride keypad status received',
|
|
331
|
+
buttonMap: `0x${buttonMap.toString(16)}`,
|
|
332
|
+
buttons: pressedButtons
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
this.logEvent({ message: 'Error', fn: 'onRideKeyPadStatus', error: err.message, stack: err.stack });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
288
339
|
onDeviceInformation(m) {
|
|
289
340
|
try {
|
|
290
341
|
const envelope = zwift_hub_js_1.DeviceDataEnvelope.fromBinary(m);
|
|
@@ -506,6 +557,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
|
|
|
506
557
|
this.isHubServiceActive = false;
|
|
507
558
|
delete this.initHubServicePromise;
|
|
508
559
|
delete this.prevHubSettings;
|
|
560
|
+
this.rideKeyPadStates = new Map();
|
|
509
561
|
}
|
|
510
562
|
getManufacturerData() {
|
|
511
563
|
const data = this.peripheral.getManufacturerData();
|
|
@@ -234,10 +234,13 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
|
|
|
234
234
|
const m = this.adapter?.getWeight() ?? 85;
|
|
235
235
|
const vCurrent = this.data.speed * 1000 / 3600;
|
|
236
236
|
const eKinCurrent = m * vCurrent * vCurrent / 2;
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
const cadence = this.data?.pedalRpm ?? 0;
|
|
238
|
+
const gear = this.gear ?? 1;
|
|
239
|
+
const simSlope = this.simSlope ?? 0;
|
|
240
|
+
if (cadence > 0) {
|
|
241
|
+
const virtualSpeed = (0, calculations_js_1.calculateVirtualSpeed)(cadence, this.gearRatios[gear - 1]) * 3.6;
|
|
239
242
|
const v = virtualSpeed / 3.6;
|
|
240
|
-
const newPower = calculations_js_1.default.calculatePower(m, virtualSpeed / 3.6,
|
|
243
|
+
const newPower = calculations_js_1.default.calculatePower(m, virtualSpeed / 3.6, simSlope);
|
|
241
244
|
const prevPower = this.data.power;
|
|
242
245
|
const prev = (this.prevSimPower ?? prevPower ?? 0);
|
|
243
246
|
const delta = newPower - prev;
|
|
@@ -253,7 +256,7 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
|
|
|
253
256
|
this.logEvent({ message: 'set simulated power (gear change)', target: this.simPower, gear: this.gear, simSlope: this.simSlope, routeSlope: this.data.slope, prevTarget: this.prevSimPower, actualPower: prevPower, newPower });
|
|
254
257
|
}
|
|
255
258
|
else if (changed === 'slope' || changed === 'cadence') {
|
|
256
|
-
const adjustTime = this.simSlope < 0 ? 5 : 3;
|
|
259
|
+
const adjustTime = (this.simSlope ?? 0) < 0 ? 5 : 3;
|
|
257
260
|
this.simPower = prev + delta / adjustTime;
|
|
258
261
|
this.logEvent({ message: `set simulated power (${changed} change)`, target: this.simPower, gear: this.gear, simSlope: this.simSlope, routeSlope: this.data.slope, prevTarget: this.prevSimPower, actualPower: prevPower, newPower });
|
|
259
262
|
}
|
|
@@ -344,9 +347,13 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
|
|
|
344
347
|
else {
|
|
345
348
|
if (this.gear === undefined) {
|
|
346
349
|
const initialGear = (0, utils_js_1.intVal)(this.getSetting('startGear'));
|
|
347
|
-
this.gear = initialGear + request.gearDelta;
|
|
350
|
+
this.gear = initialGear + (request.gearDelta ?? 0);
|
|
348
351
|
}
|
|
349
|
-
|
|
352
|
+
if (this.gear < 1)
|
|
353
|
+
this.gear = 1;
|
|
354
|
+
if (this.gear > this.gearRatios.length - 1)
|
|
355
|
+
this.gear = this.gearRatios.length - 1;
|
|
356
|
+
newRequest.gearRatio = this.gearRatios[this.gear - 1];
|
|
350
357
|
this.logEvent({ message: 'gear initialized', gear: this.gear, gearRatio: newRequest.gearRatio });
|
|
351
358
|
}
|
|
352
359
|
break;
|
|
@@ -356,10 +363,10 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
|
|
|
356
363
|
}
|
|
357
364
|
}
|
|
358
365
|
verifySimPower() {
|
|
359
|
-
if (this.simPower < 0) {
|
|
366
|
+
if ((this.simPower ?? 0) < 0) {
|
|
360
367
|
this.simPower = 0;
|
|
361
368
|
}
|
|
362
|
-
if (this.data.pedalRpm > 0 && this.simPower < MIN_POWER) {
|
|
369
|
+
if ((this.data.pedalRpm ?? 0) > 0 && (this.simPower ?? 0) < MIN_POWER) {
|
|
363
370
|
this.simPower = MIN_POWER;
|
|
364
371
|
}
|
|
365
372
|
if (!this.data.isPedalling) {
|
package/lib/cjs/modes/types.js
CHANGED
|
@@ -672,11 +672,12 @@ class RideKeyPadStatus$Type extends runtime_4.MessageType {
|
|
|
672
672
|
constructor() {
|
|
673
673
|
super("org.cagnulen.qdomyoszwift.RideKeyPadStatus", [
|
|
674
674
|
{ no: 1, name: "ButtonMap", kind: "scalar", jsonName: "ButtonMap", opt: true, T: 13 },
|
|
675
|
-
{ no:
|
|
675
|
+
{ no: 3, name: "AnalogButtons", kind: "message", jsonName: "AnalogButtons", repeat: 2, T: () => exports.RideAnalogKeyPress }
|
|
676
676
|
]);
|
|
677
677
|
}
|
|
678
678
|
create(value) {
|
|
679
679
|
const message = globalThis.Object.create((this.messagePrototype));
|
|
680
|
+
message.analogButtons = [];
|
|
680
681
|
if (value !== undefined)
|
|
681
682
|
(0, runtime_3.reflectionMergePartial)(this, message, value);
|
|
682
683
|
return message;
|
|
@@ -689,8 +690,8 @@ class RideKeyPadStatus$Type extends runtime_4.MessageType {
|
|
|
689
690
|
case 1:
|
|
690
691
|
message.buttonMap = reader.uint32();
|
|
691
692
|
break;
|
|
692
|
-
case
|
|
693
|
-
message.analogButtons
|
|
693
|
+
case 3:
|
|
694
|
+
message.analogButtons.push(exports.RideAnalogKeyPress.internalBinaryRead(reader, reader.uint32(), options));
|
|
694
695
|
break;
|
|
695
696
|
default:
|
|
696
697
|
let u = options.readUnknownField;
|
|
@@ -706,8 +707,8 @@ class RideKeyPadStatus$Type extends runtime_4.MessageType {
|
|
|
706
707
|
internalBinaryWrite(message, writer, options) {
|
|
707
708
|
if (message.buttonMap !== undefined)
|
|
708
709
|
writer.tag(1, runtime_1.WireType.Varint).uint32(message.buttonMap);
|
|
709
|
-
|
|
710
|
-
exports.
|
|
710
|
+
for (let i = 0; i < message.analogButtons.length; i++)
|
|
711
|
+
exports.RideAnalogKeyPress.internalBinaryWrite(message.analogButtons[i], writer.tag(3, runtime_1.WireType.LengthDelimited).fork(), options).join();
|
|
711
712
|
let u = options.writeUnknownFields;
|
|
712
713
|
if (u !== false)
|
|
713
714
|
(u == true ? runtime_2.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { EventLogger } from "gd-eventlog";
|
|
3
3
|
import { InteruptableTask } from "../../utils/task.js";
|
|
4
4
|
import { BlePeripheral } from "./peripheral.js";
|
|
5
|
-
import { beautifyUUID, getPeripheralInfo, parseUUID } from "../utils.js";
|
|
5
|
+
import { beautifyUUID, getPeripheralInfo, matches, parseUUID } from "../utils.js";
|
|
6
6
|
import { InterfaceFactory } from "./types.js";
|
|
7
7
|
import { BleAdapterFactory } from "../factories/index.js";
|
|
8
8
|
const BLE_EXPIRATION_TIMEOUT = 10 * 1000 * 60;
|
|
@@ -95,10 +95,10 @@ export class BleInterface extends EventEmitter {
|
|
|
95
95
|
await this.connectInternal();
|
|
96
96
|
}
|
|
97
97
|
async connect(reconnect) {
|
|
98
|
-
if (this.isAutoStart)
|
|
98
|
+
if (this.isAutoStart && !reconnect)
|
|
99
99
|
return this.isConnected();
|
|
100
100
|
else
|
|
101
|
-
return this.connectInternal(reconnect);
|
|
101
|
+
return await this.connectInternal(reconnect);
|
|
102
102
|
}
|
|
103
103
|
async connectInternal(reconnect) {
|
|
104
104
|
if (this.isConnected())
|
|
@@ -295,6 +295,11 @@ export class BleInterface extends EventEmitter {
|
|
|
295
295
|
const { peripheral } = service;
|
|
296
296
|
if (peripheral.address === undefined || peripheral.address === '')
|
|
297
297
|
peripheral.address = peripheral.id || peripheral.name;
|
|
298
|
+
if (service.name === 'Zwift Ride' && service.serviceUUIDs.some(uuid => matches(uuid, 'FC82'))) {
|
|
299
|
+
const protocol = 'zwift-play';
|
|
300
|
+
const { id, name, address } = getPeripheralInfo(peripheral);
|
|
301
|
+
return { interface: BleInterface.INTERFACE_NAME, protocol, id, name, address };
|
|
302
|
+
}
|
|
298
303
|
const protocol = this.getAdapterFactory().getProtocol(service.serviceUUIDs);
|
|
299
304
|
const { id, name, address } = getPeripheralInfo(peripheral);
|
|
300
305
|
return { interface: BleInterface.INTERFACE_NAME, protocol, id, name, address };
|
|
@@ -570,6 +575,8 @@ export class BleInterface extends EventEmitter {
|
|
|
570
575
|
return false;
|
|
571
576
|
const found = service.serviceUUIDs.map(parseUUID);
|
|
572
577
|
const expected = this.expectedServices.map(parseUUID);
|
|
578
|
+
if (service.name.startsWith('Zwift'))
|
|
579
|
+
return true;
|
|
573
580
|
const supported = found.filter(uuid => expected.includes(uuid)) ?? [];
|
|
574
581
|
if (!supported.length) {
|
|
575
582
|
this.logEvent({ message: 'peripheral not supported', name: service.name, uuids: service.serviceUUIDs });
|
|
@@ -230,9 +230,11 @@ export default class BleFmAdapter extends BleAdapter {
|
|
|
230
230
|
async checkCapabilities() {
|
|
231
231
|
const before = this.capabilities.join(',');
|
|
232
232
|
const sensor = this.getSensor();
|
|
233
|
+
let updateRequired = false;
|
|
233
234
|
if (!sensor.features) {
|
|
234
235
|
try {
|
|
235
236
|
await sensor.getFitnessMachineFeatures();
|
|
237
|
+
updateRequired = true;
|
|
236
238
|
}
|
|
237
239
|
catch (err) {
|
|
238
240
|
this.logEvent({ message: 'error getting fitness machine features', device: this.getName(), interface: this.getInterface(), error: err });
|
|
@@ -245,6 +247,9 @@ export default class BleFmAdapter extends BleAdapter {
|
|
|
245
247
|
if (before !== after) {
|
|
246
248
|
this.logEvent({ message: 'device capabilities updated', name: this.getSettings().name, interface: this.getSettings().interface, capabilities: this.capabilities });
|
|
247
249
|
this.emit('device-info', this.getSettings(), { capabilities: this.capabilities });
|
|
250
|
+
updateRequired = true;
|
|
251
|
+
}
|
|
252
|
+
if (updateRequired) {
|
|
248
253
|
this.updateCyclingModeConfig();
|
|
249
254
|
}
|
|
250
255
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClickKeyPadStatus, DeviceDataEnvelope, DeviceInformationContent, DeviceSettings, HubCommand, HubRequest, HubRidingData, Idle, PlayButtonStatus, TrainerResponse } from "../../../proto/zwift_hub.js";
|
|
1
|
+
import { ClickKeyPadStatus, DeviceDataEnvelope, DeviceInformationContent, DeviceSettings, HubCommand, HubRequest, HubRidingData, Idle, PlayButtonStatus, RideKeyPadStatus, TrainerResponse } from "../../../proto/zwift_hub.js";
|
|
2
2
|
import { TBleSensor } from "../../base/sensor.js";
|
|
3
3
|
import { beautifyUUID, fullUUID } from "../../utils.js";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
@@ -16,6 +16,7 @@ export class BleZwiftPlaySensor extends TBleSensor {
|
|
|
16
16
|
prevClickMessage;
|
|
17
17
|
upState;
|
|
18
18
|
downState;
|
|
19
|
+
rideKeyPadStates;
|
|
19
20
|
deviceType;
|
|
20
21
|
publicKey;
|
|
21
22
|
privateKey;
|
|
@@ -169,6 +170,9 @@ export class BleZwiftPlaySensor extends TBleSensor {
|
|
|
169
170
|
else if (type === 0x03) {
|
|
170
171
|
this.onRidingData(message);
|
|
171
172
|
}
|
|
173
|
+
else if (type === 0x23) {
|
|
174
|
+
this.onRideKeyPadStatus(message);
|
|
175
|
+
}
|
|
172
176
|
else if (type === 0x2A) {
|
|
173
177
|
this.onTrainerResponse(message);
|
|
174
178
|
}
|
|
@@ -282,6 +286,53 @@ export class BleZwiftPlaySensor extends TBleSensor {
|
|
|
282
286
|
this.logEvent({ message: 'Error', fn: 'onRidingData', error: err.message, stack: err.stack });
|
|
283
287
|
}
|
|
284
288
|
}
|
|
289
|
+
onRideKeyPadStatus(m) {
|
|
290
|
+
try {
|
|
291
|
+
const data = RideKeyPadStatus.fromBinary(m);
|
|
292
|
+
const buttonNames = new Map([
|
|
293
|
+
[0x10, 'a'],
|
|
294
|
+
[0x20, 'b'],
|
|
295
|
+
[0x40, 'y'],
|
|
296
|
+
[0x80, 'z'],
|
|
297
|
+
[0x1000, 'r-shift-up'],
|
|
298
|
+
[0x2000, 'r-shift-down'],
|
|
299
|
+
[0x4000, 'ride-on'],
|
|
300
|
+
[0x8000, 'r-power']
|
|
301
|
+
]);
|
|
302
|
+
const buttonMap = data.buttonMap ?? 0;
|
|
303
|
+
const currentPresses = new Set();
|
|
304
|
+
buttonNames.forEach((name, bit) => {
|
|
305
|
+
const isPressed = (buttonMap & bit) === 0;
|
|
306
|
+
if (isPressed) {
|
|
307
|
+
currentPresses.add(bit);
|
|
308
|
+
const prevState = this.rideKeyPadStates.get(bit);
|
|
309
|
+
if (!prevState || !prevState.pressed) {
|
|
310
|
+
this.rideKeyPadStates.set(bit, { pressed: true, timestamp: Date.now() });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
this.rideKeyPadStates.forEach((state, bit) => {
|
|
315
|
+
if (!currentPresses.has(bit) && state.pressed) {
|
|
316
|
+
const keyName = buttonNames.get(bit);
|
|
317
|
+
const duration = Date.now() - state.timestamp;
|
|
318
|
+
this.emit('key-pressed', { key: keyName, duration, deviceType: this.deviceType });
|
|
319
|
+
this.rideKeyPadStates.set(bit, { pressed: false, timestamp: Date.now() });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
const pressedButtons = Array.from(currentPresses).map(bit => ({
|
|
323
|
+
bit: `0x${bit.toString(16).padStart(8, '0')}`,
|
|
324
|
+
name: buttonNames.get(bit)
|
|
325
|
+
}));
|
|
326
|
+
this.logEvent({
|
|
327
|
+
message: 'ride keypad status received',
|
|
328
|
+
buttonMap: `0x${buttonMap.toString(16)}`,
|
|
329
|
+
buttons: pressedButtons
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
this.logEvent({ message: 'Error', fn: 'onRideKeyPadStatus', error: err.message, stack: err.stack });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
285
336
|
onDeviceInformation(m) {
|
|
286
337
|
try {
|
|
287
338
|
const envelope = DeviceDataEnvelope.fromBinary(m);
|
|
@@ -503,6 +554,7 @@ export class BleZwiftPlaySensor extends TBleSensor {
|
|
|
503
554
|
this.isHubServiceActive = false;
|
|
504
555
|
delete this.initHubServicePromise;
|
|
505
556
|
delete this.prevHubSettings;
|
|
557
|
+
this.rideKeyPadStates = new Map();
|
|
506
558
|
}
|
|
507
559
|
getManufacturerData() {
|
|
508
560
|
const data = this.peripheral.getManufacturerData();
|
|
@@ -196,10 +196,13 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
|
|
|
196
196
|
const m = this.adapter?.getWeight() ?? 85;
|
|
197
197
|
const vCurrent = this.data.speed * 1000 / 3600;
|
|
198
198
|
const eKinCurrent = m * vCurrent * vCurrent / 2;
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const cadence = this.data?.pedalRpm ?? 0;
|
|
200
|
+
const gear = this.gear ?? 1;
|
|
201
|
+
const simSlope = this.simSlope ?? 0;
|
|
202
|
+
if (cadence > 0) {
|
|
203
|
+
const virtualSpeed = calculateVirtualSpeed(cadence, this.gearRatios[gear - 1]) * 3.6;
|
|
201
204
|
const v = virtualSpeed / 3.6;
|
|
202
|
-
const newPower = calc.calculatePower(m, virtualSpeed / 3.6,
|
|
205
|
+
const newPower = calc.calculatePower(m, virtualSpeed / 3.6, simSlope);
|
|
203
206
|
const prevPower = this.data.power;
|
|
204
207
|
const prev = (this.prevSimPower ?? prevPower ?? 0);
|
|
205
208
|
const delta = newPower - prev;
|
|
@@ -215,7 +218,7 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
|
|
|
215
218
|
this.logEvent({ message: 'set simulated power (gear change)', target: this.simPower, gear: this.gear, simSlope: this.simSlope, routeSlope: this.data.slope, prevTarget: this.prevSimPower, actualPower: prevPower, newPower });
|
|
216
219
|
}
|
|
217
220
|
else if (changed === 'slope' || changed === 'cadence') {
|
|
218
|
-
const adjustTime = this.simSlope < 0 ? 5 : 3;
|
|
221
|
+
const adjustTime = (this.simSlope ?? 0) < 0 ? 5 : 3;
|
|
219
222
|
this.simPower = prev + delta / adjustTime;
|
|
220
223
|
this.logEvent({ message: `set simulated power (${changed} change)`, target: this.simPower, gear: this.gear, simSlope: this.simSlope, routeSlope: this.data.slope, prevTarget: this.prevSimPower, actualPower: prevPower, newPower });
|
|
221
224
|
}
|
|
@@ -306,9 +309,13 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
|
|
|
306
309
|
else {
|
|
307
310
|
if (this.gear === undefined) {
|
|
308
311
|
const initialGear = intVal(this.getSetting('startGear'));
|
|
309
|
-
this.gear = initialGear + request.gearDelta;
|
|
312
|
+
this.gear = initialGear + (request.gearDelta ?? 0);
|
|
310
313
|
}
|
|
311
|
-
|
|
314
|
+
if (this.gear < 1)
|
|
315
|
+
this.gear = 1;
|
|
316
|
+
if (this.gear > this.gearRatios.length - 1)
|
|
317
|
+
this.gear = this.gearRatios.length - 1;
|
|
318
|
+
newRequest.gearRatio = this.gearRatios[this.gear - 1];
|
|
312
319
|
this.logEvent({ message: 'gear initialized', gear: this.gear, gearRatio: newRequest.gearRatio });
|
|
313
320
|
}
|
|
314
321
|
break;
|
|
@@ -318,10 +325,10 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
|
|
|
318
325
|
}
|
|
319
326
|
}
|
|
320
327
|
verifySimPower() {
|
|
321
|
-
if (this.simPower < 0) {
|
|
328
|
+
if ((this.simPower ?? 0) < 0) {
|
|
322
329
|
this.simPower = 0;
|
|
323
330
|
}
|
|
324
|
-
if (this.data.pedalRpm > 0 && this.simPower < MIN_POWER) {
|
|
331
|
+
if ((this.data.pedalRpm ?? 0) > 0 && (this.simPower ?? 0) < MIN_POWER) {
|
|
325
332
|
this.simPower = MIN_POWER;
|
|
326
333
|
}
|
|
327
334
|
if (!this.data.isPedalling) {
|
package/lib/esm/modes/types.js
CHANGED
|
@@ -669,11 +669,12 @@ class RideKeyPadStatus$Type extends MessageType {
|
|
|
669
669
|
constructor() {
|
|
670
670
|
super("org.cagnulen.qdomyoszwift.RideKeyPadStatus", [
|
|
671
671
|
{ no: 1, name: "ButtonMap", kind: "scalar", jsonName: "ButtonMap", opt: true, T: 13 },
|
|
672
|
-
{ no:
|
|
672
|
+
{ no: 3, name: "AnalogButtons", kind: "message", jsonName: "AnalogButtons", repeat: 2, T: () => RideAnalogKeyPress }
|
|
673
673
|
]);
|
|
674
674
|
}
|
|
675
675
|
create(value) {
|
|
676
676
|
const message = globalThis.Object.create((this.messagePrototype));
|
|
677
|
+
message.analogButtons = [];
|
|
677
678
|
if (value !== undefined)
|
|
678
679
|
reflectionMergePartial(this, message, value);
|
|
679
680
|
return message;
|
|
@@ -686,8 +687,8 @@ class RideKeyPadStatus$Type extends MessageType {
|
|
|
686
687
|
case 1:
|
|
687
688
|
message.buttonMap = reader.uint32();
|
|
688
689
|
break;
|
|
689
|
-
case
|
|
690
|
-
message.analogButtons
|
|
690
|
+
case 3:
|
|
691
|
+
message.analogButtons.push(RideAnalogKeyPress.internalBinaryRead(reader, reader.uint32(), options));
|
|
691
692
|
break;
|
|
692
693
|
default:
|
|
693
694
|
let u = options.readUnknownField;
|
|
@@ -703,8 +704,8 @@ class RideKeyPadStatus$Type extends MessageType {
|
|
|
703
704
|
internalBinaryWrite(message, writer, options) {
|
|
704
705
|
if (message.buttonMap !== undefined)
|
|
705
706
|
writer.tag(1, WireType.Varint).uint32(message.buttonMap);
|
|
706
|
-
|
|
707
|
-
|
|
707
|
+
for (let i = 0; i < message.analogButtons.length; i++)
|
|
708
|
+
RideAnalogKeyPress.internalBinaryWrite(message.analogButtons[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join();
|
|
708
709
|
let u = options.writeUnknownFields;
|
|
709
710
|
if (u !== false)
|
|
710
711
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -27,6 +27,7 @@ export declare class BleZwiftPlaySensor extends TBleSensor {
|
|
|
27
27
|
protected prevClickMessage: string;
|
|
28
28
|
protected upState: ButtonState;
|
|
29
29
|
protected downState: ButtonState;
|
|
30
|
+
protected rideKeyPadStates: Map<number, ButtonState>;
|
|
30
31
|
protected deviceType: DeviceType;
|
|
31
32
|
protected publicKey: Buffer;
|
|
32
33
|
protected privateKey: Buffer;
|
|
@@ -56,6 +57,7 @@ export declare class BleZwiftPlaySensor extends TBleSensor {
|
|
|
56
57
|
sendHubCommand(command: HubCommand): Promise<Buffer<ArrayBufferLike>>;
|
|
57
58
|
protected onTrainerResponse(m: Buffer): void;
|
|
58
59
|
protected onRidingData(m: Buffer): void;
|
|
60
|
+
protected onRideKeyPadStatus(m: Buffer): void;
|
|
59
61
|
protected onDeviceInformation(m: Buffer): void;
|
|
60
62
|
onClickButtonMessage(d: Buffer): void;
|
|
61
63
|
onPingMessage(message: Buffer): void;
|
|
@@ -28,14 +28,14 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase i
|
|
|
28
28
|
})[];
|
|
29
29
|
};
|
|
30
30
|
protected gearDelta: number;
|
|
31
|
-
protected gear
|
|
32
|
-
protected tsStart
|
|
33
|
-
protected simPower
|
|
34
|
-
protected simSlope
|
|
35
|
-
protected maintainPower
|
|
36
|
-
protected prevData
|
|
37
|
-
protected prevEkin
|
|
38
|
-
protected prevSimPower
|
|
31
|
+
protected gear?: number;
|
|
32
|
+
protected tsStart?: number;
|
|
33
|
+
protected simPower?: number;
|
|
34
|
+
protected simSlope?: number;
|
|
35
|
+
protected maintainPower?: number;
|
|
36
|
+
protected prevData?: any;
|
|
37
|
+
protected prevEkin?: number;
|
|
38
|
+
protected prevSimPower?: number;
|
|
39
39
|
protected readonly gearRatios: number[];
|
|
40
40
|
constructor(adapter: IAdapter, props?: any);
|
|
41
41
|
getBikeInitRequest(): UpdateRequest;
|
|
@@ -58,6 +58,6 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase i
|
|
|
58
58
|
getData(): Partial<IncyclistBikeData>;
|
|
59
59
|
protected updateRequired(request?: UpdateRequest): boolean;
|
|
60
60
|
sendBikeUpdate(incoming: UpdateRequest): UpdateRequest;
|
|
61
|
-
protected getGearString(): string;
|
|
61
|
+
protected getGearString(): string | undefined;
|
|
62
62
|
protected getFeatureToogle(): import("../features/features.js").FeatureToggle;
|
|
63
63
|
}
|
|
@@ -5,7 +5,7 @@ export declare abstract class CyclingModeBase extends CyclingMode implements ICy
|
|
|
5
5
|
adapter: IAdapter;
|
|
6
6
|
settings: Settings;
|
|
7
7
|
properties: Settings;
|
|
8
|
-
localConfig
|
|
8
|
+
localConfig?: CyclingModeConfig;
|
|
9
9
|
protected static config: CyclingModeConfig;
|
|
10
10
|
protected static isERG: boolean;
|
|
11
11
|
protected prevUpdate: UpdateRequest;
|
|
@@ -47,7 +47,7 @@ export default interface ICyclingMode {
|
|
|
47
47
|
buildUpdate(request: UpdateRequest): UpdateRequest;
|
|
48
48
|
confirmed(request: UpdateRequest): void;
|
|
49
49
|
updateData(data: IncyclistBikeData): IncyclistBikeData;
|
|
50
|
-
setSettings(settings: any):
|
|
50
|
+
setSettings(settings: any): void;
|
|
51
51
|
setSetting(name: string, value: any): void;
|
|
52
52
|
getSetting(name: string): any;
|
|
53
53
|
getSettings(): Settings;
|
|
@@ -88,4 +88,5 @@ export declare class CyclingMode implements ICyclingMode {
|
|
|
88
88
|
resetConfig(): void;
|
|
89
89
|
getData(): Partial<IncyclistBikeData>;
|
|
90
90
|
confirmed(request: UpdateRequest): void;
|
|
91
|
+
onAdapterCapabilitiesChanged(): void;
|
|
91
92
|
}
|
|
@@ -10,7 +10,7 @@ export type InterfaceProps = {
|
|
|
10
10
|
export interface IncyclistInterface extends EventEmitter {
|
|
11
11
|
getName(): string;
|
|
12
12
|
setBinding(binding: any): void;
|
|
13
|
-
connect(): Promise<boolean>;
|
|
13
|
+
connect(reconnect?: boolean): Promise<boolean>;
|
|
14
14
|
disconnect(): Promise<boolean>;
|
|
15
15
|
terminate(): Promise<void>;
|
|
16
16
|
isConnected(): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incyclist-devices",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.16",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"lint": "eslint . --ext .ts",
|
|
6
6
|
"build": "npm run build:esm && npm run build:cjs",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@serialport/binding-mock": "^10.2.2",
|
|
32
32
|
"@serialport/bindings-cpp": "^13.0.1",
|
|
33
33
|
"@stoprocent/noble": "^2.4.0",
|
|
34
|
+
"@types/jest": "^30.0.0",
|
|
34
35
|
"@types/node": "^25.6.0",
|
|
35
36
|
"@typescript-eslint/eslint-plugin": "^8.59.0",
|
|
36
37
|
"@typescript-eslint/parser": "^8.59.0",
|