From 63e0ecba63913a002a2c27a198115f9564c1cf03 Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Thu, 11 Dec 2025 16:45:53 +0100 Subject: [PATCH] Garmin: Better Local Message Handling Use a temporary MessageHandler to send Definitions and Data in sequence instead of in parallel. Add functionality to populate FitLocalMessageBuilder record by record instead of with a List, also add helper to get an unused localMessageType. Don't use hardcoded localMessageTypes for weather message as they are not required. --- .../garmin/FitLocalMessageHandler.java | 46 +++++++++++++++++++ .../service/devices/garmin/GarminSupport.java | 32 ++++++++----- .../garmin/fit/FitLocalMessageBuilder.java | 41 +++++++++++++++-- .../status/FitDefinitionStatusMessage.java | 4 ++ .../garmin/fit/messages/FitWeatherTest.java | 5 +- 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FitLocalMessageHandler.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FitLocalMessageHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FitLocalMessageHandler.java new file mode 100644 index 0000000000..023a8d33fd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FitLocalMessageHandler.java @@ -0,0 +1,46 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitLocalMessageBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDataMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.FitDataStatusMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.FitDefinitionStatusMessage; + +public class FitLocalMessageHandler implements MessageHandler{ + private static final Logger LOG = LoggerFactory.getLogger(FitLocalMessageHandler.class); + private final GarminSupport deviceSupport; + private final FitLocalMessageBuilder localMessageBuilder; + + public FitLocalMessageHandler(GarminSupport deviceSupport, FitLocalMessageBuilder localMessageBuilder) { + this.deviceSupport = deviceSupport; + this.localMessageBuilder = localMessageBuilder; + } + + public FitDefinitionMessage init() { + return new FitDefinitionMessage(localMessageBuilder.getDefinitions()); + } + + private FitDataMessage sendFollowUp(FitDefinitionStatusMessage fitDefinitionStatusMessage) { + if (fitDefinitionStatusMessage.getFitDefinitionStatusCode() != FitDefinitionStatusMessage.FitDefinitionStatusCode.APPLIED) + LOG.warn("FitDefinition was not applied, will send FitData anyway."); + return new FitDataMessage(localMessageBuilder.getRecordDataList()); + } + + private void unregisterSelf() { + deviceSupport.unregisterHandler(this); + } + + @Override + public GFDIMessage handle(GFDIMessage message) { + if (message instanceof FitDefinitionStatusMessage) + return sendFollowUp((FitDefinitionStatusMessage) message); + else if (message instanceof FitDataStatusMessage) { + unregisterSelf(); + } + return null; + } +} 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 a891e9af40..c9cb40379b 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 @@ -33,7 +33,6 @@ import java.time.ZoneId; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; @@ -599,14 +598,22 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC return; } - final FitLocalMessageBuilder weatherLocalMessage = new FitLocalMessageBuilder(encodeWeather(weather)); + final FitLocalMessageBuilder weatherLocalMessage = encodeWeather(weather); - sendOutgoingMessage("send " + weatherLocalMessage.getDefinitions().size() + " weather definitions", new FitDefinitionMessage(weatherLocalMessage.getDefinitions())); - sendOutgoingMessage("send weather data", new FitDataMessage(weatherLocalMessage.getData())); + final FitLocalMessageHandler weatherHandler = new FitLocalMessageHandler(this, weatherLocalMessage); + messageHandlers.add(weatherHandler); + + sendOutgoingMessage("send " + weatherLocalMessage.getDefinitions().size() + " weather definitions", weatherHandler.init()); } - public static List encodeWeather(final WeatherSpec weather) { - List weatherData = new ArrayList<>(); + protected void unregisterHandler(MessageHandler registered) { + messageHandlers.remove(registered); + LOG.warn("handlers: {}", messageHandlers.stream().count()); + } + + public static FitLocalMessageBuilder encodeWeather(final WeatherSpec weather) { + + final FitLocalMessageBuilder weatherLocalMessage = new FitLocalMessageBuilder(); final FitWeather.Builder today = new FitWeather.Builder(); today.setWeatherReport(FieldDefinitionWeatherReport.Type.current); @@ -628,8 +635,9 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC today.setAirQuality(FieldDefinitionWeatherAqi.aqiAbsoluteValueToEnum(weather.getAirQuality().getAqi())); } today.setLocation(weather.getLocation()); - weatherData.add(today.build(6)); + weatherLocalMessage.addRecordData(today.build(weatherLocalMessage.getNextAvailableLocalMessageType())); + final int hourlyMessageType = weatherLocalMessage.getNextAvailableLocalMessageType(); for (int hour = 0; hour <= 11; hour++) { if (hour < weather.getHourly().size()) { WeatherSpec.Hourly hourly = weather.getHourly().get(hour); @@ -647,10 +655,12 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC // weatherHourlyForecast.setDewPoint(0); // TODO: add once Hourly contains this information weatherHourlyForecast.setUvIndex(hourly.getUvIndex()); // weatherHourlyForecast.setAirQuality(0); // TODO: add once Hourly contains this information - weatherData.add(weatherHourlyForecast.build(9)); + weatherLocalMessage.addRecordData(weatherHourlyForecast.build(hourlyMessageType)); } } // + final int dailyMessageType = weatherLocalMessage.getNextAvailableLocalMessageType(); + final FitWeather.Builder todayDailyForecast = new FitWeather.Builder(); todayDailyForecast.setWeatherReport(FieldDefinitionWeatherReport.Type.daily_forecast); todayDailyForecast.setTimestamp((long) weather.getTimestamp()); @@ -663,7 +673,7 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC if (null != weather.getAirQuality()) { todayDailyForecast.setAirQuality(FieldDefinitionWeatherAqi.aqiAbsoluteValueToEnum(weather.getAirQuality().getAqi())); } - weatherData.add(todayDailyForecast.build(10)); + weatherLocalMessage.addRecordData(todayDailyForecast.build(dailyMessageType)); for (int day = 0; day < 4; day++) { @@ -681,11 +691,11 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC if (null != daily.getAirQuality()) { weatherDailyForecast.setAirQuality(FieldDefinitionWeatherAqi.aqiAbsoluteValueToEnum(daily.getAirQuality().getAqi())); } - weatherData.add(weatherDailyForecast.build(10)); + weatherLocalMessage.addRecordData(weatherDailyForecast.build(dailyMessageType)); } } - return weatherData; + return weatherLocalMessage; } private void completeInitialization() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitLocalMessageBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitLocalMessageBuilder.java index e59689c4b7..3f5c853b3b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitLocalMessageBuilder.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitLocalMessageBuilder.java @@ -14,20 +14,45 @@ public class FitLocalMessageBuilder { private final List recordData = new ArrayList<>(); private final Map definitionsByLocalType = new LinkedHashMap<>(); + public FitLocalMessageBuilder() { + } + + public FitLocalMessageBuilder addRecordData(RecordData data) { + Objects.requireNonNull(data, "RecordData cannot be null"); + Objects.requireNonNull(data.getRecordDefinition(), "RecordDefinition cannot be null"); + + processRecordData(data); + return this; + } + public FitLocalMessageBuilder(List recordDataList) { Objects.requireNonNull(recordDataList, "recordDataList cannot be null"); for (RecordData data : recordDataList) { Objects.requireNonNull(data.getRecordDefinition(), "recordDefinition cannot be null"); - processRecordData(data); + addRecordData(data); } } + public int getNextAvailableLocalMessageType() { + for (int i = 0; i < MAX_DEFINITIONS; i++) { + if (!definitionsByLocalType.containsKey(i)) { + return i; + } + } + + throw new IllegalStateException( + String.format("No available localMessageType: all %d slots are occupied", MAX_DEFINITIONS) + ); + } + private void processRecordData(RecordData data) { final RecordDefinition definition = data.getRecordDefinition(); final int localMessageType = definition.getRecordHeader().getLocalMessageType(); + validateLocalMessageType(localMessageType); + if (!definitionsByLocalType.containsKey(localMessageType)) { addNewDefinition(data, definition, localMessageType); } else { @@ -35,6 +60,15 @@ public class FitLocalMessageBuilder { } } + private void validateLocalMessageType(int localMessageType) { + if (localMessageType < 0 || localMessageType >= MAX_DEFINITIONS) { + throw new IllegalArgumentException( + String.format("localMessageType must be between 0 and %d, got: %d", + MAX_DEFINITIONS - 1, localMessageType) + ); + } + } + private void addNewDefinition(RecordData data, RecordDefinition definition, int localMessageType) { if (definitionsByLocalType.size() >= MAX_DEFINITIONS) { throw new IllegalStateException( @@ -77,8 +111,7 @@ public class FitLocalMessageBuilder { return new ArrayList<>(definitionsByLocalType.values()); } - public List getData() { + public List getRecordDataList() { return new ArrayList<>(recordData); } -} - +} \ No newline at end of file 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 index 2b2cb31033..927a7c3681 100644 --- 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 @@ -31,6 +31,10 @@ public class FitDefinitionStatusMessage extends GFDIStatusMessage { return new FitDefinitionStatusMessage(garminMessage, status, fitDefinitionStatusCode); } + public FitDefinitionStatusCode getFitDefinitionStatusCode() { + return fitDefinitionStatusCode; + } + public enum FitDefinitionStatusCode { APPLIED, NOT_UNIQUE, diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java index 60785181cc..a331bd7565 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitWeatherTest.java @@ -292,7 +292,8 @@ public class FitWeatherTest { Assert.assertEquals("FitWeather{weather_report=2, condition=CLEAR, precipitation_probability=53, day_of_week=TUESDAY, high_temperature=301, low_temperature=286, timestamp=1764364324}", weatherData.get(17).toString()); */ - List weatherData = GarminSupport.encodeWeather(weather); + FitLocalMessageBuilder weatherLocalMessage = GarminSupport.encodeWeather(weather); + List weatherData = weatherLocalMessage.getRecordDataList(); // Get all distinct record definitions Set seenDefinitions = new HashSet<>(); @@ -305,9 +306,7 @@ public class FitWeatherTest { } } - FitLocalMessageBuilder weatherLocalMessage = new FitLocalMessageBuilder(weatherData); - FitLocalMessageBuilder test = new FitLocalMessageBuilder(null); for (RecordDefinition weatherDefinition : weatherDefinitions) { MessageWriter writer = new MessageWriter(); weatherDefinition.generateOutgoingPayload(writer);