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.
- package/lib/cjs/activities/base/utils/helpers.js +3 -3
- package/lib/cjs/activities/ride/service.js +2 -2
- package/lib/cjs/autoupdate/api.js +1 -1
- package/lib/cjs/devices/configuration/service.js +1 -6
- package/lib/cjs/routes/base/parsers/utils.js +1 -1
- package/lib/cjs/video/VideoConversion.js +1 -1
- package/lib/cjs/workouts/base/model/Step.js +2 -2
- package/lib/cjs/workouts/base/model/Workout.js +2 -0
- package/lib/cjs/workouts/base/parsers/incyclist/Json.js +2 -2
- package/lib/cjs/workouts/base/parsers/intervals/parser.js +1 -1
- package/lib/cjs/workouts/ride/service.js +42 -17
- package/lib/esm/activities/base/utils/helpers.js +3 -3
- package/lib/esm/activities/ride/service.js +2 -2
- package/lib/esm/autoupdate/api.js +1 -1
- package/lib/esm/devices/configuration/service.js +1 -6
- package/lib/esm/routes/base/parsers/utils.js +1 -1
- package/lib/esm/video/VideoConversion.js +1 -1
- package/lib/esm/workouts/base/model/Step.js +2 -2
- package/lib/esm/workouts/base/model/Workout.js +2 -0
- package/lib/esm/workouts/base/parsers/incyclist/Json.js +2 -2
- package/lib/esm/workouts/base/parsers/intervals/parser.js +1 -1
- package/lib/esm/workouts/ride/service.js +42 -17
- package/lib/types/appstate/types.d.ts +1 -1
- package/lib/types/devices/configuration/service.d.ts +0 -1
- package/lib/types/workouts/base/model/Workout.d.ts +1 -0
- package/lib/types/workouts/base/model/types.d.ts +2 -0
- package/lib/types/workouts/ride/service.d.ts +3 -0
- package/package.json +5 -5
|
@@ -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
|
|
40
|
-
if (
|
|
41
|
-
name =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
343
|
-
if (
|
|
344
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
if (
|
|
379
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
if (
|
|
24
|
-
name =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
340
|
-
if (
|
|
341
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
if (
|
|
376
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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' | '
|
|
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.
|
|
3
|
+
"version": "1.7.112",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"gd-eventlog": "^0.1.27"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@garmin/fitsdk": "^21.
|
|
9
|
-
"axios": "^1.
|
|
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.
|
|
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.
|
|
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",
|