362 lines
18 KiB
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());
|
|
}
|
|
}
|
|
|
|
}
|