diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java index 652a810d8..a14312d18 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/instinct2s/GarminInstinct2SCoordinator.java @@ -44,4 +44,10 @@ public class GarminInstinct2SCoordinator extends AbstractBLEDeviceCoordinator { public boolean supportsFindDevice() { return true; } + + @Override + public boolean supportsWeather() { + return true; + } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivomove/GarminVivomoveStyleCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivomove/GarminVivomoveStyleCoordinator.java index a7f6db6e0..fb9881e73 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivomove/GarminVivomoveStyleCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/vivomove/GarminVivomoveStyleCoordinator.java @@ -45,4 +45,11 @@ public class GarminVivomoveStyleCoordinator extends AbstractBLEDeviceCoordinator return true; } + + @Override + public boolean supportsWeather() { + return true; + } + + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index 0b82d52c6..f73d5bd70 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -7,8 +7,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; @@ -17,6 +19,7 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto; @@ -26,6 +29,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateA import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.ICommunicator; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v2.CommunicatorV2; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitWeatherConditions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalDefinitionsEnum; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ConfigurationMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MusicControlEntityUpdateMessage; @@ -129,11 +135,93 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } } + @Override + public void onSendWeather(final ArrayList weatherSpecs) { + sendWeatherConditions(weatherSpecs.get(0)); + } + + private void sendWeatherConditions(WeatherSpec weather) { + List weatherData = new ArrayList<>(); + + try { + + RecordData today = new RecordData(GlobalDefinitionsEnum.TODAY_WEATHER_CONDITIONS.getRecordDefinition()); + today.setFieldByName("weather_report", 0); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast + today.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp)); + today.setFieldByName("observed_at_time", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp)); + today.setFieldByName("temperature", weather.currentTemp - 273.15); + today.setFieldByName("low_temperature", weather.todayMinTemp - 273.15); + today.setFieldByName("high_temperature", weather.todayMaxTemp - 273.15); + today.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(weather.currentConditionCode)); + today.setFieldByName("wind_direction", weather.windDirection); + today.setFieldByName("precipitation_probability", weather.precipProbability); + today.setFieldByName("wind_speed", Math.round(weather.windSpeed)); + today.setFieldByName("temperature_feels_like", weather.feelsLikeTemp - 273.15); + today.setFieldByName("relative_humidity", weather.currentHumidity); + today.setFieldByName("observed_location_lat", weather.latitude); + today.setFieldByName("observed_location_long", weather.longitude); + today.setFieldByName("location", weather.location); + weatherData.add(today); + + for (int hour = 0; hour <= 11; hour++) { + if (hour < weather.hourly.size()) { + WeatherSpec.Hourly hourly = weather.hourly.get(hour); + RecordData weatherHourlyForecast = new RecordData(GlobalDefinitionsEnum.HOURLY_WEATHER_FORECAST.getRecordDefinition()); + weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast + weatherHourlyForecast.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(hourly.timestamp)); + weatherHourlyForecast.setFieldByName("temperature", hourly.temp - 273.15); + weatherHourlyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(hourly.conditionCode)); + weatherHourlyForecast.setFieldByName("wind_direction", hourly.windDirection); + weatherHourlyForecast.setFieldByName("wind_speed", Math.round(hourly.windSpeed)); + weatherHourlyForecast.setFieldByName("precipitation_probability", hourly.precipProbability); + weatherHourlyForecast.setFieldByName("relative_humidity", hourly.humidity); +// weatherHourlyForecast.setFieldByName("dew_point", 0); // dew_point sint8 + weatherHourlyForecast.setFieldByName("uv_index", hourly.uvIndex); +// weatherHourlyForecast.setFieldByName("air_quality", 0); // air_quality enum + weatherData.add(weatherHourlyForecast); + } + } +// + RecordData todayDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition()); + todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast + todayDailyForecast.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp)); + todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp - 273.15); + todayDailyForecast.setFieldByName("high_temperature", weather.todayMaxTemp - 273.15); + todayDailyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(weather.currentConditionCode)); + todayDailyForecast.setFieldByName("precipitation_probability", weather.precipProbability); + todayDailyForecast.setFieldByName("day_of_week", GarminTimeUtils.unixTimeToGarminDayOfWeek(weather.timestamp)); + weatherData.add(todayDailyForecast); + + + for (int day = 0; day < 4; day++) { + if (day < weather.forecasts.size()) { + WeatherSpec.Daily daily = weather.forecasts.get(day); + int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; //TODO: is this needed? + RecordData weatherDailyForecast = new RecordData(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition()); + weatherDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast + weatherDailyForecast.setFieldByName("timestamp", GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp)); + weatherDailyForecast.setFieldByName("low_temperature", daily.minTemp - 273.15); + weatherDailyForecast.setFieldByName("high_temperature", daily.maxTemp - 273.15); + weatherDailyForecast.setFieldByName("condition", FitWeatherConditions.openWeatherCodeToFitWeatherStatus(daily.conditionCode)); + weatherDailyForecast.setFieldByName("precipitation_probability", daily.precipProbability); + weatherDailyForecast.setFieldByName("day_of_week", GarminTimeUtils.unixTimeToGarminDayOfWeek(ts)); + weatherData.add(weatherDailyForecast); + } + } + + byte[] message = new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDataMessage(weatherData).getOutgoingMessage(); + communicator.sendMessage(message); + } catch (Exception e) { + LOG.error(e.getMessage()); + } + + } + private void completeInitialization() { - enableWeather(); onSetTime(); + enableWeather(); //following is needed for vivomove style communicator.sendMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_READY, 0).getOutgoingMessage()); @@ -158,9 +246,8 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } private void enableWeather() { - final Map settings = new LinkedHashMap<>(2); + final Map settings = new LinkedHashMap<>(1); settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true); - settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_ALERTS_ENABLED, true); communicator.sendMessage(new SetDeviceSettingsMessage(settings).getOutgoingMessage()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminTimeUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminTimeUtils.java new file mode 100644 index 000000000..bc08437cd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminTimeUtils.java @@ -0,0 +1,29 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin; + +import org.threeten.bp.Instant; +import org.threeten.bp.ZoneId; + +public class GarminTimeUtils { + + private static final int GARMIN_TIME_EPOCH = 631065600; + + public static int unixTimeToGarminTimestamp(int unixTime) { + return unixTime - GARMIN_TIME_EPOCH; + } + + public static int javaMillisToGarminTimestamp(long millis) { + return (int) (millis / 1000) - GARMIN_TIME_EPOCH; + } + + public static long garminTimestampToJavaMillis(int timestamp) { + return (timestamp + GARMIN_TIME_EPOCH) * 1000L; + } + + public static int garminTimestampToUnixTime(int timestamp) { + return timestamp + GARMIN_TIME_EPOCH; + } + + public static int unixTimeToGarminDayOfWeek(int unixTime) { + return (Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).getDayOfWeek().getValue() % 7); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java new file mode 100644 index 000000000..44b30556c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java @@ -0,0 +1,56 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; + +public class DevFieldDefinition { + public final ByteBuffer valueHolder; + private final int localNumber; + private final int size; + private final int developerDataIndex; + private final String name; + + public DevFieldDefinition(int localNumber, int size, int developerDataIndex, String name) { + this.localNumber = localNumber; + this.size = size; + this.developerDataIndex = developerDataIndex; + this.name = name; + this.valueHolder = ByteBuffer.allocate(size); + } + + public static DevFieldDefinition parseIncoming(MessageReader reader) { + int number = reader.readByte(); + int size = reader.readByte(); + int developerDataIndex = reader.readByte(); + + return new DevFieldDefinition(number, size, developerDataIndex, ""); + + } + + public int getLocalNumber() { + return localNumber; + } + + public int getSize() { + return size; + } + + public String getName() { + return name; + } + + public void generateOutgoingPayload(MessageWriter writer) { //TODO + } + + public Object decode() { //TODO + return null; + } + + + public void encode(Object o) { //TODO + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java new file mode 100644 index 000000000..6c64238a6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java @@ -0,0 +1,73 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; + +public class FieldDefinition { + private final int localNumber; + private final int size; + private final BaseType baseType; + private final String name; + private final int scale; + private final int offset; + + public FieldDefinition(int localNumber, int size, BaseType baseType, String name, int scale, int offset) { + this.localNumber = localNumber; + this.size = size; + this.baseType = baseType; + this.name = name; + this.scale = scale; + this.offset = offset; + } + + public FieldDefinition(int localNumber, int size, BaseType baseType, String name) { + this(localNumber, size, baseType, name, 1, 0); + } + + public static FieldDefinition parseIncoming(MessageReader reader) { + int localNumber = reader.readByte(); + int size = reader.readByte(); + int baseTypeIdentifier = reader.readByte(); + + BaseType baseType = BaseType.fromIdentifier(baseTypeIdentifier); + + if (size % baseType.getSize() != 0) { + baseType = BaseType.BASE_TYPE_BYTE; + } + + return new FieldDefinition(localNumber, size, baseType, ""); + + } + + public int getScale() { + return scale; + } + + public int getOffset() { + return offset; + } + + public int getLocalNumber() { + return localNumber; + } + + public int getSize() { + return size; + } + + public BaseType getBaseType() { + return baseType; + } + + public String getName() { + return name; + } + + public void generateOutgoingPayload(MessageWriter writer) { + writer.writeByte(localNumber); + writer.writeByte(size); + writer.writeByte(baseType.getIdentifier()); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitWeatherConditions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitWeatherConditions.java new file mode 100644 index 000000000..403bbcf52 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitWeatherConditions.java @@ -0,0 +1,141 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +public final class FitWeatherConditions { + public static final int CLEAR = 0; + public static final int PARTLY_CLOUDY = 1; + public static final int MOSTLY_CLOUDY = 2; + public static final int RAIN = 3; + public static final int SNOW = 4; + public static final int WINDY = 5; + public static final int THUNDERSTORMS = 6; + public static final int WINTRY_MIX = 7; + public static final int FOG = 8; + public static final int HAZY = 11; + public static final int HAIL = 12; + public static final int SCATTERED_SHOWERS = 13; + public static final int SCATTERED_THUNDERSTORMS = 14; + public static final int UNKNOWN_PRECIPITATION = 15; + public static final int LIGHT_RAIN = 16; + public static final int HEAVY_RAIN = 17; + public static final int LIGHT_SNOW = 18; + public static final int HEAVY_SNOW = 19; + public static final int LIGHT_RAIN_SNOW = 20; + public static final int HEAVY_RAIN_SNOW = 21; + public static final int CLOUDY = 22; + + public static int openWeatherCodeToFitWeatherStatus(int openWeatherCode) { + switch (openWeatherCode) { +//Group 2xx: Thunderstorm + case 200: //thunderstorm with light rain: //11d + case 201: //thunderstorm with rain: //11d + case 202: //thunderstorm with heavy rain: //11d + case 210: //light thunderstorm:: //11d + case 211: //thunderstorm: //11d + case 212: //heavy thunderstorm: //11d + case 230: //thunderstorm with light drizzle: //11d + case 231: //thunderstorm with drizzle: //11d + case 232: //thunderstorm with heavy drizzle: //11d + return THUNDERSTORMS; + case 221: //ragged thunderstorm: //11d + return SCATTERED_THUNDERSTORMS; +//Group 3xx: Drizzle + case 300: //light intensity drizzle: //09d + case 310: //light intensity drizzle rain: //09d + case 313: //shower rain and drizzle: //09d + return LIGHT_RAIN; + case 301: //drizzle: //09d + case 311: //drizzle rain: //09d + return RAIN; + case 302: //heavy intensity drizzle: //09d + case 312: //heavy intensity drizzle rain: //09d + case 314: //heavy shower rain and drizzle: //09d + return HEAVY_RAIN; + case 321: //shower drizzle: //09d + return SCATTERED_SHOWERS; +//Group 5xx: Rain + case 500: //light rain: //10d + case 520: //light intensity shower rain: //09d + case 521: //shower rain: //09d + return LIGHT_RAIN; + case 501: //moderate rain: //10d + case 531: //ragged shower rain: //09d + return RAIN; + case 502: //heavy intensity rain: //10d + case 503: //very heavy rain: //10d + case 504: //extreme rain: //10d + case 522: //heavy intensity shower rain: //09d + return HEAVY_RAIN; + case 511: //freezing rain: //13d + return UNKNOWN_PRECIPITATION; +//Group 6xx: Snow + case 600: //light snow: //[[file:13d.png]] + return LIGHT_SNOW; + case 601: //snow: //[[file:13d.png]] + case 620: //light shower snow: //[[file:13d.png]] + case 621: //shower snow: //[[file:13d.png]] + return SNOW; + case 602: //heavy snow: //[[file:13d.png]] + case 622: //heavy shower snow: //[[file:13d.png]] + return HEAVY_SNOW; + case 611: //sleet: //[[file:13d.png]] + case 612: //light shower sleet: //[[file:13d.png]] + case 613: //shower sleet: //[[file:13d.png]] + return WINTRY_MIX; + case 615: //light rain and snow: //[[file:13d.png]] + return LIGHT_RAIN_SNOW; + case 616: //rain and snow: //[[file:13d.png]] + return HEAVY_RAIN_SNOW; + +//Group 7xx: Atmosphere + case 701: //mist: //[[file:50d.png]] + case 711: //smoke: //[[file:50d.png]] + case 721: //haze: //[[file:50d.png]] + case 731: //sandcase dust whirls: //[[file:50d.png]] + case 751: //sand: //[[file:50d.png]] + case 761: //dust: //[[file:50d.png]] + case 762: //volcanic ash: //[[file:50d.png]] + return HAZY; + case 741: //fog: //[[file:50d.png]] + return FOG; + case 771: //squalls: //[[file:50d.png]] + case 781: //tornado: //[[file:50d.png]] + return WINDY; +//Group 800: Clear + case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]] + return CLEAR; + +//Group 80x: Clouds + case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]] + case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]] + return PARTLY_CLOUDY; + case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]] + return MOSTLY_CLOUDY; + case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]] + return CLOUDY; +//Group 90x: Extreme + case 901: //tropical storm + return THUNDERSTORMS; + case 906: //hail + return HAIL; + case 903: //cold + case 904: //hot + case 905: //windy +//Group 9xx: Additional + case 951: //calm + case 952: //light breeze + case 953: //gentle breeze + case 954: //moderate breeze + case 955: //fresh breeze + case 956: //strong breeze + case 957: //high windcase near gale + case 958: //gale + case 959: //severe gale + case 960: //storm + case 961: //violent storm + case 902: //hurricane + case 962: //hurricane + default: + throw new IllegalArgumentException("Unknown weather code " + openWeatherCode); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java new file mode 100644 index 000000000..2fb9c0f32 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java @@ -0,0 +1,87 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import androidx.annotation.Nullable; + +import java.nio.ByteOrder; +import java.util.Arrays; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public enum GlobalDefinitionsEnum { + TODAY_WEATHER_CONDITIONS(MesgType.TODAY_WEATHER_CONDITIONS, new RecordDefinition( + new RecordHeader(true, false, MesgType.TODAY_WEATHER_CONDITIONS, null), + ByteOrder.BIG_ENDIAN, + MesgType.TODAY_WEATHER_CONDITIONS, + Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"), + new FieldDefinition(253, 4, BaseType.UINT32, "timestamp"), + new FieldDefinition(9, 4, BaseType.UINT32, "observed_at_time"), + new FieldDefinition(1, 1, BaseType.SINT8, "temperature"), + new FieldDefinition(14, 1, BaseType.SINT8, "low_temperature"), + new FieldDefinition(13, 1, BaseType.SINT8, "high_temperature"), + new FieldDefinition(2, 1, BaseType.ENUM, "condition"), + new FieldDefinition(3, 2, BaseType.UINT16, "wind_direction"), + new FieldDefinition(5, 1, BaseType.UINT8, "precipitation_probability"), + new FieldDefinition(4, 2, BaseType.UINT16, "wind_speed", 298, 0), + new FieldDefinition(6, 1, BaseType.SINT8, "temperature_feels_like"), + new FieldDefinition(7, 1, BaseType.UINT8, "relative_humidity"), + new FieldDefinition(10, 4, BaseType.SINT32, "observed_location_lat"), + new FieldDefinition(11, 4, BaseType.SINT32, "observed_location_long"), + new FieldDefinition(8, 15, BaseType.STRING, "location")))), + + HOURLY_WEATHER_FORECAST(MesgType.HOURLY_WEATHER_FORECAST, new RecordDefinition( + new RecordHeader(true, false, MesgType.HOURLY_WEATHER_FORECAST, null), + ByteOrder.BIG_ENDIAN, + MesgType.HOURLY_WEATHER_FORECAST, + Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"), + new FieldDefinition(253, 4, BaseType.UINT32, "timestamp"), + new FieldDefinition(1, 1, BaseType.SINT8, "temperature"), + new FieldDefinition(2, 1, BaseType.ENUM, "condition"), + new FieldDefinition(3, 2, BaseType.UINT16, "wind_direction"), + new FieldDefinition(4, 2, BaseType.UINT16, "wind_speed", 298, 0), + new FieldDefinition(5, 1, BaseType.UINT8, "precipitation_probability"), + new FieldDefinition(7, 1, BaseType.UINT8, "relative_humidity"), + new FieldDefinition(15, 1, BaseType.SINT8, "dew_point"), + new FieldDefinition(16, 4, BaseType.FLOAT32, "uv_index"), + new FieldDefinition(17, 1, BaseType.ENUM, "air_quality")))), + + DAILY_WEATHER_FORECAST(MesgType.DAILY_WEATHER_FORECAST, new RecordDefinition( + new RecordHeader(true, false, MesgType.DAILY_WEATHER_FORECAST, null), + ByteOrder.BIG_ENDIAN, + MesgType.DAILY_WEATHER_FORECAST, + Arrays.asList(new FieldDefinition(0, 1, BaseType.ENUM, "weather_report"), + new FieldDefinition(253, 4, BaseType.UINT32, "timestamp"), + new FieldDefinition(14, 1, BaseType.SINT8, "low_temperature"), + new FieldDefinition(13, 1, BaseType.SINT8, "high_temperature"), + new FieldDefinition(2, 1, BaseType.ENUM, "condition"), + new FieldDefinition(5, 1, BaseType.UINT8, "precipitation_probability"), + new FieldDefinition(12, 1, BaseType.ENUM, "day_of_week")))), + ; + + private final MesgType mesgType; + private final RecordDefinition recordDefinition; + + GlobalDefinitionsEnum(MesgType mesgType, RecordDefinition recordDefinition) { + this.mesgType = mesgType; + this.recordDefinition = recordDefinition; + } + + @Nullable + public static RecordDefinition getRecordDefinitionfromMesgType(final MesgType code) { + for (final GlobalDefinitionsEnum globalDefinitionsEnum : GlobalDefinitionsEnum.values()) { + if (globalDefinitionsEnum.getMesgType().getIdentifier() == code.getIdentifier()) { + return globalDefinitionsEnum.getRecordDefinition(); + } + } + return null; + } + + public MesgType getMesgType() { + return mesgType; + } + + public RecordDefinition getRecordDefinition() { + return recordDefinition; + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java new file mode 100644 index 000000000..ae894f71e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java @@ -0,0 +1,32 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +public enum MesgType { + TODAY_WEATHER_CONDITIONS(6, 128), + HOURLY_WEATHER_FORECAST(9, 128), + DAILY_WEATHER_FORECAST(10, 128); + + private final int identifier; + private final int globalMesgNum; + + MesgType(int id, int globalMesgNum) { + this.identifier = id; + this.globalMesgNum = globalMesgNum; + } + + public static MesgType fromIdentifier(int identifier) { + for (final MesgType mesgType : MesgType.values()) { + if (mesgType.getIdentifier() == identifier) { + return mesgType; + } + } + throw new IllegalArgumentException("Unknown type " + identifier); //TODO: perhaps we need to handle unknown message types + } + + public int getIdentifier() { + return identifier; + } + + public int getGlobalMesgNum() { + return globalMesgNum; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java new file mode 100644 index 000000000..d1aa0f2c6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java @@ -0,0 +1,186 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; + +import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType.STRING; + +public class RecordData { + + private final RecordHeader recordHeader; + protected ByteBuffer valueHolder; + private List fieldDataList; + + public RecordData(RecordDefinition recordDefinition) { + fieldDataList = new ArrayList<>(); + + this.recordHeader = recordDefinition.getRecordHeader(); + + int totalSize = 0; + + + for (FieldDefinition fieldDef : + recordDefinition.getFieldDefinitions()) { + fieldDataList.add(new FieldData(fieldDef.getBaseType(), totalSize, fieldDef.getSize(), fieldDef.getName(), fieldDef.getLocalNumber(), fieldDef.getScale(), fieldDef.getOffset())); + totalSize += fieldDef.getSize(); + + } + + this.valueHolder = ByteBuffer.allocate(totalSize); + valueHolder.order(recordDefinition.getByteOrder()); + + for (FieldData fieldData : + fieldDataList) { + fieldData.invalidate(); + } + + } + + public void parseDataMessage(MessageReader reader) { + reader.setByteOrder(valueHolder.order()); + for (FieldData fieldData : fieldDataList) { + fieldData.parseDataMessage(reader); + } + } + + public void generateOutgoingDataPayload(MessageWriter writer) { + writer.writeByte(recordHeader.generateOutgoingDataPayload()); + writer.writeBytes(valueHolder.array()); + } + + public void setFieldByNumber(int number, Object... value) { + boolean found = false; + for (FieldData fieldData : + fieldDataList) { + if (fieldData.getNumber() == number) { + fieldData.encode(value); + found = true; + break; + } + } + if (!found) { + throw new IllegalArgumentException("Unknown field number " + number); + } + } + + public void setFieldByName(String name, Object... value) { + boolean found = false; + for (FieldData fieldData : + fieldDataList) { + if (fieldData.getName().equals(name)) { + fieldData.encode(value); + found = true; + break; + } + } + if (!found) { + throw new IllegalArgumentException("Unknown field name " + name); + } + } + + public String toString() { + StringBuilder oBuilder = new StringBuilder(); + for (FieldData fieldData : + fieldDataList) { + if (fieldData.getName() != null) { + oBuilder.append(fieldData.getName()); + } else { + oBuilder.append(fieldData.getNumber()); + } + oBuilder.append(": "); + oBuilder.append(fieldData.decode()); + oBuilder.append(" "); + } + + return oBuilder.toString(); + } + + + private class FieldData { + private final BaseType baseType; + private final int position; + private final int size; + private final String name; + private final int scale; + private final int offset; + private final int number; + public FieldData(BaseType baseType, int position, int size, String name, int number, int scale, int offset) { + this.baseType = baseType; + this.position = position; + this.size = size; + this.name = name; + this.number = number; + this.scale = scale; + this.offset = offset; + } + + public BaseType getBaseType() { + return baseType; + } + + public String getName() { + return name; + } + + public int getNumber() { + return number; + } + + public void invalidate() { + goToPosition(); + if (STRING.equals(getBaseType())) { + for (int i = 0; i < size; i++) { + valueHolder.put((byte) 0); + } + return; + } + baseType.invalidate(valueHolder); + } + + private void goToPosition() { + valueHolder.position(position); + } + + public void parseDataMessage(MessageReader reader) { + goToPosition(); + valueHolder.put(reader.readBytes(size)); + } + + public void encode(Object... objects) { + if (objects.length > 1) + throw new IllegalArgumentException("Array of values not supported yet"); //TODO: handle arrays + Object o = objects[0]; + goToPosition(); + if (STRING.equals(getBaseType())) { + final byte[] bytes = ((String) o).getBytes(StandardCharsets.UTF_8); + valueHolder.put(Arrays.copyOf(bytes, Math.min(this.size - 1, bytes.length))); + valueHolder.put((byte) 0); + return; + } + getBaseType().encode(valueHolder, o, scale, offset); + } + + public Object decode() { + goToPosition(); + if (STRING.equals(getBaseType())) { + final byte[] bytes = new byte[size]; + valueHolder.get(bytes); + final int zero = ArrayUtils.indexOf((byte) 0, bytes); + if (zero < 0) { + return new String(bytes, StandardCharsets.UTF_8); + } + return new String(bytes, 0, zero, StandardCharsets.UTF_8); + } + //TODO: handle arrays + return getBaseType().decode(valueHolder, scale, offset); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java new file mode 100644 index 000000000..70f5f0bdd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java @@ -0,0 +1,108 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; + +public class RecordDefinition { + private final RecordHeader recordHeader; + private final int globalMesgNum; + private final java.nio.ByteOrder byteOrder; + private final MesgType mesgType; + private List fieldDefinitions; + private List devFieldDefinitions; + + + public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, int globalMesgNum, List fieldDefinitions, List devFieldDefinitions) { + this.recordHeader = recordHeader; + this.byteOrder = byteOrder; + this.mesgType = mesgType; + this.globalMesgNum = globalMesgNum; + this.fieldDefinitions = fieldDefinitions; + this.devFieldDefinitions = devFieldDefinitions; + } + + public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, List fieldDefinitions) { + this(recordHeader, byteOrder, mesgType, mesgType.getGlobalMesgNum(), fieldDefinitions, null); + } + + public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, int globalMesgNum) { + this(recordHeader, byteOrder, mesgType, globalMesgNum, null, null); + } + + public static RecordDefinition parseIncoming(MessageReader reader, RecordHeader recordHeader) { + if (!recordHeader.isDefinition()) + return null; + reader.readByte();//ignore + ByteOrder byteOrder = reader.readByte() == 0x01 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN; + reader.setByteOrder(byteOrder); + final int globalMesgNum = reader.readShort(); + + RecordDefinition definitionMessage = new RecordDefinition(recordHeader, byteOrder, recordHeader.getMesgType(), globalMesgNum); + + final int numFields = reader.readByte(); + List fieldDefinitions = new ArrayList<>(numFields); + for (int i = 0; i < numFields; i++) { + fieldDefinitions.add(FieldDefinition.parseIncoming(reader)); + } + + definitionMessage.setFieldDefinitions(fieldDefinitions); + + if (recordHeader.isDeveloperData()) { + final int numDevFields = reader.readByte(); + List devFieldDefinitions = new ArrayList<>(numDevFields); + for (int i = 0; i < numDevFields; i++) { + devFieldDefinitions.add(DevFieldDefinition.parseIncoming(reader)); + } + definitionMessage.setDevFieldDefinitions(devFieldDefinitions); + } + + reader.warnIfLeftover(); + + return definitionMessage; + } + + public ByteOrder getByteOrder() { + return byteOrder; + } + + public List getDevFieldDefinitions() { + return devFieldDefinitions; + } + + public void setDevFieldDefinitions(List devFieldDefinitions) { + this.devFieldDefinitions = devFieldDefinitions; + } + + public RecordHeader getRecordHeader() { + return recordHeader; + } + + public List getFieldDefinitions() { + return fieldDefinitions; + } + + public void setFieldDefinitions(List fieldDefinitions) { + this.fieldDefinitions = fieldDefinitions; + } + + public void generateOutgoingPayload(MessageWriter writer) { + writer.writeByte(recordHeader.generateOutgoingDefinitionPayload()); + writer.writeByte(0);//ignore + writer.writeByte(byteOrder == ByteOrder.LITTLE_ENDIAN ? 0 : 1); + writer.setByteOrder(byteOrder); + writer.writeShort(globalMesgNum); + writer.writeByte(fieldDefinitions.size()); + for (FieldDefinition fieldDefinition : fieldDefinitions) { + fieldDefinition.generateOutgoingPayload(writer); + } + } + + public String getName() { + return mesgType != null ? mesgType.name() : "unknown_" + globalMesgNum; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java new file mode 100644 index 000000000..5c0184b00 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java @@ -0,0 +1,64 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; + +public class RecordHeader { + private final boolean definition; + private final boolean developerData; + private final MesgType mesgType; + private final Integer timeOffset; + + public RecordHeader(boolean definition, boolean developerData, MesgType mesgType, Integer timeOffset) { + this.definition = definition; + this.developerData = developerData; + this.mesgType = mesgType; + this.timeOffset = timeOffset; + } + + //see https://github.com/polyvertex/fitdecode/blob/master/fitdecode/reader.py#L512 + public RecordHeader(byte header) { + if ((header & 0x80) == 0x80) { //compressed timestamp TODO add support + definition = false; + developerData = false; + mesgType = MesgType.fromIdentifier((header >> 5) & 0x3); + timeOffset = header & 0x1f; + } else { + definition = ((header & 0x40) == 0x40); + developerData = ((header & 0x20) == 0x20); + mesgType = MesgType.fromIdentifier(header & 0xf); + timeOffset = null; + } + } + + public boolean isDeveloperData() { + return developerData; + } + + public boolean isDefinition() { + return definition; + } + + public MesgType getMesgType() { + return mesgType; + } + + public byte generateOutgoingDefinitionPayload() { + if (!definition && !developerData) + return (byte) (timeOffset | (((byte) mesgType.getIdentifier()) << 5)); + byte base = (byte) mesgType.getIdentifier(); + if (definition) + base = (byte) (base | 0x40); + if (developerData) + base = (byte) (base | 0x20); + + return base; + } + + public byte generateOutgoingDataPayload() { //TODO: unclear if correct + if (!definition && !developerData) + return (byte) (timeOffset | (((byte) mesgType.getIdentifier()) << 5)); + byte base = (byte) mesgType.getIdentifier(); + if (developerData) + base = (byte) (base | 0x20); + + return base; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseType.java new file mode 100644 index 000000000..dbfbc1ba6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseType.java @@ -0,0 +1,62 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +//see https://github.com/dtcooper/python-fitparse/blob/master/fitparse/records.py +public enum BaseType { + ENUM(0x00, new BaseTypeByte(true, 0xFF)), + SINT8(0x01, new BaseTypeByte(false, 0xFF)), + UINT8(0x02, new BaseTypeByte(true, 0xFF)), + SINT16(0x83, new BaseTypeShort(false, 0x7FFF)), + UINT16(0x84, new BaseTypeShort(true, 0xFFFF)), + SINT32(0x85, new BaseTypeInt(false, 0x7FFFFFFF)), + UINT32(0x86, new BaseTypeInt(true, 0xFFFFFFFF)), + STRING(0x07, new BaseTypeByte(true, 0x00)), + FLOAT32(0x88, new BaseTypeFloat()), + FLOAT64(0x89, new BaseTypeDouble()), + UINT8Z(0x0A, new BaseTypeByte(true, 0x00)), + UINT16Z(0x8B, new BaseTypeShort(true, 0)), + UINT32Z(0x8C, new BaseTypeInt(true, 0)), + BASE_TYPE_BYTE(0x0D, new BaseTypeByte(true, 0xFF)), + SINT64(0x8E, new BaseTypeLong(false, 0x7FFFFFFFFFFFFFFFL)), + UINT64(0x8F, new BaseTypeLong(true, 0xFFFFFFFFFFFFFFFFL)), + UINT64Z(0x8F, new BaseTypeLong(true, 0)), + ; + + private final int identifier; + private final BaseTypeInterface baseTypeInterface; + + BaseType(int identifier, BaseTypeInterface byteBaseType) { + this.identifier = identifier; + this.baseTypeInterface = byteBaseType; + } + + public static BaseType fromIdentifier(int identifier) { + for (final BaseType status : BaseType.values()) { + if (status.getIdentifier() == identifier) { + return status; + } + } + throw new IllegalArgumentException("Unknown type " + identifier); + } + + public int getSize() { + return baseTypeInterface.getByteSize(); + } + + public int getIdentifier() { + return identifier; + } + + public Object decode(ByteBuffer byteBuffer, int scale, int offset) { + return baseTypeInterface.decode(byteBuffer, scale, offset); + } + + public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) { + baseTypeInterface.encode(byteBuffer, o, scale, offset); + } + + public void invalidate(ByteBuffer byteBuffer) { + baseTypeInterface.invalidate(byteBuffer); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeByte.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeByte.java new file mode 100644 index 000000000..3419a8801 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeByte.java @@ -0,0 +1,53 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +public class BaseTypeByte implements BaseTypeInterface { + + private final int min; + private final int max; + private final int invalid; + private final boolean unsigned; + private final int size = 1; + + BaseTypeByte(boolean unsigned, int invalid) { + if (unsigned) { + min = 0; + max = 0xff; + } else { + min = Byte.MIN_VALUE; + max = Byte.MAX_VALUE; + } + this.invalid = invalid; + this.unsigned = unsigned; + } + + + public int getByteSize() { + return size; + } + + @Override + public Object decode(final ByteBuffer byteBuffer, int scale, int offset) { + int i = (byteBuffer.get() + offset) / scale; + if (i < min || i > max) + return invalid; + return i; + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) { + int i = ((Number) o).intValue() * scale - offset; + if (!unsigned && (i < min || i > max)) { + byteBuffer.put((byte) invalid); + return; + } + byteBuffer.put((byte) i); + } + + @Override + public void invalidate(ByteBuffer byteBuffer) { + byteBuffer.put((byte) invalid); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeDouble.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeDouble.java new file mode 100644 index 000000000..f2abc6a66 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeDouble.java @@ -0,0 +1,40 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +public class BaseTypeDouble implements BaseTypeInterface { + private final int size = 8; + private final double min; + private final double max; + private final double invalid; + + BaseTypeDouble() { + this.min = Double.MIN_VALUE; + this.max = Double.MAX_VALUE; + this.invalid = Double.longBitsToDouble(0xFFFFFFFFFFFFFFFFL); + } + + public int getByteSize() { + return size; + } + + @Override + public Object decode(final ByteBuffer byteBuffer, int scale, int offset) { + return (byteBuffer.getDouble() + offset) / scale; + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) { + double d = ((Number) o).doubleValue() * scale - offset; + if (d < min || d > max) { + byteBuffer.putDouble(invalid); + return; + } + byteBuffer.putDouble(d); + } + + @Override + public void invalidate(ByteBuffer byteBuffer) { + byteBuffer.putDouble(invalid); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeFloat.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeFloat.java new file mode 100644 index 000000000..da9b0b1d9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeFloat.java @@ -0,0 +1,43 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +public class BaseTypeFloat implements BaseTypeInterface { + private final int size = 4; + private final double min; + private final double max; + private final double invalid; + private final boolean unsigned; + + BaseTypeFloat() { + this.min = -Float.MAX_VALUE; + this.max = Float.MAX_VALUE; + this.invalid = Float.intBitsToFloat(0xFFFFFFFF); + this.unsigned = false; + } + + public int getByteSize() { + return size; + } + + @Override + public Object decode(ByteBuffer byteBuffer, int scale, int offset) { + return (byteBuffer.getFloat() + offset) / scale; + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) { + float f = ((Number) o).floatValue() * scale - offset; + if (!unsigned && (f < min || f > max)) { + byteBuffer.putFloat((float) invalid); + return; + } + byteBuffer.putFloat((float) f); + } + + @Override + public void invalidate(ByteBuffer byteBuffer) { + byteBuffer.putFloat((float) invalid); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java new file mode 100644 index 000000000..0241c7d7f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java @@ -0,0 +1,57 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +public class BaseTypeInt implements BaseTypeInterface { + private final int min; + private final int max; + private final int invalid; + private final boolean unsigned; + private final int size = 4; + + BaseTypeInt(boolean unsigned, int invalid) { + if (unsigned) { + this.min = 0; + this.max = 0xffffffff; + } else { + this.min = Integer.MIN_VALUE; + this.max = Integer.MAX_VALUE; + } + this.invalid = invalid; + this.unsigned = unsigned; + } + + public int getByteSize() { + return size; + } + + @Override + public Object decode(final ByteBuffer byteBuffer, int scale, int offset) { + if (unsigned) { + long i = ((byteBuffer.getInt() & 0xffffffffL) + offset) / scale; + return i; + } else { + int i = (byteBuffer.getInt() + offset) / scale; + if (i < min || i > max) + return invalid; + return i; + } + + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) { + long l = ((Number) o).longValue() * scale - offset; + if (!unsigned && (l < min || l > max)) { + byteBuffer.putInt((int) invalid); + return; + } + byteBuffer.putInt((int) l); + } + + @Override + public void invalidate(ByteBuffer byteBuffer) { + byteBuffer.putInt((int) invalid); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInterface.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInterface.java new file mode 100644 index 000000000..3784c0ac8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInterface.java @@ -0,0 +1,13 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +public interface BaseTypeInterface { + int getByteSize(); + + Object decode(ByteBuffer byteBuffer, int scale, int offset); + + void encode(ByteBuffer byteBuffer, Object o, int scale, int offset); + + void invalidate(ByteBuffer byteBuffer); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeLong.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeLong.java new file mode 100644 index 000000000..1f3e54a6a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeLong.java @@ -0,0 +1,55 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +public class BaseTypeLong implements BaseTypeInterface { + private final int size = 8; + private final double min; + private final double max; + private final double invalid; + private final boolean unsigned; + + BaseTypeLong(boolean unsigned, long invalid) { + if (unsigned) { + this.min = 0; + this.max = 0xFFFFFFFFFFFFFFFFL; + } else { + this.min = Long.MIN_VALUE; + this.max = Long.MAX_VALUE; + } + this.invalid = invalid; + this.unsigned = unsigned; + } + + public int getByteSize() { + return size; + } + + @Override + public Object decode(ByteBuffer byteBuffer, int scale, int offset) { + if (unsigned) { + return ((byteBuffer.getLong() & 0xFFFFFFFFFFFFFFFFL + offset) / scale); + } else { + long l = (byteBuffer.getLong() + offset) / scale; + if (l < min || l > max) + return invalid; + return l; + } + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) { + long l = ((Number) o).longValue() * scale - offset; + if (!unsigned && (l < min || l > max)) { + byteBuffer.putLong((long) invalid); + return; + } + byteBuffer.putLong(l); + } + + @Override + public void invalidate(ByteBuffer byteBuffer) { + byteBuffer.putLong((long) invalid); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeShort.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeShort.java new file mode 100644 index 000000000..1bbac467f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeShort.java @@ -0,0 +1,56 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes; + +import java.nio.ByteBuffer; + +public class BaseTypeShort implements BaseTypeInterface { + private final int min; + private final int max; + private final int invalid; + private final boolean unsigned; + private final int size = 2; + + BaseTypeShort(boolean unsigned, int invalid) { + if (unsigned) { + this.min = 0; + this.max = 0xffff; + } else { + this.min = Short.MIN_VALUE; + this.max = Short.MAX_VALUE; + } + this.invalid = invalid; + this.unsigned = unsigned; + } + + public int getByteSize() { + return size; + } + + @Override + public Object decode(final ByteBuffer byteBuffer, int scale, int offset) { + if (unsigned) { + int s = (((byteBuffer.getShort() & 0xffff) + offset) / scale); + return s; + } else { + short s = (short) ((byteBuffer.getShort() + offset) / scale); + if (s < min || s > max) + return invalid; + return s; + } + + } + + @Override + public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) { + int i = ((Number) o).intValue() * scale - offset; + if (!unsigned && (i < min || i > max)) { + byteBuffer.putShort((short) invalid); + return; + } + byteBuffer.putShort((short) i); + } + + @Override + public void invalidate(ByteBuffer byteBuffer) { + byteBuffer.putShort((short) invalid); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CurrentTimeRequestMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CurrentTimeRequestMessage.java index 9588eadaa..e6de88ed3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CurrentTimeRequestMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CurrentTimeRequestMessage.java @@ -4,7 +4,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; import java.util.Calendar; import java.util.TimeZone; -import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils; public class CurrentTimeRequestMessage extends GFDIMessage { private final int referenceID; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java new file mode 100644 index 000000000..db75a56fe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java @@ -0,0 +1,55 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalDefinitionsEnum; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; + +public class FitDataMessage extends GFDIMessage { + private final List recordDataList; + private final int messageType; + + public FitDataMessage(List recordDataList, int messageType) { + this.recordDataList = recordDataList; + this.messageType = messageType; + } + + public FitDataMessage(List recordDataList) { + this.recordDataList = recordDataList; + this.messageType = GarminMessage.FIT_DATA.getId(); + } + + public static FitDataMessage parseIncoming(MessageReader reader, int messageType) { + List recordDataList = new ArrayList<>(); + + + while (!reader.isEndOfPayload()) { + RecordHeader recordHeader = new RecordHeader((byte) reader.readByte()); + if (recordHeader.isDefinition()) + return null; + RecordData recordData = new RecordData(GlobalDefinitionsEnum.getRecordDefinitionfromMesgType(recordHeader.getMesgType())); + recordData.parseDataMessage(reader); + recordDataList.add(recordData); + } + + return new FitDataMessage(recordDataList, messageType); + } + + public List getRecordDataList() { + return recordDataList; + } + + @Override + protected boolean generateOutgoing() { + final MessageWriter writer = new MessageWriter(response); + writer.writeShort(0); // packet size will be filled below + writer.writeShort(messageType); + for (RecordData recordData : recordDataList) { + recordData.generateOutgoingDataPayload(writer); + } + return true; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDefinitionMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDefinitionMessage.java new file mode 100644 index 000000000..791de9731 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDefinitionMessage.java @@ -0,0 +1,50 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; + +public class FitDefinitionMessage extends GFDIMessage { + + private final List recordDefinitions; + private final int messageType; + + public FitDefinitionMessage(List recordDefinitions, int messageType) { + this.recordDefinitions = recordDefinitions; + this.messageType = messageType; + } + + public FitDefinitionMessage(List recordDefinitions) { + this.recordDefinitions = recordDefinitions; + this.messageType = GarminMessage.FIT_DEFINITION.getId(); + } + + public static FitDefinitionMessage parseIncoming(MessageReader reader, int messageType) { + List recordDefinitions = new ArrayList<>(); + + while (!reader.isEndOfPayload()) { + RecordHeader recordHeader = new RecordHeader((byte) reader.readByte()); + recordDefinitions.add(RecordDefinition.parseIncoming(reader, recordHeader)); + } + + return new FitDefinitionMessage(recordDefinitions, messageType); + } + + public List getRecordDefinitions() { + return recordDefinitions; + } + + @Override + protected boolean generateOutgoing() { + final MessageWriter writer = new MessageWriter(response); + writer.writeShort(0); // packet size will be filled below + writer.writeShort(messageType); + for (RecordDefinition recordDefinition : recordDefinitions) { + recordDefinition.generateOutgoingPayload(writer); + } + return true; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java index c79dcc32d..f992436bb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java @@ -1,7 +1,5 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; -import androidx.annotation.Nullable; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,7 +12,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.stat import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage; public abstract class GFDIMessage { - public static final int MESSAGE_RESPONSE = 5000; //TODO: MESSAGE_STATUS is a better name? public static final int MESSAGE_REQUEST = 5001; public static final int MESSAGE_DOWNLOAD_REQUEST = 5002; public static final int MESSAGE_UPLOAD_REQUEST = 5003; @@ -22,28 +19,13 @@ public abstract class GFDIMessage { public static final int MESSAGE_CREATE_FILE_REQUEST = 5005; public static final int MESSAGE_DIRECTORY_FILE_FILTER_REQUEST = 5007; public static final int MESSAGE_FILE_READY = 5009; - public static final int MESSAGE_FIT_DEFINITION = 5011; - public static final int MESSAGE_FIT_DATA = 5012; - public static final int MESSAGE_WEATHER_REQUEST = 5014; public static final int MESSAGE_BATTERY_STATUS = 5023; - public static final int MESSAGE_DEVICE_INFORMATION = 5024; - public static final int MESSAGE_DEVICE_SETTINGS = 5026; - public static final int MESSAGE_SYSTEM_EVENT = 5030; public static final int MESSAGE_SUPPORTED_FILE_TYPES_REQUEST = 5031; public static final int MESSAGE_NOTIFICATION_SOURCE = 5033; public static final int MESSAGE_GNCS_CONTROL_POINT_REQUEST = 5034; public static final int MESSAGE_GNCS_DATA_SOURCE = 5035; public static final int MESSAGE_NOTIFICATION_SERVICE_SUBSCRIPTION = 5036; public static final int MESSAGE_SYNC_REQUEST = 5037; - public static final int MESSAGE_FIND_MY_PHONE = 5039; - public static final int MESSAGE_CANCEL_FIND_MY_PHONE = 5040; - public static final int MESSAGE_MUSIC_CONTROL = 5041; - public static final int MESSAGE_MUSIC_CONTROL_CAPABILITIES = 5042; - public static final int MESSAGE_PROTOBUF_REQUEST = 5043; - public static final int MESSAGE_PROTOBUF_RESPONSE = 5044; - public static final int MESSAGE_MUSIC_CONTROL_ENTITY_UPDATE = 5049; - public static final int MESSAGE_CONFIGURATION = 5050; - public static final int MESSAGE_CURRENT_TIME_REQUEST = 5052; public static final int MESSAGE_AUTH_NEGOTIATION = 5101; protected static final Logger LOG = LoggerFactory.getLogger(GFDIMessage.class); protected final ByteBuffer response = ByteBuffer.allocate(1000); @@ -101,9 +83,12 @@ public abstract class GFDIMessage { public enum GarminMessage { RESPONSE(5000, GFDIStatusMessage.class), //TODO: STATUS is a better name? - SYSTEM_EVENT(5030, SystemEventMessage.class), + FIT_DEFINITION(5011, FitDefinitionMessage.class), + FIT_DATA(5012, FitDataMessage.class), + WEATHER_REQUEST(5014, WeatherMessage.class), DEVICE_INFORMATION(5024, DeviceInformationMessage.class), DEVICE_SETTINGS(5026, SetDeviceSettingsMessage.class), + SYSTEM_EVENT(5030, SystemEventMessage.class), FIND_MY_PHONE(5039, FindMyPhoneRequestMessage.class), CANCEL_FIND_MY_PHONE(5040, FindMyPhoneRequestMessage.class), MUSIC_CONTROL(5041, MusicControlMessage.class), @@ -156,15 +141,13 @@ public abstract class GFDIMessage { CRC_ERROR, LENGTH_ERROR; - @Nullable public static Status fromCode(final int code) { for (final Status status : Status.values()) { if (status.ordinal() == code) { return status; } } - - return null; + throw new IllegalArgumentException("Unknown status code " + code); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java new file mode 100644 index 000000000..4919d13af --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java @@ -0,0 +1,55 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalDefinitionsEnum; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; + +public class WeatherMessage extends GFDIMessage { + private final int format; + private final int latitude; + private final int longitude; + private final int hoursOfForecast; + private final int messageType; + + + private final List weatherDefinitions; + + public WeatherMessage(int format, int latitude, int longitude, int hoursOfForecast, int messageType) { + this.format = format; + this.latitude = latitude; + this.longitude = longitude; + this.hoursOfForecast = hoursOfForecast; + this.messageType = messageType; + + + weatherDefinitions = new ArrayList<>(3); + weatherDefinitions.add(GlobalDefinitionsEnum.TODAY_WEATHER_CONDITIONS.getRecordDefinition()); + weatherDefinitions.add(GlobalDefinitionsEnum.HOURLY_WEATHER_FORECAST.getRecordDefinition()); + weatherDefinitions.add(GlobalDefinitionsEnum.DAILY_WEATHER_FORECAST.getRecordDefinition()); + + this.statusMessage = this.getStatusMessage(messageType); + + } + + public static WeatherMessage parseIncoming(MessageReader reader, int messageType) { + final int format = reader.readByte(); + final int latitude = reader.readInt(); + final int longitude = reader.readInt(); + final int hoursOfForecast = reader.readByte(); + + return new WeatherMessage(format, latitude, longitude, hoursOfForecast, messageType); + } + + @Override + protected boolean generateOutgoing() { + final MessageWriter writer = new MessageWriter(response); + writer.writeShort(0); // packet size will be filled below + writer.writeShort(GarminMessage.FIT_DEFINITION.getId()); + for (RecordDefinition definition : weatherDefinitions) { + definition.generateOutgoingPayload(writer); + } + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDataStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDataStatusMessage.java new file mode 100644 index 000000000..6772d9b7b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDataStatusMessage.java @@ -0,0 +1,51 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status; + +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader; + +public class FitDataStatusMessage extends GFDIStatusMessage { + + private final Status status; + private final FitDataStatusCode fitDataStatusCode; + private final int messageType; + + public FitDataStatusMessage(int messageType, Status status, FitDataStatusCode fitDataStatusCode) { + this.messageType = messageType; + this.status = status; + this.fitDataStatusCode = fitDataStatusCode; + switch (fitDataStatusCode) { + case APPLIED: + LOG.info("FIT DATA RETURNED STATUS: {}", fitDataStatusCode.name()); + break; + default: + LOG.warn("FIT DATA RETURNED STATUS: {}", fitDataStatusCode.name()); + } + } + + public static FitDataStatusMessage parseIncoming(MessageReader reader, int messageType) { + final Status status = Status.fromCode(reader.readByte()); + final FitDataStatusCode fitDataStatusCode = FitDataStatusCode.fromCode(reader.readByte()); + + reader.warnIfLeftover(); + return new FitDataStatusMessage(messageType, status, fitDataStatusCode); + } + + public enum FitDataStatusCode { + APPLIED, + NO_DEFINITION, + MISMATCH, + NOT_READY, + ; + + @Nullable + public static FitDataStatusCode fromCode(final int code) { + for (final FitDataStatusCode fitDataStatusCode : FitDataStatusCode.values()) { + if (fitDataStatusCode.ordinal() == code) { + return fitDataStatusCode; + } + } + return null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDefinitionStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDefinitionStatusMessage.java new file mode 100644 index 000000000..9bfefdd19 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDefinitionStatusMessage.java @@ -0,0 +1,51 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status; + +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader; + +public class FitDefinitionStatusMessage extends GFDIStatusMessage { + + private final Status status; + private final FitDefinitionStatusCode fitDefinitionStatusCode; + private final int messageType; + + public FitDefinitionStatusMessage(int messageType, Status status, FitDefinitionStatusCode fitDefinitionStatusCode) { + this.messageType = messageType; + this.status = status; + this.fitDefinitionStatusCode = fitDefinitionStatusCode; + switch (fitDefinitionStatusCode) { + case APPLIED: + LOG.info("FIT DEFINITION RETURNED STATUS: {}", fitDefinitionStatusCode.name()); + break; + default: + LOG.warn("FIT DEFINITION RETURNED STATUS: {}", fitDefinitionStatusCode.name()); + } + } + + public static FitDefinitionStatusMessage parseIncoming(MessageReader reader, int messageType) { + final Status status = Status.fromCode(reader.readByte()); + final FitDefinitionStatusCode fitDefinitionStatusCode = FitDefinitionStatusCode.fromCode(reader.readByte()); + + reader.warnIfLeftover(); + return new FitDefinitionStatusMessage(messageType, status, fitDefinitionStatusCode); + } + + public enum FitDefinitionStatusCode { + APPLIED, + NOT_UNIQUE, + OUT_OF_RANGE, + NOT_READY, + ; + + @Nullable + public static FitDefinitionStatusCode fromCode(final int code) { + for (final FitDefinitionStatusCode fitDefinitionStatusCode : FitDefinitionStatusCode.values()) { + if (fitDefinitionStatusCode.ordinal() == code) { + return fitDefinitionStatusCode; + } + } + return null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java index 1625c1e33..18e770714 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java @@ -5,21 +5,23 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDI import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader; public abstract class GFDIStatusMessage extends GFDIMessage { - Status status; + private Status status; public static GFDIStatusMessage parseIncoming(MessageReader reader, int messageType) { final GarminMessage garminMessage = GFDIMessage.GarminMessage.fromId(reader.readShort()); if (GarminMessage.PROTOBUF_REQUEST.equals(garminMessage) || GarminMessage.PROTOBUF_RESPONSE.equals(garminMessage)) { return ProtobufStatusMessage.parseIncoming(reader, messageType); + } else if (GarminMessage.FIT_DEFINITION.equals(garminMessage)) { + return FitDefinitionStatusMessage.parseIncoming(reader, messageType); + } else if (GarminMessage.FIT_DATA.equals(garminMessage)) { + return FitDataStatusMessage.parseIncoming(reader, messageType); } else { final Status status = Status.fromCode(reader.readByte()); - switch (status) { - case ACK: - LOG.info("Received ACK for message {}", garminMessage.name()); - break; - default: - LOG.warn("Received {} for message {}", status, garminMessage.name()); + if (Status.ACK == status) { + LOG.info("Received ACK for message {}", garminMessage.name()); + } else { + LOG.warn("Received {} for message {}", status, garminMessage.name()); } reader.warnIfLeftover();