Garmin: Improve fit parsing

* Remove the dependency on PredefinedLocalMessage from generic fit parsing code
* Standardize toString methods, omit types for known fields
* Return null on unknown field number or names, instead of crashing
* Map more Global FIT messages (device info, monitoring, sleep stages, sleep stats, stress level)
* Prioritize "timestamp" over "253_timestamp" if specified explicitly in the global message definition
* Introduce RecordData wrappers for each global message, allowing us to have proper types when getting data. If missing or unknown, the getter returns null. All classes are auto-generated by the FitCodeGen.
* Persist a list of RecordData, instead of a Map from RecordDefinition
* Fix parsing of compressed timestamps - keep them in computedTimestamp on each data record
* Use timestamp16 if available in Monitoring records
This commit is contained in:
José Rebelo 2024-04-28 23:54:21 +01:00
parent 982e839d5e
commit 09c44f05f8
37 changed files with 1810 additions and 233 deletions

View File

@ -317,15 +317,19 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
List<RecordData> weatherData = new ArrayList<>();
final RecordDefinition recordDefinitionToday = PredefinedLocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition();
final RecordDefinition recordDefinitionHourly = PredefinedLocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition();
final RecordDefinition recordDefinitionDaily = PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition();
List<RecordDefinition> weatherDefinitions = new ArrayList<>(3);
weatherDefinitions.add(PredefinedLocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
weatherDefinitions.add(PredefinedLocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
weatherDefinitions.add(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
weatherDefinitions.add(recordDefinitionToday);
weatherDefinitions.add(recordDefinitionHourly);
weatherDefinitions.add(recordDefinitionDaily);
sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage(weatherDefinitions));
try {
RecordData today = new RecordData(PredefinedLocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
RecordData today = new RecordData(recordDefinitionToday, recordDefinitionToday.getRecordHeader());
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);
@ -346,7 +350,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(PredefinedLocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
RecordData weatherHourlyForecast = new RecordData(recordDefinitionHourly, recordDefinitionHourly.getRecordHeader());
weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
weatherHourlyForecast.setFieldByName("timestamp", hourly.timestamp);
weatherHourlyForecast.setFieldByName("temperature", hourly.temp);
@ -362,7 +366,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
}
}
//
RecordData todayDailyForecast = new RecordData(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
RecordData todayDailyForecast = new RecordData(recordDefinitionDaily, recordDefinitionDaily.getRecordHeader());
todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
todayDailyForecast.setFieldByName("timestamp", weather.timestamp);
todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp);
@ -377,7 +381,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(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
RecordData weatherDailyForecast = new RecordData(recordDefinitionDaily, recordDefinitionDaily.getRecordHeader());
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

@ -1,5 +1,8 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
@ -8,6 +11,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class FieldDefinition implements FieldInterface {
protected static final Logger LOG = LoggerFactory.getLogger(FieldDefinition.class);
protected final BaseType baseType;
protected final int scale;
protected final int offset;
@ -33,13 +38,19 @@ public class FieldDefinition implements FieldInterface {
int size = garminByteBufferReader.readByte();
int baseTypeIdentifier = garminByteBufferReader.readByte();
BaseType baseType = BaseType.fromIdentifier(baseTypeIdentifier);
if (number == 253 && size == 4 && baseType.equals(BaseType.UINT32))
return new FieldDefinitionTimestamp(number, size, baseType, "253_timestamp");
FieldDefinition global = globalFITMessage.getFieldDefinition(number, size);
if (null != global && global.getBaseType().equals(baseType)) {
return global;
if (global != null) {
if (global.getBaseType().equals(baseType)) {
return global;
} else {
LOG.warn("Global is of type {}, but message declares {}", global.getBaseType(), baseType);
}
}
if (number == 253 && size == 4 && baseType.equals(BaseType.UINT32)) {
return new FieldDefinitionTimestamp(number, size, baseType, "253_timestamp");
}
return new FieldDefinition(number, size, baseType, "");
}

View File

@ -8,16 +8,12 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi
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.FieldDefinitionSleepStage;
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);
@ -43,6 +39,8 @@ public class FieldDefinitionFactory {
return new FieldDefinitionWeatherCondition(localNumber, size, baseType, name);
case LANGUAGE:
return new FieldDefinitionLanguage(localNumber, size, baseType, name);
case SLEEP_STAGE:
return new FieldDefinitionSleepStage(localNumber, size, baseType, name);
default:
return new FieldDefinition(localNumber, size, baseType, name);
}
@ -59,5 +57,6 @@ public class FieldDefinitionFactory {
TIMESTAMP,
WEATHER_CONDITION,
LANGUAGE,
SLEEP_STAGE,
}
}

View File

@ -13,27 +13,27 @@ import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecordDataFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class FitFile {
protected static final Logger LOG = LoggerFactory.getLogger(FitFile.class);
private final Header header;
private final Map<RecordDefinition, List<RecordData>> dataRecords;
private final List<RecordData> dataRecords;
private final boolean canGenerateOutput;
public FitFile(Header header, Map<RecordDefinition, List<RecordData>> dataRecords) {
public FitFile(Header header, List<RecordData> dataRecords) {
this.header = header;
this.dataRecords = dataRecords;
this.canGenerateOutput = false;
}
public FitFile(LinkedHashMap<RecordDefinition, List<RecordData>> dataRecords) {
public FitFile(List<RecordData> dataRecords) {
this.dataRecords = dataRecords;
this.header = new Header(true, 16, 21117);
this.canGenerateOutput = true;
@ -64,35 +64,42 @@ public class FitFile {
final Header header = Header.parseIncomingHeader(garminByteBufferReader);
Map<RecordHeader, RecordDefinition> recordDefinitionMap = new HashMap<>(); //needed because the headers can be redefined in the file. The last header wins
Map<RecordDefinition, List<RecordData>> dataRecords = new LinkedHashMap<>();
// needed because the headers can be redefined in the file. The last header for a local message number wins
Map<Integer, RecordDefinition> recordDefinitionMap = new HashMap<>();
List<RecordData> dataRecords = new ArrayList<>();
Long referenceTimestamp = null;
while (garminByteBufferReader.getPosition() < header.getHeaderSize() + header.getDataSize()) {
byte rawRecordHeader = (byte) garminByteBufferReader.readByte();
RecordHeader recordHeader = new RecordHeader(rawRecordHeader);
if (recordHeader.isCompressedTimestamp()) {
referenceTimestamp += recordHeader.getTimeOffset();
recordHeader.setReferenceTimestamp(referenceTimestamp);
final Integer timeOffset = recordHeader.getTimeOffset();
if (timeOffset != null) {
if (referenceTimestamp == null) {
throw new IllegalArgumentException("Got compressed timestamp without knowing current timestamp");
}
if (timeOffset >= (referenceTimestamp & 0x1FL)) {
referenceTimestamp = (referenceTimestamp & ~0x1FL) + timeOffset;
} else if (timeOffset < (referenceTimestamp & 0x1FL)) {
referenceTimestamp = (referenceTimestamp & ~0x1FL) + timeOffset + 0x20;
}
}
if (recordHeader.isDefinition()) {
final RecordDefinition recordDefinition = RecordDefinition.parseIncoming(garminByteBufferReader, recordHeader);
if (recordDefinition != null) {
if (recordHeader.isDeveloperData())
for (RecordDefinition rd : dataRecords.keySet()) {
for (RecordData rd : dataRecords) {
if (GlobalFITMessage.FIELD_DESCRIPTION.equals(rd.getGlobalFITMessage()))
recordDefinition.populateDevFields(dataRecords.get(rd));
recordDefinition.populateDevFields(rd);
}
recordDefinitionMap.put(recordHeader, recordDefinition);
dataRecords.put(recordDefinition, new ArrayList<>());
recordDefinitionMap.put(recordHeader.getLocalMessageType(), recordDefinition);
}
} else {
final RecordDefinition referenceRecordDefinition = recordDefinitionMap.get(recordHeader);
final List<RecordData> myList = dataRecords.get(referenceRecordDefinition);
final RecordDefinition referenceRecordDefinition = recordDefinitionMap.get(recordHeader.getLocalMessageType());
if (referenceRecordDefinition != null) {
final RecordData runningData = new RecordData(referenceRecordDefinition, recordHeader);
myList.add(runningData);
Long newTimestamp = runningData.parseDataMessage(garminByteBufferReader);
final RecordData runningData = FitRecordDataFactory.create(referenceRecordDefinition, recordHeader);
dataRecords.add(runningData);
Long newTimestamp = runningData.parseDataMessage(garminByteBufferReader, referenceTimestamp);
if (newTimestamp != null)
referenceTimestamp = newTimestamp;
}
@ -108,28 +115,31 @@ public class FitFile {
public List<RecordData> getRecordsByGlobalMessage(GlobalFITMessage globalFITMessage) {
final List<RecordData> filtered = new ArrayList<>();
for (RecordDefinition rd : dataRecords.keySet()) {
for (RecordData rd : dataRecords) {
if (globalFITMessage.equals(rd.getGlobalFITMessage()))
filtered.addAll(dataRecords.get(rd));
filtered.add(rd);
}
return filtered;
}
public List<RecordData> getRecords() {
return dataRecords;
}
public void generateOutgoingDataPayload(MessageWriter writer) {
if (!canGenerateOutput)
throw new IllegalArgumentException("Generation of previously parsed FIT file not supported.");
MessageWriter temporary = new MessageWriter();
temporary.setByteOrder(ByteOrder.LITTLE_ENDIAN);
for (Map.Entry<RecordDefinition, List<RecordData>> entry : dataRecords.entrySet()) {
RecordDefinition key = entry.getKey();
List<RecordData> valueList = entry.getValue();
key.generateOutgoingPayload(temporary);
for (RecordData rd :
valueList) {
rd.generateOutgoingDataPayload(temporary);
RecordDefinition prevDefinition = null;
for (final RecordData rd : dataRecords) {
if (!rd.getRecordDefinition().equals(prevDefinition)) {
rd.getRecordDefinition().generateOutgoingPayload(temporary);
prevDefinition = rd.getRecordDefinition();
}
rd.generateOutgoingDataPayload(temporary);
}
this.header.setDataSize(temporary.getSize());
@ -145,7 +155,7 @@ public class FitFile {
return dataRecords.toString();
}
static class Header {
public static class Header {
public static final int MAGIC = 0x5449462E;
private final int headerSize;

View File

@ -87,10 +87,29 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(3, BaseType.UINT8, "heart_rate"),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage DEVICE_INFO = new GlobalFITMessage(23, "DEVICE_INFO", Arrays.asList(
new FieldDefinitionPrimitive(2, BaseType.UINT16, "manufacturer"),
new FieldDefinitionPrimitive(3, BaseType.UINT32Z, "serial_number"),
new FieldDefinitionPrimitive(4, BaseType.UINT16, "product"),
new FieldDefinitionPrimitive(5, BaseType.UINT16, "software_version"),
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 MONITORING = new GlobalFITMessage(55, "MONITORING", Arrays.asList(
new FieldDefinitionPrimitive(2, BaseType.UINT32, "distance"),
new FieldDefinitionPrimitive(3, BaseType.UINT32, "cycles"),
new FieldDefinitionPrimitive(4, BaseType.UINT32, "active_time"),
new FieldDefinitionPrimitive(5, BaseType.ENUM, "activity_type"),
new FieldDefinitionPrimitive(19, BaseType.UINT16, "active_calories"),
new FieldDefinitionPrimitive(29, BaseType.UINT16, "duration_min"),
new FieldDefinitionPrimitive(24, BaseType.BASE_TYPE_BYTE, "current_activity_type_intensity"),
new FieldDefinitionPrimitive(26, BaseType.UINT16, "timestamp_16"),
new FieldDefinitionPrimitive(27, BaseType.UINT8, "heart_rate"),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage CONNECTIVITY = new GlobalFITMessage(127, "CONNECTIVITY", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.ENUM, "bluetooth_enabled"),
new FieldDefinitionPrimitive(3, BaseType.STRING, 20, "name"),
@ -144,8 +163,19 @@ public class GlobalFITMessage {
public static GlobalFITMessage ALARM_SETTINGS = new GlobalFITMessage(222, "ALARM_SETTINGS", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT16, "time", FieldDefinitionFactory.FIELD.ALARM)
));
public static GlobalFITMessage STRESS_LEVEL = new GlobalFITMessage(227, "STRESS_LEVEL", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.SINT16, "stress_level_value"),
new FieldDefinitionPrimitive(1, BaseType.UINT32, "stress_level_time", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static Map<Integer, GlobalFITMessage> KNOWNMESSAGES = new HashMap<Integer, GlobalFITMessage>() {{
public static GlobalFITMessage SLEEP_STAGE = new GlobalFITMessage(275, "SLEEP_STAGE", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.ENUM, "sleep_stage", FieldDefinitionFactory.FIELD.SLEEP_STAGE),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage SLEEP_STATS = new GlobalFITMessage(346, "SLEEP_STATS", Arrays.asList(
));
public static Map<Integer, GlobalFITMessage> KNOWN_MESSAGES = new HashMap<Integer, GlobalFITMessage>() {{
put(0, FILE_ID);
put(2, DEVICE_SETTINGS);
put(3, USER_PROFILE);
@ -153,13 +183,18 @@ public class GlobalFITMessage {
put(12, SPORT);
put(15, GOALS);
put(20, RECORD);
put(23, DEVICE_INFO);
put(49, FILE_CREATOR);
put(55, MONITORING);
put(127, CONNECTIVITY);
put(128, WEATHER);
put(159, WATCHFACE_SETTINGS);
put(206, FIELD_DESCRIPTION);
put(207, DEVELOPER_DATA);
put(222, ALARM_SETTINGS);
put(227, STRESS_LEVEL);
put(275, SLEEP_STAGE);
put(346, SLEEP_STATS);
}};
private final int number;
private final String name;
@ -173,7 +208,7 @@ public class GlobalFITMessage {
}
public static GlobalFITMessage fromNumber(final int number) {
final GlobalFITMessage found = KNOWNMESSAGES.get(number);
final GlobalFITMessage found = KNOWN_MESSAGES.get(number);
if (found != null) {
return found;
}
@ -188,6 +223,10 @@ public class GlobalFITMessage {
return number;
}
public List<FieldDefinitionPrimitive> getFieldDefinitionPrimitives() {
return fieldDefinitionPrimitives;
}
@Nullable
public List<FieldDefinition> getFieldDefinitions(int... ids) {
if (null == fieldDefinitionPrimitives)
@ -205,6 +244,25 @@ public class GlobalFITMessage {
return subset;
}
@Nullable
public FieldDefinition getFieldDefinition(String name) {
for (FieldDefinitionPrimitive fieldDefinitionPrimitive :
fieldDefinitionPrimitives) {
if (fieldDefinitionPrimitive.name.equals(name)) {
return FieldDefinitionFactory.create(
fieldDefinitionPrimitive.number,
fieldDefinitionPrimitive.size,
fieldDefinitionPrimitive.type,
fieldDefinitionPrimitive.baseType,
fieldDefinitionPrimitive.name,
fieldDefinitionPrimitive.scale,
fieldDefinitionPrimitive.offset
);
}
}
return null;
}
@Nullable
public FieldDefinition getFieldDefinition(int id, int size) {
if (null == fieldDefinitionPrimitives)
@ -218,7 +276,7 @@ public class GlobalFITMessage {
return null;
}
static class FieldDefinitionPrimitive {
public static class FieldDefinitionPrimitive {
private final int number;
private final BaseType baseType;
private final String name;
@ -252,5 +310,33 @@ public class GlobalFITMessage {
public FieldDefinitionPrimitive(int number, BaseType baseType, String name, int scale, int offset) {
this(number, baseType, baseType.getSize(), name, null, scale, offset);
}
public int getNumber() {
return number;
}
public BaseType getBaseType() {
return baseType;
}
public String getName() {
return name;
}
public FieldDefinitionFactory.FIELD getType() {
return type;
}
public int getScale() {
return scale;
}
public int getOffset() {
return offset;
}
public int getSize() {
return size;
}
}
}

View File

@ -36,12 +36,16 @@ public enum PredefinedLocalMessage {
return null;
}
public List<FieldDefinition> getLocalFieldDefinitions() {
return globalFITMessage.getFieldDefinitions(globalDefinitionIds);
}
public RecordDefinition getRecordDefinition() {
return new RecordDefinition(ByteOrder.BIG_ENDIAN, this);
final RecordHeader recordHeader = new RecordHeader(true, false, type, null);
final List<FieldDefinition> fieldDefinitions = globalFITMessage.getFieldDefinitions(globalDefinitionIds);
return new RecordDefinition(
recordHeader,
ByteOrder.BIG_ENDIAN,
globalFITMessage,
fieldDefinitions,
null
);
}
public int getType() {

View File

@ -6,28 +6,35 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GBToStringBuilder;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType.STRING;
import org.apache.commons.lang3.StringUtils;
public class RecordData {
private final RecordDefinition recordDefinition;
private final RecordHeader recordHeader;
private final GlobalFITMessage globalFITMessage;
private final List<FieldData> fieldDataList;
protected ByteBuffer valueHolder;
public RecordData(RecordDefinition recordDefinition, RecordHeader recordHeader) {
private Long computedTimestamp = null;
public RecordData(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
if (null == recordDefinition.getFieldDefinitions())
throw new IllegalArgumentException("Cannot create record data without FieldDefinitions " + recordDefinition);
fieldDataList = new ArrayList<>();
this.recordDefinition = recordDefinition;
this.recordHeader = recordHeader;
this.globalFITMessage = recordDefinition.getGlobalFITMessage();
@ -58,21 +65,24 @@ public class RecordData {
}
public RecordData(RecordDefinition recordDefinition) {
this(recordDefinition, recordDefinition.getRecordHeader());
}
public GlobalFITMessage getGlobalFITMessage() {
return globalFITMessage;
}
public Long parseDataMessage(GarminByteBufferReader garminByteBufferReader) {
public RecordDefinition getRecordDefinition() {
return recordDefinition;
}
public Long parseDataMessage(final GarminByteBufferReader garminByteBufferReader, final Long currentTimestamp) {
garminByteBufferReader.setByteOrder(valueHolder.order());
computedTimestamp = currentTimestamp;
Long referenceTimestamp = null;
for (FieldData fieldData : fieldDataList) {
Long runningTimestamp = fieldData.parseDataMessage(garminByteBufferReader);
if (runningTimestamp != null)
if (runningTimestamp != null) {
computedTimestamp = runningTimestamp;
referenceTimestamp = runningTimestamp;
}
}
return referenceTimestamp;
}
@ -119,7 +129,7 @@ public class RecordData {
return fieldData.decode();
}
}
throw new IllegalArgumentException("Unknown field number " + number);
return null;
}
public Object getFieldByName(String name) {
@ -129,7 +139,7 @@ public class RecordData {
return fieldData.decode();
}
}
throw new IllegalArgumentException("Unknown field name " + name);
return null;
}
public int[] getFieldsNumbers() {
@ -143,45 +153,41 @@ public class RecordData {
}
public Long getComputedTimestamp() {
for (FieldData fieldData : fieldDataList) {
if (fieldData.getNumber() == 253 || fieldData.fieldDefinition instanceof FieldDefinitionTimestamp)
return (long) fieldData.decode();
}
if (recordHeader.isCompressedTimestamp())
return (long) recordHeader.getResultingTimestamp();
return null;
return computedTimestamp;
}
@NonNull
@Override
public String toString() {
StringBuilder oBuilder = new StringBuilder();
oBuilder.append(System.lineSeparator());
for (FieldData fieldData :
fieldDataList) {
if (fieldData.getName() != null && !fieldData.getName().equals("")) {
oBuilder.append(fieldData.getName());
} else {
oBuilder.append("unknown_" + fieldData.getNumber());
}
oBuilder.append(fieldData);
oBuilder.append(": ");
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(" ");
}
if (recordHeader.isCompressedTimestamp())
oBuilder.append("compressed_timestamp: " + getComputedTimestamp());
return oBuilder.toString();
}
final GBToStringBuilder tsb = new GBToStringBuilder(this);
public PredefinedLocalMessage getPredefinedLocalMessage() {
return recordHeader.getPredefinedLocalMessage();
if (this.getClass().getName().equals(RecordData.class.getName())) {
tsb.append(globalFITMessage.name());
}
if (computedTimestamp != null) {
tsb.append(new Date(computedTimestamp * 1000L));
}
for (FieldData fieldData : fieldDataList) {
final String fieldName;
if (!StringUtils.isBlank(fieldData.getName())) {
fieldName = fieldData.getName();
} else {
fieldName = "unknown_" + fieldData.getNumber() + fieldData;
}
Object o = fieldData.decode();
final String fieldValueString;
if (o == null) {
fieldValueString = null;
} else if (o instanceof Object[]) {
fieldValueString = "[" + StringUtils.join((Object[]) o, ",") + "]";
} else {
fieldValueString = o.toString();
}
tsb.append(fieldName, fieldValueString);
}
return tsb.build();
}
private class FieldData {
@ -225,7 +231,7 @@ public class RecordData {
private Long parseDataMessage(GarminByteBufferReader garminByteBufferReader) {
goToPosition();
valueHolder.put(garminByteBufferReader.readBytes(size));
if (fieldDefinition instanceof FieldDefinitionTimestamp)
if (fieldDefinition.getNumber() == 253)
return (Long) decode();
return null;
}

View File

@ -14,28 +14,18 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.Mess
public class RecordDefinition {
private final RecordHeader recordHeader;
private final GlobalFITMessage globalFITMessage;
private final PredefinedLocalMessage predefinedLocalMessage;
private final java.nio.ByteOrder byteOrder;
private List<FieldDefinition> fieldDefinitions;
private List<DevFieldDefinition> devFieldDefinitions;
public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, PredefinedLocalMessage predefinedLocalMessage, GlobalFITMessage globalFITMessage, List<FieldDefinition> fieldDefinitions, List<DevFieldDefinition> devFieldDefinitions) {
public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, GlobalFITMessage globalFITMessage, List<FieldDefinition> fieldDefinitions, List<DevFieldDefinition> devFieldDefinitions) {
this.recordHeader = recordHeader;
this.byteOrder = byteOrder;
this.predefinedLocalMessage = predefinedLocalMessage;
this.globalFITMessage = globalFITMessage;
this.fieldDefinitions = fieldDefinitions;
this.devFieldDefinitions = devFieldDefinitions;
}
public RecordDefinition(ByteOrder byteOrder, PredefinedLocalMessage predefinedLocalMessage) {
this(new RecordHeader(true, false, predefinedLocalMessage, null), byteOrder, predefinedLocalMessage, predefinedLocalMessage.getGlobalFITMessage(), predefinedLocalMessage.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) {
if (!recordHeader.isDefinition())
return null;
@ -45,7 +35,7 @@ public class RecordDefinition {
final int globalMesgNum = garminByteBufferReader.readShort();
final GlobalFITMessage globalFITMessage = GlobalFITMessage.fromNumber(globalMesgNum);
RecordDefinition definitionMessage = new RecordDefinition(byteOrder, recordHeader, globalFITMessage, null);
RecordDefinition definitionMessage = new RecordDefinition(recordHeader, byteOrder, globalFITMessage, null, null);
final int numFields = garminByteBufferReader.readByte();
List<FieldDefinition> fieldDefinitions = new ArrayList<>(numFields);
@ -113,33 +103,24 @@ public class RecordDefinition {
}
}
public String getName() {
return predefinedLocalMessage != null ? predefinedLocalMessage.name() : "unknown_" + globalFITMessage;
}
@NonNull
public String toString() {
return System.lineSeparator() + 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
public void populateDevFields(RecordData recordData) {
for (DevFieldDefinition devFieldDef : getDevFieldDefinitions()) {
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

@ -7,47 +7,36 @@ import androidx.annotation.Nullable;
public class RecordHeader {
private final boolean definition;
private final boolean developerData;
private final PredefinedLocalMessage predefinedLocalMessage;
private final int rawLocalMessageType;
private final int localMessageType;
private final Integer timeOffset;
private long referenceTimestamp;
public RecordHeader(boolean definition, boolean developerData, PredefinedLocalMessage predefinedLocalMessage, Integer timeOffset) {
public RecordHeader(boolean definition, boolean developerData, int localMessageType, Integer timeOffset) {
this.definition = definition;
this.developerData = developerData;
this.predefinedLocalMessage = predefinedLocalMessage;
this.rawLocalMessageType = predefinedLocalMessage.getType();
this.localMessageType = localMessageType;
this.timeOffset = timeOffset;
}
//see https://github.com/polyvertex/fitdecode/blob/master/fitdecode/reader.py#L512
public RecordHeader(byte header) {
this(header, false);
}
public RecordHeader(byte header, boolean inferLocalMessage) {
if ((header & 0x80) == 0x80) { //compressed timestamp TODO add support
if ((header & 0x80) == 0x80) { //compressed timestamp
definition = false;
developerData = false;
rawLocalMessageType = (header >> 5) & 0x3;
localMessageType = (header >> 5) & 0x3;
timeOffset = header & 0x1f;
} else {
definition = ((header & 0x40) == 0x40);
developerData = ((header & 0x20) == 0x20);
rawLocalMessageType = header & 0xf;
localMessageType = header & 0xf;
timeOffset = null;
}
if (inferLocalMessage)
predefinedLocalMessage = PredefinedLocalMessage.fromType(rawLocalMessageType);
else
predefinedLocalMessage = null;
}
public void setReferenceTimestamp(long referenceTimestamp) {
this.referenceTimestamp = referenceTimestamp;
public int getLocalMessageType() {
return localMessageType;
}
@Nullable
public Integer getTimeOffset() {
return timeOffset;
}
@ -56,10 +45,6 @@ public class RecordHeader {
return timeOffset != null;
}
public Long getResultingTimestamp() {
return referenceTimestamp + timeOffset;
}
public boolean isDeveloperData() {
return developerData;
}
@ -68,15 +53,12 @@ public class RecordHeader {
return definition;
}
@Nullable
public PredefinedLocalMessage getPredefinedLocalMessage() {
return predefinedLocalMessage;
}
public byte generateOutgoingDefinitionPayload() {
if (!definition && !developerData)
return (byte) (timeOffset | (((byte) predefinedLocalMessage.getType()) << 5));
byte base = (byte) (null == predefinedLocalMessage ? rawLocalMessageType : predefinedLocalMessage.getType());
if (!definition && !developerData) {
assert timeOffset != null;
return (byte) (timeOffset | (((byte) localMessageType) << 5));
}
byte base = (byte) localMessageType;
if (definition)
base = (byte) (base | 0x40);
if (developerData)
@ -86,9 +68,11 @@ public class RecordHeader {
}
public byte generateOutgoingDataPayload() { //TODO: unclear if correct
if (!definition && !developerData)
return (byte) (timeOffset | (((byte) predefinedLocalMessage.getType()) << 5));
byte base = (byte) (null == predefinedLocalMessage ? rawLocalMessageType : predefinedLocalMessage.getType());
if (!definition && !developerData) {
assert timeOffset != null;
return (byte) (timeOffset | (((byte) localMessageType) << 5));
}
byte base = (byte) localMessageType;
if (developerData)
base = (byte) (base | 0x20);
@ -98,24 +82,6 @@ public class RecordHeader {
@NonNull
@Override
public String toString() {
return "Local Message: " + (null == predefinedLocalMessage ? "raw: " + rawLocalMessageType : "type: " + predefinedLocalMessage.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 (rawLocalMessageType != that.rawLocalMessageType) return false;
return predefinedLocalMessage == that.predefinedLocalMessage;
}
@Override
public int hashCode() {
int result = (predefinedLocalMessage != null ? predefinedLocalMessage.hashCode() : 0);
result = 31 * result + rawLocalMessageType;
return result;
return "Local Message: " + localMessageType;
}
}

View File

@ -0,0 +1,297 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.threeten.bp.DayOfWeek;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage;
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.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.FieldDefinitionSleepStage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherCondition;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
// This class is only used to generate code, and will not be packaged in the final apk
@RequiresApi(api = Build.VERSION_CODES.O)
public class FitCodeGen {
public static void main(final String[] args) throws Exception {
new FitCodeGen().generate();
}
public void generate() throws IOException {
final File factoryFile = new File("app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java");
final StringBuilder sbFactory = new StringBuilder();
String header = getHeader(factoryFile);
if (!header.isEmpty()) {
sbFactory.append(header);
sbFactory.append("\n");
}
sbFactory.append("package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;\n");
sbFactory.append("\n");
sbFactory.append("import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;\n");
sbFactory.append("import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;\n");
sbFactory.append("import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;\n");
sbFactory.append("\n");
sbFactory.append("//\n");
sbFactory.append("// WARNING: This class was auto-generated, please avoid modifying it directly.\n");
sbFactory.append("// See ").append(getClass().getCanonicalName()).append("\n");
sbFactory.append("//\n");
sbFactory.append("public class FitRecordDataFactory {\n");
sbFactory.append(" private FitRecordDataFactory() {\n");
sbFactory.append(" // use create\n");
sbFactory.append(" }\n");
sbFactory.append("\n");
sbFactory.append(" public static RecordData create(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {\n");
sbFactory.append(" switch (recordDefinition.getGlobalFITMessage().getNumber()) {\n");
final ArrayList<GlobalFITMessage> globalFITMessages = new ArrayList<>(GlobalFITMessage.KNOWN_MESSAGES.values());
Collections.sort(globalFITMessages, Comparator.comparingInt(GlobalFITMessage::getNumber));
for (final GlobalFITMessage value : globalFITMessages) {
final String className = "Fit" + capitalize(toCamelCase(value.name()));
sbFactory.append(" case ").append(value.getNumber()).append(":\n");
sbFactory.append(" return new ").append(className).append("(recordDefinition, recordHeader);\n");
process(value);
}
sbFactory.append(" }\n");
sbFactory.append("\n");
sbFactory.append(" return new RecordData(recordDefinition, recordHeader);\n");
sbFactory.append(" }\n");
sbFactory.append("}\n");
FileUtils.copyStringToFile(sbFactory.toString(), factoryFile, "replace");
}
public void process(final GlobalFITMessage globalFITMessage) throws IOException {
final String className = "Fit" + capitalize(toCamelCase(globalFITMessage.name()));
final File outputFile = new File("app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/" + className + ".java");
final List<String> imports = new ArrayList<>();
imports.add(Nullable.class.getCanonicalName());
imports.add(RecordData.class.getCanonicalName());
imports.add(RecordDefinition.class.getCanonicalName());
imports.add(RecordHeader.class.getCanonicalName());
//imports.add(GBToStringBuilder.class.getCanonicalName());
Collections.sort(imports);
for (final GlobalFITMessage.FieldDefinitionPrimitive primitive : globalFITMessage.getFieldDefinitionPrimitives()) {
final Class<?> fieldType = getFieldType(primitive);
if (!Objects.requireNonNull(fieldType.getCanonicalName()).startsWith("java.lang")) {
imports.add(fieldType.getCanonicalName());
}
}
final StringBuilder sb = new StringBuilder();
String header = getHeader(outputFile);
if (!header.isEmpty()) {
sb.append(header);
sb.append("\n");
}
sb.append("package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;");
sb.append("\n");
sb.append("\n");
boolean anyImport = false;
for (final String i : imports) {
if (i.startsWith("androidx")) {
sb.append("import ").append(i).append(";\n");
anyImport = true;
}
}
if (anyImport) {
sb.append("\n");
anyImport = false;
}
for (final String i : imports) {
if (i.startsWith("nodomain.freeyourgadget")) {
sb.append("import ").append(i).append(";\n");
anyImport = true;
}
}
if (anyImport) {
sb.append("\n");
anyImport = false;
}
for (final String i : imports) {
if (!i.startsWith("androidx") && !i.startsWith("nodomain.freeyourgadget")) {
sb.append("import ").append(i).append(";\n");
anyImport = true;
}
}
if (anyImport) {
sb.append("\n");
}
sb.append("//\n");
sb.append("// WARNING: This class was auto-generated, please avoid modifying it directly.\n");
sb.append("// See ").append(getClass().getCanonicalName()).append("\n");
sb.append("//\n");
sb.append("public class ").append(className).append(" extends RecordData {\n");
sb.append(" public ").append(className).append("(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {\n");
sb.append(" super(recordDefinition, recordHeader);\n");
sb.append("\n");
sb.append(" final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();\n");
sb.append(" if (globalNumber != ").append(globalFITMessage.getNumber()).append(") {\n");
sb.append(" throw new IllegalArgumentException(\"FitFileId expects global messages of \" + ").append(globalFITMessage.getNumber()).append(" + \", got \" + globalNumber);\n");
sb.append(" }\n");
sb.append(" }\n");
for (final GlobalFITMessage.FieldDefinitionPrimitive primitive : globalFITMessage.getFieldDefinitionPrimitives()) {
final Class<?> fieldType = getFieldType(primitive);
final String fieldTypeName = fieldType.getSimpleName();
sb.append("\n");
sb.append(" @Nullable\n");
sb.append(" public ").append(fieldTypeName).append(method(" get", primitive)).append("() {\n");
sb.append(" return (").append(fieldTypeName).append(") getFieldByNumber(").append(primitive.getNumber()).append(");\n");
sb.append(" }\n");
}
//sb.append("\n");
//sb.append(" @NonNull\n");
//sb.append(" @Override\n");
//sb.append(" public String toString() {\n");
//sb.append(" return new GBToStringBuilder(this)\n");
//for (final GlobalFITMessage.FieldDefinitionPrimitive primitive : globalFITMessage.getFieldDefinitionPrimitives()) {
// sb.append(" .append(\"").append(primitive.getName()).append("\",").append(method(" get", primitive)).append("())\n");
//}
//sb.append(" .build();\n");
//sb.append(" }\n");
if (outputFile.exists()) {
// Keep manual changes if any
final String fileContents = new String(Files.readAllBytes(outputFile.toPath()), StandardCharsets.UTF_8);
final int manualChangesIndex = fileContents.indexOf("// manual changes below");
if (manualChangesIndex > 0) {
sb.append("\n");
sb.append(" ");
sb.append(fileContents.substring(manualChangesIndex));
} else {
sb.append("}\n");
}
} else {
sb.append("}\n");
}
FileUtils.copyStringToFile(sb.toString(), outputFile, "replace");
}
public Class<?> getFieldType(final GlobalFITMessage.FieldDefinitionPrimitive primitive) {
if (primitive.getType() != null) {
switch (primitive.getType()) {
case ALARM:
return Calendar.class;
case DAY_OF_WEEK:
return DayOfWeek.class;
case FILE_TYPE:
return FieldDefinitionFileType.Type.class;
case GOAL_SOURCE:
return FieldDefinitionGoalSource.Source.class;
case GOAL_TYPE:
return FieldDefinitionGoalType.Type.class;
case MEASUREMENT_SYSTEM:
return FieldDefinitionMeasurementSystem.Type.class;
case TEMPERATURE:
return Integer.class;
case TIMESTAMP:
return Long.class;
case WEATHER_CONDITION:
return FieldDefinitionWeatherCondition.Condition.class;
case LANGUAGE:
return FieldDefinitionLanguage.Language.class;
case SLEEP_STAGE:
return FieldDefinitionSleepStage.SleepStage.class;
}
throw new RuntimeException("Unknown field type " + primitive.getType());
}
switch (primitive.getBaseType()) {
case ENUM:
case SINT8:
case UINT8:
case SINT16:
case UINT16:
case UINT8Z:
case UINT16Z:
case BASE_TYPE_BYTE:
return Integer.class;
case SINT32:
case UINT32:
case UINT32Z:
case SINT64:
case UINT64:
case UINT64Z:
return Long.class;
case STRING:
return String.class;
case FLOAT32:
return Float.class;
case FLOAT64:
return Double.class;
}
throw new RuntimeException("Unknown base type " + primitive.getBaseType());
}
public String toCamelCase(final String str) {
final StringBuilder sb = new StringBuilder(str.toLowerCase());
for (int i = 0; i < sb.length(); i++) {
if (sb.charAt(i) == '_') {
sb.deleteCharAt(i);
sb.replace(i, i + 1, String.valueOf(Character.toUpperCase(sb.charAt(i))));
}
}
return sb.toString();
}
public String method(final String methodName, final GlobalFITMessage.FieldDefinitionPrimitive primitive) {
return methodName + capitalize(toCamelCase(primitive.getName()));
}
public String capitalize(final String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
public String getHeader(final File file) throws IOException {
if (file.exists()) {
final String fileContents = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
final int packageIndex = fileContents.indexOf("package") - 1;
if (packageIndex > 0) {
return fileContents.substring(0, packageIndex);
}
}
return "";
}
}

View File

@ -28,7 +28,7 @@ public class FieldDefinitionLanguage extends FieldDefinition {
baseType.encode(byteBuffer, o, scale, offset);
}
private enum Language {
public enum Language {
english(0),
italian(2),
;

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 FieldDefinitionSleepStage extends FieldDefinition {
public FieldDefinitionSleepStage(final int localNumber, final int size, final BaseType baseType, final String name) {
super(localNumber, size, baseType, name, 1, 0);
}
@Override
public Object decode(final ByteBuffer byteBuffer) {
final int raw = (int) baseType.decode(byteBuffer, scale, offset);
return SleepStage.fromId(raw);
}
@Override
public void encode(final ByteBuffer byteBuffer, final Object o) {
if (o instanceof SleepStage) {
baseType.encode(byteBuffer, (((SleepStage) o).getId()), scale, offset);
return;
}
baseType.encode(byteBuffer, o, scale, offset);
}
public enum SleepStage {
AWAKE(1),
LIGHT(2),
DEEP(3),
REM(4),
;
private final int id;
SleepStage(final int i) {
id = i;
}
@Nullable
public static SleepStage fromId(final int id) {
for (SleepStage stage : SleepStage.values()) {
if (id == stage.getId()) {
return stage;
}
}
return null;
}
public int getId() {
return id;
}
}
}

View File

@ -142,7 +142,7 @@ public class FieldDefinitionWeatherCondition extends FieldDefinition {
}
}
enum Condition {
public enum Condition {
CLEAR,
PARTLY_CLOUDY,
MOSTLY_CLOUDY,

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import java.util.Calendar;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitAlarmSettings extends RecordData {
public FitAlarmSettings(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 222) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 222 + ", got " + globalNumber);
}
}
@Nullable
public Calendar getTime() {
return (Calendar) getFieldByNumber(0);
}
}

View File

@ -0,0 +1,67 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitConnectivity extends RecordData {
public FitConnectivity(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 127) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 127 + ", got " + globalNumber);
}
}
@Nullable
public Integer getBluetoothEnabled() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public String getName() {
return (String) getFieldByNumber(3);
}
@Nullable
public Integer getLiveTrackingEnabled() {
return (Integer) getFieldByNumber(4);
}
@Nullable
public Integer getWeatherConditionsEnabled() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Integer getWeatherAlertsEnabled() {
return (Integer) getFieldByNumber(6);
}
@Nullable
public Integer getAutoActivityUploadEnabled() {
return (Integer) getFieldByNumber(7);
}
@Nullable
public Integer getCourseDownloadEnabled() {
return (Integer) getFieldByNumber(8);
}
@Nullable
public Integer getWorkoutDownloadEnabled() {
return (Integer) getFieldByNumber(9);
}
@Nullable
public Integer getGpsEphemerisDownloadEnabled() {
return (Integer) getFieldByNumber(10);
}
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitDeveloperData extends RecordData {
public FitDeveloperData(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 207) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 207 + ", got " + globalNumber);
}
}
@Nullable
public Integer getApplicationId() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Integer getDeveloperDataIndex() {
return (Integer) getFieldByNumber(3);
}
}

View File

@ -0,0 +1,47 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitDeviceInfo extends RecordData {
public FitDeviceInfo(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 23) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 23 + ", got " + globalNumber);
}
}
@Nullable
public Integer getManufacturer() {
return (Integer) getFieldByNumber(2);
}
@Nullable
public Long getSerialNumber() {
return (Long) getFieldByNumber(3);
}
@Nullable
public Integer getProduct() {
return (Integer) getFieldByNumber(4);
}
@Nullable
public Integer getSoftwareVersion() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}

View File

@ -0,0 +1,102 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitDeviceSettings extends RecordData {
public FitDeviceSettings(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 2) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 2 + ", got " + globalNumber);
}
}
@Nullable
public Integer getActiveTimeZone() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Long getUtcOffset() {
return (Long) getFieldByNumber(1);
}
@Nullable
public Long getTimeOffset() {
return (Long) getFieldByNumber(2);
}
@Nullable
public Integer getTimeMode() {
return (Integer) getFieldByNumber(4);
}
@Nullable
public Integer getTimeZoneOffset() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Integer getBacklightMode() {
return (Integer) getFieldByNumber(12);
}
@Nullable
public Integer getActivityTrackerEnabled() {
return (Integer) getFieldByNumber(36);
}
@Nullable
public Integer getMoveAlertEnabled() {
return (Integer) getFieldByNumber(46);
}
@Nullable
public Integer getDateMode() {
return (Integer) getFieldByNumber(47);
}
@Nullable
public Integer getDisplayOrientation() {
return (Integer) getFieldByNumber(55);
}
@Nullable
public Integer getMountingSide() {
return (Integer) getFieldByNumber(56);
}
@Nullable
public Integer getDefaultPage() {
return (Integer) getFieldByNumber(57);
}
@Nullable
public Integer getAutosyncMinSteps() {
return (Integer) getFieldByNumber(58);
}
@Nullable
public Integer getAutosyncMinTime() {
return (Integer) getFieldByNumber(59);
}
@Nullable
public Integer getBleAutoUploadEnabled() {
return (Integer) getFieldByNumber(86);
}
@Nullable
public Long getAutoActivityDetect() {
return (Long) getFieldByNumber(90);
}
}

View File

@ -0,0 +1,47 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitFieldDescription extends RecordData {
public FitFieldDescription(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 206) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 206 + ", got " + globalNumber);
}
}
@Nullable
public Integer getDeveloperDataIndex() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getFieldDefinitionNumber() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Integer getFitBaseTypeId() {
return (Integer) getFieldByNumber(2);
}
@Nullable
public String getFieldName() {
return (String) getFieldByNumber(3);
}
@Nullable
public String getUnits() {
return (String) getFieldByNumber(8);
}
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitFileCreator extends RecordData {
public FitFileCreator(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 49) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 49 + ", got " + globalNumber);
}
}
@Nullable
public Integer getSoftwareVersion() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getHardwareVersion() {
return (Integer) getFieldByNumber(1);
}
}

View File

@ -0,0 +1,63 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType.Type;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitFileId extends RecordData {
public FitFileId(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 0) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 0 + ", got " + globalNumber);
}
}
@Nullable
public Type getType() {
return (Type) getFieldByNumber(0);
}
@Nullable
public Integer getManufacturer() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Integer getProduct() {
return (Integer) getFieldByNumber(2);
}
@Nullable
public Long getSerialNumber() {
return (Long) getFieldByNumber(3);
}
@Nullable
public Long getTimeCreated() {
return (Long) getFieldByNumber(4);
}
@Nullable
public Integer getNumber() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Integer getManufacturerPartner() {
return (Integer) getFieldByNumber(6);
}
@Nullable
public String getProductName() {
return (String) getFieldByNumber(8);
}
}

View File

@ -0,0 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType.Type;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource.Source;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitGoals extends RecordData {
public FitGoals(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 15) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 15 + ", got " + globalNumber);
}
}
@Nullable
public Type getType() {
return (Type) getFieldByNumber(4);
}
@Nullable
public Long getTargetValue() {
return (Long) getFieldByNumber(7);
}
@Nullable
public Source getSource() {
return (Source) getFieldByNumber(11);
}
}

View File

@ -0,0 +1,84 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitMonitoring extends RecordData {
public FitMonitoring(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 55) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 55 + ", got " + globalNumber);
}
}
@Nullable
public Long getDistance() {
return (Long) getFieldByNumber(2);
}
@Nullable
public Long getCycles() {
return (Long) getFieldByNumber(3);
}
@Nullable
public Long getActiveTime() {
return (Long) getFieldByNumber(4);
}
@Nullable
public Integer getActivityType() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Integer getActiveCalories() {
return (Integer) getFieldByNumber(19);
}
@Nullable
public Integer getDurationMin() {
return (Integer) getFieldByNumber(29);
}
@Nullable
public Integer getCurrentActivityTypeIntensity() {
return (Integer) getFieldByNumber(24);
}
@Nullable
public Integer getTimestamp16() {
return (Integer) getFieldByNumber(26);
}
@Nullable
public Integer getHeartRate() {
return (Integer) getFieldByNumber(27);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
// manual changes below
@Override
public Long getComputedTimestamp() {
final Integer timestamp16 = getTimestamp16();
final Long computedTimestamp = super.getComputedTimestamp();
if (timestamp16 != null && computedTimestamp != null) {
return (computedTimestamp & ~0xFFFFL) | timestamp16;
}
return computedTimestamp;
}
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitRecord extends RecordData {
public FitRecord(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 20) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 20 + ", got " + globalNumber);
}
}
@Nullable
public Integer getHeartRate() {
return (Integer) getFieldByNumber(3);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}

View File

@ -0,0 +1,60 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitRecordDataFactory {
private FitRecordDataFactory() {
// use create
}
public static RecordData create(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
switch (recordDefinition.getGlobalFITMessage().getNumber()) {
case 0:
return new FitFileId(recordDefinition, recordHeader);
case 2:
return new FitDeviceSettings(recordDefinition, recordHeader);
case 3:
return new FitUserProfile(recordDefinition, recordHeader);
case 7:
return new FitZonesTarget(recordDefinition, recordHeader);
case 12:
return new FitSport(recordDefinition, recordHeader);
case 15:
return new FitGoals(recordDefinition, recordHeader);
case 20:
return new FitRecord(recordDefinition, recordHeader);
case 23:
return new FitDeviceInfo(recordDefinition, recordHeader);
case 49:
return new FitFileCreator(recordDefinition, recordHeader);
case 55:
return new FitMonitoring(recordDefinition, recordHeader);
case 127:
return new FitConnectivity(recordDefinition, recordHeader);
case 128:
return new FitWeather(recordDefinition, recordHeader);
case 159:
return new FitWatchfaceSettings(recordDefinition, recordHeader);
case 206:
return new FitFieldDescription(recordDefinition, recordHeader);
case 207:
return new FitDeveloperData(recordDefinition, recordHeader);
case 222:
return new FitAlarmSettings(recordDefinition, recordHeader);
case 227:
return new FitStressLevel(recordDefinition, recordHeader);
case 275:
return new FitSleepStage(recordDefinition, recordHeader);
case 346:
return new FitSleepStats(recordDefinition, recordHeader);
}
return new RecordData(recordDefinition, recordHeader);
}
}

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage.SleepStage;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitSleepStage extends RecordData {
public FitSleepStage(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 275) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 275 + ", got " + globalNumber);
}
}
@Nullable
public SleepStage getSleepStage() {
return (SleepStage) getFieldByNumber(0);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}

View File

@ -0,0 +1,22 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitSleepStats extends RecordData {
public FitSleepStats(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 346) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 346 + ", got " + globalNumber);
}
}
}

View File

@ -0,0 +1,37 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitSport extends RecordData {
public FitSport(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 12) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 12 + ", got " + globalNumber);
}
}
@Nullable
public Integer getSport() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getSubSport() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public String getName() {
return (String) getFieldByNumber(3);
}
}

View File

@ -0,0 +1,48 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitStressLevel extends RecordData {
public FitStressLevel(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 227) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 227 + ", got " + globalNumber);
}
}
@Nullable
public Integer getStressLevelValue() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Long getStressLevelTime() {
return (Long) getFieldByNumber(1);
}
}

View File

@ -0,0 +1,144 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage.Language;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem.Type;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitUserProfile extends RecordData {
public FitUserProfile(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 3) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 3 + ", got " + globalNumber);
}
}
@Nullable
public String getFriendlyName() {
return (String) getFieldByNumber(0);
}
@Nullable
public Integer getGender() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Integer getAge() {
return (Integer) getFieldByNumber(2);
}
@Nullable
public Integer getHeight() {
return (Integer) getFieldByNumber(3);
}
@Nullable
public Integer getWeight() {
return (Integer) getFieldByNumber(4);
}
@Nullable
public Language getLanguage() {
return (Language) getFieldByNumber(5);
}
@Nullable
public Type getElevSetting() {
return (Type) getFieldByNumber(6);
}
@Nullable
public Type getWeightSetting() {
return (Type) getFieldByNumber(7);
}
@Nullable
public Integer getRestingHeartRate() {
return (Integer) getFieldByNumber(8);
}
@Nullable
public Integer getDefaultMaxBikingHeartRate() {
return (Integer) getFieldByNumber(10);
}
@Nullable
public Integer getDefaultMaxHeartRate() {
return (Integer) getFieldByNumber(11);
}
@Nullable
public Integer getHrSetting() {
return (Integer) getFieldByNumber(12);
}
@Nullable
public Type getSpeedSetting() {
return (Type) getFieldByNumber(13);
}
@Nullable
public Type getDistSetting() {
return (Type) getFieldByNumber(14);
}
@Nullable
public Integer getPowerSetting() {
return (Integer) getFieldByNumber(16);
}
@Nullable
public Integer getActivityClass() {
return (Integer) getFieldByNumber(17);
}
@Nullable
public Integer getPositionSetting() {
return (Integer) getFieldByNumber(18);
}
@Nullable
public Type getTemperatureSetting() {
return (Type) getFieldByNumber(21);
}
@Nullable
public Long getWakeTime() {
return (Long) getFieldByNumber(28);
}
@Nullable
public Long getSleepTime() {
return (Long) getFieldByNumber(29);
}
@Nullable
public Type getHeightSetting() {
return (Type) getFieldByNumber(30);
}
@Nullable
public Integer getUserRunningStepLength() {
return (Integer) getFieldByNumber(31);
}
@Nullable
public Integer getUserWalkingStepLength() {
return (Integer) getFieldByNumber(32);
}
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitWatchfaceSettings extends RecordData {
public FitWatchfaceSettings(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 159) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 159 + ", got " + globalNumber);
}
}
@Nullable
public Integer getMode() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getLayout() {
return (Integer) getFieldByNumber(1);
}
}

View File

@ -0,0 +1,120 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherCondition.Condition;
import org.threeten.bp.DayOfWeek;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitWeather extends RecordData {
public FitWeather(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 128) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 128 + ", got " + globalNumber);
}
}
@Nullable
public Integer getWeatherReport() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getTemperature() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Condition getCondition() {
return (Condition) getFieldByNumber(2);
}
@Nullable
public Integer getWindDirection() {
return (Integer) getFieldByNumber(3);
}
@Nullable
public Integer getWindSpeed() {
return (Integer) getFieldByNumber(4);
}
@Nullable
public Integer getPrecipitationProbability() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Integer getTemperatureFeelsLike() {
return (Integer) getFieldByNumber(6);
}
@Nullable
public Integer getRelativeHumidity() {
return (Integer) getFieldByNumber(7);
}
@Nullable
public String getLocation() {
return (String) getFieldByNumber(8);
}
@Nullable
public Long getObservedAtTime() {
return (Long) getFieldByNumber(9);
}
@Nullable
public Long getObservedLocationLat() {
return (Long) getFieldByNumber(10);
}
@Nullable
public Long getObservedLocationLong() {
return (Long) getFieldByNumber(11);
}
@Nullable
public DayOfWeek getDayOfWeek() {
return (DayOfWeek) getFieldByNumber(12);
}
@Nullable
public Integer getHighTemperature() {
return (Integer) getFieldByNumber(13);
}
@Nullable
public Integer getLowTemperature() {
return (Integer) getFieldByNumber(14);
}
@Nullable
public Integer getDewPoint() {
return (Integer) getFieldByNumber(15);
}
@Nullable
public Float getUvIndex() {
return (Float) getFieldByNumber(16);
}
@Nullable
public Integer getAirQuality() {
return (Integer) getFieldByNumber(17);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}

View File

@ -0,0 +1,47 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitZonesTarget extends RecordData {
public FitZonesTarget(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 7) {
throw new IllegalArgumentException("FitFileId expects global messages of " + 7 + ", got " + globalNumber);
}
}
@Nullable
public Integer getFunctionalThresholdPower() {
return (Integer) getFieldByNumber(3);
}
@Nullable
public Integer getMaxHeartRate() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Integer getThresholdHeartRate() {
return (Integer) getFieldByNumber(2);
}
@Nullable
public Integer getHrCalcType() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public Integer getPwrCalcType() {
return (Integer) getFieldByNumber(7);
}
}

View File

@ -24,17 +24,20 @@ public class FitDataMessage extends GFDIMessage {
final List<RecordData> recordDataList = new ArrayList<>();
while (reader.remaining() > 0) {
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte(), true);
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte());
if (recordHeader.isDefinition())
return null;
PredefinedLocalMessage predefinedLocalMessage = recordHeader.getPredefinedLocalMessage();
PredefinedLocalMessage predefinedLocalMessage = PredefinedLocalMessage.fromType(recordHeader.getLocalMessageType());
if (predefinedLocalMessage == null) {
LOG.warn("Local message is null");
return null;
}
RecordData recordData = new RecordData(predefinedLocalMessage.getRecordDefinition());
recordData.parseDataMessage(reader);
RecordData recordData = new RecordData(
predefinedLocalMessage.getRecordDefinition(),
predefinedLocalMessage.getRecordDefinition().getRecordHeader()
);
recordData.parseDataMessage(reader, null);
recordDataList.add(recordData);
}

View File

@ -25,7 +25,7 @@ public class FitDefinitionMessage extends GFDIMessage {
List<RecordDefinition> recordDefinitions = new ArrayList<>();
while (reader.remaining() > 0) {
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte(), true);
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte());
recordDefinitions.add(RecordDefinition.parseIncoming(reader, recordHeader));
}

View File

@ -0,0 +1,50 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class GBToStringBuilder extends ToStringBuilder {
public static final GBToStringStyle STYLE = new GBToStringStyle();
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
public GBToStringBuilder(final Object object) {
super(object, STYLE);
}
public static class GBToStringStyle extends ToStringStyle {
public GBToStringStyle() {
super();
this.setUseShortClassName(true);
this.setUseIdentityHashCode(false);
this.setContentStart("{");
this.setContentEnd("}");
this.setArrayStart("[");
this.setArrayEnd("]");
this.setFieldSeparator(", ");
this.setFieldNameValueSeparator("=");
this.setNullText("null");
}
@Override
public void append(final StringBuffer buffer, final String fieldName, final Object value, final Boolean fullDetail) {
// omit nulls
if (value != null) {
if (value instanceof Date) {
super.append(buffer, fieldName, SDF.format(value), fullDetail);
} else {
super.append(buffer, fieldName, value, fullDetail);
}
}
}
}
}

View File

@ -112,7 +112,7 @@ public class GarminSupportTest {
@Test
public void testBaseFields() {
RecordDefinition recordDefinition = new RecordDefinition(ByteOrder.LITTLE_ENDIAN, new RecordHeader((byte) 6), GlobalFITMessage.WEATHER, null); //just some random data
RecordDefinition recordDefinition = new RecordDefinition(new RecordHeader((byte) 6), ByteOrder.LITTLE_ENDIAN, GlobalFITMessage.WEATHER, null, null); //just some random data
List<FieldDefinition> fieldDefinitionList = new ArrayList<>();
for (BaseType baseType :
BaseType.values()) {
@ -121,7 +121,7 @@ public class GarminSupportTest {
}
recordDefinition.setFieldDefinitions(fieldDefinitionList);
RecordData test = new RecordData(recordDefinition);
RecordData test = new RecordData(recordDefinition, recordDefinition.getRecordHeader());
for (BaseType baseType :
BaseType.values()) {
@ -421,35 +421,24 @@ public class GarminSupportTest {
"020b00004b00007f00090309070001000401000501000601000701000801" +
"000901000a01000b45646765203531300000ffffffffffffff09ef");//https://github.com/polyvertex/fitdecode/blob/48b6554d8a3baf33f8b5b9b2fd079fcbe9ac8ce2/tests/files/Settings2.fit
String expectedOutput = "{\n" +
"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: raw: 6 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: raw: 9 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: raw: 10 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 ]}";
String expectedOutput = "[" +
"FitFileId{serial_number=3889965805, manufacturer=1, product=1561, type=settings}, " +
"FitFileCreator{software_version=340}, " +
"FitDeviceSettings{utc_offset=0, time_offset=0, active_time_zone=0, unknown_3(ENUM/1)=0, time_mode=0, time_zone_offset=0, unknown_10(ENUM/1)=3, unknown_11(ENUM/1)=0, backlight_mode=2, unknown_13(UINT8/1)=0, unknown_14(UINT8/1)=0, unknown_15(UINT8/1)=50, 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}, " +
"FitUserProfile{friendly_name=edge510, weight=78, gender=1, age=41, height=183, language=english, elev_setting=metric, weight_setting=metric, resting_heart_rate=60, default_max_biking_heart_rate=185, default_max_heart_rate=185, hr_setting=1, speed_setting=metric, dist_setting=metric, power_setting=1, activity_class=168, position_setting=2, temperature_setting=metric}, " +
"RecordData{UNK_4, unknown_254(UINT16/2)=0, unknown_1(UINT16Z/2)=50008, unknown_0(UINT8/1)=1, unknown_3(UINT8Z/1)=1}, " +
"RecordData{UNK_6, 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_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_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_24(UINT8Z/1)=5, unknown_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_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_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_24(UINT8Z/1)=5, unknown_35(UINT8/3)=[0,118,190], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_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_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_24(UINT8Z/1)=5, unknown_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_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_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_24(UINT8Z/1)=5, unknown_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_8(UINT16/2)=2096, unknown_9(UINT16/2)=0, unknown_10(UINT16/2)=95, unknown_11(UINT16/2)=500, 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_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_8(UINT16/2)=2096, unknown_9(UINT16/2)=0, unknown_10(UINT16/2)=95, unknown_11(UINT16/2)=500, 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_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_8(UINT16/2)=2096, unknown_9(UINT16/2)=0, unknown_10(UINT16/2)=95, unknown_11(UINT16/2)=500, 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_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_8(UINT16/2)=2096, unknown_9(UINT16/2)=0, unknown_10(UINT16/2)=95, unknown_11(UINT16/2)=500, 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_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_8(UINT16/2)=2096, unknown_9(UINT16/2)=0, unknown_10(UINT16/2)=95, unknown_11(UINT16/2)=500, 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_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"RecordData{UNK_6, 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_8(UINT16/2)=2096, unknown_9(UINT16/2)=0, unknown_10(UINT16/2)=95, unknown_11(UINT16/2)=500, 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_35(UINT8/3)=[0,50,], unknown_36(ENUM/1)=4, unknown_38(UINT8Z/1)=2, unknown_40(UINT8Z/1)=11, unknown_44(ENUM/1)=0}, " +
"FitConnectivity{name=Edge 510, bluetooth_enabled=0}" +
"]";
FitFile fitFile = FitFile.parseIncoming(fileContents);
Assert.assertEquals(expectedOutput, fitFile.toString());
@ -460,17 +449,14 @@ public class GarminSupportTest {
public void TestFitFileDevelopersField() {
byte[] fileContents = GB.hexStringToByteArray("0e206806a20000002e464954bed040000100000401028400010002028403048c00000f042329000006a540000100cf0201100d030102000101020305080d1522375990e97962db0040000100ce05000102010102020102031107080a0700000001646f7567686e7574735f6561726e656400646f7567686e7574730060000100140403010204010205048606028401000100008c580000c738b98001008f5a00032c808e400200905c0005a9388a1003d39e");//https://github.com/polyvertex/fitdecode/blob/48b6554d8a3baf33f8b5b9b2fd079fcbe9ac8ce2/tests/files/DeveloperData.fit
String expectedOutput = "{\n" +
"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 ]}";
String expectedOutput = "[" +
"FitFileId{manufacturer=15, type=activity, product=9001, serial_number=1701}, " +
"FitDeveloperData{application_id=[1,1,2,3,5,8,13,21,34,55,89,144,233,121,98,219], developer_data_index=0}, " +
"FitFieldDescription{developer_data_index=0, field_definition_number=0, fit_base_type_id=1, field_name=doughnuts_earned, units=doughnuts}, " +
"FitRecord{heart_rate=140, unknown_4(UINT8/1)=88, unknown_5(UINT32/4)=51000, unknown_6(UINT16/2)=47488, doughnuts_earned=1}, " +
"FitRecord{heart_rate=143, unknown_4(UINT8/1)=90, unknown_5(UINT32/4)=208000, unknown_6(UINT16/2)=36416, doughnuts_earned=2}, " +
"FitRecord{heart_rate=144, unknown_4(UINT8/1)=92, unknown_5(UINT32/4)=371000, unknown_6(UINT16/2)=35344, doughnuts_earned=3}" +
"]";
FitFile fitFile = FitFile.parseIncoming(fileContents);
Assert.assertEquals(expectedOutput, fitFile.toString());