From 918791560be2b3e1444dfb7140ab9036b3f9ddaa Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Tue, 2 Apr 2024 15:10:28 +0200 Subject: [PATCH] Garmin protocol: change naming and logic of several FIT classes - refactor the logic of Global and Local messages - add some Global messages with naming taken from [1] - Global messages are not enum because there are too many - introduce the concept of FieldDefinitionPrimitive - add new Field Definitions - add support for developer fields and array fields - add test case for FIT files taken from [0] [0] https://github.com/polyvertex/fitdecode/ [1] https://www.fitfileviewer.com/ --- .../service/devices/garmin/GarminSupport.java | 16 +- .../garmin/fit/DevFieldDefinition.java | 40 +-- .../devices/garmin/fit/FieldDefinition.java | 41 ++- .../garmin/fit/FieldDefinitionFactory.java | 63 +++++ .../garmin/fit/GlobalDefinitionsEnum.java | 91 ------- .../devices/garmin/fit/GlobalFITMessage.java | 256 ++++++++++++++++++ .../devices/garmin/fit/LocalMessage.java | 54 ++++ .../service/devices/garmin/fit/MesgType.java | 32 --- .../devices/garmin/fit/RecordData.java | 97 +++++-- .../devices/garmin/fit/RecordDefinition.java | 77 ++++-- .../devices/garmin/fit/RecordHeader.java | 56 +++- .../garmin/fit/baseTypes/BaseTypeInt.java | 2 +- .../FieldDefinitionAlarm.java | 31 +++ .../FieldDefinitionFileType.java | 62 +++++ .../FieldDefinitionGoalSource.java | 48 ++++ .../FieldDefinitionGoalType.java | 56 ++++ .../FieldDefinitionLanguage.java | 57 ++++ .../FieldDefinitionMeasurementSystem.java | 44 +++ .../garmin/messages/FitDataMessage.java | 3 +- .../devices/garmin/GarminSupportTest.java | 187 ++++++++++++- 20 files changed, 1074 insertions(+), 239 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/LocalMessage.java delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarm.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalSource.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionLanguage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionMeasurementSystem.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index ae2e8257c..0b9ba3e1e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -32,7 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator. import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v2.CommunicatorV2; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalDefinitionsEnum; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.LocalMessage; 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.messages.ConfigurationMessage; @@ -159,14 +159,14 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni List weatherData = new ArrayList<>(); List weatherDefinitions = new ArrayList<>(3); - weatherDefinitions.add(GlobalDefinitionsEnum.TODAY_WEATHER_CONDITIONS.getRecordDefinition()); - weatherDefinitions.add(GlobalDefinitionsEnum.HOURLY_WEATHER_FORECAST.getRecordDefinition()); - weatherDefinitions.add(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition()); + weatherDefinitions.add(LocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition()); + weatherDefinitions.add(LocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition()); + weatherDefinitions.add(LocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition()); communicator.sendMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage(weatherDefinitions).getOutgoingMessage()); try { - RecordData today = new RecordData(GlobalDefinitionsEnum.TODAY_WEATHER_CONDITIONS.getRecordDefinition()); + RecordData today = new RecordData(LocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition()); today.setFieldByName("weather_report", 0); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast today.setFieldByName("timestamp", weather.timestamp); today.setFieldByName("observed_at_time", weather.timestamp); @@ -187,7 +187,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni for (int hour = 0; hour <= 11; hour++) { if (hour < weather.hourly.size()) { WeatherSpec.Hourly hourly = weather.hourly.get(hour); - RecordData weatherHourlyForecast = new RecordData(GlobalDefinitionsEnum.HOURLY_WEATHER_FORECAST.getRecordDefinition()); + RecordData weatherHourlyForecast = new RecordData(LocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition()); weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast weatherHourlyForecast.setFieldByName("timestamp", hourly.timestamp); weatherHourlyForecast.setFieldByName("temperature", hourly.temp); @@ -203,7 +203,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } } // - RecordData todayDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition()); + RecordData todayDailyForecast = new RecordData(LocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition()); todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast todayDailyForecast.setFieldByName("timestamp", weather.timestamp); todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp); @@ -218,7 +218,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni if (day < weather.forecasts.size()) { WeatherSpec.Daily daily = weather.forecasts.get(day); int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; //TODO: is this needed? - RecordData weatherDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition()); + RecordData weatherDailyForecast = new RecordData(LocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition()); weatherDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast weatherDailyForecast.setFieldByName("timestamp", weather.timestamp); weatherDailyForecast.setFieldByName("low_temperature", daily.minTemp); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java index aa31a34e4..ea1c410a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java @@ -3,17 +3,18 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; import java.nio.ByteBuffer; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; public class DevFieldDefinition { public final ByteBuffer valueHolder; - private final int localNumber; + private final int fieldDefinitionNumber; private final int size; private final int developerDataIndex; - private final String name; + private BaseType baseType; + private String name; - public DevFieldDefinition(int localNumber, int size, int developerDataIndex, String name) { - this.localNumber = localNumber; + public DevFieldDefinition(int fieldDefinitionNumber, int size, int developerDataIndex, String name) { + this.fieldDefinitionNumber = fieldDefinitionNumber; this.size = size; this.developerDataIndex = developerDataIndex; this.name = name; @@ -29,8 +30,20 @@ public class DevFieldDefinition { } - public int getLocalNumber() { - return localNumber; + public BaseType getBaseType() { + return baseType; + } + + public void setBaseType(BaseType baseType) { + this.baseType = baseType; + } + + public int getDeveloperDataIndex() { + return developerDataIndex; + } + + public int getFieldDefinitionNumber() { + return fieldDefinitionNumber; } public int getSize() { @@ -41,16 +54,7 @@ public class DevFieldDefinition { return name; } - public void generateOutgoingPayload(MessageWriter writer) { //TODO + public void setName(String name) { + this.name = name; } - - public Object decode() { //TODO - return null; - } - - - public void encode(Object o) { //TODO - } - - } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java index 25b9f9bcb..d86b203de 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java @@ -7,15 +7,15 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; public class FieldDefinition implements FieldInterface { - private final int localNumber; - private final int size; protected final BaseType baseType; - private final String name; protected final int scale; protected final int offset; + private final int number; + private final int size; + private final String name; - public FieldDefinition(int localNumber, int size, BaseType baseType, String name, int scale, int offset) { - this.localNumber = localNumber; + public FieldDefinition(int number, int size, BaseType baseType, String name, int scale, int offset) { + this.number = number; this.size = size; this.baseType = baseType; this.name = name; @@ -23,34 +23,25 @@ public class FieldDefinition implements FieldInterface { this.offset = offset; } - public FieldDefinition(int localNumber, int size, BaseType baseType, String name) { - this(localNumber, size, baseType, name, 1, 0); + public FieldDefinition(int number, int size, BaseType baseType, String name) { + this(number, size, baseType, name, 1, 0); } - public static FieldDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader) { - int localNumber = garminByteBufferReader.readByte(); + public static FieldDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader, GlobalFITMessage globalFITMessage) { + int number = garminByteBufferReader.readByte(); int size = garminByteBufferReader.readByte(); int baseTypeIdentifier = garminByteBufferReader.readByte(); BaseType baseType = BaseType.fromIdentifier(baseTypeIdentifier); - if (size % baseType.getSize() != 0) { - baseType = BaseType.BASE_TYPE_BYTE; + FieldDefinition global = globalFITMessage.getFieldDefinition(number, size); + if (null != global && global.getBaseType().equals(baseType)) { + return global; } - - return new FieldDefinition(localNumber, size, baseType, ""); - + return new FieldDefinition(number, size, baseType, ""); } - public int getScale() { - return scale; - } - - public int getOffset() { - return offset; - } - - public int getLocalNumber() { - return localNumber; + public int getNumber() { + return number; } public int getSize() { @@ -66,7 +57,7 @@ public class FieldDefinition implements FieldInterface { } public void generateOutgoingPayload(MessageWriter writer) { - writer.writeByte(localNumber); + writer.writeByte(number); writer.writeByte(size); writer.writeByte(baseType.getIdentifier()); } 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 new file mode 100644 index 000000000..220639c46 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java @@ -0,0 +1,63 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarm; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionDayOfWeek; +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.FieldDefinitionLanguage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTemperature; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherCondition; + +public class FieldDefinitionFactory { + + public static FieldDefinition create(int localNumber, int size, BaseType baseType, String name, int scale, int offset) { + return new FieldDefinition(localNumber, size, baseType, name, scale, offset); + } + + public static FieldDefinition create(int localNumber, int size, FIELD field, BaseType baseType, String name, int scale, int offset) { + if (null == field) { + return new FieldDefinition(localNumber, size, baseType, name, scale, offset); + } + switch (field) { + case ALARM: + return new FieldDefinitionAlarm(localNumber, size, baseType, name); + case DAY_OF_WEEK: + return new FieldDefinitionDayOfWeek(localNumber, size, baseType, name); + case FILE_TYPE: + return new FieldDefinitionFileType(localNumber, size, baseType, name); + case GOAL_SOURCE: + return new FieldDefinitionGoalSource(localNumber, size, baseType, name); + case GOAL_TYPE: + return new FieldDefinitionGoalType(localNumber, size, baseType, name); + case MEASUREMENT_SYSTEM: + return new FieldDefinitionMeasurementSystem(localNumber, size, baseType, name); + case TEMPERATURE: + return new FieldDefinitionTemperature(localNumber, size, baseType, name); + case TIMESTAMP: + return new FieldDefinitionTimestamp(localNumber, size, baseType, name); + case WEATHER_CONDITION: + return new FieldDefinitionWeatherCondition(localNumber, size, baseType, name); + case LANGUAGE: + return new FieldDefinitionLanguage(localNumber, size, baseType, name); + default: + return new FieldDefinition(localNumber, size, baseType, name); + } + } + + public enum FIELD { + ALARM, + DAY_OF_WEEK, + FILE_TYPE, + GOAL_SOURCE, + GOAL_TYPE, + MEASUREMENT_SYSTEM, + TEMPERATURE, + TIMESTAMP, + WEATHER_CONDITION, + LANGUAGE, + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java deleted file mode 100644 index 550a9ed0a..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java +++ /dev/null @@ -1,91 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; - -import androidx.annotation.Nullable; - -import java.nio.ByteOrder; -import java.util.Arrays; - -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionDayOfWeek; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTemperature; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherCondition; - -public enum GlobalDefinitionsEnum { - TODAY_WEATHER_CONDITIONS(MesgType.TODAY_WEATHER_CONDITIONS, new RecordDefinition( - new RecordHeader(true, false, MesgType.TODAY_WEATHER_CONDITIONS, null), - ByteOrder.BIG_ENDIAN, - MesgType.TODAY_WEATHER_CONDITIONS, - Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"), - new FieldDefinitionTimestamp(253, 4, BaseType.UINT32, "timestamp"), - new FieldDefinitionTimestamp(9, 4, BaseType.UINT32, "observed_at_time"), - new FieldDefinitionTemperature(1, 1, BaseType.SINT8, "temperature"), - new FieldDefinitionTemperature(14, 1, BaseType.SINT8, "low_temperature"), - new FieldDefinitionTemperature(13, 1, BaseType.SINT8, "high_temperature"), - new FieldDefinitionWeatherCondition(2, 1, BaseType.ENUM, "condition"), - new FieldDefinition(3, 2, BaseType.UINT16, "wind_direction"), - new FieldDefinition(5, 1, BaseType.UINT8, "precipitation_probability"), - new FieldDefinition(4, 2, BaseType.UINT16, "wind_speed", 298, 0), - new FieldDefinitionTemperature(6, 1, BaseType.SINT8, "temperature_feels_like"), - new FieldDefinition(7, 1, BaseType.UINT8, "relative_humidity"), - new FieldDefinition(10, 4, BaseType.SINT32, "observed_location_lat"), - new FieldDefinition(11, 4, BaseType.SINT32, "observed_location_long"), - new FieldDefinition(8, 15, BaseType.STRING, "location")))), - - HOURLY_WEATHER_FORECAST(MesgType.HOURLY_WEATHER_FORECAST, new RecordDefinition( - new RecordHeader(true, false, MesgType.HOURLY_WEATHER_FORECAST, null), - ByteOrder.BIG_ENDIAN, - MesgType.HOURLY_WEATHER_FORECAST, - Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"), - new FieldDefinitionTimestamp(253, 4, BaseType.UINT32, "timestamp"), - new FieldDefinitionTemperature(1, 1, BaseType.SINT8, "temperature"), - new FieldDefinitionWeatherCondition(2, 1, BaseType.ENUM, "condition"), - new FieldDefinition(3, 2, BaseType.UINT16, "wind_direction"), - new FieldDefinition(4, 2, BaseType.UINT16, "wind_speed", 298, 0), - new FieldDefinition(5, 1, BaseType.UINT8, "precipitation_probability"), - new FieldDefinition(7, 1, BaseType.UINT8, "relative_humidity"), - new FieldDefinition(15, 1, BaseType.SINT8, "dew_point"), - new FieldDefinition(16, 4, BaseType.FLOAT32, "uv_index"), - new FieldDefinition(17, 1, BaseType.ENUM, "air_quality")))), - - DAILY_WEATHER_FORECAST(MesgType.DAILY_WEATHER_FORECAST, new RecordDefinition( - new RecordHeader(true, false, MesgType.DAILY_WEATHER_FORECAST, null), - ByteOrder.BIG_ENDIAN, - MesgType.DAILY_WEATHER_FORECAST, - Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"), - new FieldDefinitionTimestamp(253, 4, BaseType.UINT32, "timestamp"), - new FieldDefinitionTemperature(14, 1, BaseType.SINT8, "low_temperature"), - new FieldDefinitionTemperature(13, 1, BaseType.SINT8, "high_temperature"), - new FieldDefinitionWeatherCondition(2, 1, BaseType.ENUM, "condition"), - new FieldDefinition(5, 1, BaseType.UINT8, "precipitation_probability"), - new FieldDefinitionDayOfWeek(12, 1, BaseType.ENUM, "day_of_week")))), - ; - - private final MesgType mesgType; - private final RecordDefinition recordDefinition; - - GlobalDefinitionsEnum(MesgType mesgType, RecordDefinition recordDefinition) { - this.mesgType = mesgType; - this.recordDefinition = recordDefinition; - } - - @Nullable - public static RecordDefinition getRecordDefinitionfromMesgType(final MesgType code) { - for (final GlobalDefinitionsEnum globalDefinitionsEnum : GlobalDefinitionsEnum.values()) { - if (globalDefinitionsEnum.getMesgType().getIdentifier() == code.getIdentifier()) { - return globalDefinitionsEnum.getRecordDefinition(); - } - } - return null; - } - - public MesgType getMesgType() { - return mesgType; - } - - public RecordDefinition getRecordDefinition() { - return recordDefinition; - } - - -} 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 new file mode 100644 index 000000000..e6b945b90 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java @@ -0,0 +1,256 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class GlobalFITMessage { + public static GlobalFITMessage FILE_ID = new GlobalFITMessage(0, "FILE_ID", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.ENUM, "type", FieldDefinitionFactory.FIELD.FILE_TYPE), + new FieldDefinitionPrimitive(1, BaseType.UINT16, "manufacturer"), + new FieldDefinitionPrimitive(2, BaseType.UINT16, "product"), + new FieldDefinitionPrimitive(3, BaseType.UINT32Z, "serial_number"), + new FieldDefinitionPrimitive(4, BaseType.UINT32, "time_created", FieldDefinitionFactory.FIELD.TIMESTAMP), + new FieldDefinitionPrimitive(5, BaseType.UINT16, "number"), + new FieldDefinitionPrimitive(6, BaseType.UINT16, "manufacturer_partner"), + new FieldDefinitionPrimitive(8, BaseType.STRING, 20, "product_name") + )); + public static GlobalFITMessage DEVICE_SETTINGS = new GlobalFITMessage(2, "DEVICE_SETTINGS", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT8, "active_time_zone"), + new FieldDefinitionPrimitive(1, BaseType.UINT32, "utc_offset"), + new FieldDefinitionPrimitive(2, BaseType.UINT32, "time_offset"), + new FieldDefinitionPrimitive(4, BaseType.ENUM, "time_mode"), + new FieldDefinitionPrimitive(5, BaseType.SINT8, "time_zone_offset"), + new FieldDefinitionPrimitive(12, BaseType.ENUM, "backlight_mode"), + new FieldDefinitionPrimitive(36, BaseType.ENUM, "activity_tracker_enabled"), + new FieldDefinitionPrimitive(46, BaseType.ENUM, "move_alert_enabled"), + new FieldDefinitionPrimitive(47, BaseType.ENUM, "date_mode"), + new FieldDefinitionPrimitive(55, BaseType.ENUM, "display_orientation"), + new FieldDefinitionPrimitive(56, BaseType.ENUM, "mounting_side"), + new FieldDefinitionPrimitive(57, BaseType.UINT16, "default_page"), + new FieldDefinitionPrimitive(58, BaseType.UINT16, "autosync_min_steps"), + new FieldDefinitionPrimitive(59, BaseType.UINT16, "autosync_min_time"), + new FieldDefinitionPrimitive(86, BaseType.ENUM, "ble_auto_upload_enabled"), + new FieldDefinitionPrimitive(90, BaseType.UINT32, "auto_activity_detect") + )); + public static GlobalFITMessage USER_PROFILE = new GlobalFITMessage(3, "USER_PROFILE", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.STRING, 8, "friendly_name"), + new FieldDefinitionPrimitive(1, BaseType.ENUM, "gender"), + new FieldDefinitionPrimitive(2, BaseType.UINT8, "age"), + new FieldDefinitionPrimitive(3, BaseType.UINT8, "height"), + new FieldDefinitionPrimitive(4, BaseType.UINT16, "weight", 10, 0), + new FieldDefinitionPrimitive(5, BaseType.ENUM, "language", FieldDefinitionFactory.FIELD.LANGUAGE), + new FieldDefinitionPrimitive(6, BaseType.ENUM, "elev_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM), + new FieldDefinitionPrimitive(7, BaseType.ENUM, "weight_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM), + new FieldDefinitionPrimitive(8, BaseType.UINT8, "resting_heart_rate"), + new FieldDefinitionPrimitive(10, BaseType.UINT8, "default_max_biking_heart_rate"), + new FieldDefinitionPrimitive(11, BaseType.UINT8, "default_max_heart_rate"), + new FieldDefinitionPrimitive(12, BaseType.ENUM, "hr_setting"), + new FieldDefinitionPrimitive(13, BaseType.ENUM, "speed_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM), + new FieldDefinitionPrimitive(14, BaseType.ENUM, "dist_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM), + new FieldDefinitionPrimitive(16, BaseType.ENUM, "power_setting"), + new FieldDefinitionPrimitive(17, BaseType.ENUM, "activity_class"), + new FieldDefinitionPrimitive(18, BaseType.ENUM, "position_setting"), + new FieldDefinitionPrimitive(21, BaseType.ENUM, "temperature_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM), + new FieldDefinitionPrimitive(28, BaseType.UINT32, "wake_time"), + new FieldDefinitionPrimitive(29, BaseType.UINT32, "sleep_time"), + new FieldDefinitionPrimitive(30, BaseType.ENUM, "height_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM), + new FieldDefinitionPrimitive(31, BaseType.UINT16, "user_running_step_length"), + new FieldDefinitionPrimitive(32, BaseType.UINT16, "user_walking_step_length") + )); + public static GlobalFITMessage ZONES_TARGET = new GlobalFITMessage(7, "ZONES_TARGET", Arrays.asList( + new FieldDefinitionPrimitive(3, BaseType.UINT16, "functional_threshold_power"), + 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 + )); + public static GlobalFITMessage SPORT = new GlobalFITMessage(12, "SPORT", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.ENUM, "sport"), + new FieldDefinitionPrimitive(1, BaseType.ENUM, "sub_sport"), + new FieldDefinitionPrimitive(3, BaseType.STRING, 24, "name") + )); + + public static GlobalFITMessage GOALS = new GlobalFITMessage(15, "GOALS", Arrays.asList( + new FieldDefinitionPrimitive(4, BaseType.ENUM, "type", FieldDefinitionFactory.FIELD.GOAL_TYPE), + new FieldDefinitionPrimitive(7, BaseType.UINT32, "target_value"), + new FieldDefinitionPrimitive(11, BaseType.ENUM, "source", FieldDefinitionFactory.FIELD.GOAL_SOURCE) + )); + + public static GlobalFITMessage RECORD = new GlobalFITMessage(20, "RECORD", Arrays.asList( + new FieldDefinitionPrimitive(3, BaseType.UINT8, "heart_rate"), + new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) + )); + public static GlobalFITMessage FILE_CREATOR = new GlobalFITMessage(49, "FILE_CREATOR", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT16, "software_version"), + new FieldDefinitionPrimitive(1, BaseType.UINT8, "hardware_version") + )); + public static GlobalFITMessage CONNECTIVITY = new GlobalFITMessage(127, "CONNECTIVITY", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.ENUM, "bluetooth_enabled"), + new FieldDefinitionPrimitive(3, BaseType.STRING, 20, "name"), + new FieldDefinitionPrimitive(4, BaseType.ENUM, "live_tracking_enabled"), + new FieldDefinitionPrimitive(5, BaseType.ENUM, "weather_conditions_enabled"), + new FieldDefinitionPrimitive(6, BaseType.ENUM, "weather_alerts_enabled"), + new FieldDefinitionPrimitive(7, BaseType.ENUM, "auto_activity_upload_enabled"), + new FieldDefinitionPrimitive(8, BaseType.ENUM, "course_download_enabled"), + new FieldDefinitionPrimitive(9, BaseType.ENUM, "workout_download_enabled"), + new FieldDefinitionPrimitive(10, BaseType.ENUM, "gps_ephemeris_download_enabled") + )); + + public static GlobalFITMessage WEATHER = new GlobalFITMessage(128, "WEATHER", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.ENUM, "weather_report"), + new FieldDefinitionPrimitive(1, BaseType.SINT8, "temperature", FieldDefinitionFactory.FIELD.TEMPERATURE), + new FieldDefinitionPrimitive(2, BaseType.ENUM, "condition", FieldDefinitionFactory.FIELD.WEATHER_CONDITION), + new FieldDefinitionPrimitive(3, BaseType.UINT16, "wind_direction"), + new FieldDefinitionPrimitive(4, BaseType.UINT16, "wind_speed", 298, 0), + new FieldDefinitionPrimitive(5, BaseType.UINT8, "precipitation_probability"), + new FieldDefinitionPrimitive(6, BaseType.SINT8, "temperature_feels_like", FieldDefinitionFactory.FIELD.TEMPERATURE), + new FieldDefinitionPrimitive(7, BaseType.UINT8, "relative_humidity"), + new FieldDefinitionPrimitive(8, BaseType.STRING, 15, "location"), + new FieldDefinitionPrimitive(9, BaseType.UINT32, "observed_at_time", FieldDefinitionFactory.FIELD.TIMESTAMP), + new FieldDefinitionPrimitive(10, BaseType.SINT32, "observed_location_lat"), + new FieldDefinitionPrimitive(11, BaseType.SINT32, "observed_location_long"), + new FieldDefinitionPrimitive(12, BaseType.ENUM, "day_of_week", FieldDefinitionFactory.FIELD.DAY_OF_WEEK), + new FieldDefinitionPrimitive(13, BaseType.SINT8, "high_temperature", FieldDefinitionFactory.FIELD.TEMPERATURE), + new FieldDefinitionPrimitive(14, BaseType.SINT8, "low_temperature", FieldDefinitionFactory.FIELD.TEMPERATURE), + new FieldDefinitionPrimitive(15, BaseType.SINT8, "dew_point"), + new FieldDefinitionPrimitive(16, BaseType.FLOAT32, "uv_index"), + new FieldDefinitionPrimitive(17, BaseType.ENUM, "air_quality"), + new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP) + )); + public static GlobalFITMessage WATCHFACE_SETTINGS = new GlobalFITMessage(159, "WATCHFACE_SETTINGS", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.ENUM, "mode"), //1=analog + new FieldDefinitionPrimitive(1, BaseType.BASE_TYPE_BYTE, "layout") + )); + + public static GlobalFITMessage FIELD_DESCRIPTION = new GlobalFITMessage(206, "FIELD_DESCRIPTION", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT8, "developer_data_index"), + new FieldDefinitionPrimitive(1, BaseType.UINT8, "field_definition_number"), + new FieldDefinitionPrimitive(2, BaseType.UINT8, "fit_base_type_id"), + new FieldDefinitionPrimitive(3, BaseType.STRING, 64, "field_name"), + new FieldDefinitionPrimitive(8, BaseType.STRING, 16, "units") + )); + public static GlobalFITMessage DEVELOPER_DATA = new GlobalFITMessage(207, "DEVELOPER_DATA", Arrays.asList( + new FieldDefinitionPrimitive(1, BaseType.BASE_TYPE_BYTE, 16, "application_id"), + new FieldDefinitionPrimitive(3, BaseType.UINT8, "developer_data_index") + )); + // UNK_216(216, null), //activity + public static GlobalFITMessage ALARM_SETTINGS = new GlobalFITMessage(222, "ALARM_SETTINGS", Arrays.asList( + new FieldDefinitionPrimitive(0, BaseType.UINT16, "time", FieldDefinitionFactory.FIELD.ALARM) + )); + + public static Map KNOWNMESSAGES = new HashMap() {{ + put(0, FILE_ID); + put(2, DEVICE_SETTINGS); + put(3, USER_PROFILE); + put(7, ZONES_TARGET); + put(12, SPORT); + put(15, GOALS); + put(20, RECORD); + put(49, FILE_CREATOR); + put(127, CONNECTIVITY); + put(128, WEATHER); + put(159, WATCHFACE_SETTINGS); + put(206, FIELD_DESCRIPTION); + put(207, DEVELOPER_DATA); + put(222, ALARM_SETTINGS); + }}; + private final int number; + private final String name; + + private final List fieldDefinitionPrimitives; + + GlobalFITMessage(int number, String name, List fieldDefinitionPrimitives) { + this.number = number; + this.name = name; + this.fieldDefinitionPrimitives = fieldDefinitionPrimitives; + } + + public static GlobalFITMessage fromNumber(final int number) { + final GlobalFITMessage found = KNOWNMESSAGES.get(number); + if (found != null) { + return found; + } + return new GlobalFITMessage(number, "UNK_" + number, null); + } + + public String name() { + return this.name; + } + + public int getNumber() { + return number; + } + + @Nullable + public List getFieldDefinitions(int... ids) { + if (null == fieldDefinitionPrimitives) + return null; + List subset = new ArrayList<>(ids.length); + for (int id : + ids) { + for (FieldDefinitionPrimitive fieldDefinitionPrimitive : + fieldDefinitionPrimitives) { + if (fieldDefinitionPrimitive.number == id) { + subset.add(FieldDefinitionFactory.create(fieldDefinitionPrimitive.number, fieldDefinitionPrimitive.size, fieldDefinitionPrimitive.type, fieldDefinitionPrimitive.baseType, fieldDefinitionPrimitive.name, fieldDefinitionPrimitive.scale, fieldDefinitionPrimitive.offset)); + } + } + } + return subset; + } + + @Nullable + public FieldDefinition getFieldDefinition(int id, int size) { + if (null == fieldDefinitionPrimitives) + return null; + for (GlobalFITMessage.FieldDefinitionPrimitive fieldDefinitionPrimitive : + fieldDefinitionPrimitives) { + if (fieldDefinitionPrimitive.number == id) { + return FieldDefinitionFactory.create(fieldDefinitionPrimitive.number, size, fieldDefinitionPrimitive.type, fieldDefinitionPrimitive.baseType, fieldDefinitionPrimitive.name, fieldDefinitionPrimitive.scale, fieldDefinitionPrimitive.offset); + } + } + return null; + } + + static class FieldDefinitionPrimitive { + private final int number; + private final BaseType baseType; + private final String name; + private final FieldDefinitionFactory.FIELD type; + private final int scale; + private final int offset; + private final int size; + + public FieldDefinitionPrimitive(int number, BaseType baseType, int size, String name, FieldDefinitionFactory.FIELD type, int scale, int offset) { + this.number = number; + this.baseType = baseType; + this.size = size; + this.name = name; + this.type = type; + this.scale = scale; + this.offset = offset; + } + + public FieldDefinitionPrimitive(int number, BaseType baseType, String name, FieldDefinitionFactory.FIELD type) { + this(number, baseType, baseType.getSize(), name, type, 1, 0); + } + + public FieldDefinitionPrimitive(int number, BaseType baseType, String name) { + this(number, baseType, baseType.getSize(), name, null, 1, 0); + } + + public FieldDefinitionPrimitive(int number, BaseType baseType, int size, String name) { + this(number, baseType, size, name, null, 1, 0); + } + + public FieldDefinitionPrimitive(int number, BaseType baseType, String name, int scale, int offset) { + this(number, baseType, baseType.getSize(), name, null, scale, offset); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/LocalMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/LocalMessage.java new file mode 100644 index 000000000..7948a563f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/LocalMessage.java @@ -0,0 +1,54 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import androidx.annotation.Nullable; + +import java.nio.ByteOrder; +import java.util.List; + +public enum LocalMessage { + TODAY_WEATHER_CONDITIONS(6, GlobalFITMessage.WEATHER, + new int[]{0, 253, 9, 1, 14, 13, 2, 3, 5, 4, 6, 7, 10, 11, 8} + ), + HOURLY_WEATHER_FORECAST(9, GlobalFITMessage.WEATHER, + new int[]{0, 253, 1, 2, 3, 4, 5, 7, 15, 16, 17} + ), + DAILY_WEATHER_FORECAST(10, GlobalFITMessage.WEATHER, + new int[]{0, 253, 14, 13, 2, 5, 12} + ); + + private final int type; + private final GlobalFITMessage globalFITMessage; + private final int[] globalDefinitionIds; + + LocalMessage(int type, GlobalFITMessage globalFITMessage, int[] globalDefinitionIds) { + this.type = type; + this.globalFITMessage = globalFITMessage; + this.globalDefinitionIds = globalDefinitionIds; + } + + @Nullable + public static LocalMessage fromType(int type) { + for (final LocalMessage localMessage : LocalMessage.values()) { + if (localMessage.getType() == type) { + return localMessage; + } + } + return null; + } + + public List getLocalFieldDefinitions() { + return globalFITMessage.getFieldDefinitions(globalDefinitionIds); + } + + public RecordDefinition getRecordDefinition() { + return new RecordDefinition(ByteOrder.BIG_ENDIAN, this); + } + + public int getType() { + return type; + } + + public GlobalFITMessage getGlobalFITMessage() { + return globalFITMessage; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java deleted file mode 100644 index ae894f71e..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java +++ /dev/null @@ -1,32 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; - -public enum MesgType { - TODAY_WEATHER_CONDITIONS(6, 128), - HOURLY_WEATHER_FORECAST(9, 128), - DAILY_WEATHER_FORECAST(10, 128); - - private final int identifier; - private final int globalMesgNum; - - MesgType(int id, int globalMesgNum) { - this.identifier = id; - this.globalMesgNum = globalMesgNum; - } - - public static MesgType fromIdentifier(int identifier) { - for (final MesgType mesgType : MesgType.values()) { - if (mesgType.getIdentifier() == identifier) { - return mesgType; - } - } - throw new IllegalArgumentException("Unknown type " + identifier); //TODO: perhaps we need to handle unknown message types - } - - public int getIdentifier() { - return identifier; - } - - public int getGlobalMesgNum() { - return globalMesgNum; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java index 013d05e53..25bc87ab2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java @@ -1,5 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; +import androidx.annotation.NonNull; + import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -15,22 +17,34 @@ import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.ba public class RecordData { private final RecordHeader recordHeader; + private final GlobalFITMessage globalFITMessage; protected ByteBuffer valueHolder; - private List fieldDataList; + private final List fieldDataList; public RecordData(RecordDefinition recordDefinition) { + if (null == recordDefinition.getFieldDefinitions()) + throw new IllegalArgumentException("Cannot create record data without FieldDefinitions " + recordDefinition); + fieldDataList = new ArrayList<>(); this.recordHeader = recordDefinition.getRecordHeader(); + this.globalFITMessage = recordDefinition.getGlobalFITMessage(); int totalSize = 0; - for (FieldDefinition fieldDef : recordDefinition.getFieldDefinitions()) { fieldDataList.add(new FieldData(fieldDef, totalSize)); totalSize += fieldDef.getSize(); + } + if (recordHeader.isDefinition() && recordDefinition.getDevFieldDefinitions() != null) { + for (DevFieldDefinition fieldDef : + recordDefinition.getDevFieldDefinitions()) { + FieldDefinition temp = new FieldDefinition(fieldDef.getFieldDefinitionNumber(), fieldDef.getSize(), fieldDef.getBaseType(), fieldDef.getName()); + fieldDataList.add(new FieldData(temp, totalSize)); + totalSize += fieldDef.getSize(); + } } this.valueHolder = ByteBuffer.allocate(totalSize); @@ -43,6 +57,10 @@ public class RecordData { } + public GlobalFITMessage getGlobalFITMessage() { + return globalFITMessage; + } + public void parseDataMessage(GarminByteBufferReader garminByteBufferReader) { garminByteBufferReader.setByteOrder(valueHolder.order()); for (FieldData fieldData : fieldDataList) { @@ -115,6 +133,7 @@ public class RecordData { return arr; } + @NonNull public String toString() { StringBuilder oBuilder = new StringBuilder(); for (FieldData fieldData : @@ -124,34 +143,48 @@ public class RecordData { } else { oBuilder.append("unknown_" + fieldData.getNumber()); } + oBuilder.append(fieldData); oBuilder.append(": "); - oBuilder.append(fieldData.decode()); + Object o = fieldData.decode(); + if (o instanceof Object[]) { + oBuilder.append("["); + oBuilder.append(org.apache.commons.lang3.StringUtils.join((Object[]) o, ",")); + oBuilder.append("]"); + } else { + oBuilder.append(o); + } oBuilder.append(" "); } return oBuilder.toString(); } + + public LocalMessage getLocalMessage() { + return recordHeader.getLocalMessage(); + } + private class FieldData { - private FieldDefinition fieldDefinition; + private final FieldDefinition fieldDefinition; private final int position; private final int size; + private final int baseSize; public FieldData(FieldDefinition fieldDefinition, int position) { this.fieldDefinition = fieldDefinition; this.position = position; this.size = fieldDefinition.getSize(); + this.baseSize = fieldDefinition.getBaseType().getSize(); } - - public String getName() { + private String getName() { return fieldDefinition.getName(); } - public int getNumber() { - return fieldDefinition.getLocalNumber(); + private int getNumber() { + return fieldDefinition.getNumber(); } - public void invalidate() { + private void invalidate() { goToPosition(); if (STRING.equals(fieldDefinition.getBaseType())) { for (int i = 0; i < size; i++) { @@ -159,7 +192,9 @@ public class RecordData { } return; } - fieldDefinition.invalidate(valueHolder); + for (int i = 0; i < (size / baseSize); i++) { + fieldDefinition.invalidate(valueHolder); + } } private void goToPosition() { @@ -171,21 +206,28 @@ public class RecordData { valueHolder.put(garminByteBufferReader.readBytes(size)); } - public void encode(Object... objects) { - if (objects.length > 1) - throw new IllegalArgumentException("Array of values not supported yet"); //TODO: handle arrays - Object o = objects[0]; - goToPosition(); - if (STRING.equals(fieldDefinition.getBaseType())) { - final byte[] bytes = ((String) o).getBytes(StandardCharsets.UTF_8); - valueHolder.put(Arrays.copyOf(bytes, Math.min(this.size - 1, bytes.length))); - valueHolder.put((byte) 0); - return; + private void encode(Object... objects) { + if (objects[0] instanceof boolean[] || objects[0] instanceof short[] || objects[0] instanceof int[] || objects[0] instanceof long[] || objects[0] instanceof float[] || objects[0] instanceof double[]) { + throw new IllegalArgumentException("Array of primitive types not supported, box them to objects"); + } + goToPosition(); + final int slots = size / baseSize; + int i = 0; + for (Object o : objects) { + if (i++ >= slots) { + throw new IllegalArgumentException("Number of elements in array was too big for the field"); + } + if (STRING.equals(fieldDefinition.getBaseType())) { + final byte[] bytes = ((String) o).getBytes(StandardCharsets.UTF_8); + valueHolder.put(Arrays.copyOf(bytes, Math.min(this.size - 1, bytes.length))); + valueHolder.put((byte) 0); + return; + } + fieldDefinition.encode(valueHolder, o); } - fieldDefinition.encode(valueHolder, o); } - public Object decode() { + private Object decode() { goToPosition(); if (STRING.equals(fieldDefinition.getBaseType())) { final byte[] bytes = new byte[size]; @@ -196,9 +238,18 @@ public class RecordData { } return new String(bytes, 0, zero, StandardCharsets.UTF_8); } - //TODO: handle arrays + if (size > baseSize) { + Object[] arr = new Object[size / baseSize]; + for (int i = 0; i < arr.length; i++) { + arr[i] = fieldDefinition.decode(valueHolder); + } + return arr; + } return fieldDefinition.decode(valueHolder); + } + public String toString() { + return "(" + fieldDefinition.getBaseType().name() + "/" + size + ")"; } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java index aabbcb6a1..b0d9bff0d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java @@ -1,36 +1,43 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; public class RecordDefinition { private final RecordHeader recordHeader; - private final int globalMesgNum; + private final GlobalFITMessage globalFITMessage; + private final LocalMessage localMessage; private final java.nio.ByteOrder byteOrder; - private final MesgType mesgType; private List fieldDefinitions; private List devFieldDefinitions; - - public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, int globalMesgNum, List fieldDefinitions, List devFieldDefinitions) { + public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, LocalMessage localMessage, GlobalFITMessage globalFITMessage, List fieldDefinitions, List devFieldDefinitions) { this.recordHeader = recordHeader; this.byteOrder = byteOrder; - this.mesgType = mesgType; - this.globalMesgNum = globalMesgNum; + this.localMessage = localMessage; + this.globalFITMessage = globalFITMessage; this.fieldDefinitions = fieldDefinitions; this.devFieldDefinitions = devFieldDefinitions; } - public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, List fieldDefinitions) { - this(recordHeader, byteOrder, mesgType, mesgType.getGlobalMesgNum(), fieldDefinitions, null); + public RecordDefinition(ByteOrder byteOrder, LocalMessage localMessage, List fieldDefinitions) { + this(new RecordHeader(true, false, localMessage, null), byteOrder, localMessage, localMessage.getGlobalFITMessage(), fieldDefinitions, null); } - public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, int globalMesgNum) { - this(recordHeader, byteOrder, mesgType, globalMesgNum, null, null); + public RecordDefinition(ByteOrder byteOrder, LocalMessage localMessage) { + this(new RecordHeader(true, false, localMessage, null), byteOrder, localMessage, localMessage.getGlobalFITMessage(), localMessage.getLocalFieldDefinitions(), null); + } + + public RecordDefinition(ByteOrder byteOrder, RecordHeader recordHeader, GlobalFITMessage globalFITMessage, List fieldDefinitions) { + this(recordHeader, byteOrder, null, globalFITMessage, fieldDefinitions, null); } public static RecordDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader, RecordHeader recordHeader) { @@ -40,13 +47,15 @@ public class RecordDefinition { ByteOrder byteOrder = garminByteBufferReader.readByte() == 0x01 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; garminByteBufferReader.setByteOrder(byteOrder); final int globalMesgNum = garminByteBufferReader.readShort(); + final GlobalFITMessage globalFITMessage = GlobalFITMessage.fromNumber(globalMesgNum); - RecordDefinition definitionMessage = new RecordDefinition(recordHeader, byteOrder, recordHeader.getMesgType(), globalMesgNum); + RecordDefinition definitionMessage = new RecordDefinition(byteOrder, recordHeader, globalFITMessage, null); final int numFields = garminByteBufferReader.readByte(); List fieldDefinitions = new ArrayList<>(numFields); + for (int i = 0; i < numFields; i++) { - fieldDefinitions.add(FieldDefinition.parseIncoming(garminByteBufferReader)); + fieldDefinitions.add(FieldDefinition.parseIncoming(garminByteBufferReader, globalFITMessage)); } definitionMessage.setFieldDefinitions(fieldDefinitions); @@ -63,6 +72,11 @@ public class RecordDefinition { return definitionMessage; } + public GlobalFITMessage getGlobalFITMessage() { + return globalFITMessage; + } + + public ByteOrder getByteOrder() { return byteOrder; } @@ -79,6 +93,7 @@ public class RecordDefinition { return recordHeader; } + @Nullable public List getFieldDefinitions() { return fieldDefinitions; } @@ -92,15 +107,43 @@ public class RecordDefinition { writer.writeByte(0);//ignore writer.writeByte(byteOrder == ByteOrder.LITTLE_ENDIAN ? 0 : 1); writer.setByteOrder(byteOrder); - writer.writeShort(globalMesgNum); - writer.writeByte(fieldDefinitions.size()); - for (FieldDefinition fieldDefinition : fieldDefinitions) { - fieldDefinition.generateOutgoingPayload(writer); + writer.writeShort(globalFITMessage.getNumber()); + + if (fieldDefinitions != null) { + writer.writeByte(fieldDefinitions.size()); + for (FieldDefinition fieldDefinition : fieldDefinitions) { + fieldDefinition.generateOutgoingPayload(writer); + } } } public String getName() { - return mesgType != null ? mesgType.name() : "unknown_" + globalMesgNum; + return localMessage != null ? localMessage.name() : "unknown_" + globalFITMessage; } + @NonNull + public String toString() { + return recordHeader.toString() + + " Global Message Number: " + globalFITMessage.name(); + } + + public void populateDevFields(List developerFieldData) { + for (DevFieldDefinition devFieldDef : + getDevFieldDefinitions()) { + for (RecordData recordData : + developerFieldData) { + try { + if (devFieldDef.getFieldDefinitionNumber() == (int) recordData.getFieldByName("field_definition_number") && + devFieldDef.getDeveloperDataIndex() == (int) recordData.getFieldByName("developer_data_index")) { + BaseType baseType = BaseType.fromIdentifier((int) recordData.getFieldByName("fit_base_type_id")); + devFieldDef.setBaseType(baseType); + devFieldDef.setName((String) recordData.getFieldByName("field_name")); + } + } catch (Exception e) { + //ignore + } + } + } + + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java index 5c0184b00..b0c338270 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java @@ -1,15 +1,21 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; +import androidx.annotation.Nullable; + +import java.util.Objects; + public class RecordHeader { private final boolean definition; private final boolean developerData; - private final MesgType mesgType; + private final LocalMessage localMessage; + private final int rawLocalMessageType; private final Integer timeOffset; - public RecordHeader(boolean definition, boolean developerData, MesgType mesgType, Integer timeOffset) { + public RecordHeader(boolean definition, boolean developerData, LocalMessage localMessage, Integer timeOffset) { this.definition = definition; this.developerData = developerData; - this.mesgType = mesgType; + this.localMessage = localMessage; + this.rawLocalMessageType = localMessage.getType(); this.timeOffset = timeOffset; } @@ -18,14 +24,15 @@ public class RecordHeader { if ((header & 0x80) == 0x80) { //compressed timestamp TODO add support definition = false; developerData = false; - mesgType = MesgType.fromIdentifier((header >> 5) & 0x3); + rawLocalMessageType = (header >> 5) & 0x3; timeOffset = header & 0x1f; } else { definition = ((header & 0x40) == 0x40); developerData = ((header & 0x20) == 0x20); - mesgType = MesgType.fromIdentifier(header & 0xf); + rawLocalMessageType = header & 0xf; timeOffset = null; } + localMessage = LocalMessage.fromType(rawLocalMessageType); } public boolean isDeveloperData() { @@ -36,14 +43,15 @@ public class RecordHeader { return definition; } - public MesgType getMesgType() { - return mesgType; + @Nullable + public LocalMessage getLocalMessage() { + return localMessage; } public byte generateOutgoingDefinitionPayload() { if (!definition && !developerData) - return (byte) (timeOffset | (((byte) mesgType.getIdentifier()) << 5)); - byte base = (byte) mesgType.getIdentifier(); + return (byte) (timeOffset | (((byte) localMessage.getType()) << 5)); + byte base = (byte) (null == localMessage ? rawLocalMessageType : localMessage.getType()); if (definition) base = (byte) (base | 0x40); if (developerData) @@ -54,11 +62,37 @@ public class RecordHeader { public byte generateOutgoingDataPayload() { //TODO: unclear if correct if (!definition && !developerData) - return (byte) (timeOffset | (((byte) mesgType.getIdentifier()) << 5)); - byte base = (byte) mesgType.getIdentifier(); + return (byte) (timeOffset | (((byte) localMessage.getType()) << 5)); + byte base = (byte) (null == localMessage ? rawLocalMessageType : localMessage.getType()); if (developerData) base = (byte) (base | 0x20); return base; } + + public String toString() { + return "Local Message: " + (null == localMessage ? "raw: " + rawLocalMessageType : "type: " + localMessage.name()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RecordHeader that = (RecordHeader) o; + + if (definition != that.definition) return false; + if (rawLocalMessageType != that.rawLocalMessageType) return false; + if (localMessage != that.localMessage) return false; + return Objects.equals(timeOffset, that.timeOffset); + } + + @Override + public int hashCode() { + int result = (definition ? 1 : 0); + result = 31 * result + (localMessage != null ? localMessage.hashCode() : 0); + result = 31 * result + rawLocalMessageType; + result = 31 * result + (timeOffset != null ? timeOffset.hashCode() : 0); + return result; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java index a0615dab3..b463062f2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java @@ -32,7 +32,7 @@ public class BaseTypeInt implements BaseTypeInterface { return null; if (i == invalid) return null; - return (int) ((i + offset) / scale); + return ((i + offset) / scale); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarm.java new file mode 100644 index 000000000..34f13723a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarm.java @@ -0,0 +1,31 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import java.nio.ByteBuffer; +import java.util.Calendar; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionAlarm extends FieldDefinition { + public FieldDefinitionAlarm(int localNumber, int size, BaseType baseType, String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(ByteBuffer byteBuffer) { + int raw = (int) baseType.decode(byteBuffer, scale, offset); + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, Math.round(raw / 60)); + calendar.set(Calendar.MINUTE, raw % 60); + return calendar; + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o) { + if (o instanceof Calendar) { + baseType.encode(byteBuffer, ((Calendar) o).get(Calendar.HOUR_OF_DAY) * 60 + ((Calendar) o).get(Calendar.MINUTE), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java new file mode 100644 index 000000000..d443647b7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionFileType.java @@ -0,0 +1,62 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionFileType extends FieldDefinition { + + public FieldDefinitionFileType(int localNumber, int size, BaseType baseType, String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(ByteBuffer byteBuffer) { + int raw = (int) baseType.decode(byteBuffer, scale, offset); + return Type.fromId(raw) == null ? raw : Type.fromId(raw); + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o) { + if (o instanceof Type) { + baseType.encode(byteBuffer, (((Type) o).getId()), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } + + public enum Type { + settings(2), + activity(4), //FIT_TYPE_4 stands for activity directory + goals(11), + monitor(32), //FIT_TYPE_32 + changelog(41), // FIT_TYPE_41 stands for changelog directory + metrics(44), //FIT_TYPE_41 + sleep(49), //FIT_TYPE_49 + ; + + private final int id; + + Type(int i) { + this.id = i; + } + + @Nullable + public static Type fromId(int id) { + for (Type type : + Type.values()) { + if (id == type.getId()) { + return type; + } + } + return null; + } + + public int getId() { + return this.id; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalSource.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalSource.java new file mode 100644 index 000000000..4ab317f5f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalSource.java @@ -0,0 +1,48 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionGoalSource extends FieldDefinition { + + public FieldDefinitionGoalSource(int localNumber, int size, BaseType baseType, String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(ByteBuffer byteBuffer) { + int raw = (int) baseType.decode(byteBuffer, scale, offset); + return Source.fromId(raw); + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o) { + if (o instanceof Source) { + baseType.encode(byteBuffer, (((Source) o).ordinal()), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } + + public enum Source { + auto, + community, + manual, + ; + + @Nullable + public static Source fromId(int id) { + for (Source source : + Source.values()) { + if (id == source.ordinal()) { + return source; + } + } + return null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalType.java new file mode 100644 index 000000000..a08f2dc77 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionGoalType.java @@ -0,0 +1,56 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionGoalType extends FieldDefinition { + + public FieldDefinitionGoalType(int localNumber, int size, BaseType baseType, String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(ByteBuffer byteBuffer) { + int raw = (int) baseType.decode(byteBuffer, scale, offset); + return Type.fromId(raw); + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o) { + if (o instanceof Type) { + baseType.encode(byteBuffer, (((Type) o).getId()), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } + + public enum Type { + steps(4), + ; + + private final int id; + + Type(int i) { + id = i; + } + + @Nullable + public static Type fromId(int id) { + for (Type type : + Type.values()) { + if (id == type.getId()) { + return type; + } + } + return null; + } + + public int getId() { + return id; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionLanguage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionLanguage.java new file mode 100644 index 000000000..fdf6a0bca --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionLanguage.java @@ -0,0 +1,57 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionLanguage extends FieldDefinition { + + public FieldDefinitionLanguage(int localNumber, int size, BaseType baseType, String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(ByteBuffer byteBuffer) { + int raw = (int) baseType.decode(byteBuffer, scale, offset); + return Language.fromId(raw); + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o) { + if (o instanceof Language) { + baseType.encode(byteBuffer, (((Language) o).getId()), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } + + private enum Language { + english(0), + italian(2), + ; + + private final int id; + + Language(int i) { + id = i; + } + + @Nullable + public static Language fromId(int id) { + for (Language language : + Language.values()) { + if (id == language.getId()) { + return language; + } + } + return null; + } + + public int getId() { + return id; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionMeasurementSystem.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionMeasurementSystem.java new file mode 100644 index 000000000..83348f090 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionMeasurementSystem.java @@ -0,0 +1,44 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionMeasurementSystem extends FieldDefinition { + + + public FieldDefinitionMeasurementSystem(int localNumber, int size, BaseType baseType, String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(ByteBuffer byteBuffer) { + int raw = (int) baseType.decode(byteBuffer, scale, offset); + return Type.fromId(raw) == null ? raw : Type.fromId(raw); + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o) { + if (o instanceof Type) { + baseType.encode(byteBuffer, (((Type) o).ordinal()), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } + + public enum Type { + metric, + ; + + public static Type fromId(int id) { + for (Type type : + Type.values()) { + if (type.ordinal() == id) { + return type; + } + } + return null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java index 56844792e..9397e0bd0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java @@ -3,7 +3,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; import java.util.ArrayList; import java.util.List; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalDefinitionsEnum; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; @@ -28,7 +27,7 @@ public class FitDataMessage extends GFDIMessage { RecordHeader recordHeader = new RecordHeader((byte) reader.readByte()); if (recordHeader.isDefinition()) return null; - RecordData recordData = new RecordData(GlobalDefinitionsEnum.getRecordDefinitionfromMesgType(recordHeader.getMesgType())); + RecordData recordData = new RecordData(recordHeader.getLocalMessage().getRecordDefinition()); recordData.parseDataMessage(reader); recordDataList.add(recordData); } diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java index a65f316bb..319f934fb 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java @@ -8,15 +8,20 @@ import org.threeten.bp.ZoneId; import java.math.BigInteger; import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.TimeZone; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.CobsCoDec; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.MesgType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.LocalMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ChecksumCalculator; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -113,7 +118,7 @@ public class GarminSupportTest { @Test public void testBaseFields() { - RecordDefinition recordDefinition = new RecordDefinition(new RecordHeader((byte) 6), ByteOrder.LITTLE_ENDIAN, MesgType.TODAY_WEATHER_CONDITIONS, 123); //just some random data + RecordDefinition recordDefinition = new RecordDefinition(ByteOrder.LITTLE_ENDIAN, new RecordHeader((byte) 6), GlobalFITMessage.WEATHER, null); //just some random data List fieldDefinitionList = new ArrayList<>(); for (BaseType baseType : BaseType.values()) { @@ -189,28 +194,28 @@ public class GarminSupportTest { Assert.assertNull(endVal); break; case "SINT32": - startVal = (int) Integer.MIN_VALUE; + startVal = (long) Integer.MIN_VALUE; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); Assert.assertEquals(startVal, endVal); - startVal = (int) Integer.MAX_VALUE - 1; + startVal = (long) Integer.MAX_VALUE - 1; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); Assert.assertEquals(startVal, endVal); - startVal = (int) Integer.MIN_VALUE - 1; + startVal = (long) Integer.MIN_VALUE - 1; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); Assert.assertNull(endVal); break; case "UINT32": - startVal = 0; + startVal = 0L; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); Assert.assertEquals(startVal, endVal); startVal = (long) 0xffffffffL - 1; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); - Assert.assertEquals(startVal, (long) ((int) endVal & 0xffffffffL)); + Assert.assertEquals(startVal, (long) ((long) endVal & 0xffffffffL)); startVal = 0xffffffff; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); @@ -289,14 +294,14 @@ public class GarminSupportTest { Assert.assertNull(endVal); break; case "UINT32Z": - startVal = 1; + startVal = 1L; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); Assert.assertEquals(startVal, endVal); startVal = (long) 0xffffffffL; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); - Assert.assertEquals(startVal, (long) ((int) endVal & 0xffffffffL)); + Assert.assertEquals(startVal, (long) ((long) endVal & 0xffffffffL)); startVal = -1; test.setFieldByName(baseType.name(), startVal); endVal = test.getFieldByName(baseType.name()); @@ -360,8 +365,168 @@ public class GarminSupportTest { } + private String fitFileParser(byte[] fileContents) { + StringBuilder oBuilder = new StringBuilder(); + + GarminByteBufferReader garminByteBufferReader = new GarminByteBufferReader(fileContents); + garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN); + //parseHeader + int headerSize = garminByteBufferReader.readByte(); //1 + int protocolVersion = garminByteBufferReader.readByte(); //2 + int profileVersion = garminByteBufferReader.readShort(); //4 + int dataSize = garminByteBufferReader.readInt(); //8 + int magic = garminByteBufferReader.readInt(); //12 + Assert.assertEquals(0x5449462E, magic); + int headerCrc = garminByteBufferReader.readShort(); + Assert.assertEquals(ChecksumCalculator.computeCrc(fileContents, 0, headerSize - 2), headerCrc); + //end of parse header + + Map recordDefinitionMap = new HashMap<>(); //questo va bene qui (ultimo vince) + Map recordDataMap = new HashMap<>(); + List developerFieldDescriptionData = new ArrayList<>(); + + while (garminByteBufferReader.getPosition() < fileContents.length - 2) { + byte rawRecordHeader = (byte) garminByteBufferReader.readByte(); + RecordHeader recordHeader = new RecordHeader(rawRecordHeader); + if (recordHeader.isDefinition()) { + final RecordDefinition recordDefinition = RecordDefinition.parseIncoming(garminByteBufferReader, recordHeader); + if (recordDefinition != null) { + if (recordHeader.isDeveloperData()) { + recordDefinition.populateDevFields(developerFieldDescriptionData); + } + oBuilder.append(recordDefinition.toString()); + recordDefinitionMap.put(recordHeader, recordDefinition); + recordDataMap.put(new RecordHeader(recordHeader.generateOutgoingDataPayload()), new RecordData(recordDefinition)); + } + } else { + final RecordData recordData = recordDataMap.get(recordHeader); + if (recordData != null) { + recordData.parseDataMessage(garminByteBufferReader); + if (GlobalFITMessage.FIELD_DESCRIPTION.equals(recordData.getGlobalFITMessage())) { + developerFieldDescriptionData.add(recordData); + } + oBuilder.append(recordData.toString()); + } + } + oBuilder.append(System.lineSeparator()); + } + garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN); + int fileCrc = garminByteBufferReader.readShort(); + Assert.assertEquals(ChecksumCalculator.computeCrc(fileContents, headerSize, fileContents.length - headerSize - 2), fileCrc); + + return oBuilder.toString(); + } @Test - public void runningTest() { - System.out.println(Instant.ofEpochSecond(System.currentTimeMillis()).atZone(ZoneId.systemDefault()).getDayOfWeek().getValue()); + public void TestFitFileSettings2() { + + byte[] fileContents = GB.hexStringToByteArray("0e101405b90600002e464954b18b40000000000603048c04048601028402" + + "028405028400010000ed2adce7ffffffff01001906ffff02410000310002" + + "000284010102015401ff4200000200160104860204860001020301000401" + + "000501010a01000b01000c01000d01020e01020f01021001001101001201" + + "001501001601001a01001b01001d01003401003501000200000000000000" + + "0000000000030002000032ffffff0100fe00000001430000030013000807" + + "0402840101000201020301020501000601000701000801020a01020b0102" + + "0c01000d01000e0100100100110100120100150100180102036564676535" + + "3130000c030129b70000003cb9b901000001a80200ff440000040004fe02" + + "8401028b00010203010a04000058c3010145000006002400040703048627" + + "040a290c0afe028404028b05028b06028b07028b0802840902840a02840b" + + "02842a028b0101000201000c01020d01020e01020f010210010211010212" + + "010213010214010215010a16010a17010a18010a23030224010025010226" + + "010a28010a2b010a2c01000545564f00849eb90227350000171513121110" + + "0f0e0d0c0b00000000000000000001ba300800005000f4010000ffff0101" + + "0000000001fe01000000050032ff04ff020b000046000006002400050703" + + "048627040a290c0afe028404028b05028b06028b07028b0802840902840a" + + "02840b02842a028b0101000201000c01020d01020e01020f010210010211" + + "010212010213010214010215010a16010a17010a18010a23030224010025" + + "010226010a28010a2b010a2c0100065032534c0000000000273500001715" + + "131211100f0e0d0c0b000100000000000000316e300800005a00f4010000" + + "ffff01010000000001fe01000000050076be04ff020b0000470000060024" + + "00090703048627040a290c0afe028404028b05028b06028b07028b080284" + + "0902840a02840b02842a028b0101000201000c01020d01020e01020f0102" + + "10010211010212010213010214010215010a16010a17010a18010a230302" + + "24010025010226010a28010a2b010a2c0100074c414e47535445520013cc" + + "1200273500001715131211100f0e0d0c0b000200000000000000632a3008" + + "00005f00f4010000ffff010100000000010001000000050032ff04ff020b" + + "000048000006002400020703048627040a290c0afe028404028b05028b06" + + "028b07028b0802840902840a02840b02842a028b0101000201000c01020d" + + "01020e01020f010210010211010212010213010214010215010a16010a17" + + "010a18010a23030224010025010226010a28010a2b010a2c0100084d0000" + + "000000352700001715131211100f0e0d0c0b000300000000000000697a30" + + "0800005f00f4010000ffff010100000000010001000000050032ff04ff02" + + "0b000049000006002400070703048627040a290c0afe028404028b05028b" + + "06028b07028b0802840902840a02840b02842a028b0101000201000c0102" + + "0d01020e01020f010210010211010212010213010214010215010a16010a" + + "17010a18010a23030224010025010226010a28010a2b010a2c0100094269" + + "6b6520350000000000273500001715131211100f0e0d0c0b000400000000" + + "0000000000300800005f00f4010000ffff01010000000000fe0000000000" + + "0032ff04ff020b00000942696b6520360000000000273500001715131211" + + "100f0e0d0c0b0005000000000000000000300800005f00f4010000ffff01" + + "010000000000fe00000000000032ff04ff020b00000942696b6520370000" + + "000000273500001715131211100f0e0d0c0b000600000000000000000030" + + "0800005f00f4010000ffff01010000000000fe00000000000032ff04ff02" + + "0b00000942696b6520380000000000273500001715131211100f0e0d0c0b" + + "0007000000000000000000300800005f00f4010000ffff01010000000000" + + "fe00000000000032ff04ff020b00000942696b6520390000000000273500" + + "001715131211100f0e0d0c0b0008000000000000000000300800005f00f4" + + "010000ffff01010000000000fe00000000000032ff04ff020b00004a0000" + + "06002400080703048627040a290c0afe028404028b05028b06028b07028b" + + "0802840902840a02840b02842a028b0101000201000c01020d01020e0102" + + "0f010210010211010212010213010214010215010a16010a17010a18010a" + + "23030224010025010226010a28010a2b010a2c01000a42696b6520313000" + + "00000000273500001715131211100f0e0d0c0b0009000000000000000000" + + "300800005f00f4010000ffff01010000000000fe00000000000032ff04ff" + + "020b00004b00007f00090309070001000401000501000601000701000801" + + "000901000a01000b45646765203531300000ffffffffffffff09ef");//https://github.com/polyvertex/fitdecode/blob/48b6554d8a3baf33f8b5b9b2fd079fcbe9ac8ce2/tests/files/Settings2.fit + + String expectedOutput = "Local Message: raw: 0 Global Message Number: FILE_ID\n" + + "serial_number(UINT32Z/4): 3889965805 time_created(UINT32/4): null manufacturer(UINT16/2): 1 product(UINT16/2): 1561 number(UINT16/2): null type(ENUM/1): settings \n" + + "Local Message: raw: 1 Global Message Number: FILE_CREATOR\n" + + "software_version(UINT16/2): 340 hardware_version(UINT8/1): null \n" + + "Local Message: raw: 2 Global Message Number: DEVICE_SETTINGS\n" + + "utc_offset(UINT32/4): 0 time_offset(UINT32/4): 0 active_time_zone(UINT8/1): 0 unknown_3(ENUM/1): 0 time_mode(ENUM/1): 0 time_zone_offset(SINT8/1): 0 unknown_10(ENUM/1): 3 unknown_11(ENUM/1): 0 backlight_mode(ENUM/1): 2 unknown_13(UINT8/1): 0 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 50 unknown_16(ENUM/1): null unknown_17(ENUM/1): null unknown_18(ENUM/1): null unknown_21(ENUM/1): 1 unknown_22(ENUM/1): 0 unknown_26(ENUM/1): 254 unknown_27(ENUM/1): 0 unknown_29(ENUM/1): 0 unknown_52(ENUM/1): 0 unknown_53(ENUM/1): 1 \n" + + "Local Message: raw: 3 Global Message Number: USER_PROFILE\n" + + "friendly_name(STRING/8): edge510 weight(UINT16/2): 78 gender(ENUM/1): 1 age(UINT8/1): 41 height(UINT8/1): 183 language(ENUM/1): english elev_setting(ENUM/1): metric weight_setting(ENUM/1): metric resting_heart_rate(UINT8/1): 60 default_max_biking_heart_rate(UINT8/1): 185 default_max_heart_rate(UINT8/1): 185 hr_setting(ENUM/1): 1 speed_setting(ENUM/1): metric dist_setting(ENUM/1): metric power_setting(ENUM/1): 1 activity_class(ENUM/1): 168 position_setting(ENUM/1): 2 temperature_setting(ENUM/1): metric unknown_24(UINT8/1): null \n" + + "Local Message: raw: 4 Global Message Number: UNK_4\n" + + "unknown_254(UINT16/2): 0 unknown_1(UINT16Z/2): 50008 unknown_0(UINT8/1): 1 unknown_3(UINT8Z/1): 1 \n" + + "Local Message: raw: 5 Global Message Number: UNK_6\n" + + "unknown_0(STRING/4): EVO unknown_3(UINT32/4): 45719172 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 0 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): 47617 unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 80 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 1 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 1 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): 5 unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "Local Message: type: TODAY_WEATHER_CONDITIONS Global Message Number: UNK_6\n" + + "unknown_0(STRING/5): P2SL unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 1 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): 28209 unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 90 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 1 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 1 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): 5 unknown_35(UINT8/3): [0,118,190] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "Local Message: raw: 7 Global Message Number: UNK_6\n" + + "unknown_0(STRING/9): LANGSTER unknown_3(UINT32/4): 1231891 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 2 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): 10851 unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 1 unknown_19(UINT8/1): 0 unknown_20(UINT8/1): 1 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): 5 unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "Local Message: raw: 8 Global Message Number: UNK_6\n" + + "unknown_0(STRING/2): M unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [53,39,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 3 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): 31337 unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 1 unknown_19(UINT8/1): 0 unknown_20(UINT8/1): 1 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): 5 unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "Local Message: type: HOURLY_WEATHER_FORECAST Global Message Number: UNK_6\n" + + "unknown_0(STRING/7): Bike 5 unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 4 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): null unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 0 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 0 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): null unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "unknown_0(STRING/7): Bike 6 unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 5 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): null unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 0 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 0 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): null unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "unknown_0(STRING/7): Bike 7 unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 6 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): null unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 0 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 0 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): null unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "unknown_0(STRING/7): Bike 8 unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 7 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): null unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 0 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 0 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): null unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "unknown_0(STRING/7): Bike 9 unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 8 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): null unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 0 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 0 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): null unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "Local Message: type: DAILY_WEATHER_FORECAST Global Message Number: UNK_6\n" + + "unknown_0(STRING/8): Bike 10 unknown_3(UINT32/4): 0 unknown_39(UINT8Z/4): [39,53,,] unknown_41(UINT8Z/12): [23,21,19,18,17,16,15,14,13,12,11,] unknown_254(UINT16/2): 9 unknown_4(UINT16Z/2): null unknown_5(UINT16Z/2): null unknown_6(UINT16Z/2): null unknown_7(UINT16Z/2): null unknown_8(UINT16/2): 2096 unknown_9(UINT16/2): 0 unknown_10(UINT16/2): 95 unknown_11(UINT16/2): 500 unknown_42(UINT16Z/2): null unknown_1(ENUM/1): null unknown_2(ENUM/1): null unknown_12(UINT8/1): 1 unknown_13(UINT8/1): 1 unknown_14(UINT8/1): 0 unknown_15(UINT8/1): 0 unknown_16(UINT8/1): 0 unknown_17(UINT8/1): 0 unknown_18(UINT8/1): 0 unknown_19(UINT8/1): 254 unknown_20(UINT8/1): 0 unknown_21(UINT8Z/1): null unknown_22(UINT8Z/1): null unknown_23(UINT8Z/1): null unknown_24(UINT8Z/1): null unknown_35(UINT8/3): [0,50,] unknown_36(ENUM/1): 4 unknown_37(UINT8/1): null unknown_38(UINT8Z/1): 2 unknown_40(UINT8Z/1): 11 unknown_43(UINT8Z/1): null unknown_44(ENUM/1): 0 \n" + + "Local Message: raw: 11 Global Message Number: CONNECTIVITY\n" + + "name(STRING/9): Edge 510 bluetooth_enabled(ENUM/1): 0 live_tracking_enabled(ENUM/1): null weather_conditions_enabled(ENUM/1): null weather_alerts_enabled(ENUM/1): null auto_activity_upload_enabled(ENUM/1): null course_download_enabled(ENUM/1): null workout_download_enabled(ENUM/1): null gps_ephemeris_download_enabled(ENUM/1): null \n"; + + + Assert.assertEquals(expectedOutput, fitFileParser(fileContents)); + + } + + @Test + public void TestFitFileDevelopersField() { + byte[] fileContents = GB.hexStringToByteArray("0e206806a20000002e464954bed040000100000401028400010002028403048c00000f042329000006a540000100cf0201100d030102000101020305080d1522375990e97962db0040000100ce05000102010102020102031107080a0700000001646f7567686e7574735f6561726e656400646f7567686e7574730060000100140403010204010205048606028401000100008c580000c738b98001008f5a00032c808e400200905c0005a9388a1003d39e");//https://github.com/polyvertex/fitdecode/blob/48b6554d8a3baf33f8b5b9b2fd079fcbe9ac8ce2/tests/files/DeveloperData.fit + + String expectedOutput = "Local Message: raw: 0 Global Message Number: FILE_ID\n" + + "manufacturer(UINT16/2): 15 type(ENUM/1): activity product(UINT16/2): 9001 serial_number(UINT32Z/4): 1701 \n" + + "Local Message: raw: 0 Global Message Number: DEVELOPER_DATA\n" + + "application_id(BASE_TYPE_BYTE/16): [1,1,2,3,5,8,13,21,34,55,89,144,233,121,98,219] developer_data_index(UINT8/1): 0 \n" + + "Local Message: raw: 0 Global Message Number: FIELD_DESCRIPTION\n" + + "developer_data_index(UINT8/1): 0 field_definition_number(UINT8/1): 0 fit_base_type_id(UINT8/1): 1 field_name(STRING/17): doughnuts_earned units(STRING/10): doughnuts \n" + + "Local Message: raw: 0 Global Message Number: RECORD\n" + + "heart_rate(UINT8/1): 140 unknown_4(UINT8/1): 88 unknown_5(UINT32/4): 51000 unknown_6(UINT16/2): 47488 doughnuts_earned(SINT8/1): 1 \n" + + "heart_rate(UINT8/1): 143 unknown_4(UINT8/1): 90 unknown_5(UINT32/4): 208000 unknown_6(UINT16/2): 36416 doughnuts_earned(SINT8/1): 2 \n" + + "heart_rate(UINT8/1): 144 unknown_4(UINT8/1): 92 unknown_5(UINT32/4): 371000 unknown_6(UINT16/2): 35344 doughnuts_earned(SINT8/1): 3 \n"; + + Assert.assertEquals(expectedOutput, fitFileParser(fileContents)); } }