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.
This commit is contained in:
Daniele Gobbetti
2025-12-21 13:54:21 +00:00
committed by José Rebelo
parent fe6def63ee
commit 63e0ecba63
5 changed files with 110 additions and 18 deletions
@@ -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;
}
}
@@ -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<RecordData> encodeWeather(final WeatherSpec weather) {
List<RecordData> 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() {
@@ -14,20 +14,45 @@ public class FitLocalMessageBuilder {
private final List<RecordData> recordData = new ArrayList<>();
private final Map<Integer, RecordDefinition> 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<RecordData> 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<RecordData> getData() {
public List<RecordData> getRecordDataList() {
return new ArrayList<>(recordData);
}
}
}
@@ -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,
@@ -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<RecordData> weatherData = GarminSupport.encodeWeather(weather);
FitLocalMessageBuilder weatherLocalMessage = GarminSupport.encodeWeather(weather);
List<RecordData> weatherData = weatherLocalMessage.getRecordDataList();
// Get all distinct record definitions
Set<Integer> 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);