Skip to content

Commit

Permalink
[447] map strength exercises to Garmin exercises (philosowaffle#450)
Browse files Browse the repository at this point in the history
* [447] map strength and yoga exercises to Garmin exercises

* adjust fit converter

* working
  • Loading branch information
philosowaffle authored Mar 4, 2023
1 parent 6a82467 commit 09a0d8e
Show file tree
Hide file tree
Showing 20 changed files with 7,762 additions and 1,441 deletions.
84 changes: 84 additions & 0 deletions src/Common/Dto/P2GWorkout.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Common.Dto.Peloton;
using System.Collections.Generic;
using System.Linq;

namespace Common.Dto
{
Expand All @@ -9,6 +11,7 @@ public class P2GWorkout
public UserData UserData { get; set; }
public Workout Workout { get; set; }
public WorkoutSamples WorkoutSamples { get; set; }
public ICollection<P2GExercise> Exercises { get; set; }

public dynamic Raw { get; set; }

Expand Down Expand Up @@ -43,5 +46,86 @@ public static WorkoutType GetWorkoutType(this Workout workout)
_ => WorkoutType.None,
};
}

public static ICollection<P2GExercise> GetClassPlanExercises(Workout workout, RideSegments rideSegments)
{
var movements = new List<P2GExercise>();

var trackedRepData = workout?.Movement_Tracker_Data?.Completed_Movements_Summary_Data?.Repetition_Summary_Data;
if (trackedRepData is not null && trackedRepData.Count > 0)
{
foreach (var repData in trackedRepData)
{
var movement = new P2GExercise()
{
Id = repData.Movement_Id,
Name = repData.Movement_Name,
Type = repData.Is_Hold ? MovementTargetType.Time : MovementTargetType.Reps,
StartOffsetSeconds = repData.Offset,
DurationSeconds = repData.Length,
Reps = repData.Completed_Number,
Weight = new P2GWeight()
{
Unit = repData?.Weight?.FirstOrDefault()?.Weight_Data?.Weight_Unit,
Value = repData?.Weight?.FirstOrDefault().Weight_Data?.Weight_Value ?? 0
}
};
movements.Add(movement);
}

return movements;
}

var segments = rideSegments?.Segments?.Segment_List;
if (segments is null || segments.Count <= 0) return movements;

foreach (var segment in segments)
{
foreach (var subSegment in segment.SubSegments_V2)
{
var mov = subSegment.Movements.FirstOrDefault();
var movement = new P2GExercise()
{
Id = mov.Id,
Name = mov.Name,
Type = MovementTargetType.Time,
StartOffsetSeconds = subSegment.Offset.GetValueOrDefault(),
DurationSeconds = subSegment.Length.GetValueOrDefault(),
Reps = subSegment.Rounds
};
movements.Add(movement);
}
}

return movements;
}
}

public record P2GExercise
{
public string Id { get; init; }
public string Name { get; init; }
public int StartOffsetSeconds { get; init; }
public int DurationSeconds { get; init; }
public MovementTargetType Type { get; init; }
public int? Reps { get; init; }
public P2GWeight? Weight { get; init; }
}

public record P2GWeight
{
public double Value { get; init; }

/// <summary>
/// lb
/// </summary>
public string Unit { get; init; }
}

public enum MovementTargetType : byte
{
Unknown = 0,
Reps = 1,
Time = 2
}
}
5 changes: 0 additions & 5 deletions src/Common/Dto/Peloton/CompletedMovementsSummaryData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public record RepetitionSummaryData
/// True when doing the exercise for Time instead of for Reps
/// </summary>
public bool Is_Hold { get; init; }
public int Target_Number { get; init; }
public int Completed_Number { get; init; }
/// <summary>
/// Seconds offset from start.... I think?
Expand All @@ -32,10 +31,6 @@ public record RepetitionSummaryData
/// Length in seconds to complete exercise
/// </summary>
public int Length { get; init; }
/// <summary>
/// Total load of the weight lifted and number of reps
/// </summary>
public int? Total_Volume { get; init; }
public ICollection<Weight> Weight { get; init; }
}

Expand Down
36 changes: 23 additions & 13 deletions src/Common/Dto/Peloton/Segment.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
using System;
using System.Collections.Generic;

namespace Common.Dto.Peloton;

namespace Common.Dto.Peloton
public class Segment
{
public class Segment
{
public string Id { get; set; }
public int Length { get; set; }
public int Start_Time_Offset { get; set; }
public Uri Icon_Url { get; set; }
public double Intensity_In_Mets { get; set; }
public string Metrics_Type { get; set; } // enum
public string Icon_Name { get; set; }
public string Icon_Slug { get; set; }
public string Name { get; set; }
}
public int Length { get; set; }
public int Start_Time_Offset { get; set; }
public ICollection<SubSegment> SubSegments_V2 { get; set; }
}

public record SubSegment
{
public string Type { get; init; } // enum
public int? Offset { get; init; }
public int? Length { get; init; }
public int? Rounds { get; init; }
public ICollection<Movement> Movements { get; init; }
}

public record Movement
{
public string Id { get; init; }
public string Name { get; init; }

}
13 changes: 13 additions & 0 deletions src/Common/Dto/Peloton/WorkoutSegments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;

namespace Common.Dto.Peloton;

public record RideSegments
{
public Segments Segments { get; init; }
}

public record Segments
{
public ICollection<Segment> Segment_List { get; init; }
}
12 changes: 10 additions & 2 deletions src/Conversion/ExerciseMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static class ExerciseMapping

// C
/* Chest Fly */ { "45207949aa384783b5d71451f7fe1c3d", new (ExerciseCategory.Flye, FlyeExerciseName.DumbbellFlye) },
/* Clam Shell */ { "5749daaf9be3448397af9d813d760ff3", new (ExerciseCategory.HipRaise, HipRaiseExerciseName.Clams) },
/* Crunch */ { "61ac0d64602c48fba25af7e5e5dc1f97", new (ExerciseCategory.Crunch, CrunchExerciseName.Crunch) },
/* Concentrated Curl */ { "3695ef0ec2ce484faedc8ce2bfa2819d", new (ExerciseCategory.Curl, CurlExerciseName.SeatedDumbbellBicepsCurl) },

Expand All @@ -39,9 +40,11 @@ public static class ExerciseMapping
/* High Plank */ { "194cc4f6a88c4abd80afe9bbddb25915", new (ExerciseCategory.Plank, PlankExerciseName.StraightArmPlank) },
/* Hip Bridge */ { "06a504988ace45faabd927af1479f454", new (ExerciseCategory.HipRaise, HipRaiseExerciseName.BridgeWithLegExtension) },
/* Hollow Hold */ { "060174b84e3744e6a19fe4ce80411113", new (ExerciseCategory.Crunch, CrunchExerciseName.HollowRock) },
/* Hollow Rock */ { "b264d06330c5442d83ffeaff878cf31d", new (ExerciseCategory.Crunch, CrunchExerciseName.HollowRock) },

// L
/* Lateral Lunge */ { "fb63e1ea19264145ae6856eefacbcb22", new (ExerciseCategory.Lunge, LungeExerciseName.SlidingLateralLunge)},
/* Leg Lift */ { "7a5f7d80783f4b77b44dad8d6a0d2fae", new (ExerciseCategory.HipRaise, HipRaiseExerciseName.LegLift)},

// N
/* Neutral Grip Chest Press */ { "802f10996b5048d08f320d8661f13ee1", new (ExerciseCategory.BenchPress, BenchPressExerciseName.NeutralGripDumbbellBenchPress) },
Expand All @@ -56,19 +59,19 @@ public static class ExerciseMapping
/* Punches */ { "d56b610f9958400eb4c40d2385f32aaf", new (ExerciseCategory.Invalid, 0) },

// R
/* Roll Up */ { "0b853e45afb04c31968b20fc7deaa718", new (ExerciseCategory.Core, CoreExerciseName.RollUp) },
/* Russian Twist */ { "5c7b2bc65abc4c44849e2119f1338120", new (ExerciseCategory.Core, CoreExerciseName.RussianTwist) },
/* Reverse Lunge */ { "c430accc3802486a86ad2de9cb8f01cc", new (ExerciseCategory.Lunge, LungeExerciseName.ReverseSlidingLunge) },

// S
///
/* Scissor Kick */ { "f6a10df381004afba2a2b63447d9968f", new (ExerciseCategory.Crunch, CrunchExerciseName.LegLevers) },
/* Shoulder Tap */ { "5b33283433e7479390c0d5fc11722f80", new (ExerciseCategory.Plank, PlankExerciseName.StraightArmPlankWithShoulderTouch) },
/* Skull Crusher */ { "3c72e60de73d43f4b5a774c90dea90cd", new (ExerciseCategory.TricepsExtension, TricepsExtensionExerciseName.DumbbellLyingTricepsExtension) },
/* Snatch */ { "0ddf8f94acfe4c2289aef5a9bf59e8d9", new (ExerciseCategory.OlympicLift, OlympicLiftExerciseName.SingleArmDumbbellSnatch) },
/* Split Squat */ { "28833fd99466476ea273d6b94747e3db", new (ExerciseCategory.Squat, SquatExerciseName.DumbbellSplitSquat) },
///* Squat Jump */ { "", new (ExerciseCategory.Plyo, PlyoExerciseName.DumbbellJumpSquat) },

// T
/* Table Top Lateral Leg Lift */ { "5bb2d37f052e4d2faf1b0f1de4489531", new (ExerciseCategory.Plank, PlankExerciseName.KneelingSidePlankWithLegLift) },
/* Tricep Kickback */ { "da89d743904640d58e8b3f667f08783c", new (ExerciseCategory.TricepsExtension, TricepsExtensionExerciseName.DumbbellKickback) },
/* Tuck up */ { "3069e7ba28b84005b71c16a3781dda8d", new (ExerciseCategory.SitUp, SitUpExerciseName.BentKneeVUp) },
/* Twisting Mountain Climber */ { "cc70d143627c45e5b64e2cb116619899", new (ExerciseCategory.Plank, PlankExerciseName.CrossBodyMountainClimber) },
Expand All @@ -80,6 +83,11 @@ public static class ExerciseMapping
/* Wide Grip Bent Over Row */ { "d861cb497fcc4e1cba994b7a949a3bac", new (ExerciseCategory.Row, RowExerciseName.WideGripSeatedCableRow) },
/* Wide Grip Overhead Press */ { "258884d9586b45b3973228147a6b0c48", new (ExerciseCategory.ShoulderPress, ShoulderPressExerciseName.OverheadDumbbellPress) },
};

public static bool IsRest(this string pelotonExerciseId)
{
return pelotonExerciseId == "3ca251f6d68746ad91aebea5c89694ca";
}
}

public record GarminExercise
Expand Down
73 changes: 41 additions & 32 deletions src/Conversion/FitConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ protected override void Save(Tuple<string, ICollection<Mesg>> data, string path)
}
}

protected override async Task<Tuple<string, ICollection<Mesg>>> ConvertInternalAsync(Workout workout, WorkoutSamples workoutSamples, UserData userData, Settings settings)
protected override async Task<Tuple<string, ICollection<Mesg>>> ConvertInternalAsync(P2GWorkout workoutData, Settings settings)
{
using var tracing = Tracing.Trace($"{nameof(FitConverter)}.{nameof(ConvertAsync)}")
.WithTag(TagKey.Format, FileFormat.Fit.ToString())
.WithWorkoutId(workout.Id);
.WithWorkoutId(workoutData.Workout.Id);

var workout = workoutData.Workout;
var workoutSamples = workoutData.WorkoutSamples;
var userData = workoutData.UserData;

// MESSAGE ORDER MATTERS
var messages = new List<Mesg>();
Expand Down Expand Up @@ -132,7 +136,7 @@ protected override async Task<Tuple<string, ICollection<Mesg>>> ConvertInternalA

if (subSport == SubSport.StrengthTraining)
{
var sets = GetStrengthWorkoutSteps(workout, startTime, settings);
var sets = GetExerciseBasedSteps(workoutData.Exercises, startTime, settings);

// Add sets in order
foreach (var set in sets)
Expand Down Expand Up @@ -509,59 +513,64 @@ private Dictionary<int, Tuple<WorkoutStepMesg, LapMesg>> GetWorkoutStepsAndLaps(
return stepsAndLaps;
}

private ICollection<SetMesg> GetStrengthWorkoutSteps(Workout workout, Dynastream.Fit.DateTime startTime, Settings settings)
private ICollection<SetMesg> GetExerciseBasedSteps(ICollection<P2GExercise> exercises, Dynastream.Fit.DateTime startTime, Settings settings)
{
using var tracing = Tracing.Trace($"{nameof(FitConverter)}.{nameof(GetStrengthWorkoutSteps)}")
using var tracing = Tracing.Trace($"{nameof(FitConverter)}.{nameof(GetExerciseBasedSteps)}")
.WithTag(TagKey.Format, FileFormat.Fit.ToString());

var steps = new List<SetMesg>();

if (workout is null)
if (exercises is null || exercises.Count <= 0)
return steps;

var trackedMovements = workout.Movement_Tracker_Data;
if (trackedMovements is null) return steps;

var completedMovementSummary = trackedMovements.Completed_Movements_Summary_Data;
if (completedMovementSummary is null) return steps;

var repSummaryData = completedMovementSummary.Repetition_Summary_Data;
if (repSummaryData is null) return steps;

ushort stepIndex = 0;
foreach (var repdata in repSummaryData)
foreach (var p2gExercise in exercises)
{
if (!ExerciseMapping.StrengthExerciseMappings.TryGetValue(repdata.Movement_Id, out var exercise))
var isRest = p2gExercise.Id.IsRest();
GarminExercise exercise = null;
if (!isRest)
{
_logger.Error($"Found Peloton Strength exercise with no Garmin mapping: {repdata.Movement_Name} {repdata.Movement_Id}");
continue;
}
if (!ExerciseMapping.StrengthExerciseMappings.TryGetValue(p2gExercise.Id, out exercise))
{
_logger.Information($"Found Peloton Strength exercise with no Garmin mapping: {p2gExercise.Name} {p2gExercise.Id}");
continue;
}

if (exercise.ExerciseCategory == ExerciseCategory.Invalid) continue; // AMRAP -- no details provided for these segments
if (exercise.ExerciseCategory == ExerciseCategory.Invalid) continue; // AMRAP -- no details provided for these segments
}

var setMesg = new SetMesg();
var setStartTime = new Dynastream.Fit.DateTime(startTime);
setStartTime.Add(repdata.Offset);
setStartTime.Add(p2gExercise.StartOffsetSeconds);
setMesg.SetStartTime(setStartTime);
setMesg.SetDuration(repdata.Length);
setMesg.SetDuration(p2gExercise.DurationSeconds);

setMesg.SetCategory(0, exercise.ExerciseCategory);
setMesg.SetCategorySubtype(0, exercise.ExerciseName);
if (!isRest)
{
setMesg.SetCategory(0, exercise.ExerciseCategory);
setMesg.SetCategorySubtype(0, exercise.ExerciseName);
}

setMesg.SetMessageIndex(stepIndex);
setMesg.SetSetType(SetType.Active);
setMesg.SetSetType(isRest ? SetType.Rest : SetType.Active);
setMesg.SetWktStepIndex(stepIndex);

var reps = repdata.Completed_Number;
if (repdata.Is_Hold)
reps = repdata.Completed_Number / settings.Format.Strength.DefaultSecondsPerRep;
var reps = p2gExercise.Reps;
if (p2gExercise.Type == MovementTargetType.Time)
{
if (reps > 0)
reps = reps / settings.Format.Strength.DefaultSecondsPerRep;

if (reps is null)
reps = p2gExercise.DurationSeconds / settings.Format.Strength.DefaultSecondsPerRep;
}


setMesg.SetRepetitions((ushort)reps);

if (repdata.Weight is object && repdata.Weight.FirstOrDefault() is not null)
if (p2gExercise.Weight is object)
{
var weight = repdata.Weight.FirstOrDefault();
setMesg.SetWeight(ConvertWeightToKilograms(weight.Weight_Data.Weight_Value, weight.Weight_Data.Weight_Unit));
setMesg.SetWeight(ConvertWeightToKilograms(p2gExercise.Weight.Value, p2gExercise.Weight.Unit));
}

stepIndex++;
Expand Down
6 changes: 3 additions & 3 deletions src/Conversion/IConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public Converter(ISettingsService settingsService, IFileHandling fileHandler)

protected abstract bool ShouldConvert(Format settings);

protected abstract Task<T> ConvertInternalAsync(Workout workout, WorkoutSamples workoutSamples, UserData userData, Settings settings);
protected abstract Task<T> ConvertInternalAsync(P2GWorkout workoutData, Settings settings);

protected abstract void Save(T data, string path);

Expand All @@ -116,14 +116,14 @@ public async Task<ConvertStatus> ConvertAsync(P2GWorkout workoutData)
var workoutTitle = WorkoutHelper.GetUniqueTitle(workoutData.Workout);
try
{
converted = await ConvertInternalAsync(workoutData.Workout, workoutData.WorkoutSamples, workoutData.UserData, settings);
converted = await ConvertInternalAsync(workoutData, settings);
}
catch (Exception e)
{
_logger.Error(e, "Failed to convert workout data to format {@Format} {@Workout}", Format, workoutTitle);
status.Result = ConversionResult.Failed;
status.ErrorMessage = $"Unknown error while trying to convert workout data for {workoutTitle} - {e.Message}";
tracing?.AddTag("excetpion.message", e.Message);
tracing?.AddTag("exception.message", e.Message);
tracing?.AddTag("exception.stacktrace", e.StackTrace);
tracing?.AddTag("convert.success", false);
tracing?.AddTag("convert.errormessage", status.ErrorMessage);
Expand Down
Loading

0 comments on commit 09a0d8e

Please sign in to comment.