mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-26 17:47:34 +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
|
@Override
|
||||||
public int getDeviceNameResource() {
|
public int getDeviceNameResource() {
|
||||||
return R.string.devicetype_vivomove_hr;
|
return R.string.devicetype_garmin_vivomove_hr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.GalaxyBudsDeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsLiveDeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsLiveDeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsProDeviceCoordinator;
|
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.EXRIZUK8Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator;
|
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.liveview.LiveviewCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
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.MijiaLywsd02Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaLywsd03Coordinator;
|
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.miscale2.MiScale2DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
|
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.SonyWF1000XM4Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM5Coordinator;
|
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.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.SonyWH1000XM2Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator;
|
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.SonyWH1000XM4Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
|
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.sony.wena3.SonyWena3Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.SoundcoreLiberty3ProCoordinator;
|
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.miband8.MiBand8Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8active.MiBand8ActiveCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8active.MiBand8ActiveCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8pro.MiBand8ProCoordinator;
|
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.miwatchcolorsport.MiWatchColorSportCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartband2.RedmiSmartBand2Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartband2.RedmiSmartBand2Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartbandpro.RedmiSmartBandProCoordinator;
|
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.xiaomi.watchs3.XiaomiWatchS3Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatch.MiWatchLiteCoordinator;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmiwatch3active.RedmiWatch3ActiveCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmiwatch3active.RedmiWatch3ActiveCoordinator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -329,6 +332,9 @@ public enum DeviceType {
|
|||||||
ITAG(ITagCoordinator.class),
|
ITAG(ITagCoordinator.class),
|
||||||
NUTMINI(NutCoordinator.class),
|
NUTMINI(NutCoordinator.class),
|
||||||
VIVOMOVE_HR(VivomoveHrCoordinator.class),
|
VIVOMOVE_HR(VivomoveHrCoordinator.class),
|
||||||
|
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
|
||||||
|
GARMIN_VIVOMOVE_STYLE(GarminVivomoveStyleCoordinator.class),
|
||||||
|
GARMIN_VENU_3(GarminVenu3Coordinator.class),
|
||||||
VIBRATISSIMO(VibratissimoCoordinator.class),
|
VIBRATISSIMO(VibratissimoCoordinator.class),
|
||||||
SONY_SWR12(SonySWR12DeviceCoordinator.class),
|
SONY_SWR12(SonySWR12DeviceCoordinator.class),
|
||||||
LIVEVIEW(LiveviewCoordinator.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;
|
return end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getEndSeconds() {
|
||||||
|
return (int) (end / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
public long getDuration() {
|
public long getDuration() {
|
||||||
return end - begin;
|
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 {
|
message LocationUpdatedSetEnabledRequest {
|
||||||
optional bool enabled = 1;
|
optional bool enabled = 1;
|
||||||
repeated Request requests = 2;
|
repeated Request requests = 2;
|
||||||
@ -56,17 +64,30 @@ message CoreService {
|
|||||||
optional DataType requested = 1;
|
optional DataType requested = 1;
|
||||||
optional float min_update_threshold = 2;
|
optional float min_update_threshold = 2;
|
||||||
optional float distance_threshold = 3;
|
optional float distance_threshold = 3;
|
||||||
|
|
||||||
enum DataType {
|
|
||||||
SIGNIFICANT_LOCATION = 0;
|
|
||||||
GENERAL_LOCATION = 1;
|
|
||||||
REALTIME_TRACKING = 2;
|
|
||||||
INREACH_TRACKING = 3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message LocationUpdatedSetEnabledResponse {
|
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 {
|
message LocationUpdatedNotification {
|
||||||
@ -74,6 +95,19 @@ message CoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message LocationData {
|
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_find_my_watch.proto";
|
||||||
import "garmin_vivomovehr/gdi_core.proto";
|
import "garmin_vivomovehr/gdi_core.proto";
|
||||||
import "garmin_vivomovehr/gdi_sms_notification.proto";
|
import "garmin_vivomovehr/gdi_sms_notification.proto";
|
||||||
|
import "garmin_vivomovehr/gdi_calendar_service.proto";
|
||||||
|
|
||||||
message Smart {
|
message Smart {
|
||||||
|
optional CalendarService calendar_service = 1;
|
||||||
optional DeviceStatusService device_status_service = 8;
|
optional DeviceStatusService device_status_service = 8;
|
||||||
optional FindMyWatchService find_my_watch_service = 12;
|
optional FindMyWatchService find_my_watch_service = 12;
|
||||||
optional CoreService core_service = 13;
|
optional CoreService core_service = 13;
|
||||||
|
@ -1486,7 +1486,9 @@
|
|||||||
<string name="devicetype_amazfit_gts2e">Amazfit GTS 2e</string>
|
<string name="devicetype_amazfit_gts2e">Amazfit GTS 2e</string>
|
||||||
<string name="devicetype_amazfit_x">Amazfit X</string>
|
<string name="devicetype_amazfit_x">Amazfit X</string>
|
||||||
<string name="devicetype_zepp_e">Zepp E</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_vibratissimo">Vibratissimo</string>
|
||||||
<string name="devicetype_um25">UM-25</string>
|
<string name="devicetype_um25">UM-25</string>
|
||||||
<string name="devicetype_liveview">LiveView</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