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 a23847460..ec615dfda 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java @@ -105,6 +105,7 @@ public class ActivitySummaryEntries { public static final String WORKOUT_LOAD = "currentWorkoutLoad"; 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 CYCLING_POWER_AVERAGE = "cyclingPowerAverage"; public static final String CYCLING_POWER_MIN = "cyclingPowerMin"; @@ -114,6 +115,7 @@ public class ActivitySummaryEntries { public static final String UNIT_CM = "cm"; public static final String UNIT_UNIX_EPOCH_SECONDS = "unix_epoch_seconds"; public static final String UNIT_KCAL = "calories_unit"; + public static final String UNIT_ML = "ml"; public static final String UNIT_LAPS = "laps_unit"; public static final String UNIT_KILOMETERS = "km"; public static final String UNIT_METERS = "meters"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java index 44426d45f..62c7ec60d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java @@ -7,6 +7,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrTimeInZone; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrZoneHighBoundary; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem; @@ -34,6 +36,10 @@ public class FieldDefinitionFactory { return new FieldDefinitionGoalType(localNumber, size, baseType, name); case HRV_STATUS: return new FieldDefinitionHrvStatus(localNumber, size, baseType, name); + case HR_TIME_IN_ZONE: + return new FieldDefinitionHrTimeInZone(localNumber, size, baseType, name); + case HR_ZONE_HIGH_BOUNDARY: + return new FieldDefinitionHrZoneHighBoundary(localNumber, size, baseType, name); case MEASUREMENT_SYSTEM: return new FieldDefinitionMeasurementSystem(localNumber, size, baseType, name); case TEMPERATURE: @@ -62,6 +68,8 @@ public class FieldDefinitionFactory { GOAL_SOURCE, GOAL_TYPE, HRV_STATUS, + HR_TIME_IN_ZONE, + HR_ZONE_HIGH_BOUNDARY, MEASUREMENT_SYSTEM, TEMPERATURE, TIMESTAMP, 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 8dd0e8e1f..f56cf0bde 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 @@ -5,8 +5,19 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries. import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_BURNT; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DESCENT_DISTANCE; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ESTIMATED_SWEAT_LOSS; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_AVG; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MAX; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_AEROBIC; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_ANAEROBIC; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_EXTREME; +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.UNIT_BPM; 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_SECONDS; import android.content.Context; @@ -336,6 +347,15 @@ public class FitImporter { if (session.getTotalCalories() != null) { summaryData.add(CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL); } + if (session.getEstimatedSweatLoss() != null) { + summaryData.add(ESTIMATED_SWEAT_LOSS, session.getEstimatedSweatLoss(), UNIT_ML); + } + if (session.getAverageHeartRate() != null) { + summaryData.add(HR_AVG, session.getAverageHeartRate(), UNIT_BPM); + } + if (session.getMaxHeartRate() != null) { + summaryData.add(HR_MAX, session.getMaxHeartRate(), UNIT_BPM); + } if (session.getTotalAscent() != null) { summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS); } @@ -343,16 +363,21 @@ public class FitImporter { summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS); } - //FitTimeInZone timeInZone = null; - //for (final FitTimeInZone fitTimeInZone : timesInZone) { - // // Find the firt time in zone for the session (assumes single-session) - // if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) { - // timeInZone = fitTimeInZone; - // break; - // } - //} - //if (timeInZone != null) { - //} + for (final FitTimeInZone fitTimeInZone : timesInZone) { + // Find the firt time in zone for the session (assumes single-session) + if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) { + final Double[] timeInZone = fitTimeInZone.getTimeInZone(); + if (timeInZone != null && timeInZone.length == 6) { + summaryData.add(HR_ZONE_NA, timeInZone[0].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_WARM_UP, timeInZone[1].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_FAT_BURN, timeInZone[2].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_AEROBIC, timeInZone[3].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_ANAEROBIC, timeInZone[4].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_EXTREME, timeInZone[5].floatValue(), UNIT_SECONDS); + } + break; + } + } summary.setSummaryData(summaryData.toString()); if (file != null) { 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 f7d33ce73..9273f1a44 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 @@ -99,9 +99,12 @@ public class GlobalFITMessage { new FieldDefinitionPrimitive(8, BaseType.UINT32, "total_timer_time"), // no pauses new FieldDefinitionPrimitive(9, BaseType.UINT32, "total_distance"), new FieldDefinitionPrimitive(11, BaseType.UINT16, "total_calories"), + new FieldDefinitionPrimitive(16, BaseType.UINT8, "average_heart_rate"), + new FieldDefinitionPrimitive(17, BaseType.UINT8, "max_heart_rate"), new FieldDefinitionPrimitive(22, BaseType.UINT16, "total_ascent"), new FieldDefinitionPrimitive(23, BaseType.UINT16, "total_descent"), new FieldDefinitionPrimitive(110, BaseType.STRING, 64, "sport_profile_name"), + new FieldDefinitionPrimitive(178, BaseType.UINT16, "estimated_sweat_loss"), new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) )); @@ -231,8 +234,8 @@ public class GlobalFITMessage { public static GlobalFITMessage TIME_IN_ZONE = new GlobalFITMessage(216, "TIME_IN_ZONE", Arrays.asList( new FieldDefinitionPrimitive(0, BaseType.UINT16, "reference_message"), new FieldDefinitionPrimitive(1, BaseType.UINT16, "reference_index"), - new FieldDefinitionPrimitive(2, BaseType.UINT32, "time_in_zone"), // seconds - new FieldDefinitionPrimitive(6, BaseType.UINT8, "hr_zone_high_boundary"), // bpm + new FieldDefinitionPrimitive(2, BaseType.UINT32, "time_in_zone", FieldDefinitionFactory.FIELD.HR_TIME_IN_ZONE), // seconds + new FieldDefinitionPrimitive(6, BaseType.UINT8, "hr_zone_high_boundary", FieldDefinitionFactory.FIELD.HR_ZONE_HIGH_BOUNDARY), // bpm new FieldDefinitionPrimitive(10, BaseType.ENUM, "hr_calc_type"), // 1 percent max hr new FieldDefinitionPrimitive(11, BaseType.UINT8, "max_heart_rate"), new FieldDefinitionPrimitive(12, BaseType.UINT8, "resting_heart_rate"), @@ -244,6 +247,14 @@ public class GlobalFITMessage { new FieldDefinitionPrimitive(0, BaseType.UINT16, "time", FieldDefinitionFactory.FIELD.ALARM) )); + public static GlobalFITMessage SET = new GlobalFITMessage(225, "SET", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT32, "duration"), + new FieldDefinitionPrimitive(5, BaseType.UINT8, "set_type"), // 1 active 0 rest + new FieldDefinitionPrimitive(6, BaseType.UINT32, "start_time", FieldDefinitionFactory.FIELD.TIMESTAMP), + new FieldDefinitionPrimitive(10, BaseType.UINT16, "message_index"), + new FieldDefinitionPrimitive(254, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) + )); + 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), @@ -309,6 +320,7 @@ public class GlobalFITMessage { put(207, DEVELOPER_DATA); put(216, TIME_IN_ZONE); put(222, ALARM_SETTINGS); + put(225, SET); put(227, STRESS_LEVEL); put(269, SPO2); put(275, SLEEP_STAGE); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java index 57cb13398..71d801f43 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java @@ -190,7 +190,19 @@ public class FitCodeGen { sb.append("\n"); sb.append(" @Nullable\n"); sb.append(" public ").append(fieldTypeName).append(method(" get", primitive)).append("() {\n"); - sb.append(" return (").append(fieldTypeName).append(") getFieldByNumber(").append(primitive.getNumber()).append(");\n"); + if (fieldTypeName.endsWith("[]")) { + // Special case for arrays, since these are decoded in RecordData and we can't easily decode them with the correct type + // FIXME this should be refactored... + final String simpleTypeName = fieldTypeName.replace("[]", ""); + sb.append(" final Object[] objectsArray = (Object[]) getFieldByNumber(").append(primitive.getNumber()).append(");\n"); + sb.append(" final ").append(fieldTypeName).append(" ret = new ").append(simpleTypeName).append("[objectsArray.length];\n"); + sb.append(" for (int i = 0; i < objectsArray.length; i++) {\n"); + sb.append(" ret[i] = (").append(simpleTypeName).append(") objectsArray[i];\n"); + sb.append(" }\n"); + sb.append(" return ret;\n"); + } else { + sb.append(" return (").append(fieldTypeName).append(") getFieldByNumber(").append(primitive.getNumber()).append(");\n"); + } sb.append(" }\n"); } @@ -238,6 +250,10 @@ public class FitCodeGen { return FieldDefinitionGoalType.Type.class; case HRV_STATUS: return FieldDefinitionHrvStatus.HrvStatus.class; + case HR_TIME_IN_ZONE: + return Double[].class; + case HR_ZONE_HIGH_BOUNDARY: + return Integer[].class; case MEASUREMENT_SYSTEM: return FieldDefinitionMeasurementSystem.Type.class; case TEMPERATURE: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrTimeInZone.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrTimeInZone.java new file mode 100644 index 000000000..423da311f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrTimeInZone.java @@ -0,0 +1,10 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionHrTimeInZone extends FieldDefinition { + public FieldDefinitionHrTimeInZone(final int localNumber, final int size, final BaseType baseType, final String name) { + super(localNumber, size, baseType, name, 1000, 0); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrZoneHighBoundary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrZoneHighBoundary.java new file mode 100644 index 000000000..f7c04bc6b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionHrZoneHighBoundary.java @@ -0,0 +1,10 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionHrZoneHighBoundary extends FieldDefinition { + public FieldDefinitionHrZoneHighBoundary(final int localNumber, final int size, final BaseType baseType, final String name) { + super(localNumber, size, baseType, name, 1, 0); + } +} 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 a7f392df8..e2a33e988 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 @@ -59,6 +59,8 @@ public class FitRecordDataFactory { return new FitTimeInZone(recordDefinition, recordHeader); case 222: return new FitAlarmSettings(recordDefinition, recordHeader); + case 225: + return new FitSet(recordDefinition, recordHeader); case 227: return new FitStressLevel(recordDefinition, recordHeader); case 269: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSession.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSession.java index 7b47c3d8b..ab63e5099 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSession.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSession.java @@ -75,6 +75,16 @@ public class FitSession extends RecordData { return (Integer) getFieldByNumber(11); } + @Nullable + public Integer getAverageHeartRate() { + return (Integer) getFieldByNumber(16); + } + + @Nullable + public Integer getMaxHeartRate() { + return (Integer) getFieldByNumber(17); + } + @Nullable public Integer getTotalAscent() { return (Integer) getFieldByNumber(22); @@ -90,6 +100,11 @@ public class FitSession extends RecordData { return (String) getFieldByNumber(110); } + @Nullable + public Integer getEstimatedSweatLoss() { + return (Integer) getFieldByNumber(178); + } + @Nullable public Long getTimestamp() { return (Long) getFieldByNumber(253); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSet.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSet.java new file mode 100644 index 000000000..cb254b282 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitSet.java @@ -0,0 +1,47 @@ +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 FitSet extends RecordData { + public FitSet(final RecordDefinition recordDefinition, final RecordHeader recordHeader) { + super(recordDefinition, recordHeader); + + final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber(); + if (globalNumber != 225) { + throw new IllegalArgumentException("FitSet expects global messages of " + 225 + ", got " + globalNumber); + } + } + + @Nullable + public Long getDuration() { + return (Long) getFieldByNumber(0); + } + + @Nullable + public Integer getSetType() { + return (Integer) getFieldByNumber(5); + } + + @Nullable + public Long getStartTime() { + return (Long) getFieldByNumber(6); + } + + @Nullable + public Integer getMessageIndex() { + return (Integer) getFieldByNumber(10); + } + + @Nullable + public Long getTimestamp() { + return (Long) getFieldByNumber(254); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitTimeInZone.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitTimeInZone.java index d5b80024a..0d69961d0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitTimeInZone.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitTimeInZone.java @@ -31,13 +31,23 @@ public class FitTimeInZone extends RecordData { } @Nullable - public Long getTimeInZone() { - return (Long) getFieldByNumber(2); + public Double[] getTimeInZone() { + final Object[] objectsArray = (Object[]) getFieldByNumber(2); + final Double[] ret = new Double[objectsArray.length]; + for (int i = 0; i < objectsArray.length; i++) { + ret[i] = (Double) objectsArray[i]; + } + return ret; } @Nullable - public Integer getHrZoneHighBoundary() { - return (Integer) getFieldByNumber(6); + public Integer[] getHrZoneHighBoundary() { + final Object[] objectsArray = (Object[]) getFieldByNumber(6); + final Integer[] ret = new Integer[objectsArray.length]; + for (int i = 0; i < objectsArray.length; i++) { + ret[i] = (Integer) objectsArray[i]; + } + return ret; } @Nullable diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e2a41320..3df5ba088 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2092,6 +2092,7 @@ Anaerobic Effect Workout Load Maximum Oxygen Uptake + Estimated Sweat Loss Average Stride Max Stride Min Stride