mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-12 10:55:49 +01:00
Garmin protocol: initial refactoring and basic functionalities
This commit takes aims to bring many new garmin devices up to a working status, with basic functionalities such as: - garmin protocol initialization - basic message exchange - support for some messages in Garmin own format - support for some messages in protobuf format
This commit is contained in:
parent
7ea2261ba3
commit
559a73cc5e
@ -0,0 +1,47 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2s;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||
|
||||
public class GarminInstinct2SCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("Instinct 2S");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Garmin";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return GarminSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_instinct_2s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.venu3;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||
|
||||
public class GarminVenu3Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("Venu 3");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Garmin";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return GarminSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivomove_style;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivomove;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||
|
||||
public class GarminVivomoveStyleCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("vívomove Style");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Garmin";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return GarminSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivomove_style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -143,7 +143,7 @@ public class VivomoveHrCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_vivomove_hr;
|
||||
return R.string.devicetype_garmin_vivomove_hr;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -50,6 +50,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2ProDe
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsLiveDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsProDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2s.GarminInstinct2SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.venu3.GarminVenu3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivomove.GarminVivomoveStyleCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.EXRIZUK8Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator;
|
||||
@ -142,9 +145,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlus
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaMhoC303Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaLywsd02Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaLywsd03Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaMhoC303Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
|
||||
@ -166,11 +169,11 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM5Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWFSP800NCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM4Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.SoundcoreLiberty3ProCoordinator;
|
||||
@ -188,6 +191,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband7pro.MiBand7Pro
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8.MiBand8Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8active.MiBand8ActiveCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8pro.MiBand8ProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatch.MiWatchLiteCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatchcolorsport.MiWatchColorSportCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartband2.RedmiSmartBand2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartbandpro.RedmiSmartBandProCoordinator;
|
||||
@ -201,7 +205,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watchs1pro.XiaomiWatc
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watchs3.XiaomiWatchS3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatch.MiWatchLiteCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmiwatch3active.RedmiWatch3ActiveCoordinator;
|
||||
|
||||
/**
|
||||
@ -329,6 +332,9 @@ public enum DeviceType {
|
||||
ITAG(ITagCoordinator.class),
|
||||
NUTMINI(NutCoordinator.class),
|
||||
VIVOMOVE_HR(VivomoveHrCoordinator.class),
|
||||
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
|
||||
GARMIN_VIVOMOVE_STYLE(GarminVivomoveStyleCoordinator.class),
|
||||
GARMIN_VENU_3(GarminVenu3Coordinator.class),
|
||||
VIBRATISSIMO(VibratissimoCoordinator.class),
|
||||
SONY_SWR12(SonySWR12DeviceCoordinator.class),
|
||||
LIVEVIEW(LiveviewCoordinator.class),
|
||||
|
@ -0,0 +1,253 @@
|
||||
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.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
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.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.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.ProtobufStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetDeviceSettingsMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
|
||||
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());
|
||||
|
||||
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 ProtobufMessage) {
|
||||
ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufMessage) parsedMessage);
|
||||
if (protobufMessage != null) {
|
||||
communicator.sendMessage(protobufMessage.getOutgoingMessage());
|
||||
communicator.sendMessage(protobufMessage.getAckBytestream());
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage instanceof ProtobufStatusMessage) {
|
||||
ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufStatusMessage) parsedMessage);
|
||||
if (protobufMessage != null) {
|
||||
communicator.sendMessage(protobufMessage.getOutgoingMessage());
|
||||
communicator.sendMessage(protobufMessage.getAckBytestream());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void completeInitialization() {
|
||||
|
||||
enableWeather();
|
||||
|
||||
onSetTime();
|
||||
|
||||
//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<>(2);
|
||||
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true);
|
||||
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_ALERTS_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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,294 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCalendarService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
|
||||
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.devices.garmin.messages.GFDIMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
|
||||
|
||||
public class ProtocolBufferHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ProtocolBufferHandler.class);
|
||||
private final GarminSupport deviceSupport;
|
||||
private final Map<Integer, ProtobufFragment> chunkedFragmentsMap;
|
||||
private final int maxChunkSize = 375; //tested on Vívomove Style
|
||||
private int lastProtobufRequestId;
|
||||
|
||||
public ProtocolBufferHandler(GarminSupport deviceSupport) {
|
||||
this.deviceSupport = deviceSupport;
|
||||
chunkedFragmentsMap = new HashMap<>();
|
||||
}
|
||||
|
||||
private int getNextProtobufRequestId() {
|
||||
lastProtobufRequestId = (lastProtobufRequestId + 1) % 65536;
|
||||
return lastProtobufRequestId;
|
||||
}
|
||||
|
||||
ProtobufMessage processIncoming(ProtobufMessage message) {
|
||||
ProtobufFragment protobufFragment = processChunkedMessage(message);
|
||||
|
||||
if (protobufFragment.isComplete()) { //message is now complete
|
||||
LOG.info("Received protobuf message #{}, {}B: {}", message.getRequestId(), protobufFragment.totalLength, GB.hexdump(protobufFragment.fragmentBytes, 0, protobufFragment.totalLength));
|
||||
|
||||
final GdiSmartProto.Smart smart;
|
||||
try {
|
||||
smart = GdiSmartProto.Smart.parseFrom(protobufFragment.fragmentBytes);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
LOG.error("Failed to parse protobuf message ({}): {}", e.getLocalizedMessage(), GB.hexdump(protobufFragment.fragmentBytes));
|
||||
return null;
|
||||
}
|
||||
boolean processed = false;
|
||||
if (smart.hasCoreService()) { //TODO: unify request and response???
|
||||
processed = true;
|
||||
processProtobufCoreResponse(smart.getCoreService());
|
||||
// return prepareProtobufResponse(processProtobufCoreRequest(smart.getCoreService()), message.getRequestId());
|
||||
}
|
||||
if (smart.hasCalendarService()) {
|
||||
return prepareProtobufResponse(processProtobufCalendarRequest(smart.getCalendarService()), message.getRequestId());
|
||||
}
|
||||
if (smart.hasDeviceStatusService()) {
|
||||
processed = true;
|
||||
processProtobufDeviceStatusResponse(smart.getDeviceStatusService());
|
||||
}
|
||||
if (smart.hasFindMyWatchService()) {
|
||||
processed = true;
|
||||
processProtobufFindMyWatchResponse(smart.getFindMyWatchService());
|
||||
}
|
||||
if (!processed) {
|
||||
LOG.warn("Unknown protobuf request: {}", smart);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ProtobufMessage processIncoming(ProtobufStatusMessage statusMessage) {
|
||||
LOG.info("Processing protobuf status message #{}@{}: status={}, error={}", statusMessage.getRequestId(), statusMessage.getDataOffset(), statusMessage.getProtobufStatus(), statusMessage.getError());
|
||||
//TODO: check status and react accordingly, right now we blindly proceed to next chunk
|
||||
if (chunkedFragmentsMap.containsKey(statusMessage.getRequestId()) && statusMessage.isOK()) {
|
||||
final ProtobufFragment protobufFragment = chunkedFragmentsMap.get(statusMessage.getRequestId());
|
||||
LOG.debug("Protobuf message #{} found in queue: {}", statusMessage.getRequestId(), GB.hexdump(protobufFragment.fragmentBytes));
|
||||
|
||||
if (protobufFragment.totalLength <= (statusMessage.getDataOffset() + maxChunkSize)) {
|
||||
chunkedFragmentsMap.remove(protobufFragment);
|
||||
}
|
||||
|
||||
return protobufFragment.getNextChunk(statusMessage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ProtobufFragment processChunkedMessage(ProtobufMessage message) {
|
||||
if (message.isComplete()) //comment this out if for any reason also smaller messages should end up in the map
|
||||
return new ProtobufFragment(message.getMessageBytes());
|
||||
|
||||
if (message.getDataOffset() == 0) { //store new messages beginning at 0, overwrite old messages
|
||||
chunkedFragmentsMap.put(message.getRequestId(), new ProtobufFragment(message));
|
||||
LOG.info("Protobuf request put in queue: #{} , {}", message.getRequestId(), GB.hexdump(message.getMessageBytes()));
|
||||
} else {
|
||||
if (chunkedFragmentsMap.containsKey(message.getRequestId())) {
|
||||
ProtobufFragment oldFragment = chunkedFragmentsMap.get(message.getRequestId());
|
||||
chunkedFragmentsMap.put(message.getRequestId(),
|
||||
new ProtobufFragment(oldFragment, message));
|
||||
}
|
||||
}
|
||||
return chunkedFragmentsMap.get(message.getRequestId());
|
||||
}
|
||||
|
||||
private GdiSmartProto.Smart processProtobufCalendarRequest(GdiCalendarService.CalendarService calendarService) {
|
||||
if (calendarService.hasCalendarRequest()) {
|
||||
GdiCalendarService.CalendarService.CalendarServiceRequest calendarServiceRequest = calendarService.getCalendarRequest();
|
||||
|
||||
CalendarManager upcomingEvents = new CalendarManager(deviceSupport.getContext(), deviceSupport.getDevice().getAddress());
|
||||
List<CalendarEvent> mEvents = upcomingEvents.getCalendarEventList();
|
||||
List<GdiCalendarService.CalendarService.CalendarEvent> watchEvents = new ArrayList<>();
|
||||
|
||||
for (CalendarEvent mEvt : mEvents) {
|
||||
if (mEvt.getEndSeconds() < calendarServiceRequest.getBegin() ||
|
||||
mEvt.getBeginSeconds() > calendarServiceRequest.getEnd()) {
|
||||
LOG.debug("CalendarService Skipping event {} that is out of requested time range", mEvt.getTitle());
|
||||
continue;
|
||||
}
|
||||
|
||||
watchEvents.add(GdiCalendarService.CalendarService.CalendarEvent.newBuilder()
|
||||
.setTitle(mEvt.getTitle())
|
||||
.setAllDay(mEvt.isAllDay())
|
||||
.setBegin(mEvt.getBeginSeconds())
|
||||
.setEnd(mEvt.getEndSeconds())
|
||||
.setLocation(StringUtils.defaultString(mEvt.getLocation()))
|
||||
.setDescription(StringUtils.defaultString(mEvt.getDescription()))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
LOG.debug("CalendarService Sending {} events to watch", watchEvents.size());
|
||||
return GdiSmartProto.Smart.newBuilder().setCalendarService(
|
||||
GdiCalendarService.CalendarService.newBuilder().setCalendarResponse(
|
||||
GdiCalendarService.CalendarService.CalendarServiceResponse.newBuilder()
|
||||
.addAllCalendarEvent(watchEvents)
|
||||
.setUnknown(1)
|
||||
)
|
||||
).build();
|
||||
}
|
||||
LOG.warn("Unknown CalendarService request: {}", calendarService);
|
||||
return GdiSmartProto.Smart.newBuilder().setCalendarService(
|
||||
GdiCalendarService.CalendarService.newBuilder().setCalendarResponse(
|
||||
GdiCalendarService.CalendarService.CalendarServiceResponse.newBuilder()
|
||||
.setUnknown(0)
|
||||
)
|
||||
).build();
|
||||
}
|
||||
|
||||
private void processProtobufCoreResponse(GdiCore.CoreService coreService) {
|
||||
if (coreService.hasSyncResponse()) {
|
||||
final GdiCore.CoreService.SyncResponse syncResponse = coreService.getSyncResponse();
|
||||
LOG.info("Received sync status: {}", syncResponse.getStatus());
|
||||
}
|
||||
LOG.warn("Unknown CoreService response: {}", coreService);
|
||||
}
|
||||
|
||||
private void processProtobufDeviceStatusResponse(GdiDeviceStatus.DeviceStatusService deviceStatusService) {
|
||||
if (deviceStatusService.hasRemoteDeviceBatteryStatusResponse()) {
|
||||
final GdiDeviceStatus.DeviceStatusService.RemoteDeviceBatteryStatusResponse batteryStatusResponse = deviceStatusService.getRemoteDeviceBatteryStatusResponse();
|
||||
final int batteryLevel = batteryStatusResponse.getCurrentBatteryLevel();
|
||||
LOG.info("Received remote battery status {}: level={}", batteryStatusResponse.getStatus(), batteryLevel);
|
||||
final GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo();
|
||||
batteryEvent.level = (short) batteryLevel;
|
||||
deviceSupport.evaluateGBDeviceEvent(batteryEvent);
|
||||
return;
|
||||
}
|
||||
if (deviceStatusService.hasActivityStatusResponse()) {
|
||||
final GdiDeviceStatus.DeviceStatusService.ActivityStatusResponse activityStatusResponse = deviceStatusService.getActivityStatusResponse();
|
||||
LOG.info("Received activity status: {}", activityStatusResponse.getStatus());
|
||||
return;
|
||||
}
|
||||
LOG.warn("Unknown DeviceStatusService response: {}", deviceStatusService);
|
||||
}
|
||||
|
||||
// private GdiSmartProto.Smart processProtobufCoreRequest(GdiCore.CoreService coreService) {
|
||||
// if (coreService.hasLocationUpdatedSetEnabledRequest()) { //TODO: enable location support in devicesupport
|
||||
// LOG.debug("Location CoreService: {}", coreService);
|
||||
//
|
||||
// final GdiCore.CoreService.LocationUpdatedSetEnabledRequest locationUpdatedSetEnabledRequest = coreService.getLocationUpdatedSetEnabledRequest();
|
||||
//
|
||||
// LOG.info("Received locationUpdatedSetEnabledRequest status: {}", locationUpdatedSetEnabledRequest.getEnabled());
|
||||
//
|
||||
// GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Builder response = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.newBuilder()
|
||||
// .setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Status.OK);
|
||||
//
|
||||
// //TODO: check and follow the preference in coordinator (see R.xml.devicesettings_workout_send_gps_to_band )
|
||||
// if(locationUpdatedSetEnabledRequest.getEnabled()) {
|
||||
// response.addRequests(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.newBuilder()
|
||||
// .setRequested(locationUpdatedSetEnabledRequest.getRequests(0).getRequested())
|
||||
// .setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.OK));
|
||||
// }
|
||||
//
|
||||
// deviceSupport.processLocationUpdateRequest(locationUpdatedSetEnabledRequest.getEnabled(), locationUpdatedSetEnabledRequest.getRequestsList());
|
||||
//
|
||||
// return GdiSmartProto.Smart.newBuilder().setCoreService(
|
||||
// GdiCore.CoreService.newBuilder().setLocationUpdatedSetEnabledResponse(response)).build();
|
||||
// }
|
||||
// LOG.warn("Unknown CoreService request: {}", coreService);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
private void processProtobufFindMyWatchResponse(GdiFindMyWatch.FindMyWatchService findMyWatchService) {
|
||||
if (findMyWatchService.hasCancelRequest()) {
|
||||
LOG.info("Watch found");
|
||||
}
|
||||
if (findMyWatchService.hasCancelResponse() || findMyWatchService.hasFindResponse()) {
|
||||
LOG.debug("Received findMyWatch response");
|
||||
}
|
||||
LOG.warn("Unknown FindMyWatchService response: {}", findMyWatchService);
|
||||
}
|
||||
|
||||
public ProtobufMessage prepareProtobufRequest(GdiSmartProto.Smart protobufPayload) {
|
||||
if (null == protobufPayload)
|
||||
return null;
|
||||
final int requestId = getNextProtobufRequestId();
|
||||
return prepareProtobufMessage(protobufPayload.toByteArray(), GFDIMessage.GarminMessage.PROTOBUF_REQUEST.getId(), requestId);
|
||||
}
|
||||
|
||||
private ProtobufMessage prepareProtobufResponse(GdiSmartProto.Smart protobufPayload, int requestId) {
|
||||
if (null == protobufPayload)
|
||||
return null;
|
||||
return prepareProtobufMessage(protobufPayload.toByteArray(), GFDIMessage.GarminMessage.PROTOBUF_RESPONSE.getId(), requestId);
|
||||
}
|
||||
|
||||
private ProtobufMessage prepareProtobufMessage(byte[] bytes, int messageType, int requestId) {
|
||||
if (bytes == null || bytes.length == 0)
|
||||
return null;
|
||||
LOG.info("Preparing protobuf message. Type{}, #{}, {}B: {}", messageType, requestId, bytes.length, GB.hexdump(bytes, 0, bytes.length));
|
||||
|
||||
if (bytes.length > maxChunkSize) {
|
||||
chunkedFragmentsMap.put(requestId, new ProtobufFragment(bytes));
|
||||
return new ProtobufMessage(messageType,
|
||||
requestId,
|
||||
0,
|
||||
bytes.length,
|
||||
maxChunkSize,
|
||||
ArrayUtils.subarray(bytes, 0, maxChunkSize));
|
||||
}
|
||||
return new ProtobufMessage(messageType, requestId, 0, bytes.length, bytes.length, bytes);
|
||||
}
|
||||
|
||||
class ProtobufFragment {
|
||||
private final byte[] fragmentBytes;
|
||||
private final int totalLength;
|
||||
|
||||
public ProtobufFragment(byte[] fragmentBytes) {
|
||||
this.fragmentBytes = fragmentBytes;
|
||||
this.totalLength = fragmentBytes.length;
|
||||
}
|
||||
|
||||
public ProtobufFragment(ProtobufMessage message) {
|
||||
if (message.getDataOffset() != 0)
|
||||
throw new IllegalArgumentException("Cannot create fragment if message is not the first of the sequence");
|
||||
this.fragmentBytes = message.getMessageBytes();
|
||||
this.totalLength = message.getTotalProtobufLength();
|
||||
}
|
||||
|
||||
public ProtobufFragment(ProtobufFragment existing, ProtobufMessage toMerge) {
|
||||
if (toMerge.getDataOffset() != existing.fragmentBytes.length)
|
||||
throw new IllegalArgumentException("Cannot merge fragment: incoming message has different offset than needed");
|
||||
this.fragmentBytes = ArrayUtils.addAll(existing.fragmentBytes, toMerge.getMessageBytes());
|
||||
this.totalLength = existing.totalLength;
|
||||
}
|
||||
|
||||
public ProtobufMessage getNextChunk(ProtobufStatusMessage protobufStatusMessage) {
|
||||
int start = protobufStatusMessage.getDataOffset() + maxChunkSize;
|
||||
int length = Math.min(maxChunkSize, this.fragmentBytes.length - start);
|
||||
|
||||
return new ProtobufMessage(protobufStatusMessage.getMessageType(),
|
||||
protobufStatusMessage.getRequestId(),
|
||||
start,
|
||||
this.totalLength,
|
||||
length,
|
||||
ArrayUtils.subarray(this.fragmentBytes, start, start + length));
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return totalLength == fragmentBytes.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class CobsCoDec {
|
||||
private static final long BUFFER_TIMEOUT = 1500L; // turn this value up while debugging
|
||||
private final ByteBuffer byteBuffer = ByteBuffer.allocate(1000);
|
||||
private long lastUpdate;
|
||||
private byte[] cobsDecodedMessage;
|
||||
|
||||
/**
|
||||
* Accumulates received bytes in a local buffer, clearing it after a timeout, and attempts to
|
||||
* parse it.
|
||||
*
|
||||
* @param bytes
|
||||
*/
|
||||
public void receivedBytes(byte[] bytes) {
|
||||
final long now = System.currentTimeMillis();
|
||||
if ((now - lastUpdate) > BUFFER_TIMEOUT) {
|
||||
reset();
|
||||
}
|
||||
lastUpdate = now;
|
||||
|
||||
byteBuffer.put(bytes);
|
||||
decode();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
cobsDecodedMessage = null;
|
||||
byteBuffer.clear();
|
||||
}
|
||||
|
||||
public byte[] retrieveMessage() {
|
||||
final byte[] resultPacket = cobsDecodedMessage;
|
||||
cobsDecodedMessage = null;
|
||||
return resultPacket;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* COBS decoding algorithm variant, which relies on a leading and a trailing 0 byte (the former
|
||||
* is not part of default implementations).
|
||||
* This function removes the complete message from the internal buffer, if it could be decoded.
|
||||
*/
|
||||
private void decode() {
|
||||
if (cobsDecodedMessage != null) {
|
||||
// packet is waiting, unable to parse more
|
||||
return;
|
||||
}
|
||||
if (byteBuffer.position() < 4) {
|
||||
// minimal payload length including the padding
|
||||
return;
|
||||
}
|
||||
if (0 != byteBuffer.get(byteBuffer.position() - 1))
|
||||
return; //no 0x00 at the end, hence no full packet
|
||||
byteBuffer.position(byteBuffer.position() - 1); //don't process the trailing 0
|
||||
byteBuffer.flip();
|
||||
if (0 != byteBuffer.get())
|
||||
return; //no 0x00 at the start
|
||||
ByteBuffer decodedBytesBuffer = ByteBuffer.allocate(byteBuffer.limit()); //leading and trailing 0x00 bytes
|
||||
while (byteBuffer.hasRemaining()) {
|
||||
byte code = byteBuffer.get();
|
||||
if (code == 0) {
|
||||
break;
|
||||
}
|
||||
int codeValue = code & 0xFF;
|
||||
int payloadSize = codeValue - 1;
|
||||
for (int i = 0; i < payloadSize; i++) {
|
||||
decodedBytesBuffer.put(byteBuffer.get());
|
||||
}
|
||||
if (codeValue != 0xFF && byteBuffer.hasRemaining()) {
|
||||
decodedBytesBuffer.put((byte) 0); // Append a zero byte after the payload
|
||||
}
|
||||
}
|
||||
|
||||
decodedBytesBuffer.flip();
|
||||
cobsDecodedMessage = new byte[decodedBytesBuffer.remaining()];
|
||||
decodedBytesBuffer.get(cobsDecodedMessage);
|
||||
byteBuffer.compact();
|
||||
}
|
||||
|
||||
// this implementation of COBS relies on a leading and a trailing 0 byte (the former is not part of default implementations)
|
||||
public byte[] encode(byte[] data) {
|
||||
ByteBuffer encodedBytesBuffer = ByteBuffer.allocate((data.length * 2) + 1); // Maximum expansion
|
||||
|
||||
encodedBytesBuffer.put((byte) 0);// Garmin initial padding
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
|
||||
while (buffer.position() < buffer.limit()) {
|
||||
int startPos = buffer.position();
|
||||
int zeroIndex = buffer.position();
|
||||
|
||||
while (buffer.hasRemaining() && buffer.get() != 0) {
|
||||
zeroIndex++;
|
||||
}
|
||||
|
||||
int payloadSize = zeroIndex - startPos;
|
||||
|
||||
while (payloadSize > 0xFE) {
|
||||
encodedBytesBuffer.put((byte) 0xFF); // Maximum payload size indicator
|
||||
for (int i = 0; i < 0xFE; i++) {
|
||||
encodedBytesBuffer.put(data[startPos + i]);
|
||||
}
|
||||
payloadSize -= 0xFE;
|
||||
startPos += 0xFE;
|
||||
}
|
||||
|
||||
encodedBytesBuffer.put((byte) (payloadSize + 1));
|
||||
|
||||
for (int i = startPos; i < zeroIndex; i++) {
|
||||
encodedBytesBuffer.put(data[i]);
|
||||
}
|
||||
|
||||
if (buffer.hasRemaining()) {
|
||||
zeroIndex++; // Include the zero byte in the next block
|
||||
}
|
||||
|
||||
if (!buffer.hasRemaining() && payloadSize == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.position(zeroIndex);
|
||||
}
|
||||
|
||||
encodedBytesBuffer.put((byte) 0); // Append a zero byte to indicate end of encoding
|
||||
encodedBytesBuffer.flip();
|
||||
|
||||
byte[] encodedBytes = new byte[encodedBytesBuffer.remaining()];
|
||||
encodedBytesBuffer.get(encodedBytes);
|
||||
|
||||
return encodedBytes;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
|
||||
public interface ICommunicator {
|
||||
void sendMessage(byte[] message);
|
||||
|
||||
void initializeDevice(TransactionBuilder builder);
|
||||
|
||||
boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);
|
||||
|
||||
interface Callback {
|
||||
void onMessage(byte[] message);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.ICommunicator;
|
||||
|
||||
public class CommunicatorV1 implements ICommunicator {
|
||||
public static final UUID UUID_SERVICE_GARMIN_GFDI = VivomoveConstants.UUID_SERVICE_GARMIN_GFDI;
|
||||
|
||||
private final GarminSupport mSupport;
|
||||
|
||||
public CommunicatorV1(final GarminSupport garminSupport) {
|
||||
this.mSupport = garminSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeDevice(final TransactionBuilder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(final byte[] message) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v2;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.CobsCoDec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.ICommunicator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class CommunicatorV2 implements ICommunicator {
|
||||
public static final UUID UUID_SERVICE_GARMIN_ML_GFDI = UUID.fromString("6A4E2800-667B-11E3-949A-0800200C9A66"); //VivomoveConstants.UUID_SERVICE_GARMIN_ML_GFDI;
|
||||
public static final UUID UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND = UUID.fromString("6a4e2822-667b-11e3-949a-0800200c9a66"); //VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND;
|
||||
public static final UUID UUID_CHARACTERISTIC_GARMIN_ML_GFDI_RECEIVE = UUID.fromString("6a4e2812-667b-11e3-949a-0800200c9a66"); //VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_ML_GFDI_RECEIVE;
|
||||
|
||||
public static final int MAX_WRITE_SIZE = 20; //VivomoveConstants.MAX_WRITE_SIZE
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CommunicatorV2.class);
|
||||
public final CobsCoDec cobsCoDec;
|
||||
private final GarminSupport mSupport;
|
||||
private final long gadgetBridgeClientID = 2L;
|
||||
private int gfdiHandle = 0;
|
||||
|
||||
public CommunicatorV2(final GarminSupport garminSupport) {
|
||||
this.mSupport = garminSupport;
|
||||
this.cobsCoDec = new CobsCoDec();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeDevice(final TransactionBuilder builder) {
|
||||
builder.notify(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_RECEIVE), true);
|
||||
builder.write(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND), closeAllServices());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(final byte[] message) {
|
||||
if (null == message)
|
||||
return;
|
||||
if (0 == gfdiHandle) {
|
||||
LOG.error("CANNOT SENT GFDI MESSAGE, HANDLE NOT YET SET. MESSAGE {}", message);
|
||||
return;
|
||||
}
|
||||
final byte[] payload = cobsCoDec.encode(message);
|
||||
// LOG.debug("SENDING MESSAGE: {} - COBS ENCODED: {}", GB.hexdump(message), GB.hexdump(payload));
|
||||
final TransactionBuilder builder = new TransactionBuilder("sendMessage()");
|
||||
int remainingBytes = payload.length;
|
||||
if (remainingBytes > MAX_WRITE_SIZE - 1) {
|
||||
int position = 0;
|
||||
while (remainingBytes > 0) {
|
||||
final byte[] fragment = Arrays.copyOfRange(payload, position, position + Math.min(remainingBytes, MAX_WRITE_SIZE - 1));
|
||||
builder.write(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND), ArrayUtils.addAll(new byte[]{(byte) gfdiHandle}, fragment));
|
||||
position += fragment.length;
|
||||
remainingBytes -= fragment.length;
|
||||
}
|
||||
} else {
|
||||
builder.write(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND), ArrayUtils.addAll(new byte[]{(byte) gfdiHandle}, payload));
|
||||
}
|
||||
builder.queue(this.mSupport.getQueue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
||||
ByteBuffer message = ByteBuffer.wrap(characteristic.getValue()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
// LOG.debug("RECEIVED: {}", GB.hexdump(message.array()));
|
||||
final byte handle = message.get();
|
||||
if (0x00 == handle) { //handle management message
|
||||
|
||||
final byte type = message.get();
|
||||
final long incomingClientID = message.getLong();
|
||||
|
||||
if (incomingClientID != this.gadgetBridgeClientID) {
|
||||
LOG.debug("Ignoring incoming message, client ID is not ours. Message: {}", GB.hexdump(message.array()));
|
||||
}
|
||||
RequestType requestType = RequestType.fromCode(type);
|
||||
if (null == requestType) {
|
||||
LOG.error("Unknown request type. Message: {}", message.array());
|
||||
return true;
|
||||
}
|
||||
switch (requestType) {
|
||||
case REGISTER_ML_REQ: //register service request
|
||||
case CLOSE_HANDLE_REQ: //close handle request
|
||||
case CLOSE_ALL_REQ: //close all handles request
|
||||
case UNK_REQ: //unknown request
|
||||
LOG.warn("Received handle request, expecting responses. Message: {}", message.array());
|
||||
case REGISTER_ML_RESP: //register service response
|
||||
LOG.debug("Received register response. Message: {}", message.array());
|
||||
final short registeredService = message.getShort();
|
||||
final byte status = message.get();
|
||||
if (0 == status && 1 == registeredService) { //success
|
||||
this.gfdiHandle = message.get();
|
||||
}
|
||||
break;
|
||||
case CLOSE_HANDLE_RESP: //close handle response
|
||||
LOG.debug("Received close handle response. Message: {}", message.array());
|
||||
break;
|
||||
case CLOSE_ALL_RESP: //close all handles response
|
||||
LOG.debug("Received close all handles response. Message: {}", message.array());
|
||||
new TransactionBuilder("open GFDI")
|
||||
.write(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND), registerGFDI())
|
||||
.queue(this.mSupport.getQueue());
|
||||
break;
|
||||
case UNK_RESP: //unknown response
|
||||
LOG.debug("Received unknown. Message: {}", message.array());
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (this.gfdiHandle == handle) {
|
||||
|
||||
byte[] partial = new byte[message.remaining()];
|
||||
message.get(partial);
|
||||
this.cobsCoDec.receivedBytes(partial);
|
||||
|
||||
this.mSupport.onMessage(this.cobsCoDec.retrieveMessage());
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected byte[] closeAllServices() {
|
||||
ByteBuffer toSend = ByteBuffer.allocate(13);
|
||||
toSend.order(ByteOrder.BIG_ENDIAN);
|
||||
toSend.putShort((short) RequestType.CLOSE_ALL_REQ.ordinal()); //close all services
|
||||
toSend.order(ByteOrder.LITTLE_ENDIAN);
|
||||
toSend.putLong(this.gadgetBridgeClientID);
|
||||
toSend.putShort((short) 0);
|
||||
return toSend.array();
|
||||
}
|
||||
|
||||
protected byte[] registerGFDI() {
|
||||
ByteBuffer toSend = ByteBuffer.allocate(13);
|
||||
toSend.order(ByteOrder.BIG_ENDIAN);
|
||||
toSend.putShort((short) RequestType.REGISTER_ML_REQ.ordinal()); //register service request
|
||||
toSend.order(ByteOrder.LITTLE_ENDIAN);
|
||||
toSend.putLong(this.gadgetBridgeClientID);
|
||||
toSend.putShort((short) 1); //service GFDI
|
||||
return toSend.array();
|
||||
}
|
||||
|
||||
enum RequestType {
|
||||
REGISTER_ML_REQ,
|
||||
REGISTER_ML_RESP,
|
||||
CLOSE_HANDLE_REQ,
|
||||
CLOSE_HANDLE_RESP,
|
||||
UNK_HANDLE,
|
||||
CLOSE_ALL_REQ,
|
||||
CLOSE_ALL_RESP,
|
||||
UNK_REQ,
|
||||
UNK_RESP;
|
||||
|
||||
public static RequestType fromCode(final int code) {
|
||||
for (final RequestType requestType : RequestType.values()) {
|
||||
if (requestType.ordinal() == code) {
|
||||
return requestType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/* Copyright (C) 2023-2024 Petr Kadlec
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class ChecksumCalculator {
|
||||
private static final int[] CONSTANTS = {0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400};
|
||||
|
||||
private ChecksumCalculator() {
|
||||
}
|
||||
|
||||
public static int computeCrc(byte[] data, int offset, int length) {
|
||||
return computeCrc(0, data, offset, length);
|
||||
}
|
||||
|
||||
public static int computeCrc(ByteBuffer byteBuffer, int offset, int length) {
|
||||
byteBuffer.rewind();
|
||||
byte[] data = new byte[length];
|
||||
byteBuffer.get(data);
|
||||
return computeCrc(0, data, offset, length);
|
||||
}
|
||||
|
||||
public static int computeCrc(int initialCrc, byte[] data, int offset, int length) {
|
||||
int crc = initialCrc;
|
||||
for (int i = offset; i < offset + length; ++i) {
|
||||
int b = data[i];
|
||||
crc = (((crc >> 4) & 4095) ^ CONSTANTS[crc & 15]) ^ CONSTANTS[b & 15];
|
||||
crc = (((crc >> 4) & 4095) ^ CONSTANTS[crc & 15]) ^ CONSTANTS[(b >> 4) & 15];
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
|
||||
|
||||
public class ConfigurationMessage extends GFDIMessage {
|
||||
public final Set<GarminCapability> OUR_CAPABILITIES = GarminCapability.ALL_CAPABILITIES;
|
||||
private final byte[] incomingConfigurationPayload;
|
||||
private final int messageType;
|
||||
private final byte[] ourConfigurationPayload = GarminCapability.setToBinary(OUR_CAPABILITIES);
|
||||
|
||||
public ConfigurationMessage(int messageType, byte[] configurationPayload) {
|
||||
this.messageType = messageType;
|
||||
if (configurationPayload.length > 255)
|
||||
throw new IllegalArgumentException("Too long payload");
|
||||
this.incomingConfigurationPayload = configurationPayload;
|
||||
|
||||
Set<GarminCapability> capabilities = GarminCapability.setFromBinary(configurationPayload);
|
||||
LOG.info("Received configuration message; capabilities: {}", GarminCapability.setToString(capabilities));
|
||||
|
||||
this.statusMessage = this.getStatusMessage(messageType);
|
||||
}
|
||||
|
||||
public static ConfigurationMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final int endOfPayload = reader.readByte();
|
||||
ConfigurationMessage configurationMessage = new ConfigurationMessage(messageType, reader.readBytes(endOfPayload - reader.getPosition()));
|
||||
reader.warnIfLeftover();
|
||||
return configurationMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // placeholder for packet size
|
||||
writer.writeShort(messageType);
|
||||
writer.writeByte(ourConfigurationPayload.length);
|
||||
writer.writeBytes(ourConfigurationPayload);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminTimeUtils;
|
||||
|
||||
public class CurrentTimeRequestMessage extends GFDIMessage {
|
||||
private final int referenceID;
|
||||
private final int messageType;
|
||||
|
||||
public CurrentTimeRequestMessage(int messageType, int referenceID) {
|
||||
this.messageType = messageType;
|
||||
this.referenceID = referenceID;
|
||||
this.statusMessage = this.getStatusMessage(messageType);
|
||||
}
|
||||
|
||||
public static CurrentTimeRequestMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final int referenceID = reader.readInt();
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new CurrentTimeRequestMessage(messageType, referenceID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
long now = System.currentTimeMillis();
|
||||
final TimeZone timeZone = TimeZone.getDefault();
|
||||
final Calendar calendar = Calendar.getInstance(timeZone);
|
||||
calendar.setTimeInMillis(now);
|
||||
int dstOffset = calendar.get(Calendar.DST_OFFSET) / 1000;
|
||||
int timeZoneOffset = timeZone.getOffset(now) / 1000;
|
||||
int garminTimestamp = GarminTimeUtils.javaMillisToGarminTimestamp(now);
|
||||
|
||||
LOG.info("Processing current time request #{}: time={}, DST={}, ofs={}", referenceID, garminTimestamp, dstOffset, timeZoneOffset);
|
||||
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(GarminMessage.RESPONSE.getId());
|
||||
writer.writeShort(messageType);
|
||||
writer.writeByte(Status.ACK.ordinal());
|
||||
writer.writeInt(referenceID);
|
||||
writer.writeInt(garminTimestamp);
|
||||
writer.writeInt(timeZoneOffset);
|
||||
// TODO: next DST start/end
|
||||
writer.writeInt(0);
|
||||
writer.writeInt(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
|
||||
public class DeviceInformationMessage extends GFDIMessage {
|
||||
|
||||
final int ourUnitNumber = -1;
|
||||
final int ourSoftwareVersion = 7791;
|
||||
final int ourMaxPacketSize = -1;
|
||||
private final int messageType;
|
||||
private final int incomingProtocolVersion;
|
||||
private final int ourProtocolVersion = 150;
|
||||
private final int incomingProductNumber;
|
||||
private final int ourProductNumber = -1;
|
||||
private final String incomingUnitNumber;
|
||||
private final int incomingSoftwareVersion;
|
||||
private final int incomingMaxPacketSize;
|
||||
private final String bluetoothFriendlyName;
|
||||
private final String deviceName;
|
||||
private final String deviceModel;
|
||||
// dual-pairing flags & MAC addresses...
|
||||
|
||||
public DeviceInformationMessage(int messageType, int protocolVersion, int productNumber, String unitNumber, int softwareVersion, int maxPacketSize, String bluetoothFriendlyName, String deviceName, String deviceModel) {
|
||||
this.messageType = messageType;
|
||||
this.incomingProtocolVersion = protocolVersion;
|
||||
this.incomingProductNumber = productNumber;
|
||||
this.incomingUnitNumber = unitNumber;
|
||||
this.incomingSoftwareVersion = softwareVersion;
|
||||
this.incomingMaxPacketSize = maxPacketSize;
|
||||
this.bluetoothFriendlyName = bluetoothFriendlyName;
|
||||
this.deviceName = deviceName;
|
||||
this.deviceModel = deviceModel;
|
||||
}
|
||||
|
||||
public static DeviceInformationMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final int protocolVersion = reader.readShort();
|
||||
final int productNumber = reader.readShort();
|
||||
final String unitNumber = Long.toString(reader.readInt() & 0xFFFFFFFFL);
|
||||
final int softwareVersion = reader.readShort();
|
||||
final int maxPacketSize = reader.readShort();
|
||||
final String bluetoothFriendlyName = reader.readString();
|
||||
final String deviceName = reader.readString();
|
||||
final String deviceModel = reader.readString();
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new DeviceInformationMessage(messageType, protocolVersion, productNumber, unitNumber, softwareVersion, maxPacketSize, bluetoothFriendlyName, deviceName, deviceModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
|
||||
final int protocolFlags = this.incomingProtocolVersion / 100 == 1 ? 1 : 0;
|
||||
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // placeholder for packet size
|
||||
writer.writeShort(GarminMessage.RESPONSE.getId());
|
||||
writer.writeShort(messageType);
|
||||
writer.writeByte(Status.ACK.ordinal());
|
||||
writer.writeShort(ourProtocolVersion);
|
||||
writer.writeShort(ourProductNumber);
|
||||
writer.writeInt(ourUnitNumber);
|
||||
writer.writeShort(ourSoftwareVersion);
|
||||
writer.writeShort(ourMaxPacketSize);
|
||||
writer.writeString(BluetoothAdapter.getDefaultAdapter().getName());
|
||||
writer.writeString(Build.MANUFACTURER);
|
||||
writer.writeString(Build.DEVICE);
|
||||
writer.writeByte(protocolFlags);
|
||||
return true;
|
||||
}
|
||||
|
||||
public GBDeviceEventVersionInfo getGBDeviceEvent() {
|
||||
|
||||
LOG.info("Received device information: protocol {}, product {}, unit {}, SW {}, max packet {}, BT name {}, device name {}, device model {}", incomingProtocolVersion, incomingProductNumber, incomingUnitNumber, getSoftwareVersionStr(), incomingMaxPacketSize, bluetoothFriendlyName, deviceName, deviceModel);
|
||||
|
||||
GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||
versionCmd.fwVersion = getSoftwareVersionStr();
|
||||
versionCmd.hwVersion = deviceModel;
|
||||
return versionCmd;
|
||||
}
|
||||
|
||||
private String getSoftwareVersionStr() {
|
||||
int softwareVersionMajor = incomingSoftwareVersion / 100;
|
||||
int softwareVersionMinor = incomingSoftwareVersion % 100;
|
||||
return String.format(Locale.ROOT, "%d.%02d", softwareVersionMajor, softwareVersionMinor);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
||||
|
||||
public class FindMyPhoneRequestMessage extends GFDIMessage {
|
||||
private final int duration;
|
||||
private final int messageType;
|
||||
|
||||
public FindMyPhoneRequestMessage(int messageType, int duration) {
|
||||
this.messageType = messageType;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public static FindMyPhoneRequestMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final int duration = reader.readByte();
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new FindMyPhoneRequestMessage(messageType, duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceEvent getGBDeviceEvent() {
|
||||
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
findPhoneEvent.event = messageType == GarminMessage.FIND_MY_PHONE.getId() ? GBDeviceEventFindPhone.Event.START : GBDeviceEventFindPhone.Event.STOP;
|
||||
return findPhoneEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
|
||||
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;
|
||||
public static final int MESSAGE_FILE_TRANSFER_DATA = 5004;
|
||||
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);
|
||||
protected GFDIStatusMessage statusMessage;
|
||||
|
||||
public static GFDIMessage parseIncoming(byte[] message) {
|
||||
final MessageReader messageReader = new MessageReader(message);
|
||||
|
||||
final int messageType = messageReader.readShort();
|
||||
try {
|
||||
// Class<? extends GFDIMessage> objectClass = GarminMessage.fromId(messageType);
|
||||
Method m = GarminMessage.fromId(messageType).getMethod("parseIncoming", MessageReader.class, int.class);
|
||||
return GarminMessage.fromId(messageType).cast(m.invoke(null, messageReader, messageType));
|
||||
} catch (Exception e) {
|
||||
LOG.error("UNHANDLED GFDI MESSAGE TYPE {}, MESSAGE {}", messageType, message);
|
||||
return new UnhandledMessage(messageType);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract boolean generateOutgoing();
|
||||
|
||||
public byte[] getOutgoingMessage() {
|
||||
response.clear();
|
||||
boolean toSend = generateOutgoing();
|
||||
if (!toSend)
|
||||
return null;
|
||||
addLengthAndChecksum();
|
||||
response.flip();
|
||||
|
||||
final byte[] packet = new byte[response.limit()];
|
||||
response.get(packet);
|
||||
return packet;
|
||||
}
|
||||
|
||||
protected GFDIStatusMessage getStatusMessage(int messageType) {
|
||||
return new GenericStatusMessage(messageType, Status.ACK);
|
||||
}
|
||||
|
||||
public GBDeviceEvent getGBDeviceEvent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] getAckBytestream() {
|
||||
if (null == this.statusMessage) {
|
||||
return null;
|
||||
}
|
||||
return this.statusMessage.getOutgoingMessage();
|
||||
}
|
||||
|
||||
private void addLengthAndChecksum() {
|
||||
response.putShort(0, (short) (response.position() + 2));
|
||||
response.putShort((short) ChecksumCalculator.computeCrc(response.asReadOnlyBuffer(), 0, response.position()));
|
||||
}
|
||||
|
||||
public enum GarminMessage {
|
||||
RESPONSE(5000, GFDIStatusMessage.class), //TODO: STATUS is a better name?
|
||||
SYSTEM_EVENT(5030, SystemEventMessage.class),
|
||||
DEVICE_INFORMATION(5024, DeviceInformationMessage.class),
|
||||
FIND_MY_PHONE(5039, FindMyPhoneRequestMessage.class),
|
||||
CANCEL_FIND_MY_PHONE(5040, FindMyPhoneRequestMessage.class),
|
||||
MUSIC_CONTROL(5041, MusicControlMessage.class),
|
||||
MUSIC_CONTROL_CAPABILITIES(5042, MusicControlCapabilitiesMessage.class),
|
||||
PROTOBUF_REQUEST(5043, ProtobufMessage.class),
|
||||
PROTOBUF_RESPONSE(5044, ProtobufMessage.class),
|
||||
MUSIC_CONTROL_ENTITY_UPDATE(5049, MusicControlEntityUpdateMessage.class),
|
||||
CONFIGURATION(5050, ConfigurationMessage.class),
|
||||
CURRENT_TIME_REQUEST(5052, CurrentTimeRequestMessage.class),
|
||||
;
|
||||
private final Class<? extends GFDIMessage> objectClass;
|
||||
private final int id;
|
||||
|
||||
GarminMessage(int id, Class<? extends GFDIMessage> objectClass) {
|
||||
this.id = id;
|
||||
this.objectClass = objectClass;
|
||||
}
|
||||
|
||||
public static Class<? extends GFDIMessage> fromId(final int id) {
|
||||
for (final GarminMessage garminMessage : GarminMessage.values()) {
|
||||
if (garminMessage.getId() == id) {
|
||||
return garminMessage.getObjectClass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private Class<? extends GFDIMessage> getObjectClass() {
|
||||
return objectClass;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
ACK,
|
||||
NAK,
|
||||
UNSUPPORTED,
|
||||
DECODE_ERROR,
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
public abstract class GFDIStatusMessage extends GFDIMessage {
|
||||
Status status;
|
||||
|
||||
public static GFDIStatusMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final int requestMessageType = reader.readShort();
|
||||
if (GarminMessage.PROTOBUF_REQUEST.getId() == requestMessageType || GarminMessage.PROTOBUF_RESPONSE.getId() == requestMessageType) {
|
||||
return ProtobufStatusMessage.parseIncoming(reader, messageType);
|
||||
} else {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new GenericStatusMessage(messageType, status);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class GenericStatusMessage extends GFDIStatusMessage {
|
||||
|
||||
private final int messageType;
|
||||
private final Status status;
|
||||
|
||||
public GenericStatusMessage(int originalRequestID, Status status) {
|
||||
this.messageType = originalRequestID;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(GarminMessage.RESPONSE.getId());
|
||||
writer.writeShort(messageType);
|
||||
writer.writeByte(status.ordinal());
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class MessageReader {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(MessageReader.class);
|
||||
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
private final int payloadSize;
|
||||
|
||||
public MessageReader(byte[] data) {
|
||||
this.byteBuffer = ByteBuffer.wrap(data);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
this.payloadSize = readShort();
|
||||
|
||||
checkSize();
|
||||
checkCRC();
|
||||
}
|
||||
|
||||
public void setByteOrder(ByteOrder byteOrder) {
|
||||
this.byteBuffer.order(byteOrder);
|
||||
}
|
||||
|
||||
public boolean isEof() {
|
||||
return !byteBuffer.hasRemaining();
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
public void skip(int offset) {
|
||||
if (byteBuffer.remaining() < offset) throw new IllegalStateException();
|
||||
byteBuffer.position(byteBuffer.position() + offset);
|
||||
}
|
||||
|
||||
public int readByte() {
|
||||
if (!byteBuffer.hasRemaining()) throw new IllegalStateException();
|
||||
|
||||
return Byte.toUnsignedInt(byteBuffer.get());
|
||||
}
|
||||
|
||||
public int readShort() {
|
||||
if (byteBuffer.remaining() < 2) throw new IllegalStateException();
|
||||
|
||||
return Short.toUnsignedInt(byteBuffer.getShort());
|
||||
}
|
||||
|
||||
public int readInt() {
|
||||
if (byteBuffer.remaining() < 4) throw new IllegalStateException();
|
||||
|
||||
return byteBuffer.getInt();
|
||||
}
|
||||
|
||||
public long readLong() {
|
||||
if (byteBuffer.remaining() < 8) throw new IllegalStateException();
|
||||
|
||||
return byteBuffer.getLong();
|
||||
}
|
||||
|
||||
public float readFloat32() {
|
||||
if (byteBuffer.remaining() < 4) throw new IllegalStateException();
|
||||
|
||||
return byteBuffer.getFloat();
|
||||
}
|
||||
|
||||
public double readFloat64() {
|
||||
if (byteBuffer.remaining() < 8) throw new IllegalStateException();
|
||||
|
||||
return byteBuffer.getDouble();
|
||||
}
|
||||
|
||||
public String readString() {
|
||||
final int size = readByte();
|
||||
byte[] bytes = new byte[size];
|
||||
if (byteBuffer.remaining() < size) throw new IllegalStateException();
|
||||
byteBuffer.get(bytes);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public byte[] readBytes(int size) {
|
||||
byte[] bytes = new byte[size];
|
||||
|
||||
if (byteBuffer.remaining() < size) throw new IllegalStateException();
|
||||
byteBuffer.get(bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private int getCapacity() {
|
||||
return byteBuffer.capacity();
|
||||
}
|
||||
|
||||
|
||||
private void checkSize() {
|
||||
if (payloadSize > getCapacity()) {
|
||||
LOG.error("Received GFDI packet with invalid length: {} vs {}", payloadSize, getCapacity());
|
||||
throw new IllegalArgumentException("Received GFDI packet with invalid length");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCRC() {
|
||||
final int crc = Short.toUnsignedInt(byteBuffer.getShort(payloadSize - 2));
|
||||
final int correctCrc = ChecksumCalculator.computeCrc(byteBuffer.asReadOnlyBuffer(), 0, payloadSize - 2);
|
||||
if (crc != correctCrc) {
|
||||
LOG.error("Received GFDI packet with invalid CRC: {} vs {}", crc, correctCrc);
|
||||
throw new IllegalArgumentException("Received GFDI packet with invalid CRC");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void warnIfLeftover() {
|
||||
if (byteBuffer.hasRemaining() && byteBuffer.position() < (byteBuffer.limit() - 2)) {
|
||||
int numBytes = (byteBuffer.limit() - 2) - byteBuffer.position();
|
||||
byte[] leftover = new byte[numBytes];
|
||||
byteBuffer.get(leftover);
|
||||
LOG.warn("Leftover bytes when parsing message. Bytes: {}, complete message: {}", GB.hexdump(leftover), GB.hexdump(byteBuffer.array()));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class MessageWriter {
|
||||
private static final int DEFAULT_BUFFER_SIZE = 16384;
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public MessageWriter() {
|
||||
this(DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
public MessageWriter(int bufferSize) {
|
||||
this.byteBuffer = ByteBuffer.allocate(bufferSize);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
public MessageWriter(ByteBuffer byteBuffer) {
|
||||
this.byteBuffer = byteBuffer;
|
||||
this.byteBuffer.clear();
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
public void setByteOrder(ByteOrder byteOrder) {
|
||||
this.byteBuffer.order(byteOrder);
|
||||
}
|
||||
|
||||
public void writeByte(int value) {
|
||||
byteBuffer.put((byte) value);
|
||||
}
|
||||
|
||||
public void writeShort(int value) {
|
||||
byteBuffer.putShort((short) value);
|
||||
}
|
||||
|
||||
public void writeInt(int value) {
|
||||
byteBuffer.putInt(value);
|
||||
}
|
||||
|
||||
public void writeLong(long value) {
|
||||
byteBuffer.putLong(value);
|
||||
}
|
||||
|
||||
public void writeFloat32(float value) {
|
||||
byteBuffer.putFloat(value);
|
||||
}
|
||||
|
||||
public void writeFloat64(double value) {
|
||||
byteBuffer.putDouble(value);
|
||||
}
|
||||
|
||||
public void writeString(String value) {
|
||||
final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
final int size = bytes.length;
|
||||
if (size > 255) throw new IllegalArgumentException("Too long string");
|
||||
|
||||
if (byteBuffer.position() + 1 + size > byteBuffer.capacity())
|
||||
throw new IllegalStateException();
|
||||
byteBuffer.put((byte) size);
|
||||
byteBuffer.put(bytes);
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
//TODO: implement the correct flip()/compat() logic
|
||||
return byteBuffer.hasRemaining() ? Arrays.copyOf(byteBuffer.array(), byteBuffer.position()) : byteBuffer.array();
|
||||
}
|
||||
|
||||
public byte[] peekBytes() {
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
public void writeBytes(byte[] bytes) {
|
||||
writeBytes(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public void writeBytes(byte[] bytes, int offset, int size) {
|
||||
if (byteBuffer.position() + size > byteBuffer.capacity()) throw new IllegalStateException();
|
||||
byteBuffer.put(Arrays.copyOfRange(bytes, offset, offset + size));
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class MusicControlCapabilitiesMessage extends GFDIMessage {
|
||||
|
||||
private final int supportedCapabilities;
|
||||
private final GarminMusicControlCommand[] commands = GarminMusicControlCommand.values();
|
||||
private final int messageType;
|
||||
|
||||
public MusicControlCapabilitiesMessage(int messageType, int supportedCapabilities) {
|
||||
this.messageType = messageType;
|
||||
this.supportedCapabilities = supportedCapabilities;
|
||||
this.statusMessage = this.getStatusMessage(messageType);
|
||||
}
|
||||
|
||||
public static MusicControlCapabilitiesMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final int supportedCapabilities = reader.readByte();
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new MusicControlCapabilitiesMessage(messageType, supportedCapabilities);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
if (commands.length > 255)
|
||||
throw new IllegalArgumentException("Too many supported commands");
|
||||
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(GarminMessage.RESPONSE.getId());
|
||||
writer.writeShort(messageType);
|
||||
writer.writeByte(Status.ACK.ordinal());
|
||||
writer.writeByte(commands.length);
|
||||
for (GarminMusicControlCommand command : commands) {
|
||||
writer.writeByte(command.ordinal());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
enum GarminMusicControlCommand {
|
||||
TOGGLE_PLAY_PAUSE,
|
||||
SKIP_TO_NEXT_ITEM,
|
||||
SKIP_TO_PREVIOUS_ITEM,
|
||||
VOLUME_UP,
|
||||
VOLUME_DOWN,
|
||||
PLAY,
|
||||
PAUSE,
|
||||
SKIP_FORWARD,
|
||||
SKIP_BACKWARDS
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
public class MusicControlEntityUpdateMessage extends GFDIMessage {
|
||||
|
||||
private final Map<MusicEntity, String> attributes;
|
||||
|
||||
public MusicControlEntityUpdateMessage(Map<MusicEntity, String> attributes) {
|
||||
|
||||
this.attributes = attributes;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(GarminMessage.MUSIC_CONTROL_ENTITY_UPDATE.getId());
|
||||
|
||||
for (Map.Entry<MusicEntity, String> entry : attributes.entrySet()) {
|
||||
MusicEntity a = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (null == value)
|
||||
value = "";
|
||||
byte[] v = value.getBytes(StandardCharsets.UTF_8);
|
||||
if (v.length > 252) throw new IllegalArgumentException("Too long value");
|
||||
|
||||
writer.writeByte((v.length + 3) & 0xff); //the three following bytes
|
||||
writer.writeByte(a.getEntityId());
|
||||
writer.writeByte(a.ordinal());
|
||||
writer.writeByte(0);//TODO what is this?
|
||||
writer.writeBytes(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum PLAYER implements MusicEntity {
|
||||
NAME,
|
||||
PLAYBACK_INFO,
|
||||
VOLUME;
|
||||
|
||||
@Override
|
||||
public int getEntityId() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum QUEUE implements MusicEntity {
|
||||
INDEX,
|
||||
COUNT,
|
||||
SHUFFLE,
|
||||
REPEAT;
|
||||
|
||||
@Override
|
||||
public int getEntityId() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public enum TRACK implements MusicEntity {
|
||||
ARTIST,
|
||||
ALBUM,
|
||||
TITLE,
|
||||
DURATION;
|
||||
|
||||
@Override
|
||||
public int getEntityId() {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public interface MusicEntity {
|
||||
int getEntityId();
|
||||
|
||||
int ordinal();
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
|
||||
public class MusicControlMessage extends GFDIMessage {
|
||||
|
||||
private static final MusicControlCapabilitiesMessage.GarminMusicControlCommand[] commands = MusicControlCapabilitiesMessage.GarminMusicControlCommand.values();
|
||||
final int messageType;
|
||||
private final GBDeviceEventMusicControl event;
|
||||
|
||||
public MusicControlMessage(int messageType, MusicControlCapabilitiesMessage.GarminMusicControlCommand command) {
|
||||
this.event = new GBDeviceEventMusicControl();
|
||||
this.messageType = messageType;
|
||||
switch (command) {
|
||||
case TOGGLE_PLAY_PAUSE:
|
||||
event.event = GBDeviceEventMusicControl.Event.PLAYPAUSE;
|
||||
break;
|
||||
case SKIP_TO_NEXT_ITEM:
|
||||
event.event = GBDeviceEventMusicControl.Event.NEXT;
|
||||
break;
|
||||
case SKIP_TO_PREVIOUS_ITEM:
|
||||
event.event = GBDeviceEventMusicControl.Event.PREVIOUS;
|
||||
break;
|
||||
}
|
||||
|
||||
this.statusMessage = this.getStatusMessage(messageType);
|
||||
}
|
||||
|
||||
public static MusicControlMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
MusicControlCapabilitiesMessage.GarminMusicControlCommand command = commands[reader.readByte()];
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new MusicControlMessage(messageType, command);
|
||||
}
|
||||
|
||||
public GBDeviceEventMusicControl getGBDeviceEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufStatusMessage.ProtobufStatusCode.NO_ERROR;
|
||||
|
||||
public class ProtobufMessage extends GFDIMessage {
|
||||
|
||||
|
||||
private final int requestId;
|
||||
private final int messageType;
|
||||
private final int dataOffset;
|
||||
private final int totalProtobufLength;
|
||||
private final int protobufDataLength;
|
||||
private final byte[] messageBytes;
|
||||
private final boolean sendOutgoing;
|
||||
|
||||
public ProtobufMessage(int messageType, int requestId, int dataOffset, int totalProtobufLength, int protobufDataLength, byte[] messageBytes) {
|
||||
this(messageType, requestId, dataOffset, totalProtobufLength, protobufDataLength, messageBytes, true);
|
||||
}
|
||||
|
||||
public ProtobufMessage(int messageType, int requestId, int dataOffset, int totalProtobufLength, int protobufDataLength, byte[] messageBytes, boolean sendOutgoing) {
|
||||
this.messageType = messageType;
|
||||
this.requestId = requestId;
|
||||
this.dataOffset = dataOffset;
|
||||
this.totalProtobufLength = totalProtobufLength;
|
||||
this.protobufDataLength = protobufDataLength;
|
||||
this.messageBytes = messageBytes;
|
||||
this.sendOutgoing = sendOutgoing;
|
||||
|
||||
if (isComplete()) {
|
||||
this.statusMessage = new GenericStatusMessage(messageType, GFDIMessage.Status.ACK);
|
||||
} else {
|
||||
this.statusMessage = new ProtobufStatusMessage(messageType, GFDIMessage.Status.ACK, requestId, dataOffset, NO_ERROR, NO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public static ProtobufMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final int requestID = reader.readShort();
|
||||
final int dataOffset = reader.readInt();
|
||||
final int totalProtobufLength = reader.readInt();
|
||||
final int protobufDataLength = reader.readInt();
|
||||
final byte[] messageBytes = reader.readBytes(protobufDataLength);
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new ProtobufMessage(messageType, requestID, dataOffset, totalProtobufLength, protobufDataLength, messageBytes, false);
|
||||
}
|
||||
|
||||
public int getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public int getMessageType() {
|
||||
return messageType;
|
||||
}
|
||||
|
||||
public int getDataOffset() {
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
public int getTotalProtobufLength() {
|
||||
return totalProtobufLength;
|
||||
}
|
||||
|
||||
public byte[] getMessageBytes() {
|
||||
return messageBytes;
|
||||
}
|
||||
|
||||
public boolean isChunked() {
|
||||
return (totalProtobufLength != protobufDataLength);
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return (dataOffset == 0 && !isChunked());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(messageType);
|
||||
writer.writeShort(requestId);
|
||||
writer.writeInt(dataOffset);
|
||||
writer.writeInt(totalProtobufLength);
|
||||
writer.writeInt(protobufDataLength);
|
||||
writer.writeBytes(messageBytes);
|
||||
return sendOutgoing;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class ProtobufStatusMessage extends GFDIStatusMessage {
|
||||
|
||||
private final Status status;
|
||||
private final int requestId;
|
||||
private final int dataOffset;
|
||||
private final ProtobufStatusCode protobufStatus;
|
||||
private final ProtobufStatusCode error; //TODO: why is this duplicated?
|
||||
private final int messageType;
|
||||
private final boolean sendOutgoing;
|
||||
|
||||
public ProtobufStatusMessage(int messageType, Status status, int requestId, int dataOffset, ProtobufStatusCode protobufStatus, ProtobufStatusCode error) {
|
||||
this(messageType, status, requestId, dataOffset, protobufStatus, error, true);
|
||||
}
|
||||
public ProtobufStatusMessage(int messageType, Status status, int requestId, int dataOffset, ProtobufStatusCode protobufStatus, ProtobufStatusCode error, boolean sendOutgoing) {
|
||||
this.messageType = messageType;
|
||||
this.status = status;
|
||||
this.requestId = requestId;
|
||||
this.dataOffset = dataOffset;
|
||||
this.protobufStatus = protobufStatus;
|
||||
this.error = error;
|
||||
this.sendOutgoing = sendOutgoing;
|
||||
}
|
||||
|
||||
public static ProtobufStatusMessage parseIncoming(MessageReader reader, int messageType) {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
final int requestID = reader.readShort();
|
||||
final int dataOffset = reader.readInt();
|
||||
final ProtobufStatusCode protobufStatus = ProtobufStatusCode.fromCode(reader.readByte());
|
||||
final ProtobufStatusCode error = ProtobufStatusCode.fromCode(reader.readByte());
|
||||
|
||||
reader.warnIfLeftover();
|
||||
return new ProtobufStatusMessage(messageType, status, requestID, dataOffset, protobufStatus, error, false);
|
||||
}
|
||||
|
||||
public int getDataOffset() {
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
public ProtobufStatusCode getProtobufStatus() {
|
||||
return protobufStatus;
|
||||
}
|
||||
|
||||
public ProtobufStatusCode getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public int getMessageType() {
|
||||
return messageType;
|
||||
}
|
||||
|
||||
public int getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public boolean isOK() {
|
||||
return this.status.equals(Status.ACK) &&
|
||||
this.protobufStatus.equals(ProtobufStatusCode.NO_ERROR) &&
|
||||
this.error.equals(ProtobufStatusCode.NO_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(GarminMessage.RESPONSE.getId());
|
||||
writer.writeShort(messageType);
|
||||
writer.writeByte(status.ordinal());
|
||||
writer.writeShort(requestId);
|
||||
writer.writeInt(dataOffset);
|
||||
writer.writeByte(protobufStatus.code);
|
||||
writer.writeByte(error.code);
|
||||
return sendOutgoing;
|
||||
}
|
||||
|
||||
protected Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public enum ProtobufStatusCode {
|
||||
NO_ERROR(0),
|
||||
UNKNOWN_ERROR(1),
|
||||
UNKNOWN_REQUEST_ID(100),
|
||||
DUPLICATE_PACKET(101),
|
||||
MISSING_PACKET(102),
|
||||
EXCEEDED_TOTAL_PROTOBUF_LENGTH(103),
|
||||
PROTOBUF_PARSE_ERROR(200),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
ProtobufStatusCode(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ProtobufStatusCode fromCode(final int code) {
|
||||
for (final ProtobufStatusCode protobufStatusCode : ProtobufStatusCode.values()) {
|
||||
if (protobufStatusCode.getCode() == code) {
|
||||
return protobufStatusCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class SetDeviceSettingsMessage extends GFDIMessage {
|
||||
private final Map<GarminDeviceSetting, Object> settings;
|
||||
|
||||
public SetDeviceSettingsMessage(Map<GarminDeviceSetting, Object> settings) {
|
||||
this.settings = settings;
|
||||
final int settingsCount = settings.size();
|
||||
if (settingsCount == 0) throw new IllegalArgumentException("Empty settings");
|
||||
if (settingsCount > 255) throw new IllegalArgumentException("Too many settings");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(GarminMessage.DEVICE_INFORMATION.getId());
|
||||
writer.writeByte(settings.size());
|
||||
for (Map.Entry<GarminDeviceSetting, Object> settingPair : settings.entrySet()) {
|
||||
final GarminDeviceSetting setting = settingPair.getKey();
|
||||
writer.writeByte(setting.ordinal());
|
||||
final Object value = settingPair.getValue();
|
||||
if (value instanceof String) {
|
||||
writer.writeString((String) value);
|
||||
} else if (value instanceof Integer) {
|
||||
writer.writeByte(4);
|
||||
writer.writeInt((Integer) value);
|
||||
} else if (value instanceof Boolean) {
|
||||
writer.writeByte(1);
|
||||
writer.writeByte(Boolean.TRUE.equals(value) ? 1 : 0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported setting value type " + value);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum GarminDeviceSetting {
|
||||
DEVICE_NAME,
|
||||
CURRENT_TIME,
|
||||
DAYLIGHT_SAVINGS_TIME_OFFSET,
|
||||
TIME_ZONE_OFFSET,
|
||||
NEXT_DAYLIGHT_SAVINGS_START,
|
||||
NEXT_DAYLIGHT_SAVINGS_END,
|
||||
AUTO_UPLOAD_ENABLED,
|
||||
WEATHER_CONDITIONS_ENABLED,
|
||||
WEATHER_ALERTS_ENABLED
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class SystemEventMessage extends GFDIMessage {
|
||||
|
||||
private final GarminSystemEventType eventType;
|
||||
private final Object value;
|
||||
|
||||
public SystemEventMessage(GarminSystemEventType eventType, Object value) {
|
||||
this.eventType = eventType;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(GarminMessage.SYSTEM_EVENT.getId());
|
||||
writer.writeByte(eventType.ordinal());
|
||||
if (value instanceof String) {
|
||||
writer.writeString((String) value);
|
||||
} else if (value instanceof Integer) {
|
||||
writer.writeByte((Integer) value);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported event value type " + value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum GarminSystemEventType {
|
||||
SYNC_COMPLETE,
|
||||
SYNC_FAIL,
|
||||
FACTORY_RESET,
|
||||
PAIR_START,
|
||||
PAIR_COMPLETE,
|
||||
PAIR_FAIL,
|
||||
HOST_DID_ENTER_FOREGROUND,
|
||||
HOST_DID_ENTER_BACKGROUND,
|
||||
SYNC_READY,
|
||||
NEW_DOWNLOAD_AVAILABLE,
|
||||
DEVICE_SOFTWARE_UPDATE,
|
||||
DEVICE_DISCONNECT,
|
||||
TUTORIAL_COMPLETE,
|
||||
SETUP_WIZARD_START,
|
||||
SETUP_WIZARD_COMPLETE,
|
||||
SETUP_WIZARD_SKIPPED,
|
||||
TIME_UPDATED
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class UnhandledMessage extends GFDIMessage {
|
||||
|
||||
private final int messageType;
|
||||
|
||||
public UnhandledMessage(int messageType) {
|
||||
this.messageType = messageType;
|
||||
|
||||
this.statusMessage = new GenericStatusMessage(messageType, Status.UNSUPPORTED);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -68,6 +68,10 @@ public class CalendarEvent {
|
||||
return end;
|
||||
}
|
||||
|
||||
public int getEndSeconds() {
|
||||
return (int) (end / 1000);
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return end - begin;
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package garmin_vivomovehr;
|
||||
|
||||
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr";
|
||||
|
||||
message CalendarService {
|
||||
optional CalendarServiceRequest calendar_request = 1;
|
||||
optional CalendarServiceResponse calendar_response = 2;
|
||||
|
||||
message CalendarServiceRequest {
|
||||
optional uint32 begin = 1;
|
||||
optional uint32 end = 2;
|
||||
}
|
||||
|
||||
message CalendarServiceResponse {
|
||||
optional uint32 unknown = 1;
|
||||
repeated CalendarEvent calendar_event = 2;
|
||||
}
|
||||
|
||||
message CalendarEvent {
|
||||
optional string title = 2;
|
||||
optional string location = 3 [default = ""];
|
||||
optional string description = 4 [default = ""];
|
||||
optional uint32 begin = 5;
|
||||
optional uint32 end = 6;
|
||||
optional bool all_day = 7;
|
||||
}
|
||||
}
|
@ -47,6 +47,14 @@ message CoreService {
|
||||
}
|
||||
}
|
||||
|
||||
enum DataType {
|
||||
SIGNIFICANT_LOCATION = 0;
|
||||
GENERAL_LOCATION = 1;
|
||||
REALTIME_TRACKING = 2;
|
||||
INREACH_TRACKING = 3;
|
||||
TRACKING_EVENT = 4;
|
||||
}
|
||||
|
||||
message LocationUpdatedSetEnabledRequest {
|
||||
optional bool enabled = 1;
|
||||
repeated Request requests = 2;
|
||||
@ -56,17 +64,30 @@ message CoreService {
|
||||
optional DataType requested = 1;
|
||||
optional float min_update_threshold = 2;
|
||||
optional float distance_threshold = 3;
|
||||
|
||||
enum DataType {
|
||||
SIGNIFICANT_LOCATION = 0;
|
||||
GENERAL_LOCATION = 1;
|
||||
REALTIME_TRACKING = 2;
|
||||
INREACH_TRACKING = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message LocationUpdatedSetEnabledResponse {
|
||||
// TODO
|
||||
optional Status status = 1;
|
||||
repeated Requested requests = 2;
|
||||
|
||||
|
||||
enum Status {
|
||||
OK = 1;
|
||||
UNAVAILABLE = 2;
|
||||
UNKNOWN3 = 3;
|
||||
UNKNOWN4 = 4;
|
||||
}
|
||||
|
||||
message Requested {
|
||||
optional DataType requested = 1;
|
||||
optional RequestedStatus status = 2;
|
||||
|
||||
enum RequestedStatus {
|
||||
OK = 1;
|
||||
KO = 2;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
message LocationUpdatedNotification {
|
||||
@ -74,6 +95,19 @@ message CoreService {
|
||||
}
|
||||
|
||||
message LocationData {
|
||||
// TODO
|
||||
required LatLon position=1;
|
||||
required float altitude=2;
|
||||
required uint32 timestamp=3;
|
||||
required float h_accuracy=4;
|
||||
required float v_accuracy=5;
|
||||
required DataType position_type=6;
|
||||
required float bearing=9;
|
||||
required float speed=10;
|
||||
}
|
||||
|
||||
message LatLon {
|
||||
required sint32 lat = 1;
|
||||
required sint32 lon = 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,8 +8,10 @@ import "garmin_vivomovehr/gdi_device_status.proto";
|
||||
import "garmin_vivomovehr/gdi_find_my_watch.proto";
|
||||
import "garmin_vivomovehr/gdi_core.proto";
|
||||
import "garmin_vivomovehr/gdi_sms_notification.proto";
|
||||
import "garmin_vivomovehr/gdi_calendar_service.proto";
|
||||
|
||||
message Smart {
|
||||
optional CalendarService calendar_service = 1;
|
||||
optional DeviceStatusService device_status_service = 8;
|
||||
optional FindMyWatchService find_my_watch_service = 12;
|
||||
optional CoreService core_service = 13;
|
||||
|
@ -1486,7 +1486,9 @@
|
||||
<string name="devicetype_amazfit_gts2e">Amazfit GTS 2e</string>
|
||||
<string name="devicetype_amazfit_x">Amazfit X</string>
|
||||
<string name="devicetype_zepp_e">Zepp E</string>
|
||||
<string name="devicetype_vivomove_hr">Garmin Vivomove HR</string>
|
||||
<string name="devicetype_garmin_vivomove_hr">Garmin Vivomove HR</string>
|
||||
<string name="devicetype_garmin_vivomove_style">Vívomove Style</string>
|
||||
<string name="devicetype_garmin_instinct_2s">Garmin Instinct 2S</string>
|
||||
<string name="devicetype_vibratissimo">Vibratissimo</string>
|
||||
<string name="devicetype_um25">UM-25</string>
|
||||
<string name="devicetype_liveview">LiveView</string>
|
||||
|
@ -0,0 +1,100 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.CobsCoDec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public class GarminSupportTest {
|
||||
//test strings from https://github.com/themarpe/cobs-java/blob/master/tests-java/Tests.java
|
||||
static final byte[] test_string_0 = new byte[]{0, 0, 0, 0};
|
||||
static final byte[] test_string_1 = new byte[]{0, '1', '2', '3', '4', '5'};
|
||||
static final byte[] test_string_2 = new byte[]{0, '1', '2', '3', '4', '5', 0};
|
||||
static final byte[] test_string_3 = new byte[]{'1', '2', '3', '4', '5', 0, '6', '7', '8', '9'};
|
||||
static final byte[] test_string_4 = new byte[]{0, '1', '2', '3', '4', '5', 0, '6', '7', '8', '9', 0};
|
||||
static final byte[] test_string_5 = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, -128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0};
|
||||
static final byte[] test_string_6 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, -128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0};
|
||||
static final byte[] test_string_7 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, -128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1};
|
||||
static final byte[] test_string_8 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, -128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, -128, -127, -126, -125, -124, -123, -122, -121, -120, -119, -118, -117, -116, -115, -114, -113, -112, -111, -110, -109, -108, -107, -106, -105, -104, -103, -102, -101, -100, -99, -98, -97, -96, -95, -94, -93, -92, -91, -90, -89, -88, -87, -86, -85, -84, -83, -82, -81, -80, -79, -78, -77, -76, -75, -74, -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -63, -62, -61, -60, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, -49, -48, -47, -46, -45, -44, -43, -42, -41, -40, -39, -38, -37, -36, -35, -34, -33, -32, -31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, 0, -5, -4, -3, -2, -1};
|
||||
final CobsCoDec cobsCoDec = new CobsCoDec();
|
||||
final byte[][] allTests = new byte[][]{test_string_1, test_string_2, test_string_3, test_string_4, test_string_5, test_string_6, test_string_7, test_string_8};
|
||||
|
||||
@Test
|
||||
public void testCobsDecoder() {
|
||||
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("00022C04A0139623310F684C1BCA840508020B"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("496E7374696E637420325308496E7374696E63"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("74023253010304B800"));
|
||||
Assert.assertArrayEquals(GB.hexStringToByteArray("2C00A0139600310F684C1BCA840508020B496E7374696E637420325308496E7374696E6374023253000004B8"),
|
||||
cobsCoDec.retrieveMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCobsDecoder2() {
|
||||
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("00022b058813a013029623ffffffffffffa71fffff046c61726a07756e6b6e6f776e0758512d4343373201f9cf00"));
|
||||
Assert.assertArrayEquals(new byte[]{0x2b, 0x00, (byte) 0x88, 0x13, (byte) 0xa0, 0x13, 0x00, (byte) 0x96, 0x00, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xa7, 0x1f, (byte) 0xff, (byte) 0xff, 0x04, 0x6c, 0x61, 0x72, 0x6a, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x07, 0x58, 0x51, 0x2d, 0x43, 0x43, 0x37, 0x32, 0x01, (byte) 0xf9, (byte) 0xcf},
|
||||
cobsCoDec.retrieveMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCobsEncoder2() {
|
||||
|
||||
byte[] result = cobsCoDec.encode(GB.hexStringToByteArray("022b058813a013029623ffffffffffffa71fffff046c61726a07756e6b6e6f776e0758512d4343373201f9cf00"));
|
||||
Assert.assertArrayEquals(new byte[]{0x00, 0x2d, (byte) 0x02, (byte) 0x2b, (byte) 0x05, (byte) 0x88, (byte) 0x13, (byte) 0xa0, (byte) 0x13, (byte) 0x02, (byte) 0x96, (byte) 0x23, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xa7, (byte) 0x1f, (byte) 0xff, (byte) 0xff, (byte) 0x04, (byte) 0x6c, (byte) 0x61, (byte) 0x72, (byte) 0x6a, (byte) 0x07, (byte) 0x75, (byte) 0x6e, (byte) 0x6b, (byte) 0x6e, (byte) 0x6f, (byte) 0x77, (byte) 0x6e, (byte) 0x07, (byte) 0x58, (byte) 0x51, (byte) 0x2d, (byte) 0x43, (byte) 0x43, (byte) 0x37, (byte) 0x32, (byte) 0x01, (byte) 0xf9, (byte) 0xcf, (byte) 0x01, (byte) 0x00},
|
||||
result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCobsDecoderSingleByteAtStart() {
|
||||
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("00"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("022C04A0139623310F684C1BCA840508020B"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("496E7374696E637420325308496E7374696E63"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("74023253010304B800"));
|
||||
Assert.assertArrayEquals(GB.hexStringToByteArray("2C00A0139600310F684C1BCA840508020B496E7374696E637420325308496E7374696E6374023253000004B8"),
|
||||
cobsCoDec.retrieveMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCobsDecoderSingleByteAtEnd() {
|
||||
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("00022C04A0139623310F684C1BCA840508020B"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("496E7374696E637420325308496E7374696E63"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("74023253010304B8"));
|
||||
Assert.assertNull(cobsCoDec.retrieveMessage());
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("00"));
|
||||
Assert.assertArrayEquals(GB.hexStringToByteArray("2C00A0139600310F684C1BCA840508020B496E7374696E637420325308496E7374696E6374023253000004B8"),
|
||||
cobsCoDec.retrieveMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCobsEncoder() {
|
||||
Assert.assertArrayEquals(GB.hexStringToByteArray("00022C04A0139623310F684C1BCA840508020B496E7374696E637420325308496E7374696E6374023253010304B800"),
|
||||
cobsCoDec.encode(GB.hexStringToByteArray("2C00A0139600310F684C1BCA840508020B496E7374696E637420325308496E7374696E6374023253000004B8")));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongPayload() {
|
||||
|
||||
for (byte[] payload : allTests) {
|
||||
byte[] encodedData = cobsCoDec.encode(payload);
|
||||
cobsCoDec.receivedBytes(encodedData);
|
||||
byte[] decodedData = cobsCoDec.retrieveMessage();
|
||||
|
||||
Assert.assertArrayEquals(payload, decodedData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user