Merge pull request 'ActivityPoint update and Garmin parsing' (#6067)

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6067
This commit is contained in:
José Rebelo
2026-05-09 00:05:31 +02:00
30 changed files with 689 additions and 150 deletions
@@ -1,7 +1,25 @@
/* Copyright (C) 2024-2026 José Rebelo, Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.workouts;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_CM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_EPOC_TIME;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_FOOT;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_FOOT_PER_HOUR;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KILOMETERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH;
@@ -10,15 +28,21 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_HOUR;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MILE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MILE_PER_HOUR;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MINUTES_PER_100_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MINUTES_PER_100_YARDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MINUTES_PER_KM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MINUTES_PER_MILE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NAUTICAL_MILES;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_RAW_STRING;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_100_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_100_YARDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_M;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_SPORT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -113,46 +137,46 @@ public class WorkoutValueFormatter {
case UNIT_CM:
if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value * 0.0328084;
unit = "ft";
unit = UNIT_FOOT;
}
break;
case UNIT_METERS_PER_SECOND:
if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value * 2.236936D;
unit = "mi_h";
unit = UNIT_MILE_PER_HOUR;
} else { //metric
value = value * 3.6;
unit = "km_h";
unit = UNIT_KMPH;
}
break;
case UNIT_METERS_PER_HOUR:
if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value * 3.28084D;
unit = "foot_per_hour";
unit = UNIT_FOOT_PER_HOUR;
}
break;
case UNIT_SECONDS_PER_M:
if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value * (1609.344 / 60D);
unit = "minutes_mi";
unit = UNIT_MINUTES_PER_MILE;
} else { //metric
value = value * (1000 / 60D);
unit = "minutes_km";
unit = UNIT_MINUTES_PER_KM;
}
break;
case UNIT_SECONDS_PER_KM:
if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value / 60D * 1.609344;
unit = "minutes_mi";
unit = UNIT_MINUTES_PER_MILE;
} else { //metric
value = value / 60D;
unit = "minutes_km";
unit = UNIT_MINUTES_PER_KM;
}
break;
case UNIT_KILOMETERS:
if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value * 0.621371D;
unit = "mi";
unit = UNIT_MILE;
}
break;
case UNIT_METERS:
@@ -161,15 +185,15 @@ public class WorkoutValueFormatter {
unit = UNIT_NAUTICAL_MILES;
} else if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value * 3.28084D;
unit = "ft";
unit = UNIT_FOOT;
if (value > 6000) {
value = value * 0.0001893939D;
unit = "mi";
unit = UNIT_MILE;
}
} else { //metric
if (value > 2000) {
value = value / 1000;
unit = "km";
unit = UNIT_KILOMETERS;
}
}
break;
@@ -179,7 +203,7 @@ public class WorkoutValueFormatter {
unit = UNIT_KNOTS;
} else if (distanceUnit == DistanceUnit.IMPERIAL) {
value = value * 0.621371D;
unit = "mi_h";
unit = UNIT_MILE_PER_HOUR;
}
break;
case UNIT_SECONDS_PER_100_METERS:
@@ -208,9 +232,14 @@ public class WorkoutValueFormatter {
}
}
if (unit.equals("seconds") && !show_raw_data && showUnit) { //rather then plain seconds, show formatted duration
if (unit.equals(UNIT_SECONDS) && !show_raw_data && showUnit) { //rather then plain seconds, show formatted duration
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
} else if (unit.equals("minutes_km") || unit.equals("minutes_mi") || unit.equals("minutes_100m") || unit.equals("minutes_100yd")) {
}else if (unit.equals(UNIT_SECONDS_SPORT) && !show_raw_data && showUnit) {
return DateTimeUtils.formatSportsDuration(Math.round(1000L * (double) value), TimeUnit.MILLISECONDS);
} else if (UNIT_EPOC_TIME.equals(unit) && !show_raw_data) {
long epoc = ((Number) rawValue).longValue();
return DateTimeUtils.formatLocalTime(epoc * 1000L);
} else if (unit.equals(UNIT_MINUTES_PER_KM) || unit.equals(UNIT_MINUTES_PER_MILE) || unit.equals(UNIT_MINUTES_PER_100_METERS) || unit.equals(UNIT_MINUTES_PER_100_YARDS)) {
// Format pace
String format = showUnit ? "%d:%02d %s" : "%d:%02d";
return String.format(
@@ -76,7 +76,6 @@ public class DefaultWorkoutCharts {
final List<Entry> temperatureDataPoints = new ArrayList<>(initalCapacity);
final List<Entry> depthDataPoints = new ArrayList<>(initalCapacity);
final List<Entry> distancePoints = new ArrayList<>(initalCapacity);
final List<Entry> stridePoints = new ArrayList<>(initalCapacity);
final List<Entry> staminaPoints = new ArrayList<>(initalCapacity);
final List<Entry> bodyEnergyPoints = new ArrayList<>(initalCapacity);
final List<Entry> stepLengthPoints = new ArrayList<>(initalCapacity);
@@ -92,7 +91,6 @@ public class DefaultWorkoutCharts {
boolean hasTemperatureValues = false;
boolean hasDepthValues = false;
boolean hasDistanceValues = false;
boolean hasStrideValues = false;
boolean hasBodyEnergyValues = false;
boolean hasStaminaValues = false;
boolean hasStepLengthValues = false;
@@ -168,13 +166,6 @@ public class DefaultWorkoutCharts {
hasDistanceValues = hasDistanceValues || (distance > 0);
}
// Stride
final float stride = point.getStride();
if (stride >= 0.0f) {
stridePoints.add(new Entry(tsShorten, stride));
hasStrideValues = hasStrideValues || (stride > 0.0f);
}
// Body Energy
final float bodyEnergy = point.getBodyEnergy();
if (bodyEnergy >= 0.0f) {
@@ -247,10 +238,6 @@ public class DefaultWorkoutCharts {
charts.add(createDistanceChart(context, distancePoints));
}
if (hasStrideValues && !stridePoints.isEmpty()) {
charts.add(createStrideChart(context, stridePoints));
}
if (hasBodyEnergyValues && !bodyEnergyPoints.isEmpty()) {
charts.add(createBodyEnergyChart(context, bodyEnergyPoints));
}
@@ -486,26 +473,6 @@ public class DefaultWorkoutCharts {
);
}
private static WorkoutChart createStrideChart(final Context context,
final List<Entry> stridePoints) {
final String label = String.format("%s(%s)", context.getString(R.string.stride), getUnitString(context, UNIT_MM));
final LineDataSet dataset = createLineDataSet(context, stridePoints, label, ContextCompat.getColor(context, R.color.chart_line_stride));
final ValueFormatter valueFormatter = new ValueFormatter() {
@Override
public String getFormattedValue(float value) {
return String.valueOf((int) value);
}
};
return new WorkoutChart(
"chart_stride",
context.getString(R.string.stride),
ActivitySummaryEntries.GROUP_STEPS,
new LineData(dataset),
valueFormatter,
getUnitString(context, UNIT_MM)
);
}
private static WorkoutChart createBodyEnergyChart(final Context context,
final List<Entry> bodyEnergyPoints) {
final String label = String.format("%s(%s)", context.getString(R.string.body_energy), getUnitString(context, UNIT_PERCENTAGE));
@@ -266,6 +266,9 @@ object ActivitySummaryGroup {
ActivitySummaryEntries.RECOVERY_TIME,
ActivitySummaryEntries.BODY_ENERGY_AT_START,
ActivitySummaryEntries.BODY_ENERGY_AT_END,
ActivitySummaryEntries.STAMINA_AT_START,
ActivitySummaryEntries.STAMINA_AT_END,
ActivitySummaryEntries.STAMINA_MIN,
ActivitySummaryEntries.LACTATE_THRESHOLD_HR,
ActivitySummaryEntries.RATING_OF_PERCEIVED_EXERTION,
ActivitySummaryEntries.WORKOUT_FEEL,
@@ -398,22 +398,26 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summary.setEndTime(new Date(summary.getStartTime().getTime() + session.getTotalElapsedTime().intValue()));
}
if (session.getTotalTimerTime() != null) {
summaryData.add(ACTIVE_SECONDS, session.getTotalTimerTime() / 1000.0f, UNIT_SECONDS);
}
if (session.getTotalDistance() != null) {
summaryData.add(DISTANCE_METERS, session.getTotalDistance() / 100.0f, UNIT_METERS);
}
summaryData.add(ACTIVE_SECONDS, session.getTotalTimerTime(), UNIT_SECONDS);
summaryData.add(DISTANCE_METERS, session.getTotalDistance(), UNIT_METERS);
summaryData.add(POOL_LENGTH, session.getPoolLength(), UNIT_METERS);
summaryData.add(SWOLF_AVG, session.getAvgSwolf(), UNIT_NONE);
if (session.getTotalCycles() != null && cycleUnit != ActivityKind.CycleUnit.NONE) {
if (cycleUnit == ActivityKind.CycleUnit.STEPS) {
summaryData.addTotal(session.getTotalCycles() * 2, cycleUnit);
} else {
// FIXME some of the rest might also need adjusting...
summaryData.addTotal(session.getTotalCycles(), cycleUnit);
if (cycleUnit != ActivityKind.CycleUnit.NONE) {
Number totalCycles = session.getTotalCycles();
if (totalCycles != null) {
final Number totalFractionalCycles = session.getTotalFractionalCycles();
if (totalFractionalCycles != null) {
totalCycles = totalCycles.doubleValue() + totalFractionalCycles.doubleValue();
}
if (cycleUnit == ActivityKind.CycleUnit.STEPS) {
summaryData.addTotal(totalCycles.doubleValue() * 2, cycleUnit);
} else {
// FIXME some of the rest might also need adjusting...
summaryData.addTotal(totalCycles, cycleUnit);
}
}
}
summaryData.add(STEP_LENGTH_AVG, session.getAvgStepLength(), UNIT_MM);
if (session.getTotalCalories() != null) {
summaryData.add(CALORIES_CONSUMED, session.getCaloriesConsumed(), UNIT_KCAL);
@@ -449,22 +453,34 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
session.getEnhancedAvgAltitude(), session.getAvgAltitude());
summaryData.add(STRESS_AVG, session.getAvgStress(), UNIT_NONE);
if (session.getAvgCadence() != null) {
Number avgCadence = session.getAvgCadence();
if (avgCadence != null) {
final Number avgFractionalCadence = session.getAvgFractionalCadence();
if (avgFractionalCadence != null) {
avgCadence = avgCadence.doubleValue() + avgFractionalCadence.doubleValue();
}
if (cycleUnit == ActivityKind.CycleUnit.STEPS) {
summaryData.addCadenceAvg(session.getAvgCadence() * 2, cycleUnit);
summaryData.addCadenceAvg(avgCadence.doubleValue() * 2, cycleUnit);
} else {
// FIXME some of the rest might also need adjusting...
summaryData.addCadenceAvg(session.getAvgCadence(), cycleUnit);
summaryData.addCadenceAvg(avgCadence, cycleUnit);
}
}
if (session.getMaxCadence() != null) {
Number maxCadence = session.getMaxCadence();
if (maxCadence != null) {
final Number maxFractionalCadence = session.getMaxFractionalCadence();
if (maxFractionalCadence != null) {
maxCadence = maxCadence.doubleValue() + maxFractionalCadence.doubleValue();
}
if (cycleUnit == ActivityKind.CycleUnit.STEPS) {
summaryData.addCadenceMax(session.getMaxCadence() * 2, cycleUnit);
summaryData.addCadenceMax(maxCadence.doubleValue() * 2, cycleUnit);
} else {
// FIXME some of the rest might also need adjusting...
summaryData.addCadenceMax(session.getMaxCadence(), cycleUnit);
summaryData.addCadenceMax(maxCadence, cycleUnit);
}
}
if (!ActivityKind.isDiving(activityKind)) {
if(!summaryData.add(TOTAL_ASCENT, session.getTotalAscent(), UNIT_METERS) && physiologicalMetrics != null){
summaryData.add(TOTAL_ASCENT, physiologicalMetrics.getTotalAscent(), UNIT_METERS);
@@ -517,9 +533,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.add(NORMALIZED_POWER, session.getNormalizedPower(), UNIT_WATT);
summaryData.add(TOTAL_WORK, session.getTotalWork(), UNIT_JOULE);
if (session.getStandTime() != null) {
summaryData.add(STANDING_TIME, session.getStandTime() / 1000, UNIT_SECONDS);
}
summaryData.add(STANDING_TIME, session.getStandTime(), UNIT_SECONDS);
summaryData.add(STANDING_COUNT, session.getStandCount(), UNIT_NONE);
summaryData.add(AVG_LEFT_PCO, session.getAvgLeftPco(), UNIT_MM);
summaryData.add(AVG_RIGHT_PCO, session.getAvgRightPco(), UNIT_MM);
@@ -527,13 +541,14 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.add(AVG_VERTICAL_OSCILLATION, session.getAvgVerticalOscillation(), UNIT_MM);
summaryData.add(AVG_GROUND_CONTACT_TIME, session.getAvgStanceTime(), UNIT_MILLISECONDS);
summaryData.add(AVG_VERTICAL_RATIO, session.getAvgVerticalRatio(), UNIT_PERCENTAGE);
if (session.getAvgStanceTimeBalance() != null) {
final Float avgStanceTimeBalance = session.getAvgStanceTimeBalance();
if (avgStanceTimeBalance != null && avgStanceTimeBalance > 0.0f) {
summaryData.add(
AVG_GROUND_CONTACT_TIME_BALANCE,
context.getString(
R.string.range_percentage_float,
session.getAvgStanceTimeBalance(),
100.0f - session.getAvgStanceTimeBalance()
avgStanceTimeBalance,
100.0f - avgStanceTimeBalance
)
);
}
@@ -713,13 +728,17 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
}
if (!summaryData.add(TRAINING_EFFECT_ANAEROBIC, session.getTotalAnaerobicTrainingEffect(), UNIT_NONE, true)) {
if (physiologicalMetrics != null) {
summaryData.add(TRAINING_EFFECT_ANAEROBIC, physiologicalMetrics.getAnaerobicEffect(), UNIT_NONE, true);
}
}
if (physiologicalMetrics != null) {
if (physiologicalMetrics.getAerobicEffect() != null) {
summaryData.add(TRAINING_EFFECT_AEROBIC, physiologicalMetrics.getAerobicEffect(), UNIT_NONE, true);
}
if (physiologicalMetrics.getAnaerobicEffect() != null) {
summaryData.add(TRAINING_EFFECT_ANAEROBIC, physiologicalMetrics.getAnaerobicEffect(), UNIT_NONE, true);
}
if (physiologicalMetrics.getMetMax() != null) {
summaryData.add(MAXIMUM_OXYGEN_UPTAKE, physiologicalMetrics.getMetMax().floatValue() * 3.5f, UNIT_ML_KG_MIN);
}
@@ -729,20 +748,24 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.add(LACTATE_THRESHOLD_HR, physiologicalMetrics.getLactateThresholdHeartRate(), UNIT_BPM);
}
// diving related
summaryData.add(DIVE_NUMBER, UNIT_NONE, session.getDiveNumber(), diveSummary != null ? diveSummary.getDiveNumber() : null);
if (diveSummary != null) {
summaryData.add(DIVE_NUMBER, diveSummary.getDiveNumber(), UNIT_NONE);
summaryData.add(BOTTOM_TIME, diveSummary.getBottomTime(), UNIT_SECONDS);
summaryData.add(AVG_DEPTH, diveSummary.getAvgDepth(), UNIT_METERS);
summaryData.add(MAX_DEPTH, diveSummary.getMaxDepth(), UNIT_METERS);
}
summaryData.add(AVG_DEPTH, UNIT_METERS, session.getAvgDepth(), diveSummary != null ? diveSummary.getAvgDepth() : null);
summaryData.add(MAX_DEPTH, UNIT_METERS, session.getMaxDepth(), diveSummary != null ? diveSummary.getMaxDepth() : null);
// force display even if value is 0
summaryData.add(START_CNS, diveSummary.getStartCns(), UNIT_PERCENTAGE, true);
summaryData.add(END_CNS, diveSummary.getEndCns(), UNIT_PERCENTAGE, true);
summaryData.add(START_N2, diveSummary.getStartN2(), UNIT_PERCENTAGE, true);
summaryData.add(END_N2, diveSummary.getEndN2(), UNIT_PERCENTAGE, true);
summaryData.add(OXYGEN_TOXICITY, diveSummary.getO2Toxicity(), UNIT_OXYGEN_TOXICITY_UNITs, true);
// force display even if value is 0
summaryData.add(START_CNS, UNIT_PERCENTAGE, session.getStartCns(), diveSummary != null ? diveSummary.getStartCns() : null, true);
summaryData.add(END_CNS, UNIT_PERCENTAGE, session.getEndCns(), diveSummary != null ? diveSummary.getEndCns() : null, true);
summaryData.add(START_N2, UNIT_PERCENTAGE, session.getStartN2(), diveSummary != null ? diveSummary.getStartN2() : null, true);
summaryData.add(END_N2, UNIT_PERCENTAGE, session.getEndN2(), diveSummary != null ? diveSummary.getEndN2() : null, true);
summaryData.add(OXYGEN_TOXICITY, UNIT_OXYGEN_TOXICITY_UNITs, session.getO2Toxicity(), diveSummary != null ? diveSummary.getO2Toxicity() : null, true);
summaryData.add(SURFACE_INTERVAL, diveSummary.getSurfaceInterval(), UNIT_SECONDS);
summaryData.add(SURFACE_INTERVAL, UNIT_SECONDS, session.getSurfaceInterval(), diveSummary != null ? diveSummary.getSurfaceInterval() : null);
if (diveSummary != null) {
summaryData.add(PRESSURE_SAC_AVG, diveSummary.getAvgPressureSac(), UNIT_BAR_PER_MINUTE);
}
@@ -812,6 +835,9 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
summaryData.add(BODY_ENERGY_AT_END, physiologicalMetrics.getEndingBodyBattery(), UNIT_PERCENTAGE);
}
}
summaryData.add(STAMINA_AT_START, session.getBeginningPotential(), UNIT_PERCENTAGE);
summaryData.add(STAMINA_AT_END, session.getEndingPotential(), UNIT_PERCENTAGE);
summaryData.add(STAMINA_MIN, session.getMinStamina(), UNIT_PERCENTAGE);
summaryData.add(SOLAR_INTENSITY, session.getSolarIntensity(), UNIT_PERCENTAGE);
summaryData.add(BATTERY_GAIN, session.getBatteryGain(), UNIT_SECONDS);
@@ -1010,7 +1036,35 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
final boolean anySwimmingLaps = laps.stream()
.anyMatch(lap -> lap.getSwimStyle() != null);
if (anyValidLaps && diveLaps.isEmpty()) {
if (activityKind == ActivityKind.STOP_WATCH) {
// The start value encoded in the FIT file is shifted to a later time if the stop watch was paused.
// There is not enough information in the FIT file to fix this so display the values as they
// were encoded.
// The duration / active seconds are accurate.
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_INTERVALS, "intervals_header", Arrays.asList(
"#",
"start",
"stop",
ACTIVE_SECONDS
));
for (int lapIndex = 0; lapIndex < laps.size(); lapIndex++) {
FitLap lap = laps.get(lapIndex);
Integer index = lap.getMessageIndex();
if (index != null) {
index = index + 1;
} else {
index = lapIndex + 1;
}
final List<ActivitySummaryValue> row = new ArrayList<>();
row.add(new ActivitySummaryValue(index, UNIT_NONE));
row.add(new ActivitySummaryValue(lap.getStartTime(), UNIT_EPOC_TIME));
row.add(new ActivitySummaryValue(lap.getTimestamp(), UNIT_EPOC_TIME));
row.add(new ActivitySummaryValue(lap.getTotalTimerTime(), UNIT_SECONDS_SPORT));
tableBuilder.addRow("interval_" + lapIndex, row);
}
tableBuilder.addToSummaryData(summaryData);
} else if (anyValidLaps && diveLaps.isEmpty()) {
// Unfortunately our tables do not yet scroll horizontally, so can't always add all possible columns
final List<String> header = new ArrayList<>();
header.add("#");
@@ -60,7 +60,7 @@ public enum ActivityKind {
// Non-legacy activity kinds after 0x04000000
NAVIGATE(0x04000000, R.string.activity_type_navigate, R.drawable.ic_navigation),
INDOOR_TRACK(0x04000001, R.string.activity_type_indoor_track),
INDOOR_TRACK_RUNNING(0x04000001, R.string.activity_type_indoor_track, R.drawable.ic_run_circle),
HANDCYCLING(0x04000002, R.string.activity_type_handcycling),
E_BIKE(0x04000003, R.string.activity_type_e_bike, R.drawable.ic_activity_electric_bike),
BIKE_COMMUTE(0x04000004, R.string.activity_type_bike_commute, R.drawable.ic_activity_bike_lane),
@@ -27,7 +27,6 @@ public class ActivityPoint {
private GPSCoordinate location;
private int heartRate;
private float speed = -1;
private int stride = -1;
private int stepLength = -1;
private int cadence = -1;
private float power = -1;
@@ -145,12 +144,6 @@ public class ActivityPoint {
this.speed = speed;
}
/// distance from one foot landing to the same foot landing again
/// @see ActivitySummaryEntries#UNIT_MM
public int getStride() {
return stride;
}
/// distance from one foot landing to the opposite foot landing
/// @see ActivitySummaryEntries#UNIT_MM
public int getStepLength() {
@@ -210,7 +203,6 @@ public class ActivityPoint {
if (!(o instanceof ActivityPoint that)) return false;
return heartRate == that.heartRate &&
Float.compare(speed, that.speed) == 0 &&
stride == that.stride &&
cadence == that.cadence &&
Float.compare(power, that.power) == 0 &&
Float.compare(respiratoryRate, that.respiratoryRate) == 0 &&
@@ -227,7 +219,7 @@ public class ActivityPoint {
@Override
public int hashCode() {
return Objects.hash(time, location, heartRate, speed, stride, cadence, power, respiratoryRate, depth, temperature, description, distance, altitude, bodyEnergy, stamina);
return Objects.hash(time, location, heartRate, speed, cadence, power, respiratoryRate, depth, temperature, description, distance, altitude, bodyEnergy, stamina);
}
public static class Builder {
@@ -245,7 +237,6 @@ public class ActivityPoint {
private int heartRate = Integer.MIN_VALUE;
private float speed = Float.NaN;
private int stride = Integer.MIN_VALUE;
private int stepLength = Integer.MIN_VALUE;
private int cadence = Integer.MIN_VALUE;
private float power = Float.NaN;
@@ -349,23 +340,6 @@ public class ActivityPoint {
this.speed = speed;
}
/// @see ActivitySummaryEntries#UNIT_MM
public int getStride() {
return stride;
}
/// distance from one foot landing to the same foot landing again
/// @see ActivitySummaryEntries#UNIT_MM
public void setStride(@Nullable Number stride) {
this.stride = (stride == null) ? Integer.MIN_VALUE : stride.intValue();
}
/// distance from one foot landing to the same foot landing again
/// @see ActivitySummaryEntries#UNIT_MM
public void setStride(int stride) {
this.stride = stride;
}
public int getCadence() {
return cadence;
}
@@ -564,9 +538,6 @@ public class ActivityPoint {
if (cadence > Integer.MIN_VALUE) {
activityPoint.setCadence(cadence);
}
if (stride > Integer.MIN_VALUE) {
activityPoint.stride = stride;
}
if (!Float.isNaN(power)) {
activityPoint.setPower(power);
}
@@ -69,10 +69,15 @@ public class ActivitySummaryData {
/// @return {@code true} if the value was actually added
public boolean add(final String key, final String unit, final Number value, final Number valueFallback) {
return add(key, unit, value, valueFallback, false);
}
/// @return {@code true} if the value was actually added
public boolean add(final String key, final String unit, final Number value, final Number valueFallback, boolean force) {
if (value != null) {
return add(null, key, value, unit, false);
return add(null, key, value, unit, force);
} else {
return add(null, key, valueFallback, unit, false);
return add(null, key, valueFallback, unit, force);
}
}
@@ -267,6 +267,15 @@ public class ActivitySummaryEntries {
public static final String UNIT_KG_PER_M3 = "unit_kg_per_m3";
public static final String UNIT_BAR = "unit_bar";
public static final String UNIT_LITER = "unit_liter";
/// UNIX epoc seconds to be formated in local time without showing date
public static final String UNIT_EPOC_TIME = "unit_epoc_time";
/// display time including milli seconds if any
public static final String UNIT_SECONDS_SPORT = "unit_sports_seconds";
public static final String UNIT_FOOT = "ft";
public static final String UNIT_FOOT_PER_HOUR = "foot_per_hour";
public static final String UNIT_MILE = "mi";
public static final String UNIT_MILE_PER_HOUR = "mi_h";
public static final String UNIT_MINUTES_PER_MILE = "minutes_mi";
public static final String GROUP_PACE = "Pace";
public static final String GROUP_ACTIVITY = "Activity";
@@ -307,6 +316,9 @@ public class ActivitySummaryEntries {
public static final String RECOVERY_TIME_REMAINING_AT_START = "recovery_time_remaining_at_start";
public static final String BODY_ENERGY_AT_START = "body_energy_at_start";
public static final String BODY_ENERGY_AT_END = "body_energy_at_end";
public static final String STAMINA_AT_START = "stamina_at_start";
public static final String STAMINA_AT_END = "stamina_at_end";
public static final String STAMINA_MIN = "stamina_min";
public static final String TOTAL_WORK = "total_work";
public static final String BATTERY_LEVEL_START = "battery_level_start";
public static final String BATTERY_LEVEL_END = "battery_level_end";
@@ -22,6 +22,19 @@ import java.util.Locale
data class AntGadget(val manufacturer: Int, val product: Int, val name: String) {
companion object {
val All: Collection<AntGadget> = listOf(
AntGadget(1, 1, "HRM"),
AntGadget(1, 5, "HRM2"),
AntGadget(1, 7, "HRM3"),
AntGadget(1, 8, "HRM Run"),
AntGadget(1, 9, "BSM"),
AntGadget(1, 10, "BCM"),
AntGadget(1, 12, "HRM-Tri"),
AntGadget(1, 13, "HRM4-Run"),
AntGadget(1, 15, "BSM v3"),
AntGadget(1, 16, "BCM v3"),
AntGadget(1, 18, "HRM-Dual"),
AntGadget(1, 21, "HRMPro+"),
AntGadget(1, 22, "HRM-Fit"),
AntGadget(1, 255, "OHR"),
AntGadget(1, 357, "Rally RS200"),
AntGadget(1, 359, "Varia RTL515"),
@@ -77,6 +90,7 @@ data class AntGadget(val manufacturer: Int, val product: Int, val name: String)
AntGadget(1, 2327, "HRM-Run"),
AntGadget(1, 2337, "Vivoactive HR"),
AntGadget(1, 2348, "Vivosmart HR"),
AntGadget(1, 2396, "Forerunner 235 Asia"),
AntGadget(1, 2413, "Fenix 3 HR"),
AntGadget(1, 2413, "Fenix 3"),
AntGadget(1, 2431, "Forerunner 235"),
@@ -89,6 +103,7 @@ data class AntGadget(val manufacturer: Int, val product: Int, val name: String)
AntGadget(1, 2544, "Fenix 5S"),
AntGadget(1, 2567, "Varia UT800"),
AntGadget(1, 2593, "RD Pod"),
AntGadget(1, 2599, "Edge 820 China"),
AntGadget(1, 2604, "Fenix 5X"),
AntGadget(1, 2622, "Vivosmart 3"),
AntGadget(1, 2623, "Vivosport"),
@@ -295,11 +310,16 @@ data class AntGadget(val manufacturer: Int, val product: Int, val name: String)
AntGadget(23, 56, "Suunto 5 Peak"),
AntGadget(23, 58, "Suunto 9 Peak Pro"),
AntGadget(23, 59, "Suunto Vertical"),
AntGadget(23, 60, "Suunto Race"),
AntGadget(23, 61, "Suunto Race S"),
AntGadget(23, 62, "Suunto Ocean"),
AntGadget(23, 65, "Suunto Run"),
AntGadget(23, 66, "Suunto Race 2"),
AntGadget(23, 67, "Suunto Vertical 2"),
AntGadget(28, 0, "TrainingPeaks Virtual"),
AntGadget(32, 8, "TICKRX"),
AntGadget(32, 28, "ELEMNT"),
AntGadget(32, 31, "ELEMNT BOLT"),
AntGadget(32, 33, "ELEMNT RIVAL"),
AntGadget(32, 35, "TICKR FIT"),
AntGadget(32, 37, "ELEMNT ROAM"),
@@ -309,6 +329,8 @@ data class AntGadget(val manufacturer: Int, val product: Int, val name: String)
AntGadget(32, 338, "TRACKR RADAR"),
AntGadget(69, 1, "Stages DashL50"),
AntGadget(69, 3, "Stages M200"),
AntGadget(86, 28, "Elite SUITO"),
AntGadget(89, 2850, "Tacx NEO 2"),
AntGadget(107, 344, "Magene C506SE"),
AntGadget(123, 2, "Polar H10"),
AntGadget(123, 3, "Polar H9"),
@@ -319,6 +341,7 @@ data class AntGadget(val manufacturer: Int, val product: Int, val name: String)
AntGadget(132, 3, "Cycplus C3"),
AntGadget(136, 345, "Geoid CC600"),
AntGadget(258, 4, "Lezyne Super GPS"),
AntGadget(258, 9, "Lezyne Enhanced Micro C GPS"),
AntGadget(263, 10, "Assioma Uno"),
AntGadget(263, 12, "Assioma Duo"),
AntGadget(267, 1902, "Rider 750"),
@@ -330,9 +353,11 @@ data class AntGadget(val manufacturer: Int, val product: Int, val name: String)
AntGadget(289, 1, "Karoo"),
AntGadget(289, 2, "Karoo 2"),
AntGadget(289, 3, "Karoo 3"),
AntGadget(294, 801, "COROS PACE"),
AntGadget(294, 802, "COROS PACE 2"),
AntGadget(294, 804, "COROS PACE 3"),
AntGadget(294, 805, "COROS PACE Pro"),
AntGadget(294, 806, "COROS PACE 4"),
AntGadget(294, 811, "COROS APEX 42mm"),
AntGadget(294, 812, "COROS APEX 2"),
AntGadget(294, 821, "COROS APEX"),
@@ -309,6 +309,9 @@ public class FitImporter {
if (event.getEvent() == null) {
LOG.warn("Event in {} is null", event);
continue;
} else if (ts == null) {
LOG.warn("Timestamp for {} is null", event);
continue;
}
LOG.trace("Event at {}: {}", ts, event);
@@ -156,6 +156,7 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(40, BaseType.UINT8Z, "rear_gear_num"),
new FieldDefinitionPrimitive(41, BaseType.UINT8Z, "rear_gear", FieldDefinitionFactory.FIELD.ARRAY),
new FieldDefinitionPrimitive(44, BaseType.ENUM, "shimano_di2_enabled", FieldDefinitionFactory.FIELD.BOOLEAN),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP),
new FieldDefinitionPrimitive(254, BaseType.UINT16, "message_index")
));
@@ -164,7 +165,9 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(1, BaseType.UINT8, "max_heart_rate"),
new FieldDefinitionPrimitive(2, BaseType.UINT8, "threshold_heart_rate"),
new FieldDefinitionPrimitive(5, BaseType.ENUM, "hr_calc_type"), //1=percent_max_hr
new FieldDefinitionPrimitive(7, BaseType.ENUM, "pwr_calc_type") //1=percent_ftp
new FieldDefinitionPrimitive(7, BaseType.ENUM, "pwr_calc_type"), //1=percent_ftp
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP),
new FieldDefinitionPrimitive(254, BaseType.UINT16, "message_index")
));
public static final NativeFITMessage HR_ZONE = new NativeFITMessage(8, "HR_ZONE", Arrays.asList(
@@ -293,9 +296,9 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(4, BaseType.SINT32, "start_longitude", FieldDefinitionFactory.FIELD.COORDINATE),
new FieldDefinitionPrimitive(5, BaseType.ENUM, "sport"),
new FieldDefinitionPrimitive(6, BaseType.ENUM, "sub_sport"),
new FieldDefinitionPrimitive(7, BaseType.UINT32, "total_elapsed_time"), // with pauses
new FieldDefinitionPrimitive(8, BaseType.UINT32, "total_timer_time"), // no pauses
new FieldDefinitionPrimitive(9, BaseType.UINT32, "total_distance"), // cm
new FieldDefinitionPrimitive(7, BaseType.UINT32, "total_elapsed_time"), // ms - with pauses - keep it in ms to avoid rounding issue for DB calculations
new FieldDefinitionPrimitive(8, BaseType.UINT32, "total_timer_time", 1000, 0), // s - no pauses
new FieldDefinitionPrimitive(9, BaseType.UINT32, "total_distance", 100, 0), // m
new FieldDefinitionPrimitive(10, BaseType.UINT32, "total_cycles"),
new FieldDefinitionPrimitive(11, BaseType.UINT16, "total_calories"),
new FieldDefinitionPrimitive(13, BaseType.UINT16, "total_fat_calories"), // kcal
@@ -387,7 +390,7 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(108, BaseType.UINT16, "rear_shifts"),
new FieldDefinitionPrimitive(110, BaseType.STRING, 64, "sport_profile_name"),
new FieldDefinitionPrimitive(111, BaseType.UINT8, "sport_index"),
new FieldDefinitionPrimitive(112, BaseType.UINT32, "stand_time"), // s
new FieldDefinitionPrimitive(112, BaseType.UINT32, "stand_time", 1000, 0), // s
new FieldDefinitionPrimitive(113, BaseType.UINT16, "stand_count"),
new FieldDefinitionPrimitive(114, BaseType.SINT8, "avg_left_pco"), // mm
new FieldDefinitionPrimitive(115, BaseType.SINT8, "avg_right_pco"), // mm
@@ -1126,8 +1129,9 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(1, BaseType.ENUM, "sport"),
new FieldDefinitionPrimitive(2, BaseType.UINT32, "distance", 100, 0), // m
new FieldDefinitionPrimitive(3, BaseType.UINT32, "time", 1000, 0), // s
new FieldDefinitionPrimitive(4, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP),
new FieldDefinitionPrimitive(5, BaseType.ENUM, "personal_record")
new FieldDefinitionPrimitive(4, BaseType.UINT32, "start_time", FieldDefinitionFactory.FIELD.TIMESTAMP),
new FieldDefinitionPrimitive(5, BaseType.ENUM, "personal_record"),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static final NativeFITMessage PERSONAL_RECORD = new NativeFITMessage(114, "PERSONAL_RECORD", Arrays.asList(
@@ -1328,6 +1332,19 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(254, BaseType.UINT16, "message_index")
));
public static final NativeFITMessage MULTISPORT_SETTINGS = new NativeFITMessage(143, "MULTISPORT_SETTINGS", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.STRING, "name"),
new FieldDefinitionPrimitive(1, BaseType.ENUM, "transitions"),
new FieldDefinitionPrimitive(2, BaseType.UINT8, "number_of_activities"),
new FieldDefinitionPrimitive(3, BaseType.ENUM, "auto_pause"),
new FieldDefinitionPrimitive(4, BaseType.ENUM, "alerts"),
new FieldDefinitionPrimitive(5, BaseType.ENUM, "auto_lap"),
new FieldDefinitionPrimitive(6, BaseType.ENUM, "power_save_timeout"),
new FieldDefinitionPrimitive(7, BaseType.ENUM, "auto_scroll"),
new FieldDefinitionPrimitive(8, BaseType.ENUM, "repeat"),
new FieldDefinitionPrimitive(10, BaseType.ENUM, "sport_change")
));
public static final NativeFITMessage MULTISPORT_ACTIVITY = new NativeFITMessage(144, "MULTISPORT_ACTIVITY", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.ENUM, "sport"),
new FieldDefinitionPrimitive(1, BaseType.ENUM, "sub_sport"),
@@ -1507,6 +1524,11 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(101, BaseType.BASE_TYPE_BYTE, "field_bits")
));
public static final NativeFITMessage CLUBS = new NativeFITMessage(173, "CLUBS", Arrays.asList(
new FieldDefinitionPrimitive(6, BaseType.UINT32, "average_distance", 100, 0), // m
new FieldDefinitionPrimitive(19, BaseType.UINT32, "max_distance", 100, 0) // m
));
public static final NativeFITMessage OBDII_DATA = new NativeFITMessage(174, "OBDII_DATA", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT16, "timestamp_ms"), // ms
new FieldDefinitionPrimitive(1, BaseType.UINT16, "time_offset", FieldDefinitionFactory.FIELD.ARRAY), // ms
@@ -1573,6 +1595,10 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static final NativeFITMessage WAYPOINT_HANDLING = new NativeFITMessage(189, "WAYPOINT_HANDLING", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.ENUM, "action") // 0=add_to_existing, 1=replace_existing 2=delete_all
));
public static final NativeFITMessage GOLF_COURSE = new NativeFITMessage(190, "GOLF_COURSE", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT32, "course_id"),
new FieldDefinitionPrimitive(1, BaseType.STRING, "name"),
@@ -1760,7 +1786,7 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(9, BaseType.UINT16, "weight_display_unit"),
new FieldDefinitionPrimitive(10, BaseType.UINT16, "message_index"),
new FieldDefinitionPrimitive(11, BaseType.UINT16, "wkt_step_index"),
new FieldDefinitionPrimitive(254, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
new FieldDefinitionPrimitive(254, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) // this timestamp is really 254 and not the typical 253
));
public static final NativeFITMessage DIVE_SETTINGS = new NativeFITMessage(258, "DIVE_SETTINGS", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.STRING, "name"),
@@ -2004,7 +2030,8 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(11, BaseType.SINT32, "end_position_lat", FieldDefinitionFactory.FIELD.COORDINATE),
new FieldDefinitionPrimitive(12, BaseType.SINT32, "end_position_long", FieldDefinitionFactory.FIELD.COORDINATE),
new FieldDefinitionPrimitive(13, BaseType.UINT32, "start_altitude", 500, 5), // m
new FieldDefinitionPrimitive(14, BaseType.UINT32, "end_altitude", 500, 5) // m
new FieldDefinitionPrimitive(14, BaseType.UINT32, "end_altitude", 500, 5), // m
new FieldDefinitionPrimitive(254, BaseType.UINT16, "message_index")
));
public static final NativeFITMessage SPLIT = new NativeFITMessage(312, "SPLIT", Arrays.asList(
@@ -2027,6 +2054,7 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(74, BaseType.UINT32, "start_elevation", 5, 500), // meter
new FieldDefinitionPrimitive(78, BaseType.UINT32, "active_time", 1000, 0), // s
new FieldDefinitionPrimitive(110, BaseType.UINT32, "total_moving_time", 1000, 0), // seconds
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP),
new FieldDefinitionPrimitive(254, BaseType.UINT16, "message_index")
));
@@ -2378,7 +2406,8 @@ public class NativeFITMessage {
new FieldDefinitionPrimitive(5, BaseType.UINT8, "est_aerobic_te", 10, 0),
new FieldDefinitionPrimitive(6, BaseType.UINT8, "est_anaer_te", 10, 0),
new FieldDefinitionPrimitive(7, BaseType.ENUM, "sport"),
new FieldDefinitionPrimitive(9, BaseType.UINT32, "duration",1000, 0) //s
new FieldDefinitionPrimitive(9, BaseType.UINT32, "duration",1000, 0), //s
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static final NativeFITMessage SLEEP_DISRUPTION_SEVERITY_PERIOD = new NativeFITMessage(470, "SLEEP_DISRUPTION_SEVERITY_PERIOD", Arrays.asList(
@@ -2457,6 +2486,7 @@ public class NativeFITMessage {
put(140, PHYSIOLOGICAL_METRICS);
put(141, EPO_STATUS);
put(142, SEGMENT_LAP);
put(143, MULTISPORT_SETTINGS);
put(144, MULTISPORT_ACTIVITY);
put(145, MEMO_GLOB);
put(147, SENSOR_SETTINGS);
@@ -2475,6 +2505,7 @@ public class NativeFITMessage {
put(167, THREE_D_SENSOR_CALIBRATION);
put(169, VIDEO_FRAME);
put(170, CONNECT_IQ_FIELD);
put(173, CLUBS);
put(174, OBDII_DATA);
put(177, NMEA_SENTENCE);
put(178, AVIATION_ATTITUDE);
@@ -2483,6 +2514,7 @@ public class NativeFITMessage {
put(186, VIDEO_DESCRIPTION);
put(187, VIDEO_CLIP);
put(188, OHR_SETTINGS);
put(189, WAYPOINT_HANDLING);
put(190, GOLF_COURSE);
put(191, GOLF_STATS);
put(192, SCORE);
@@ -25,7 +25,7 @@ public enum GarminSport {
STREET_RUN(1, 2, ActivityKind.STREET_RUNNING),
TRAIL_RUN(1, 3, ActivityKind.TRAIL_RUN),
TRACK_RUN(1, 4, ActivityKind.TRACK_RUN),
INDOOR_TRACK(1, 45, ActivityKind.INDOOR_TRACK),
INDOOR_TRACK(1, 45, ActivityKind.INDOOR_TRACK_RUNNING),
ORIENTEERING(1, 50, ActivityKind.ORIENTEERING),
VIRTUAL_RUN(1, 58, ActivityKind.VIRTUAL_RUN),
OBSTACLE_RACING(1, 59, ActivityKind.OBSTACLE_RACE),
@@ -55,7 +55,7 @@ public class FitBestEffort extends RecordData {
}
@Nullable
public Long getTimestamp() {
public Long getStartTime() {
return getFieldByNumber(4, Long.class);
}
@@ -64,6 +64,11 @@ public class FitBestEffort extends RecordData {
return getFieldByNumber(5, Integer.class);
}
@Nullable
public Long getTimestamp() {
return getFieldByNumber(253, Long.class);
}
/**
* @noinspection unused
*/
@@ -87,7 +92,7 @@ public class FitBestEffort extends RecordData {
return this;
}
public Builder setTimestamp(final Long value) {
public Builder setStartTime(final Long value) {
setFieldByNumber(4, value);
return this;
}
@@ -97,6 +102,11 @@ public class FitBestEffort extends RecordData {
return this;
}
public Builder setTimestamp(final Long value) {
setFieldByNumber(253, value);
return this;
}
@Override
public FitBestEffort build() {
return (FitBestEffort) super.build();
@@ -194,6 +194,11 @@ public class FitBikeProfile extends RecordData {
return getFieldByNumber(44, Boolean.class);
}
@Nullable
public Long getTimestamp() {
return getFieldByNumber(253, Long.class);
}
@Nullable
public Integer getMessageIndex() {
return getFieldByNumber(254, Integer.class);
@@ -362,6 +367,11 @@ public class FitBikeProfile extends RecordData {
return this;
}
public Builder setTimestamp(final Long value) {
setFieldByNumber(253, value);
return this;
}
public Builder setMessageIndex(final Integer value) {
setFieldByNumber(254, value);
return this;
@@ -0,0 +1,80 @@
/* Copyright (C) 2026 Freeyourgadget
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitRecordDataBuilder;
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 {@link nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen}
*
* @noinspection unused
*/
public class FitClubs extends RecordData {
public FitClubs(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int nativeNumber = recordDefinition.getNativeFITMessage().getNumber();
if (nativeNumber != 173) {
throw new IllegalArgumentException("FitClubs expects native messages of " + 173 + ", got " + nativeNumber);
}
}
@Nullable
public Double getAverageDistance() {
return getFieldByNumber(6, Double.class);
}
@Nullable
public Double getMaxDistance() {
return getFieldByNumber(19, Double.class);
}
/**
* @noinspection unused
*/
public static class Builder extends FitRecordDataBuilder {
public Builder() {
super(173);
}
public Builder setAverageDistance(final Double value) {
setFieldByNumber(6, value);
return this;
}
public Builder setMaxDistance(final Double value) {
setFieldByNumber(19, value);
return this;
}
@Override
public FitClubs build() {
return (FitClubs) super.build();
}
@Override
public FitClubs build(final int localMessageType) {
return (FitClubs) super.build(localMessageType);
}
}
}
@@ -0,0 +1,160 @@
/* Copyright (C) 2026 Freeyourgadget
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitRecordDataBuilder;
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 {@link nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen}
*
* @noinspection unused
*/
public class FitMultisportSettings extends RecordData {
public FitMultisportSettings(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int nativeNumber = recordDefinition.getNativeFITMessage().getNumber();
if (nativeNumber != 143) {
throw new IllegalArgumentException("FitMultisportSettings expects native messages of " + 143 + ", got " + nativeNumber);
}
}
@Nullable
public String getName() {
return getFieldByNumber(0, String.class);
}
@Nullable
public Integer getTransitions() {
return getFieldByNumber(1, Integer.class);
}
@Nullable
public Integer getNumberOfActivities() {
return getFieldByNumber(2, Integer.class);
}
@Nullable
public Integer getAutoPause() {
return getFieldByNumber(3, Integer.class);
}
@Nullable
public Integer getAlerts() {
return getFieldByNumber(4, Integer.class);
}
@Nullable
public Integer getAutoLap() {
return getFieldByNumber(5, Integer.class);
}
@Nullable
public Integer getPowerSaveTimeout() {
return getFieldByNumber(6, Integer.class);
}
@Nullable
public Integer getAutoScroll() {
return getFieldByNumber(7, Integer.class);
}
@Nullable
public Integer getRepeat() {
return getFieldByNumber(8, Integer.class);
}
@Nullable
public Integer getSportChange() {
return getFieldByNumber(10, Integer.class);
}
/**
* @noinspection unused
*/
public static class Builder extends FitRecordDataBuilder {
public Builder() {
super(143);
}
public Builder setName(final String value) {
setFieldByNumber(0, value);
return this;
}
public Builder setTransitions(final Integer value) {
setFieldByNumber(1, value);
return this;
}
public Builder setNumberOfActivities(final Integer value) {
setFieldByNumber(2, value);
return this;
}
public Builder setAutoPause(final Integer value) {
setFieldByNumber(3, value);
return this;
}
public Builder setAlerts(final Integer value) {
setFieldByNumber(4, value);
return this;
}
public Builder setAutoLap(final Integer value) {
setFieldByNumber(5, value);
return this;
}
public Builder setPowerSaveTimeout(final Integer value) {
setFieldByNumber(6, value);
return this;
}
public Builder setAutoScroll(final Integer value) {
setFieldByNumber(7, value);
return this;
}
public Builder setRepeat(final Integer value) {
setFieldByNumber(8, value);
return this;
}
public Builder setSportChange(final Integer value) {
setFieldByNumber(10, value);
return this;
}
@Override
public FitMultisportSettings build() {
return (FitMultisportSettings) super.build();
}
@Override
public FitMultisportSettings build(final int localMessageType) {
return (FitMultisportSettings) super.build(localMessageType);
}
}
}
@@ -1025,7 +1025,7 @@ public class FitRecord extends RecordData {
builder.setN2Load(getN2Load());
builder.setPower(getPower());
builder.setStamina(getStamina());
builder.setStride(getStepLength());
builder.setStepLength(getStepLength());
builder.setTemperature(getTemperature());
final Double enhancedAltitude = getEnhancedAltitude();
@@ -95,6 +95,7 @@ public class FitRecordDataFactory {
case 140 -> new FitPhysiologicalMetrics(recordDefinition, recordHeader);
case 141 -> new FitEpoStatus(recordDefinition, recordHeader);
case 142 -> new FitSegmentLap(recordDefinition, recordHeader);
case 143 -> new FitMultisportSettings(recordDefinition, recordHeader);
case 144 -> new FitMultisportActivity(recordDefinition, recordHeader);
case 145 -> new FitMemoGlob(recordDefinition, recordHeader);
case 147 -> new FitSensorSettings(recordDefinition, recordHeader);
@@ -113,6 +114,7 @@ public class FitRecordDataFactory {
case 167 -> new FitThreeDSensorCalibration(recordDefinition, recordHeader);
case 169 -> new FitVideoFrame(recordDefinition, recordHeader);
case 170 -> new FitConnectIqField(recordDefinition, recordHeader);
case 173 -> new FitClubs(recordDefinition, recordHeader);
case 174 -> new FitObdiiData(recordDefinition, recordHeader);
case 177 -> new FitNmeaSentence(recordDefinition, recordHeader);
case 178 -> new FitAviationAttitude(recordDefinition, recordHeader);
@@ -121,6 +123,7 @@ public class FitRecordDataFactory {
case 186 -> new FitVideoDescription(recordDefinition, recordHeader);
case 187 -> new FitVideoClip(recordDefinition, recordHeader);
case 188 -> new FitOhrSettings(recordDefinition, recordHeader);
case 189 -> new FitWaypointHandling(recordDefinition, recordHeader);
case 190 -> new FitGolfCourse(recordDefinition, recordHeader);
case 191 -> new FitGolfStats(recordDefinition, recordHeader);
case 192 -> new FitScore(recordDefinition, recordHeader);
@@ -86,13 +86,13 @@ public class FitSession extends RecordData {
}
@Nullable
public Long getTotalTimerTime() {
return getFieldByNumber(8, Long.class);
public Double getTotalTimerTime() {
return getFieldByNumber(8, Double.class);
}
@Nullable
public Long getTotalDistance() {
return getFieldByNumber(9, Long.class);
public Double getTotalDistance() {
return getFieldByNumber(9, Double.class);
}
@Nullable
@@ -551,8 +551,8 @@ public class FitSession extends RecordData {
}
@Nullable
public Long getStandTime() {
return getFieldByNumber(112, Long.class);
public Double getStandTime() {
return getFieldByNumber(112, Double.class);
}
@Nullable
@@ -1018,12 +1018,12 @@ public class FitSession extends RecordData {
return this;
}
public Builder setTotalTimerTime(final Long value) {
public Builder setTotalTimerTime(final Double value) {
setFieldByNumber(8, value);
return this;
}
public Builder setTotalDistance(final Long value) {
public Builder setTotalDistance(final Double value) {
setFieldByNumber(9, value);
return this;
}
@@ -1483,7 +1483,7 @@ public class FitSession extends RecordData {
return this;
}
public Builder setStandTime(final Long value) {
public Builder setStandTime(final Double value) {
setFieldByNumber(112, value);
return this;
}
@@ -134,6 +134,11 @@ public class FitSplit extends RecordData {
return getFieldByNumber(110, Double.class);
}
@Nullable
public Long getTimestamp() {
return getFieldByNumber(253, Long.class);
}
@Nullable
public Integer getMessageIndex() {
return getFieldByNumber(254, Integer.class);
@@ -242,6 +247,11 @@ public class FitSplit extends RecordData {
return this;
}
public Builder setTimestamp(final Long value) {
setFieldByNumber(253, value);
return this;
}
public Builder setMessageIndex(final Integer value) {
setFieldByNumber(254, value);
return this;
@@ -94,6 +94,11 @@ public class FitSplitTime extends RecordData {
return getFieldByNumber(14, Double.class);
}
@Nullable
public Integer getMessageIndex() {
return getFieldByNumber(254, Integer.class);
}
/**
* @noinspection unused
*/
@@ -157,6 +162,11 @@ public class FitSplitTime extends RecordData {
return this;
}
public Builder setMessageIndex(final Integer value) {
setFieldByNumber(254, value);
return this;
}
@Override
public FitSplitTime build() {
return (FitSplitTime) super.build();
@@ -0,0 +1,70 @@
/* Copyright (C) 2026 Freeyourgadget
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitRecordDataBuilder;
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 {@link nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen}
*
* @noinspection unused
*/
public class FitWaypointHandling extends RecordData {
public FitWaypointHandling(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int nativeNumber = recordDefinition.getNativeFITMessage().getNumber();
if (nativeNumber != 189) {
throw new IllegalArgumentException("FitWaypointHandling expects native messages of " + 189 + ", got " + nativeNumber);
}
}
@Nullable
public Integer getAction() {
return getFieldByNumber(0, Integer.class);
}
/**
* @noinspection unused
*/
public static class Builder extends FitRecordDataBuilder {
public Builder() {
super(189);
}
public Builder setAction(final Integer value) {
setFieldByNumber(0, value);
return this;
}
@Override
public FitWaypointHandling build() {
return (FitWaypointHandling) super.build();
}
@Override
public FitWaypointHandling build(final int localMessageType) {
return (FitWaypointHandling) super.build(localMessageType);
}
}
}
@@ -69,6 +69,11 @@ public class FitWorkoutSchedule extends RecordData {
return getFieldByNumber(9, Double.class);
}
@Nullable
public Long getTimestamp() {
return getFieldByNumber(253, Long.class);
}
/**
* @noinspection unused
*/
@@ -107,6 +112,11 @@ public class FitWorkoutSchedule extends RecordData {
return this;
}
public Builder setTimestamp(final Long value) {
setFieldByNumber(253, value);
return this;
}
@Override
public FitWorkoutSchedule build() {
return (FitWorkoutSchedule) super.build();
@@ -64,6 +64,16 @@ public class FitZonesTarget extends RecordData {
return getFieldByNumber(7, Integer.class);
}
@Nullable
public Long getTimestamp() {
return getFieldByNumber(253, Long.class);
}
@Nullable
public Integer getMessageIndex() {
return getFieldByNumber(254, Integer.class);
}
/**
* @noinspection unused
*/
@@ -97,6 +107,16 @@ public class FitZonesTarget extends RecordData {
return this;
}
public Builder setTimestamp(final Long value) {
setFieldByNumber(253, value);
return this;
}
public Builder setMessageIndex(final Integer value) {
setFieldByNumber(254, value);
return this;
}
@Override
public FitZonesTarget build() {
return (FitZonesTarget) super.build();
@@ -316,7 +316,7 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
final short pace = buf.getShort(); // sec/km
activityPointBuilder.setCadence(cadence);
activityPointBuilder.setStride(stride * 10.0f);
activityPointBuilder.setStepLength(stride / 2.0f * 10.0f); // stride cm -> step mm
if (pace != 0) {
activityPointBuilder.setSpeed(1000f / pace); // s/km -> m/s
}
@@ -339,6 +339,17 @@ public class DateTimeUtils {
return format.format(date);
}
/// format duration day - milli seconds with max 3 units shown
public static String formatSportsDuration(long duration, TimeUnit unit) {
DurationFormatter df = DurationFormatter.Builder.SYMBOLS
.maximum(TimeUnit.HOURS)
.minimum(TimeUnit.MILLISECONDS)
.suppressZeros(DurationFormatter.SuppressZeros.LEADING, DurationFormatter.SuppressZeros.TRAILING)
.maximumAmountOfUnitsToShow(3)
.build();
return df.format(duration, unit);
}
/// number of seconds since UTC epoch of 1970-01-01T00:00:00Z
public static long getEpochSeconds() {
final long epoc;
@@ -123,7 +123,7 @@ internal object WorkoutSyncerUtils {
ActivityKind.FITNESS_EQUIPMENT, ActivityKind.INDOOR_FITNESS, ActivityKind.SOMATOSENSORY_GAME, ActivityKind.VIDEO_GAMING,
ActivityKind.HEALTH_SNAPSHOT, ActivityKind.TACTICAL, ActivityKind.GRINDING, ActivityKind.WINTER_SPORT, ActivityKind.TEAM_SPORT,
ActivityKind.SNOW_SPORTS, ActivityKind.DISC_SPORTS, ActivityKind.OTHER_WATER_SPORTS, ActivityKind.OTHER_WINTER_SPORTS,
ActivityKind.MARINE, ActivityKind.PARA_SPORT, ActivityKind.RACKET, ActivityKind.NAVIGATE, ActivityKind.INDOOR_TRACK,
ActivityKind.MARINE, ActivityKind.PARA_SPORT, ActivityKind.RACKET, ActivityKind.NAVIGATE, ActivityKind.INDOOR_TRACK_RUNNING,
ActivityKind.TRANSITION, ActivityKind.VIVOMOVE_HR_TRANSITION,
ActivityKind.FLYING, ActivityKind.MOTORCYCLING, ActivityKind.BOATING, ActivityKind.DRIVING, ActivityKind.HUNTING, ActivityKind.FISHING,
ActivityKind.AUTO_RACING, ActivityKind.SNOWMOBILING, ActivityKind.KARTING, ActivityKind.BILLIARDS, ActivityKind.BOWLING,
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M520,720L520,577L475,528L454,634L304,603L312,564L423,587L462,391L400,414L400,480L360,480L360,386L491,338Q506,333 520.5,339Q535,345 542,359Q567,410 597.5,425Q628,440 640,440L640,480Q619,480 585,463.5Q551,447 524,410L507,504L560,561L560,720L520,720ZM540,320Q523,320 511.5,308.5Q500,297 500,280Q500,263 511.5,251.5Q523,240 540,240Q557,240 568.5,251.5Q580,263 580,280Q580,297 568.5,308.5Q557,320 540,320ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800Z"/>
</vector>
+8 -5
View File
@@ -1059,9 +1059,9 @@
<string name="stride">Stride</string>
<string name="stamina">Stamina</string>
<string name="step_length">Step Length</string>
<string name="diving_cns_toxicity">CNS toxicity</string>
<string name="diving_nitrogen_load">Nitrogen load</string>
<string name="diving_oxygen_toxicity">Oxygen toxicity</string>
<string name="diving_cns_toxicity">CNS Toxicity</string>
<string name="diving_nitrogen_load">Nitrogen Load</string>
<string name="diving_oxygen_toxicity">Oxygen Toxicity</string>
<string name="unit_oxygen_toxicity_units">OTU</string>
<string name="diving_surface_interval">Surface Interval</string>
<string name="clock">Clock</string>
@@ -2726,8 +2726,11 @@
<string name="battery_status_new">New</string>
<string name="battery_status_ok">Ok</string>
<string name="battery_status_unknown">Unknown</string>
<string name="body_energy_at_end">Finishing body energy</string>
<string name="body_energy_at_start">Initial body energy</string>
<string name="body_energy_at_end">Finishing Body Energy</string>
<string name="body_energy_at_start">Initial Body Energy</string>
<string name="stamina_at_end">Finishing Stamina</string>
<string name="stamina_at_start">Initial Stamina</string>
<string name="stamina_min">Minimum Stamina</string>
<string name="device">Device</string>
<string name="diving_pressure_sac_avg">Surface air consumption</string>
<string name="hr_user_max">User Max Heart Rate</string>
@@ -1,5 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums;
/* Copyright (C) 2024-2026 José Rebelo, Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
import static org.junit.Assert.assertTrue;
import android.util.Pair;
@@ -7,8 +23,10 @@ import android.util.Pair;
import org.junit.Test;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
public class GarminSportTest extends TestBase {
@@ -28,4 +46,16 @@ public class GarminSportTest extends TestBase {
assertTrue("Duplicated sport codes: " + duplicates, duplicates.isEmpty());
}
// Ensure that every GarminSport has a subsport=0 fallback
@Test
public void testFallbackSport() {
int defaultIcon = ActivityKind.ACTIVITY.getIcon();
for (final GarminSport sport : GarminSport.values()) {
if (sport.getSubtype() != 0) {
Optional<GarminSport> fallback = GarminSport.fromCodes(sport.getType(), 0);
assertTrue(sport.name() + " has no fallback", fallback.isPresent());
}
}
}
}