From 35b2fcef4d3637d6cb67818937815f935190d11f Mon Sep 17 00:00:00 2001 From: Max Heller Date: Fri, 3 Mar 2023 15:15:57 -0500 Subject: [PATCH 01/12] power zone targets --- src/Common/Dto/P2GWorkout.cs | 1 + src/Common/Dto/Peloton/RideDetails.cs | 35 +++ src/Conversion/FitConverter.cs | 87 ++++-- src/Conversion/IConverter.cs | 405 +++++++++++++++----------- src/Peloton/ApiClient.cs | 11 + src/Peloton/PelotonService.cs | 6 +- 6 files changed, 349 insertions(+), 196 deletions(-) create mode 100644 src/Common/Dto/Peloton/RideDetails.cs diff --git a/src/Common/Dto/P2GWorkout.cs b/src/Common/Dto/P2GWorkout.cs index 8c51adf0..f1e3aa31 100644 --- a/src/Common/Dto/P2GWorkout.cs +++ b/src/Common/Dto/P2GWorkout.cs @@ -11,6 +11,7 @@ public class P2GWorkout public UserData UserData { get; set; } public Workout Workout { get; set; } public WorkoutSamples WorkoutSamples { get; set; } + public RideDetails RideDetails { get; set; } public ICollection Exercises { get; set; } public dynamic Raw { get; set; } diff --git a/src/Common/Dto/Peloton/RideDetails.cs b/src/Common/Dto/Peloton/RideDetails.cs new file mode 100644 index 00000000..f4bd86f7 --- /dev/null +++ b/src/Common/Dto/Peloton/RideDetails.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace Common.Dto.Peloton +{ + public class RideDetails + { + public Ride Ride { get; set; } + public TargetMetricsData Target_Metrics_Data { get; set; } + } + + public class TargetMetricsData + { + public ICollection Target_Metrics { get; set; } + } + + public class TargetMetric + { + public Offsets Offsets { get; set; } + public string Segment_Type { get; set; } + public ICollection Metrics { get; set; } + } +} + +public class Offsets +{ + public int Start { get; set; } + public int End { get; set; } +} + +public class MetricData +{ + public string Name { get; set; } + public float Upper { get; set; } + public float Lower { get; set; } +} \ No newline at end of file diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index 160c46c8..5d181f79 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -53,6 +53,7 @@ protected override async Task>> ConvertInternalA var workout = workoutData.Workout; var workoutSamples = workoutData.WorkoutSamples; + var rideDetails = workoutData.RideDetails; var userData = workoutData.UserData; // MESSAGE ORDER MATTERS @@ -156,9 +157,10 @@ protected override async Task>> ConvertInternalA preferredLapType = settings.Format.Rowing.PreferredLapType; if ((preferredLapType == PreferredLapType.Class_Targets || preferredLapType == PreferredLapType.Default) - && workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence")?.Graph_Data is object) + && (workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence")?.Graph_Data is object + || rideDetails?.Target_Metrics_Data?.Target_Metrics?.Count > 0)) { - var stepsAndLaps = GetWorkoutStepsAndLaps(workoutSamples, startTime, sport, subSport); + var stepsAndLaps = GetWorkoutStepsAndLaps(workoutSamples, rideDetails, startTime, sport, subSport); workoutSteps = stepsAndLaps.Values.Select(v => v.Item1).ToList(); laps = stepsAndLaps.Values.Select(v => v.Item2).ToList(); } @@ -427,7 +429,7 @@ private SessionMesg GetSessionMesg(Workout workout, WorkoutSamples workoutSample return sessionMesg; } - private Dictionary> GetWorkoutStepsAndLaps(WorkoutSamples workoutSamples, Dynastream.Fit.DateTime startTime, Sport sport, SubSport subSport) + private Dictionary> GetWorkoutStepsAndLaps(WorkoutSamples workoutSamples, RideDetails rideDetails, Dynastream.Fit.DateTime startTime, Sport sport, SubSport subSport) { using var tracing = Tracing.Trace($"{nameof(FitConverter)}.{nameof(GetWorkoutStepsAndLaps)}") .WithTag(TagKey.Format, FileFormat.Fit.ToString()); @@ -437,13 +439,47 @@ private Dictionary> GetWorkoutStepsAndLaps( if (workoutSamples is null) return stepsAndLaps; - var cadenceTargets = GetCadenceTargets(workoutSamples); + var targets = GetCadenceTargets(workoutSamples); + targets = (targets is not null) ? targets : GetRideTargets(workoutSamples, rideDetails); - if (cadenceTargets is null) + if (targets is null) + return stepsAndLaps; + var (lowerTargets, upperTargets) = (targets.Graph_Data.Lower, targets.Graph_Data.Upper); + + Func parseTargetType = type => { + switch (type) { + case "resistance": + return WktStepTarget.Cadence; + case "cadence": + return WktStepTarget.Cadence; + case "power_zone": + return WktStepTarget.Power; + default: + return WktStepTarget.Invalid; + } + }; + var targetType = parseTargetType(targets.Type); + if (targetType == WktStepTarget.Invalid) return stepsAndLaps; - uint previousCadenceLower = 0; - uint previousCadenceUpper = 0; + Func intensity = targetUpper => { + switch (targetType) { + case WktStepTarget.Resistance: + if (targetUpper > 20) return Intensity.Active; + else return Intensity.Rest; + case WktStepTarget.Cadence: + if (targetUpper > 60) return Intensity.Active; + else return Intensity.Rest; + case WktStepTarget.Power: + if (targetUpper == 1) return Intensity.Rest; + else return Intensity.Active; + default: + return Intensity.Active; + } + }; + + uint previousTargetLower = 0; + uint previousTargetUpper = 0; ushort stepIndex = 0; var duration = 0; float lapDistanceInMeters = 0; @@ -454,7 +490,6 @@ private Dictionary> GetWorkoutStepsAndLaps( foreach (var secondSinceStart in workoutSamples.Seconds_Since_Pedaling_Start) { var index = secondSinceStart <= 0 ? 0 : secondSinceStart - 1; - duration++; if (speedMetrics is object && index < speedMetrics.Values.Length) { @@ -462,11 +497,11 @@ private Dictionary> GetWorkoutStepsAndLaps( lapDistanceInMeters += 1 * currentSpeedInMPS; } - var currentCadenceLower = index < cadenceTargets.Lower.Length ? (uint)cadenceTargets.Lower[index] : 0; - var currentCadenceUpper = index < cadenceTargets.Upper.Length ? (uint)cadenceTargets.Upper[index] : 0; + var currentTargetLower = index < lowerTargets.Length ? (uint)lowerTargets[index] : 0; + var currentTargetUpper = index < upperTargets.Length ? (uint)upperTargets[index] : 0; - if (currentCadenceLower != previousCadenceLower - || currentCadenceUpper != previousCadenceUpper) + if (currentTargetLower != previousTargetLower + || currentTargetUpper != previousTargetUpper) { if (workoutStep != null && lapMesg != null) { @@ -489,10 +524,10 @@ private Dictionary> GetWorkoutStepsAndLaps( workoutStep = new WorkoutStepMesg(); workoutStep.SetDurationType(WktStepDuration.Time); workoutStep.SetMessageIndex(stepIndex); - workoutStep.SetTargetType(WktStepTarget.Cadence); - workoutStep.SetCustomTargetValueHigh(currentCadenceUpper); - workoutStep.SetCustomTargetValueLow(currentCadenceLower); - workoutStep.SetIntensity(currentCadenceUpper > 60 ? Intensity.Active : Intensity.Rest); + workoutStep.SetTargetType(targetType); + workoutStep.SetCustomTargetValueHigh(currentTargetUpper); + workoutStep.SetCustomTargetValueLow(currentTargetLower); + workoutStep.SetIntensity(intensity(currentTargetUpper)); lapMesg = new LapMesg(); var lapStartTime = new Dynastream.Fit.DateTime(startTime); @@ -505,9 +540,25 @@ private Dictionary> GetWorkoutStepsAndLaps( lapMesg.SetSport(sport); lapMesg.SetSubSport(subSport); - previousCadenceLower = currentCadenceLower; - previousCadenceUpper = currentCadenceUpper; + previousTargetLower = currentTargetLower; + previousTargetUpper = currentTargetUpper; } + duration++; + } + + if (workoutStep != null && lapMesg != null) + { + workoutStep.SetDurationValue((uint)duration * 1000); // milliseconds + + var lapEndTime = new Dynastream.Fit.DateTime(startTime); + lapEndTime.Add(workoutSamples.Seconds_Since_Pedaling_Start.Count); + lapMesg.SetTotalElapsedTime(duration); + lapMesg.SetTotalTimerTime(duration); + lapMesg.SetTimestamp(lapEndTime); + lapMesg.SetEventType(EventType.Stop); + lapMesg.SetTotalDistance(lapDistanceInMeters); + + stepsAndLaps.Add(stepIndex, new Tuple(workoutStep, lapMesg)); } return stepsAndLaps; diff --git a/src/Conversion/IConverter.cs b/src/Conversion/IConverter.cs index 981b61c6..78e8c8c8 100644 --- a/src/Conversion/IConverter.cs +++ b/src/Conversion/IConverter.cs @@ -1,5 +1,5 @@ using Common; -using Common.Dto; +using Common.Dto; using Common.Dto.Garmin; using Common.Dto.Peloton; using Common.Helpers; @@ -9,29 +9,29 @@ using Dynastream.Fit; using Prometheus; using Serilog; -using System; +using System; using System.IO; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -using Metrics = Prometheus.Metrics; +using Metrics = Prometheus.Metrics; using Summary = Common.Dto.Peloton.Summary; -namespace Conversion -{ +namespace Conversion +{ public interface IConverter - { - Task ConvertAsync(P2GWorkout workoutData); - } - - public abstract class Converter : IConverter - { + { + Task ConvertAsync(P2GWorkout workoutData); + } + + public abstract class Converter : IConverter + { private static readonly Histogram WorkoutsConverted = Metrics.CreateHistogram($"{Statics.MetricPrefix}_workouts_converted_duration_seconds", "The histogram of workouts converted.", new HistogramConfiguration() - { + { LabelNames = new string[] { Common.Observe.Metrics.Label.FileType } - }); - - private static readonly ILogger _logger = LogContext.ForClass>(); - + }); + + private static readonly ILogger _logger = LogContext.ForClass>(); + private static readonly GarminDeviceInfo RowingDevice = new GarminDeviceInfo() { Name = "Epix", // Max 20 Chars @@ -60,8 +60,8 @@ public abstract class Converter : IConverter BuildMajor = 0, BuildMinor = 0, } - }; - + }; + private static readonly GarminDeviceInfo DefaultDevice = new GarminDeviceInfo() { Name = "Forerunner 945", // Max 20 Chars @@ -75,31 +75,31 @@ public abstract class Converter : IConverter BuildMajor = 0, BuildMinor = 0, } - }; - + }; + public static readonly float _metersPerMile = 1609.34f; - public FileFormat Format { get; init; } - - protected readonly ISettingsService _settingsService; - protected readonly IFileHandling _fileHandler; - + public FileFormat Format { get; init; } + + protected readonly ISettingsService _settingsService; + protected readonly IFileHandling _fileHandler; + public Converter(ISettingsService settingsService, IFileHandling fileHandler) - { - _settingsService = settingsService; - _fileHandler = fileHandler; + { + _settingsService = settingsService; + _fileHandler = fileHandler; } - protected abstract bool ShouldConvert(Format settings); - - protected abstract Task ConvertInternalAsync(P2GWorkout workoutData, Settings settings); - - protected abstract void Save(T data, string path); - + protected abstract bool ShouldConvert(Format settings); + + protected abstract Task ConvertInternalAsync(P2GWorkout workoutData, Settings settings); + + protected abstract void Save(T data, string path); + public async Task ConvertAsync(P2GWorkout workoutData) { using var tracing = Tracing.Trace($"{nameof(IConverter)}.{nameof(ConvertAsync)}.Workout")? - .WithWorkoutId(workoutData.Workout.Id) + .WithWorkoutId(workoutData.Workout.Id) .WithTag(TagKey.Format, Format.ToString()); var status = new ConvertStatus(); @@ -200,70 +200,70 @@ protected void CopyToLocalSaveDir(string sourcePath, string workoutTitle, Settin { _logger.Error(e, "Failed to backup {@Format} file for {@Workout} to directory {@Path}", Format, workoutTitle, localSaveDir); } - } - - protected System.DateTime GetStartTimeUtc(Workout workout) - { - var startTimeInSeconds = workout.Start_Time; - var dateTime = DateTimeOffset.FromUnixTimeSeconds(startTimeInSeconds); - return dateTime.UtcDateTime; - } - + } + + protected System.DateTime GetStartTimeUtc(Workout workout) + { + var startTimeInSeconds = workout.Start_Time; + var dateTime = DateTimeOffset.FromUnixTimeSeconds(startTimeInSeconds); + return dateTime.UtcDateTime; + } + protected System.DateTime GetEndTimeUtc(Workout workout, WorkoutSamples workoutSamples) { - var endTimeSeconds = workout.End_Time ?? workoutSamples.Duration + workout.Start_Time; - var dateTime = DateTimeOffset.FromUnixTimeSeconds(endTimeSeconds); + var endTimeSeconds = workout.End_Time ?? workoutSamples.Duration + workout.Start_Time; + var dateTime = DateTimeOffset.FromUnixTimeSeconds(endTimeSeconds); return dateTime.UtcDateTime; - } - - protected string GetTimeStamp(System.DateTime startTime, long offset = 0) - { - return startTime.AddSeconds(offset).ToString("yyyy-MM-ddTHH:mm:ssZ"); - } - - public static float ConvertDistanceToMeters(double value, string unit) - { - var distanceUnit = UnitHelpers.GetDistanceUnit(unit); - switch (distanceUnit) - { - case DistanceUnit.Kilometers: - return (float)value * 1000; - case DistanceUnit.Miles: - return (float)value * _metersPerMile; - case DistanceUnit.Feet: + } + + protected string GetTimeStamp(System.DateTime startTime, long offset = 0) + { + return startTime.AddSeconds(offset).ToString("yyyy-MM-ddTHH:mm:ssZ"); + } + + public static float ConvertDistanceToMeters(double value, string unit) + { + var distanceUnit = UnitHelpers.GetDistanceUnit(unit); + switch (distanceUnit) + { + case DistanceUnit.Kilometers: + return (float)value * 1000; + case DistanceUnit.Miles: + return (float)value * _metersPerMile; + case DistanceUnit.Feet: return (float)value * 0.3048f; case DistanceUnit.FiveHundredMeters: - return (float)value / 500; - case DistanceUnit.Meters: - default: - return (float)value; - } + return (float)value / 500; + case DistanceUnit.Meters: + default: + return (float)value; + } } public static float ConvertWeightToKilograms(double value, string unit) { - var weightUnit = UnitHelpers.GetWeightUnit(unit); - switch (weightUnit) - { - case WeightUnit.Pounds: - return (float)(value * 0.453592); - case WeightUnit.Kilograms: - default: - return (float)value; + var weightUnit = UnitHelpers.GetWeightUnit(unit); + switch (weightUnit) + { + case WeightUnit.Pounds: + return (float)(value * 0.453592); + case WeightUnit.Kilograms: + default: + return (float)value; } - } - - public static float GetTotalDistance(WorkoutSamples workoutSamples) - { - var distanceSummary = GetDistanceSummary(workoutSamples); - if (distanceSummary is null) return 0.0f; - - var unit = distanceSummary.Display_Unit; - return ConvertDistanceToMeters(distanceSummary.Value.GetValueOrDefault(), unit); - } - - public static float ConvertToMetersPerSecond(double? value, string displayUnit) - { + } + + public static float GetTotalDistance(WorkoutSamples workoutSamples) + { + var distanceSummary = GetDistanceSummary(workoutSamples); + if (distanceSummary is null) return 0.0f; + + var unit = distanceSummary.Display_Unit; + return ConvertDistanceToMeters(distanceSummary.Value.GetValueOrDefault(), unit); + } + + public static float ConvertToMetersPerSecond(double? value, string displayUnit) + { float val = (float)value.GetValueOrDefault(); if (val <= 0) return 0.0f; @@ -284,35 +284,35 @@ public static float ConvertToMetersPerSecond(double? value, string displayUnit) default: Log.Error("Found unknown speed unit {@Unit}", unit); return 0; - } - } - + } + } + private static Summary GetDistanceSummary(WorkoutSamples workoutSamples) { if (workoutSamples?.Summaries is null) - { - _logger.Verbose("No workout Summaries found."); + { + _logger.Verbose("No workout Summaries found."); return null; - } - - var summaries = workoutSamples.Summaries; - var distanceSummary = summaries.FirstOrDefault(s => s.Slug == "distance"); + } + + var summaries = workoutSamples.Summaries; + var distanceSummary = summaries.FirstOrDefault(s => s.Slug == "distance"); if (distanceSummary is null) _logger.Verbose("No distance slug found."); return distanceSummary; - } - + } + public static Summary GetCalorieSummary(WorkoutSamples workoutSamples) { if (workoutSamples?.Summaries is null) - { - _logger.Verbose("No workout Summaries found."); + { + _logger.Verbose("No workout Summaries found."); return null; - } - - var summaries = workoutSamples.Summaries; - var caloriesSummary = summaries.FirstOrDefault(s => s.Slug == "calories"); + } + + var summaries = workoutSamples.Summaries; + var caloriesSummary = summaries.FirstOrDefault(s => s.Slug == "calories"); if (caloriesSummary is not null) return caloriesSummary; @@ -323,46 +323,46 @@ public static Summary GetCalorieSummary(WorkoutSamples workoutSamples) _logger.Verbose("No calories slug found."); return null; - } - - protected float GetMaxSpeedMetersPerSecond(WorkoutSamples workoutSamples) - { - var speedSummary = GetSpeedSummary(workoutSamples); - if (speedSummary is null) return 0.0f; - - var max = speedSummary.Max_Value.GetValueOrDefault(); - return ConvertToMetersPerSecond(max, speedSummary.Display_Unit); - } - - protected float GetAvgSpeedMetersPerSecond(WorkoutSamples workoutSamples) - { - var speedSummary = GetSpeedSummary(workoutSamples); - if (speedSummary is null) return 0.0f; - - var avg = speedSummary.Average_Value.GetValueOrDefault(); - return ConvertToMetersPerSecond(avg, speedSummary.Display_Unit); - } - + } + + protected float GetMaxSpeedMetersPerSecond(WorkoutSamples workoutSamples) + { + var speedSummary = GetSpeedSummary(workoutSamples); + if (speedSummary is null) return 0.0f; + + var max = speedSummary.Max_Value.GetValueOrDefault(); + return ConvertToMetersPerSecond(max, speedSummary.Display_Unit); + } + + protected float GetAvgSpeedMetersPerSecond(WorkoutSamples workoutSamples) + { + var speedSummary = GetSpeedSummary(workoutSamples); + if (speedSummary is null) return 0.0f; + + var avg = speedSummary.Average_Value.GetValueOrDefault(); + return ConvertToMetersPerSecond(avg, speedSummary.Display_Unit); + } + protected float GetAvgGrade(WorkoutSamples workoutSamples) { - var gradeSummary = GetGradeSummary(workoutSamples); + var gradeSummary = GetGradeSummary(workoutSamples); if (gradeSummary is null) return 0.0f; return (float)gradeSummary.Average_Value; - } + } protected float GetMaxGrade(WorkoutSamples workoutSamples) { - var gradeSummary = GetGradeSummary(workoutSamples); + var gradeSummary = GetGradeSummary(workoutSamples); if (gradeSummary is null) return 0.0f; return (float)gradeSummary.Max_Value; - } - + } + protected Metric GetGradeSummary(WorkoutSamples workoutSamples) { return GetMetric("incline", workoutSamples); - } - + } + protected Metric GetSpeedSummary(WorkoutSamples workoutSamples) { var speed = GetMetric("speed", workoutSamples); @@ -371,24 +371,24 @@ protected Metric GetSpeedSummary(WorkoutSamples workoutSamples) speed = GetMetric("split_pace", workoutSamples); return speed; - } - - protected byte? GetUserMaxHeartRate(WorkoutSamples workoutSamples) - { - var maxZone = GetHeartRateZone(5, workoutSamples); - - return (byte?)maxZone?.Max_Value; - } - + } + + protected byte? GetUserMaxHeartRate(WorkoutSamples workoutSamples) + { + var maxZone = GetHeartRateZone(5, workoutSamples); + + return (byte?)maxZone?.Max_Value; + } + protected Zone GetHeartRateZone(int zone, WorkoutSamples workoutSamples) - { - var hrData = GetHeartRateSummary(workoutSamples); - - if (hrData is null) return null; - - return hrData.Zones?.FirstOrDefault(z => z.Slug == $"zone{zone}"); - } - + { + var hrData = GetHeartRateSummary(workoutSamples); + + if (hrData is null) return null; + + return hrData.Zones?.FirstOrDefault(z => z.Slug == $"zone{zone}"); + } + protected PowerZones CalculatePowerZones(Workout workout) { var ftpMaybe = workout.Ftp_Info?.Ftp; @@ -440,8 +440,8 @@ protected PowerZones CalculatePowerZones(Workout workout) Max_Value = int.MaxValue } }; - } - + } + protected PowerZones GetTimeInPowerZones(Workout workout, WorkoutSamples workoutSamples) { var powerZoneData = GetOutputSummary(workoutSamples); @@ -510,13 +510,13 @@ protected PowerZones GetTimeInPowerZones(Workout workout, WorkoutSamples workout protected Metric GetOutputSummary(WorkoutSamples workoutSamples) { return GetMetric("output", workoutSamples); - } - + } + protected Metric GetHeartRateSummary(WorkoutSamples workoutSamples) { return GetMetric("heart_rate", workoutSamples); - } - + } + protected static Metric GetCadenceSummary(WorkoutSamples workoutSamples, Sport sport) { if (sport == Sport.Rowing) @@ -525,30 +525,83 @@ protected static Metric GetCadenceSummary(WorkoutSamples workoutSamples, Sport s return GetMetric("cadence", workoutSamples); } - public static GraphData GetCadenceTargets(WorkoutSamples workoutSamples) + public static TargetGraphMetrics GetCadenceTargets(WorkoutSamples workoutSamples) { - var targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence")?.Graph_Data; + var targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence"); if (targets is null) - targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "stroke_rate")?.Graph_Data; + targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "stroke_rate"); return targets; - } - + } + + public static TargetGraphMetrics GetRideTargets(WorkoutSamples workoutSamples, RideDetails rideDetails) + { + var targets = rideDetails.Target_Metrics_Data.Target_Metrics.GetEnumerator(); + if (!targets.MoveNext()) + { + return null; + } + var data = new GraphData + { + Lower = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], + Upper = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], + Average = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], + }; + string targetType = null; + var defaultTarget = (0, 0, 0); + var currentTarget = defaultTarget; + var pedalingStartOffset = rideDetails.Ride.Pedaling_Start_Offset; + + foreach (var (secondsSinceStart, index) in workoutSamples.Seconds_Since_Pedaling_Start.Select((s, i) => (s, i))) + { + int offset = secondsSinceStart - 1 + pedalingStartOffset; + if (targets.Current is not null && offset > targets.Current.Offsets.End) + { + currentTarget = defaultTarget; + targets.MoveNext(); + } + if (targets.Current is not null && offset == targets.Current.Offsets.Start) + { + var metric = targets.Current.Metrics.First(m => targetType is null || m.Name == targetType); + if (metric is not null) + { + if (targetType is null) targetType = metric.Name; + currentTarget = ((int)metric.Lower, (int)metric.Upper, (int)((metric.Lower + metric.Upper) / 2)); + } + else + { + throw new Exception("Mismatched target metric types"); + } + } + data.Lower[index] = currentTarget.Item1; + data.Upper[index] = currentTarget.Item2; + data.Average[index] = currentTarget.Item3; + } + return new TargetGraphMetrics + { + Graph_Data = data, + Max = data.Upper.Max(), + Min = data.Lower.Min(), + Average = (int)data.Average.Average(), + Type = targetType, + }; + } + protected Metric GetResistanceSummary(WorkoutSamples workoutSamples) { return GetMetric("resistance", workoutSamples); - } - + } + protected static Metric GetMetric(string slug, WorkoutSamples workoutSamples) { if (workoutSamples?.Metrics is null) - { - _logger.Verbose("No workout Metrics found."); + { + _logger.Verbose("No workout Metrics found."); return null; - } - - var metric = workoutSamples.Metrics.FirstOrDefault(s => s.Slug == slug); + } + + var metric = workoutSamples.Metrics.FirstOrDefault(s => s.Slug == slug); if (metric is null) { var alts = workoutSamples.Metrics @@ -561,8 +614,8 @@ protected static Metric GetMetric(string slug, WorkoutSamples workoutSamples) _logger.Verbose($"No {slug} found."); return metric; - } - + } + protected async Task GetDeviceInfoAsync(FitnessDiscipline sport, Settings settings) { GarminDeviceInfo userProvidedDeviceInfo = await _settingsService.GetCustomDeviceInfoAsync(settings.Garmin.Email); @@ -576,8 +629,8 @@ protected async Task GetDeviceInfoAsync(FitnessDiscipline spor return RowingDevice; return DefaultDevice; - } - + } + protected ushort? GetCyclingFtp(Workout workout, UserData userData) { ushort? ftp = null; @@ -630,6 +683,6 @@ protected static Sport GetGarminSport(Workout workout) default: return Sport.Invalid; } - } - } -} + } + } +} diff --git a/src/Peloton/ApiClient.cs b/src/Peloton/ApiClient.cs index 723c1799..9351a67f 100644 --- a/src/Peloton/ApiClient.cs +++ b/src/Peloton/ApiClient.cs @@ -21,6 +21,7 @@ public interface IPelotonApi Task> GetWorkoutsAsync(DateTime fromUtc, DateTime toUtc); Task GetWorkoutByIdAsync(string id); Task GetWorkoutSamplesByIdAsync(string id); + Task GetRideDetailsByIdAsync(string id); Task GetUserDataAsync(); Task GetJoinedChallengesAsync(int userId); Task GetUserChallengeDetailsAsync(int userId, string challengeId); @@ -180,6 +181,16 @@ public async Task GetWorkoutSamplesByIdAsync(string id) .GetJsonAsync(); } + public async Task GetRideDetailsByIdAsync(string id) + { + var auth = await GetAuthAsync(); + return await $"{BaseUrl}/ride/{id}/details" + .WithCookie("peloton_session_id", auth.SessionId) + .WithCommonHeaders() + .StripSensitiveDataFromLogging(auth.Email, auth.Password) + .GetJsonAsync(); + } + public async Task GetJoinedChallengesAsync(int userId) { var auth = await GetAuthAsync(); diff --git a/src/Peloton/PelotonService.cs b/src/Peloton/PelotonService.cs index 4c63fd15..d312e485 100644 --- a/src/Peloton/PelotonService.cs +++ b/src/Peloton/PelotonService.cs @@ -272,8 +272,9 @@ public async Task GetWorkoutDetailsAsync(string workoutId) var workout = await workoutTask; var workoutSamples = await workoutSamplesTask; + var rideDetails = await _pelotonApi.GetRideDetailsByIdAsync((string)workout?["ride"]?["id"]); - var p2gWorkoutData = await BuildP2GWorkoutAsync(workoutId, workout, workoutSamples); + var p2gWorkoutData = await BuildP2GWorkoutAsync(workoutId, workout, workoutSamples, rideDetails); if (p2gWorkoutData is object) { @@ -284,7 +285,7 @@ public async Task GetWorkoutDetailsAsync(string workoutId) return p2gWorkoutData; } - private async Task BuildP2GWorkoutAsync(string workoutId, JObject workout, JObject workoutSamples) + private async Task BuildP2GWorkoutAsync(string workoutId, JObject workout, JObject workoutSamples, JObject rideDetails) { using var tracing = Tracing.Trace($"{nameof(PelotonService)}.{nameof(BuildP2GWorkoutAsync)}") .WithWorkoutId(workoutId); @@ -292,6 +293,7 @@ private async Task BuildP2GWorkoutAsync(string workoutId, JObject wo dynamic data = new JObject(); data.Workout = workout; data.WorkoutSamples = workoutSamples; + data.RideDetails = rideDetails; P2GWorkout deSerializedData = null; try From 869468a2f00ab222febc7818519bf16fd069595c Mon Sep 17 00:00:00 2001 From: Max Heller Date: Mon, 6 Mar 2023 11:30:13 -0500 Subject: [PATCH 02/12] cleanup --- src/Common/Dto/P2GWorkout.cs | 4 +- src/Common/Dto/Peloton/RideDetails.cs | 51 +++++++++++++---------- src/Common/Dto/Peloton/WorkoutSegments.cs | 13 ------ src/Peloton/ApiClient.cs | 17 ++------ src/Peloton/PelotonService.cs | 11 +++-- 5 files changed, 38 insertions(+), 58 deletions(-) delete mode 100644 src/Common/Dto/Peloton/WorkoutSegments.cs diff --git a/src/Common/Dto/P2GWorkout.cs b/src/Common/Dto/P2GWorkout.cs index f1e3aa31..8490a3ba 100644 --- a/src/Common/Dto/P2GWorkout.cs +++ b/src/Common/Dto/P2GWorkout.cs @@ -48,7 +48,7 @@ public static WorkoutType GetWorkoutType(this Workout workout) }; } - public static ICollection GetClassPlanExercises(Workout workout, RideSegments rideSegments) + public static ICollection GetClassPlanExercises(Workout workout, Segments rideSegments) { var movements = new List(); @@ -77,7 +77,7 @@ public static ICollection GetClassPlanExercises(Workout workout, Ri return movements; } - var segments = rideSegments?.Segments?.Segment_List; + var segments = rideSegments?.Segment_List; if (segments is null || segments.Count <= 0) return movements; foreach (var segment in segments) diff --git a/src/Common/Dto/Peloton/RideDetails.cs b/src/Common/Dto/Peloton/RideDetails.cs index f4bd86f7..a8a740e3 100644 --- a/src/Common/Dto/Peloton/RideDetails.cs +++ b/src/Common/Dto/Peloton/RideDetails.cs @@ -1,35 +1,40 @@ using System.Collections.Generic; -namespace Common.Dto.Peloton +namespace Common.Dto.Peloton; + +public record RideDetails +{ + public Ride Ride { get; init; } + public TargetMetricsData Target_Metrics_Data { get; init; } + public Segments Segments { get; init; } +} + +public record Segments { - public class RideDetails - { - public Ride Ride { get; set; } - public TargetMetricsData Target_Metrics_Data { get; set; } - } + public ICollection Segment_List { get; init; } +} - public class TargetMetricsData - { - public ICollection Target_Metrics { get; set; } - } +public record TargetMetricsData +{ + public ICollection Target_Metrics { get; init; } +} - public class TargetMetric - { - public Offsets Offsets { get; set; } - public string Segment_Type { get; set; } - public ICollection Metrics { get; set; } - } +public record TargetMetric +{ + public Offsets Offsets { get; init; } + public string Segment_Type { get; init; } + public ICollection Metrics { get; init; } } -public class Offsets +public record Offsets { - public int Start { get; set; } - public int End { get; set; } + public int Start { get; init; } + public int End { get; init; } } -public class MetricData +public record MetricData { - public string Name { get; set; } - public float Upper { get; set; } - public float Lower { get; set; } + public string Name { get; init; } + public float Upper { get; init; } + public float Lower { get; init; } } \ No newline at end of file diff --git a/src/Common/Dto/Peloton/WorkoutSegments.cs b/src/Common/Dto/Peloton/WorkoutSegments.cs deleted file mode 100644 index f5f26772..00000000 --- a/src/Common/Dto/Peloton/WorkoutSegments.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace Common.Dto.Peloton; - -public record RideSegments -{ - public Segments Segments { get; init; } -} - -public record Segments -{ - public ICollection Segment_List { get; init; } -} diff --git a/src/Peloton/ApiClient.cs b/src/Peloton/ApiClient.cs index 9351a67f..9b5113cb 100644 --- a/src/Peloton/ApiClient.cs +++ b/src/Peloton/ApiClient.cs @@ -21,11 +21,10 @@ public interface IPelotonApi Task> GetWorkoutsAsync(DateTime fromUtc, DateTime toUtc); Task GetWorkoutByIdAsync(string id); Task GetWorkoutSamplesByIdAsync(string id); - Task GetRideDetailsByIdAsync(string id); Task GetUserDataAsync(); Task GetJoinedChallengesAsync(int userId); Task GetUserChallengeDetailsAsync(int userId, string challengeId); - Task GetClassSegmentsAsync(string rideId); + Task GetRideDetailsAsync(string rideId); } public class ApiClient : IPelotonApi @@ -181,16 +180,6 @@ public async Task GetWorkoutSamplesByIdAsync(string id) .GetJsonAsync(); } - public async Task GetRideDetailsByIdAsync(string id) - { - var auth = await GetAuthAsync(); - return await $"{BaseUrl}/ride/{id}/details" - .WithCookie("peloton_session_id", auth.SessionId) - .WithCommonHeaders() - .StripSensitiveDataFromLogging(auth.Email, auth.Password) - .GetJsonAsync(); - } - public async Task GetJoinedChallengesAsync(int userId) { var auth = await GetAuthAsync(); @@ -215,14 +204,14 @@ public async Task GetUserChallengeDetailsAsync(int u .GetJsonAsync(); } - public async Task GetClassSegmentsAsync(string rideId) + public async Task GetRideDetailsAsync(string rideId) { var auth = await GetAuthAsync(); return await $"{BaseUrl}/ride/{rideId}/details" .WithCookie("peloton_session_id", auth.SessionId) .WithCommonHeaders() .StripSensitiveDataFromLogging(auth.Email, auth.Password) - .GetJsonAsync(); + .GetJsonAsync(); } } } diff --git a/src/Peloton/PelotonService.cs b/src/Peloton/PelotonService.cs index d312e485..32b190c9 100644 --- a/src/Peloton/PelotonService.cs +++ b/src/Peloton/PelotonService.cs @@ -272,20 +272,20 @@ public async Task GetWorkoutDetailsAsync(string workoutId) var workout = await workoutTask; var workoutSamples = await workoutSamplesTask; - var rideDetails = await _pelotonApi.GetRideDetailsByIdAsync((string)workout?["ride"]?["id"]); - var p2gWorkoutData = await BuildP2GWorkoutAsync(workoutId, workout, workoutSamples, rideDetails); + var p2gWorkoutData = await BuildP2GWorkoutAsync(workoutId, workout, workoutSamples); if (p2gWorkoutData is object) { - var workoutSegments = await _pelotonApi.GetClassSegmentsAsync(p2gWorkoutData.Workout.Ride.Id); - p2gWorkoutData.Exercises = Common.Dto.Extensions.GetClassPlanExercises(p2gWorkoutData.Workout, workoutSegments); + var rideDetails = await _pelotonApi.GetRideDetailsAsync(p2gWorkoutData.Workout.Ride.Id); + p2gWorkoutData.RideDetails = rideDetails; + p2gWorkoutData.Exercises = Common.Dto.Extensions.GetClassPlanExercises(p2gWorkoutData.Workout, rideDetails?.Segments); } return p2gWorkoutData; } - private async Task BuildP2GWorkoutAsync(string workoutId, JObject workout, JObject workoutSamples, JObject rideDetails) + private async Task BuildP2GWorkoutAsync(string workoutId, JObject workout, JObject workoutSamples) { using var tracing = Tracing.Trace($"{nameof(PelotonService)}.{nameof(BuildP2GWorkoutAsync)}") .WithWorkoutId(workoutId); @@ -293,7 +293,6 @@ private async Task BuildP2GWorkoutAsync(string workoutId, JObject wo dynamic data = new JObject(); data.Workout = workout; data.WorkoutSamples = workoutSamples; - data.RideDetails = rideDetails; P2GWorkout deSerializedData = null; try From 732a0ea9ceda30c04239b7926cec263580c96306 Mon Sep 17 00:00:00 2001 From: Max Heller Date: Mon, 6 Mar 2023 11:33:07 -0500 Subject: [PATCH 03/12] cleanup history --- src/Common/Dto/Peloton/RideDetails.cs | 78 +++++++++++++-------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Common/Dto/Peloton/RideDetails.cs b/src/Common/Dto/Peloton/RideDetails.cs index a8a740e3..1f585687 100644 --- a/src/Common/Dto/Peloton/RideDetails.cs +++ b/src/Common/Dto/Peloton/RideDetails.cs @@ -1,40 +1,40 @@ -using System.Collections.Generic; - -namespace Common.Dto.Peloton; - -public record RideDetails -{ - public Ride Ride { get; init; } - public TargetMetricsData Target_Metrics_Data { get; init; } - public Segments Segments { get; init; } -} - -public record Segments -{ - public ICollection Segment_List { get; init; } -} - -public record TargetMetricsData -{ - public ICollection Target_Metrics { get; init; } -} - -public record TargetMetric -{ - public Offsets Offsets { get; init; } - public string Segment_Type { get; init; } - public ICollection Metrics { get; init; } -} - -public record Offsets -{ - public int Start { get; init; } - public int End { get; init; } -} - -public record MetricData -{ - public string Name { get; init; } - public float Upper { get; init; } - public float Lower { get; init; } +using System.Collections.Generic; + +namespace Common.Dto.Peloton; + +public record RideDetails +{ + public Ride Ride { get; init; } + public TargetMetricsData Target_Metrics_Data { get; init; } + public Segments Segments { get; init; } +} + +public record Segments +{ + public ICollection Segment_List { get; init; } +} + +public record TargetMetricsData +{ + public ICollection Target_Metrics { get; init; } +} + +public record TargetMetric +{ + public Offsets Offsets { get; init; } + public string Segment_Type { get; init; } + public ICollection Metrics { get; init; } +} + +public record Offsets +{ + public int Start { get; init; } + public int End { get; init; } +} + +public record MetricData +{ + public string Name { get; init; } + public float Upper { get; init; } + public float Lower { get; init; } } \ No newline at end of file From 6392964a962de13b478813072f173f5ea24d4a8b Mon Sep 17 00:00:00 2001 From: Max Heller Date: Mon, 6 Mar 2023 14:10:35 -0500 Subject: [PATCH 04/12] wip --- src/Conversion/FitConverter.cs | 105 ++++++++++++++++++++++++--------- src/Conversion/IConverter.cs | 63 +++++++++++--------- 2 files changed, 112 insertions(+), 56 deletions(-) diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index 5d181f79..f780d1c2 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -160,7 +160,7 @@ protected override async Task>> ConvertInternalA && (workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence")?.Graph_Data is object || rideDetails?.Target_Metrics_Data?.Target_Metrics?.Count > 0)) { - var stepsAndLaps = GetWorkoutStepsAndLaps(workoutSamples, rideDetails, startTime, sport, subSport); + var stepsAndLaps = GetWorkoutStepsAndLaps(workout, workoutSamples, rideDetails, startTime, sport, subSport); workoutSteps = stepsAndLaps.Values.Select(v => v.Item1).ToList(); laps = stepsAndLaps.Values.Select(v => v.Item2).ToList(); } @@ -429,7 +429,7 @@ private SessionMesg GetSessionMesg(Workout workout, WorkoutSamples workoutSample return sessionMesg; } - private Dictionary> GetWorkoutStepsAndLaps(WorkoutSamples workoutSamples, RideDetails rideDetails, Dynastream.Fit.DateTime startTime, Sport sport, SubSport subSport) + private Dictionary> GetWorkoutStepsAndLaps(Workout workout, WorkoutSamples workoutSamples, RideDetails rideDetails, Dynastream.Fit.DateTime startTime, Sport sport, SubSport subSport) { using var tracing = Tracing.Trace($"{nameof(FitConverter)}.{nameof(GetWorkoutStepsAndLaps)}") .WithTag(TagKey.Format, FileFormat.Fit.ToString()); @@ -439,12 +439,12 @@ private Dictionary> GetWorkoutStepsAndLaps( if (workoutSamples is null) return stepsAndLaps; - var targets = GetCadenceTargets(workoutSamples); - targets = (targets is not null) ? targets : GetRideTargets(workoutSamples, rideDetails); + var targets = GetRideTargets(workoutSamples, rideDetails); + var cadenceTargets = GetCadenceTargets(workoutSamples); + if (cadenceTargets is not null) targets.Append(cadenceTargets); - if (targets is null) + if (targets.Count == 0) return stepsAndLaps; - var (lowerTargets, upperTargets) = (targets.Graph_Data.Lower, targets.Graph_Data.Upper); Func parseTargetType = type => { switch (type) { @@ -458,28 +458,75 @@ private Dictionary> GetWorkoutStepsAndLaps( return WktStepTarget.Invalid; } }; - var targetType = parseTargetType(targets.Type); - if (targetType == WktStepTarget.Invalid) - return stepsAndLaps; - Func intensity = targetUpper => { - switch (targetType) { + Func intensity = (type, target) => { + switch (type) { case WktStepTarget.Resistance: - if (targetUpper > 20) return Intensity.Active; + if (target > 20) return Intensity.Active; else return Intensity.Rest; case WktStepTarget.Cadence: - if (targetUpper > 60) return Intensity.Active; + if (target > 60) return Intensity.Active; else return Intensity.Rest; case WktStepTarget.Power: - if (targetUpper == 1) return Intensity.Rest; + if (target == 1) return Intensity.Rest; else return Intensity.Active; default: return Intensity.Active; } }; - uint previousTargetLower = 0; - uint previousTargetUpper = 0; + var updateStepFuncs = targets.Select>(target => + { + var type = parseTargetType(target.Type); + + Func<(uint, uint), (uint, uint)> targetRange = range => range; + switch (type) { + case WktStepTarget.Power: + var zones = CalculatePowerZones(workout); + targetRange = target => + { + Func zone = zone => + { + switch (zone) { + case 1: return zones.Zone1; + case 2: return zones.Zone2; + case 3: return zones.Zone3; + case 4: return zones.Zone4; + case 5: return zones.Zone5; + case 6: return zones.Zone6; + case 7: return zones.Zone7; + default: return new Zone(); + } + }; + var (low, high) = target; + return ((uint)zone(low).Min_Value, (uint)zone(high).Max_Value); + }; + break; + } + + return (step, target) => + { + var (low, high) = targetRange(target); + step.SetTargetType(type); + step.SetIntensity(intensity(type, target.Item2)); + switch (type) { + case WktStepTarget.Cadence: + step.SetCustomTargetCadenceLow(low); + step.SetCustomTargetCadenceHigh(high); + break; + case WktStepTarget.Power: + step.SetCustomTargetPowerLow(low); + step.SetCustomTargetPowerHigh(high); + break; + default: + step.SetCustomTargetValueLow(low); + step.SetCustomTargetValueHigh(high); + break; + } + }; + }).ToList(); + + List<(uint, uint)> previousTargets = null; ushort stepIndex = 0; var duration = 0; float lapDistanceInMeters = 0; @@ -497,11 +544,16 @@ private Dictionary> GetWorkoutStepsAndLaps( lapDistanceInMeters += 1 * currentSpeedInMPS; } - var currentTargetLower = index < lowerTargets.Length ? (uint)lowerTargets[index] : 0; - var currentTargetUpper = index < upperTargets.Length ? (uint)upperTargets[index] : 0; - - if (currentTargetLower != previousTargetLower - || currentTargetUpper != previousTargetUpper) + var currentTargets = targets.Select(target => + { + var (lowerTargets, upperTargets) = (target.Graph_Data.Lower, target.Graph_Data.Upper); + var lower = index < lowerTargets.Length ? (uint)lowerTargets[index] : 0; + var upper = index < upperTargets.Length ? (uint)upperTargets[index] : 0; + return (lower, upper); + }).ToList(); + + if (previousTargets is null + || currentTargets.Select((x, i) => (x, i)).Any(current => current.x != previousTargets[current.i])) // TODO: ugly list equality check { if (workoutStep != null && lapMesg != null) { @@ -524,10 +576,10 @@ private Dictionary> GetWorkoutStepsAndLaps( workoutStep = new WorkoutStepMesg(); workoutStep.SetDurationType(WktStepDuration.Time); workoutStep.SetMessageIndex(stepIndex); - workoutStep.SetTargetType(targetType); - workoutStep.SetCustomTargetValueHigh(currentTargetUpper); - workoutStep.SetCustomTargetValueLow(currentTargetLower); - workoutStep.SetIntensity(intensity(currentTargetUpper)); + foreach (var (target, updateStep) in currentTargets.Zip(updateStepFuncs)) + { + updateStep(workoutStep, target); + } lapMesg = new LapMesg(); var lapStartTime = new Dynastream.Fit.DateTime(startTime); @@ -540,8 +592,7 @@ private Dictionary> GetWorkoutStepsAndLaps( lapMesg.SetSport(sport); lapMesg.SetSubSport(subSport); - previousTargetLower = currentTargetLower; - previousTargetUpper = currentTargetUpper; + previousTargets = currentTargets; } duration++; } diff --git a/src/Conversion/IConverter.cs b/src/Conversion/IConverter.cs index 78e8c8c8..ce1b84ff 100644 --- a/src/Conversion/IConverter.cs +++ b/src/Conversion/IConverter.cs @@ -10,6 +10,7 @@ using Prometheus; using Serilog; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -535,22 +536,15 @@ public static TargetGraphMetrics GetCadenceTargets(WorkoutSamples workoutSamples return targets; } - public static TargetGraphMetrics GetRideTargets(WorkoutSamples workoutSamples, RideDetails rideDetails) + public static List GetRideTargets(WorkoutSamples workoutSamples, RideDetails rideDetails) { var targets = rideDetails.Target_Metrics_Data.Target_Metrics.GetEnumerator(); if (!targets.MoveNext()) { - return null; + return new List(); } - var data = new GraphData - { - Lower = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], - Upper = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], - Average = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], - }; - string targetType = null; + var targetsMap = new Dictionary(); var defaultTarget = (0, 0, 0); - var currentTarget = defaultTarget; var pedalingStartOffset = rideDetails.Ride.Pedaling_Start_Offset; foreach (var (secondsSinceStart, index) in workoutSamples.Seconds_Since_Pedaling_Start.Select((s, i) => (s, i))) @@ -558,34 +552,45 @@ public static TargetGraphMetrics GetRideTargets(WorkoutSamples workoutSamples, R int offset = secondsSinceStart - 1 + pedalingStartOffset; if (targets.Current is not null && offset > targets.Current.Offsets.End) { - currentTarget = defaultTarget; + foreach (var (type, (data, _)) in targetsMap) + targetsMap[type] = (data, defaultTarget); targets.MoveNext(); } if (targets.Current is not null && offset == targets.Current.Offsets.Start) { - var metric = targets.Current.Metrics.First(m => targetType is null || m.Name == targetType); - if (metric is not null) + foreach (var metric in targets.Current.Metrics) { - if (targetType is null) targetType = metric.Name; - currentTarget = ((int)metric.Lower, (int)metric.Upper, (int)((metric.Lower + metric.Upper) / 2)); - } - else - { - throw new Exception("Mismatched target metric types"); + if (!targetsMap.ContainsKey(metric.Name)) + { + var defaultTargetData = new GraphData + { + Lower = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], + Upper = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], + Average = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], + }; + targetsMap[metric.Name] = (defaultTargetData, defaultTarget); + } + var (data, _) = targetsMap[metric.Name]; + var currentTarget = ((int)metric.Lower, (int)metric.Upper, (int)((metric.Lower + metric.Upper) / 2)); + targetsMap[metric.Name] = (data, currentTarget); + data.Lower[index] = currentTarget.Item1; + data.Upper[index] = currentTarget.Item2; + data.Average[index] = currentTarget.Item3; } } - data.Lower[index] = currentTarget.Item1; - data.Upper[index] = currentTarget.Item2; - data.Average[index] = currentTarget.Item3; } - return new TargetGraphMetrics + return targetsMap.Select(target => { - Graph_Data = data, - Max = data.Upper.Max(), - Min = data.Lower.Min(), - Average = (int)data.Average.Average(), - Type = targetType, - }; + var data = target.Value.Item1; + return new TargetGraphMetrics + { + Graph_Data = data, + Max = data.Upper.Max(), + Min = data.Lower.Min(), + Average = (int)data.Average.Average(), + Type = target.Key, + }; + }).ToList(); } protected Metric GetResistanceSummary(WorkoutSamples workoutSamples) From 5145b1b30c473fd434496e81ac8e1be84e675c90 Mon Sep 17 00:00:00 2001 From: Max Heller Date: Tue, 7 Mar 2023 09:41:10 -0500 Subject: [PATCH 05/12] target abstraction --- src/Conversion/ITargetMetrics.cs | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/Conversion/ITargetMetrics.cs diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs new file mode 100644 index 00000000..1edba848 --- /dev/null +++ b/src/Conversion/ITargetMetrics.cs @@ -0,0 +1,97 @@ +using Dynastream.Fit; +using System.Collections.Generic; + +namespace Conversion; + +public abstract class Target +{ + public WktStepTarget TargetType; + public abstract uint TargetValue(); + public abstract uint CustomTargetValueLow(); + public abstract uint CustomTargetValueHigh(); + + public abstract Intensity GetIntensity(); + + public void ApplyToWorkoutStep(WorkoutStepMesg step) + { + var targetValue = TargetValue(); + var (low, high) = (CustomTargetValueLow(), CustomTargetValueHigh()); + switch (TargetType) { + case WktStepTarget.Cadence: + step.SetTargetCadenceZone(targetValue); + step.SetCustomTargetCadenceLow(low); + step.SetCustomTargetCadenceHigh(high); + break; + case WktStepTarget.HeartRate: + step.SetTargetHrZone(targetValue); + step.SetCustomTargetHeartRateLow(low); + step.SetCustomTargetHeartRateHigh(high); + break; + case WktStepTarget.Power: + step.SetTargetPowerZone(targetValue); + step.SetCustomTargetPowerLow(low); + step.SetCustomTargetPowerHigh(high); + break; + case WktStepTarget.Speed: + step.SetTargetSpeedZone(targetValue); + step.SetCustomTargetSpeedLow(low); + step.SetCustomTargetSpeedHigh(high); + break; + default: + step.SetTargetValue(targetValue); + step.SetCustomTargetValueLow(low); + step.SetCustomTargetValueHigh(high); + break; + } + } +} + +public class TargetZone : Target +{ + public uint Zone { get; init; } + public override uint TargetValue() { return Zone; } + public override uint CustomTargetValueLow() { return 0; } + public override uint CustomTargetValueHigh() { return 0; } + + public override Intensity GetIntensity() + { + switch (TargetType) { + case WktStepTarget.Power: + if (Zone == 1) return Intensity.Rest; + else return Intensity.Active; + case WktStepTarget.HeartRate: + if (Zone == 1) return Intensity.Rest; + else return Intensity.Active; + default: + return Intensity.Active; + } + } +} + +public class TargetRange : Target +{ + public uint Low { get; init; } + public uint High { get; init; } + public override uint TargetValue() { return 0; } + public override uint CustomTargetValueLow() { return Low; } + public override uint CustomTargetValueHigh() { return High; } + + public override Intensity GetIntensity() + { + switch (TargetType) { + case WktStepTarget.Resistance: + if (High > 20) return Intensity.Active; + else return Intensity.Rest; + case WktStepTarget.Cadence: + if (High > 60) return Intensity.Active; + else return Intensity.Rest; + default: + return Intensity.Active; + } + } +} + +public interface ITargetMetrics : IEnumerable +{ + +} \ No newline at end of file From 589a747d861092a8a2326172592e8fb609f04377 Mon Sep 17 00:00:00 2001 From: Max Heller Date: Tue, 7 Mar 2023 13:16:08 -0500 Subject: [PATCH 06/12] wip --- src/Conversion/FitConverter.cs | 165 +++++++++++++----------------- src/Conversion/IConverter.cs | 60 +---------- src/Conversion/ITargetMetrics.cs | 166 +++++++++++++++++++++++++++---- 3 files changed, 219 insertions(+), 172 deletions(-) diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index f780d1c2..5038b2a7 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -439,94 +439,66 @@ private Dictionary> GetWorkoutStepsAndLaps( if (workoutSamples is null) return stepsAndLaps; - var targets = GetRideTargets(workoutSamples, rideDetails); + var targetMetrics = GetRideTargets(workoutSamples, rideDetails); var cadenceTargets = GetCadenceTargets(workoutSamples); - if (cadenceTargets is not null) targets.Append(cadenceTargets); + if (cadenceTargets is not null) targetMetrics.Append(cadenceTargets); + + var targets = targetMetrics.Select(target => target.Get1sTargets()).ToList(); if (targets.Count == 0) return stepsAndLaps; - Func parseTargetType = type => { - switch (type) { - case "resistance": - return WktStepTarget.Cadence; - case "cadence": - return WktStepTarget.Cadence; - case "power_zone": - return WktStepTarget.Power; - default: - return WktStepTarget.Invalid; - } - }; - - Func intensity = (type, target) => { - switch (type) { - case WktStepTarget.Resistance: - if (target > 20) return Intensity.Active; - else return Intensity.Rest; - case WktStepTarget.Cadence: - if (target > 60) return Intensity.Active; - else return Intensity.Rest; - case WktStepTarget.Power: - if (target == 1) return Intensity.Rest; - else return Intensity.Active; - default: - return Intensity.Active; - } - }; + // var updateStepFuncs = targets.Select>(target => + // { + // var type = parseTargetType(target.Type); + + // Func<(uint, uint), (uint, uint)> targetRange = range => range; + // switch (type) { + // case WktStepTarget.Power: + // var zones = CalculatePowerZones(workout); + // targetRange = target => + // { + // Func zone = zone => + // { + // switch (zone) { + // case 1: return zones.Zone1; + // case 2: return zones.Zone2; + // case 3: return zones.Zone3; + // case 4: return zones.Zone4; + // case 5: return zones.Zone5; + // case 6: return zones.Zone6; + // case 7: return zones.Zone7; + // default: return new Zone(); + // } + // }; + // var (low, high) = target; + // return ((uint)zone(low).Min_Value, (uint)zone(high).Max_Value); + // }; + // break; + // } + + // return (step, target) => + // { + // var (low, high) = targetRange(target); + // step.SetTargetType(type); + // step.SetIntensity(intensity(type, target.Item2)); + // switch (type) { + // case WktStepTarget.Cadence: + // step.SetCustomTargetCadenceLow(low); + // step.SetCustomTargetCadenceHigh(high); + // break; + // case WktStepTarget.Power: + // step.SetCustomTargetPowerLow(low); + // step.SetCustomTargetPowerHigh(high); + // break; + // default: + // step.SetCustomTargetValueLow(low); + // step.SetCustomTargetValueHigh(high); + // break; + // } + // }; + // }).ToList(); - var updateStepFuncs = targets.Select>(target => - { - var type = parseTargetType(target.Type); - - Func<(uint, uint), (uint, uint)> targetRange = range => range; - switch (type) { - case WktStepTarget.Power: - var zones = CalculatePowerZones(workout); - targetRange = target => - { - Func zone = zone => - { - switch (zone) { - case 1: return zones.Zone1; - case 2: return zones.Zone2; - case 3: return zones.Zone3; - case 4: return zones.Zone4; - case 5: return zones.Zone5; - case 6: return zones.Zone6; - case 7: return zones.Zone7; - default: return new Zone(); - } - }; - var (low, high) = target; - return ((uint)zone(low).Min_Value, (uint)zone(high).Max_Value); - }; - break; - } - - return (step, target) => - { - var (low, high) = targetRange(target); - step.SetTargetType(type); - step.SetIntensity(intensity(type, target.Item2)); - switch (type) { - case WktStepTarget.Cadence: - step.SetCustomTargetCadenceLow(low); - step.SetCustomTargetCadenceHigh(high); - break; - case WktStepTarget.Power: - step.SetCustomTargetPowerLow(low); - step.SetCustomTargetPowerHigh(high); - break; - default: - step.SetCustomTargetValueLow(low); - step.SetCustomTargetValueHigh(high); - break; - } - }; - }).ToList(); - - List<(uint, uint)> previousTargets = null; ushort stepIndex = 0; var duration = 0; float lapDistanceInMeters = 0; @@ -544,16 +516,19 @@ private Dictionary> GetWorkoutStepsAndLaps( lapDistanceInMeters += 1 * currentSpeedInMPS; } - var currentTargets = targets.Select(target => + var targetsChanged = targets.Aggregate(false, (changed, target) => { - var (lowerTargets, upperTargets) = (target.Graph_Data.Lower, target.Graph_Data.Upper); - var lower = index < lowerTargets.Length ? (uint)lowerTargets[index] : 0; - var upper = index < upperTargets.Length ? (uint)upperTargets[index] : 0; - return (lower, upper); - }).ToList(); - - if (previousTargets is null - || currentTargets.Select((x, i) => (x, i)).Any(current => current.x != previousTargets[current.i])) // TODO: ugly list equality check + var prev = target.Current; + var moved = target.MoveNext(); + if (prev is null ^ target.Current is null) + return true; + else if (prev is null && target.Current is null) + return changed; + else + return changed || !target.Current.Equals(prev); + }); + + if (targetsChanged) { if (workoutStep != null && lapMesg != null) { @@ -576,10 +551,8 @@ private Dictionary> GetWorkoutStepsAndLaps( workoutStep = new WorkoutStepMesg(); workoutStep.SetDurationType(WktStepDuration.Time); workoutStep.SetMessageIndex(stepIndex); - foreach (var (target, updateStep) in currentTargets.Zip(updateStepFuncs)) - { - updateStep(workoutStep, target); - } + foreach (var target in targets) + target.Current?.ApplyToWorkoutStep(workoutStep); lapMesg = new LapMesg(); var lapStartTime = new Dynastream.Fit.DateTime(startTime); @@ -591,8 +564,6 @@ private Dictionary> GetWorkoutStepsAndLaps( lapMesg.SetLapTrigger(LapTrigger.Time); lapMesg.SetSport(sport); lapMesg.SetSubSport(subSport); - - previousTargets = currentTargets; } duration++; } diff --git a/src/Conversion/IConverter.cs b/src/Conversion/IConverter.cs index ce1b84ff..0cca69e2 100644 --- a/src/Conversion/IConverter.cs +++ b/src/Conversion/IConverter.cs @@ -526,71 +526,19 @@ protected static Metric GetCadenceSummary(WorkoutSamples workoutSamples, Sport s return GetMetric("cadence", workoutSamples); } - public static TargetGraphMetrics GetCadenceTargets(WorkoutSamples workoutSamples) + public static ITargetMetrics GetCadenceTargets(WorkoutSamples workoutSamples) { var targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence"); if (targets is null) targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "stroke_rate"); - return targets; + return new TargetGraphMetrics(targets); } - public static List GetRideTargets(WorkoutSamples workoutSamples, RideDetails rideDetails) + public static List GetRideTargets(WorkoutSamples workoutSamples, RideDetails rideDetails) { - var targets = rideDetails.Target_Metrics_Data.Target_Metrics.GetEnumerator(); - if (!targets.MoveNext()) - { - return new List(); - } - var targetsMap = new Dictionary(); - var defaultTarget = (0, 0, 0); - var pedalingStartOffset = rideDetails.Ride.Pedaling_Start_Offset; - - foreach (var (secondsSinceStart, index) in workoutSamples.Seconds_Since_Pedaling_Start.Select((s, i) => (s, i))) - { - int offset = secondsSinceStart - 1 + pedalingStartOffset; - if (targets.Current is not null && offset > targets.Current.Offsets.End) - { - foreach (var (type, (data, _)) in targetsMap) - targetsMap[type] = (data, defaultTarget); - targets.MoveNext(); - } - if (targets.Current is not null && offset == targets.Current.Offsets.Start) - { - foreach (var metric in targets.Current.Metrics) - { - if (!targetsMap.ContainsKey(metric.Name)) - { - var defaultTargetData = new GraphData - { - Lower = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], - Upper = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], - Average = new int[workoutSamples.Seconds_Since_Pedaling_Start.Count], - }; - targetsMap[metric.Name] = (defaultTargetData, defaultTarget); - } - var (data, _) = targetsMap[metric.Name]; - var currentTarget = ((int)metric.Lower, (int)metric.Upper, (int)((metric.Lower + metric.Upper) / 2)); - targetsMap[metric.Name] = (data, currentTarget); - data.Lower[index] = currentTarget.Item1; - data.Upper[index] = currentTarget.Item2; - data.Average[index] = currentTarget.Item3; - } - } - } - return targetsMap.Select(target => - { - var data = target.Value.Item1; - return new TargetGraphMetrics - { - Graph_Data = data, - Max = data.Upper.Max(), - Min = data.Lower.Min(), - Average = (int)data.Average.Average(), - Type = target.Key, - }; - }).ToList(); + return Conversion.TargetMetrics.Extract(rideDetails).ToList(); } protected Metric GetResistanceSummary(WorkoutSamples workoutSamples) diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs index 1edba848..c975ce43 100644 --- a/src/Conversion/ITargetMetrics.cs +++ b/src/Conversion/ITargetMetrics.cs @@ -1,17 +1,40 @@ using Dynastream.Fit; +using System; using System.Collections.Generic; +using System.Linq; namespace Conversion; public abstract class Target { public WktStepTarget TargetType; - public abstract uint TargetValue(); - public abstract uint CustomTargetValueLow(); - public abstract uint CustomTargetValueHigh(); + public abstract uint TargetValue(); + public abstract uint CustomTargetValueLow(); + public abstract uint CustomTargetValueHigh(); public abstract Intensity GetIntensity(); + public static Target New(WktStepTarget type, bool isZone, uint low, uint high) + { + if (isZone) + { + if (low != high) throw new Exception("Target cannot span multiple zones"); + return new TargetZone + { + TargetType = type, + Zone = (uint)low, + }; + } + else + { + return new TargetRange + { + TargetType = type, + Low = (uint)low, High = (uint)high + }; + } + } + public void ApplyToWorkoutStep(WorkoutStepMesg step) { var targetValue = TargetValue(); @@ -44,17 +67,49 @@ public void ApplyToWorkoutStep(WorkoutStepMesg step) break; } } + + public static (WktStepTarget, bool isZone) ParseTargetType(string typeName) { + switch (typeName) { + case "resistance": + return (WktStepTarget.Cadence, false); + case "cadence": + return (WktStepTarget.Cadence, false); + case "power_zone": + return (WktStepTarget.Power, true); + default: + return (WktStepTarget.Invalid, false); + } + } + + public override bool Equals(object obj) + { + return Equals(obj as Target); + } + + public bool Equals(Target other) + { + return other != null + && TargetType == other.TargetType + && TargetValue() == other.TargetValue() + && CustomTargetValueLow() == other.CustomTargetValueLow() + && CustomTargetValueHigh() == other.CustomTargetValueHigh(); + } + + public override int GetHashCode() + { + return HashCode.Combine(TargetType, TargetValue(), CustomTargetValueLow(), CustomTargetValueHigh()); + } } public class TargetZone : Target { - public uint Zone { get; init; } - public override uint TargetValue() { return Zone; } - public override uint CustomTargetValueLow() { return 0; } - public override uint CustomTargetValueHigh() { return 0; } + public uint Zone { get; init; } + public override uint TargetValue() { return Zone; } + public override uint CustomTargetValueLow() { return 0; } + public override uint CustomTargetValueHigh() { return 0; } - public override Intensity GetIntensity() - { + public override Intensity GetIntensity() + { switch (TargetType) { case WktStepTarget.Power: if (Zone == 1) return Intensity.Rest; @@ -65,19 +120,19 @@ public override Intensity GetIntensity() default: return Intensity.Active; } - } + } } public class TargetRange : Target { - public uint Low { get; init; } - public uint High { get; init; } - public override uint TargetValue() { return 0; } - public override uint CustomTargetValueLow() { return Low; } - public override uint CustomTargetValueHigh() { return High; } + public uint Low { get; init; } + public uint High { get; init; } + public override uint TargetValue() { return 0; } + public override uint CustomTargetValueLow() { return Low; } + public override uint CustomTargetValueHigh() { return High; } - public override Intensity GetIntensity() - { + public override Intensity GetIntensity() + { switch (TargetType) { case WktStepTarget.Resistance: if (High > 20) return Intensity.Active; @@ -88,10 +143,83 @@ public override Intensity GetIntensity() default: return Intensity.Active; } - } + } +} + +public interface ITargetMetrics +{ + public IEnumerator Get1sTargets(); +} + +public class TargetGraphMetrics : ITargetMetrics +{ + Common.Dto.Peloton.TargetGraphMetrics metrics; + (WktStepTarget, bool isZone) targetType; + + public TargetGraphMetrics(Common.Dto.Peloton.TargetGraphMetrics metrics) + { + this.metrics = metrics; + targetType = Target.ParseTargetType(metrics.Type); + } + + public IEnumerator Get1sTargets() + { + var (low, high) = (metrics.Graph_Data.Lower, metrics.Graph_Data.Upper); + return low.Zip(high, (low, high) => Target.New(targetType.Item1, targetType.isZone, (uint)low, (uint)high)).GetEnumerator(); + } +} + +struct Offsets +{ + public int Start { get; init; } + public int End { get; init; } } -public interface ITargetMetrics : IEnumerable +public class TargetMetrics : ITargetMetrics { + ICollection<(Offsets offsets, Target target)> targets; + + public static IEnumerable Extract(Common.Dto.Peloton.RideDetails rideDetails) + { + var pedalingStartOffset = rideDetails.Ride.Pedaling_Start_Offset; + return rideDetails.Target_Metrics_Data.Target_Metrics + .SelectMany(metric => metric.Metrics.Select(target => (target, metric))) + .GroupBy(x => x.target.Name) // TODO: does GroupBy preserve ordering? + .Select(group => + { + var (type, isZone) = Target.ParseTargetType(group.Key); + return new TargetMetrics + { + targets = group.Select(x => + { + var offsets = x.metric.Offsets; + var adjustedOffsets = new Offsets + { + Start = offsets.Start-pedalingStartOffset, + End = offsets.End-pedalingStartOffset, + }; + var target = Target.New(type, isZone, (uint)x.target.Lower, (uint)x.target.Upper); + return (adjustedOffsets, target); + }).ToList(), + }; + }); + } + + public IEnumerator Get1sTargets() + { + var targets = this.targets.GetEnumerator(); + var offset = 0; + while (targets.MoveNext()) + { + if (offset > targets.Current.offsets.End) + if (!targets.MoveNext()) break; + + if (offset >= targets.Current.offsets.Start) + yield return targets.Current.target; + else + yield return null; + offset++; + } + } } \ No newline at end of file From 5c4bb5db59797a6264282d991fe5833339fb3460 Mon Sep 17 00:00:00 2001 From: Max Heller Date: Tue, 7 Mar 2023 16:40:20 -0500 Subject: [PATCH 07/12] wip --- src/Conversion/FitConverter.cs | 39 ++++++++++++++++++++------------ src/Conversion/IConverter.cs | 3 +++ src/Conversion/ITargetMetrics.cs | 17 +++++++------- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index 5038b2a7..b9a7bfba 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -505,27 +505,38 @@ private Dictionary> GetWorkoutStepsAndLaps( WorkoutStepMesg workoutStep = null; LapMesg lapMesg = null; var speedMetrics = GetSpeedSummary(workoutSamples); + var speedMetricsEnumerator = speedMetrics?.Values.GetEnumerator(); + int? prevSecondSinceStart = null; foreach (var secondSinceStart in workoutSamples.Seconds_Since_Pedaling_Start) { - var index = secondSinceStart <= 0 ? 0 : secondSinceStart - 1; - - if (speedMetrics is object && index < speedMetrics.Values.Length) + var secondsSincePrevIter = (secondSinceStart - prevSecondSinceStart).GetValueOrDefault(1); + if (secondsSincePrevIter > 1) + _logger.Debug("skip"); + prevSecondSinceStart = secondSinceStart; + + speedMetricsEnumerator?.MoveNext(); + var speed = (double?)speedMetricsEnumerator?.Current; + if (speed is not null) { - var currentSpeedInMPS = ConvertToMetersPerSecond(speedMetrics.GetValue(index), speedMetrics.Display_Unit); - lapDistanceInMeters += 1 * currentSpeedInMPS; + var currentSpeedInMPS = ConvertToMetersPerSecond(speed, speedMetrics.Display_Unit); + lapDistanceInMeters += secondsSincePrevIter * currentSpeedInMPS; } var targetsChanged = targets.Aggregate(false, (changed, target) => { - var prev = target.Current; - var moved = target.MoveNext(); - if (prev is null ^ target.Current is null) - return true; - else if (prev is null && target.Current is null) - return changed; - else - return changed || !target.Current.Equals(prev); + Target prev = target.Current; + var moved = false; + for (int i = 0; i < secondsSincePrevIter; i++) + { + prev = target.Current; + moved |= target.MoveNext(); + if (prev is null ^ target.Current is null) + changed = true; + else if (prev is not null && target.Current is not null) + changed |= !target.Current.Equals(prev); + } + return changed; }); if (targetsChanged) @@ -565,7 +576,7 @@ private Dictionary> GetWorkoutStepsAndLaps( lapMesg.SetSport(sport); lapMesg.SetSubSport(subSport); } - duration++; + duration += secondsSincePrevIter; } if (workoutStep != null && lapMesg != null) diff --git a/src/Conversion/IConverter.cs b/src/Conversion/IConverter.cs index 0cca69e2..4713bd3e 100644 --- a/src/Conversion/IConverter.cs +++ b/src/Conversion/IConverter.cs @@ -533,6 +533,9 @@ public static ITargetMetrics GetCadenceTargets(WorkoutSamples workoutSamples) if (targets is null) targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "stroke_rate"); + if (targets is null) + return null; + return new TargetGraphMetrics(targets); } diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs index c975ce43..02b8caac 100644 --- a/src/Conversion/ITargetMetrics.cs +++ b/src/Conversion/ITargetMetrics.cs @@ -212,14 +212,15 @@ public IEnumerator Get1sTargets() var offset = 0; while (targets.MoveNext()) { - if (offset > targets.Current.offsets.End) - if (!targets.MoveNext()) break; - - if (offset >= targets.Current.offsets.Start) - yield return targets.Current.target; - else - yield return null; - offset++; + while (offset <= targets.Current.offsets.End) + { + if (offset >= targets.Current.offsets.Start) + yield return targets.Current.target; + else + yield return null; + offset++; + } + if (!targets.MoveNext()) break; } } } \ No newline at end of file From 078994908f97fbc4d59e4fd1dbd857b88c56cdee Mon Sep 17 00:00:00 2001 From: Max Heller Date: Tue, 7 Mar 2023 16:55:25 -0500 Subject: [PATCH 08/12] working --- src/Conversion/FitConverter.cs | 2 -- src/Conversion/ITargetMetrics.cs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index b9a7bfba..c032ed23 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -511,8 +511,6 @@ private Dictionary> GetWorkoutStepsAndLaps( foreach (var secondSinceStart in workoutSamples.Seconds_Since_Pedaling_Start) { var secondsSincePrevIter = (secondSinceStart - prevSecondSinceStart).GetValueOrDefault(1); - if (secondsSincePrevIter > 1) - _logger.Debug("skip"); prevSecondSinceStart = secondSinceStart; speedMetricsEnumerator?.MoveNext(); diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs index 02b8caac..a620cbc1 100644 --- a/src/Conversion/ITargetMetrics.cs +++ b/src/Conversion/ITargetMetrics.cs @@ -39,6 +39,8 @@ public void ApplyToWorkoutStep(WorkoutStepMesg step) { var targetValue = TargetValue(); var (low, high) = (CustomTargetValueLow(), CustomTargetValueHigh()); + step.SetTargetType(TargetType); + step.SetIntensity(GetIntensity()); switch (TargetType) { case WktStepTarget.Cadence: step.SetTargetCadenceZone(targetValue); From a85e06024b582d6d8396fa5539cf50f91ce3f3d2 Mon Sep 17 00:00:00 2001 From: Max Heller Date: Tue, 7 Mar 2023 17:40:35 -0500 Subject: [PATCH 09/12] wip --- src/Conversion/FitConverter.cs | 55 ++------------------------------ src/Conversion/ITargetMetrics.cs | 43 ++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index c032ed23..391d65bc 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -444,61 +444,11 @@ private Dictionary> GetWorkoutStepsAndLaps( if (cadenceTargets is not null) targetMetrics.Append(cadenceTargets); var targets = targetMetrics.Select(target => target.Get1sTargets()).ToList(); + var powerZones = CalculatePowerZones(workout); if (targets.Count == 0) return stepsAndLaps; - // var updateStepFuncs = targets.Select>(target => - // { - // var type = parseTargetType(target.Type); - - // Func<(uint, uint), (uint, uint)> targetRange = range => range; - // switch (type) { - // case WktStepTarget.Power: - // var zones = CalculatePowerZones(workout); - // targetRange = target => - // { - // Func zone = zone => - // { - // switch (zone) { - // case 1: return zones.Zone1; - // case 2: return zones.Zone2; - // case 3: return zones.Zone3; - // case 4: return zones.Zone4; - // case 5: return zones.Zone5; - // case 6: return zones.Zone6; - // case 7: return zones.Zone7; - // default: return new Zone(); - // } - // }; - // var (low, high) = target; - // return ((uint)zone(low).Min_Value, (uint)zone(high).Max_Value); - // }; - // break; - // } - - // return (step, target) => - // { - // var (low, high) = targetRange(target); - // step.SetTargetType(type); - // step.SetIntensity(intensity(type, target.Item2)); - // switch (type) { - // case WktStepTarget.Cadence: - // step.SetCustomTargetCadenceLow(low); - // step.SetCustomTargetCadenceHigh(high); - // break; - // case WktStepTarget.Power: - // step.SetCustomTargetPowerLow(low); - // step.SetCustomTargetPowerHigh(high); - // break; - // default: - // step.SetCustomTargetValueLow(low); - // step.SetCustomTargetValueHigh(high); - // break; - // } - // }; - // }).ToList(); - ushort stepIndex = 0; var duration = 0; float lapDistanceInMeters = 0; @@ -561,7 +511,8 @@ private Dictionary> GetWorkoutStepsAndLaps( workoutStep.SetDurationType(WktStepDuration.Time); workoutStep.SetMessageIndex(stepIndex); foreach (var target in targets) - target.Current?.ApplyToWorkoutStep(workoutStep); + // Convert to range targets because Garmin doesn't visualize zone targets + target.Current?.ToRange(powerZones).ApplyToWorkoutStep(workoutStep); lapMesg = new LapMesg(); var lapStartTime = new Dynastream.Fit.DateTime(startTime); diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs index a620cbc1..b65ca210 100644 --- a/src/Conversion/ITargetMetrics.cs +++ b/src/Conversion/ITargetMetrics.cs @@ -35,6 +35,8 @@ public static Target New(WktStepTarget type, bool isZone, uint low, uint high) } } + public abstract TargetRange ToRange(Common.Dto.Peloton.PowerZones powerZones); + public void ApplyToWorkoutStep(WorkoutStepMesg step) { var targetValue = TargetValue(); @@ -49,13 +51,20 @@ public void ApplyToWorkoutStep(WorkoutStepMesg step) break; case WktStepTarget.HeartRate: step.SetTargetHrZone(targetValue); - step.SetCustomTargetHeartRateLow(low); - step.SetCustomTargetHeartRateHigh(high); + if (high > 0) + { + step.SetCustomTargetHeartRateLow(low+100); + step.SetCustomTargetHeartRateHigh(high+100); + } break; case WktStepTarget.Power: step.SetTargetPowerZone(targetValue); - step.SetCustomTargetPowerLow(low); - step.SetCustomTargetPowerHigh(high); + if (high > 0) + { + // FIXME: maybe need values above zero? + step.SetCustomTargetPowerLow(low+1); + step.SetCustomTargetPowerHigh(high+1); + } break; case WktStepTarget.Speed: step.SetTargetSpeedZone(targetValue); @@ -110,6 +119,31 @@ public class TargetZone : Target public override uint CustomTargetValueLow() { return 0; } public override uint CustomTargetValueHigh() { return 0; } + public override TargetRange ToRange(Common.Dto.Peloton.PowerZones powerZones) + { + switch (TargetType) { + case WktStepTarget.Power: + Func range = zone => new TargetRange + { + TargetType = TargetType, + Low = (uint)zone.Min_Value, + High = (uint)zone.Max_Value, + }; + switch (Zone) { + case 1: return range(powerZones.Zone1); + case 2: return range(powerZones.Zone2); + case 3: return range(powerZones.Zone3); + case 4: return range(powerZones.Zone4); + case 5: return range(powerZones.Zone5); + case 6: return range(powerZones.Zone6); + case 7: return range(powerZones.Zone7); + default: throw new Exception("Invalid power zone: " + Zone); + } + default: + throw new Exception("Unhandled zone type"); + } + } + public override Intensity GetIntensity() { switch (TargetType) { @@ -132,6 +166,7 @@ public class TargetRange : Target public override uint TargetValue() { return 0; } public override uint CustomTargetValueLow() { return Low; } public override uint CustomTargetValueHigh() { return High; } + public override TargetRange ToRange(Common.Dto.Peloton.PowerZones powerZones) { return this; } public override Intensity GetIntensity() { From b01be2451082f926b267772f1ecb37f6ed791729 Mon Sep 17 00:00:00 2001 From: Max Heller Date: Tue, 7 Mar 2023 17:46:31 -0500 Subject: [PATCH 10/12] power zone targets visible on chart! --- src/Conversion/ITargetMetrics.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs index b65ca210..e832ea88 100644 --- a/src/Conversion/ITargetMetrics.cs +++ b/src/Conversion/ITargetMetrics.cs @@ -257,7 +257,6 @@ public IEnumerator Get1sTargets() yield return null; offset++; } - if (!targets.MoveNext()) break; } } } \ No newline at end of file From 5219ef00803ce2a5ad57f7664598453ddb149094 Mon Sep 17 00:00:00 2001 From: Max Heller Date: Tue, 7 Mar 2023 18:16:10 -0500 Subject: [PATCH 11/12] fix --- src/Conversion/ITargetMetrics.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs index e832ea88..43705384 100644 --- a/src/Conversion/ITargetMetrics.cs +++ b/src/Conversion/ITargetMetrics.cs @@ -51,19 +51,16 @@ public void ApplyToWorkoutStep(WorkoutStepMesg step) break; case WktStepTarget.HeartRate: step.SetTargetHrZone(targetValue); - if (high > 0) - { - step.SetCustomTargetHeartRateLow(low+100); - step.SetCustomTargetHeartRateHigh(high+100); - } + step.SetCustomTargetHeartRateLow(low); + step.SetCustomTargetHeartRateHigh(high); break; case WktStepTarget.Power: step.SetTargetPowerZone(targetValue); if (high > 0) { - // FIXME: maybe need values above zero? - step.SetCustomTargetPowerLow(low+1); - step.SetCustomTargetPowerHigh(high+1); + // Target values need to be non-zero or Garmin ignores them + step.SetCustomTargetPowerLow(Math.Max(low, 1)); + step.SetCustomTargetPowerHigh(high); } break; case WktStepTarget.Speed: From e0128adff900b93af17167d442d0711ba2fb3e3c Mon Sep 17 00:00:00 2001 From: Max Heller Date: Thu, 9 Mar 2023 11:26:44 -0500 Subject: [PATCH 12/12] cleanup --- src/Conversion/FitConverter.cs | 36 +++++++++++++++----------------- src/Conversion/IConverter.cs | 16 ++++---------- src/Conversion/ITargetMetrics.cs | 25 +++++++++++++++++----- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/Conversion/FitConverter.cs b/src/Conversion/FitConverter.cs index 391d65bc..f2df9920 100644 --- a/src/Conversion/FitConverter.cs +++ b/src/Conversion/FitConverter.cs @@ -156,13 +156,16 @@ protected override async Task>> ConvertInternalA if (sport == Sport.Rowing) preferredLapType = settings.Format.Rowing.PreferredLapType; - if ((preferredLapType == PreferredLapType.Class_Targets || preferredLapType == PreferredLapType.Default) - && (workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence")?.Graph_Data is object - || rideDetails?.Target_Metrics_Data?.Target_Metrics?.Count > 0)) + if (preferredLapType == PreferredLapType.Class_Targets || preferredLapType == PreferredLapType.Default) { var stepsAndLaps = GetWorkoutStepsAndLaps(workout, workoutSamples, rideDetails, startTime, sport, subSport); - workoutSteps = stepsAndLaps.Values.Select(v => v.Item1).ToList(); - laps = stepsAndLaps.Values.Select(v => v.Item2).ToList(); + if (stepsAndLaps.Count > 0) + { + workoutSteps = stepsAndLaps.Values.Select(v => v.Item1).ToList(); + laps = stepsAndLaps.Values.Select(v => v.Item2).ToList(); + } + else + laps = GetLaps(preferredLapType, workoutSamples, startTime, sport, subSport).ToList(); } else { @@ -439,9 +442,8 @@ private Dictionary> GetWorkoutStepsAndLaps( if (workoutSamples is null) return stepsAndLaps; - var targetMetrics = GetRideTargets(workoutSamples, rideDetails); - var cadenceTargets = GetCadenceTargets(workoutSamples); - if (cadenceTargets is not null) targetMetrics.Append(cadenceTargets); + var targetMetrics = GetRideTargets(rideDetails, workoutSamples).ToList(); + targetMetrics.AddRange(GetWorkoutTargets(workoutSamples)); var targets = targetMetrics.Select(target => target.Get1sTargets()).ToList(); var powerZones = CalculatePowerZones(workout); @@ -474,17 +476,13 @@ private Dictionary> GetWorkoutStepsAndLaps( var targetsChanged = targets.Aggregate(false, (changed, target) => { Target prev = target.Current; - var moved = false; - for (int i = 0; i < secondsSincePrevIter; i++) - { - prev = target.Current; - moved |= target.MoveNext(); - if (prev is null ^ target.Current is null) - changed = true; - else if (prev is not null && target.Current is not null) - changed |= !target.Current.Equals(prev); - } - return changed; + var moved = target.MoveNext(); + if (prev is null ^ target.Current is null) + return true; + else if (prev is not null && target.Current is not null) + return changed || !target.Current.Equals(prev); + else + return changed; }); if (targetsChanged) diff --git a/src/Conversion/IConverter.cs b/src/Conversion/IConverter.cs index 4713bd3e..f521bd5b 100644 --- a/src/Conversion/IConverter.cs +++ b/src/Conversion/IConverter.cs @@ -526,22 +526,14 @@ protected static Metric GetCadenceSummary(WorkoutSamples workoutSamples, Sport s return GetMetric("cadence", workoutSamples); } - public static ITargetMetrics GetCadenceTargets(WorkoutSamples workoutSamples) + public static IEnumerable GetWorkoutTargets(WorkoutSamples workoutSamples) { - var targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "cadence"); - - if (targets is null) - targets = workoutSamples.Target_Performance_Metrics?.Target_Graph_Metrics?.FirstOrDefault(w => w.Type == "stroke_rate"); - - if (targets is null) - return null; - - return new TargetGraphMetrics(targets); + return TargetGraphMetrics.Extract(workoutSamples); } - public static List GetRideTargets(WorkoutSamples workoutSamples, RideDetails rideDetails) + public static IEnumerable GetRideTargets(RideDetails rideDetails, WorkoutSamples workoutSamples) { - return Conversion.TargetMetrics.Extract(rideDetails).ToList(); + return Conversion.TargetMetrics.Extract(rideDetails, workoutSamples); } protected Metric GetResistanceSummary(WorkoutSamples workoutSamples) diff --git a/src/Conversion/ITargetMetrics.cs b/src/Conversion/ITargetMetrics.cs index 43705384..9a9addc4 100644 --- a/src/Conversion/ITargetMetrics.cs +++ b/src/Conversion/ITargetMetrics.cs @@ -82,6 +82,8 @@ public static (WktStepTarget, bool isZone) ParseTargetType(string typeName) { return (WktStepTarget.Cadence, false); case "cadence": return (WktStepTarget.Cadence, false); + case "stroke_rate": + return (WktStepTarget.Cadence, false); case "power_zone": return (WktStepTarget.Power, true); default: @@ -190,10 +192,13 @@ public class TargetGraphMetrics : ITargetMetrics Common.Dto.Peloton.TargetGraphMetrics metrics; (WktStepTarget, bool isZone) targetType; - public TargetGraphMetrics(Common.Dto.Peloton.TargetGraphMetrics metrics) + public static IEnumerable Extract(Common.Dto.Peloton.WorkoutSamples workoutSamples) { - this.metrics = metrics; - targetType = Target.ParseTargetType(metrics.Type); + if (workoutSamples?.Target_Performance_Metrics?.Target_Graph_Metrics is null) + return Enumerable.Empty(); + + return workoutSamples.Target_Performance_Metrics.Target_Graph_Metrics + .Select(metrics => new TargetGraphMetrics { metrics = metrics, targetType = Target.ParseTargetType(metrics.Type) }); } public IEnumerator Get1sTargets() @@ -211,10 +216,14 @@ struct Offsets public class TargetMetrics : ITargetMetrics { + ICollection secondsSincePedalingStart; ICollection<(Offsets offsets, Target target)> targets; - public static IEnumerable Extract(Common.Dto.Peloton.RideDetails rideDetails) + public static IEnumerable Extract(Common.Dto.Peloton.RideDetails rideDetails, Common.Dto.Peloton.WorkoutSamples workoutSamples) { + if (rideDetails?.Ride is null || rideDetails?.Target_Metrics_Data?.Target_Metrics is null) + return Enumerable.Empty(); + var pedalingStartOffset = rideDetails.Ride.Pedaling_Start_Offset; return rideDetails.Target_Metrics_Data.Target_Metrics .SelectMany(metric => metric.Metrics.Select(target => (target, metric))) @@ -224,6 +233,7 @@ public static IEnumerable Extract(Common.Dto.Peloton.RideDetails var (type, isZone) = Target.ParseTargetType(group.Key); return new TargetMetrics { + secondsSincePedalingStart = workoutSamples.Seconds_Since_Pedaling_Start, targets = group.Select(x => { var offsets = x.metric.Offsets; @@ -242,6 +252,7 @@ public static IEnumerable Extract(Common.Dto.Peloton.RideDetails public IEnumerator Get1sTargets() { var targets = this.targets.GetEnumerator(); + var offsets = secondsSincePedalingStart.Zip(secondsSincePedalingStart.Skip(1), (x, y) => y - x).GetEnumerator(); var offset = 0; while (targets.MoveNext()) @@ -252,7 +263,11 @@ public IEnumerator Get1sTargets() yield return targets.Current.target; else yield return null; - offset++; + + if (offsets.MoveNext()) + offset += offsets.Current; + else + yield break; } } }