From e691042265634d959cc4ab7d3996719e34182901 Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Sat, 23 Mar 2024 15:32:57 +0100 Subject: [PATCH] Garmin protocol: add initial support for FIT messages note: only weather message definition and data tested so far also enable weather support for Instinct 2S and vivomove style also cleanup some unused constants that have been migrated to new enums in GFDIMessage additionally switch to new local implementation of GarminTimeUtils with needed methods --- .../GarminInstinct2SCoordinator.java | 6 + .../GarminVivomoveStyleCoordinator.java | 7 + .../service/devices/garmin/GarminSupport.java | 93 ++++++++- .../devices/garmin/GarminTimeUtils.java | 29 +++ .../garmin/fit/DevFieldDefinition.java | 56 ++++++ .../devices/garmin/fit/FieldDefinition.java | 73 +++++++ .../garmin/fit/FitWeatherConditions.java | 141 +++++++++++++ .../garmin/fit/GlobalDefinitionsEnum.java | 87 ++++++++ .../service/devices/garmin/fit/MesgType.java | 32 +++ .../devices/garmin/fit/RecordData.java | 186 ++++++++++++++++++ .../devices/garmin/fit/RecordDefinition.java | 108 ++++++++++ .../devices/garmin/fit/RecordHeader.java | 64 ++++++ .../garmin/fit/baseTypes/BaseType.java | 62 ++++++ .../garmin/fit/baseTypes/BaseTypeByte.java | 53 +++++ .../garmin/fit/baseTypes/BaseTypeDouble.java | 40 ++++ .../garmin/fit/baseTypes/BaseTypeFloat.java | 43 ++++ .../garmin/fit/baseTypes/BaseTypeInt.java | 57 ++++++ .../fit/baseTypes/BaseTypeInterface.java | 13 ++ .../garmin/fit/baseTypes/BaseTypeLong.java | 55 ++++++ .../garmin/fit/baseTypes/BaseTypeShort.java | 56 ++++++ .../messages/CurrentTimeRequestMessage.java | 2 +- .../garmin/messages/FitDataMessage.java | 55 ++++++ .../garmin/messages/FitDefinitionMessage.java | 50 +++++ .../devices/garmin/messages/GFDIMessage.java | 27 +-- .../garmin/messages/WeatherMessage.java | 55 ++++++ .../messages/status/FitDataStatusMessage.java | 51 +++++ .../status/FitDefinitionStatusMessage.java | 51 +++++ .../messages/status/GFDIStatusMessage.java | 16 +- 28 files changed, 1535 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminTimeUtils.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/DevFieldDefinition.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinition.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitWeatherConditions.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalDefinitionsEnum.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/MesgType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordDefinition.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeByte.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeDouble.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeFloat.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInt.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeInterface.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeLong.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/baseTypes/BaseTypeShort.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDataMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FitDefinitionMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDataStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FitDefinitionStatusMessage.java 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();