mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-09-12 09:16:34 +02:00
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:
parent
b89079b20f
commit
0b858c2919
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user