incyclist-services 1.7.110 → 1.7.112

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.
@@ -36,9 +36,9 @@ const buildSummary = (activity, proposedName) => {
36
36
  }
37
37
  if (name === undefined && fileName !== undefined) {
38
38
  const parts = fileName.split(/[/\\]/);
39
- const match = /([^\\/]+)\.json/.exec(parts.at(-1));
40
- if (match?.[1])
41
- name = match[1];
39
+ const last = parts.at(-1);
40
+ if (last?.endsWith('.json'))
41
+ name = last.slice(0, -5);
42
42
  }
43
43
  let startTime;
44
44
  if (startTimeUTC)
@@ -747,7 +747,7 @@ let ActivityRideService = (() => {
747
747
  calculateTimeGap(sameDistance, current) {
748
748
  const s = sameDistance.distance - current.distance;
749
749
  const v = sameDistance.speed / 3.6;
750
- const t = v === 0 ? Infinity : s / v;
750
+ const t = Math.abs(v) < 1e-6 ? Infinity : s / v;
751
751
  sameDistance.time -= t;
752
752
  const timeDelta = sameDistance.time - current.time;
753
753
  const prefix = Math.sign(timeDelta) > 0 ? '+' : '-';
@@ -1130,7 +1130,7 @@ let ActivityRideService = (() => {
1130
1130
  const distance = v * t;
1131
1131
  this.activity.distance += distance;
1132
1132
  this.current.routeDistance += distance;
1133
- if (distance !== 0 && this.activity.routeType !== 'None') {
1133
+ if (Math.abs(distance) > 1e-6 && this.activity.routeType !== 'None') {
1134
1134
  const prev = this.current.position;
1135
1135
  if (!this.current?.route?.points && !this.current.pointsErrorLogged) {
1136
1136
  this.logEvent({ message: 'error', error: 'route does not have any points', route: this.current.route });
@@ -167,7 +167,7 @@ let IncyclistUpdatesApi = (() => {
167
167
  if (!lines?.length)
168
168
  return;
169
169
  const final = lines[lines.length - 1];
170
- const versionMatch = /.* incyclist-(.*)-full.nupkg/.exec(final);
170
+ const versionMatch = / incyclist-([^\s]+)-full\.nupkg/.exec(final);
171
171
  const version = versionMatch?.length === 2 ? versionMatch[1] : undefined;
172
172
  const path = `incyclist-${version}-setup.exe`;
173
173
  const downloadUrl = `${this.getBaseUrl()}/download/app/latest/win64/${path}`;
@@ -788,9 +788,7 @@ let DeviceConfigurationService = (() => {
788
788
  this.verifyCapabilityExists(incyclist_devices_1.IncyclistCapability.HeartRate);
789
789
  this.verifyCapabilityExists(incyclist_devices_1.IncyclistCapability.Speed);
790
790
  this.verifyCapabilityExists(incyclist_devices_1.IncyclistCapability.Cadence);
791
- if (this.hasFTControllers()) {
792
- this.verifyCapabilityExists(incyclist_devices_1.IncyclistCapability.AppControl);
793
- }
791
+ this.verifyCapabilityExists(incyclist_devices_1.IncyclistCapability.AppControl);
794
792
  if (bikeCapIdx !== -1) {
795
793
  capabilities.splice(bikeCapIdx, 1);
796
794
  }
@@ -830,9 +828,6 @@ let DeviceConfigurationService = (() => {
830
828
  if (isEnabled('wifi') && !isConfigured('wifi'))
831
829
  this.initWifiInterface();
832
830
  }
833
- hasFTControllers() {
834
- return this.getUserSettings().getValue('CONTROLLERS', false);
835
- }
836
831
  doesAppSupportsWifi() {
837
832
  const features = this.getAppState().getAppFeatures();
838
833
  const isEnabled = (i) => {
@@ -247,7 +247,7 @@ const getUtf8Data = (res) => {
247
247
  }
248
248
  }
249
249
  const str = typeof res === 'string' ? res : buf.toString('utf-8');
250
- if (str.charCodeAt(0) === 0xFEFF)
250
+ if (str.codePointAt(0) === 0xFEFF)
251
251
  return str.slice(1);
252
252
  if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF)
253
253
  return buf.subarray(3).toString('utf-8');
@@ -135,7 +135,7 @@ let VideoConversion = (() => {
135
135
  const frames = mapping?.length > 0 ? Math.max(...mapping.map(m => m.frame)) : 0;
136
136
  const framerate = this.route.details?.video?.framerate;
137
137
  const time = framerate ? Math.round(info.frames / framerate) : parseTime(info.timemark);
138
- const pct = (frames && !isNaN(frames)) ? Math.floor(info.frames / frames * 100) : undefined;
138
+ const pct = (frames && !Number.isNaN(frames)) ? Math.floor(info.frames / frames * 100) : undefined;
139
139
  const cpu = this.getCpuStats(info);
140
140
  if (frames || time) {
141
141
  this.observer.emit('convert-progress', pct, info.frames, time);
@@ -140,8 +140,8 @@ class Step {
140
140
  if (!limit) {
141
141
  return ({});
142
142
  }
143
- const sl = (limits) => this.getRemainder(isPower ? { ...limits, type } : limits, includeStepInfo);
144
- const type = isPower ? limit.type : undefined;
143
+ const sl = (limits) => this.getRemainder(isPower ? { ...limits, type, locked } : limits, includeStepInfo);
144
+ const { type, locked } = isPower ? limit : { type: undefined, locked: undefined };
145
145
  const part = (ts - this.start) / this.duration;
146
146
  if (limit.max === undefined && limit.min === undefined) {
147
147
  return sl({});
@@ -13,6 +13,7 @@ class Workout extends Segment_1.Segment {
13
13
  category;
14
14
  isLocal;
15
15
  isDeleted;
16
+ lockedPowerTargets;
16
17
  explicitId;
17
18
  constructor(opts) {
18
19
  super(opts, true);
@@ -25,6 +26,7 @@ class Workout extends Segment_1.Segment {
25
26
  this.category = opts.category;
26
27
  this.isLocal = opts.isLocal;
27
28
  this.isDeleted = opts.isDeleted;
29
+ this.lockedPowerTargets = opts.lockedPowerTargets;
28
30
  this.steps = [];
29
31
  this.start = this.end = this.duration = 0;
30
32
  opts.steps?.forEach(s => {
@@ -16,8 +16,8 @@ class JsonParser {
16
16
  try {
17
17
  const str = await this.getData(file, data);
18
18
  const parsedData = JSON.parse(str);
19
- const { name, description, category, steps, type = 'workout' } = parsedData;
20
- const workout = new Workout_1.Workout({ type, name, description, category, isLocal: true });
19
+ const { name, description, category, steps, type = 'workout', lockedPowerTargets } = parsedData;
20
+ const workout = new Workout_1.Workout({ type, name, description, category, isLocal: true, lockedPowerTargets });
21
21
  steps.forEach(step => {
22
22
  if (step.type === 'step') {
23
23
  workout.addStep(step);
@@ -113,7 +113,7 @@ let IntervalsJsonParser = (() => {
113
113
  throw new Error('Could not open file');
114
114
  }
115
115
  const resData = typeof (res.data) === 'string' ? res.data : res.data.toString('utf-8');
116
- const cleaned = resData.charCodeAt(0) === 0xFEFF ? resData.slice(1) : resData;
116
+ const cleaned = resData.codePointAt(0) === 0xFEFF ? resData.slice(1) : resData;
117
117
  return cleaned;
118
118
  }
119
119
  convertStep(step, workout) {
@@ -279,6 +279,18 @@ let WorkoutRide = (() => {
279
279
  ? this.currentLimits?.targetPower < this.currentLimits?.maxPower
280
280
  : this.currentLimits?.targetPower > this.currentLimits?.minPower;
281
281
  }
282
+ isPowerLocked(power) {
283
+ return power?.locked ?? this.workout?.lockedPowerTargets ?? false;
284
+ }
285
+ isWattFallbackAdjustment() {
286
+ const power = this.currentStep?.power;
287
+ return power?.type === 'watt' && !this.isPowerLocked(power);
288
+ }
289
+ trackTargetToBoundary(increase) {
290
+ if (this.currentLimits?.maxPower === undefined)
291
+ return;
292
+ this.currentLimits.targetPower = increase ? this.currentLimits.maxPower : this.currentLimits.minPower;
293
+ }
282
294
  getPowerRangeDeltaVal(magnitude) {
283
295
  if (!this.settings?.ftp)
284
296
  return magnitude;
@@ -308,9 +320,11 @@ let WorkoutRide = (() => {
308
320
  const label = (magnitude, increase) => {
309
321
  const sign = increase ? '+' : '-';
310
322
  const delta = increase ? magnitude : -magnitude;
311
- return this.isPowerRangeAdjustable(delta)
312
- ? `${sign}${this.getPowerRangeAdjustmentWatts(magnitude, increase)}W`
313
- : `${sign}${magnitude}%`;
323
+ if (this.isPowerRangeAdjustable(delta))
324
+ return `${sign}${this.getPowerRangeAdjustmentWatts(magnitude, increase)}W`;
325
+ if (this.isWattFallbackAdjustment())
326
+ return `${sign}${magnitude}W`;
327
+ return `${sign}${magnitude}%`;
314
328
  };
315
329
  return {
316
330
  inc5: label(5, true),
@@ -339,17 +353,22 @@ let WorkoutRide = (() => {
339
353
  this.emit('update', this.getDashboardDisplayProperties());
340
354
  return { type: 'targetPower', value: this.currentLimits.targetPower };
341
355
  }
342
- let adjustedFtp;
343
- if (this.settings?.ftp) {
344
- this.settings.ftp = this.settings.ftp * (1 + delta / 100);
356
+ const wattFallback = this.isWattFallbackAdjustment();
357
+ if (wattFallback)
358
+ this.manualPowerOffset += delta;
359
+ else {
360
+ this.settings.ftp = (this.settings.ftp ?? DEFAULT_FTP) * (1 + delta / 100);
345
361
  this.workoutList.setStartSettings(this.settings);
346
362
  this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
347
- adjustedFtp = Math.round(this.settings.ftp);
348
363
  }
349
- this.manualPowerOffset += delta;
350
364
  this.setCurrentLimits();
365
+ this.trackTargetToBoundary(true);
351
366
  this.emit('update', this.getDashboardDisplayProperties());
352
- return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
367
+ if (wattFallback) {
368
+ this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
369
+ return { type: 'targetPower', value: this.currentLimits.targetPower };
370
+ }
371
+ return { type: 'ftp', value: Math.round(this.settings.ftp) };
353
372
  }
354
373
  catch (err) {
355
374
  this.logError(err, 'powerUp');
@@ -374,17 +393,22 @@ let WorkoutRide = (() => {
374
393
  this.emit('update', this.getDashboardDisplayProperties());
375
394
  return { type: 'targetPower', value: this.currentLimits.targetPower };
376
395
  }
377
- let adjustedFtp;
378
- if (this.settings?.ftp) {
379
- this.settings.ftp = this.settings.ftp / (1 + delta / 100);
396
+ const wattFallback = this.isWattFallbackAdjustment();
397
+ if (wattFallback)
398
+ this.manualPowerOffset -= delta;
399
+ else {
400
+ this.settings.ftp = (this.settings.ftp ?? DEFAULT_FTP) / (1 + delta / 100);
380
401
  this.workoutList.setStartSettings(this.settings);
381
402
  this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
382
- adjustedFtp = Math.round(this.settings.ftp);
383
403
  }
384
- this.manualPowerOffset -= delta;
385
404
  this.setCurrentLimits();
405
+ this.trackTargetToBoundary(false);
386
406
  this.emit('update', this.getDashboardDisplayProperties());
387
- return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
407
+ if (wattFallback) {
408
+ this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
409
+ return { type: 'targetPower', value: this.currentLimits.targetPower };
410
+ }
411
+ return { type: 'ftp', value: Math.round(this.settings.ftp) };
388
412
  }
389
413
  catch (err) {
390
414
  this.logError(err, 'powerDown');
@@ -633,9 +657,10 @@ let WorkoutRide = (() => {
633
657
  if (power.type === 'pct of FTP') {
634
658
  const pct = val;
635
659
  const ftp = this.settings.ftp ?? DEFAULT_FTP;
636
- const ftpVal = this.settings.ftp ?? ftp + this.manualPowerOffset;
637
- return Math.round(pct * ftpVal / 100);
660
+ return Math.round(pct * ftp / 100);
638
661
  }
662
+ if (this.isPowerLocked(power))
663
+ return Math.round(val);
639
664
  return Math.round(val + this.manualPowerOffset);
640
665
  }
641
666
  getFtpFromUserSettings() {
@@ -19,9 +19,9 @@ export const buildSummary = (activity, proposedName) => {
19
19
  }
20
20
  if (name === undefined && fileName !== undefined) {
21
21
  const parts = fileName.split(/[/\\]/);
22
- const match = /([^\\/]+)\.json/.exec(parts.at(-1));
23
- if (match?.[1])
24
- name = match[1];
22
+ const last = parts.at(-1);
23
+ if (last?.endsWith('.json'))
24
+ name = last.slice(0, -5);
25
25
  }
26
26
  let startTime;
27
27
  if (startTimeUTC)
@@ -741,7 +741,7 @@ let ActivityRideService = (() => {
741
741
  calculateTimeGap(sameDistance, current) {
742
742
  const s = sameDistance.distance - current.distance;
743
743
  const v = sameDistance.speed / 3.6;
744
- const t = v === 0 ? Infinity : s / v;
744
+ const t = Math.abs(v) < 1e-6 ? Infinity : s / v;
745
745
  sameDistance.time -= t;
746
746
  const timeDelta = sameDistance.time - current.time;
747
747
  const prefix = Math.sign(timeDelta) > 0 ? '+' : '-';
@@ -1124,7 +1124,7 @@ let ActivityRideService = (() => {
1124
1124
  const distance = v * t;
1125
1125
  this.activity.distance += distance;
1126
1126
  this.current.routeDistance += distance;
1127
- if (distance !== 0 && this.activity.routeType !== 'None') {
1127
+ if (Math.abs(distance) > 1e-6 && this.activity.routeType !== 'None') {
1128
1128
  const prev = this.current.position;
1129
1129
  if (!this.current?.route?.points && !this.current.pointsErrorLogged) {
1130
1130
  this.logEvent({ message: 'error', error: 'route does not have any points', route: this.current.route });
@@ -164,7 +164,7 @@ let IncyclistUpdatesApi = (() => {
164
164
  if (!lines?.length)
165
165
  return;
166
166
  const final = lines[lines.length - 1];
167
- const versionMatch = /.* incyclist-(.*)-full.nupkg/.exec(final);
167
+ const versionMatch = / incyclist-([^\s]+)-full\.nupkg/.exec(final);
168
168
  const version = versionMatch?.length === 2 ? versionMatch[1] : undefined;
169
169
  const path = `incyclist-${version}-setup.exe`;
170
170
  const downloadUrl = `${this.getBaseUrl()}/download/app/latest/win64/${path}`;
@@ -782,9 +782,7 @@ let DeviceConfigurationService = (() => {
782
782
  this.verifyCapabilityExists(IncyclistCapability.HeartRate);
783
783
  this.verifyCapabilityExists(IncyclistCapability.Speed);
784
784
  this.verifyCapabilityExists(IncyclistCapability.Cadence);
785
- if (this.hasFTControllers()) {
786
- this.verifyCapabilityExists(IncyclistCapability.AppControl);
787
- }
785
+ this.verifyCapabilityExists(IncyclistCapability.AppControl);
788
786
  if (bikeCapIdx !== -1) {
789
787
  capabilities.splice(bikeCapIdx, 1);
790
788
  }
@@ -824,9 +822,6 @@ let DeviceConfigurationService = (() => {
824
822
  if (isEnabled('wifi') && !isConfigured('wifi'))
825
823
  this.initWifiInterface();
826
824
  }
827
- hasFTControllers() {
828
- return this.getUserSettings().getValue('CONTROLLERS', false);
829
- }
830
825
  doesAppSupportsWifi() {
831
826
  const features = this.getAppState().getAppFeatures();
832
827
  const isEnabled = (i) => {
@@ -236,7 +236,7 @@ export const getUtf8Data = (res) => {
236
236
  }
237
237
  }
238
238
  const str = typeof res === 'string' ? res : buf.toString('utf-8');
239
- if (str.charCodeAt(0) === 0xFEFF)
239
+ if (str.codePointAt(0) === 0xFEFF)
240
240
  return str.slice(1);
241
241
  if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF)
242
242
  return buf.subarray(3).toString('utf-8');
@@ -132,7 +132,7 @@ let VideoConversion = (() => {
132
132
  const frames = mapping?.length > 0 ? Math.max(...mapping.map(m => m.frame)) : 0;
133
133
  const framerate = this.route.details?.video?.framerate;
134
134
  const time = framerate ? Math.round(info.frames / framerate) : parseTime(info.timemark);
135
- const pct = (frames && !isNaN(frames)) ? Math.floor(info.frames / frames * 100) : undefined;
135
+ const pct = (frames && !Number.isNaN(frames)) ? Math.floor(info.frames / frames * 100) : undefined;
136
136
  const cpu = this.getCpuStats(info);
137
137
  if (frames || time) {
138
138
  this.observer.emit('convert-progress', pct, info.frames, time);
@@ -134,8 +134,8 @@ export class Step {
134
134
  if (!limit) {
135
135
  return ({});
136
136
  }
137
- const sl = (limits) => this.getRemainder(isPower ? { ...limits, type } : limits, includeStepInfo);
138
- const type = isPower ? limit.type : undefined;
137
+ const sl = (limits) => this.getRemainder(isPower ? { ...limits, type, locked } : limits, includeStepInfo);
138
+ const { type, locked } = isPower ? limit : { type: undefined, locked: undefined };
139
139
  const part = (ts - this.start) / this.duration;
140
140
  if (limit.max === undefined && limit.min === undefined) {
141
141
  return sl({});
@@ -10,6 +10,7 @@ export class Workout extends Segment {
10
10
  category;
11
11
  isLocal;
12
12
  isDeleted;
13
+ lockedPowerTargets;
13
14
  explicitId;
14
15
  constructor(opts) {
15
16
  super(opts, true);
@@ -22,6 +23,7 @@ export class Workout extends Segment {
22
23
  this.category = opts.category;
23
24
  this.isLocal = opts.isLocal;
24
25
  this.isDeleted = opts.isDeleted;
26
+ this.lockedPowerTargets = opts.lockedPowerTargets;
25
27
  this.steps = [];
26
28
  this.start = this.end = this.duration = 0;
27
29
  opts.steps?.forEach(s => {
@@ -13,8 +13,8 @@ export class JsonParser {
13
13
  try {
14
14
  const str = await this.getData(file, data);
15
15
  const parsedData = JSON.parse(str);
16
- const { name, description, category, steps, type = 'workout' } = parsedData;
17
- const workout = new Workout({ type, name, description, category, isLocal: true });
16
+ const { name, description, category, steps, type = 'workout', lockedPowerTargets } = parsedData;
17
+ const workout = new Workout({ type, name, description, category, isLocal: true, lockedPowerTargets });
18
18
  steps.forEach(step => {
19
19
  if (step.type === 'step') {
20
20
  workout.addStep(step);
@@ -110,7 +110,7 @@ let IntervalsJsonParser = (() => {
110
110
  throw new Error('Could not open file');
111
111
  }
112
112
  const resData = typeof (res.data) === 'string' ? res.data : res.data.toString('utf-8');
113
- const cleaned = resData.charCodeAt(0) === 0xFEFF ? resData.slice(1) : resData;
113
+ const cleaned = resData.codePointAt(0) === 0xFEFF ? resData.slice(1) : resData;
114
114
  return cleaned;
115
115
  }
116
116
  convertStep(step, workout) {
@@ -276,6 +276,18 @@ let WorkoutRide = (() => {
276
276
  ? this.currentLimits?.targetPower < this.currentLimits?.maxPower
277
277
  : this.currentLimits?.targetPower > this.currentLimits?.minPower;
278
278
  }
279
+ isPowerLocked(power) {
280
+ return power?.locked ?? this.workout?.lockedPowerTargets ?? false;
281
+ }
282
+ isWattFallbackAdjustment() {
283
+ const power = this.currentStep?.power;
284
+ return power?.type === 'watt' && !this.isPowerLocked(power);
285
+ }
286
+ trackTargetToBoundary(increase) {
287
+ if (this.currentLimits?.maxPower === undefined)
288
+ return;
289
+ this.currentLimits.targetPower = increase ? this.currentLimits.maxPower : this.currentLimits.minPower;
290
+ }
279
291
  getPowerRangeDeltaVal(magnitude) {
280
292
  if (!this.settings?.ftp)
281
293
  return magnitude;
@@ -305,9 +317,11 @@ let WorkoutRide = (() => {
305
317
  const label = (magnitude, increase) => {
306
318
  const sign = increase ? '+' : '-';
307
319
  const delta = increase ? magnitude : -magnitude;
308
- return this.isPowerRangeAdjustable(delta)
309
- ? `${sign}${this.getPowerRangeAdjustmentWatts(magnitude, increase)}W`
310
- : `${sign}${magnitude}%`;
320
+ if (this.isPowerRangeAdjustable(delta))
321
+ return `${sign}${this.getPowerRangeAdjustmentWatts(magnitude, increase)}W`;
322
+ if (this.isWattFallbackAdjustment())
323
+ return `${sign}${magnitude}W`;
324
+ return `${sign}${magnitude}%`;
311
325
  };
312
326
  return {
313
327
  inc5: label(5, true),
@@ -336,17 +350,22 @@ let WorkoutRide = (() => {
336
350
  this.emit('update', this.getDashboardDisplayProperties());
337
351
  return { type: 'targetPower', value: this.currentLimits.targetPower };
338
352
  }
339
- let adjustedFtp;
340
- if (this.settings?.ftp) {
341
- this.settings.ftp = this.settings.ftp * (1 + delta / 100);
353
+ const wattFallback = this.isWattFallbackAdjustment();
354
+ if (wattFallback)
355
+ this.manualPowerOffset += delta;
356
+ else {
357
+ this.settings.ftp = (this.settings.ftp ?? DEFAULT_FTP) * (1 + delta / 100);
342
358
  this.workoutList.setStartSettings(this.settings);
343
359
  this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
344
- adjustedFtp = Math.round(this.settings.ftp);
345
360
  }
346
- this.manualPowerOffset += delta;
347
361
  this.setCurrentLimits();
362
+ this.trackTargetToBoundary(true);
348
363
  this.emit('update', this.getDashboardDisplayProperties());
349
- return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
364
+ if (wattFallback) {
365
+ this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
366
+ return { type: 'targetPower', value: this.currentLimits.targetPower };
367
+ }
368
+ return { type: 'ftp', value: Math.round(this.settings.ftp) };
350
369
  }
351
370
  catch (err) {
352
371
  this.logError(err, 'powerUp');
@@ -371,17 +390,22 @@ let WorkoutRide = (() => {
371
390
  this.emit('update', this.getDashboardDisplayProperties());
372
391
  return { type: 'targetPower', value: this.currentLimits.targetPower };
373
392
  }
374
- let adjustedFtp;
375
- if (this.settings?.ftp) {
376
- this.settings.ftp = this.settings.ftp / (1 + delta / 100);
393
+ const wattFallback = this.isWattFallbackAdjustment();
394
+ if (wattFallback)
395
+ this.manualPowerOffset -= delta;
396
+ else {
397
+ this.settings.ftp = (this.settings.ftp ?? DEFAULT_FTP) / (1 + delta / 100);
377
398
  this.workoutList.setStartSettings(this.settings);
378
399
  this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
379
- adjustedFtp = Math.round(this.settings.ftp);
380
400
  }
381
- this.manualPowerOffset -= delta;
382
401
  this.setCurrentLimits();
402
+ this.trackTargetToBoundary(false);
383
403
  this.emit('update', this.getDashboardDisplayProperties());
384
- return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
404
+ if (wattFallback) {
405
+ this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
406
+ return { type: 'targetPower', value: this.currentLimits.targetPower };
407
+ }
408
+ return { type: 'ftp', value: Math.round(this.settings.ftp) };
385
409
  }
386
410
  catch (err) {
387
411
  this.logError(err, 'powerDown');
@@ -630,9 +654,10 @@ let WorkoutRide = (() => {
630
654
  if (power.type === 'pct of FTP') {
631
655
  const pct = val;
632
656
  const ftp = this.settings.ftp ?? DEFAULT_FTP;
633
- const ftpVal = this.settings.ftp ?? ftp + this.manualPowerOffset;
634
- return Math.round(pct * ftpVal / 100);
657
+ return Math.round(pct * ftp / 100);
635
658
  }
659
+ if (this.isPowerLocked(power))
660
+ return Math.round(val);
636
661
  return Math.round(val + this.manualPowerOffset);
637
662
  }
638
663
  getFtpFromUserSettings() {
@@ -1,4 +1,4 @@
1
- export type FeatureToggle = 'NEW_SEARCH_UI' | 'CONTROLLERS' | 'WEBBLE_WINDOWS' | 'MOBILE_NEXT_VIDEO';
1
+ export type FeatureToggle = 'NEW_SEARCH_UI' | 'WEBBLE_WINDOWS' | 'MOBILE_NEXT_VIDEO' | 'VIDEO_ROUTE';
2
2
  export type Interfaces = 'ant' | 'ble' | 'serial' | 'wifi' | 'tcpip';
3
3
  export type BLEFeatures = {
4
4
  services: Array<string>;
@@ -74,7 +74,6 @@ export declare class DeviceConfigurationService extends IncyclistService {
74
74
  protected verifyCapabilitySettings(): void;
75
75
  protected initCapabilties(): void;
76
76
  protected initInterfaces(): void;
77
- protected hasFTControllers(): boolean;
78
77
  protected doesAppSupportsWifi(): boolean;
79
78
  protected getAppVersion(): string;
80
79
  protected getAppChannel(): string;
@@ -8,6 +8,7 @@ export declare class Workout extends Segment implements WorkoutDefinition {
8
8
  category?: Category;
9
9
  isLocal?: boolean;
10
10
  isDeleted?: boolean;
11
+ lockedPowerTargets?: boolean;
11
12
  protected explicitId: boolean;
12
13
  constructor(opts: WorkoutDefinition);
13
14
  get hash(): string;
@@ -5,6 +5,7 @@ export type Limit = {
5
5
  export type PowerLimitType = 'watt' | 'pct of FTP';
6
6
  export interface PowerLimit extends Limit {
7
7
  type?: PowerLimitType;
8
+ locked?: boolean;
8
9
  }
9
10
  export type DataType = 'step' | 'segment' | 'workout' | 'plan' | 'scheduled';
10
11
  export interface StepDefinition {
@@ -48,6 +49,7 @@ export interface WorkoutDefinition extends SegmentDefinition {
48
49
  category?: Category;
49
50
  isLocal?: boolean;
50
51
  isDeleted?: boolean;
52
+ lockedPowerTargets?: boolean;
51
53
  }
52
54
  export interface ScheduledWorkout {
53
55
  week: number;
@@ -35,6 +35,9 @@ export declare class WorkoutRide extends IncyclistService {
35
35
  forward(): void;
36
36
  backward(): void;
37
37
  private isPowerRangeAdjustable;
38
+ private isPowerLocked;
39
+ private isWattFallbackAdjustment;
40
+ private trackTargetToBoundary;
38
41
  private getPowerRangeDeltaVal;
39
42
  private getPowerRangeAdjustmentWatts;
40
43
  getLoadButtonMode(): LoadButtonMode;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.7.110",
3
+ "version": "1.7.112",
4
4
  "peerDependencies": {
5
5
  "gd-eventlog": "^0.1.27"
6
6
  },
7
7
  "dependencies": {
8
- "@garmin/fitsdk": "^21.200.0",
9
- "axios": "^1.15.2",
8
+ "@garmin/fitsdk": "^21.213.0",
9
+ "axios": "^1.19.0",
10
10
  "incyclist-devices": "^3.0.33",
11
11
  "promise.any": "^2.0.6",
12
12
  "semver": "^7.7.4",
@@ -21,7 +21,7 @@
21
21
  "@types/jest": "^30.0.0",
22
22
  "@types/node": "^26.1.2",
23
23
  "@typescript-eslint/eslint-plugin": "^8.65.0",
24
- "@typescript-eslint/parser": "^8.66.0",
24
+ "@typescript-eslint/parser": "^8.67.0",
25
25
  "dotenv": "^17.4.2",
26
26
  "eslint": "^10.2.1",
27
27
  "formdata-node": "^6.0.3",
@@ -29,7 +29,7 @@
29
29
  "jest": "^30.3.0",
30
30
  "jsdoc": "^4.0.5",
31
31
  "ts-jest": "^29.4.9",
32
- "typedoc": "^0.28.19",
32
+ "typedoc": "^0.28.20",
33
33
  "typedoc-plugin-markdown": "^4.12.0",
34
34
  "typedoc-plugin-no-inherit": "^1.6.1",
35
35
  "typescript": "^5.9.3",