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:
Daniele Gobbetti 2024-03-11 19:11:09 +01:00 committed by José Rebelo
parent 881e8e36e8
commit 4363f94661
35 changed files with 2425 additions and 14 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -143,7 +143,7 @@ public class VivomoveHrCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_vivomove_hr;
return R.string.devicetype_garmin_vivomove_hr;
}
@Override

View File

@ -49,6 +49,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBuds2ProDe
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsLiveDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsProDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2s.GarminInstinct2SCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.venu3.GarminVenu3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivomove.GarminVivomoveStyleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.EXRIZUK8Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator;
@ -137,9 +140,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlus
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaMhoC303Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaLywsd02Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaLywsd03Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaMhoC303Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
@ -161,11 +164,11 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWFSP800NCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM4Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
@ -182,6 +185,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband7pro.MiBand7Pro
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8.MiBand8Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8active.MiBand8ActiveCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8pro.MiBand8ProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatch.MiWatchLiteCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatchcolorsport.MiWatchColorSportCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartband2.RedmiSmartBand2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartbandpro.RedmiSmartBandProCoordinator;
@ -195,7 +199,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watchs1pro.XiaomiWatc
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watchs3.XiaomiWatchS3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatch.MiWatchLiteCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmiwatch3active.RedmiWatch3ActiveCoordinator;
/**
@ -322,6 +325,9 @@ public enum DeviceType {
ITAG(ITagCoordinator.class),
NUTMINI(NutCoordinator.class),
VIVOMOVE_HR(VivomoveHrCoordinator.class),
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
GARMIN_VIVOMOVE_STYLE(GarminVivomoveStyleCoordinator.class),
GARMIN_VENU_3(GarminVenu3Coordinator.class),
VIBRATISSIMO(VibratissimoCoordinator.class),
SONY_SWR12(SonySWR12DeviceCoordinator.class),
LIVEVIEW(LiveviewCoordinator.class),

View File

@ -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());
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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()));
}
}
}

View File

@ -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));
}
}

View File

@ -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
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -68,6 +68,10 @@ public class CalendarEvent {
return end;
}
public int getEndSeconds() {
return (int) (end / 1000);
}
public long getDuration() {
return end - begin;
}

View File

@ -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;
}
}

View File

@ -47,6 +47,14 @@ message CoreService {
}
}
enum DataType {
SIGNIFICANT_LOCATION = 0;
GENERAL_LOCATION = 1;
REALTIME_TRACKING = 2;
INREACH_TRACKING = 3;
TRACKING_EVENT = 4;
}
message LocationUpdatedSetEnabledRequest {
optional bool enabled = 1;
repeated Request requests = 2;
@ -56,17 +64,30 @@ message CoreService {
optional DataType requested = 1;
optional float min_update_threshold = 2;
optional float distance_threshold = 3;
enum DataType {
SIGNIFICANT_LOCATION = 0;
GENERAL_LOCATION = 1;
REALTIME_TRACKING = 2;
INREACH_TRACKING = 3;
}
}
message LocationUpdatedSetEnabledResponse {
// TODO
optional Status status = 1;
repeated Requested requests = 2;
enum Status {
OK = 1;
UNAVAILABLE = 2;
UNKNOWN3 = 3;
UNKNOWN4 = 4;
}
message Requested {
optional DataType requested = 1;
optional RequestedStatus status = 2;
enum RequestedStatus {
OK = 1;
KO = 2;
}
}
}
message LocationUpdatedNotification {
@ -74,6 +95,19 @@ message CoreService {
}
message LocationData {
// TODO
required LatLon position=1;
required float altitude=2;
required uint32 timestamp=3;
required float h_accuracy=4;
required float v_accuracy=5;
required DataType position_type=6;
required float bearing=9;
required float speed=10;
}
message LatLon {
required sint32 lat = 1;
required sint32 lon = 2;
}
}

View File

@ -8,8 +8,10 @@ import "garmin_vivomovehr/gdi_device_status.proto";
import "garmin_vivomovehr/gdi_find_my_watch.proto";
import "garmin_vivomovehr/gdi_core.proto";
import "garmin_vivomovehr/gdi_sms_notification.proto";
import "garmin_vivomovehr/gdi_calendar_service.proto";
message Smart {
optional CalendarService calendar_service = 1;
optional DeviceStatusService device_status_service = 8;
optional FindMyWatchService find_my_watch_service = 12;
optional CoreService core_service = 13;

View File

@ -1481,7 +1481,9 @@
<string name="devicetype_amazfit_gts2e">Amazfit GTS 2e</string>
<string name="devicetype_amazfit_x">Amazfit X</string>
<string name="devicetype_zepp_e">Zepp E</string>
<string name="devicetype_vivomove_hr">Garmin Vivomove HR</string>
<string name="devicetype_garmin_vivomove_hr">Garmin Vivomove HR</string>
<string name="devicetype_garmin_vivomove_style">Vívomove Style</string>
<string name="devicetype_garmin_instinct_2s">Garmin Instinct 2S</string>
<string name="devicetype_vibratissimo">Vibratissimo</string>
<string name="devicetype_um25">UM-25</string>
<string name="devicetype_liveview">LiveView</string>

View File

@ -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);
}
}
}