Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java

362 lines
18 KiB
Java

package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
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;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
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;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
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.deviceevents.WeatherRequestDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.LocalMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
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;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetDeviceSettingsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback {
private static final Logger LOG = LoggerFactory.getLogger(GarminSupport.class);
private final ProtocolBufferHandler protocolBufferHandler;
private ICommunicator communicator;
private MusicStateSpec musicStateSpec;
private Timer musicStateTimer;
public GarminSupport() {
super(LOG);
addSupportedService(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI);
addSupportedService(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI);
protocolBufferHandler = new ProtocolBufferHandler(this);
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
if (getSupportedServices().contains(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI)) {
communicator = new CommunicatorV2(this);
} else if (getSupportedServices().contains(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI)) {
communicator = new CommunicatorV1(this);
} else {
LOG.warn("Failed to find a known Garmin service");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.NOT_CONNECTED, getContext()));
return builder;
}
communicator.initializeDevice(builder);
return builder;
}
@Override
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
final UUID characteristicUUID = characteristic.getUuid();
if (super.onCharacteristicChanged(gatt, characteristic)) {
LOG.debug("Change of characteristic {} handled by parent", characteristicUUID);
return true;
}
return communicator.onCharacteristicChanged(gatt, characteristic);
}
@Override
public void onMessage(final byte[] message) {
if (null == message) {
return; //message is not complete yet TODO check before calling
}
// LOG.debug("COBS decoded MESSAGE: {}", GB.hexdump(message));
GFDIMessage parsedMessage = GFDIMessage.parseIncoming(message);
if (null == parsedMessage) {
return; //message cannot be handled
}
evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent());
if (parsedMessage instanceof ProtobufMessage) {
ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufMessage) parsedMessage);
if (protobufMessage != null) {
communicator.sendMessage(protobufMessage.getOutgoingMessage());
communicator.sendMessage(protobufMessage.getAckBytestream());
}
}
communicator.sendMessage(parsedMessage.getAckBytestream());
byte[] response = parsedMessage.getOutgoingMessage();
if (null != response) {
// LOG.debug("sending response {}", GB.hexdump(response));
communicator.sendMessage(response);
}
if (parsedMessage instanceof ConfigurationMessage) { //the last forced message exchange
completeInitialization();
}
if (parsedMessage instanceof ProtobufStatusMessage) {
ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufStatusMessage) parsedMessage);
if (protobufMessage != null) {
communicator.sendMessage(protobufMessage.getOutgoingMessage());
communicator.sendMessage(protobufMessage.getAckBytestream());
}
}
}
@Override
public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) {
if (deviceEvent instanceof WeatherRequestDeviceEvent) {
WeatherSpec weather = Weather.getInstance().getWeatherSpec();
if (weather != null) {
sendWeatherConditions(weather);
}
}
super.evaluateGBDeviceEvent(deviceEvent);
}
@Override
public void onSendWeather(final ArrayList<WeatherSpec> weatherSpecs) {
sendWeatherConditions(weatherSpecs.get(0));
}
private void sendWeatherConditions(WeatherSpec weather) {
List<RecordData> weatherData = new ArrayList<>();
List<RecordDefinition> weatherDefinitions = new ArrayList<>(3);
weatherDefinitions.add(LocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
weatherDefinitions.add(LocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
weatherDefinitions.add(LocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
communicator.sendMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage(weatherDefinitions).getOutgoingMessage());
try {
RecordData today = new RecordData(LocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
today.setFieldByName("weather_report", 0); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
today.setFieldByName("timestamp", weather.timestamp);
today.setFieldByName("observed_at_time", weather.timestamp);
today.setFieldByName("temperature", weather.currentTemp);
today.setFieldByName("low_temperature", weather.todayMinTemp);
today.setFieldByName("high_temperature", weather.todayMaxTemp);
today.setFieldByName("condition", 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);
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(LocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
weatherHourlyForecast.setFieldByName("timestamp", hourly.timestamp);
weatherHourlyForecast.setFieldByName("temperature", hourly.temp);
weatherHourlyForecast.setFieldByName("condition", 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(LocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
todayDailyForecast.setFieldByName("timestamp", weather.timestamp);
todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp);
todayDailyForecast.setFieldByName("high_temperature", weather.todayMaxTemp);
todayDailyForecast.setFieldByName("condition", weather.currentConditionCode);
todayDailyForecast.setFieldByName("precipitation_probability", weather.precipProbability);
todayDailyForecast.setFieldByName("day_of_week", 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(LocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
weatherDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
weatherDailyForecast.setFieldByName("timestamp", weather.timestamp);
weatherDailyForecast.setFieldByName("low_temperature", daily.minTemp);
weatherDailyForecast.setFieldByName("high_temperature", daily.maxTemp);
weatherDailyForecast.setFieldByName("condition", daily.conditionCode);
weatherDailyForecast.setFieldByName("precipitation_probability", daily.precipProbability);
weatherDailyForecast.setFieldByName("day_of_week", 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() {
onSetTime();
enableWeather();
//following is needed for vivomove style
communicator.sendMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_READY, 0).getOutgoingMessage());
enableBatteryLevelUpdate();
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
}
private void enableBatteryLevelUpdate() {
final ProtobufMessage batteryLevelProtobufRequest = protocolBufferHandler.prepareProtobufRequest(GdiSmartProto.Smart.newBuilder()
.setDeviceStatusService(
GdiDeviceStatus.DeviceStatusService.newBuilder()
.setRemoteDeviceBatteryStatusRequest(
GdiDeviceStatus.DeviceStatusService.RemoteDeviceBatteryStatusRequest.newBuilder()
)
)
.build());
communicator.sendMessage(batteryLevelProtobufRequest.getOutgoingMessage());
}
private void enableWeather() {
final Map<SetDeviceSettingsMessage.GarminDeviceSetting, Object> settings = new LinkedHashMap<>(1);
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true);
communicator.sendMessage(new SetDeviceSettingsMessage(settings).getOutgoingMessage());
}
@Override
public void onSetTime() {
communicator.sendMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.TIME_UPDATED, 0).getOutgoingMessage());
}
@Override
public void onFindDevice(boolean start) {
if (start) {
final ProtobufMessage findMyWatch = protocolBufferHandler.prepareProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(
GdiFindMyWatch.FindMyWatchService.newBuilder()
.setFindRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder()
.setTimeout(60)
)
)
.build());
communicator.sendMessage(findMyWatch.getOutgoingMessage());
} else {
final ProtobufMessage cancelFindMyWatch = protocolBufferHandler.prepareProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(
GdiFindMyWatch.FindMyWatchService.newBuilder()
.setCancelRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder()
)
)
.build());
communicator.sendMessage(cancelFindMyWatch.getOutgoingMessage());
}
}
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.TRACK.ARTIST, musicSpec.artist);
attributes.put(MusicControlEntityUpdateMessage.TRACK.ALBUM, musicSpec.album);
attributes.put(MusicControlEntityUpdateMessage.TRACK.TITLE, musicSpec.track);
attributes.put(MusicControlEntityUpdateMessage.TRACK.DURATION, String.valueOf(musicSpec.duration));
communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage());
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
musicStateSpec = stateSpec;
if (musicStateTimer != null) {
musicStateTimer.cancel();
musicStateTimer.purge();
musicStateTimer = null;
}
musicStateTimer = new Timer();
int updatePeriod = 4000; //milliseconds
LOG.debug("onSetMusicState: {}", stateSpec.toString());
if (stateSpec.state == MusicStateSpec.STATE_PLAYING) {
musicStateTimer.schedule(new TimerTask() {
@Override
public void run() {
String playing = "1";
String playRate = "1.0";
String position = new DecimalFormat("#.000").format(musicStateSpec.position);
musicStateSpec.position += updatePeriod / 1000;
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString());
communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage());
}
}, 0, updatePeriod);
} else {
String playing = "0";
String playRate = "0.0";
String position = new DecimalFormat("#.###").format(stateSpec.position);
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString());
communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage());
}
}
}