incyclist-devices 3.0.15 → 3.0.17

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.
@@ -29,7 +29,8 @@ class BleAdapter extends adpater_js_1.default {
29
29
  if (settings.name?.match(/\d/g) || settings.address === undefined)
30
30
  return this.getName();
31
31
  else {
32
- const addressHash = settings.id?.slice(-4)?.toUpperCase() ?? (settings.address?.substring(0, 2) ?? '') + (settings.address?.slice(-2) ?? '');
32
+ const id = (settings.id ?? settings.address ?? '').replace(/[:\-]/g, '');
33
+ const addressHash = id.length > 4 ? id.slice(-4).toUpperCase() : id.toUpperCase();
33
34
  return `${this.getName()} ${addressHash}`;
34
35
  }
35
36
  }
@@ -267,7 +267,7 @@ class BleInterface extends node_events_1.EventEmitter {
267
267
  return new peripheral_js_1.BlePeripheral(announcement);
268
268
  }
269
269
  createPeripheralFromSettings(settings) {
270
- const info = this.getAll().find(a => a.service?.name === settings.name || a.service?.peripheral?.address === settings.address);
270
+ const info = this.getAll().find(a => a.service?.peripheral?.address === settings.address);
271
271
  if (!info?.service)
272
272
  return null;
273
273
  return this.createPeripheral(info.service);
@@ -281,7 +281,7 @@ class BleInterface extends node_events_1.EventEmitter {
281
281
  if (!wasDiscovering)
282
282
  this.startPeripheralScan();
283
283
  const onDevice = (device) => {
284
- if (device.name === settings.name || device.address === settings.address) {
284
+ if (device.address === settings.address) {
285
285
  const peripheral = this.createPeripheralFromSettings(device);
286
286
  if (peripheral) {
287
287
  this.off('device', onDevice);
@@ -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 });
@@ -581,7 +588,7 @@ class BleInterface extends node_events_1.EventEmitter {
581
588
  return supported.length > 0;
582
589
  }
583
590
  find(service) {
584
- return this.services.find(a => a.service.name === service.name && a.ts > Date.now() - BLE_EXPIRATION_TIMEOUT);
591
+ return this.services.find(a => a.service.name === service.name && a.service?.peripheral?.address && a.ts > Date.now() - BLE_EXPIRATION_TIMEOUT);
585
592
  }
586
593
  getAll() {
587
594
  return this.services.filter(a => a.ts > Date.now() - BLE_EXPIRATION_TIMEOUT);
@@ -250,7 +250,6 @@ class BleFmAdapter extends adapter_js_1.default {
250
250
  if (before !== after) {
251
251
  this.logEvent({ message: 'device capabilities updated', name: this.getSettings().name, interface: this.getSettings().interface, capabilities: this.capabilities });
252
252
  this.emit('device-info', this.getSettings(), { capabilities: this.capabilities });
253
- this.updateCyclingModeConfig();
254
253
  }
255
254
  }
256
255
  updateCapabilitiesFromFeatures(features) {
@@ -11,6 +11,7 @@ const index_js_1 = require("../../../types/index.js");
11
11
  class ZwiftPlayAdapter extends adapter_js_1.default {
12
12
  static INCYCLIST_PROFILE_NAME = 'Controller';
13
13
  static CAPABILITIES = [index_js_1.IncyclistCapability.AppControl];
14
+ keyPressedHandler;
14
15
  constructor(settings, props) {
15
16
  super(settings, props);
16
17
  this.logger = new gd_eventlog_1.EventLogger('ZwiftPlay');
@@ -25,9 +26,13 @@ class ZwiftPlayAdapter extends adapter_js_1.default {
25
26
  let connected = await super.startSensor();
26
27
  if (connected) {
27
28
  const sensor = this.getSensor();
28
- sensor.on('key-pressed', (event) => {
29
- this.emit('key-pressed', this.getSettings(), event);
30
- });
29
+ if (this.keyPressedHandler) {
30
+ sensor.off('key-pressed', this.keyPressedHandler);
31
+ }
32
+ else {
33
+ this.keyPressedHandler = this.onKeyPressed.bind(this);
34
+ }
35
+ sensor.on('key-pressed', this.keyPressedHandler);
31
36
  }
32
37
  return connected;
33
38
  }
@@ -36,10 +41,18 @@ class ZwiftPlayAdapter extends adapter_js_1.default {
36
41
  return false;
37
42
  }
38
43
  }
44
+ onKeyPressed(event) {
45
+ this.emit('key-pressed', this.getSettings(), event);
46
+ }
47
+ isEqual(settings) {
48
+ const equal = super.isEqual(settings) &&
49
+ settings.address == this.settings.address;
50
+ return equal;
51
+ }
39
52
  isSame(adapter) {
40
53
  if (!(adapter instanceof ZwiftPlayAdapter))
41
54
  return false;
42
- return this.isEqual(adapter.settings);
55
+ return this.isEqual(adapter.settings) && this.getUniqueName() === adapter.getUniqueName();
43
56
  }
44
57
  updateSensor(peripheral) {
45
58
  this.device = new sensor_js_1.BleZwiftPlaySensor(peripheral, { logger: this.logger });
@@ -47,6 +60,22 @@ class ZwiftPlayAdapter extends adapter_js_1.default {
47
60
  getProfile() {
48
61
  return ZwiftPlayAdapter.INCYCLIST_PROFILE_NAME;
49
62
  }
63
+ getUniqueName() {
64
+ return this.getName();
65
+ }
66
+ getName() {
67
+ const settings = this.settings;
68
+ let name = settings.name;
69
+ if (settings.name === 'Zwift-Ride') {
70
+ if (this.device.getDeviceType() === 'ride-left')
71
+ name = name + '-L';
72
+ if (this.device.getDeviceType() === 'ride-right')
73
+ name = name + '-R';
74
+ }
75
+ const id = (settings.id ?? settings.address ?? '').replace(/[:\-]/g, '');
76
+ const addressHash = id.length > 4 ? id.slice(-4).toUpperCase() : id.toUpperCase();
77
+ return `${name} ${addressHash}`;
78
+ }
50
79
  getDisplayName() {
51
80
  return this.getName();
52
81
  }
@@ -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;
@@ -66,6 +67,42 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
66
67
  getRequiredCharacteristics() {
67
68
  return ['00000002-19ca-4651-86e5-fa29dcdd09d1', '00000004-19ca-4651-86e5-fa29dcdd09d1'];
68
69
  }
70
+ getDeviceType() {
71
+ if (this.deviceType)
72
+ return this.deviceType;
73
+ if (this.isFM) {
74
+ this.deviceType = 'hub';
75
+ }
76
+ else if (this.peripheral?.getManufacturerData) {
77
+ const manufacturerData = this.getManufacturerData();
78
+ if (manufacturerData?.startsWith('4a09')) {
79
+ const typeVal = Number('0x' + manufacturerData.substring(2, 4));
80
+ if (typeVal === 9) {
81
+ this.deviceType = 'click';
82
+ this.encrypted = false;
83
+ }
84
+ else if (typeVal === 2) {
85
+ this.deviceType = 'right';
86
+ }
87
+ else if (typeVal === 3) {
88
+ this.deviceType = 'left';
89
+ }
90
+ else if (typeVal === 7) {
91
+ this.deviceType = 'ride-right';
92
+ }
93
+ else if (typeVal === 8) {
94
+ this.deviceType = 'ride-left';
95
+ }
96
+ }
97
+ }
98
+ console.log('# [ZwiftPlay] device type ', this.deviceType, this.peripheral?.getInfo().name, this.peripheral?.getInfo().address);
99
+ if (!this.deviceType && !this.encryptedSupported()) {
100
+ this.deviceType = 'click';
101
+ this.encrypted = false;
102
+ }
103
+ this.deviceType = this.deviceType ?? 'click';
104
+ return this.deviceType;
105
+ }
69
106
  onData(characteristic, data, isNotify) {
70
107
  const uuid = (0, utils_js_1.beautifyUUID)(characteristic).toLowerCase();
71
108
  if (uuid === '00000002-19ca-4651-86e5-fa29dcdd09d1') {
@@ -153,7 +190,6 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
153
190
  }
154
191
  onMeasurement(d) {
155
192
  const data = Buffer.from(d);
156
- this.logEvent({ message: 'got hub notification', raw: data.toString('hex') });
157
193
  if (data?.length < 1) {
158
194
  console.log('Invalid click measurement data', data.toString('hex'));
159
195
  return false;
@@ -172,15 +208,19 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
172
208
  else if (type === 0x03) {
173
209
  this.onRidingData(message);
174
210
  }
211
+ else if (type === 0x23) {
212
+ this.onRideKeyPadStatus(message);
213
+ }
175
214
  else if (type === 0x2A) {
176
215
  this.onTrainerResponse(message);
177
216
  }
178
217
  else if (type === 0x3c) {
179
218
  this.onDeviceInformation(message);
180
219
  }
220
+ else if (type === 0x15) {
221
+ }
181
222
  else {
182
- console.log('Unknown click measurement type', type, message.toString('hex'));
183
- this.emit('data', { raw: data.toString('hex') });
223
+ this.logEvent({ message: 'got hub notification', raw: data.toString('hex') });
184
224
  }
185
225
  return true;
186
226
  }
@@ -285,6 +325,55 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
285
325
  this.logEvent({ message: 'Error', fn: 'onRidingData', error: err.message, stack: err.stack });
286
326
  }
287
327
  }
328
+ onRideKeyPadStatus(m) {
329
+ try {
330
+ const data = zwift_hub_js_1.RideKeyPadStatus.fromBinary(m);
331
+ const buttonNames = new Map([
332
+ [0x01, 'left'],
333
+ [0x02, 'up'],
334
+ [0x04, 'right'],
335
+ [0x08, 'down'],
336
+ [0x10, 'a'],
337
+ [0x20, 'b'],
338
+ [0x40, 'y'],
339
+ [0x80, 'z'],
340
+ [0x0100, 'l-shift-up'],
341
+ [0x0200, 'l-shift-down'],
342
+ [0x1000, 'r-shift-up'],
343
+ [0x2000, 'r-shift-down'],
344
+ [0x0400, 'l-power-up'],
345
+ [0x4000, 'r-power-up'],
346
+ [0x0800, 'l-power'],
347
+ [0x8000, 'r-power'],
348
+ ]);
349
+ const buttonMap = data.buttonMap ?? 0;
350
+ const currentPresses = new Set();
351
+ const address = this.peripheral?.getInfo()?.address;
352
+ const name = this.peripheral?.getInfo()?.name;
353
+ buttonNames.forEach((name, bit) => {
354
+ const isPressed = (buttonMap & bit) === 0;
355
+ if (isPressed) {
356
+ currentPresses.add(bit);
357
+ const prevState = this.rideKeyPadStates.get(bit);
358
+ if (!prevState || !prevState.pressed) {
359
+ this.rideKeyPadStates.set(bit, { pressed: true, timestamp: Date.now() });
360
+ }
361
+ }
362
+ });
363
+ this.rideKeyPadStates.forEach((state, bit) => {
364
+ if (!currentPresses.has(bit) && state.pressed) {
365
+ const keyName = buttonNames.get(bit);
366
+ const duration = Date.now() - state.timestamp;
367
+ this.logEvent({ message: 'key pressed', key: keyName, name, address, duration, deviceType: this.deviceType });
368
+ this.emit('key-pressed', { key: keyName, duration, deviceType: this.deviceType });
369
+ this.rideKeyPadStates.set(bit, { pressed: false, timestamp: Date.now() });
370
+ }
371
+ });
372
+ }
373
+ catch (err) {
374
+ this.logEvent({ message: 'Error', fn: 'onRideKeyPadStatus', error: err.message, stack: err.stack });
375
+ }
376
+ }
288
377
  onDeviceInformation(m) {
289
378
  try {
290
379
  const envelope = zwift_hub_js_1.DeviceDataEnvelope.fromBinary(m);
@@ -312,6 +401,8 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
312
401
  }
313
402
  onClickButtonMessage(d) {
314
403
  try {
404
+ const address = this.peripheral?.getInfo()?.address;
405
+ const name = this.peripheral?.getInfo()?.name;
315
406
  const message = Buffer.from(d);
316
407
  const messageStr = message.toString('hex');
317
408
  if (messageStr === this.prevClickMessage) {
@@ -322,6 +413,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
322
413
  const prev = { ...this.upState };
323
414
  this.upState = { pressed: false, timestamp: Date.now() };
324
415
  if (prev.pressed) {
416
+ this.logEvent({ message: 'key pressed', key: 'up', name, address, duration: this.upState.timestamp - prev.timestamp, deviceType: this.deviceType });
325
417
  this.emit('key-pressed', { key: 'up', duration: this.upState.timestamp - prev.timestamp, deviceType: this.deviceType });
326
418
  }
327
419
  }
@@ -332,6 +424,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
332
424
  const prev = { ...this.downState };
333
425
  this.downState = { pressed: false, timestamp: Date.now() };
334
426
  if (prev.pressed) {
427
+ this.logEvent({ message: 'key pressed', key: 'down', name, address, duration: this.downState.timestamp - prev.timestamp, deviceType: this.deviceType });
335
428
  this.emit('key-pressed', { key: 'down', duration: this.downState.timestamp - prev.timestamp, deviceType: this.deviceType });
336
429
  }
337
430
  }
@@ -430,7 +523,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
430
523
  this.deviceType = 'hub';
431
524
  this.encrypted = false;
432
525
  }
433
- else if (this.peripheral.getManufacturerData) {
526
+ else if (this.peripheral?.getManufacturerData) {
434
527
  manufacturerData = this.getManufacturerData();
435
528
  if (manufacturerData?.startsWith('4a09')) {
436
529
  const typeVal = Number('0x' + manufacturerData.substring(2, 4));
@@ -444,6 +537,12 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
444
537
  else if (typeVal === 3) {
445
538
  this.deviceType = 'left';
446
539
  }
540
+ else if (typeVal === 7) {
541
+ this.deviceType = 'ride-right';
542
+ }
543
+ else if (typeVal === 8) {
544
+ this.deviceType = 'ride-left';
545
+ }
447
546
  }
448
547
  }
449
548
  if (!this.deviceType && !this.encryptedSupported()) {
@@ -451,7 +550,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
451
550
  this.encrypted = false;
452
551
  }
453
552
  this.deviceType = this.deviceType ?? 'click';
454
- this.logEvent({ message: 'Play protocol pairing info', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData });
553
+ this.logEvent({ message: 'Play protocol pairing info', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData: this.getManufacturerData() });
455
554
  let message;
456
555
  if (this.isFM) {
457
556
  message = Buffer.concat([Buffer.from('RideOn'), Buffer.from([0x02, 0x01])]);
@@ -468,7 +567,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
468
567
  this.logEvent({ message: `send rideOn` });
469
568
  await this.write((0, utils_js_1.fullUUID)('00000003-19ca-4651-86e5-fa29dcdd09d1'), message, { withoutResponse: true });
470
569
  this.isHubServicePaired = true;
471
- this.logEvent({ message: 'pairing done', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData });
570
+ this.logEvent({ message: 'pairing done', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData: this.getManufacturerData() });
472
571
  return true;
473
572
  }
474
573
  catch (err) {
@@ -479,6 +578,15 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
479
578
  }
480
579
  reset() {
481
580
  }
581
+ getManufacturerData() {
582
+ const data = this.peripheral?.getManufacturerData?.();
583
+ if (typeof data === 'string')
584
+ return data;
585
+ if (Buffer.isBuffer(data)) {
586
+ return data.toString('hex');
587
+ }
588
+ return undefined;
589
+ }
482
590
  getCrypto() {
483
591
  let crypto = index_js_1.BindingsFactory.getInstance()?.getBinding()?.crypto;
484
592
  if (!crypto)
@@ -506,15 +614,7 @@ class BleZwiftPlaySensor extends sensor_js_1.TBleSensor {
506
614
  this.isHubServiceActive = false;
507
615
  delete this.initHubServicePromise;
508
616
  delete this.prevHubSettings;
509
- }
510
- getManufacturerData() {
511
- const data = this.peripheral.getManufacturerData();
512
- if (typeof data === 'string')
513
- return data;
514
- if (Buffer.isBuffer(data)) {
515
- return data.toString('hex');
516
- }
517
- return undefined;
617
+ this.rideKeyPadStates = new Map();
518
618
  }
519
619
  }
520
620
  exports.BleZwiftPlaySensor = BleZwiftPlaySensor;
@@ -99,34 +99,40 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
99
99
  if (startGearIdx !== -1) {
100
100
  config.properties.splice(startGearIdx, 1);
101
101
  }
102
+ try {
103
+ const device = this.adapter?.getName();
104
+ this.logger.logEvent({ message: 'reset config', device });
105
+ }
106
+ catch { }
102
107
  }
103
108
  getConfig() {
104
109
  const config = super.getConfig();
105
- const virtShiftEnabled = this.getFeatureToogle().has('VirtualShifting');
106
110
  let virtshift = config.properties.find(p => p.key === 'virtshift');
107
111
  let startGear = config.properties.find(p => p.key === 'startGear');
108
- if (!virtshift && !this.adapter?.supportsVirtualShifting()) {
109
- const options = virtShiftEnabled ? [
110
- 'Disabled',
111
- { key: 'Incyclist', display: 'App only (beta)' },
112
- { key: 'Mixed', display: 'App + Bike' }
113
- ] :
114
- [
112
+ if (!virtshift) {
113
+ const device = this.adapter?.getName();
114
+ const supportsZwift = this.adapter?.supportsVirtualShifting();
115
+ this.logger.logEvent({ message: 'prepare gear settings config', device, supportsZwift });
116
+ if (supportsZwift) {
117
+ const options = [
118
+ 'Disabled',
119
+ { key: 'Incyclist', display: 'App only (beta)' },
120
+ { key: 'Mixed', display: 'App + Bike' },
121
+ { key: 'SmartTrainer', display: 'SmartTreiner (beta)' }
122
+ ];
123
+ virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: types_js_1.CyclingModeProperyType.SingleSelect, options, default: 'Mixed' };
124
+ config.properties.push(virtshift);
125
+ }
126
+ else {
127
+ const options = [
115
128
  'Disabled',
116
- { key: 'Mixed', display: 'Enabled' }
129
+ { key: 'Incyclist', display: 'App only (beta)' },
130
+ { key: 'Mixed', display: 'App + Bike' }
117
131
  ];
118
- virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: types_js_1.CyclingModeProperyType.SingleSelect, options, default: 'Disabled' };
119
- config.properties.push(virtshift);
120
- }
121
- if (!virtshift && virtShiftEnabled && this.adapter?.supportsVirtualShifting()) {
122
- const options = [
123
- 'Disabled',
124
- { key: 'Incyclist', display: 'App only (beta)' },
125
- { key: 'Mixed', display: 'App + Bike' },
126
- { key: 'SmartTrainer', display: 'SmartTreiner (beta)' }
127
- ];
128
- virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: types_js_1.CyclingModeProperyType.SingleSelect, options, default: 'Mixed' };
129
- config.properties.push(virtshift);
132
+ virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: types_js_1.CyclingModeProperyType.SingleSelect, options, default: 'Disabled' };
133
+ config.properties.push(virtshift);
134
+ }
135
+ this.logger.logEvent({ message: 'gear settings config', config: config.properties });
130
136
  }
131
137
  if (virtshift && !startGear) {
132
138
  startGear = { key: 'startGear', name: 'Initial Gear', description: 'Initial Gear', type: types_js_1.CyclingModeProperyType.Integer, default: 12, min: 1, max: 24, condition: (s) => s?.virtshift === 'Incyclist' || s?.virtshift === 'SmartTrainer' };
@@ -234,10 +240,13 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
234
240
  const m = this.adapter?.getWeight() ?? 85;
235
241
  const vCurrent = this.data.speed * 1000 / 3600;
236
242
  const eKinCurrent = m * vCurrent * vCurrent / 2;
237
- if (this.data.pedalRpm > 0) {
238
- const virtualSpeed = (0, calculations_js_1.calculateVirtualSpeed)(this.data.pedalRpm, this.gearRatios[this.gear - 1]) * 3.6;
243
+ const cadence = this.data?.pedalRpm ?? 0;
244
+ const gear = this.gear ?? 1;
245
+ const simSlope = this.simSlope ?? 0;
246
+ if (cadence > 0) {
247
+ const virtualSpeed = (0, calculations_js_1.calculateVirtualSpeed)(cadence, this.gearRatios[gear - 1]) * 3.6;
239
248
  const v = virtualSpeed / 3.6;
240
- const newPower = calculations_js_1.default.calculatePower(m, virtualSpeed / 3.6, this.simSlope ?? 0);
249
+ const newPower = calculations_js_1.default.calculatePower(m, virtualSpeed / 3.6, simSlope);
241
250
  const prevPower = this.data.power;
242
251
  const prev = (this.prevSimPower ?? prevPower ?? 0);
243
252
  const delta = newPower - prev;
@@ -253,7 +262,7 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
253
262
  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
263
  }
255
264
  else if (changed === 'slope' || changed === 'cadence') {
256
- const adjustTime = this.simSlope < 0 ? 5 : 3;
265
+ const adjustTime = (this.simSlope ?? 0) < 0 ? 5 : 3;
257
266
  this.simPower = prev + delta / adjustTime;
258
267
  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
268
  }
@@ -344,9 +353,13 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
344
353
  else {
345
354
  if (this.gear === undefined) {
346
355
  const initialGear = (0, utils_js_1.intVal)(this.getSetting('startGear'));
347
- this.gear = initialGear + request.gearDelta;
356
+ this.gear = initialGear + (request.gearDelta ?? 0);
348
357
  }
349
- newRequest.gearRatio = this.gearRatios[this.gear];
358
+ if (this.gear < 1)
359
+ this.gear = 1;
360
+ if (this.gear > this.gearRatios.length - 1)
361
+ this.gear = this.gearRatios.length - 1;
362
+ newRequest.gearRatio = this.gearRatios[this.gear - 1];
350
363
  this.logEvent({ message: 'gear initialized', gear: this.gear, gearRatio: newRequest.gearRatio });
351
364
  }
352
365
  break;
@@ -356,10 +369,10 @@ class SmartTrainerCyclingMode extends power_base_js_1.default {
356
369
  }
357
370
  }
358
371
  verifySimPower() {
359
- if (this.simPower < 0) {
372
+ if ((this.simPower ?? 0) < 0) {
360
373
  this.simPower = 0;
361
374
  }
362
- if (this.data.pedalRpm > 0 && this.simPower < MIN_POWER) {
375
+ if ((this.data.pedalRpm ?? 0) > 0 && (this.simPower ?? 0) < MIN_POWER) {
363
376
  this.simPower = MIN_POWER;
364
377
  }
365
378
  if (!this.data.isPedalling) {
@@ -70,5 +70,7 @@ class CyclingMode {
70
70
  }
71
71
  confirmed(request) {
72
72
  }
73
+ onAdapterCapabilitiesChanged() {
74
+ }
73
75
  }
74
76
  exports.CyclingMode = CyclingMode;
@@ -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: 2, name: "AnalogButtons", kind: "message", jsonName: "AnalogButtons", T: () => exports.RideAnalogKeyGroup }
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 2:
693
- message.analogButtons = exports.RideAnalogKeyGroup.internalBinaryRead(reader, reader.uint32(), options, 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
- if (message.analogButtons)
710
- exports.RideAnalogKeyGroup.internalBinaryWrite(message.analogButtons, writer.tag(2, runtime_1.WireType.LengthDelimited).fork(), options).join();
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);
@@ -24,7 +24,8 @@ export default class BleAdapter extends IncyclistDevice {
24
24
  if (settings.name?.match(/\d/g) || settings.address === undefined)
25
25
  return this.getName();
26
26
  else {
27
- const addressHash = settings.id?.slice(-4)?.toUpperCase() ?? (settings.address?.substring(0, 2) ?? '') + (settings.address?.slice(-2) ?? '');
27
+ const id = (settings.id ?? settings.address ?? '').replace(/[:\-]/g, '');
28
+ const addressHash = id.length > 4 ? id.slice(-4).toUpperCase() : id.toUpperCase();
28
29
  return `${this.getName()} ${addressHash}`;
29
30
  }
30
31
  }
@@ -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;
@@ -264,7 +264,7 @@ export class BleInterface extends EventEmitter {
264
264
  return new BlePeripheral(announcement);
265
265
  }
266
266
  createPeripheralFromSettings(settings) {
267
- const info = this.getAll().find(a => a.service?.name === settings.name || a.service?.peripheral?.address === settings.address);
267
+ const info = this.getAll().find(a => a.service?.peripheral?.address === settings.address);
268
268
  if (!info?.service)
269
269
  return null;
270
270
  return this.createPeripheral(info.service);
@@ -278,7 +278,7 @@ export class BleInterface extends EventEmitter {
278
278
  if (!wasDiscovering)
279
279
  this.startPeripheralScan();
280
280
  const onDevice = (device) => {
281
- if (device.name === settings.name || device.address === settings.address) {
281
+ if (device.address === settings.address) {
282
282
  const peripheral = this.createPeripheralFromSettings(device);
283
283
  if (peripheral) {
284
284
  this.off('device', onDevice);
@@ -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 });
@@ -578,7 +585,7 @@ export class BleInterface extends EventEmitter {
578
585
  return supported.length > 0;
579
586
  }
580
587
  find(service) {
581
- return this.services.find(a => a.service.name === service.name && a.ts > Date.now() - BLE_EXPIRATION_TIMEOUT);
588
+ return this.services.find(a => a.service.name === service.name && a.service?.peripheral?.address && a.ts > Date.now() - BLE_EXPIRATION_TIMEOUT);
582
589
  }
583
590
  getAll() {
584
591
  return this.services.filter(a => a.ts > Date.now() - BLE_EXPIRATION_TIMEOUT);
@@ -245,7 +245,6 @@ export default class BleFmAdapter extends BleAdapter {
245
245
  if (before !== after) {
246
246
  this.logEvent({ message: 'device capabilities updated', name: this.getSettings().name, interface: this.getSettings().interface, capabilities: this.capabilities });
247
247
  this.emit('device-info', this.getSettings(), { capabilities: this.capabilities });
248
- this.updateCyclingModeConfig();
249
248
  }
250
249
  }
251
250
  updateCapabilitiesFromFeatures(features) {
@@ -5,6 +5,7 @@ import { IncyclistCapability } from '../../../types/index.js';
5
5
  export class ZwiftPlayAdapter extends BleAdapter {
6
6
  static INCYCLIST_PROFILE_NAME = 'Controller';
7
7
  static CAPABILITIES = [IncyclistCapability.AppControl];
8
+ keyPressedHandler;
8
9
  constructor(settings, props) {
9
10
  super(settings, props);
10
11
  this.logger = new EventLogger('ZwiftPlay');
@@ -19,9 +20,13 @@ export class ZwiftPlayAdapter extends BleAdapter {
19
20
  let connected = await super.startSensor();
20
21
  if (connected) {
21
22
  const sensor = this.getSensor();
22
- sensor.on('key-pressed', (event) => {
23
- this.emit('key-pressed', this.getSettings(), event);
24
- });
23
+ if (this.keyPressedHandler) {
24
+ sensor.off('key-pressed', this.keyPressedHandler);
25
+ }
26
+ else {
27
+ this.keyPressedHandler = this.onKeyPressed.bind(this);
28
+ }
29
+ sensor.on('key-pressed', this.keyPressedHandler);
25
30
  }
26
31
  return connected;
27
32
  }
@@ -30,10 +35,18 @@ export class ZwiftPlayAdapter extends BleAdapter {
30
35
  return false;
31
36
  }
32
37
  }
38
+ onKeyPressed(event) {
39
+ this.emit('key-pressed', this.getSettings(), event);
40
+ }
41
+ isEqual(settings) {
42
+ const equal = super.isEqual(settings) &&
43
+ settings.address == this.settings.address;
44
+ return equal;
45
+ }
33
46
  isSame(adapter) {
34
47
  if (!(adapter instanceof ZwiftPlayAdapter))
35
48
  return false;
36
- return this.isEqual(adapter.settings);
49
+ return this.isEqual(adapter.settings) && this.getUniqueName() === adapter.getUniqueName();
37
50
  }
38
51
  updateSensor(peripheral) {
39
52
  this.device = new BleZwiftPlaySensor(peripheral, { logger: this.logger });
@@ -41,6 +54,22 @@ export class ZwiftPlayAdapter extends BleAdapter {
41
54
  getProfile() {
42
55
  return ZwiftPlayAdapter.INCYCLIST_PROFILE_NAME;
43
56
  }
57
+ getUniqueName() {
58
+ return this.getName();
59
+ }
60
+ getName() {
61
+ const settings = this.settings;
62
+ let name = settings.name;
63
+ if (settings.name === 'Zwift-Ride') {
64
+ if (this.device.getDeviceType() === 'ride-left')
65
+ name = name + '-L';
66
+ if (this.device.getDeviceType() === 'ride-right')
67
+ name = name + '-R';
68
+ }
69
+ const id = (settings.id ?? settings.address ?? '').replace(/[:\-]/g, '');
70
+ const addressHash = id.length > 4 ? id.slice(-4).toUpperCase() : id.toUpperCase();
71
+ return `${name} ${addressHash}`;
72
+ }
44
73
  getDisplayName() {
45
74
  return this.getName();
46
75
  }
@@ -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;
@@ -63,6 +64,42 @@ export class BleZwiftPlaySensor extends TBleSensor {
63
64
  getRequiredCharacteristics() {
64
65
  return ['00000002-19ca-4651-86e5-fa29dcdd09d1', '00000004-19ca-4651-86e5-fa29dcdd09d1'];
65
66
  }
67
+ getDeviceType() {
68
+ if (this.deviceType)
69
+ return this.deviceType;
70
+ if (this.isFM) {
71
+ this.deviceType = 'hub';
72
+ }
73
+ else if (this.peripheral?.getManufacturerData) {
74
+ const manufacturerData = this.getManufacturerData();
75
+ if (manufacturerData?.startsWith('4a09')) {
76
+ const typeVal = Number('0x' + manufacturerData.substring(2, 4));
77
+ if (typeVal === 9) {
78
+ this.deviceType = 'click';
79
+ this.encrypted = false;
80
+ }
81
+ else if (typeVal === 2) {
82
+ this.deviceType = 'right';
83
+ }
84
+ else if (typeVal === 3) {
85
+ this.deviceType = 'left';
86
+ }
87
+ else if (typeVal === 7) {
88
+ this.deviceType = 'ride-right';
89
+ }
90
+ else if (typeVal === 8) {
91
+ this.deviceType = 'ride-left';
92
+ }
93
+ }
94
+ }
95
+ console.log('# [ZwiftPlay] device type ', this.deviceType, this.peripheral?.getInfo().name, this.peripheral?.getInfo().address);
96
+ if (!this.deviceType && !this.encryptedSupported()) {
97
+ this.deviceType = 'click';
98
+ this.encrypted = false;
99
+ }
100
+ this.deviceType = this.deviceType ?? 'click';
101
+ return this.deviceType;
102
+ }
66
103
  onData(characteristic, data, isNotify) {
67
104
  const uuid = beautifyUUID(characteristic).toLowerCase();
68
105
  if (uuid === '00000002-19ca-4651-86e5-fa29dcdd09d1') {
@@ -150,7 +187,6 @@ export class BleZwiftPlaySensor extends TBleSensor {
150
187
  }
151
188
  onMeasurement(d) {
152
189
  const data = Buffer.from(d);
153
- this.logEvent({ message: 'got hub notification', raw: data.toString('hex') });
154
190
  if (data?.length < 1) {
155
191
  console.log('Invalid click measurement data', data.toString('hex'));
156
192
  return false;
@@ -169,15 +205,19 @@ export class BleZwiftPlaySensor extends TBleSensor {
169
205
  else if (type === 0x03) {
170
206
  this.onRidingData(message);
171
207
  }
208
+ else if (type === 0x23) {
209
+ this.onRideKeyPadStatus(message);
210
+ }
172
211
  else if (type === 0x2A) {
173
212
  this.onTrainerResponse(message);
174
213
  }
175
214
  else if (type === 0x3c) {
176
215
  this.onDeviceInformation(message);
177
216
  }
217
+ else if (type === 0x15) {
218
+ }
178
219
  else {
179
- console.log('Unknown click measurement type', type, message.toString('hex'));
180
- this.emit('data', { raw: data.toString('hex') });
220
+ this.logEvent({ message: 'got hub notification', raw: data.toString('hex') });
181
221
  }
182
222
  return true;
183
223
  }
@@ -282,6 +322,55 @@ export class BleZwiftPlaySensor extends TBleSensor {
282
322
  this.logEvent({ message: 'Error', fn: 'onRidingData', error: err.message, stack: err.stack });
283
323
  }
284
324
  }
325
+ onRideKeyPadStatus(m) {
326
+ try {
327
+ const data = RideKeyPadStatus.fromBinary(m);
328
+ const buttonNames = new Map([
329
+ [0x01, 'left'],
330
+ [0x02, 'up'],
331
+ [0x04, 'right'],
332
+ [0x08, 'down'],
333
+ [0x10, 'a'],
334
+ [0x20, 'b'],
335
+ [0x40, 'y'],
336
+ [0x80, 'z'],
337
+ [0x0100, 'l-shift-up'],
338
+ [0x0200, 'l-shift-down'],
339
+ [0x1000, 'r-shift-up'],
340
+ [0x2000, 'r-shift-down'],
341
+ [0x0400, 'l-power-up'],
342
+ [0x4000, 'r-power-up'],
343
+ [0x0800, 'l-power'],
344
+ [0x8000, 'r-power'],
345
+ ]);
346
+ const buttonMap = data.buttonMap ?? 0;
347
+ const currentPresses = new Set();
348
+ const address = this.peripheral?.getInfo()?.address;
349
+ const name = this.peripheral?.getInfo()?.name;
350
+ buttonNames.forEach((name, bit) => {
351
+ const isPressed = (buttonMap & bit) === 0;
352
+ if (isPressed) {
353
+ currentPresses.add(bit);
354
+ const prevState = this.rideKeyPadStates.get(bit);
355
+ if (!prevState || !prevState.pressed) {
356
+ this.rideKeyPadStates.set(bit, { pressed: true, timestamp: Date.now() });
357
+ }
358
+ }
359
+ });
360
+ this.rideKeyPadStates.forEach((state, bit) => {
361
+ if (!currentPresses.has(bit) && state.pressed) {
362
+ const keyName = buttonNames.get(bit);
363
+ const duration = Date.now() - state.timestamp;
364
+ this.logEvent({ message: 'key pressed', key: keyName, name, address, duration, deviceType: this.deviceType });
365
+ this.emit('key-pressed', { key: keyName, duration, deviceType: this.deviceType });
366
+ this.rideKeyPadStates.set(bit, { pressed: false, timestamp: Date.now() });
367
+ }
368
+ });
369
+ }
370
+ catch (err) {
371
+ this.logEvent({ message: 'Error', fn: 'onRideKeyPadStatus', error: err.message, stack: err.stack });
372
+ }
373
+ }
285
374
  onDeviceInformation(m) {
286
375
  try {
287
376
  const envelope = DeviceDataEnvelope.fromBinary(m);
@@ -309,6 +398,8 @@ export class BleZwiftPlaySensor extends TBleSensor {
309
398
  }
310
399
  onClickButtonMessage(d) {
311
400
  try {
401
+ const address = this.peripheral?.getInfo()?.address;
402
+ const name = this.peripheral?.getInfo()?.name;
312
403
  const message = Buffer.from(d);
313
404
  const messageStr = message.toString('hex');
314
405
  if (messageStr === this.prevClickMessage) {
@@ -319,6 +410,7 @@ export class BleZwiftPlaySensor extends TBleSensor {
319
410
  const prev = { ...this.upState };
320
411
  this.upState = { pressed: false, timestamp: Date.now() };
321
412
  if (prev.pressed) {
413
+ this.logEvent({ message: 'key pressed', key: 'up', name, address, duration: this.upState.timestamp - prev.timestamp, deviceType: this.deviceType });
322
414
  this.emit('key-pressed', { key: 'up', duration: this.upState.timestamp - prev.timestamp, deviceType: this.deviceType });
323
415
  }
324
416
  }
@@ -329,6 +421,7 @@ export class BleZwiftPlaySensor extends TBleSensor {
329
421
  const prev = { ...this.downState };
330
422
  this.downState = { pressed: false, timestamp: Date.now() };
331
423
  if (prev.pressed) {
424
+ this.logEvent({ message: 'key pressed', key: 'down', name, address, duration: this.downState.timestamp - prev.timestamp, deviceType: this.deviceType });
332
425
  this.emit('key-pressed', { key: 'down', duration: this.downState.timestamp - prev.timestamp, deviceType: this.deviceType });
333
426
  }
334
427
  }
@@ -427,7 +520,7 @@ export class BleZwiftPlaySensor extends TBleSensor {
427
520
  this.deviceType = 'hub';
428
521
  this.encrypted = false;
429
522
  }
430
- else if (this.peripheral.getManufacturerData) {
523
+ else if (this.peripheral?.getManufacturerData) {
431
524
  manufacturerData = this.getManufacturerData();
432
525
  if (manufacturerData?.startsWith('4a09')) {
433
526
  const typeVal = Number('0x' + manufacturerData.substring(2, 4));
@@ -441,6 +534,12 @@ export class BleZwiftPlaySensor extends TBleSensor {
441
534
  else if (typeVal === 3) {
442
535
  this.deviceType = 'left';
443
536
  }
537
+ else if (typeVal === 7) {
538
+ this.deviceType = 'ride-right';
539
+ }
540
+ else if (typeVal === 8) {
541
+ this.deviceType = 'ride-left';
542
+ }
444
543
  }
445
544
  }
446
545
  if (!this.deviceType && !this.encryptedSupported()) {
@@ -448,7 +547,7 @@ export class BleZwiftPlaySensor extends TBleSensor {
448
547
  this.encrypted = false;
449
548
  }
450
549
  this.deviceType = this.deviceType ?? 'click';
451
- this.logEvent({ message: 'Play protocol pairing info', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData });
550
+ this.logEvent({ message: 'Play protocol pairing info', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData: this.getManufacturerData() });
452
551
  let message;
453
552
  if (this.isFM) {
454
553
  message = Buffer.concat([Buffer.from('RideOn'), Buffer.from([0x02, 0x01])]);
@@ -465,7 +564,7 @@ export class BleZwiftPlaySensor extends TBleSensor {
465
564
  this.logEvent({ message: `send rideOn` });
466
565
  await this.write(fullUUID('00000003-19ca-4651-86e5-fa29dcdd09d1'), message, { withoutResponse: true });
467
566
  this.isHubServicePaired = true;
468
- this.logEvent({ message: 'pairing done', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData });
567
+ this.logEvent({ message: 'pairing done', deviceType: this.deviceType, encrypted: this.encrypted, manufacturerData: this.getManufacturerData() });
469
568
  return true;
470
569
  }
471
570
  catch (err) {
@@ -476,6 +575,15 @@ export class BleZwiftPlaySensor extends TBleSensor {
476
575
  }
477
576
  reset() {
478
577
  }
578
+ getManufacturerData() {
579
+ const data = this.peripheral?.getManufacturerData?.();
580
+ if (typeof data === 'string')
581
+ return data;
582
+ if (Buffer.isBuffer(data)) {
583
+ return data.toString('hex');
584
+ }
585
+ return undefined;
586
+ }
479
587
  getCrypto() {
480
588
  let crypto = BindingsFactory.getInstance()?.getBinding()?.crypto;
481
589
  if (!crypto)
@@ -503,14 +611,6 @@ export class BleZwiftPlaySensor extends TBleSensor {
503
611
  this.isHubServiceActive = false;
504
612
  delete this.initHubServicePromise;
505
613
  delete this.prevHubSettings;
506
- }
507
- getManufacturerData() {
508
- const data = this.peripheral.getManufacturerData();
509
- if (typeof data === 'string')
510
- return data;
511
- if (Buffer.isBuffer(data)) {
512
- return data.toString('hex');
513
- }
514
- return undefined;
614
+ this.rideKeyPadStates = new Map();
515
615
  }
516
616
  }
@@ -61,34 +61,40 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
61
61
  if (startGearIdx !== -1) {
62
62
  config.properties.splice(startGearIdx, 1);
63
63
  }
64
+ try {
65
+ const device = this.adapter?.getName();
66
+ this.logger.logEvent({ message: 'reset config', device });
67
+ }
68
+ catch { }
64
69
  }
65
70
  getConfig() {
66
71
  const config = super.getConfig();
67
- const virtShiftEnabled = this.getFeatureToogle().has('VirtualShifting');
68
72
  let virtshift = config.properties.find(p => p.key === 'virtshift');
69
73
  let startGear = config.properties.find(p => p.key === 'startGear');
70
- if (!virtshift && !this.adapter?.supportsVirtualShifting()) {
71
- const options = virtShiftEnabled ? [
72
- 'Disabled',
73
- { key: 'Incyclist', display: 'App only (beta)' },
74
- { key: 'Mixed', display: 'App + Bike' }
75
- ] :
76
- [
74
+ if (!virtshift) {
75
+ const device = this.adapter?.getName();
76
+ const supportsZwift = this.adapter?.supportsVirtualShifting();
77
+ this.logger.logEvent({ message: 'prepare gear settings config', device, supportsZwift });
78
+ if (supportsZwift) {
79
+ const options = [
80
+ 'Disabled',
81
+ { key: 'Incyclist', display: 'App only (beta)' },
82
+ { key: 'Mixed', display: 'App + Bike' },
83
+ { key: 'SmartTrainer', display: 'SmartTreiner (beta)' }
84
+ ];
85
+ virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: CyclingModeProperyType.SingleSelect, options, default: 'Mixed' };
86
+ config.properties.push(virtshift);
87
+ }
88
+ else {
89
+ const options = [
77
90
  'Disabled',
78
- { key: 'Mixed', display: 'Enabled' }
91
+ { key: 'Incyclist', display: 'App only (beta)' },
92
+ { key: 'Mixed', display: 'App + Bike' }
79
93
  ];
80
- virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: CyclingModeProperyType.SingleSelect, options, default: 'Disabled' };
81
- config.properties.push(virtshift);
82
- }
83
- if (!virtshift && virtShiftEnabled && this.adapter?.supportsVirtualShifting()) {
84
- const options = [
85
- 'Disabled',
86
- { key: 'Incyclist', display: 'App only (beta)' },
87
- { key: 'Mixed', display: 'App + Bike' },
88
- { key: 'SmartTrainer', display: 'SmartTreiner (beta)' }
89
- ];
90
- virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: CyclingModeProperyType.SingleSelect, options, default: 'Mixed' };
91
- config.properties.push(virtshift);
94
+ virtshift = { key: 'virtshift', name: 'Virtual Shifting', description: 'Enable virtual shifting', type: CyclingModeProperyType.SingleSelect, options, default: 'Disabled' };
95
+ config.properties.push(virtshift);
96
+ }
97
+ this.logger.logEvent({ message: 'gear settings config', config: config.properties });
92
98
  }
93
99
  if (virtshift && !startGear) {
94
100
  startGear = { key: 'startGear', name: 'Initial Gear', description: 'Initial Gear', type: CyclingModeProperyType.Integer, default: 12, min: 1, max: 24, condition: (s) => s?.virtshift === 'Incyclist' || s?.virtshift === 'SmartTrainer' };
@@ -196,10 +202,13 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
196
202
  const m = this.adapter?.getWeight() ?? 85;
197
203
  const vCurrent = this.data.speed * 1000 / 3600;
198
204
  const eKinCurrent = m * vCurrent * vCurrent / 2;
199
- if (this.data.pedalRpm > 0) {
200
- const virtualSpeed = calculateVirtualSpeed(this.data.pedalRpm, this.gearRatios[this.gear - 1]) * 3.6;
205
+ const cadence = this.data?.pedalRpm ?? 0;
206
+ const gear = this.gear ?? 1;
207
+ const simSlope = this.simSlope ?? 0;
208
+ if (cadence > 0) {
209
+ const virtualSpeed = calculateVirtualSpeed(cadence, this.gearRatios[gear - 1]) * 3.6;
201
210
  const v = virtualSpeed / 3.6;
202
- const newPower = calc.calculatePower(m, virtualSpeed / 3.6, this.simSlope ?? 0);
211
+ const newPower = calc.calculatePower(m, virtualSpeed / 3.6, simSlope);
203
212
  const prevPower = this.data.power;
204
213
  const prev = (this.prevSimPower ?? prevPower ?? 0);
205
214
  const delta = newPower - prev;
@@ -215,7 +224,7 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
215
224
  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
225
  }
217
226
  else if (changed === 'slope' || changed === 'cadence') {
218
- const adjustTime = this.simSlope < 0 ? 5 : 3;
227
+ const adjustTime = (this.simSlope ?? 0) < 0 ? 5 : 3;
219
228
  this.simPower = prev + delta / adjustTime;
220
229
  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
230
  }
@@ -306,9 +315,13 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
306
315
  else {
307
316
  if (this.gear === undefined) {
308
317
  const initialGear = intVal(this.getSetting('startGear'));
309
- this.gear = initialGear + request.gearDelta;
318
+ this.gear = initialGear + (request.gearDelta ?? 0);
310
319
  }
311
- newRequest.gearRatio = this.gearRatios[this.gear];
320
+ if (this.gear < 1)
321
+ this.gear = 1;
322
+ if (this.gear > this.gearRatios.length - 1)
323
+ this.gear = this.gearRatios.length - 1;
324
+ newRequest.gearRatio = this.gearRatios[this.gear - 1];
312
325
  this.logEvent({ message: 'gear initialized', gear: this.gear, gearRatio: newRequest.gearRatio });
313
326
  }
314
327
  break;
@@ -318,10 +331,10 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase {
318
331
  }
319
332
  }
320
333
  verifySimPower() {
321
- if (this.simPower < 0) {
334
+ if ((this.simPower ?? 0) < 0) {
322
335
  this.simPower = 0;
323
336
  }
324
- if (this.data.pedalRpm > 0 && this.simPower < MIN_POWER) {
337
+ if ((this.data.pedalRpm ?? 0) > 0 && (this.simPower ?? 0) < MIN_POWER) {
325
338
  this.simPower = MIN_POWER;
326
339
  }
327
340
  if (!this.data.isPedalling) {
@@ -67,4 +67,6 @@ export class CyclingMode {
67
67
  }
68
68
  confirmed(request) {
69
69
  }
70
+ onAdapterCapabilitiesChanged() {
71
+ }
70
72
  }
@@ -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: 2, name: "AnalogButtons", kind: "message", jsonName: "AnalogButtons", T: () => RideAnalogKeyGroup }
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 2:
690
- message.analogButtons = RideAnalogKeyGroup.internalBinaryRead(reader, reader.uint32(), options, 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
- if (message.analogButtons)
707
- RideAnalogKeyGroup.internalBinaryWrite(message.analogButtons, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
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);
@@ -7,12 +7,17 @@ import { BleDeviceSettings, IBlePeripheral } from '../../types.js';
7
7
  export declare class ZwiftPlayAdapter extends BleAdapter<BleDeviceData, BleZwiftPlaySensor> {
8
8
  protected static INCYCLIST_PROFILE_NAME: LegacyProfile;
9
9
  protected static CAPABILITIES: IncyclistCapability[];
10
+ protected keyPressedHandler?: (event: any) => void;
10
11
  constructor(settings: BleDeviceSettings, props?: DeviceProperties);
11
12
  protected checkCapabilities(): Promise<void>;
12
13
  startSensor(): Promise<boolean>;
14
+ protected onKeyPressed(event: any): void;
15
+ isEqual(settings: BleDeviceSettings): boolean;
13
16
  isSame(adapter: IAdapter): boolean;
14
17
  updateSensor(peripheral: IBlePeripheral): void;
15
18
  getProfile(): LegacyProfile;
19
+ getUniqueName(): string;
20
+ getName(): string;
16
21
  getDisplayName(): string;
17
22
  mapData(deviceData: BleDeviceData): IncyclistAdapterData;
18
23
  }
@@ -13,7 +13,7 @@ type BleZwiftPlaySensorProps = {
13
13
  logger?: EventLogger;
14
14
  isTrainer?: boolean;
15
15
  };
16
- type DeviceType = 'left' | 'right' | 'click' | 'hub';
16
+ type DeviceType = 'left' | 'right' | 'click' | 'hub' | 'ride-left' | 'ride-right';
17
17
  export declare class BleZwiftPlaySensor extends TBleSensor {
18
18
  static readonly profile: LegacyProfile;
19
19
  static readonly protocol: BleProtocol;
@@ -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;
@@ -44,6 +45,7 @@ export declare class BleZwiftPlaySensor extends TBleSensor {
44
45
  reconnectSensor(): Promise<boolean>;
45
46
  stopSensor(): Promise<boolean>;
46
47
  protected getRequiredCharacteristics(): Array<string>;
48
+ getDeviceType(): DeviceType;
47
49
  onData(characteristic: string, data: Buffer, isNotify?: boolean): boolean;
48
50
  requestDataUpdate(dataId: number): Promise<void>;
49
51
  setSimulationData(data?: SimulationParam): Promise<void>;
@@ -56,6 +58,7 @@ export declare class BleZwiftPlaySensor extends TBleSensor {
56
58
  sendHubCommand(command: HubCommand): Promise<Buffer<ArrayBufferLike>>;
57
59
  protected onTrainerResponse(m: Buffer): void;
58
60
  protected onRidingData(m: Buffer): void;
61
+ protected onRideKeyPadStatus(m: Buffer): void;
59
62
  protected onDeviceInformation(m: Buffer): void;
60
63
  onClickButtonMessage(d: Buffer): void;
61
64
  onPingMessage(message: Buffer): void;
@@ -63,10 +66,10 @@ export declare class BleZwiftPlaySensor extends TBleSensor {
63
66
  read(characteristic: string, ignoreErrors?: boolean): Promise<Buffer | null>;
64
67
  pair(): Promise<boolean>;
65
68
  reset(): void;
69
+ getManufacturerData(): string;
66
70
  protected getCrypto(): ICryptoBinding;
67
71
  protected encryptedSupported(): boolean;
68
72
  protected createKeyPair(): any;
69
73
  protected setInitialState(): void;
70
- protected getManufacturerData(): string;
71
74
  }
72
75
  export {};
@@ -28,14 +28,14 @@ export default class SmartTrainerCyclingMode extends PowerBasedCyclingModeBase i
28
28
  })[];
29
29
  };
30
30
  protected gearDelta: number;
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;
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: CyclingModeConfig;
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): 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
  }
@@ -65,7 +65,7 @@ export interface RideAnalogKeyGroup {
65
65
  }
66
66
  export interface RideKeyPadStatus {
67
67
  buttonMap?: number;
68
- analogButtons?: RideAnalogKeyGroup;
68
+ analogButtons: RideAnalogKeyPress[];
69
69
  }
70
70
  export interface TrainerResponse {
71
71
  unknown?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-devices",
3
- "version": "3.0.15",
3
+ "version": "3.0.17",
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",