Garmin: Swimming workout intervals

This commit is contained in:
José Rebelo
2025-04-26 15:59:32 +01:00
parent 5e7c5ac059
commit 431d25b935
9 changed files with 165 additions and 57 deletions
@@ -14,16 +14,18 @@ import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableRowEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableBuilder;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
@@ -211,6 +213,10 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.add(STEP_LENGTH_AVG, session.getAvgStepLength(), UNIT_MM);
if (session.getTotalCalories() != null) {
summaryData.add(CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL);
if (session.getRestingCalories() != null) {
summaryData.add(CALORIES_ACTIVE, session.getTotalCalories() - session.getRestingCalories(), UNIT_KCAL);
summaryData.add(CALORIES_RESTING, session.getRestingCalories(), UNIT_KCAL);
}
}
if (session.getEstimatedSweatLoss() != null) {
summaryData.add(ESTIMATED_SWEAT_LOSS, session.getEstimatedSweatLoss(), UNIT_ML);
@@ -305,7 +311,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
context.getString(
R.string.range_percentage_float,
session.getAvgStanceTimeBalance(),
100f - session.getAvgStanceTimeBalance()
100f - session.getAvgStanceTimeBalance()
)
);
}
@@ -505,61 +511,65 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.add(TRAINING_STRESS_SCORE, session.getTrainingStressScore(), UNIT_NONE);
if (!sets.isEmpty()) {
final boolean anyReps = sets.stream().anyMatch(s -> s.getRepetitions() != null);
final boolean anyWeight = sets.stream().anyMatch(s -> s.getWeight() != null);
final List<ActivitySummaryValue> header = new LinkedList<>();
header.add(new ActivitySummaryValue("set"));
header.add(new ActivitySummaryValue("workout_set_reps"));
header.add(new ActivitySummaryValue("menuitem_weight"));
header.add(new ActivitySummaryValue("activity_detail_duration_label"));
summaryData.add(
"sets_header",
new ActivitySummaryTableRowEntry(
SETS,
header,
true,
true
)
);
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(SETS, "sets_header", Arrays.asList(
"set",
"workout_set_reps",
"menuitem_weight",
"activity_detail_duration_label"
));
int i = 1;
for (final FitSet set : sets) {
if (set.getSetType() != null && set.getDuration() != null && set.getSetType() == 1) {
final List<ActivitySummaryValue> columns = new LinkedList<>();
columns.add(new ActivitySummaryValue(i, UNIT_NONE));
if (set.getRepetitions() != null) {
columns.add(new ActivitySummaryValue(String.valueOf(set.getRepetitions())));
} else {
columns.add(new ActivitySummaryValue("stats_empty_value"));
}
if (set.getWeight() != null) {
columns.add(new ActivitySummaryValue(set.getWeight(), weightUnit));
} else {
columns.add(new ActivitySummaryValue("stats_empty_value"));
}
columns.add(new ActivitySummaryValue(set.getDuration().longValue(), UNIT_SECONDS));
summaryData.add(
tableBuilder.addRow(
"set_" + i,
new ActivitySummaryTableRowEntry(
SETS,
columns,
false,
true
Arrays.asList(
new ActivitySummaryValue(i, UNIT_NONE),
new ActivitySummaryValue(set.getRepetitions() != null ? String.valueOf(set.getRepetitions()) : null),
new ActivitySummaryValue(set.getWeight(), weightUnit),
new ActivitySummaryValue(set.getDuration().longValue(), UNIT_SECONDS)
)
);
i++;
}
}
tableBuilder.addToSummaryData(summaryData);
}
if (!laps.isEmpty()) {
final boolean anySwolf = laps.stream().anyMatch(l -> l.getAvgSwolf() != null);
// FIXME: For now we only support swimming intervals
final boolean anyValidLaps = laps.stream()
.anyMatch(lap -> lap.getTotalDistance() != null && lap.getTotalDistance() != 0 && lap.getSwimStyle() != null);
if (anyValidLaps) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_INTERVALS, "intervals_header", Arrays.asList(
"#",
"swimming_stroke",
"Distance",
"pref_header_time"
));
int i = 1;
for (final FitLap lap : laps) {
if (lap.getTotalDistance() == null || lap.getTotalDistance() == 0) {
continue;
}
tableBuilder.addRow(
"interval_" + i,
Arrays.asList(
new ActivitySummaryValue(i, UNIT_NONE),
new ActivitySummaryValue(lap.getSwimStyle() != null ? context.getString(lap.getSwimStyle().getNameResId()) : null, UNIT_NONE),
new ActivitySummaryValue(lap.getTotalDistance(), UNIT_METERS),
new ActivitySummaryValue(lap.getTotalTimerTime(), UNIT_SECONDS)
)
);
i++;
}
tableBuilder.addToSummaryData(summaryData);
}
summaryData.add(
@@ -446,13 +446,24 @@ public enum ActivityKind {
}
public enum CycleUnit {
NONE,
UNKNOWN,
STEPS,
STROKES,
JUMPS,
REPS,
REVOLUTIONS,
SWINGS,
NONE("n_a"),
UNKNOWN("unknown"),
STEPS("steps"),
STROKES("strokes"),
JUMPS("Jumps"),
REPS("workout_set_reps"),
REVOLUTIONS("workout_revolutions"),
SWINGS("workout_swings"),
;
private final String name;
CycleUnit(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
@@ -156,6 +156,8 @@ public class ActivitySummaryEntries {
public static final String SWIM_AVG_CADENCE = "swim_avg_cadence";
public static final String CALORIES_BURNT = "caloriesBurnt";
public static final String CALORIES_ACTIVE = "active_calories";
public static final String CALORIES_RESTING = "restingCalories";
public static final String TRAINING_EFFECT_AEROBIC = "aerobicTrainingEffect";
public static final String TRAINING_EFFECT_ANAEROBIC = "anaerobicTrainingEffect";
public static final String WORKOUT_LOAD = "currentWorkoutLoad";
@@ -226,6 +228,7 @@ public class ActivitySummaryEntries {
public static final String GROUP_TRAINING_EFFECT = "TrainingEffect";
public static final String GROUP_LAPS = "laps";
public static final String GROUP_RUNNING_FORM = "RunningForm";
public static final String GROUP_INTERVALS = "workout_intervals";
/**
* Used to signal that this activity has a gps track. This is currently used by ActivitySummaryDetail
@@ -14,6 +14,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSwimStyle;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTemperature;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherAqi;
@@ -59,6 +60,8 @@ public class FieldDefinitionFactory {
return new FieldDefinitionWeatherAqi(localNumber, size, baseType, name);
case COORDINATE:
return new FieldDefinitionCoordinate(localNumber, size, baseType, name);
case SWIM_STYLE:
return new FieldDefinitionSwimStyle(localNumber, size, baseType, name);
default:
return new FieldDefinition(localNumber, size, baseType, name);
}
@@ -81,6 +84,7 @@ public class FieldDefinitionFactory {
LANGUAGE,
SLEEP_STAGE,
WEATHER_AQI,
COORDINATE
COORDINATE,
SWIM_STYLE,
}
}
@@ -198,7 +198,7 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(32, BaseType.UINT16, "num_lengths"),
new FieldDefinitionPrimitive(35, BaseType.UINT16, "first_length_index"),
new FieldDefinitionPrimitive(37, BaseType.UINT16, "avg_stroke_distance"),
new FieldDefinitionPrimitive(38, BaseType.ENUM, "swim_style"), // 0 freestyle 2 breaststroke 5 mixed
new FieldDefinitionPrimitive(38, BaseType.ENUM, "swim_style", FieldDefinitionFactory.FIELD.SWIM_STYLE),
new FieldDefinitionPrimitive(39, BaseType.ENUM, "sub_sport"), // 17 = lap swimming
new FieldDefinitionPrimitive(40, BaseType.UINT16, "num_active_lengths"),
new FieldDefinitionPrimitive(73, BaseType.UINT16, "avg_swolf"),
@@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSwimStyle;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherAqi;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherCondition;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@@ -274,6 +275,8 @@ public class FitCodeGen {
return FieldDefinitionWeatherAqi.AQI_LEVELS.class;
case COORDINATE:
return Double.class;
case SWIM_STYLE:
return FieldDefinitionSwimStyle.SwimStyle.class;
}
throw new RuntimeException("Unknown field type " + primitive.getType());
@@ -0,0 +1,69 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
public class FieldDefinitionSwimStyle extends FieldDefinition {
public FieldDefinitionSwimStyle(int localNumber, int size, BaseType baseType, String name) {
super(localNumber, size, baseType, name, 1, 0);
}
@Override
public Object decode(ByteBuffer byteBuffer) {
int raw = (int) baseType.decode(byteBuffer, scale, offset);
return SwimStyle.fromId(raw);
}
@Override
public void encode(ByteBuffer byteBuffer, Object o) {
if (o instanceof SwimStyle) {
baseType.encode(byteBuffer, (((SwimStyle) o).getId()), scale, offset);
return;
}
baseType.encode(byteBuffer, o, scale, offset);
}
public enum SwimStyle {
FREESTYLE(0, R.string.freestyle),
BACKSTROKE(1, R.string.backstroke),
BREASTSTROKE(2, R.string.breaststroke),
BUTTERFLY(3, R.string.swim_style_butterfly),
DRILL(4, R.string.swim_style_drill),
MIXED(5, R.string.swim_style_mixed),
;
private final int id;
private final int nameResId;
SwimStyle(int i, int nameResId) {
this.id = i;
this.nameResId = nameResId;
}
@Nullable
public static SwimStyle fromId(int id) {
for (SwimStyle language : SwimStyle.values()) {
if (id == language.getId()) {
return language;
}
}
return null;
}
public int getId() {
return id;
}
@StringRes
public int getNameResId() {
return nameResId;
}
}
}
@@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSwimStyle.SwimStyle;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
@@ -131,8 +132,8 @@ public class FitLap extends RecordData {
}
@Nullable
public Integer getSwimStyle() {
return (Integer) getFieldByNumber(38);
public SwimStyle getSwimStyle() {
return (SwimStyle) getFieldByNumber(38);
}
@Nullable
+7
View File
@@ -2389,6 +2389,7 @@
<string name="distance_total">Distance Total</string>
<string name="activeSeconds">Active</string>
<string name="caloriesBurnt">Calories</string>
<string name="restingCalories">Resting Calories</string>
<string name="maxSpeed">Maximum</string>
<string name="minSpeed">Minimum</string>
<string name="minPace">Slowest Pace</string>
@@ -2535,6 +2536,7 @@
<string name="ground_contact_time_balance">Ground Contact Time Balance</string>
<string name="workout_repetitions">Repetitions</string>
<string name="workout_revolutions">Revolutions</string>
<string name="workout_swings">Swings</string>
<string name="seconds">sec</string>
<string name="milliseconds">milliseconds</string>
<string name="milliseconds_ms">ms</string>
@@ -2553,6 +2555,7 @@
<string name="Pace">Pace</string>
<!-- activity summary groups-->
<string name="Strokes">Strokes</string>
<string name="swimming_stroke">Stroke</string>
<string name="Jumps">Jumps</string>
<string name="Swimming">Swimming</string>
<string name="Distance">Distance</string>
@@ -2615,6 +2618,10 @@
<string name="freestyle">Freestyle</string>
<string name="backstroke">Backstroke</string>
<string name="medley">Medley</string>
<string name="swim_style_butterfly">Butterfly</string>
<string name="swim_style_drill">Drill</string>
<string name="swim_style_mixed">Mixed</string>
<string name="workout_intervals">Intervals</string>
<string name="devicetype_nut_mini">Nut mini</string>
<string name="qhybrid_calibration_align_hint">Use the buttons below to align the watch hands to 12:00.</string>
<string name="lorem_ipsum" translatable="false">Lorem Ipsum</string>