From 42dfb6ad4a4e3d751ba44df115d76819c1b54b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 25 Aug 2024 22:49:47 +0100 Subject: [PATCH] Garmin: Parse workout physiological metrics --- .../model/ActivitySummaryEntries.java | 1 + .../model/ActivitySummaryJsonSummary.java | 2 +- .../devices/garmin/fit/FitImporter.java | 34 ++++++++++++ .../devices/garmin/fit/GlobalFITMessage.java | 12 +++++ .../fit/messages/FitPhysiologicalMetrics.java | 52 +++++++++++++++++++ .../fit/messages/FitRecordDataFactory.java | 2 + app/src/main/res/values/strings.xml | 2 + 7 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitPhysiologicalMetrics.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java index ec615dfda..002b77f10 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java @@ -106,6 +106,7 @@ public class ActivitySummaryEntries { public static final String MAXIMUM_OXYGEN_UPTAKE = "maximumOxygenUptake"; public static final String RECOVERY_TIME = "recoveryTime"; public static final String ESTIMATED_SWEAT_LOSS = "estimatedSweatLoss"; + public static final String LACTATE_THRESHOLD_HR = "lactateThresholdHeartRate"; public static final String CYCLING_POWER_AVERAGE = "cyclingPowerAverage"; public static final String CYCLING_POWER_MIN = "cyclingPowerMin"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java index e214cfbc2..82238d142 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java @@ -226,7 +226,7 @@ public class ActivitySummaryJsonSummary { )); put("TrainingEffect", Arrays.asList( TRAINING_EFFECT_AEROBIC, TRAINING_EFFECT_ANAEROBIC, WORKOUT_LOAD, - MAXIMUM_OXYGEN_UPTAKE + MAXIMUM_OXYGEN_UPTAKE, RECOVERY_TIME, LACTATE_THRESHOLD_HR )); put("laps", Arrays.asList( LAP_PACE_AVERAGE, LAPS, LANE_LENGTH diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java index f56cf0bde..f93d5da4c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java @@ -14,11 +14,20 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries. import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_NA; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LACTATE_THRESHOLD_HR; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RECOVERY_TIME; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_HOURS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML_KG_MIN; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.WORKOUT_LOAD; import android.content.Context; import android.widget.Toast; @@ -76,6 +85,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages. import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitHrvSummary; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitHrvValue; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoring; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSession; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepStage; @@ -105,6 +115,7 @@ public class FitImporter { private FitFileId fileId = null; private FitSession session = null; private FitSport sport = null; + private FitPhysiologicalMetrics physiologicalMetrics = null; public FitImporter(final Context context, final GBDevice gbDevice) { this.context = context; @@ -201,6 +212,9 @@ public class FitImporter { // We only support 1 session session = (FitSession) record; } + } else if (record instanceof FitPhysiologicalMetrics) { + LOG.debug("Physiological Metrics: {}", record); + physiologicalMetrics = (FitPhysiologicalMetrics) record; } else if (record instanceof FitSport) { LOG.debug("Sport: {}", record); if (sport != null) { @@ -379,6 +393,25 @@ public class FitImporter { } } + if (physiologicalMetrics != null) { + // TODO lactate_threshold_heart_rate + if (physiologicalMetrics.getAerobicEffect() != null) { + summaryData.add(TRAINING_EFFECT_AEROBIC, physiologicalMetrics.getAerobicEffect(), UNIT_NONE); + } + if (physiologicalMetrics.getAnaerobicEffect() != null) { + summaryData.add(TRAINING_EFFECT_ANAEROBIC, physiologicalMetrics.getAnaerobicEffect(), UNIT_NONE); + } + if (physiologicalMetrics.getMetMax() != null) { + summaryData.add(MAXIMUM_OXYGEN_UPTAKE, physiologicalMetrics.getMetMax().floatValue() * 3.5f, UNIT_ML_KG_MIN); + } + if (physiologicalMetrics.getRecoveryTime() != null) { + summaryData.add(RECOVERY_TIME, physiologicalMetrics.getRecoveryTime() / 60f, UNIT_HOURS); + } + if (physiologicalMetrics.getLactateThresholdHeartRate() != null) { + summaryData.add(LACTATE_THRESHOLD_HR, physiologicalMetrics.getLactateThresholdHeartRate(), UNIT_BPM); + } + } + summary.setSummaryData(summaryData.toString()); if (file != null) { summary.setRawDetailsPath(file.getAbsolutePath()); @@ -457,6 +490,7 @@ public class FitImporter { fileId = null; session = null; sport = null; + physiologicalMetrics = null; } private void persistActivitySamples() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java index 9273f1a44..abc197a36 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java @@ -208,6 +208,17 @@ public class GlobalFITMessage { new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) )); + // https://github.com/GoldenCheetah/GoldenCheetah/blob/71e3928bc614f3209d9977d90cc50b942999b855/src/FileIO/FitRideFile.cpp#L1998 + public static GlobalFITMessage PHYSIOLOGICAL_METRICS = new GlobalFITMessage(140, "PHYSIOLOGICAL_METRICS", Arrays.asList( + new FieldDefinitionPrimitive(4, BaseType.UINT8, "aerobic_effect", 10, 0), + new FieldDefinitionPrimitive(7, BaseType.SINT32, "met_max", 65536, 0), + new FieldDefinitionPrimitive(9, BaseType.UINT16, "recovery_time", 1, 0), // minutes + new FieldDefinitionPrimitive(14, BaseType.UINT16, "lactate_threshold_heart_rate", 1, 0), // bpm + //new FieldDefinitionPrimitive(15, BaseType.UINT16, "lactate_threshold_speed", 1, 0), // m/s // TODO confirm scale + new FieldDefinitionPrimitive(20, BaseType.UINT8, "anaerobic_effect", 10, 0), + new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) + )); + public static GlobalFITMessage WATCHFACE_SETTINGS = new GlobalFITMessage(159, "WATCHFACE_SETTINGS", Arrays.asList( new FieldDefinitionPrimitive(0, BaseType.ENUM, "mode"), //1=analog new FieldDefinitionPrimitive(1, BaseType.BASE_TYPE_BYTE, "layout") @@ -314,6 +325,7 @@ public class GlobalFITMessage { put(55, MONITORING); put(127, CONNECTIVITY); put(128, WEATHER); + put(140, PHYSIOLOGICAL_METRICS); put(159, WATCHFACE_SETTINGS); put(160, GPS_METADATA); put(206, FIELD_DESCRIPTION); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitPhysiologicalMetrics.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitPhysiologicalMetrics.java new file mode 100644 index 000000000..6410c27a5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitPhysiologicalMetrics.java @@ -0,0 +1,52 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages; + +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; + +// +// WARNING: This class was auto-generated, please avoid modifying it directly. +// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen +// +public class FitPhysiologicalMetrics extends RecordData { + public FitPhysiologicalMetrics(final RecordDefinition recordDefinition, final RecordHeader recordHeader) { + super(recordDefinition, recordHeader); + + final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber(); + if (globalNumber != 140) { + throw new IllegalArgumentException("FitPhysiologicalMetrics expects global messages of " + 140 + ", got " + globalNumber); + } + } + + @Nullable + public Float getAerobicEffect() { + return (Float) getFieldByNumber(4); + } + + @Nullable + public Double getMetMax() { + return (Double) getFieldByNumber(7); + } + + @Nullable + public Integer getRecoveryTime() { + return (Integer) getFieldByNumber(9); + } + + @Nullable + public Integer getLactateThresholdHeartRate() { + return (Integer) getFieldByNumber(14); + } + + @Nullable + public Float getAnaerobicEffect() { + return (Float) getFieldByNumber(20); + } + + @Nullable + public Long getTimestamp() { + return (Long) getFieldByNumber(253); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java index e2a33e988..e6977b802 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java @@ -47,6 +47,8 @@ public class FitRecordDataFactory { return new FitConnectivity(recordDefinition, recordHeader); case 128: return new FitWeather(recordDefinition, recordHeader); + case 140: + return new FitPhysiologicalMetrics(recordDefinition, recordHeader); case 159: return new FitWatchfaceSettings(recordDefinition, recordHeader); case 160: diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1cc2f6a87..83581ae34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2131,6 +2131,8 @@ Workout Load Maximum Oxygen Uptake Estimated Sweat Loss + Lactate Threshold Heart Rate + Recovery Time Average Stride Max Stride Min Stride