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
This commit is contained in:
Daniele Gobbetti 2024-03-23 15:32:57 +01:00 committed by José Rebelo
parent ed908b59fd
commit 7ee82e6afa
28 changed files with 1535 additions and 33 deletions

View File

@ -44,4 +44,10 @@ public class GarminInstinct2SCoordinator extends AbstractBLEDeviceCoordinator {
public boolean supportsFindDevice() {
return true;
}
@Override
public boolean supportsWeather() {
return true;
}
}

View File

@ -45,4 +45,11 @@ public class GarminVivomoveStyleCoordinator extends AbstractBLEDeviceCoordinator
return true;
}
@Override
public boolean supportsWeather() {
return true;
}
}

View File

@ -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<WeatherSpec> weatherSpecs) {
sendWeatherConditions(weatherSpecs.get(0));
}
private void sendWeatherConditions(WeatherSpec weather) {
List<RecordData> 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<SetDeviceSettingsMessage.GarminDeviceSetting, Object> settings = new LinkedHashMap<>(2);
final Map<SetDeviceSettingsMessage.GarminDeviceSetting, Object> 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());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<FieldDefinition> fieldDefinitions;
private List<DevFieldDefinition> devFieldDefinitions;
public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, MesgType mesgType, int globalMesgNum, List<FieldDefinition> fieldDefinitions, List<DevFieldDefinition> 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<FieldDefinition> 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<FieldDefinition> 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<DevFieldDefinition> 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<DevFieldDefinition> getDevFieldDefinitions() {
return devFieldDefinitions;
}
public void setDevFieldDefinitions(List<DevFieldDefinition> devFieldDefinitions) {
this.devFieldDefinitions = devFieldDefinitions;
}
public RecordHeader getRecordHeader() {
return recordHeader;
}
public List<FieldDefinition> getFieldDefinitions() {
return fieldDefinitions;
}
public void setFieldDefinitions(List<FieldDefinition> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<RecordData> recordDataList;
private final int messageType;
public FitDataMessage(List<RecordData> recordDataList, int messageType) {
this.recordDataList = recordDataList;
this.messageType = messageType;
}
public FitDataMessage(List<RecordData> recordDataList) {
this.recordDataList = recordDataList;
this.messageType = GarminMessage.FIT_DATA.getId();
}
public static FitDataMessage parseIncoming(MessageReader reader, int messageType) {
List<RecordData> 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<RecordData> 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;
}
}

View File

@ -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<RecordDefinition> recordDefinitions;
private final int messageType;
public FitDefinitionMessage(List<RecordDefinition> recordDefinitions, int messageType) {
this.recordDefinitions = recordDefinitions;
this.messageType = messageType;
}
public FitDefinitionMessage(List<RecordDefinition> recordDefinitions) {
this.recordDefinitions = recordDefinitions;
this.messageType = GarminMessage.FIT_DEFINITION.getId();
}
public static FitDefinitionMessage parseIncoming(MessageReader reader, int messageType) {
List<RecordDefinition> 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<RecordDefinition> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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