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/
This commit is contained in:
Daniele Gobbetti 2024-04-02 15:10:28 +02:00 committed by José Rebelo
parent 76f51e9412
commit 918791560b
20 changed files with 1074 additions and 239 deletions

View File

@ -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<RecordData> weatherData = new ArrayList<>();
List<RecordDefinition> 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);

View File

@ -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
}
}

View File

@ -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());
}

View File

@ -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,
}
}

View File

@ -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;
}
}

View File

@ -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<Integer, GlobalFITMessage> KNOWNMESSAGES = new HashMap<Integer, GlobalFITMessage>() {{
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<FieldDefinitionPrimitive> fieldDefinitionPrimitives;
GlobalFITMessage(int number, String name, List<FieldDefinitionPrimitive> 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<FieldDefinition> getFieldDefinitions(int... ids) {
if (null == fieldDefinitionPrimitives)
return null;
List<FieldDefinition> 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);
}
}
}

View File

@ -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<FieldDefinition> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<FieldData> fieldDataList;
private final List<FieldData> 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 + ")";
}
}
}

View File

@ -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<FieldDefinition> fieldDefinitions;
private List<DevFieldDefinition> devFieldDefinitions;
public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, int globalMesgNum, List<FieldDefinition> fieldDefinitions, List<DevFieldDefinition> devFieldDefinitions) {
public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, LocalMessage localMessage, GlobalFITMessage globalFITMessage, List<FieldDefinition> fieldDefinitions, List<DevFieldDefinition> 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<FieldDefinition> fieldDefinitions) {
this(recordHeader, byteOrder, mesgType, mesgType.getGlobalMesgNum(), fieldDefinitions, null);
public RecordDefinition(ByteOrder byteOrder, LocalMessage localMessage, List<FieldDefinition> 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<FieldDefinition> 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<FieldDefinition> 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<FieldDefinition> 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<RecordData> 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
}
}
}
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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<FieldDefinition> 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<RecordHeader, RecordDefinition> recordDefinitionMap = new HashMap<>(); //questo va bene qui (ultimo vince)
Map<RecordHeader, RecordData> recordDataMap = new HashMap<>();
List<RecordData> 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));
}
}