Initial diving support for garmin watches

This commit is contained in:
punchdeerflyscorpion
2025-07-21 18:59:50 +02:00
parent 33c012e1b8
commit a0432afd57
11 changed files with 327 additions and 4 deletions
@@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.Gar
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionExerciseCategory.ExerciseCategory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveGas;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitLap;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
@@ -41,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitUserProfile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDiveSummary;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@@ -55,6 +57,8 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
private FitSport sport = null;
private FitUserProfile userProfile = null;
private FitPhysiologicalMetrics physiologicalMetrics = null;
private FitDiveSummary diveSummary = null;
private final List<FitDiveGas> diveGases = new ArrayList<>();
private final List<FitSet> sets = new ArrayList<>();
private final List<FitLap> laps = new ArrayList<>();
@@ -112,6 +116,8 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
sport = null;
userProfile = null;
physiologicalMetrics = null;
diveSummary = null;
diveGases.clear();
sets.clear();
laps.clear();
}
@@ -155,6 +161,15 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
// We only support 1 user profile
userProfile = (FitUserProfile) record;
}
} else if (record instanceof FitDiveSummary) {
LOG.trace("Dive summary: {}", record);
if (diveSummary != null) {
return false; // for some reason there is more than one message, in my activities the first one contains all data
}
diveSummary = (FitDiveSummary) record;
} else if (record instanceof FitDiveGas) {
LOG.trace("Dive gas: {}", record);
diveGases.add((FitDiveGas) record);
} else {
return false;
}
@@ -206,7 +221,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
if (session.getAvgSwolf() != null) {
summaryData.add(SWOLF_AVG, session.getAvgSwolf(), UNIT_NONE);
}
if (session.getTotalCycles() != null) {
if (session.getTotalCycles() != null && cycleUnit != ActivityKind.CycleUnit.NONE) {
if (cycleUnit == ActivityKind.CycleUnit.STEPS) {
summaryData.addTotal(session.getTotalCycles() * 2, cycleUnit);
} else {
@@ -268,10 +283,10 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.addCadenceMax(session.getMaxCadence(), cycleUnit);
}
}
if (session.getTotalAscent() != null) {
if (session.getTotalAscent() != null && !ActivityKind.isDiving(activityKind)) {
summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS);
}
if (session.getTotalDescent() != null) {
if (session.getTotalDescent() != null && !ActivityKind.isDiving(activityKind)) {
summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS);
}
if (session.getAvgSwimCadence() != null) {
@@ -289,6 +304,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
if (session.getEnhancedMaxSpeed() != null) {
if (ActivityKind.isPaceActivity(activityKind)) {
summaryData.add(PACE_MAX, Math.round((60 / (session.getEnhancedMaxSpeed() * 3.6)) * 60), UNIT_SECONDS);
} else if (ActivityKind.isDiving(activityKind)) { // Hide average speed for diving activities
} else {
summaryData.add(SPEED_MAX, Math.round((session.getEnhancedMaxSpeed() * 3600 / 1000) * 100.0) / 100.0, UNIT_KMPH);
}
@@ -510,10 +526,65 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
}
if (diveSummary != null) {
if (diveSummary.getAvgDepth() != null) {
summaryData.add(AVG_DEPTH, diveSummary.getAvgDepth(), UNIT_METERS);
}
if (diveSummary.getMaxDepth() != null) {
summaryData.add(MAX_DEPTH, diveSummary.getMaxDepth(), UNIT_METERS);
}
if (diveSummary.getStartCns() != null) {
summaryData.add(START_CNS, diveSummary.getStartCns(), UNIT_PERCENTAGE);
}
if (diveSummary.getEndCns() != null) {
summaryData.add(END_CNS, diveSummary.getEndCns(), UNIT_PERCENTAGE);
}
if (diveSummary.getStartN2() != null) {
summaryData.add(START_N2, diveSummary.getStartN2(), UNIT_PERCENTAGE);
}
if (diveSummary.getEndN2() != null) {
summaryData.add(END_N2, diveSummary.getEndN2(), UNIT_PERCENTAGE);
}
if (diveSummary.getDiveNumber() != null) {
summaryData.add(DIVE_NUMBER, diveSummary.getDiveNumber(), UNIT_NONE);
}
if (diveSummary.getBottomTime() != null) {
summaryData.add(BOTTOM_TIME, diveSummary.getBottomTime(), UNIT_SECONDS);
}
}
summaryData.add(TRAINING_LOAD, safeRound(session.getTrainingLoadPeak()), UNIT_NONE);
summaryData.add(INTENSITY_FACTOR, session.getIntensityFactor(), UNIT_NONE);
summaryData.add(TRAINING_STRESS_SCORE, session.getTrainingStressScore(), UNIT_NONE);
if (!diveGases.isEmpty()) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GAS, "gases_header", Arrays.asList(
"diving_gas",
"helium_content",
"oxygen_content",
"nitrogen_content"
));
int i = 1;
for (final FitDiveGas gas : diveGases) {
int helium = gas.getHeliumContent() != null ? gas.getHeliumContent(): 0;
int oxygen = gas.getOxygenContent() != null ? gas.getOxygenContent(): 0;
int nitrogen = 100 - helium - oxygen;
tableBuilder.addRow(
"gas_" + i,
Arrays.asList(
new ActivitySummaryValue(i, UNIT_NONE),
new ActivitySummaryValue(helium, UNIT_PERCENTAGE),
new ActivitySummaryValue(oxygen, UNIT_PERCENTAGE),
new ActivitySummaryValue(nitrogen, UNIT_PERCENTAGE)
)
);
i++;
}
tableBuilder.addToSummaryData(summaryData);
}
if (!sets.isEmpty()) {
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(SETS, "sets_header", Arrays.asList(
"set",
@@ -379,6 +379,10 @@ public enum ActivityKind {
activityKind.name().contains("TREADMILL") || activityKind.name().contains("WALK");
}
public static boolean isDiving(final ActivityKind activityKind) {
return activityKind.name().contains("DIVING") || activityKind.name().contains("APNEA");
}
public static CycleUnit getCycleUnit(final ActivityKind activityKind) {
switch (activityKind) {
case BUNGEE_JUMPING:
@@ -444,6 +448,12 @@ public enum ActivityKind {
case CRICKET:
case SOFTBALL:
return CycleUnit.SWINGS;
case DIVING:
case SCUBA_DIVING:
case FREE_DIVING:
case APNEA_TRAINING:
case APNEA_TEST:
return CycleUnit.NONE;
}
return CycleUnit.STEPS;
@@ -230,6 +230,14 @@ public class ActivitySummaryEntries {
public static final String GROUP_LAPS = "laps";
public static final String GROUP_RUNNING_FORM = "RunningForm";
public static final String GROUP_INTERVALS = "workout_intervals";
public static final String GROUP_DIVING = "activity_type_diving";
public static final String AVG_DEPTH = "diving_avg_depth";
public static final String START_CNS = "diving_start_cns";
public static final String END_CNS = "diving_end_cns";
public static final String START_N2 = "diving_start_n2";
public static final String END_N2 = "diving_end_n2";
public static final String DIVE_NUMBER = "dive_number";
public static final String BOTTOM_TIME = "diving_bottom_time";
// DIVING parameters
public static final String MAX_DEPTH = "diving_maximum_diving_depth";
@@ -226,6 +226,9 @@ public class ActivitySummaryJsonSummary {
HR_ZONE_NA, HR_ZONE_WARM_UP, HR_ZONE_FAT_BURN, HR_ZONE_EASY, HR_ZONE_AEROBIC, HR_ZONE_ANAEROBIC,
HR_ZONE_THRESHOLD, HR_ZONE_EXTREME, HR_ZONE_MAXIMUM
));
put(GROUP_DIVING, Arrays.asList(
AVG_DEPTH, MAX_DEPTH, START_CNS, END_CNS, START_N2, END_N2, DIVE_NUMBER, BOTTOM_TIME
));
put(SETS, Arrays.asList(
));
}};
@@ -216,6 +216,7 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(5, BaseType.UINT32, "distance", 100, 0), // m
new FieldDefinitionPrimitive(6, BaseType.UINT16, "speed", 1000, 0), // m/s
new FieldDefinitionPrimitive(7, BaseType.UINT16, "power"), // watt
new FieldDefinitionPrimitive(13, BaseType.SINT8, "temperature", 1, 0), // C
new FieldDefinitionPrimitive(29, BaseType.UINT32, "accumulated_power"), // watt
new FieldDefinitionPrimitive(39, BaseType.UINT16, "oscillation", 10, 0), // mm
new FieldDefinitionPrimitive(42, BaseType.ENUM, "activity"),
@@ -224,6 +225,14 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(78, BaseType.UINT32, "enhanced_altitude", 5, 500), // m
new FieldDefinitionPrimitive(83, BaseType.UINT16, "vertical_ratio", 100, 0), // %
new FieldDefinitionPrimitive(85, BaseType.UINT16, "step_length", 10, 0), // mm
new FieldDefinitionPrimitive(91, BaseType.UINT32, "absolute_pressure", 1, 0), // Pa
new FieldDefinitionPrimitive(92, BaseType.UINT32, "depth", 1000, 0), // m
new FieldDefinitionPrimitive(93, BaseType.UINT32, "next_stop_depth", 1000, 0), // m
new FieldDefinitionPrimitive(94, BaseType.UINT32, "next_stop_time", 1, 0), // s
new FieldDefinitionPrimitive(95, BaseType.UINT32, "time_to_surface", 1, 0), // s
new FieldDefinitionPrimitive(96, BaseType.UINT32, "ndl_time", 1, 0), // s
new FieldDefinitionPrimitive(97, BaseType.UINT8, "cns_load", 1, 0), // %
new FieldDefinitionPrimitive(98, BaseType.UINT16, "n2_load", 1, 0), // %
new FieldDefinitionPrimitive(108, BaseType.UINT16, "enhanced_respiration_rate"),
new FieldDefinitionPrimitive(136, BaseType.UINT8, "wrist_heart_rate"),
new FieldDefinitionPrimitive(143, BaseType.UINT8, "body_battery"),
@@ -404,7 +413,12 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(10, BaseType.UINT16, "message_index"),
new FieldDefinitionPrimitive(254, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage DIVE_GAS = new GlobalFITMessage(259, "DIVE_GAS", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT8, "helium_content"),
new FieldDefinitionPrimitive(1, BaseType.UINT8, "oxygen_content"),
new FieldDefinitionPrimitive(2, BaseType.UINT8, "status"),
new FieldDefinitionPrimitive(254, BaseType.UINT16, "message_index")
));
public static GlobalFITMessage STRESS_LEVEL = new GlobalFITMessage(227, "STRESS_LEVEL", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.SINT16, "stress_level_value"),
new FieldDefinitionPrimitive(1, BaseType.UINT32, "stress_level_time", FieldDefinitionFactory.FIELD.TIMESTAMP),
@@ -445,6 +459,23 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(9, BaseType.ENUM, "calibrated_data") // 1?
));
public static GlobalFITMessage DIVE_SUMMARY = new GlobalFITMessage(268, "DIVE_SUMMARY", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT16, "reference_mesg"),
new FieldDefinitionPrimitive(1, BaseType.UINT16, "reference_index"),
new FieldDefinitionPrimitive(2, BaseType.UINT32, "avg_depth", 1000, 0), // m
new FieldDefinitionPrimitive(3, BaseType.UINT32, "max_depth", 1000, 0), // m
new FieldDefinitionPrimitive(4, BaseType.UINT32, "surface_interval"), // s
new FieldDefinitionPrimitive(5, BaseType.UINT8, "start_cns"), // %
new FieldDefinitionPrimitive(6, BaseType.UINT8, "end_cns"), // %
new FieldDefinitionPrimitive(7, BaseType.UINT16, "start_n2"), // %
new FieldDefinitionPrimitive(8, BaseType.UINT16, "end_n2"), // %
new FieldDefinitionPrimitive(9, BaseType.UINT16, "o2_toxicity"), // OTUs
new FieldDefinitionPrimitive(10, BaseType.UINT32, "dive_number"),
new FieldDefinitionPrimitive(11, BaseType.UINT32, "bottom_time", 1000, 0), // s
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage RESPIRATION_RATE = new GlobalFITMessage(297, "RESPIRATION_RATE", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.SINT16, "respiration_rate", 100, 0), // breaths / min
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
@@ -548,9 +579,11 @@ public class GlobalFITMessage {
put(225, SET);
put(227, STRESS_LEVEL);
put(229, MAX_MET_DATA);
put(259, DIVE_GAS);
put(269, SPO2);
put(273, SLEEP_DATA_INFO);
put(274, SLEEP_DATA_RAW);
put(268, DIVE_SUMMARY);
put(275, SLEEP_STAGE);
put(297, RESPIRATION_RATE);
put(346, SLEEP_STATS);
@@ -84,6 +84,11 @@ public enum GarminSport {
BASEBALL(49, 0, ActivityKind.BASEBALL),
SOFTBALL(50, 0, ActivityKind.SOFTBALL),
SOFTBALL_SLOW_PITCH(51, 0, ActivityKind.SOFTBALL_SLOW_PITCH),
SINGLE_GAS_DIVING(53, 53, ActivityKind.SCUBA_DIVING),
MULTI_GAS_DIVING(53, 54, ActivityKind.SCUBA_DIVING),
GAUGE_DIVING(53, 55, ActivityKind.DIVING),
APNEA_DIVING(53, 56, ActivityKind.FREE_DIVING),
APNEA_HUNTING(53, 57, ActivityKind.FREE_DIVING),
SHOOTING(56, 0, ActivityKind.SHOOTING),
AUTO_RACING(57, 0, ActivityKind.AUTO_RACING),
WINTER_SPORT(58, 0, ActivityKind.WINTER_SPORT),
@@ -0,0 +1,42 @@
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 FitDiveGas extends RecordData {
public FitDiveGas(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 259) {
throw new IllegalArgumentException("FitDiveGas expects global messages of " + 259 + ", got " + globalNumber);
}
}
@Nullable
public Integer getHeliumContent() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getOxygenContent() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Integer getStatus() {
return (Integer) getFieldByNumber(2);
}
@Nullable
public Integer getMessageIndex() {
return (Integer) getFieldByNumber(254);
}
}
@@ -0,0 +1,87 @@
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 FitDiveSummary extends RecordData {
public FitDiveSummary(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 268) {
throw new IllegalArgumentException("FitDiveSummary expects global messages of " + 268 + ", got " + globalNumber);
}
}
@Nullable
public Integer getReferenceMesg() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getReferenceIndex() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Double getAvgDepth() {
return (Double) getFieldByNumber(2);
}
@Nullable
public Double getMaxDepth() {
return (Double) getFieldByNumber(3);
}
@Nullable
public Long getSurfaceInterval() {
return (Long) getFieldByNumber(4);
}
@Nullable
public Integer getStartCns() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Integer getEndCns() {
return (Integer) getFieldByNumber(6);
}
@Nullable
public Integer getStartN2() {
return (Integer) getFieldByNumber(7);
}
@Nullable
public Integer getEndN2() {
return (Integer) getFieldByNumber(8);
}
@Nullable
public Integer getO2Toxicity() {
return (Integer) getFieldByNumber(9);
}
@Nullable
public Long getDiveNumber() {
return (Long) getFieldByNumber(10);
}
@Nullable
public Double getBottomTime() {
return (Double) getFieldByNumber(11);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}
@@ -65,6 +65,11 @@ public class FitRecord extends RecordData {
return (Integer) getFieldByNumber(7);
}
@Nullable
public Integer getTemperature() {
return (Integer) getFieldByNumber(13);
}
@Nullable
public Long getAccumulatedPower() {
return (Long) getFieldByNumber(29);
@@ -105,6 +110,46 @@ public class FitRecord extends RecordData {
return (Float) getFieldByNumber(85);
}
@Nullable
public Long getAbsolutePressure() {
return (Long) getFieldByNumber(91);
}
@Nullable
public Double getDepth() {
return (Double) getFieldByNumber(92);
}
@Nullable
public Double getNextStopDepth() {
return (Double) getFieldByNumber(93);
}
@Nullable
public Long getNextStopTime() {
return (Long) getFieldByNumber(94);
}
@Nullable
public Long getTimeToSurface() {
return (Long) getFieldByNumber(95);
}
@Nullable
public Long getNdlTime() {
return (Long) getFieldByNumber(96);
}
@Nullable
public Integer getCnsLoad() {
return (Integer) getFieldByNumber(97);
}
@Nullable
public Integer getN2Load() {
return (Integer) getFieldByNumber(98);
}
@Nullable
public Integer getEnhancedRespirationRate() {
return (Integer) getFieldByNumber(108);
@@ -77,6 +77,10 @@ public class FitRecordDataFactory {
return new FitStressLevel(recordDefinition, recordHeader);
case 229:
return new FitMaxMetData(recordDefinition, recordHeader);
case 259:
return new FitDiveGas(recordDefinition, recordHeader);
case 268:
return new FitDiveSummary(recordDefinition, recordHeader);
case 269:
return new FitSpo2(recordDefinition, recordHeader);
case 273:
+15
View File
@@ -2584,6 +2584,17 @@
<string name="workout_set_repetitions">%1d x</string>
<string name="workout_set_repetitions_weight_kg">%1$d x %2$.2f kg</string>
<string name="workout_set_repetitions_weight_lbs">%1$d x %2$.2f lbs</string>
<string name="diving_avg_depth">Average depth</string>
<string name="diving_avg_descent_rate">Average descent rate</string>
<string name="diving_avg_ascent_rate">Average ascent rate</string>
<string name="diving_max_descent_rate">Max descent rate</string>
<string name="diving_max_ascent_rate">Max ascent rate</string>
<string name="diving_start_cns">Start CNS toxicity</string>
<string name="diving_end_cns">End CNS toxicity</string>
<string name="diving_start_n2">Start N2 load</string>
<string name="diving_end_n2">End N2 load</string>
<string name="dive_number">Dive number</string>
<string name="diving_bottom_time">Bottom time</string>
<!-- activity summary units-->
<string name="meters">m</string>
<string name="cm">cm</string>
@@ -2756,6 +2767,10 @@
<string name="diving_maximum_diving_depth">Maximum Diving Depth</string>
<string name="diving_water_type">Water Type</string>
<string name="diving_gas">Gas</string>
<string name="gases_header">Diving Gases</string>
<string name="helium_content">Helium Content</string>
<string name="oxygen_content">Oxygen Content</string>
<string name="nitrogen_content">Nitrogen Content</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>