Compare commits
35 Commits
ff51fa7309
...
2a0c2f4d90
Author | SHA1 | Date |
---|---|---|
José Rebelo | 2a0c2f4d90 | |
José Rebelo | dbebca5b6b | |
José Rebelo | 438bfa4cce | |
José Rebelo | 13c72d827f | |
Daniele Gobbetti | 1a120bcd50 | |
kuhy | 9b27a71328 | |
Daniele Gobbetti | ba560a9a4f | |
myxor | f0efc53d54 | |
Daniele Gobbetti | f127c47fe9 | |
Daniele Gobbetti | 4c25ae2d83 | |
Daniele Gobbetti | b2d6f4492a | |
hrdl | 1a9fe65a52 | |
Daniele Gobbetti | 9cc5635474 | |
José Rebelo | be66a73fd1 | |
José Rebelo | 930bd208ae | |
José Rebelo | 6074406ccc | |
José Rebelo | c710f92dc0 | |
José Rebelo | 7afb85ba03 | |
José Rebelo | 4a15e1aa92 | |
José Rebelo | 2dd6859648 | |
Daniele Gobbetti | 7829c5f1fb | |
Daniele Gobbetti | 5a273c1118 | |
Daniele Gobbetti | 2473ae2f52 | |
Daniele Gobbetti | f94299fcc1 | |
Daniele Gobbetti | f05b7f44a9 | |
Daniele Gobbetti | fc5b8c5641 | |
Daniele Gobbetti | 944b1025c2 | |
Daniele Gobbetti | e81404379e | |
Daniele Gobbetti | cebcd24c68 | |
Daniele Gobbetti | 1d8cd7dd1e | |
Daniele Gobbetti | 1f7f502fe9 | |
Daniele Gobbetti | d00b94f333 | |
Daniele Gobbetti | 16354d333e | |
Daniele Gobbetti | 8107002a87 | |
Daniele Gobbetti | f1111d3790 |
|
@ -34,6 +34,12 @@
|
|||
}
|
||||
-keepattributes JavascriptInterface
|
||||
|
||||
# Keep parseIncoming for GFDIMessage classes, as it is called by reflection in GFDIMessage#parseIncoming
|
||||
-keep public class * extends nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage
|
||||
-keepclassmembers class * extends nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage {
|
||||
public static *** parseIncoming(...);
|
||||
}
|
||||
|
||||
# https://github.com/tony19/logback-android/issues/29
|
||||
-dontwarn javax.mail.**
|
||||
|
||||
|
|
|
@ -441,4 +441,5 @@ public class DeviceSettingsPreferenceConst {
|
|||
public static final String PREF_AUTO_REPLY_INCOMING_CALL = "pref_auto_reply_phonecall";
|
||||
public static final String PREF_AUTO_REPLY_INCOMING_CALL_DELAY = "pref_auto_reply_phonecall_delay";
|
||||
public static final String PREF_SPEAK_NOTIFICATIONS_ALOUD = "pref_speak_notifications_aloud";
|
||||
public static final String PREF_GARMIN_DEFAULT_REPLY_SUFFIX = "pref_key_garmin_default_reply_suffix";
|
||||
}
|
||||
|
|
|
@ -19,23 +19,6 @@
|
|||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_SHORTCUTS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_SHORTCUTS_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_WORKOUT_ACTIVITY_TYPES_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_OFF;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_SCHEDULED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_SWIPE_UNLOCK;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
|
@ -78,6 +61,23 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_SHORTCUTS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_SHORTCUTS_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_WORKOUT_ACTIVITY_TYPES_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_OFF;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_SCHEDULED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_SWIPE_UNLOCK;
|
||||
|
||||
public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment implements DeviceSpecificSettingsHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DeviceSpecificSettingsFragment.class);
|
||||
|
@ -622,6 +622,8 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
|||
addPreferenceHandlerFor(PREF_HEARTRATE_AUTOMATIC_ENABLE);
|
||||
addPreferenceHandlerFor(PREF_SPO_AUTOMATIC_ENABLE);
|
||||
|
||||
addPreferenceHandlerFor(PREF_GARMIN_DEFAULT_REPLY_SUFFIX);
|
||||
|
||||
addPreferenceHandlerFor("lock");
|
||||
|
||||
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||
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 abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Garmin";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return GarminSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||
|
||||
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
|
||||
notifications.add(R.xml.devicesettings_send_app_notifications);
|
||||
|
||||
final List<Integer> connection = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CONNECTION);
|
||||
connection.add(R.xml.devicesettings_high_mtu);
|
||||
|
||||
if (getCannedRepliesSlotCount(device) > 0) {
|
||||
notifications.add(R.xml.devicesettings_garmin_default_reply_suffix);
|
||||
notifications.add(R.xml.devicesettings_canned_reply_16);
|
||||
notifications.add(R.xml.devicesettings_canned_dismisscall_16);
|
||||
}
|
||||
|
||||
final List<Integer> developer = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
|
||||
developer.add(R.xml.devicesettings_keep_activity_data_on_device);
|
||||
|
||||
return deviceSpecificSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
public class GarminPreferences {
|
||||
public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities";
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.forerunner245;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminForerunner245Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("Forerunner 245");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_245;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2s;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class GarminInstinct2SCoordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("Instinct 2S");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCannedRepliesSlotCount(final GBDevice device) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_instinct_2s;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.venu3;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminVenu3Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("Venu 3");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivomove_style;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive4s;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminVivoActive4SCoordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("vĂvoactive 4S");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivoactive_4s;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive5;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminVivoActive5Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("vĂvoactive 5");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivoactive_5;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivomove;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminVivomoveStyleCoordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("vĂvomove Style");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivomove_style;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -221,6 +221,8 @@ public class NotificationListener extends NotificationListenerService {
|
|||
} catch (PendingIntent.CanceledException e) {
|
||||
LOG.warn("replyToLastNotification error: " + e.getLocalizedMessage());
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Received ACTION_REPLY but cannot find the corresponding wearableAction");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -49,6 +49,12 @@ 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.forerunner245.GarminForerunner245Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2s.GarminInstinct2SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.venu3.GarminVenu3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive4s.GarminVivoActive4SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive5.GarminVivoActive5Coordinator;
|
||||
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;
|
||||
|
@ -136,9 +142,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;
|
||||
|
@ -160,11 +166,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;
|
||||
|
@ -181,6 +187,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;
|
||||
|
@ -194,7 +201,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;
|
||||
|
||||
/**
|
||||
|
@ -321,6 +327,12 @@ public enum DeviceType {
|
|||
ITAG(ITagCoordinator.class),
|
||||
NUTMINI(NutCoordinator.class),
|
||||
VIVOMOVE_HR(VivomoveHrCoordinator.class),
|
||||
GARMIN_FORERUNNER_245(GarminForerunner245Coordinator.class),
|
||||
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
|
||||
GARMIN_VIVOMOVE_STYLE(GarminVivomoveStyleCoordinator.class),
|
||||
GARMIN_VENU_3(GarminVenu3Coordinator.class),
|
||||
GARMIN_VIVOACTIVE_4S(GarminVivoActive4SCoordinator.class),
|
||||
GARMIN_VIVOACTIVE_5(GarminVivoActive5Coordinator.class),
|
||||
VIBRATISSIMO(VibratissimoCoordinator.class),
|
||||
SONY_SWR12(SonySWR12DeviceCoordinator.class),
|
||||
LIVEVIEW(LiveviewCoordinator.class),
|
||||
|
|
|
@ -744,7 +744,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
|||
LocalBroadcastManager.getInstance(context).sendBroadcast(messageIntent);
|
||||
}
|
||||
|
||||
protected Prefs getDevicePrefs() {
|
||||
public Prefs getDevicePrefs() {
|
||||
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateA
|
|||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements CmfCharacteristic.Handler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CmfWatchProSupport.class);
|
||||
|
@ -177,11 +176,6 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
|
|||
mediaManager = new MediaManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Prefs getDevicePrefs() {
|
||||
return super.getDevicePrefs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt,
|
||||
final BluetoothGattCharacteristic characteristic) {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/* 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;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class ChecksumCalculator {
|
||||
private static final int[] CONSTANTS = {
|
||||
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
|
||||
0xA001, 0x6C00,0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400
|
||||
};
|
||||
|
||||
private ChecksumCalculator() {
|
||||
}
|
||||
|
||||
public static int computeCrc(byte[] data, int offset, int length) {
|
||||
return computeCrc(0, data, offset, length);
|
||||
}
|
||||
|
||||
public static int computeCrc(ByteBuffer byteBuffer, int offset, int length) {
|
||||
byteBuffer.rewind();
|
||||
byte[] data = new byte[length];
|
||||
byteBuffer.get(data);
|
||||
return computeCrc(0, data, offset, length);
|
||||
}
|
||||
|
||||
public static int computeCrc(int initialCrc, byte[] data, int offset, int length) {
|
||||
int crc = initialCrc;
|
||||
for (int i = offset; i < offset + length; ++i) {
|
||||
int b = data[i];
|
||||
crc = (((crc >> 4) & 4095) ^ CONSTANTS[crc & 15]) ^ CONSTANTS[b & 15];
|
||||
crc = (((crc >> 4) & 4095) ^ CONSTANTS[crc & 15]) ^ CONSTANTS[(b >> 4) & 15];
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,358 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FileTransferDataMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.UploadRequestMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.CreateFileStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.DownloadRequestStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.FileTransferDataStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.UploadRequestStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
public class FileTransferHandler implements MessageHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileTransferHandler.class);
|
||||
private final GarminSupport deviceSupport;
|
||||
private final Download download;
|
||||
private final Upload upload;
|
||||
|
||||
private static final Set<FileType.FILETYPE> FILE_TYPES_TO_PROCESS = new HashSet<FileType.FILETYPE>() {{
|
||||
add(FileType.FILETYPE.DIRECTORY);
|
||||
add(FileType.FILETYPE.ACTIVITY);
|
||||
add(FileType.FILETYPE.MONITOR);
|
||||
add(FileType.FILETYPE.METRICS);
|
||||
add(FileType.FILETYPE.CHANGELOG);
|
||||
add(FileType.FILETYPE.SLEEP);
|
||||
}};
|
||||
|
||||
public FileTransferHandler(GarminSupport deviceSupport) {
|
||||
this.deviceSupport = deviceSupport;
|
||||
this.download = new Download();
|
||||
this.upload = new Upload();
|
||||
}
|
||||
|
||||
public boolean isDownloading() {
|
||||
return download.getCurrentlyDownloading() != null;
|
||||
}
|
||||
|
||||
public boolean isUploading() {
|
||||
return upload.getCurrentlyUploading() != null;
|
||||
}
|
||||
|
||||
public GFDIMessage handle(GFDIMessage message) {
|
||||
if (message instanceof DownloadRequestStatusMessage)
|
||||
download.processDownloadRequestStatusMessage((DownloadRequestStatusMessage) message);
|
||||
else if (message instanceof FileTransferDataMessage)
|
||||
download.processDownloadChunkedMessage((FileTransferDataMessage) message);
|
||||
else if (message instanceof CreateFileStatusMessage)
|
||||
return upload.setCreateFileStatusMessage((CreateFileStatusMessage) message);
|
||||
else if (message instanceof UploadRequestStatusMessage)
|
||||
return upload.setUploadRequestStatusMessage((UploadRequestStatusMessage) message);
|
||||
else if (message instanceof FileTransferDataStatusMessage)
|
||||
return upload.processUploadProgress((FileTransferDataStatusMessage) message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public DownloadRequestMessage downloadDirectoryEntry(DirectoryEntry directoryEntry) {
|
||||
download.setCurrentlyDownloading(new FileFragment(directoryEntry));
|
||||
return new DownloadRequestMessage(directoryEntry.getFileIndex(), 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
|
||||
}
|
||||
|
||||
public DownloadRequestMessage initiateDownload() {
|
||||
download.setCurrentlyDownloading(new FileFragment(new DirectoryEntry(0, FileType.FILETYPE.DIRECTORY, 0, 0, 0, 0, null)));
|
||||
return new DownloadRequestMessage(0, 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
|
||||
}
|
||||
// public DownloadRequestMessage downloadSettings() {
|
||||
// download.setCurrentlyDownloading(new FileFragment(new DirectoryEntry(0, FileType.FILETYPE.SETTINGS, 0, 0, 0, 0, null)));
|
||||
// return new DownloadRequestMessage(0, 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
|
||||
// }
|
||||
//
|
||||
// public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYPE filetype) {
|
||||
// upload.setCurrentlyUploading(new FileFragment(new DirectoryEntry(0, filetype, 0, 0, 0, fileAsByteArray.length, null), fileAsByteArray));
|
||||
// return new CreateFileMessage(fileAsByteArray.length, filetype);
|
||||
// }
|
||||
|
||||
|
||||
public class Download {
|
||||
private FileFragment currentlyDownloading;
|
||||
|
||||
public FileFragment getCurrentlyDownloading() {
|
||||
return currentlyDownloading;
|
||||
}
|
||||
|
||||
public void setCurrentlyDownloading(FileFragment currentlyDownloading) {
|
||||
this.currentlyDownloading = currentlyDownloading;
|
||||
}
|
||||
|
||||
private void processDownloadChunkedMessage(FileTransferDataMessage fileTransferDataMessage) {
|
||||
if (!isDownloading())
|
||||
throw new IllegalStateException("Received file transfer of unknown file");
|
||||
|
||||
currentlyDownloading.append(fileTransferDataMessage);
|
||||
if (!currentlyDownloading.dataHolder.hasRemaining())
|
||||
processCompleteDownload();
|
||||
}
|
||||
|
||||
private void processCompleteDownload() {
|
||||
currentlyDownloading.dataHolder.flip();
|
||||
|
||||
if (FileType.FILETYPE.DIRECTORY.equals(currentlyDownloading.directoryEntry.filetype)) { //is a directory
|
||||
parseDirectoryEntries();
|
||||
} else {
|
||||
saveFileToExternalStorage();
|
||||
}
|
||||
|
||||
currentlyDownloading = null;
|
||||
}
|
||||
|
||||
public void processDownloadRequestStatusMessage(DownloadRequestStatusMessage downloadRequestStatusMessage) {
|
||||
if (null == currentlyDownloading)
|
||||
throw new IllegalStateException("Received file transfer of unknown file");
|
||||
if (downloadRequestStatusMessage.canProceed())
|
||||
currentlyDownloading.setSize(downloadRequestStatusMessage);
|
||||
else
|
||||
currentlyDownloading = null;
|
||||
}
|
||||
|
||||
private void saveFileToExternalStorage() {
|
||||
File dir;
|
||||
try {
|
||||
dir = deviceSupport.getWritableExportDirectory();
|
||||
File outputFile = new File(dir, currentlyDownloading.getFileName());
|
||||
FileUtils.copyStreamToFile(new ByteArrayInputStream(currentlyDownloading.dataHolder.array()), outputFile);
|
||||
outputFile.setLastModified(currentlyDownloading.directoryEntry.fileDate.getTime());
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to save file", e);
|
||||
}
|
||||
|
||||
FileDownloadedDeviceEvent fileDownloadedDeviceEvent = new FileDownloadedDeviceEvent();
|
||||
fileDownloadedDeviceEvent.directoryEntry = currentlyDownloading.directoryEntry;
|
||||
deviceSupport.evaluateGBDeviceEvent(fileDownloadedDeviceEvent);
|
||||
}
|
||||
|
||||
private void parseDirectoryEntries() {
|
||||
if ((currentlyDownloading.getDataSize() % 16) != 0)
|
||||
throw new IllegalArgumentException("Invalid directory data length");
|
||||
final GarminByteBufferReader reader = new GarminByteBufferReader(currentlyDownloading.dataHolder.array());
|
||||
reader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
while (reader.remaining() > 0) {
|
||||
final int fileIndex = reader.readShort();//2
|
||||
final int fileDataType = reader.readByte();//3
|
||||
final int fileSubType = reader.readByte();//4
|
||||
final FileType.FILETYPE filetype = FileType.FILETYPE.fromDataTypeSubType(fileDataType, fileSubType);
|
||||
final int fileNumber = reader.readShort();//6
|
||||
final int specificFlags = reader.readByte();//7
|
||||
final int fileFlags = reader.readByte();//8
|
||||
final int fileSize = reader.readInt();//12
|
||||
final Date fileDate = new Date(GarminTimeUtils.garminTimestampToJavaMillis(reader.readInt()));//16
|
||||
final DirectoryEntry directoryEntry = new DirectoryEntry(fileIndex, filetype, fileNumber, specificFlags, fileFlags, fileSize, fileDate);
|
||||
if (directoryEntry.filetype == null) //silently discard unsupported files
|
||||
continue;
|
||||
if (!FILE_TYPES_TO_PROCESS.contains(directoryEntry.filetype))
|
||||
continue;
|
||||
deviceSupport.addFileToDownloadList(directoryEntry);
|
||||
}
|
||||
currentlyDownloading = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Upload {
|
||||
private FileFragment currentlyUploading;
|
||||
|
||||
private UploadRequestMessage setCreateFileStatusMessage(CreateFileStatusMessage createFileStatusMessage) {
|
||||
if (createFileStatusMessage.canProceed()) {
|
||||
LOG.info("SENDING UPLOAD FILE");
|
||||
return new UploadRequestMessage(createFileStatusMessage.getFileIndex(), currentlyUploading.getDataSize());
|
||||
} else {
|
||||
LOG.warn("Cannot proceed with upload");
|
||||
this.currentlyUploading = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FileTransferDataMessage setUploadRequestStatusMessage(UploadRequestStatusMessage uploadRequestStatusMessage) {
|
||||
if (null == currentlyUploading)
|
||||
throw new IllegalStateException("Received upload request status transfer of unknown file");
|
||||
if (uploadRequestStatusMessage.canProceed()) {
|
||||
if (uploadRequestStatusMessage.getDataOffset() != currentlyUploading.dataHolder.position())
|
||||
throw new IllegalStateException("Received upload request with unaligned offset");
|
||||
return currentlyUploading.take();
|
||||
} else {
|
||||
LOG.warn("Cannot proceed with upload");
|
||||
this.currentlyUploading = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private GFDIMessage processUploadProgress(FileTransferDataStatusMessage fileTransferDataStatusMessage) {
|
||||
if (currentlyUploading.getDataSize() <= fileTransferDataStatusMessage.getDataOffset()) {
|
||||
this.currentlyUploading = null;
|
||||
LOG.info("SENDING SYNC COMPLETE!!!");
|
||||
|
||||
return new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0);
|
||||
} else {
|
||||
if (fileTransferDataStatusMessage.canProceed()) {
|
||||
LOG.info("SENDING NEXT CHUNK!!!");
|
||||
if (fileTransferDataStatusMessage.getDataOffset() != currentlyUploading.dataHolder.position())
|
||||
throw new IllegalStateException("Received file transfer status with unaligned offset");
|
||||
return currentlyUploading.take();
|
||||
} else {
|
||||
LOG.warn("Cannot proceed with upload");
|
||||
this.currentlyUploading = null;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public FileFragment getCurrentlyUploading() {
|
||||
return this.currentlyUploading;
|
||||
}
|
||||
|
||||
public void setCurrentlyUploading(FileFragment currentlyUploading) {
|
||||
this.currentlyUploading = currentlyUploading;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FileFragment {
|
||||
private final DirectoryEntry directoryEntry;
|
||||
private final int maxBlockSize = 500;
|
||||
private int dataSize;
|
||||
private ByteBuffer dataHolder;
|
||||
private int runningCrc;
|
||||
|
||||
FileFragment(DirectoryEntry directoryEntry) {
|
||||
this.directoryEntry = directoryEntry;
|
||||
this.setRunningCrc(0);
|
||||
}
|
||||
|
||||
FileFragment(DirectoryEntry directoryEntry, byte[] contents) {
|
||||
this.directoryEntry = directoryEntry;
|
||||
this.setDataSize(contents.length);
|
||||
this.dataHolder = ByteBuffer.wrap(contents);
|
||||
this.dataHolder.flip(); //we'll be only reading from here on
|
||||
this.dataHolder.compact();
|
||||
this.setRunningCrc(0);
|
||||
}
|
||||
|
||||
private int getMaxBlockSize() {
|
||||
return Math.max(maxBlockSize, GFDIMessage.getMaxPacketSize());
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return directoryEntry.getFileName();
|
||||
}
|
||||
|
||||
private void setSize(DownloadRequestStatusMessage downloadRequestStatusMessage) {
|
||||
if (0 != getDataSize())
|
||||
throw new IllegalStateException("Data size already set");
|
||||
|
||||
this.setDataSize(downloadRequestStatusMessage.getMaxFileSize());
|
||||
this.dataHolder = ByteBuffer.allocate(getDataSize());
|
||||
}
|
||||
|
||||
private void append(FileTransferDataMessage fileTransferDataMessage) {
|
||||
if (fileTransferDataMessage.getDataOffset() != dataHolder.position())
|
||||
throw new IllegalStateException("Received message that was already received");
|
||||
|
||||
final int dataCrc = ChecksumCalculator.computeCrc(getRunningCrc(), fileTransferDataMessage.getMessage(), 0, fileTransferDataMessage.getMessage().length);
|
||||
if (fileTransferDataMessage.getCrc() != dataCrc)
|
||||
throw new IllegalStateException("Received message with invalid CRC");
|
||||
setRunningCrc(dataCrc);
|
||||
|
||||
this.dataHolder.put(fileTransferDataMessage.getMessage());
|
||||
}
|
||||
|
||||
private FileTransferDataMessage take() {
|
||||
final int currentOffset = this.dataHolder.position();
|
||||
final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize())];
|
||||
this.dataHolder.get(chunk);
|
||||
setRunningCrc(ChecksumCalculator.computeCrc(getRunningCrc(), chunk, 0, chunk.length));
|
||||
return new FileTransferDataMessage(chunk, currentOffset, getRunningCrc());
|
||||
}
|
||||
|
||||
private int getDataSize() {
|
||||
return dataSize;
|
||||
}
|
||||
|
||||
private void setDataSize(int dataSize) {
|
||||
this.dataSize = dataSize;
|
||||
}
|
||||
|
||||
private int getRunningCrc() {
|
||||
return runningCrc;
|
||||
}
|
||||
|
||||
private void setRunningCrc(int runningCrc) {
|
||||
this.runningCrc = runningCrc;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DirectoryEntry {
|
||||
private final int fileIndex;
|
||||
private final FileType.FILETYPE filetype;
|
||||
private final int fileNumber;
|
||||
private final int specificFlags;
|
||||
private final int fileFlags;
|
||||
private final int fileSize;
|
||||
private final Date fileDate;
|
||||
|
||||
public DirectoryEntry(int fileIndex, FileType.FILETYPE filetype, int fileNumber, int specificFlags, int fileFlags, int fileSize, Date fileDate) {
|
||||
this.fileIndex = fileIndex;
|
||||
this.filetype = filetype;
|
||||
this.fileNumber = fileNumber;
|
||||
this.specificFlags = specificFlags;
|
||||
this.fileFlags = fileFlags;
|
||||
this.fileSize = fileSize;
|
||||
this.fileDate = fileDate;
|
||||
}
|
||||
|
||||
public int getFileIndex() {
|
||||
return fileIndex;
|
||||
}
|
||||
|
||||
public FileType.FILETYPE getFiletype() {
|
||||
return filetype;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
|
||||
String dateString = dateFormat.format(fileDate);
|
||||
return getFiletype().name() + "_" + getFileIndex() + "_" + dateString + (getFiletype().isFitFile() ? ".fit" : ".bin");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DirectoryEntry{" +
|
||||
"fileIndex=" + fileIndex +
|
||||
", fileType=" + filetype.name() +
|
||||
", fileNumber=" + fileNumber +
|
||||
", specificFlags=" + specificFlags +
|
||||
", fileFlags=" + fileFlags +
|
||||
", fileSize=" + fileSize +
|
||||
", fileDate=" + fileDate +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class FileType {
|
||||
//common
|
||||
//128/4: FIT_TYPE_4, -> garmin/activity
|
||||
//128/32: FIT_TYPE_32, -> garmin/monitor
|
||||
//128/44: FIT_TYPE_44, ->garmin/metrics
|
||||
//128/41: FIT_TYPE_41, ->garmin/chnglog
|
||||
//128/49: FIT_TYPE_49, -> garmin/sleep
|
||||
//255/245: ErrorShutdownReports,
|
||||
|
||||
//Specific Instinct 2S:
|
||||
//128/38: FIT_TYPE_38, -> garmin/SCORCRDS
|
||||
//255/248: KPI,
|
||||
//128/58: FIT_TYPE_58, -> outputFromUnit garmin/device????
|
||||
//255/247: ULFLogs,
|
||||
//128/68: FIT_TYPE_68, -> garmin/HRVSTATUS
|
||||
//128/70: FIT_TYPE_70, -> garmin/HSA
|
||||
//128/72: FIT_TYPE_72, -> garmin/FBTBACKUP
|
||||
//128/74: FIT_TYPE_74
|
||||
|
||||
|
||||
private final FILETYPE fileType;
|
||||
private final String garminDeviceFileType;
|
||||
|
||||
public FileType(int fileDataType, int fileSubType, String garminDeviceFileType) {
|
||||
this.fileType = FILETYPE.fromDataTypeSubType(fileDataType, fileSubType);
|
||||
this.garminDeviceFileType = garminDeviceFileType;
|
||||
}
|
||||
|
||||
public FILETYPE getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public enum FILETYPE { //TODO: add specialized method to parse each file type to the enum?
|
||||
// virtual/undocumented
|
||||
DIRECTORY(0, 0),
|
||||
|
||||
// fit files
|
||||
ACTIVITY(128, 4),
|
||||
WORKOUTS(128, 5),
|
||||
SCHEDULES(128, 7),
|
||||
LOCATION(128, 8),
|
||||
TOTALS(128, 10),
|
||||
GOALS(128, 11),
|
||||
SUMMARY(128, 20),
|
||||
RECORDS(128, 29),
|
||||
MONITOR(128, 32),
|
||||
CLUBS(128, 37),
|
||||
SCORE(128, 38),
|
||||
ADJUSTMENTS(128, 39),
|
||||
CHANGELOG(128, 41),
|
||||
METRICS(128, 44),
|
||||
SLEEP(128, 49),
|
||||
MUSCLE_MAP(128, 59),
|
||||
ECG(128, 61),
|
||||
BENCHMARK(128, 62),
|
||||
HRV_STATUS(128, 68),
|
||||
HSA(128, 70),
|
||||
FBT_BACKUP(128, 72),
|
||||
SKIN_TEMP(128, 73),
|
||||
FBT_PTD_BACKUP(128, 74),
|
||||
|
||||
// Other files
|
||||
ERROR_SHUTDOWN_REPORTS(255, 245),
|
||||
IQ_ERROR_REPORTS(255, 244),
|
||||
ULF_LOGS(255, 247),
|
||||
;
|
||||
|
||||
private final int type;
|
||||
private final int subtype;
|
||||
|
||||
FILETYPE(final int type, final int subtype) {
|
||||
this.type = type;
|
||||
this.subtype = subtype;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static FILETYPE fromDataTypeSubType(int dataType, int subType) {
|
||||
for (FILETYPE ft :
|
||||
FILETYPE.values()) {
|
||||
if (ft.type == dataType && ft.subtype == subType)
|
||||
return ft;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getSubType() {
|
||||
return subtype;
|
||||
}
|
||||
|
||||
public boolean isFitFile() {
|
||||
return type == 128;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class GarminByteBufferReader {
|
||||
protected final ByteBuffer byteBuffer;
|
||||
|
||||
public GarminByteBufferReader(byte[] data) {
|
||||
this.byteBuffer = ByteBuffer.wrap(data);
|
||||
}
|
||||
|
||||
public int remaining() {
|
||||
return byteBuffer.remaining();
|
||||
}
|
||||
|
||||
public ByteBuffer asReadOnlyBuffer() {
|
||||
return byteBuffer.asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
public void setByteOrder(ByteOrder byteOrder) {
|
||||
this.byteBuffer.order(byteOrder);
|
||||
}
|
||||
|
||||
public int readByte() {
|
||||
return Byte.toUnsignedInt(byteBuffer.get());
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
public int readShort() {
|
||||
return Short.toUnsignedInt(byteBuffer.getShort());
|
||||
}
|
||||
|
||||
public int readInt() {
|
||||
return byteBuffer.getInt();
|
||||
}
|
||||
|
||||
public long readLong() {
|
||||
return byteBuffer.getLong();
|
||||
}
|
||||
|
||||
public float readFloat32() {
|
||||
return byteBuffer.getFloat();
|
||||
}
|
||||
|
||||
public double readFloat64() {
|
||||
return byteBuffer.getDouble();
|
||||
}
|
||||
|
||||
public String readString() {
|
||||
final int size = readByte();
|
||||
byte[] bytes = new byte[size];
|
||||
byteBuffer.get(bytes);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public String readNullTerminatedString() {
|
||||
int position = byteBuffer.position();
|
||||
int size = 0;
|
||||
while (byteBuffer.hasRemaining()) {
|
||||
if (byteBuffer.get() == 0)
|
||||
break;
|
||||
size++;
|
||||
}
|
||||
byteBuffer.position(position);
|
||||
byte[] bytes = new byte[size];
|
||||
byteBuffer.get(bytes);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public byte[] readBytes(int size) {
|
||||
byte[] bytes = new byte[size];
|
||||
|
||||
byteBuffer.get(bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,582 @@
|
|||
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.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Queue;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSettingsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.ICommunicator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v2.CommunicatorV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.PredefinedLocalMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ConfigurationMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MusicControlEntityUpdateMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetDeviceSettingsMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetFileFlagsMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SupportedFileTypesMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_GARMIN_DEFAULT_REPLY_SUFFIX;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SEND_APP_NOTIFICATIONS;
|
||||
|
||||
|
||||
public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminSupport.class);
|
||||
private final ProtocolBufferHandler protocolBufferHandler;
|
||||
private final NotificationsHandler notificationsHandler;
|
||||
private final FileTransferHandler fileTransferHandler;
|
||||
private final Queue<FileTransferHandler.DirectoryEntry> filesToDownload;
|
||||
private final List<MessageHandler> messageHandlers;
|
||||
private ICommunicator communicator;
|
||||
private MusicStateSpec musicStateSpec;
|
||||
private Timer musicStateTimer;
|
||||
private final List<FileType> supportedFileTypeList = new ArrayList<>();
|
||||
|
||||
public GarminSupport() {
|
||||
super(LOG);
|
||||
addSupportedService(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI);
|
||||
addSupportedService(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI);
|
||||
protocolBufferHandler = new ProtocolBufferHandler(this);
|
||||
fileTransferHandler = new FileTransferHandler(this);
|
||||
filesToDownload = new LinkedList<>();
|
||||
messageHandlers = new ArrayList<>();
|
||||
notificationsHandler = new NotificationsHandler();
|
||||
messageHandlers.add(fileTransferHandler);
|
||||
messageHandlers.add(protocolBufferHandler);
|
||||
messageHandlers.add(notificationsHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
stopMusicTimer();
|
||||
}
|
||||
|
||||
private void stopMusicTimer() {
|
||||
if (musicStateTimer != null) {
|
||||
musicStateTimer.cancel();
|
||||
musicStateTimer.purge();
|
||||
musicStateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void addFileToDownloadList(FileTransferHandler.DirectoryEntry directoryEntry) {
|
||||
filesToDownload.add(directoryEntry);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
if (getDevicePrefs().getBoolean(PREF_ALLOW_HIGH_MTU, true)) {
|
||||
builder.requestMtu(515);
|
||||
}
|
||||
|
||||
communicator.initializeDevice(builder);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(final BluetoothGatt gatt, final int mtu, final int status) {
|
||||
if (mtu < 23) {
|
||||
LOG.warn("Ignoring mtu of {}, too low", mtu);
|
||||
return;
|
||||
}
|
||||
if (!getDevicePrefs().getBoolean(PREF_ALLOW_HIGH_MTU, true)) {
|
||||
LOG.warn("Ignoring mtu change to {} - high mtu is disabled", mtu);
|
||||
return;
|
||||
}
|
||||
|
||||
communicator.onMtuChanged(mtu);
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
the handler elaborates the followup message but might change the status message since it does
|
||||
check the integrity of the incoming message payload. Hence we let the handlers elaborate the
|
||||
incoming message, then we send the status message of the incoming message, then the response
|
||||
and finally we send the followup.
|
||||
*/
|
||||
|
||||
GFDIMessage followup = null;
|
||||
for (MessageHandler han : messageHandlers) {
|
||||
followup = han.handle(parsedMessage);
|
||||
if (followup != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final List<GBDeviceEvent> events = parsedMessage.getGBDeviceEvent();
|
||||
for (final GBDeviceEvent event : events) {
|
||||
evaluateGBDeviceEvent(event);
|
||||
}
|
||||
|
||||
communicator.sendMessage(parsedMessage.getAckBytestream()); //send status message
|
||||
|
||||
sendOutgoingMessage(parsedMessage); //send reply if any
|
||||
|
||||
sendOutgoingMessage(followup); //send followup message if any
|
||||
|
||||
if (parsedMessage instanceof ConfigurationMessage) { //the last forced message exchange
|
||||
completeInitialization();
|
||||
}
|
||||
|
||||
processDownloadQueue();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
LOG.info("INCOMING CALLSPEC: {}", callSpec.command);
|
||||
sendOutgoingMessage(notificationsHandler.onSetCallState(callSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) {
|
||||
if (deviceEvent instanceof WeatherRequestDeviceEvent) {
|
||||
WeatherSpec weather = Weather.getInstance().getWeatherSpec();
|
||||
if (weather != null) {
|
||||
sendWeatherConditions(weather);
|
||||
}
|
||||
} else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
|
||||
final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
|
||||
notificationsHandler.setEnabled(enable);
|
||||
LOG.info("NOTIFICATIONS ARE NOW {}", enable ? "ON" : "OFF");
|
||||
} else if (deviceEvent instanceof SupportedFileTypesDeviceEvent) {
|
||||
this.supportedFileTypeList.clear();
|
||||
this.supportedFileTypeList.addAll(((SupportedFileTypesDeviceEvent) deviceEvent).getSupportedFileTypes());
|
||||
} else if (deviceEvent instanceof FileDownloadedDeviceEvent) {
|
||||
LOG.debug("FILE DOWNLOAD COMPLETE {}", ((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileName());
|
||||
|
||||
if (false) // delete file from watch upon successful download TODO: add device setting
|
||||
sendOutgoingMessage(new SetFileFlagsMessage(((FileDownloadedDeviceEvent) deviceEvent).directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
|
||||
}
|
||||
|
||||
super.evaluateGBDeviceEvent(deviceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(final int dataTypes) {
|
||||
if (this.supportedFileTypeList.isEmpty()) {
|
||||
LOG.warn("No known supported file types");
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME respect dataTypes?
|
||||
|
||||
sendOutgoingMessage(fileTransferHandler.initiateDownload());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
if (!getDevicePrefs().getBoolean(PREF_SEND_APP_NOTIFICATIONS, true)) {
|
||||
// FIXME: Instead of silently dropping the notification, use NotificationSubscriptionMessage
|
||||
// to signal to the watch that they're disabled
|
||||
LOG.debug("App notifications disabled - ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
sendOutgoingMessage(notificationsHandler.onNotification(notificationSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
sendOutgoingMessage(notificationsHandler.onDeleteNotification(id));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSendWeather(final ArrayList<WeatherSpec> weatherSpecs) { //todo: find the closest one relative to the requested lat/long
|
||||
sendWeatherConditions(weatherSpecs.get(0));
|
||||
}
|
||||
|
||||
private void sendOutgoingMessage(GFDIMessage message) {
|
||||
if (message == null)
|
||||
return;
|
||||
communicator.sendMessage(message.getOutgoingMessage());
|
||||
}
|
||||
|
||||
private boolean supports(final GarminCapability capability) {
|
||||
return getDevicePrefs().getStringSet(GarminPreferences.PREF_GARMIN_CAPABILITIES, Collections.emptySet())
|
||||
.contains(capability.name());
|
||||
}
|
||||
|
||||
private void sendWeatherConditions(WeatherSpec weather) {
|
||||
if (!supports(GarminCapability.WEATHER_CONDITIONS)) {
|
||||
// Device does not support sending weather as fit
|
||||
return;
|
||||
}
|
||||
|
||||
List<RecordData> weatherData = new ArrayList<>();
|
||||
|
||||
List<RecordDefinition> weatherDefinitions = new ArrayList<>(3);
|
||||
weatherDefinitions.add(PredefinedLocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
|
||||
weatherDefinitions.add(PredefinedLocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
|
||||
weatherDefinitions.add(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
|
||||
|
||||
sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage(weatherDefinitions));
|
||||
|
||||
try {
|
||||
RecordData today = new RecordData(PredefinedLocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
|
||||
today.setFieldByName("weather_report", 0); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||
today.setFieldByName("timestamp", weather.timestamp);
|
||||
today.setFieldByName("observed_at_time", weather.timestamp);
|
||||
today.setFieldByName("temperature", weather.currentTemp);
|
||||
today.setFieldByName("low_temperature", weather.todayMinTemp);
|
||||
today.setFieldByName("high_temperature", weather.todayMaxTemp);
|
||||
today.setFieldByName("condition", weather.currentConditionCode);
|
||||
today.setFieldByName("wind_direction", weather.windDirection);
|
||||
today.setFieldByName("precipitation_probability", weather.precipProbability);
|
||||
today.setFieldByName("wind_speed", Math.round(weather.windSpeed));
|
||||
today.setFieldByName("temperature_feels_like", weather.feelsLikeTemp);
|
||||
today.setFieldByName("relative_humidity", weather.currentHumidity);
|
||||
today.setFieldByName("observed_location_lat", weather.latitude);
|
||||
today.setFieldByName("observed_location_long", weather.longitude);
|
||||
today.setFieldByName("location", weather.location);
|
||||
weatherData.add(today);
|
||||
|
||||
for (int hour = 0; hour <= 11; hour++) {
|
||||
if (hour < weather.hourly.size()) {
|
||||
WeatherSpec.Hourly hourly = weather.hourly.get(hour);
|
||||
RecordData weatherHourlyForecast = new RecordData(PredefinedLocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
|
||||
weatherHourlyForecast.setFieldByName("weather_report", 1); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||
weatherHourlyForecast.setFieldByName("timestamp", hourly.timestamp);
|
||||
weatherHourlyForecast.setFieldByName("temperature", hourly.temp);
|
||||
weatherHourlyForecast.setFieldByName("condition", hourly.conditionCode);
|
||||
weatherHourlyForecast.setFieldByName("wind_direction", hourly.windDirection);
|
||||
weatherHourlyForecast.setFieldByName("wind_speed", Math.round(hourly.windSpeed));
|
||||
weatherHourlyForecast.setFieldByName("precipitation_probability", hourly.precipProbability);
|
||||
weatherHourlyForecast.setFieldByName("relative_humidity", hourly.humidity);
|
||||
// weatherHourlyForecast.setFieldByName("dew_point", 0); // dew_point sint8
|
||||
weatherHourlyForecast.setFieldByName("uv_index", hourly.uvIndex);
|
||||
// weatherHourlyForecast.setFieldByName("air_quality", 0); // air_quality enum
|
||||
weatherData.add(weatherHourlyForecast);
|
||||
}
|
||||
}
|
||||
//
|
||||
RecordData todayDailyForecast = new RecordData(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
|
||||
todayDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||
todayDailyForecast.setFieldByName("timestamp", weather.timestamp);
|
||||
todayDailyForecast.setFieldByName("low_temperature", weather.todayMinTemp);
|
||||
todayDailyForecast.setFieldByName("high_temperature", weather.todayMaxTemp);
|
||||
todayDailyForecast.setFieldByName("condition", weather.currentConditionCode);
|
||||
todayDailyForecast.setFieldByName("precipitation_probability", weather.precipProbability);
|
||||
todayDailyForecast.setFieldByName("day_of_week", weather.timestamp);
|
||||
weatherData.add(todayDailyForecast);
|
||||
|
||||
|
||||
for (int day = 0; day < 4; day++) {
|
||||
if (day < weather.forecasts.size()) {
|
||||
WeatherSpec.Daily daily = weather.forecasts.get(day);
|
||||
int ts = weather.timestamp + (day + 1) * 24 * 60 * 60; //TODO: is this needed?
|
||||
RecordData weatherDailyForecast = new RecordData(PredefinedLocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
|
||||
weatherDailyForecast.setFieldByName("weather_report", 2); // 0 = current, 1 = hourly_forecast, 2 = daily_forecast
|
||||
weatherDailyForecast.setFieldByName("timestamp", weather.timestamp);
|
||||
weatherDailyForecast.setFieldByName("low_temperature", daily.minTemp);
|
||||
weatherDailyForecast.setFieldByName("high_temperature", daily.maxTemp);
|
||||
weatherDailyForecast.setFieldByName("condition", daily.conditionCode);
|
||||
weatherDailyForecast.setFieldByName("precipitation_probability", daily.precipProbability);
|
||||
weatherDailyForecast.setFieldByName("day_of_week", ts);
|
||||
weatherData.add(weatherDailyForecast);
|
||||
}
|
||||
}
|
||||
|
||||
sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDataMessage(weatherData));
|
||||
} catch (Exception e) {
|
||||
LOG.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void completeInitialization() {
|
||||
onSetTime();
|
||||
enableWeather();
|
||||
|
||||
//following is needed for vivomove style
|
||||
sendOutgoingMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_READY, 0));
|
||||
|
||||
enableBatteryLevelUpdate();
|
||||
|
||||
gbDevice.setState(GBDevice.State.INITIALIZED);
|
||||
gbDevice.sendDeviceUpdateIntent(getContext());
|
||||
|
||||
sendOutgoingMessage(new SupportedFileTypesMessage());
|
||||
|
||||
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
|
||||
}
|
||||
|
||||
private ProtobufMessage toggleDefaultReplySuffix(boolean value) {
|
||||
final GdiSettingsService.SettingsService.Builder enableSignature = GdiSettingsService.SettingsService.newBuilder()
|
||||
.setChangeRequest(
|
||||
GdiSettingsService.ChangeRequest.newBuilder()
|
||||
.setPointer1(65566) //TODO: this might be device specific, tested on Instinct 2s
|
||||
.setPointer2(3) //TODO: this might be device specific, tested on Instinct 2s
|
||||
.setEnable(GdiSettingsService.ChangeRequest.Switch.newBuilder().setValue(value)));
|
||||
|
||||
return protocolBufferHandler.prepareProtobufRequest(
|
||||
GdiSmartProto.Smart.newBuilder()
|
||||
.setSettingsService(enableSignature).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
if (PREF_GARMIN_DEFAULT_REPLY_SUFFIX.equals(config)) {
|
||||
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
|
||||
}
|
||||
}
|
||||
|
||||
private void processDownloadQueue() {
|
||||
if (!filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) {
|
||||
if (!gbDevice.isBusy()) {
|
||||
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), "", true, 0, getContext());
|
||||
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
|
||||
try {
|
||||
FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove();
|
||||
while (checkFileExists(directoryEntry.getFileName())) {
|
||||
LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName());
|
||||
if (false) // delete file from watch if already downloaded TODO: add device setting
|
||||
sendOutgoingMessage(new SetFileFlagsMessage(directoryEntry.getFileIndex(), SetFileFlagsMessage.FileFlags.ARCHIVE));
|
||||
directoryEntry = filesToDownload.remove();
|
||||
}
|
||||
DownloadRequestMessage downloadRequestMessage = fileTransferHandler.downloadDirectoryEntry(directoryEntry);
|
||||
if (downloadRequestMessage != null) {
|
||||
sendOutgoingMessage(downloadRequestMessage);
|
||||
} else {
|
||||
LOG.debug("File: {} already downloaded, not downloading again, from inside.", directoryEntry.getFileName());
|
||||
}
|
||||
} catch (NoSuchElementException e) {
|
||||
// we ran out of files to download
|
||||
// FIXME this is ugly
|
||||
if (gbDevice.isBusy() && gbDevice.getBusyTask().equals(getContext().getString(R.string.busy_task_fetch_activity_data))) {
|
||||
getDevice().unsetBusyTask();
|
||||
GB.updateTransferNotification(null, "", false, 100, getContext());
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
}
|
||||
} else if (filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) {
|
||||
if (gbDevice.isBusy() && gbDevice.getBusyTask().equals(getContext().getString(R.string.busy_task_fetch_activity_data))) {
|
||||
getDevice().unsetBusyTask();
|
||||
GB.updateTransferNotification(null, "", false, 100, getContext());
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enableBatteryLevelUpdate() {
|
||||
final ProtobufMessage batteryLevelProtobufRequest = protocolBufferHandler.prepareProtobufRequest(GdiSmartProto.Smart.newBuilder()
|
||||
.setDeviceStatusService(
|
||||
GdiDeviceStatus.DeviceStatusService.newBuilder()
|
||||
.setRemoteDeviceBatteryStatusRequest(
|
||||
GdiDeviceStatus.DeviceStatusService.RemoteDeviceBatteryStatusRequest.newBuilder()
|
||||
)
|
||||
)
|
||||
.build());
|
||||
sendOutgoingMessage(batteryLevelProtobufRequest);
|
||||
}
|
||||
|
||||
private void enableWeather() {
|
||||
final Map<SetDeviceSettingsMessage.GarminDeviceSetting, Object> settings = new LinkedHashMap<>(3);
|
||||
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.AUTO_UPLOAD_ENABLED, false);
|
||||
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true);
|
||||
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_ALERTS_ENABLED, false);
|
||||
sendOutgoingMessage(new SetDeviceSettingsMessage(settings));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
sendOutgoingMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.TIME_UPDATED, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
final GdiFindMyWatch.FindMyWatchService.Builder a = GdiFindMyWatch.FindMyWatchService.newBuilder();
|
||||
if (start) {
|
||||
a.setFindRequest(
|
||||
GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder()
|
||||
.setTimeout(60)
|
||||
);
|
||||
} else {
|
||||
a.setCancelRequest(
|
||||
GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder()
|
||||
);
|
||||
}
|
||||
final ProtobufMessage findMyWatch = protocolBufferHandler.prepareProtobufRequest(
|
||||
GdiSmartProto.Smart.newBuilder()
|
||||
.setFindMyWatchService(a).build());
|
||||
|
||||
sendOutgoingMessage(findMyWatch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
sendOutgoingMessage(protocolBufferHandler.setCannedMessages(cannedMessagesSpec));
|
||||
}
|
||||
|
||||
@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));
|
||||
|
||||
sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
musicStateSpec = stateSpec;
|
||||
|
||||
stopMusicTimer();
|
||||
|
||||
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());
|
||||
sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
|
||||
|
||||
}
|
||||
}, 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());
|
||||
sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkFileExists(String fileName) {
|
||||
File dir;
|
||||
try {
|
||||
dir = getWritableExportDirectory();
|
||||
File outputFile = new File(dir, fileName);
|
||||
if (outputFile.exists()) //do not download again already downloaded file
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
LOG.error("IOException: " + e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public File getWritableExportDirectory() throws IOException {
|
||||
File dir;
|
||||
dir = new File(FileUtils.getExternalFilesDir() + "/" + FileUtils.makeValidFileName(getDevice().getName() + "_" + getDevice().getAddress()));
|
||||
if (!dir.isDirectory()) {
|
||||
if (!dir.mkdir()) {
|
||||
throw new IOException("Cannot create device specific directory for " + getDevice().getName());
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import org.threeten.bp.Instant;
|
||||
import org.threeten.bp.ZoneId;
|
||||
|
||||
public class GarminTimeUtils {
|
||||
|
||||
public static final int GARMIN_TIME_EPOCH = 631065600;
|
||||
|
||||
public static int unixTimeToGarminTimestamp(int unixTime) {
|
||||
return unixTime - GARMIN_TIME_EPOCH;
|
||||
}
|
||||
|
||||
public static int javaMillisToGarminTimestamp(long millis) {
|
||||
return (int) (millis / 1000) - GARMIN_TIME_EPOCH;
|
||||
}
|
||||
|
||||
public static long garminTimestampToJavaMillis(int timestamp) {
|
||||
return (timestamp + GARMIN_TIME_EPOCH) * 1000L;
|
||||
}
|
||||
|
||||
public static int garminTimestampToUnixTime(int timestamp) {
|
||||
return timestamp + GARMIN_TIME_EPOCH;
|
||||
}
|
||||
|
||||
public static int unixTimeToGarminDayOfWeek(int unixTime) {
|
||||
return (Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).getDayOfWeek().getValue() % 7);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
|
||||
|
||||
public interface MessageHandler {
|
||||
GFDIMessage handle(GFDIMessage message);
|
||||
}
|
|
@ -0,0 +1,496 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
|
||||
import android.util.SparseArray;
|
||||
|
||||
import org.apache.commons.lang3.EnumUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationControlMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationDataMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationUpdateMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationDataStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
|
||||
public class NotificationsHandler implements MessageHandler {
|
||||
public static final SimpleDateFormat NOTIFICATION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NotificationsHandler.class);
|
||||
private final Queue<NotificationSpec> notificationSpecQueue;
|
||||
private final Upload upload;
|
||||
private boolean enabled = false;
|
||||
// Keep track of Notification ID -> action handle, as BangleJSDeviceSupport.
|
||||
// TODO: This needs to be simplified.
|
||||
private final LimitedQueue<Integer, Long> mNotificationReplyAction = new LimitedQueue<>(16);
|
||||
|
||||
|
||||
public NotificationsHandler() {
|
||||
this.notificationSpecQueue = new LinkedList<>();
|
||||
this.upload = new Upload();
|
||||
}
|
||||
|
||||
private static void encodeNotificationAttribute(NotificationSpec notificationSpec, Map.Entry<NotificationAttribute, Integer> entry, MessageWriter messageWriter) {
|
||||
messageWriter.writeByte(entry.getKey().code);
|
||||
final byte[] bytes = entry.getKey().getNotificationSpecAttribute(notificationSpec, entry.getValue());
|
||||
messageWriter.writeShort(bytes.length);
|
||||
messageWriter.writeBytes(bytes);
|
||||
// LOG.info("ATTRIBUTE:{} value:{} length:{}", entry.getKey(), new String(bytes), bytes.length);
|
||||
}
|
||||
|
||||
|
||||
private boolean addNotificationToQueue(NotificationSpec notificationSpec) {
|
||||
boolean found = false;
|
||||
Iterator<NotificationSpec> iterator = notificationSpecQueue.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
NotificationSpec e = iterator.next();
|
||||
if (e.getId() == notificationSpec.getId()) {
|
||||
found = true;
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
notificationSpecQueue.offer(notificationSpec); // Add the notificationSpec to the front of the queue
|
||||
return found;
|
||||
}
|
||||
|
||||
public NotificationUpdateMessage onSetCallState(CallSpec callSpec) {
|
||||
if (!enabled)
|
||||
return null;
|
||||
if (callSpec.command == CallSpec.CALL_INCOMING) {
|
||||
NotificationSpec callNotificationSpec = new NotificationSpec(callSpec.number.hashCode());
|
||||
callNotificationSpec.phoneNumber = callSpec.number;
|
||||
callNotificationSpec.sourceAppId = callSpec.sourceAppId;
|
||||
callNotificationSpec.title = StringUtils.isEmpty(callSpec.name) ? callSpec.number : callSpec.name;
|
||||
callNotificationSpec.type = NotificationType.GENERIC_PHONE;
|
||||
callNotificationSpec.body = StringUtils.isEmpty(callSpec.name) ? callSpec.number : callSpec.name;
|
||||
|
||||
return onNotification(callNotificationSpec);
|
||||
} else {
|
||||
if (callSpec.number != null) // this happens in debug screen
|
||||
return onDeleteNotification(callSpec.number.hashCode());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public NotificationUpdateMessage onNotification(NotificationSpec notificationSpec) {
|
||||
if (!enabled)
|
||||
return null;
|
||||
final boolean isUpdate = addNotificationToQueue(notificationSpec);
|
||||
|
||||
NotificationUpdateMessage.NotificationUpdateType notificationUpdateType = isUpdate ? NotificationUpdateMessage.NotificationUpdateType.MODIFY : NotificationUpdateMessage.NotificationUpdateType.ADD;
|
||||
|
||||
if (notificationSpecQueue.size() > 10)
|
||||
notificationSpecQueue.poll(); //remove the oldest notification TODO: should send a delete notification message to watch!
|
||||
|
||||
final boolean hasActions = (null != notificationSpec.attachedActions && !notificationSpec.attachedActions.isEmpty());
|
||||
if (hasActions) {
|
||||
for (int i = 0; i < notificationSpec.attachedActions.size(); i++) {
|
||||
final NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
|
||||
|
||||
if (action.type == NotificationSpec.Action.TYPE_WEARABLE_REPLY || action.type == NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
||||
mNotificationReplyAction.add(notificationSpec.getId(), action.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new NotificationUpdateMessage(notificationUpdateType, notificationSpec.type, getNotificationsCount(notificationSpec.type), notificationSpec.getId(), hasActions);
|
||||
}
|
||||
|
||||
private int getNotificationsCount(NotificationType notificationType) {
|
||||
int count = 0;
|
||||
for (NotificationSpec e : notificationSpecQueue) {
|
||||
count += e.type == notificationType ? 1 : 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private NotificationSpec getNotificationSpecFromQueue(int id) {
|
||||
for (NotificationSpec e : notificationSpecQueue) {
|
||||
if (e.getId() == id) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public NotificationUpdateMessage onDeleteNotification(int id) {
|
||||
if (!enabled)
|
||||
return null;
|
||||
|
||||
Iterator<NotificationSpec> iterator = notificationSpecQueue.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
NotificationSpec e = iterator.next();
|
||||
if (e.getId() == id) {
|
||||
iterator.remove();
|
||||
return new NotificationUpdateMessage(NotificationUpdateMessage.NotificationUpdateType.REMOVE, e.type, getNotificationsCount(e.type), id, false);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public GFDIMessage handle(GFDIMessage message) {
|
||||
if (!enabled)
|
||||
return null;
|
||||
if (message instanceof NotificationControlMessage) {
|
||||
final NotificationSpec notificationSpec = getNotificationSpecFromQueue(((NotificationControlMessage) message).getNotificationId());
|
||||
if (notificationSpec != null) {
|
||||
switch (((NotificationControlMessage) message).getCommand()) {
|
||||
case GET_NOTIFICATION_ATTRIBUTES:
|
||||
return getNotificationDataMessage((NotificationControlMessage) message, notificationSpec);
|
||||
case PERFORM_LEGACY_NOTIFICATION_ACTION:
|
||||
LOG.info("Legacy Notification: {}", ((NotificationControlMessage) message).getLegacyNotificationAction());
|
||||
break;
|
||||
case PERFORM_NOTIFICATION_ACTION:
|
||||
performNotificationAction((NotificationControlMessage) message, notificationSpec);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG.error("NOT SUPPORTED: {}", ((NotificationControlMessage) message).getCommand());
|
||||
}
|
||||
}
|
||||
} else if (message instanceof NotificationDataStatusMessage) {
|
||||
return upload.processUploadProgress((NotificationDataStatusMessage) message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void performNotificationAction(NotificationControlMessage message, NotificationSpec notificationSpec) {
|
||||
final GBDeviceEventNotificationControl deviceEvtNotificationControl = new GBDeviceEventNotificationControl();
|
||||
deviceEvtNotificationControl.handle = notificationSpec.getId();
|
||||
final GBDeviceEventCallControl deviceEvtCallControl = new GBDeviceEventCallControl();
|
||||
switch (message.getNotificationAction()) {
|
||||
case REPLY_INCOMING_CALL:
|
||||
case REPLY_MESSAGES:
|
||||
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
|
||||
deviceEvtNotificationControl.reply = message.getActionString();
|
||||
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) {
|
||||
deviceEvtNotificationControl.phoneNumber = notificationSpec.phoneNumber;
|
||||
} else {
|
||||
deviceEvtNotificationControl.handle = mNotificationReplyAction.lookup(notificationSpec.getId()); //handle of wearable action is needed
|
||||
}
|
||||
message.setDeviceEvent(deviceEvtNotificationControl);
|
||||
break;
|
||||
case ACCEPT_INCOMING_CALL:
|
||||
deviceEvtCallControl.event = GBDeviceEventCallControl.Event.ACCEPT;
|
||||
message.setDeviceEvent(deviceEvtCallControl);
|
||||
break;
|
||||
case REJECT_INCOMING_CALL:
|
||||
deviceEvtCallControl.event = GBDeviceEventCallControl.Event.REJECT;
|
||||
message.setDeviceEvent(deviceEvtCallControl);
|
||||
break;
|
||||
case DISMISS_NOTIFICATION:
|
||||
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.DISMISS;
|
||||
message.setDeviceEvent(deviceEvtNotificationControl);
|
||||
break;
|
||||
case BLOCK_APPLICATION:
|
||||
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.MUTE;
|
||||
message.setDeviceEvent(deviceEvtNotificationControl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationDataMessage getNotificationDataMessage(NotificationControlMessage message, NotificationSpec notificationSpec) {
|
||||
final MessageWriter messageWriter = new MessageWriter();
|
||||
messageWriter.writeByte(NotificationCommand.GET_NOTIFICATION_ATTRIBUTES.code);
|
||||
messageWriter.writeInt(message.getNotificationId());
|
||||
Map.Entry<NotificationAttribute, Integer> lastEntry = null;
|
||||
for (Map.Entry<NotificationAttribute, Integer> entry : message.getNotificationAttributesMap().entrySet()) {
|
||||
if (!NotificationAttribute.MESSAGE_SIZE.equals(entry.getKey())) {
|
||||
encodeNotificationAttribute(notificationSpec, entry, messageWriter);
|
||||
} else {
|
||||
lastEntry = entry;
|
||||
}
|
||||
}
|
||||
if (lastEntry != null) {
|
||||
encodeNotificationAttribute(notificationSpec, lastEntry, messageWriter);
|
||||
}
|
||||
NotificationFragment notificationFragment = new NotificationFragment(messageWriter.getBytes());
|
||||
return upload.setCurrentlyUploading(notificationFragment);
|
||||
}
|
||||
|
||||
|
||||
public void setEnabled(boolean enable) {
|
||||
this.enabled = enable;
|
||||
}
|
||||
|
||||
public enum NotificationCommand { //was AncsCommand
|
||||
GET_NOTIFICATION_ATTRIBUTES(0),
|
||||
GET_APP_ATTRIBUTES(1), //unknown/untested
|
||||
PERFORM_LEGACY_NOTIFICATION_ACTION(2),
|
||||
PERFORM_NOTIFICATION_ACTION(128);
|
||||
|
||||
public final int code;
|
||||
|
||||
NotificationCommand(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static NotificationCommand fromCode(int code) {
|
||||
for (NotificationCommand value : values()) {
|
||||
if (value.code == code)
|
||||
return value;
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown notification command " + code);
|
||||
}
|
||||
}
|
||||
|
||||
public enum LegacyNotificationAction { //was AncsAction
|
||||
ACCEPT,
|
||||
REFUSE
|
||||
|
||||
}
|
||||
public enum NotificationAttribute { //was AncsAttribute
|
||||
APP_IDENTIFIER(0),
|
||||
TITLE(1, true),
|
||||
SUBTITLE(2, true),
|
||||
MESSAGE(3, true),
|
||||
MESSAGE_SIZE(4),
|
||||
DATE(5),
|
||||
// POSITIVE_ACTION_LABEL(6), //needed only for legacy notification actions
|
||||
// NEGATIVE_ACTION_LABEL(7), //needed only for legacy notification actions
|
||||
// Garmin extensions
|
||||
// PHONE_NUMBER(126, true),
|
||||
ACTIONS(127, false, true),
|
||||
;
|
||||
private static final SparseArray<NotificationAttribute> valueByCode;
|
||||
|
||||
static {
|
||||
final NotificationAttribute[] values = values();
|
||||
valueByCode = new SparseArray<>(values.length);
|
||||
for (NotificationAttribute value : values) {
|
||||
valueByCode.append(value.code, value);
|
||||
}
|
||||
}
|
||||
|
||||
public final int code;
|
||||
public final boolean hasLengthParam;
|
||||
public final boolean hasAdditionalParams;
|
||||
|
||||
NotificationAttribute(int code) {
|
||||
this(code, false, false);
|
||||
}
|
||||
|
||||
NotificationAttribute(int code, boolean hasLengthParam) {
|
||||
this(code, hasLengthParam, false);
|
||||
}
|
||||
|
||||
NotificationAttribute(int code, boolean hasLengthParam, boolean hasAdditionalParams) {
|
||||
this.code = code;
|
||||
this.hasLengthParam = hasLengthParam;
|
||||
this.hasAdditionalParams = hasAdditionalParams;
|
||||
}
|
||||
|
||||
public static NotificationAttribute getByCode(int code) {
|
||||
return valueByCode.get(code);
|
||||
}
|
||||
|
||||
public byte[] getNotificationSpecAttribute(NotificationSpec notificationSpec, int maxLength) {
|
||||
String toReturn = "";
|
||||
switch (this) {
|
||||
case DATE:
|
||||
final long notificationTimestamp = notificationSpec.when == 0 ? System.currentTimeMillis() : notificationSpec.when;
|
||||
toReturn = NOTIFICATION_DATE_FORMAT.format(new Date(notificationTimestamp));
|
||||
break;
|
||||
case TITLE:
|
||||
toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
|
||||
break;
|
||||
case SUBTITLE:
|
||||
toReturn = notificationSpec.subject == null ? "" : notificationSpec.subject;
|
||||
break;
|
||||
case APP_IDENTIFIER:
|
||||
toReturn = notificationSpec.sourceAppId == null ? "" : notificationSpec.sourceAppId;
|
||||
break;
|
||||
case MESSAGE:
|
||||
toReturn = notificationSpec.body == null ? "" : notificationSpec.body;
|
||||
break;
|
||||
case MESSAGE_SIZE:
|
||||
toReturn = Integer.toString(notificationSpec.body == null ? "".length() : notificationSpec.body.length());
|
||||
break;
|
||||
case ACTIONS:
|
||||
toReturn = encodeNotificationActionsString(notificationSpec);
|
||||
break;
|
||||
}
|
||||
if (maxLength == 0)
|
||||
return toReturn.getBytes(StandardCharsets.UTF_8);
|
||||
return toReturn.substring(0, Math.min(toReturn.length(), maxLength)).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String encodeNotificationActionsString(NotificationSpec notificationSpec) {
|
||||
|
||||
final List<byte[]> garminActions = new ArrayList<>();
|
||||
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) {
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_INCOMING_CALL, " ")); //text is not shown on watch
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REJECT_INCOMING_CALL, " ")); //text is not shown on watch
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.ACCEPT_INCOMING_CALL, " ")); //text is not shown on watch
|
||||
}
|
||||
if (null != notificationSpec.attachedActions) {
|
||||
for (NotificationSpec.Action action : notificationSpec.attachedActions) {
|
||||
switch (action.type) {
|
||||
case NotificationSpec.Action.TYPE_WEARABLE_REPLY:
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_MESSAGES, action.title));
|
||||
break;
|
||||
case NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS:
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.DISMISS_NOTIFICATION, action.title));
|
||||
break;
|
||||
case NotificationSpec.Action.TYPE_SYNTECTIC_MUTE:
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.BLOCK_APPLICATION, action.title));
|
||||
break;
|
||||
|
||||
}
|
||||
// LOG.info("Notification has action {} with title {}", action.type, action.title);
|
||||
}
|
||||
}
|
||||
if (garminActions.isEmpty())
|
||||
return new String(new byte[]{0x00, 0x00, 0x00, 0x00});
|
||||
|
||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
byteArrayOutputStream.write(garminActions.size());
|
||||
for (byte[] item : garminActions) {
|
||||
byteArrayOutputStream.write(item);
|
||||
}
|
||||
return byteArrayOutputStream.toString();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private byte[] encodeNotificationAction(NotificationAction notificationAction, String description) {
|
||||
final ByteBuffer action = ByteBuffer.allocate(3 + description.getBytes(StandardCharsets.UTF_8).length);
|
||||
action.put((byte) notificationAction.code);
|
||||
if (null == notificationAction.notificationActionIconPosition)
|
||||
action.put((byte) 0x00);
|
||||
else
|
||||
action.put((byte) EnumUtils.generateBitVector(NotificationActionIconPosition.class, notificationAction.notificationActionIconPosition));
|
||||
action.put((byte) description.getBytes(StandardCharsets.UTF_8).length);
|
||||
action.put(description.getBytes());
|
||||
return action.array();
|
||||
}
|
||||
}
|
||||
|
||||
public enum NotificationAction {
|
||||
REPLY_INCOMING_CALL(94, NotificationActionIconPosition.BOTTOM),
|
||||
REPLY_MESSAGES(95, NotificationActionIconPosition.BOTTOM),
|
||||
ACCEPT_INCOMING_CALL(96, NotificationActionIconPosition.RIGHT),
|
||||
REJECT_INCOMING_CALL(97, NotificationActionIconPosition.LEFT),
|
||||
DISMISS_NOTIFICATION(98, NotificationActionIconPosition.LEFT),
|
||||
BLOCK_APPLICATION(99, null),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
private final NotificationActionIconPosition notificationActionIconPosition;
|
||||
|
||||
NotificationAction(int code, NotificationActionIconPosition notificationActionIconPosition) {
|
||||
this.code = code;
|
||||
this.notificationActionIconPosition = notificationActionIconPosition;
|
||||
}
|
||||
|
||||
public static NotificationAction fromCode(final int code) {
|
||||
for (final NotificationAction notificationAction : NotificationAction.values()) {
|
||||
if (notificationAction.code == code) {
|
||||
return notificationAction;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown notification action code " + code);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum NotificationActionIconPosition { //educated guesses based on the icons' positions on vĂvomove style
|
||||
BOTTOM, //or is it reply?
|
||||
RIGHT, //or is it accept?
|
||||
LEFT, //or is it dismiss/refuse?
|
||||
}
|
||||
public static class Upload {
|
||||
|
||||
private NotificationFragment currentlyUploading;
|
||||
|
||||
public NotificationDataMessage setCurrentlyUploading(NotificationFragment currentlyUploading) {
|
||||
this.currentlyUploading = currentlyUploading;
|
||||
return currentlyUploading.take();
|
||||
}
|
||||
|
||||
private GFDIMessage processUploadProgress(NotificationDataStatusMessage notificationDataStatusMessage) {
|
||||
if (null == currentlyUploading) {
|
||||
LOG.warn("Received Upload Progress but we are not sending any notification");
|
||||
return null;
|
||||
}
|
||||
if (!currentlyUploading.dataHolder.hasRemaining()) {
|
||||
this.currentlyUploading = null;
|
||||
LOG.info("SENT ALL");
|
||||
|
||||
return new NotificationDataStatusMessage(GFDIMessage.GarminMessage.NOTIFICATION_DATA, GFDIMessage.Status.ACK, NotificationDataStatusMessage.TransferStatus.OK);
|
||||
} else {
|
||||
if (notificationDataStatusMessage.canProceed()) {
|
||||
LOG.info("SENDING NEXT CHUNK!!!");
|
||||
return currentlyUploading.take();
|
||||
} else {
|
||||
LOG.warn("Cannot proceed with upload"); //TODO: send the correct status message
|
||||
this.currentlyUploading = null;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class NotificationFragment {
|
||||
private final int dataSize;
|
||||
private final ByteBuffer dataHolder;
|
||||
private final int maxBlockSize = 300;
|
||||
private int runningCrc;
|
||||
|
||||
NotificationFragment(byte[] contents) {
|
||||
this.dataHolder = ByteBuffer.wrap(contents);
|
||||
this.dataSize = contents.length;
|
||||
this.dataHolder.flip();
|
||||
this.dataHolder.compact();
|
||||
this.setRunningCrc(0);
|
||||
}
|
||||
|
||||
public int getDataSize() {
|
||||
return dataSize;
|
||||
}
|
||||
|
||||
private int getMaxBlockSize() {
|
||||
return maxBlockSize;
|
||||
}
|
||||
|
||||
private NotificationDataMessage take() {
|
||||
final int currentOffset = this.dataHolder.position();
|
||||
final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize())];
|
||||
this.dataHolder.get(chunk);
|
||||
setRunningCrc(ChecksumCalculator.computeCrc(getRunningCrc(), chunk, 0, chunk.length));
|
||||
return new NotificationDataMessage(chunk, getDataSize(), currentOffset, getRunningCrc());
|
||||
}
|
||||
|
||||
private int getRunningCrc() {
|
||||
return runningCrc;
|
||||
}
|
||||
|
||||
private void setRunningCrc(int runningCrc) {
|
||||
this.runningCrc = runningCrc;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,407 @@
|
|||
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.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
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.GdiHttpService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmsNotification;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.HttpHandler;
|
||||
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.status.ProtobufStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
|
||||
|
||||
public class ProtocolBufferHandler implements MessageHandler {
|
||||
|
||||
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;
|
||||
|
||||
private Map<GdiSmsNotification.SmsNotificationService.CannedListType, String[]> cannedListTypeMap;
|
||||
|
||||
public ProtocolBufferHandler(GarminSupport deviceSupport) {
|
||||
this.deviceSupport = deviceSupport;
|
||||
chunkedFragmentsMap = new HashMap<>();
|
||||
}
|
||||
|
||||
private int getNextProtobufRequestId() {
|
||||
lastProtobufRequestId = (lastProtobufRequestId + 1) % 65536;
|
||||
return lastProtobufRequestId;
|
||||
}
|
||||
|
||||
public ProtobufMessage handle(GFDIMessage protobufMessage) {
|
||||
if (protobufMessage instanceof ProtobufMessage) {
|
||||
return processIncoming((ProtobufMessage) protobufMessage);
|
||||
} else if (protobufMessage instanceof ProtobufStatusMessage) {
|
||||
return processIncoming((ProtobufStatusMessage) protobufMessage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private 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.hasSmsNotificationService()) {
|
||||
return prepareProtobufResponse(processProtobufSmsNotificationMessage(smart.getSmsNotificationService()), message.getRequestId());
|
||||
}
|
||||
if (smart.hasHttpService()) {
|
||||
final GdiHttpService.HttpService response = HttpHandler.handle(smart.getHttpService());
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
return prepareProtobufResponse(GdiSmartProto.Smart.newBuilder().setHttpService(response).build(), 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);
|
||||
message.setStatusMessage(new ProtobufStatusMessage(message.getMessageType(), GFDIMessage.Status.ACK, message.getRequestId(), message.getDataOffset(), ProtobufStatusMessage.ProtobufChunkStatus.DISCARDED, ProtobufStatusMessage.ProtobufStatusCode.UNKNOWN_REQUEST_ID));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ProtobufMessage processIncoming(ProtobufStatusMessage statusMessage) {
|
||||
LOG.info("Processing protobuf status message #{}@{}: status={}, error={}", statusMessage.getRequestId(), statusMessage.getDataOffset(), statusMessage.getProtobufChunkStatus(), statusMessage.getProtobufStatusCode());
|
||||
//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 GdiSmartProto.Smart processProtobufSmsNotificationMessage(GdiSmsNotification.SmsNotificationService smsNotificationService) {
|
||||
if (smsNotificationService.hasSmsCannedListRequest()) {
|
||||
if (null == this.cannedListTypeMap || this.cannedListTypeMap.isEmpty()) {
|
||||
this.cannedListTypeMap = new HashMap<>();
|
||||
|
||||
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
|
||||
for (GdiSmsNotification.SmsNotificationService.CannedListType type :
|
||||
requestedTypes) {
|
||||
if (GdiSmsNotification.SmsNotificationService.CannedListType.SMS_MESSAGE_RESPONSE.equals(type)) {
|
||||
final ArrayList<String> messages = new ArrayList<>();
|
||||
for (int i = 1; i <= 16; i++) {
|
||||
String message = deviceSupport.getDevicePrefs().getString("canned_reply_" + i, null);
|
||||
if (message != null && !message.isEmpty()) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
if (!messages.isEmpty())
|
||||
this.cannedListTypeMap.put(type, messages.toArray(new String[0]));
|
||||
} else if (GdiSmsNotification.SmsNotificationService.CannedListType.PHONE_CALL_RESPONSE.equals(type)) {
|
||||
final ArrayList<String> messages = new ArrayList<>();
|
||||
for (int i = 1; i <= 16; i++) {
|
||||
String message = deviceSupport.getDevicePrefs().getString("canned_message_dismisscall_" + i, null);
|
||||
if (message != null && !message.isEmpty()) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
if (!messages.isEmpty())
|
||||
this.cannedListTypeMap.put(type, messages.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
|
||||
|
||||
GdiSmsNotification.SmsNotificationService.SmsCannedListResponse.Builder builder = GdiSmsNotification.SmsNotificationService.SmsCannedListResponse.newBuilder()
|
||||
.setStatus(GdiSmsNotification.SmsNotificationService.ResponseStatus.SUCCESS);
|
||||
for (GdiSmsNotification.SmsNotificationService.CannedListType requestedType : requestedTypes) {
|
||||
if (this.cannedListTypeMap.containsKey(requestedType)) {
|
||||
builder.addLists(GdiSmsNotification.SmsNotificationService.SmsCannedList.newBuilder()
|
||||
.addAllResponse(Arrays.asList(this.cannedListTypeMap.get(requestedType)))
|
||||
.setType(requestedType)
|
||||
);
|
||||
} else {
|
||||
builder.setStatus(GdiSmsNotification.SmsNotificationService.ResponseStatus.GENERIC_ERROR);
|
||||
LOG.error("Missing canned messages data for type {}", requestedType);
|
||||
}
|
||||
}
|
||||
|
||||
return GdiSmartProto.Smart.newBuilder().setSmsNotificationService(GdiSmsNotification.SmsNotificationService.newBuilder().setSmsCannedListResponse(builder)).build();
|
||||
} else {
|
||||
LOG.warn("Protobuf smsNotificationService request not implemented: {}", smsNotificationService);
|
||||
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, requestId);
|
||||
}
|
||||
|
||||
private ProtobufMessage prepareProtobufResponse(GdiSmartProto.Smart protobufPayload, int requestId) {
|
||||
if (null == protobufPayload)
|
||||
return null;
|
||||
return prepareProtobufMessage(protobufPayload.toByteArray(), GFDIMessage.GarminMessage.PROTOBUF_RESPONSE, requestId);
|
||||
}
|
||||
|
||||
private ProtobufMessage prepareProtobufMessage(byte[] bytes, GFDIMessage.GarminMessage garminMessage, int requestId) {
|
||||
if (bytes == null || bytes.length == 0)
|
||||
return null;
|
||||
LOG.info("Preparing protobuf message. Type{}, #{}, {}B: {}", garminMessage, requestId, bytes.length, GB.hexdump(bytes, 0, bytes.length));
|
||||
|
||||
if (bytes.length > maxChunkSize) {
|
||||
chunkedFragmentsMap.put(requestId, new ProtobufFragment(bytes));
|
||||
return new ProtobufMessage(garminMessage,
|
||||
requestId,
|
||||
0,
|
||||
bytes.length,
|
||||
maxChunkSize,
|
||||
ArrayUtils.subarray(bytes, 0, maxChunkSize));
|
||||
}
|
||||
return new ProtobufMessage(garminMessage, requestId, 0, bytes.length, bytes.length, bytes);
|
||||
}
|
||||
|
||||
public ProtobufMessage setCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
final GdiSmsNotification.SmsNotificationService.CannedListType cannedListType;
|
||||
switch (cannedMessagesSpec.type) {
|
||||
case CannedMessagesSpec.TYPE_REJECTEDCALLS:
|
||||
cannedListType = GdiSmsNotification.SmsNotificationService.CannedListType.PHONE_CALL_RESPONSE;
|
||||
break;
|
||||
case CannedMessagesSpec.TYPE_GENERIC:
|
||||
case CannedMessagesSpec.TYPE_NEWSMS:
|
||||
cannedListType = GdiSmsNotification.SmsNotificationService.CannedListType.SMS_MESSAGE_RESPONSE;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown canned messages type, ignoring.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null == this.cannedListTypeMap) {
|
||||
this.cannedListTypeMap = new HashMap<>();
|
||||
}
|
||||
this.cannedListTypeMap.put(cannedListType, cannedMessagesSpec.cannedMessages);
|
||||
|
||||
GdiSmartProto.Smart smart = GdiSmartProto.Smart.newBuilder()
|
||||
.setSmsNotificationService(GdiSmsNotification.SmsNotificationService.newBuilder()
|
||||
.setSmsCannedListChangedNotification(
|
||||
GdiSmsNotification.SmsNotificationService.SmsCannedListChangedNotification.newBuilder().addChangedType(cannedListType)
|
||||
)
|
||||
).build();
|
||||
|
||||
return prepareProtobufRequest(smart);
|
||||
}
|
||||
|
||||
private class ProtobufFragment {
|
||||
private final byte[] fragmentBytes;
|
||||
private final int totalLength;
|
||||
|
||||
public ProtobufFragment(byte[] fragmentBytes) {
|
||||
this.fragmentBytes = fragmentBytes;
|
||||
this.totalLength = fragmentBytes.length;
|
||||
}
|
||||
|
||||
public ProtobufFragment(ProtobufMessage message) {
|
||||
if (message.getDataOffset() != 0)
|
||||
throw new IllegalArgumentException("Cannot create fragment if message is not the first of the sequence");
|
||||
this.fragmentBytes = message.getMessageBytes();
|
||||
this.totalLength = message.getTotalProtobufLength();
|
||||
}
|
||||
|
||||
public ProtobufFragment(ProtobufFragment existing, ProtobufMessage toMerge) {
|
||||
if (toMerge.getDataOffset() != existing.fragmentBytes.length)
|
||||
throw new IllegalArgumentException("Cannot merge fragment: incoming message has different offset than needed");
|
||||
this.fragmentBytes = ArrayUtils.addAll(existing.fragmentBytes, toMerge.getMessageBytes());
|
||||
this.totalLength = existing.totalLength;
|
||||
}
|
||||
|
||||
public ProtobufMessage getNextChunk(ProtobufStatusMessage protobufStatusMessage) {
|
||||
int start = protobufStatusMessage.getDataOffset() + maxChunkSize;
|
||||
int length = Math.min(maxChunkSize, this.fragmentBytes.length - start);
|
||||
|
||||
return new ProtobufMessage(protobufStatusMessage.getMessageType(),
|
||||
protobufStatusMessage.getRequestId(),
|
||||
start,
|
||||
this.totalLength,
|
||||
length,
|
||||
ArrayUtils.subarray(this.fragmentBytes, start, start + length));
|
||||
}
|
||||
|
||||
public boolean isComplete() {
|
||||
return totalLength == fragmentBytes.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class CobsCoDec {
|
||||
private static final long BUFFER_TIMEOUT = 1500L; // turn this value up while debugging
|
||||
private final ByteBuffer byteBuffer = ByteBuffer.allocate(10_000);
|
||||
private long lastUpdate;
|
||||
private byte[] cobsDecodedMessage;
|
||||
|
||||
/**
|
||||
* Accumulates received bytes in a local buffer, clearing it after a timeout, and attempts to
|
||||
* parse it.
|
||||
*
|
||||
* @param bytes
|
||||
*/
|
||||
public void receivedBytes(byte[] bytes) {
|
||||
final long now = System.currentTimeMillis();
|
||||
if ((now - lastUpdate) > BUFFER_TIMEOUT) {
|
||||
reset();
|
||||
}
|
||||
lastUpdate = now;
|
||||
|
||||
byteBuffer.put(bytes);
|
||||
decode();
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
cobsDecodedMessage = null;
|
||||
byteBuffer.clear();
|
||||
}
|
||||
|
||||
public byte[] retrieveMessage() {
|
||||
final byte[] resultPacket = cobsDecodedMessage;
|
||||
cobsDecodedMessage = null;
|
||||
return resultPacket;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* COBS decoding algorithm variant, which relies on a leading and a trailing 0 byte (the former
|
||||
* is not part of default implementations).
|
||||
* This function removes the complete message from the internal buffer, if it could be decoded.
|
||||
*/
|
||||
private void decode() {
|
||||
if (cobsDecodedMessage != null) {
|
||||
// packet is waiting, unable to parse more
|
||||
return;
|
||||
}
|
||||
if (byteBuffer.position() < 4) {
|
||||
// minimal payload length including the padding
|
||||
return;
|
||||
}
|
||||
if (0 != byteBuffer.get(byteBuffer.position() - 1))
|
||||
return; //no 0x00 at the end, hence no full packet
|
||||
byteBuffer.position(byteBuffer.position() - 1); //don't process the trailing 0
|
||||
byteBuffer.flip();
|
||||
if (0 != byteBuffer.get())
|
||||
return; //no 0x00 at the start
|
||||
ByteBuffer decodedBytesBuffer = ByteBuffer.allocate(byteBuffer.limit()); //leading and trailing 0x00 bytes
|
||||
while (byteBuffer.hasRemaining()) {
|
||||
byte code = byteBuffer.get();
|
||||
if (code == 0) {
|
||||
break;
|
||||
}
|
||||
int codeValue = code & 0xFF;
|
||||
int payloadSize = codeValue - 1;
|
||||
for (int i = 0; i < payloadSize; i++) {
|
||||
decodedBytesBuffer.put(byteBuffer.get());
|
||||
}
|
||||
if (codeValue != 0xFF && byteBuffer.hasRemaining()) {
|
||||
decodedBytesBuffer.put((byte) 0); // Append a zero byte after the payload
|
||||
}
|
||||
}
|
||||
|
||||
decodedBytesBuffer.flip();
|
||||
cobsDecodedMessage = new byte[decodedBytesBuffer.remaining()];
|
||||
decodedBytesBuffer.get(cobsDecodedMessage);
|
||||
byteBuffer.compact();
|
||||
}
|
||||
|
||||
// this implementation of COBS relies on a leading and a trailing 0 byte (the former is not part of default implementations)
|
||||
public byte[] encode(byte[] data) {
|
||||
ByteBuffer encodedBytesBuffer = ByteBuffer.allocate((data.length * 2) + 1); // Maximum expansion
|
||||
|
||||
encodedBytesBuffer.put((byte) 0);// Garmin initial padding
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
|
||||
while (buffer.position() < buffer.limit()) {
|
||||
int startPos = buffer.position();
|
||||
int zeroIndex = buffer.position();
|
||||
|
||||
while (buffer.hasRemaining() && buffer.get() != 0) {
|
||||
zeroIndex++;
|
||||
}
|
||||
|
||||
int payloadSize = zeroIndex - startPos;
|
||||
|
||||
while (payloadSize > 0xFE) {
|
||||
encodedBytesBuffer.put((byte) 0xFF); // Maximum payload size indicator
|
||||
for (int i = 0; i < 0xFE; i++) {
|
||||
encodedBytesBuffer.put(data[startPos + i]);
|
||||
}
|
||||
payloadSize -= 0xFE;
|
||||
startPos += 0xFE;
|
||||
}
|
||||
|
||||
encodedBytesBuffer.put((byte) (payloadSize + 1));
|
||||
|
||||
for (int i = startPos; i < zeroIndex; i++) {
|
||||
encodedBytesBuffer.put(data[i]);
|
||||
}
|
||||
|
||||
if (buffer.hasRemaining()) {
|
||||
zeroIndex++; // Include the zero byte in the next block
|
||||
}
|
||||
|
||||
if (!buffer.hasRemaining() && payloadSize == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.position(zeroIndex);
|
||||
}
|
||||
|
||||
encodedBytesBuffer.put((byte) 0); // Append a zero byte to indicate end of encoding
|
||||
encodedBytesBuffer.flip();
|
||||
|
||||
byte[] encodedBytes = new byte[encodedBytesBuffer.remaining()];
|
||||
encodedBytesBuffer.get(encodedBytes);
|
||||
|
||||
return encodedBytes;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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 onMtuChanged(final int mtu);
|
||||
|
||||
void initializeDevice(TransactionBuilder builder);
|
||||
|
||||
boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);
|
||||
|
||||
interface Callback {
|
||||
void onMessage(byte[] message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
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 onMtuChanged(final int mtu) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeDevice(final TransactionBuilder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(final byte[] message) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
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 int maxWriteSize = 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 onMtuChanged(final int mtu) {
|
||||
maxWriteSize = mtu - 3;
|
||||
}
|
||||
|
||||
@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 > maxWriteSize - 1) {
|
||||
int position = 0;
|
||||
while (remainingBytes > 0) {
|
||||
final byte[] fragment = Arrays.copyOfRange(payload, position, position + Math.min(remainingBytes, maxWriteSize - 1));
|
||||
builder.write(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND), ArrayUtils.addAll(new byte[]{(byte) gfdiHandle}, fragment));
|
||||
position += fragment.length;
|
||||
remainingBytes -= fragment.length;
|
||||
}
|
||||
} else {
|
||||
builder.write(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND), ArrayUtils.addAll(new byte[]{(byte) gfdiHandle}, payload));
|
||||
}
|
||||
builder.queue(this.mSupport.getQueue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
||||
ByteBuffer message = ByteBuffer.wrap(characteristic.getValue()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
// LOG.debug("RECEIVED: {}", GB.hexdump(message.array()));
|
||||
final byte handle = message.get();
|
||||
if (0x00 == handle) { //handle management message
|
||||
|
||||
final byte type = message.get();
|
||||
final long incomingClientID = message.getLong();
|
||||
|
||||
if (incomingClientID != this.gadgetBridgeClientID) {
|
||||
LOG.debug("Ignoring incoming message, client ID is not ours. Message: {}", GB.hexdump(message.array()));
|
||||
}
|
||||
RequestType requestType = RequestType.fromCode(type);
|
||||
if (null == requestType) {
|
||||
LOG.error("Unknown request type. Message: {}", message.array());
|
||||
return true;
|
||||
}
|
||||
switch (requestType) {
|
||||
case REGISTER_ML_REQ: //register service request
|
||||
case CLOSE_HANDLE_REQ: //close handle request
|
||||
case CLOSE_ALL_REQ: //close all handles request
|
||||
case UNK_REQ: //unknown request
|
||||
LOG.warn("Received handle request, expecting responses. Message: {}", message.array());
|
||||
case REGISTER_ML_RESP: //register service response
|
||||
LOG.debug("Received register response. Message: {}", message.array());
|
||||
final short registeredService = message.getShort();
|
||||
final byte status = message.get();
|
||||
if (0 == status && 1 == registeredService) { //success
|
||||
this.gfdiHandle = message.get();
|
||||
}
|
||||
break;
|
||||
case CLOSE_HANDLE_RESP: //close handle response
|
||||
LOG.debug("Received close handle response. Message: {}", message.array());
|
||||
break;
|
||||
case CLOSE_ALL_RESP: //close all handles response
|
||||
LOG.debug("Received close all handles response. Message: {}", message.array());
|
||||
new TransactionBuilder("open GFDI")
|
||||
.write(this.mSupport.getCharacteristic(UUID_CHARACTERISTIC_GARMIN_ML_GFDI_SEND), registerGFDI())
|
||||
.queue(this.mSupport.getQueue());
|
||||
break;
|
||||
case UNK_RESP: //unknown response
|
||||
LOG.debug("Received unknown. Message: {}", message.array());
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (this.gfdiHandle == handle) {
|
||||
|
||||
byte[] partial = new byte[message.remaining()];
|
||||
message.get(partial);
|
||||
this.cobsCoDec.receivedBytes(partial);
|
||||
|
||||
this.mSupport.onMessage(this.cobsCoDec.retrieveMessage());
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected byte[] closeAllServices() {
|
||||
ByteBuffer toSend = ByteBuffer.allocate(13);
|
||||
toSend.order(ByteOrder.BIG_ENDIAN);
|
||||
toSend.putShort((short) RequestType.CLOSE_ALL_REQ.ordinal()); //close all services
|
||||
toSend.order(ByteOrder.LITTLE_ENDIAN);
|
||||
toSend.putLong(this.gadgetBridgeClientID);
|
||||
toSend.putShort((short) 0);
|
||||
return toSend.array();
|
||||
}
|
||||
|
||||
protected byte[] registerGFDI() {
|
||||
ByteBuffer toSend = ByteBuffer.allocate(13);
|
||||
toSend.order(ByteOrder.BIG_ENDIAN);
|
||||
toSend.putShort((short) RequestType.REGISTER_ML_REQ.ordinal()); //register service request
|
||||
toSend.order(ByteOrder.LITTLE_ENDIAN);
|
||||
toSend.putLong(this.gadgetBridgeClientID);
|
||||
toSend.putShort((short) 1); //service GFDI
|
||||
return toSend.array();
|
||||
}
|
||||
|
||||
enum RequestType {
|
||||
REGISTER_ML_REQ,
|
||||
REGISTER_ML_RESP,
|
||||
CLOSE_HANDLE_REQ,
|
||||
CLOSE_HANDLE_RESP,
|
||||
UNK_HANDLE,
|
||||
CLOSE_ALL_REQ,
|
||||
CLOSE_ALL_RESP,
|
||||
UNK_REQ,
|
||||
UNK_RESP;
|
||||
|
||||
public static RequestType fromCode(final int code) {
|
||||
for (final RequestType requestType : RequestType.values()) {
|
||||
if (requestType.ordinal() == code) {
|
||||
return requestType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileTransferHandler;
|
||||
|
||||
public class FileDownloadedDeviceEvent extends GBDeviceEvent {
|
||||
public FileTransferHandler.DirectoryEntry directoryEntry;
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
|
||||
public class NotificationSubscriptionDeviceEvent extends GBDeviceEvent {
|
||||
|
||||
public boolean enable;
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
|
||||
|
||||
public class SupportedFileTypesDeviceEvent extends GBDeviceEvent {
|
||||
|
||||
private final List<FileType> supportedFileTypes;
|
||||
|
||||
public SupportedFileTypesDeviceEvent(List<FileType> fileTypes) {
|
||||
this.supportedFileTypes = fileTypes;
|
||||
}
|
||||
|
||||
public List<FileType> getSupportedFileTypes() {
|
||||
return supportedFileTypes;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
|
||||
public class WeatherRequestDeviceEvent extends GBDeviceEvent {
|
||||
private final int format;
|
||||
private final int latitude;
|
||||
private final int longitude;
|
||||
private final int hoursOfForecast;
|
||||
public WeatherRequestDeviceEvent(int format, int latitude, int longitude, int hoursOfForecast) {
|
||||
this.format = format;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.hoursOfForecast = hoursOfForecast;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class DevFieldDefinition {
|
||||
public final ByteBuffer valueHolder;
|
||||
private final int fieldDefinitionNumber;
|
||||
private final int size;
|
||||
private final int developerDataIndex;
|
||||
private BaseType baseType;
|
||||
private String name;
|
||||
|
||||
public DevFieldDefinition(int fieldDefinitionNumber, int size, int developerDataIndex, String name) {
|
||||
this.fieldDefinitionNumber = fieldDefinitionNumber;
|
||||
this.size = size;
|
||||
this.developerDataIndex = developerDataIndex;
|
||||
this.name = name;
|
||||
this.valueHolder = ByteBuffer.allocate(size);
|
||||
}
|
||||
|
||||
public static DevFieldDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader) {
|
||||
int number = garminByteBufferReader.readByte();
|
||||
int size = garminByteBufferReader.readByte();
|
||||
int developerDataIndex = garminByteBufferReader.readByte();
|
||||
|
||||
return new DevFieldDefinition(number, size, developerDataIndex, "");
|
||||
|
||||
}
|
||||
|
||||
public BaseType getBaseType() {
|
||||
return baseType;
|
||||
}
|
||||
|
||||
public void setBaseType(BaseType baseType) {
|
||||
this.baseType = baseType;
|
||||
}
|
||||
|
||||
public int getDeveloperDataIndex() {
|
||||
return developerDataIndex;
|
||||
}
|
||||
|
||||
public int getFieldDefinitionNumber() {
|
||||
return fieldDefinitionNumber;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
|
||||
public class FieldDefinition implements FieldInterface {
|
||||
protected final BaseType baseType;
|
||||
protected final int scale;
|
||||
protected final int offset;
|
||||
private final int number;
|
||||
private final int size;
|
||||
private final String name;
|
||||
|
||||
public FieldDefinition(int number, int size, BaseType baseType, String name, int scale, int offset) {
|
||||
this.number = number;
|
||||
this.size = size;
|
||||
this.baseType = baseType;
|
||||
this.name = name;
|
||||
this.scale = scale;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public FieldDefinition(int number, int size, BaseType baseType, String name) {
|
||||
this(number, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
public static FieldDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader, GlobalFITMessage globalFITMessage) {
|
||||
int number = garminByteBufferReader.readByte();
|
||||
int size = garminByteBufferReader.readByte();
|
||||
int baseTypeIdentifier = garminByteBufferReader.readByte();
|
||||
BaseType baseType = BaseType.fromIdentifier(baseTypeIdentifier);
|
||||
if (number == 253 && size == 4 && baseType.equals(BaseType.UINT32))
|
||||
return new FieldDefinitionTimestamp(number, size, baseType, "253_timestamp");
|
||||
|
||||
FieldDefinition global = globalFITMessage.getFieldDefinition(number, size);
|
||||
if (null != global && global.getBaseType().equals(baseType)) {
|
||||
return global;
|
||||
}
|
||||
return new FieldDefinition(number, size, baseType, "");
|
||||
}
|
||||
|
||||
public int getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public BaseType getBaseType() {
|
||||
return baseType;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void generateOutgoingPayload(MessageWriter writer) {
|
||||
writer.writeByte(number);
|
||||
writer.writeByte(size);
|
||||
writer.writeByte(baseType.getIdentifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
return baseType.decode(byteBuffer, scale, offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
baseType.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
baseType.invalidate(byteBuffer);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionDayOfWeek;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTemperature;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionWeatherCondition;
|
||||
|
||||
public class FieldDefinitionFactory {
|
||||
|
||||
public static FieldDefinition create(int localNumber, int size, BaseType baseType, String name, int scale, int offset) {
|
||||
return new FieldDefinition(localNumber, size, baseType, name, scale, offset);
|
||||
}
|
||||
|
||||
public static FieldDefinition create(int localNumber, int size, FIELD field, BaseType baseType, String name, int scale, int offset) {
|
||||
if (null == field) {
|
||||
return new FieldDefinition(localNumber, size, baseType, name, scale, offset);
|
||||
}
|
||||
switch (field) {
|
||||
case ALARM:
|
||||
return new FieldDefinitionAlarm(localNumber, size, baseType, name);
|
||||
case DAY_OF_WEEK:
|
||||
return new FieldDefinitionDayOfWeek(localNumber, size, baseType, name);
|
||||
case FILE_TYPE:
|
||||
return new FieldDefinitionFileType(localNumber, size, baseType, name);
|
||||
case GOAL_SOURCE:
|
||||
return new FieldDefinitionGoalSource(localNumber, size, baseType, name);
|
||||
case GOAL_TYPE:
|
||||
return new FieldDefinitionGoalType(localNumber, size, baseType, name);
|
||||
case MEASUREMENT_SYSTEM:
|
||||
return new FieldDefinitionMeasurementSystem(localNumber, size, baseType, name);
|
||||
case TEMPERATURE:
|
||||
return new FieldDefinitionTemperature(localNumber, size, baseType, name);
|
||||
case TIMESTAMP:
|
||||
return new FieldDefinitionTimestamp(localNumber, size, baseType, name);
|
||||
case WEATHER_CONDITION:
|
||||
return new FieldDefinitionWeatherCondition(localNumber, size, baseType, name);
|
||||
case LANGUAGE:
|
||||
return new FieldDefinitionLanguage(localNumber, size, baseType, name);
|
||||
default:
|
||||
return new FieldDefinition(localNumber, size, baseType, name);
|
||||
}
|
||||
}
|
||||
|
||||
public enum FIELD {
|
||||
ALARM,
|
||||
DAY_OF_WEEK,
|
||||
FILE_TYPE,
|
||||
GOAL_SOURCE,
|
||||
GOAL_TYPE,
|
||||
MEASUREMENT_SYSTEM,
|
||||
TEMPERATURE,
|
||||
TIMESTAMP,
|
||||
WEATHER_CONDITION,
|
||||
LANGUAGE,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public interface FieldInterface {
|
||||
Object decode(ByteBuffer byteBuffer);
|
||||
|
||||
void encode(ByteBuffer byteBuffer, Object o);
|
||||
|
||||
void invalidate(ByteBuffer byteBuffer);
|
||||
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
|
||||
public class FitFile {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(FitFile.class);
|
||||
private final Header header;
|
||||
private final Map<RecordDefinition, List<RecordData>> dataRecords;
|
||||
private final boolean canGenerateOutput;
|
||||
|
||||
public FitFile(Header header, Map<RecordDefinition, List<RecordData>> dataRecords) {
|
||||
this.header = header;
|
||||
this.dataRecords = dataRecords;
|
||||
this.canGenerateOutput = false;
|
||||
}
|
||||
|
||||
public FitFile(LinkedHashMap<RecordDefinition, List<RecordData>> dataRecords) {
|
||||
this.dataRecords = dataRecords;
|
||||
this.header = new Header(true, 16, 21117);
|
||||
this.canGenerateOutput = true;
|
||||
}
|
||||
|
||||
private static byte[] readFileToByteArray(File file) {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); InputStream inputStream = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static FitFile parseIncoming(File file) {
|
||||
return parseIncoming(readFileToByteArray(file));
|
||||
}
|
||||
|
||||
//TODO: process file in chunks??
|
||||
public static FitFile parseIncoming(byte[] fileContents) {
|
||||
|
||||
final GarminByteBufferReader garminByteBufferReader = new GarminByteBufferReader(fileContents);
|
||||
garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
final Header header = Header.parseIncomingHeader(garminByteBufferReader);
|
||||
|
||||
Map<RecordHeader, RecordDefinition> recordDefinitionMap = new HashMap<>(); //needed because the headers can be redefined in the file. The last header wins
|
||||
Map<RecordDefinition, List<RecordData>> dataRecords = new LinkedHashMap<>();
|
||||
Long referenceTimestamp = null;
|
||||
|
||||
while (garminByteBufferReader.getPosition() < header.getHeaderSize() + header.getDataSize()) {
|
||||
byte rawRecordHeader = (byte) garminByteBufferReader.readByte();
|
||||
RecordHeader recordHeader = new RecordHeader(rawRecordHeader);
|
||||
if (recordHeader.isCompressedTimestamp()) {
|
||||
referenceTimestamp += recordHeader.getTimeOffset();
|
||||
recordHeader.setReferenceTimestamp(referenceTimestamp);
|
||||
}
|
||||
if (recordHeader.isDefinition()) {
|
||||
final RecordDefinition recordDefinition = RecordDefinition.parseIncoming(garminByteBufferReader, recordHeader);
|
||||
if (recordDefinition != null) {
|
||||
if (recordHeader.isDeveloperData())
|
||||
for (RecordDefinition rd : dataRecords.keySet()) {
|
||||
if (GlobalFITMessage.FIELD_DESCRIPTION.equals(rd.getGlobalFITMessage()))
|
||||
recordDefinition.populateDevFields(dataRecords.get(rd));
|
||||
}
|
||||
recordDefinitionMap.put(recordHeader, recordDefinition);
|
||||
dataRecords.put(recordDefinition, new ArrayList<>());
|
||||
}
|
||||
} else {
|
||||
final RecordDefinition referenceRecordDefinition = recordDefinitionMap.get(recordHeader);
|
||||
final List<RecordData> myList = dataRecords.get(referenceRecordDefinition);
|
||||
if (referenceRecordDefinition != null) {
|
||||
final RecordData runningData = new RecordData(referenceRecordDefinition, recordHeader);
|
||||
myList.add(runningData);
|
||||
Long newTimestamp = runningData.parseDataMessage(garminByteBufferReader);
|
||||
if (newTimestamp != null)
|
||||
referenceTimestamp = newTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
int fileCrc = garminByteBufferReader.readShort();
|
||||
if (fileCrc != ChecksumCalculator.computeCrc(fileContents, header.getHeaderSize(), fileContents.length - header.getHeaderSize() - 2)) {
|
||||
throw new IllegalArgumentException("Wrong CRC for FIT file");
|
||||
}
|
||||
return new FitFile(header, dataRecords);
|
||||
}
|
||||
|
||||
public List<RecordData> getRecordsByGlobalMessage(GlobalFITMessage globalFITMessage) {
|
||||
final List<RecordData> filtered = new ArrayList<>();
|
||||
for (RecordDefinition rd : dataRecords.keySet()) {
|
||||
if (globalFITMessage.equals(rd.getGlobalFITMessage()))
|
||||
filtered.addAll(dataRecords.get(rd));
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public void generateOutgoingDataPayload(MessageWriter writer) {
|
||||
if (!canGenerateOutput)
|
||||
throw new IllegalArgumentException("Generation of previously parsed FIT file not supported.");
|
||||
|
||||
MessageWriter temporary = new MessageWriter();
|
||||
temporary.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
for (Map.Entry<RecordDefinition, List<RecordData>> entry : dataRecords.entrySet()) {
|
||||
RecordDefinition key = entry.getKey();
|
||||
List<RecordData> valueList = entry.getValue();
|
||||
|
||||
key.generateOutgoingPayload(temporary);
|
||||
for (RecordData rd :
|
||||
valueList) {
|
||||
rd.generateOutgoingDataPayload(temporary);
|
||||
}
|
||||
}
|
||||
this.header.setDataSize(temporary.getSize());
|
||||
|
||||
this.header.generateOutgoingDataPayload(writer);
|
||||
writer.writeBytes(temporary.getBytes());
|
||||
writer.writeShort(ChecksumCalculator.computeCrc(writer.getBytes(), this.header.getHeaderSize(), writer.getBytes().length - this.header.getHeaderSize()));
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return dataRecords.toString();
|
||||
}
|
||||
|
||||
static class Header {
|
||||
public static final int MAGIC = 0x5449462E;
|
||||
|
||||
private final int headerSize;
|
||||
private final int protocolVersion;
|
||||
private final int profileVersion;
|
||||
private final boolean hasCRC;
|
||||
private int dataSize;
|
||||
|
||||
public Header(boolean hasCRC, int protocolVersion, int profileVersion) {
|
||||
this(hasCRC, protocolVersion, profileVersion, 0);
|
||||
}
|
||||
|
||||
public Header(boolean hasCRC, int protocolVersion, int profileVersion, int dataSize) {
|
||||
this.hasCRC = hasCRC;
|
||||
headerSize = hasCRC ? 14 : 12;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.profileVersion = profileVersion;
|
||||
this.dataSize = dataSize;
|
||||
}
|
||||
|
||||
static Header parseIncomingHeader(GarminByteBufferReader garminByteBufferReader) {
|
||||
int headerSize = garminByteBufferReader.readByte();
|
||||
if (headerSize < 12) {
|
||||
throw new IllegalArgumentException("Too short header in FIT file.");
|
||||
}
|
||||
boolean hasCRC = headerSize == 14;
|
||||
int protocolVersion = garminByteBufferReader.readByte();
|
||||
int profileVersion = garminByteBufferReader.readShort();
|
||||
int dataSize = garminByteBufferReader.readInt();
|
||||
int magic = garminByteBufferReader.readInt();
|
||||
if (magic != MAGIC) {
|
||||
throw new IllegalArgumentException("Wrong magic header in FIT file");
|
||||
}
|
||||
if (hasCRC) {
|
||||
int incomingCrc = garminByteBufferReader.readShort();
|
||||
|
||||
if (incomingCrc != ChecksumCalculator.computeCrc(garminByteBufferReader.asReadOnlyBuffer(), 0, headerSize - 2)) {
|
||||
throw new IllegalArgumentException("Wrong CRC for header in FIT file");
|
||||
}
|
||||
// LOG.info("Fit File Header didn't have CRC, no check performed.");
|
||||
}
|
||||
return new Header(hasCRC, protocolVersion, profileVersion, dataSize);
|
||||
}
|
||||
|
||||
public int getHeaderSize() {
|
||||
return headerSize;
|
||||
}
|
||||
|
||||
public int getDataSize() {
|
||||
return dataSize;
|
||||
}
|
||||
|
||||
public void setDataSize(int dataSize) {
|
||||
this.dataSize = dataSize;
|
||||
}
|
||||
|
||||
public void generateOutgoingDataPayload(MessageWriter writer) {
|
||||
writer.setByteOrder(ByteOrder.LITTLE_ENDIAN);
|
||||
writer.writeByte(headerSize);
|
||||
writer.writeByte(protocolVersion);
|
||||
writer.writeShort(profileVersion);
|
||||
writer.writeInt(dataSize);
|
||||
writer.writeInt(MAGIC);//magic
|
||||
if (hasCRC)
|
||||
writer.writeShort(ChecksumCalculator.computeCrc(writer.getBytes(), 0, writer.getBytes().length));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class GlobalFITMessage {
|
||||
public static GlobalFITMessage FILE_ID = new GlobalFITMessage(0, "FILE_ID", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.ENUM, "type", FieldDefinitionFactory.FIELD.FILE_TYPE),
|
||||
new FieldDefinitionPrimitive(1, BaseType.UINT16, "manufacturer"),
|
||||
new FieldDefinitionPrimitive(2, BaseType.UINT16, "product"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.UINT32Z, "serial_number"),
|
||||
new FieldDefinitionPrimitive(4, BaseType.UINT32, "time_created", FieldDefinitionFactory.FIELD.TIMESTAMP),
|
||||
new FieldDefinitionPrimitive(5, BaseType.UINT16, "number"),
|
||||
new FieldDefinitionPrimitive(6, BaseType.UINT16, "manufacturer_partner"),
|
||||
new FieldDefinitionPrimitive(8, BaseType.STRING, 20, "product_name")
|
||||
));
|
||||
public static GlobalFITMessage DEVICE_SETTINGS = new GlobalFITMessage(2, "DEVICE_SETTINGS", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.UINT8, "active_time_zone"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.UINT32, "utc_offset"),
|
||||
new FieldDefinitionPrimitive(2, BaseType.UINT32, "time_offset"),
|
||||
new FieldDefinitionPrimitive(4, BaseType.ENUM, "time_mode"),
|
||||
new FieldDefinitionPrimitive(5, BaseType.SINT8, "time_zone_offset"),
|
||||
new FieldDefinitionPrimitive(12, BaseType.ENUM, "backlight_mode"),
|
||||
new FieldDefinitionPrimitive(36, BaseType.ENUM, "activity_tracker_enabled"),
|
||||
new FieldDefinitionPrimitive(46, BaseType.ENUM, "move_alert_enabled"),
|
||||
new FieldDefinitionPrimitive(47, BaseType.ENUM, "date_mode"),
|
||||
new FieldDefinitionPrimitive(55, BaseType.ENUM, "display_orientation"),
|
||||
new FieldDefinitionPrimitive(56, BaseType.ENUM, "mounting_side"),
|
||||
new FieldDefinitionPrimitive(57, BaseType.UINT16, "default_page"),
|
||||
new FieldDefinitionPrimitive(58, BaseType.UINT16, "autosync_min_steps"),
|
||||
new FieldDefinitionPrimitive(59, BaseType.UINT16, "autosync_min_time"),
|
||||
new FieldDefinitionPrimitive(86, BaseType.ENUM, "ble_auto_upload_enabled"),
|
||||
new FieldDefinitionPrimitive(90, BaseType.UINT32, "auto_activity_detect")
|
||||
));
|
||||
public static GlobalFITMessage USER_PROFILE = new GlobalFITMessage(3, "USER_PROFILE", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.STRING, 8, "friendly_name"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.ENUM, "gender"),
|
||||
new FieldDefinitionPrimitive(2, BaseType.UINT8, "age"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.UINT8, "height"),
|
||||
new FieldDefinitionPrimitive(4, BaseType.UINT16, "weight", 10, 0),
|
||||
new FieldDefinitionPrimitive(5, BaseType.ENUM, "language", FieldDefinitionFactory.FIELD.LANGUAGE),
|
||||
new FieldDefinitionPrimitive(6, BaseType.ENUM, "elev_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM),
|
||||
new FieldDefinitionPrimitive(7, BaseType.ENUM, "weight_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM),
|
||||
new FieldDefinitionPrimitive(8, BaseType.UINT8, "resting_heart_rate"),
|
||||
new FieldDefinitionPrimitive(10, BaseType.UINT8, "default_max_biking_heart_rate"),
|
||||
new FieldDefinitionPrimitive(11, BaseType.UINT8, "default_max_heart_rate"),
|
||||
new FieldDefinitionPrimitive(12, BaseType.ENUM, "hr_setting"),
|
||||
new FieldDefinitionPrimitive(13, BaseType.ENUM, "speed_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM),
|
||||
new FieldDefinitionPrimitive(14, BaseType.ENUM, "dist_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM),
|
||||
new FieldDefinitionPrimitive(16, BaseType.ENUM, "power_setting"),
|
||||
new FieldDefinitionPrimitive(17, BaseType.ENUM, "activity_class"),
|
||||
new FieldDefinitionPrimitive(18, BaseType.ENUM, "position_setting"),
|
||||
new FieldDefinitionPrimitive(21, BaseType.ENUM, "temperature_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM),
|
||||
new FieldDefinitionPrimitive(28, BaseType.UINT32, "wake_time"),
|
||||
new FieldDefinitionPrimitive(29, BaseType.UINT32, "sleep_time"),
|
||||
new FieldDefinitionPrimitive(30, BaseType.ENUM, "height_setting", FieldDefinitionFactory.FIELD.MEASUREMENT_SYSTEM),
|
||||
new FieldDefinitionPrimitive(31, BaseType.UINT16, "user_running_step_length"),
|
||||
new FieldDefinitionPrimitive(32, BaseType.UINT16, "user_walking_step_length")
|
||||
));
|
||||
public static GlobalFITMessage ZONES_TARGET = new GlobalFITMessage(7, "ZONES_TARGET", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(3, BaseType.UINT16, "functional_threshold_power"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.UINT8, "max_heart_rate"),
|
||||
new FieldDefinitionPrimitive(2, BaseType.UINT8, "threshold_heart_rate"),
|
||||
new FieldDefinitionPrimitive(5, BaseType.ENUM, "hr_calc_type"), //1=percent_max_hr
|
||||
new FieldDefinitionPrimitive(7, BaseType.ENUM, "pwr_calc_type") //1=percent_ftp
|
||||
));
|
||||
public static GlobalFITMessage SPORT = new GlobalFITMessage(12, "SPORT", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.ENUM, "sport"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.ENUM, "sub_sport"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.STRING, 24, "name")
|
||||
));
|
||||
|
||||
public static GlobalFITMessage GOALS = new GlobalFITMessage(15, "GOALS", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(4, BaseType.ENUM, "type", FieldDefinitionFactory.FIELD.GOAL_TYPE),
|
||||
new FieldDefinitionPrimitive(7, BaseType.UINT32, "target_value"),
|
||||
new FieldDefinitionPrimitive(11, BaseType.ENUM, "source", FieldDefinitionFactory.FIELD.GOAL_SOURCE)
|
||||
));
|
||||
|
||||
public static GlobalFITMessage RECORD = new GlobalFITMessage(20, "RECORD", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(3, BaseType.UINT8, "heart_rate"),
|
||||
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
|
||||
));
|
||||
public static GlobalFITMessage FILE_CREATOR = new GlobalFITMessage(49, "FILE_CREATOR", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.UINT16, "software_version"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.UINT8, "hardware_version")
|
||||
));
|
||||
public static GlobalFITMessage CONNECTIVITY = new GlobalFITMessage(127, "CONNECTIVITY", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.ENUM, "bluetooth_enabled"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.STRING, 20, "name"),
|
||||
new FieldDefinitionPrimitive(4, BaseType.ENUM, "live_tracking_enabled"),
|
||||
new FieldDefinitionPrimitive(5, BaseType.ENUM, "weather_conditions_enabled"),
|
||||
new FieldDefinitionPrimitive(6, BaseType.ENUM, "weather_alerts_enabled"),
|
||||
new FieldDefinitionPrimitive(7, BaseType.ENUM, "auto_activity_upload_enabled"),
|
||||
new FieldDefinitionPrimitive(8, BaseType.ENUM, "course_download_enabled"),
|
||||
new FieldDefinitionPrimitive(9, BaseType.ENUM, "workout_download_enabled"),
|
||||
new FieldDefinitionPrimitive(10, BaseType.ENUM, "gps_ephemeris_download_enabled")
|
||||
));
|
||||
|
||||
public static GlobalFITMessage WEATHER = new GlobalFITMessage(128, "WEATHER", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.ENUM, "weather_report"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.SINT8, "temperature", FieldDefinitionFactory.FIELD.TEMPERATURE),
|
||||
new FieldDefinitionPrimitive(2, BaseType.ENUM, "condition", FieldDefinitionFactory.FIELD.WEATHER_CONDITION),
|
||||
new FieldDefinitionPrimitive(3, BaseType.UINT16, "wind_direction"),
|
||||
new FieldDefinitionPrimitive(4, BaseType.UINT16, "wind_speed", 298, 0),
|
||||
new FieldDefinitionPrimitive(5, BaseType.UINT8, "precipitation_probability"),
|
||||
new FieldDefinitionPrimitive(6, BaseType.SINT8, "temperature_feels_like", FieldDefinitionFactory.FIELD.TEMPERATURE),
|
||||
new FieldDefinitionPrimitive(7, BaseType.UINT8, "relative_humidity"),
|
||||
new FieldDefinitionPrimitive(8, BaseType.STRING, 15, "location"),
|
||||
new FieldDefinitionPrimitive(9, BaseType.UINT32, "observed_at_time", FieldDefinitionFactory.FIELD.TIMESTAMP),
|
||||
new FieldDefinitionPrimitive(10, BaseType.SINT32, "observed_location_lat"),
|
||||
new FieldDefinitionPrimitive(11, BaseType.SINT32, "observed_location_long"),
|
||||
new FieldDefinitionPrimitive(12, BaseType.ENUM, "day_of_week", FieldDefinitionFactory.FIELD.DAY_OF_WEEK),
|
||||
new FieldDefinitionPrimitive(13, BaseType.SINT8, "high_temperature", FieldDefinitionFactory.FIELD.TEMPERATURE),
|
||||
new FieldDefinitionPrimitive(14, BaseType.SINT8, "low_temperature", FieldDefinitionFactory.FIELD.TEMPERATURE),
|
||||
new FieldDefinitionPrimitive(15, BaseType.SINT8, "dew_point"),
|
||||
new FieldDefinitionPrimitive(16, BaseType.FLOAT32, "uv_index"),
|
||||
new FieldDefinitionPrimitive(17, BaseType.ENUM, "air_quality"),
|
||||
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
|
||||
));
|
||||
public static GlobalFITMessage WATCHFACE_SETTINGS = new GlobalFITMessage(159, "WATCHFACE_SETTINGS", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.ENUM, "mode"), //1=analog
|
||||
new FieldDefinitionPrimitive(1, BaseType.BASE_TYPE_BYTE, "layout")
|
||||
));
|
||||
|
||||
public static GlobalFITMessage FIELD_DESCRIPTION = new GlobalFITMessage(206, "FIELD_DESCRIPTION", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.UINT8, "developer_data_index"),
|
||||
new FieldDefinitionPrimitive(1, BaseType.UINT8, "field_definition_number"),
|
||||
new FieldDefinitionPrimitive(2, BaseType.UINT8, "fit_base_type_id"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.STRING, 64, "field_name"),
|
||||
new FieldDefinitionPrimitive(8, BaseType.STRING, 16, "units")
|
||||
));
|
||||
public static GlobalFITMessage DEVELOPER_DATA = new GlobalFITMessage(207, "DEVELOPER_DATA", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(1, BaseType.BASE_TYPE_BYTE, 16, "application_id"),
|
||||
new FieldDefinitionPrimitive(3, BaseType.UINT8, "developer_data_index")
|
||||
));
|
||||
// UNK_216(216, null), //activity
|
||||
public static GlobalFITMessage ALARM_SETTINGS = new GlobalFITMessage(222, "ALARM_SETTINGS", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(0, BaseType.UINT16, "time", FieldDefinitionFactory.FIELD.ALARM)
|
||||
));
|
||||
|
||||
public static Map<Integer, GlobalFITMessage> KNOWNMESSAGES = new HashMap<Integer, GlobalFITMessage>() {{
|
||||
put(0, FILE_ID);
|
||||
put(2, DEVICE_SETTINGS);
|
||||
put(3, USER_PROFILE);
|
||||
put(7, ZONES_TARGET);
|
||||
put(12, SPORT);
|
||||
put(15, GOALS);
|
||||
put(20, RECORD);
|
||||
put(49, FILE_CREATOR);
|
||||
put(127, CONNECTIVITY);
|
||||
put(128, WEATHER);
|
||||
put(159, WATCHFACE_SETTINGS);
|
||||
put(206, FIELD_DESCRIPTION);
|
||||
put(207, DEVELOPER_DATA);
|
||||
put(222, ALARM_SETTINGS);
|
||||
}};
|
||||
private final int number;
|
||||
private final String name;
|
||||
|
||||
private final List<FieldDefinitionPrimitive> fieldDefinitionPrimitives;
|
||||
|
||||
GlobalFITMessage(int number, String name, List<FieldDefinitionPrimitive> fieldDefinitionPrimitives) {
|
||||
this.number = number;
|
||||
this.name = name;
|
||||
this.fieldDefinitionPrimitives = fieldDefinitionPrimitives;
|
||||
}
|
||||
|
||||
public static GlobalFITMessage fromNumber(final int number) {
|
||||
final GlobalFITMessage found = KNOWNMESSAGES.get(number);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
return new GlobalFITMessage(number, "UNK_" + number, null);
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public int getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<FieldDefinition> getFieldDefinitions(int... ids) {
|
||||
if (null == fieldDefinitionPrimitives)
|
||||
return null;
|
||||
List<FieldDefinition> subset = new ArrayList<>(ids.length);
|
||||
for (int id :
|
||||
ids) {
|
||||
for (FieldDefinitionPrimitive fieldDefinitionPrimitive :
|
||||
fieldDefinitionPrimitives) {
|
||||
if (fieldDefinitionPrimitive.number == id) {
|
||||
subset.add(FieldDefinitionFactory.create(fieldDefinitionPrimitive.number, fieldDefinitionPrimitive.size, fieldDefinitionPrimitive.type, fieldDefinitionPrimitive.baseType, fieldDefinitionPrimitive.name, fieldDefinitionPrimitive.scale, fieldDefinitionPrimitive.offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
return subset;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public FieldDefinition getFieldDefinition(int id, int size) {
|
||||
if (null == fieldDefinitionPrimitives)
|
||||
return null;
|
||||
for (GlobalFITMessage.FieldDefinitionPrimitive fieldDefinitionPrimitive :
|
||||
fieldDefinitionPrimitives) {
|
||||
if (fieldDefinitionPrimitive.number == id) {
|
||||
return FieldDefinitionFactory.create(fieldDefinitionPrimitive.number, size, fieldDefinitionPrimitive.type, fieldDefinitionPrimitive.baseType, fieldDefinitionPrimitive.name, fieldDefinitionPrimitive.scale, fieldDefinitionPrimitive.offset);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static class FieldDefinitionPrimitive {
|
||||
private final int number;
|
||||
private final BaseType baseType;
|
||||
private final String name;
|
||||
private final FieldDefinitionFactory.FIELD type;
|
||||
private final int scale;
|
||||
private final int offset;
|
||||
private final int size;
|
||||
|
||||
public FieldDefinitionPrimitive(int number, BaseType baseType, int size, String name, FieldDefinitionFactory.FIELD type, int scale, int offset) {
|
||||
this.number = number;
|
||||
this.baseType = baseType;
|
||||
this.size = size;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.scale = scale;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public FieldDefinitionPrimitive(int number, BaseType baseType, String name, FieldDefinitionFactory.FIELD type) {
|
||||
this(number, baseType, baseType.getSize(), name, type, 1, 0);
|
||||
}
|
||||
|
||||
public FieldDefinitionPrimitive(int number, BaseType baseType, String name) {
|
||||
this(number, baseType, baseType.getSize(), name, null, 1, 0);
|
||||
}
|
||||
|
||||
public FieldDefinitionPrimitive(int number, BaseType baseType, int size, String name) {
|
||||
this(number, baseType, size, name, null, 1, 0);
|
||||
}
|
||||
|
||||
public FieldDefinitionPrimitive(int number, BaseType baseType, String name, int scale, int offset) {
|
||||
this(number, baseType, baseType.getSize(), name, null, scale, offset);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.List;
|
||||
|
||||
public enum PredefinedLocalMessage {
|
||||
TODAY_WEATHER_CONDITIONS(6, GlobalFITMessage.WEATHER,
|
||||
new int[]{0, 253, 9, 1, 14, 13, 2, 3, 5, 4, 6, 7, 10, 11, 8}
|
||||
),
|
||||
HOURLY_WEATHER_FORECAST(9, GlobalFITMessage.WEATHER,
|
||||
new int[]{0, 253, 1, 2, 3, 4, 5, 7, 15, 16, 17}
|
||||
),
|
||||
DAILY_WEATHER_FORECAST(10, GlobalFITMessage.WEATHER,
|
||||
new int[]{0, 253, 14, 13, 2, 5, 12}
|
||||
);
|
||||
|
||||
private final int type;
|
||||
private final GlobalFITMessage globalFITMessage;
|
||||
private final int[] globalDefinitionIds;
|
||||
|
||||
PredefinedLocalMessage(int type, GlobalFITMessage globalFITMessage, int[] globalDefinitionIds) {
|
||||
this.type = type;
|
||||
this.globalFITMessage = globalFITMessage;
|
||||
this.globalDefinitionIds = globalDefinitionIds;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PredefinedLocalMessage fromType(int type) {
|
||||
for (final PredefinedLocalMessage predefinedLocalMessage : PredefinedLocalMessage.values()) {
|
||||
if (predefinedLocalMessage.getType() == type) {
|
||||
return predefinedLocalMessage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<FieldDefinition> getLocalFieldDefinitions() {
|
||||
return globalFITMessage.getFieldDefinitions(globalDefinitionIds);
|
||||
}
|
||||
|
||||
public RecordDefinition getRecordDefinition() {
|
||||
return new RecordDefinition(ByteOrder.BIG_ENDIAN, this);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public GlobalFITMessage getGlobalFITMessage() {
|
||||
return globalFITMessage;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionTimestamp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType.STRING;
|
||||
|
||||
public class RecordData {
|
||||
|
||||
private final RecordHeader recordHeader;
|
||||
private final GlobalFITMessage globalFITMessage;
|
||||
private final List<FieldData> fieldDataList;
|
||||
protected ByteBuffer valueHolder;
|
||||
|
||||
public RecordData(RecordDefinition recordDefinition, RecordHeader recordHeader) {
|
||||
if (null == recordDefinition.getFieldDefinitions())
|
||||
throw new IllegalArgumentException("Cannot create record data without FieldDefinitions " + recordDefinition);
|
||||
|
||||
fieldDataList = new ArrayList<>();
|
||||
|
||||
this.recordHeader = recordHeader;
|
||||
this.globalFITMessage = recordDefinition.getGlobalFITMessage();
|
||||
|
||||
int totalSize = 0;
|
||||
|
||||
for (FieldDefinition fieldDef :
|
||||
recordDefinition.getFieldDefinitions()) {
|
||||
fieldDataList.add(new FieldData(fieldDef, totalSize));
|
||||
totalSize += fieldDef.getSize();
|
||||
}
|
||||
|
||||
if (recordDefinition.getDevFieldDefinitions() != null) {
|
||||
for (DevFieldDefinition fieldDef :
|
||||
recordDefinition.getDevFieldDefinitions()) {
|
||||
FieldDefinition temp = new FieldDefinition(fieldDef.getFieldDefinitionNumber(), fieldDef.getSize(), fieldDef.getBaseType(), fieldDef.getName());
|
||||
fieldDataList.add(new FieldData(temp, totalSize));
|
||||
totalSize += fieldDef.getSize();
|
||||
}
|
||||
}
|
||||
|
||||
this.valueHolder = ByteBuffer.allocate(totalSize);
|
||||
valueHolder.order(recordDefinition.getByteOrder());
|
||||
|
||||
for (FieldData fieldData :
|
||||
fieldDataList) {
|
||||
fieldData.invalidate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public RecordData(RecordDefinition recordDefinition) {
|
||||
this(recordDefinition, recordDefinition.getRecordHeader());
|
||||
}
|
||||
|
||||
public GlobalFITMessage getGlobalFITMessage() {
|
||||
return globalFITMessage;
|
||||
}
|
||||
|
||||
public Long parseDataMessage(GarminByteBufferReader garminByteBufferReader) {
|
||||
garminByteBufferReader.setByteOrder(valueHolder.order());
|
||||
Long referenceTimestamp = null;
|
||||
for (FieldData fieldData : fieldDataList) {
|
||||
Long runningTimestamp = fieldData.parseDataMessage(garminByteBufferReader);
|
||||
if (runningTimestamp != null)
|
||||
referenceTimestamp = runningTimestamp;
|
||||
}
|
||||
return referenceTimestamp;
|
||||
}
|
||||
|
||||
public void generateOutgoingDataPayload(MessageWriter writer) {
|
||||
writer.writeByte(recordHeader.generateOutgoingDataPayload());
|
||||
writer.writeBytes(valueHolder.array());
|
||||
}
|
||||
|
||||
public void setFieldByNumber(int number, Object... value) {
|
||||
boolean found = false;
|
||||
for (FieldData fieldData :
|
||||
fieldDataList) {
|
||||
if (fieldData.getNumber() == number) {
|
||||
fieldData.encode(value);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw new IllegalArgumentException("Unknown field number " + number);
|
||||
}
|
||||
}
|
||||
|
||||
public void setFieldByName(String name, Object... value) {
|
||||
boolean found = false;
|
||||
for (FieldData fieldData :
|
||||
fieldDataList) {
|
||||
if (fieldData.getName().equals(name)) {
|
||||
fieldData.encode(value);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw new IllegalArgumentException("Unknown field name " + name);
|
||||
}
|
||||
}
|
||||
|
||||
public Object getFieldByNumber(int number) {
|
||||
for (FieldData fieldData :
|
||||
fieldDataList) {
|
||||
if (fieldData.getNumber() == number) {
|
||||
return fieldData.decode();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown field number " + number);
|
||||
}
|
||||
|
||||
public Object getFieldByName(String name) {
|
||||
for (FieldData fieldData :
|
||||
fieldDataList) {
|
||||
if (fieldData.getName().equals(name)) {
|
||||
return fieldData.decode();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown field name " + name);
|
||||
}
|
||||
|
||||
public int[] getFieldsNumbers() {
|
||||
int[] arr = new int[fieldDataList.size()];
|
||||
int count = 0;
|
||||
for (FieldData fieldData : fieldDataList) {
|
||||
int number = fieldData.getNumber();
|
||||
arr[count++] = number;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
public Long getComputedTimestamp() {
|
||||
for (FieldData fieldData : fieldDataList) {
|
||||
if (fieldData.getNumber() == 253 || fieldData.fieldDefinition instanceof FieldDefinitionTimestamp)
|
||||
return (long) fieldData.decode();
|
||||
}
|
||||
if (recordHeader.isCompressedTimestamp())
|
||||
return (long) recordHeader.getResultingTimestamp();
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String toString() {
|
||||
StringBuilder oBuilder = new StringBuilder();
|
||||
oBuilder.append(System.lineSeparator());
|
||||
for (FieldData fieldData :
|
||||
fieldDataList) {
|
||||
if (fieldData.getName() != null && !fieldData.getName().equals("")) {
|
||||
oBuilder.append(fieldData.getName());
|
||||
} else {
|
||||
oBuilder.append("unknown_" + fieldData.getNumber());
|
||||
}
|
||||
oBuilder.append(fieldData);
|
||||
oBuilder.append(": ");
|
||||
Object o = fieldData.decode();
|
||||
if (o instanceof Object[]) {
|
||||
oBuilder.append("[");
|
||||
oBuilder.append(org.apache.commons.lang3.StringUtils.join((Object[]) o, ","));
|
||||
oBuilder.append("]");
|
||||
} else {
|
||||
oBuilder.append(o);
|
||||
}
|
||||
oBuilder.append(" ");
|
||||
}
|
||||
if (recordHeader.isCompressedTimestamp())
|
||||
oBuilder.append("compressed_timestamp: " + getComputedTimestamp());
|
||||
return oBuilder.toString();
|
||||
}
|
||||
|
||||
public PredefinedLocalMessage getPredefinedLocalMessage() {
|
||||
return recordHeader.getPredefinedLocalMessage();
|
||||
}
|
||||
|
||||
private class FieldData {
|
||||
private final FieldDefinition fieldDefinition;
|
||||
private final int position;
|
||||
private final int size;
|
||||
private final int baseSize;
|
||||
|
||||
public FieldData(FieldDefinition fieldDefinition, int position) {
|
||||
this.fieldDefinition = fieldDefinition;
|
||||
this.position = position;
|
||||
this.size = fieldDefinition.getSize();
|
||||
this.baseSize = fieldDefinition.getBaseType().getSize();
|
||||
}
|
||||
|
||||
private String getName() {
|
||||
return fieldDefinition.getName();
|
||||
}
|
||||
|
||||
private int getNumber() {
|
||||
return fieldDefinition.getNumber();
|
||||
}
|
||||
|
||||
private void invalidate() {
|
||||
goToPosition();
|
||||
if (STRING.equals(fieldDefinition.getBaseType())) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
valueHolder.put((byte) 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < (size / baseSize); i++) {
|
||||
fieldDefinition.invalidate(valueHolder);
|
||||
}
|
||||
}
|
||||
|
||||
private void goToPosition() {
|
||||
valueHolder.position(position);
|
||||
}
|
||||
|
||||
private Long parseDataMessage(GarminByteBufferReader garminByteBufferReader) {
|
||||
goToPosition();
|
||||
valueHolder.put(garminByteBufferReader.readBytes(size));
|
||||
if (fieldDefinition instanceof FieldDefinitionTimestamp)
|
||||
return (Long) decode();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void encode(Object... objects) {
|
||||
if (objects[0] instanceof boolean[] || objects[0] instanceof short[] || objects[0] instanceof int[] || objects[0] instanceof long[] || objects[0] instanceof float[] || objects[0] instanceof double[]) {
|
||||
throw new IllegalArgumentException("Array of primitive types not supported, box them to objects");
|
||||
}
|
||||
goToPosition();
|
||||
final int slots = size / baseSize;
|
||||
int i = 0;
|
||||
for (Object o : objects) {
|
||||
if (i++ >= slots) {
|
||||
throw new IllegalArgumentException("Number of elements in array was too big for the field");
|
||||
}
|
||||
if (STRING.equals(fieldDefinition.getBaseType())) {
|
||||
final byte[] bytes = ((String) o).getBytes(StandardCharsets.UTF_8);
|
||||
valueHolder.put(Arrays.copyOf(bytes, Math.min(this.size - 1, bytes.length)));
|
||||
valueHolder.put((byte) 0);
|
||||
return;
|
||||
}
|
||||
fieldDefinition.encode(valueHolder, o);
|
||||
}
|
||||
}
|
||||
|
||||
private Object decode() {
|
||||
goToPosition();
|
||||
if (STRING.equals(fieldDefinition.getBaseType())) {
|
||||
final byte[] bytes = new byte[size];
|
||||
valueHolder.get(bytes);
|
||||
final int zero = ArrayUtils.indexOf((byte) 0, bytes);
|
||||
if (zero < 0) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
return new String(bytes, 0, zero, StandardCharsets.UTF_8);
|
||||
}
|
||||
if (size > baseSize) {
|
||||
Object[] arr = new Object[size / baseSize];
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
arr[i] = fieldDefinition.decode(valueHolder);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
return fieldDefinition.decode(valueHolder);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "(" + fieldDefinition.getBaseType().name() + "/" + size + ")";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
|
||||
public class RecordDefinition {
|
||||
private final RecordHeader recordHeader;
|
||||
private final GlobalFITMessage globalFITMessage;
|
||||
private final PredefinedLocalMessage predefinedLocalMessage;
|
||||
private final java.nio.ByteOrder byteOrder;
|
||||
private List<FieldDefinition> fieldDefinitions;
|
||||
private List<DevFieldDefinition> devFieldDefinitions;
|
||||
|
||||
public RecordDefinition(RecordHeader recordHeader, ByteOrder byteOrder, PredefinedLocalMessage predefinedLocalMessage, GlobalFITMessage globalFITMessage, List<FieldDefinition> fieldDefinitions, List<DevFieldDefinition> devFieldDefinitions) {
|
||||
this.recordHeader = recordHeader;
|
||||
this.byteOrder = byteOrder;
|
||||
this.predefinedLocalMessage = predefinedLocalMessage;
|
||||
this.globalFITMessage = globalFITMessage;
|
||||
this.fieldDefinitions = fieldDefinitions;
|
||||
this.devFieldDefinitions = devFieldDefinitions;
|
||||
}
|
||||
|
||||
public RecordDefinition(ByteOrder byteOrder, PredefinedLocalMessage predefinedLocalMessage) {
|
||||
this(new RecordHeader(true, false, predefinedLocalMessage, null), byteOrder, predefinedLocalMessage, predefinedLocalMessage.getGlobalFITMessage(), predefinedLocalMessage.getLocalFieldDefinitions(), null);
|
||||
}
|
||||
|
||||
public RecordDefinition(ByteOrder byteOrder, RecordHeader recordHeader, GlobalFITMessage globalFITMessage, List<FieldDefinition> fieldDefinitions) {
|
||||
this(recordHeader, byteOrder, null, globalFITMessage, fieldDefinitions, null);
|
||||
}
|
||||
|
||||
public static RecordDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader, RecordHeader recordHeader) {
|
||||
if (!recordHeader.isDefinition())
|
||||
return null;
|
||||
garminByteBufferReader.readByte();//ignore
|
||||
ByteOrder byteOrder = garminByteBufferReader.readByte() == 0x01 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
|
||||
garminByteBufferReader.setByteOrder(byteOrder);
|
||||
final int globalMesgNum = garminByteBufferReader.readShort();
|
||||
final GlobalFITMessage globalFITMessage = GlobalFITMessage.fromNumber(globalMesgNum);
|
||||
|
||||
RecordDefinition definitionMessage = new RecordDefinition(byteOrder, recordHeader, globalFITMessage, null);
|
||||
|
||||
final int numFields = garminByteBufferReader.readByte();
|
||||
List<FieldDefinition> fieldDefinitions = new ArrayList<>(numFields);
|
||||
|
||||
for (int i = 0; i < numFields; i++) {
|
||||
fieldDefinitions.add(FieldDefinition.parseIncoming(garminByteBufferReader, globalFITMessage));
|
||||
}
|
||||
|
||||
definitionMessage.setFieldDefinitions(fieldDefinitions);
|
||||
|
||||
if (recordHeader.isDeveloperData()) {
|
||||
final int numDevFields = garminByteBufferReader.readByte();
|
||||
List<DevFieldDefinition> devFieldDefinitions = new ArrayList<>(numDevFields);
|
||||
for (int i = 0; i < numDevFields; i++) {
|
||||
devFieldDefinitions.add(DevFieldDefinition.parseIncoming(garminByteBufferReader));
|
||||
}
|
||||
definitionMessage.setDevFieldDefinitions(devFieldDefinitions);
|
||||
}
|
||||
|
||||
return definitionMessage;
|
||||
}
|
||||
|
||||
public GlobalFITMessage getGlobalFITMessage() {
|
||||
return globalFITMessage;
|
||||
}
|
||||
|
||||
|
||||
public ByteOrder getByteOrder() {
|
||||
return byteOrder;
|
||||
}
|
||||
|
||||
public List<DevFieldDefinition> getDevFieldDefinitions() {
|
||||
return devFieldDefinitions;
|
||||
}
|
||||
|
||||
public void setDevFieldDefinitions(List<DevFieldDefinition> devFieldDefinitions) {
|
||||
this.devFieldDefinitions = devFieldDefinitions;
|
||||
}
|
||||
|
||||
public RecordHeader getRecordHeader() {
|
||||
return recordHeader;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<FieldDefinition> getFieldDefinitions() {
|
||||
return fieldDefinitions;
|
||||
}
|
||||
|
||||
public void setFieldDefinitions(List<FieldDefinition> fieldDefinitions) {
|
||||
this.fieldDefinitions = fieldDefinitions;
|
||||
}
|
||||
|
||||
public void generateOutgoingPayload(MessageWriter writer) {
|
||||
writer.writeByte(recordHeader.generateOutgoingDefinitionPayload());
|
||||
writer.writeByte(0);//ignore
|
||||
writer.writeByte(byteOrder == ByteOrder.LITTLE_ENDIAN ? 0 : 1);
|
||||
writer.setByteOrder(byteOrder);
|
||||
writer.writeShort(globalFITMessage.getNumber());
|
||||
|
||||
if (fieldDefinitions != null) {
|
||||
writer.writeByte(fieldDefinitions.size());
|
||||
for (FieldDefinition fieldDefinition : fieldDefinitions) {
|
||||
fieldDefinition.generateOutgoingPayload(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return predefinedLocalMessage != null ? predefinedLocalMessage.name() : "unknown_" + globalFITMessage;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return System.lineSeparator() + recordHeader.toString() +
|
||||
" Global Message Number: " + globalFITMessage.name();
|
||||
}
|
||||
|
||||
public void populateDevFields(List<RecordData> developerFieldData) {
|
||||
for (DevFieldDefinition devFieldDef :
|
||||
getDevFieldDefinitions()) {
|
||||
for (RecordData recordData :
|
||||
developerFieldData) {
|
||||
try {
|
||||
if (devFieldDef.getFieldDefinitionNumber() == (int) recordData.getFieldByName("field_definition_number") &&
|
||||
devFieldDef.getDeveloperDataIndex() == (int) recordData.getFieldByName("developer_data_index")) {
|
||||
BaseType baseType = BaseType.fromIdentifier((int) recordData.getFieldByName("fit_base_type_id"));
|
||||
devFieldDef.setBaseType(baseType);
|
||||
devFieldDef.setName((String) recordData.getFieldByName("field_name"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
||||
public class RecordHeader {
|
||||
private final boolean definition;
|
||||
private final boolean developerData;
|
||||
private final PredefinedLocalMessage predefinedLocalMessage;
|
||||
private final int rawLocalMessageType;
|
||||
private final Integer timeOffset;
|
||||
private long referenceTimestamp;
|
||||
|
||||
public RecordHeader(boolean definition, boolean developerData, PredefinedLocalMessage predefinedLocalMessage, Integer timeOffset) {
|
||||
this.definition = definition;
|
||||
this.developerData = developerData;
|
||||
this.predefinedLocalMessage = predefinedLocalMessage;
|
||||
this.rawLocalMessageType = predefinedLocalMessage.getType();
|
||||
this.timeOffset = timeOffset;
|
||||
}
|
||||
|
||||
//see https://github.com/polyvertex/fitdecode/blob/master/fitdecode/reader.py#L512
|
||||
public RecordHeader(byte header) {
|
||||
this(header, false);
|
||||
}
|
||||
|
||||
public RecordHeader(byte header, boolean inferLocalMessage) {
|
||||
if ((header & 0x80) == 0x80) { //compressed timestamp TODO add support
|
||||
definition = false;
|
||||
developerData = false;
|
||||
rawLocalMessageType = (header >> 5) & 0x3;
|
||||
timeOffset = header & 0x1f;
|
||||
} else {
|
||||
definition = ((header & 0x40) == 0x40);
|
||||
developerData = ((header & 0x20) == 0x20);
|
||||
rawLocalMessageType = header & 0xf;
|
||||
timeOffset = null;
|
||||
}
|
||||
if (inferLocalMessage)
|
||||
predefinedLocalMessage = PredefinedLocalMessage.fromType(rawLocalMessageType);
|
||||
else
|
||||
predefinedLocalMessage = null;
|
||||
}
|
||||
|
||||
|
||||
public void setReferenceTimestamp(long referenceTimestamp) {
|
||||
this.referenceTimestamp = referenceTimestamp;
|
||||
}
|
||||
|
||||
public Integer getTimeOffset() {
|
||||
return timeOffset;
|
||||
}
|
||||
|
||||
public boolean isCompressedTimestamp() {
|
||||
return timeOffset != null;
|
||||
}
|
||||
|
||||
public Long getResultingTimestamp() {
|
||||
return referenceTimestamp + timeOffset;
|
||||
}
|
||||
|
||||
public boolean isDeveloperData() {
|
||||
return developerData;
|
||||
}
|
||||
|
||||
public boolean isDefinition() {
|
||||
return definition;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PredefinedLocalMessage getPredefinedLocalMessage() {
|
||||
return predefinedLocalMessage;
|
||||
}
|
||||
|
||||
public byte generateOutgoingDefinitionPayload() {
|
||||
if (!definition && !developerData)
|
||||
return (byte) (timeOffset | (((byte) predefinedLocalMessage.getType()) << 5));
|
||||
byte base = (byte) (null == predefinedLocalMessage ? rawLocalMessageType : predefinedLocalMessage.getType());
|
||||
if (definition)
|
||||
base = (byte) (base | 0x40);
|
||||
if (developerData)
|
||||
base = (byte) (base | 0x20);
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
public byte generateOutgoingDataPayload() { //TODO: unclear if correct
|
||||
if (!definition && !developerData)
|
||||
return (byte) (timeOffset | (((byte) predefinedLocalMessage.getType()) << 5));
|
||||
byte base = (byte) (null == predefinedLocalMessage ? rawLocalMessageType : predefinedLocalMessage.getType());
|
||||
if (developerData)
|
||||
base = (byte) (base | 0x20);
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Local Message: " + (null == predefinedLocalMessage ? "raw: " + rawLocalMessageType : "type: " + predefinedLocalMessage.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
RecordHeader that = (RecordHeader) o;
|
||||
|
||||
if (rawLocalMessageType != that.rawLocalMessageType) return false;
|
||||
return predefinedLocalMessage == that.predefinedLocalMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (predefinedLocalMessage != null ? predefinedLocalMessage.hashCode() : 0);
|
||||
result = 31 * result + rawLocalMessageType;
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
//see https://github.com/dtcooper/python-fitparse/blob/master/fitparse/records.py
|
||||
public enum BaseType {
|
||||
ENUM(0x00, new BaseTypeByte(true, 0xFF)),
|
||||
SINT8(0x01, new BaseTypeByte(false, 0x7F)),
|
||||
UINT8(0x02, new BaseTypeByte(true, 0xFF)),
|
||||
SINT16(0x83, new BaseTypeShort(false, 0x7FFF)),
|
||||
UINT16(0x84, new BaseTypeShort(true, 0xFFFF)),
|
||||
SINT32(0x85, new BaseTypeInt(false, 0x7FFFFFFF)),
|
||||
UINT32(0x86, new BaseTypeInt(true, 0xFFFFFFFFL)),
|
||||
STRING(0x07, new BaseTypeByte(true, 0x00)),
|
||||
FLOAT32(0x88, new BaseTypeFloat()),
|
||||
FLOAT64(0x89, new BaseTypeDouble()),
|
||||
UINT8Z(0x0A, new BaseTypeByte(true, 0x00)),
|
||||
UINT16Z(0x8B, new BaseTypeShort(true, 0)),
|
||||
UINT32Z(0x8C, new BaseTypeInt(true, 0)),
|
||||
BASE_TYPE_BYTE(0x0D, new BaseTypeByte(true, 0xFF)),
|
||||
SINT64(0x8E, new BaseTypeLong(false, 0x7FFFFFFFFFFFFFFFL)),
|
||||
UINT64(0x8F, new BaseTypeLong(true, 0xFFFFFFFFFFFFFFFFL)),
|
||||
UINT64Z(0x8F, new BaseTypeLong(true, 0)),
|
||||
;
|
||||
|
||||
private final int identifier;
|
||||
private final BaseTypeInterface baseTypeInterface;
|
||||
|
||||
BaseType(int identifier, BaseTypeInterface byteBaseType) {
|
||||
this.identifier = identifier;
|
||||
this.baseTypeInterface = byteBaseType;
|
||||
}
|
||||
|
||||
public static BaseType fromIdentifier(int identifier) {
|
||||
for (final BaseType baseType : BaseType.values()) {
|
||||
if (baseType.getIdentifier() == identifier) {
|
||||
return baseType;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown type " + identifier);
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return baseTypeInterface.getByteSize();
|
||||
}
|
||||
|
||||
public int getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
public Object decode(ByteBuffer byteBuffer, int scale, int offset) {
|
||||
return baseTypeInterface.decode(byteBuffer, scale, offset);
|
||||
}
|
||||
|
||||
public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) {
|
||||
baseTypeInterface.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
baseTypeInterface.invalidate(byteBuffer);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BaseTypeByte implements BaseTypeInterface {
|
||||
|
||||
private final int min;
|
||||
private final int max;
|
||||
private final int invalid;
|
||||
private final boolean unsigned;
|
||||
private final int size = 1;
|
||||
|
||||
BaseTypeByte(boolean unsigned, int invalid) {
|
||||
if (unsigned) {
|
||||
min = 0;
|
||||
max = 0xff;
|
||||
} else {
|
||||
min = Byte.MIN_VALUE;
|
||||
max = Byte.MAX_VALUE;
|
||||
}
|
||||
this.invalid = invalid;
|
||||
this.unsigned = unsigned;
|
||||
}
|
||||
|
||||
|
||||
public int getByteSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final ByteBuffer byteBuffer, int scale, int offset) {
|
||||
int b = unsigned ? Byte.toUnsignedInt(byteBuffer.get()) : byteBuffer.get();
|
||||
if (b < min || b > max)
|
||||
return null;
|
||||
if (b == invalid)
|
||||
return null;
|
||||
return (b + offset) / scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) {
|
||||
if (null == o) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
int i = ((Number) o).intValue() * scale - offset;
|
||||
if (i < min || i > max) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
byteBuffer.put((byte) i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
byteBuffer.put((byte) invalid);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BaseTypeDouble implements BaseTypeInterface {
|
||||
private final int size = 8;
|
||||
private final double min;
|
||||
private final double max;
|
||||
private final double invalid;
|
||||
|
||||
BaseTypeDouble() {
|
||||
this.min = -Double.MAX_VALUE;
|
||||
this.max = Double.MAX_VALUE;
|
||||
this.invalid = Double.longBitsToDouble(0xFFFFFFFFFFFFFFFFL);
|
||||
}
|
||||
|
||||
public int getByteSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final ByteBuffer byteBuffer, int scale, int offset) {
|
||||
double d = byteBuffer.getDouble();
|
||||
if (d < min || d > max) {
|
||||
return null;
|
||||
}
|
||||
if (Double.isNaN(d) || d == invalid)
|
||||
return null;
|
||||
return (d + offset) / scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) {
|
||||
if (null == o) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
double d = ((Number) o).doubleValue() * scale - offset;
|
||||
if (d < min || d > max) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
byteBuffer.putDouble(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
byteBuffer.putDouble(invalid);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BaseTypeFloat implements BaseTypeInterface {
|
||||
private final int size = 4;
|
||||
private final double min;
|
||||
private final double max;
|
||||
private final double invalid;
|
||||
|
||||
BaseTypeFloat() {
|
||||
this.min = -Float.MAX_VALUE;
|
||||
this.max = Float.MAX_VALUE;
|
||||
this.invalid = Float.intBitsToFloat(0xFFFFFFFF);
|
||||
}
|
||||
|
||||
public int getByteSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer, int scale, int offset) {
|
||||
float f = byteBuffer.getFloat();
|
||||
if (f < min || f > max) {
|
||||
return null;
|
||||
}
|
||||
if (Float.isNaN(f) || f == invalid)
|
||||
return null;
|
||||
return (f + offset) / scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) {
|
||||
if (null == o) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
float f = ((Number) o).floatValue() * scale - offset;
|
||||
if (f < min || f > max) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
byteBuffer.putFloat((float) f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
byteBuffer.putFloat((float) invalid);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BaseTypeInt implements BaseTypeInterface {
|
||||
private final long min;
|
||||
private final long max;
|
||||
private final long invalid;
|
||||
private final boolean unsigned;
|
||||
private final int size = 4;
|
||||
|
||||
BaseTypeInt(boolean unsigned, long invalid) {
|
||||
if (unsigned) {
|
||||
this.min = 0;
|
||||
this.max = 0xffffffffL;
|
||||
} else {
|
||||
this.min = Integer.MIN_VALUE;
|
||||
this.max = Integer.MAX_VALUE;
|
||||
}
|
||||
this.invalid = invalid;
|
||||
this.unsigned = unsigned;
|
||||
}
|
||||
|
||||
public int getByteSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final ByteBuffer byteBuffer, int scale, int offset) {
|
||||
long i = unsigned ? Integer.toUnsignedLong(byteBuffer.getInt()) : byteBuffer.getInt();
|
||||
if (i < min || i > max)
|
||||
return null;
|
||||
if (i == invalid)
|
||||
return null;
|
||||
return ((i + offset) / scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) {
|
||||
if (null == o) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
long l = ((Number) o).longValue() * scale - offset;
|
||||
if (l < min || l > max) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
byteBuffer.putInt((int) l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
byteBuffer.putInt((int) invalid);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public interface BaseTypeInterface {
|
||||
int getByteSize();
|
||||
|
||||
Object decode(ByteBuffer byteBuffer, int scale, int offset);
|
||||
|
||||
void encode(ByteBuffer byteBuffer, Object o, int scale, int offset);
|
||||
|
||||
void invalidate(ByteBuffer byteBuffer);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BaseTypeLong implements BaseTypeInterface {
|
||||
private final int size = 8;
|
||||
private final BigInteger min;
|
||||
private final BigInteger max;
|
||||
private final long invalid;
|
||||
private final boolean unsigned;
|
||||
|
||||
BaseTypeLong(boolean unsigned, long invalid) {
|
||||
if (unsigned) {
|
||||
this.min = BigInteger.valueOf(0);
|
||||
this.max = BigInteger.valueOf(0xFFFFFFFFFFFFFFFFL);
|
||||
} else {
|
||||
this.min = BigInteger.valueOf(Long.MIN_VALUE);
|
||||
this.max = BigInteger.valueOf(Long.MAX_VALUE);
|
||||
}
|
||||
this.invalid = invalid;
|
||||
this.unsigned = unsigned;
|
||||
}
|
||||
|
||||
public int getByteSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer, int scale, int offset) {
|
||||
BigInteger i = unsigned ? BigInteger.valueOf(byteBuffer.getLong() & 0xFFFFFFFFFFFFFFFFL) : BigInteger.valueOf(byteBuffer.getLong());
|
||||
if (!unsigned && (i.compareTo(min) < 0 || i.compareTo(max) > 0))
|
||||
return null;
|
||||
if (i.compareTo(BigInteger.valueOf(invalid)) == 0)
|
||||
return null;
|
||||
return (i.longValue() + offset) / scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) {
|
||||
if (null == o) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
BigInteger i = BigInteger.valueOf(((Number) o).longValue() * scale - offset);
|
||||
if (!unsigned && (i.compareTo(min) < 0 || i.compareTo(max) > 0)) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
byteBuffer.putLong(i.longValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
byteBuffer.putLong((long) invalid);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BaseTypeShort implements BaseTypeInterface {
|
||||
private final int min;
|
||||
private final int max;
|
||||
private final int invalid;
|
||||
private final boolean unsigned;
|
||||
private final int size = 2;
|
||||
|
||||
BaseTypeShort(boolean unsigned, int invalid) {
|
||||
if (unsigned) {
|
||||
this.min = 0;
|
||||
this.max = 0xffff;
|
||||
} else {
|
||||
this.min = Short.MIN_VALUE;
|
||||
this.max = Short.MAX_VALUE;
|
||||
}
|
||||
this.invalid = invalid;
|
||||
this.unsigned = unsigned;
|
||||
}
|
||||
|
||||
public int getByteSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final ByteBuffer byteBuffer, int scale, int offset) {
|
||||
int s = unsigned ? Short.toUnsignedInt(byteBuffer.getShort()) : byteBuffer.getShort();
|
||||
if (s < min || s > max)
|
||||
return null;
|
||||
if (s == invalid)
|
||||
return null;
|
||||
return (s + offset) / scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o, int scale, int offset) {
|
||||
if (null == o) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
int i = ((Number) o).intValue() * scale - offset;
|
||||
if (i < min || i > max) {
|
||||
invalidate(byteBuffer);
|
||||
return;
|
||||
}
|
||||
byteBuffer.putShort((short) i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(ByteBuffer byteBuffer) {
|
||||
byteBuffer.putShort((short) invalid);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Calendar;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionAlarm extends FieldDefinition {
|
||||
public FieldDefinitionAlarm(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int raw = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.HOUR_OF_DAY, Math.round(raw / 60));
|
||||
calendar.set(Calendar.MINUTE, raw % 60);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof Calendar) {
|
||||
baseType.encode(byteBuffer, ((Calendar) o).get(Calendar.HOUR_OF_DAY) * 60 + ((Calendar) o).get(Calendar.MINUTE), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import org.threeten.bp.DayOfWeek;
|
||||
import org.threeten.bp.Instant;
|
||||
import org.threeten.bp.ZoneId;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionDayOfWeek extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionDayOfWeek(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int raw = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
return DayOfWeek.of(raw == 0 ? 7 : raw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof DayOfWeek) {
|
||||
baseType.encode(byteBuffer, (((DayOfWeek) o).getValue() % 7), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, (Instant.ofEpochSecond((int) o).atZone(ZoneId.systemDefault()).getDayOfWeek().getValue() % 7), scale, offset);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionFileType extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionFileType(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int raw = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
return Type.fromId(raw) == null ? raw : Type.fromId(raw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof Type) {
|
||||
baseType.encode(byteBuffer, (((Type) o).getId()), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
settings(2),
|
||||
activity(4), //FIT_TYPE_4 stands for activity directory
|
||||
goals(11),
|
||||
monitor(32), //FIT_TYPE_32
|
||||
changelog(41), // FIT_TYPE_41 stands for changelog directory
|
||||
metrics(44), //FIT_TYPE_41
|
||||
sleep(49), //FIT_TYPE_49
|
||||
;
|
||||
|
||||
private final int id;
|
||||
|
||||
Type(int i) {
|
||||
this.id = i;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Type fromId(int id) {
|
||||
for (Type type :
|
||||
Type.values()) {
|
||||
if (id == type.getId()) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionGoalSource extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionGoalSource(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int raw = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
return Source.fromId(raw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof Source) {
|
||||
baseType.encode(byteBuffer, (((Source) o).ordinal()), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
|
||||
public enum Source {
|
||||
auto,
|
||||
community,
|
||||
manual,
|
||||
;
|
||||
|
||||
@Nullable
|
||||
public static Source fromId(int id) {
|
||||
for (Source source :
|
||||
Source.values()) {
|
||||
if (id == source.ordinal()) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionGoalType extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionGoalType(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int raw = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
return Type.fromId(raw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof Type) {
|
||||
baseType.encode(byteBuffer, (((Type) o).getId()), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
steps(4),
|
||||
;
|
||||
|
||||
private final int id;
|
||||
|
||||
Type(int i) {
|
||||
id = i;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Type fromId(int id) {
|
||||
for (Type type :
|
||||
Type.values()) {
|
||||
if (id == type.getId()) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionLanguage extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionLanguage(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int raw = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
return Language.fromId(raw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof Language) {
|
||||
baseType.encode(byteBuffer, (((Language) o).getId()), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
|
||||
private enum Language {
|
||||
english(0),
|
||||
italian(2),
|
||||
;
|
||||
|
||||
private final int id;
|
||||
|
||||
Language(int i) {
|
||||
id = i;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Language fromId(int id) {
|
||||
for (Language language :
|
||||
Language.values()) {
|
||||
if (id == language.getId()) {
|
||||
return language;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionMeasurementSystem extends FieldDefinition {
|
||||
|
||||
|
||||
public FieldDefinitionMeasurementSystem(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int raw = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
return Type.fromId(raw) == null ? raw : Type.fromId(raw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof Type) {
|
||||
baseType.encode(byteBuffer, (((Type) o).ordinal()), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, o, scale, offset);
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
metric,
|
||||
;
|
||||
|
||||
public static Type fromId(int id) {
|
||||
for (Type type :
|
||||
Type.values()) {
|
||||
if (type.ordinal() == id) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionTemperature extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionTemperature(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 273);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils.GARMIN_TIME_EPOCH;
|
||||
|
||||
public class FieldDefinitionTimestamp extends FieldDefinition {
|
||||
public FieldDefinitionTimestamp(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, GARMIN_TIME_EPOCH);
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public Object decode(ByteBuffer byteBuffer) {
|
||||
// return new Timestamp((long) baseType.decode(byteBuffer, scale, offset) * 1000L);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
// if(o instanceof Timestamp) {
|
||||
// baseType.encode(byteBuffer, (int) (((Timestamp) o).getTime() / 1000L), scale, offset);
|
||||
// return;
|
||||
// }
|
||||
// baseType.encode(byteBuffer, o, scale, offset);
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
|
||||
|
||||
public class FieldDefinitionWeatherCondition extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionWeatherCondition(int localNumber, int size, BaseType baseType, String name) {
|
||||
super(localNumber, size, baseType, name, 1, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(ByteBuffer byteBuffer) {
|
||||
int idx = (int) baseType.decode(byteBuffer, scale, offset);
|
||||
return Condition.values()[idx];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuffer byteBuffer, Object o) {
|
||||
if (o instanceof Condition) {
|
||||
baseType.encode(byteBuffer, ((Condition) o).ordinal(), scale, offset);
|
||||
return;
|
||||
}
|
||||
baseType.encode(byteBuffer, openWeatherCodeToFitWeatherStatus((int) o), scale, offset);
|
||||
}
|
||||
|
||||
private int openWeatherCodeToFitWeatherStatus(int openWeatherCode) {
|
||||
switch (openWeatherCode) {
|
||||
//Group 2xx: Thunderstorm
|
||||
case 200: //thunderstorm with light rain: //11d
|
||||
case 201: //thunderstorm with rain: //11d
|
||||
case 202: //thunderstorm with heavy rain: //11d
|
||||
case 210: //light thunderstorm:: //11d
|
||||
case 211: //thunderstorm: //11d
|
||||
case 212: //heavy thunderstorm: //11d
|
||||
case 230: //thunderstorm with light drizzle: //11d
|
||||
case 231: //thunderstorm with drizzle: //11d
|
||||
case 232: //thunderstorm with heavy drizzle: //11d
|
||||
return Condition.THUNDERSTORMS.ordinal();
|
||||
case 221: //ragged thunderstorm: //11d
|
||||
return Condition.SCATTERED_THUNDERSTORMS.ordinal();
|
||||
//Group 3xx: Drizzle
|
||||
case 300: //light intensity drizzle: //09d
|
||||
case 310: //light intensity drizzle rain: //09d
|
||||
case 313: //shower rain and drizzle: //09d
|
||||
return Condition.LIGHT_RAIN.ordinal();
|
||||
case 301: //drizzle: //09d
|
||||
case 311: //drizzle rain: //09d
|
||||
return Condition.RAIN.ordinal();
|
||||
case 302: //heavy intensity drizzle: //09d
|
||||
case 312: //heavy intensity drizzle rain: //09d
|
||||
case 314: //heavy shower rain and drizzle: //09d
|
||||
return Condition.HEAVY_RAIN.ordinal();
|
||||
case 321: //shower drizzle: //09d
|
||||
return Condition.SCATTERED_SHOWERS.ordinal();
|
||||
//Group 5xx: Rain
|
||||
case 500: //light rain: //10d
|
||||
case 520: //light intensity shower rain: //09d
|
||||
case 521: //shower rain: //09d
|
||||
return Condition.LIGHT_RAIN.ordinal();
|
||||
case 501: //moderate rain: //10d
|
||||
case 531: //ragged shower rain: //09d
|
||||
return Condition.RAIN.ordinal();
|
||||
case 502: //heavy intensity rain: //10d
|
||||
case 503: //very heavy rain: //10d
|
||||
case 504: //extreme rain: //10d
|
||||
case 522: //heavy intensity shower rain: //09d
|
||||
return Condition.HEAVY_RAIN.ordinal();
|
||||
case 511: //freezing rain: //13d
|
||||
return Condition.UNKNOWN_PRECIPITATION.ordinal();
|
||||
//Group 6xx: Snow
|
||||
case 600: //light snow: //[[file:13d.png]]
|
||||
return Condition.LIGHT_SNOW.ordinal();
|
||||
case 601: //snow: //[[file:13d.png]]
|
||||
case 620: //light shower snow: //[[file:13d.png]]
|
||||
case 621: //shower snow: //[[file:13d.png]]
|
||||
return Condition.SNOW.ordinal();
|
||||
case 602: //heavy snow: //[[file:13d.png]]
|
||||
case 622: //heavy shower snow: //[[file:13d.png]]
|
||||
return Condition.HEAVY_SNOW.ordinal();
|
||||
case 611: //sleet: //[[file:13d.png]]
|
||||
case 612: //light shower sleet: //[[file:13d.png]]
|
||||
case 613: //shower sleet: //[[file:13d.png]]
|
||||
return Condition.WINTRY_MIX.ordinal();
|
||||
case 615: //light rain and snow: //[[file:13d.png]]
|
||||
return Condition.LIGHT_RAIN_SNOW.ordinal();
|
||||
case 616: //rain and snow: //[[file:13d.png]]
|
||||
return Condition.HEAVY_RAIN_SNOW.ordinal();
|
||||
|
||||
//Group 7xx: Atmosphere
|
||||
case 701: //mist: //[[file:50d.png]]
|
||||
case 711: //smoke: //[[file:50d.png]]
|
||||
case 721: //haze: //[[file:50d.png]]
|
||||
case 731: //sandcase dust whirls: //[[file:50d.png]]
|
||||
case 751: //sand: //[[file:50d.png]]
|
||||
case 761: //dust: //[[file:50d.png]]
|
||||
case 762: //volcanic ash: //[[file:50d.png]]
|
||||
return Condition.HAZY.ordinal();
|
||||
case 741: //fog: //[[file:50d.png]]
|
||||
return Condition.FOG.ordinal();
|
||||
case 771: //squalls: //[[file:50d.png]]
|
||||
case 781: //tornado: //[[file:50d.png]]
|
||||
return Condition.WINDY.ordinal();
|
||||
//Group 800: Clear
|
||||
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
|
||||
return Condition.CLEAR.ordinal();
|
||||
|
||||
//Group 80x: Clouds
|
||||
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
|
||||
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
|
||||
return Condition.PARTLY_CLOUDY.ordinal();
|
||||
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
|
||||
return Condition.MOSTLY_CLOUDY.ordinal();
|
||||
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
|
||||
return Condition.CLOUDY.ordinal();
|
||||
//Group 90x: Extreme
|
||||
case 901: //tropical storm
|
||||
return Condition.THUNDERSTORMS.ordinal();
|
||||
case 906: //hail
|
||||
return Condition.HAIL.ordinal();
|
||||
case 903: //cold
|
||||
case 904: //hot
|
||||
case 905: //windy
|
||||
//Group 9xx: Additional
|
||||
case 951: //calm
|
||||
case 952: //light breeze
|
||||
case 953: //gentle breeze
|
||||
case 954: //moderate breeze
|
||||
case 955: //fresh breeze
|
||||
case 956: //strong breeze
|
||||
case 957: //high windcase near gale
|
||||
case 958: //gale
|
||||
case 959: //severe gale
|
||||
case 960: //storm
|
||||
case 961: //violent storm
|
||||
case 902: //hurricane
|
||||
case 962: //hurricane
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown weather code " + openWeatherCode);
|
||||
}
|
||||
}
|
||||
|
||||
enum Condition {
|
||||
CLEAR,
|
||||
PARTLY_CLOUDY,
|
||||
MOSTLY_CLOUDY,
|
||||
RAIN,
|
||||
SNOW,
|
||||
WINDY,
|
||||
THUNDERSTORMS,
|
||||
WINTRY_MIX,
|
||||
FOG,
|
||||
UNK9,
|
||||
UNK10,
|
||||
HAZY,
|
||||
HAIL,
|
||||
SCATTERED_SHOWERS,
|
||||
SCATTERED_THUNDERSTORMS,
|
||||
UNKNOWN_PRECIPITATION,
|
||||
LIGHT_RAIN,
|
||||
HEAVY_RAIN,
|
||||
LIGHT_SNOW,
|
||||
HEAVY_SNOW,
|
||||
LIGHT_RAIN_SNOW,
|
||||
HEAVY_RAIN_SNOW,
|
||||
CLOUDY,
|
||||
;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiHttpService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.HttpUtils;
|
||||
|
||||
public class HttpHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpHandler.class);
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
//.serializeNulls()
|
||||
.create();
|
||||
|
||||
public static GdiHttpService.HttpService handle(final GdiHttpService.HttpService httpService) {
|
||||
if (httpService.hasRawRequest()) {
|
||||
final GdiHttpService.HttpService.RawResponse rawResponse = handleRawRequest(httpService.getRawRequest());
|
||||
if (rawResponse != null) {
|
||||
return GdiHttpService.HttpService.newBuilder()
|
||||
.setRawResponse(rawResponse)
|
||||
.build();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LOG.warn("Unsupported http service request {}", httpService);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static GdiHttpService.HttpService.RawResponse handleRawRequest(final GdiHttpService.HttpService.RawRequest rawRequest) {
|
||||
final String urlString = rawRequest.getUrl();
|
||||
LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), urlString);
|
||||
|
||||
final URL url;
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch (final MalformedURLException e) {
|
||||
LOG.error("Failed to parse url", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
final String path = url.getPath();
|
||||
final Map<String, String> query = HttpUtils.urlQueryParameters(url);
|
||||
final Map<String, String> requestHeaders = headersToMap(rawRequest.getHeaderList());
|
||||
|
||||
final byte[] responseBody;
|
||||
final List<GdiHttpService.HttpService.Header> responseHeaders = new ArrayList<>();
|
||||
if (path.startsWith("/weather/")) {
|
||||
LOG.debug("Got weather request for {}", path);
|
||||
final Object obj = WeatherHandler.handleWeatherRequest(path, query);
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
final String json = GSON.toJson(obj);
|
||||
LOG.debug("Weather response: {}", json);
|
||||
|
||||
final byte[] stringBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
if ("gzip".equals(requestHeaders.get("accept-encoding"))) {
|
||||
responseHeaders.add(
|
||||
GdiHttpService.HttpService.Header.newBuilder()
|
||||
.setKey("Content-Encoding")
|
||||
.setValue("gzip")
|
||||
.build()
|
||||
);
|
||||
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) {
|
||||
gzos.write(stringBytes);
|
||||
gzos.finish();
|
||||
gzos.flush();
|
||||
responseBody = baos.toByteArray();
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to compress response", e);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
responseBody = stringBytes;
|
||||
}
|
||||
|
||||
responseHeaders.add(
|
||||
GdiHttpService.HttpService.Header.newBuilder()
|
||||
.setKey("Content-Type")
|
||||
.setValue("application/json")
|
||||
.build()
|
||||
);
|
||||
} else {
|
||||
LOG.warn("Unhandled path {}", urlString);
|
||||
return null;
|
||||
}
|
||||
|
||||
return GdiHttpService.HttpService.RawResponse.newBuilder()
|
||||
.setStatus(GdiHttpService.HttpService.Status.OK)
|
||||
.setHttpStatus(200)
|
||||
.setBody(ByteString.copyFrom(responseBody))
|
||||
.addAllHeader(responseHeaders)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static Map<String, String> headersToMap(final List<GdiHttpService.HttpService.Header> headers) {
|
||||
final Map<String, String> ret = new HashMap<>();
|
||||
for (final GdiHttpService.HttpService.Header header : headers) {
|
||||
ret.put(header.getKey().toLowerCase(Locale.ROOT), header.getValue());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,375 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import net.e175.klaus.solarpositioning.DeltaT;
|
||||
import net.e175.klaus.solarpositioning.SPA;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
|
||||
|
||||
public class WeatherHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WeatherHandler.class);
|
||||
|
||||
// These get requested on connection at most every 5 minutes
|
||||
public static Object handleWeatherRequest(final String path, final Map<String, String> query) {
|
||||
final WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec();
|
||||
|
||||
if (weatherSpec == null) {
|
||||
LOG.warn("No weather in weather instance");
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (path) {
|
||||
case "/weather/v2/forecast/day": {
|
||||
final int lat = getQueryNum(query, "lat", 0);
|
||||
final int lon = getQueryNum(query, "lon", 0);
|
||||
final int duration = getQueryNum(query, "duration", 5);
|
||||
final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS");
|
||||
final String provider = getQueryString(query, "provider", "dci");
|
||||
final List<WeatherForecastDay> ret = new ArrayList<>(duration);
|
||||
final GregorianCalendar date = new GregorianCalendar();
|
||||
date.setTime(new Date(weatherSpec.timestamp * 1000L));
|
||||
for (int i = 0; i < Math.min(duration, weatherSpec.forecasts.size()); i++) {
|
||||
date.add(Calendar.DAY_OF_MONTH, 1);
|
||||
ret.add(new WeatherForecastDay(date, weatherSpec.forecasts.get(i)));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
case "/weather/v2/forecast/hour": {
|
||||
final int lat = getQueryNum(query, "lat", 0);
|
||||
final int lon = getQueryNum(query, "lon", 0);
|
||||
final int duration = getQueryNum(query, "duration", 13);
|
||||
final String speedUnit = getQueryString(query, "speedUnit", "METERS_PER_SECOND");
|
||||
final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS");
|
||||
final String provider = getQueryString(query, "provider", "dci");
|
||||
final String timesOfInterest = getQueryString(query, "timesOfInterest", "");
|
||||
final List<WeatherForecastHour> ret = new ArrayList<>(duration);
|
||||
for (int i = 0; i < Math.min(duration, weatherSpec.hourly.size()); i++) {
|
||||
ret.add(new WeatherForecastHour(weatherSpec.hourly.get(i)));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
case "/weather/v2/current": {
|
||||
final int lat = getQueryNum(query, "lat", 0);
|
||||
final int lon = getQueryNum(query, "lon", 0);
|
||||
final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS");
|
||||
final String speedUnit = getQueryString(query, "speedUnit", "METERS_PER_SECOND");
|
||||
final String provider = getQueryString(query, "provider", "dci");
|
||||
return new WeatherForecastCurrent(weatherSpec);
|
||||
}
|
||||
}
|
||||
|
||||
LOG.warn("Unknown weather path {}", path);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int getQueryNum(final Map<String, String> query, final String key, final int defaultValue) {
|
||||
final String str = query.get(key);
|
||||
if (str != null) {
|
||||
return Integer.parseInt(str);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getQueryString(final Map<String, String> query, final String key, final String defaultValue) {
|
||||
final String str = query.get(key);
|
||||
if (str != null) {
|
||||
return str;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherForecastDay {
|
||||
public int dayOfWeek; // 1 monday .. 7 sunday
|
||||
public String description;
|
||||
public String summary;
|
||||
public WeatherValue high;
|
||||
public WeatherValue low;
|
||||
public Integer precipProb;
|
||||
public Integer icon;
|
||||
public Integer epochSunrise;
|
||||
public Integer epochSunset;
|
||||
public Wind wind;
|
||||
public Integer humidity;
|
||||
|
||||
public WeatherForecastDay(final GregorianCalendar date, final WeatherSpec.Daily dailyForecast) {
|
||||
dayOfWeek = BLETypeConversions.dayOfWeekToRawBytes(date);
|
||||
description = "Unknown"; // TODO from conditionCode
|
||||
summary = "Unknown"; // TODO from conditionCode
|
||||
high = new WeatherValue(dailyForecast.maxTemp - 273f, "CELSIUS");
|
||||
low = new WeatherValue(dailyForecast.minTemp - 273f, "CELSIUS");
|
||||
precipProb = dailyForecast.precipProbability;
|
||||
icon = mapToCmfCondition(dailyForecast.conditionCode);
|
||||
|
||||
if (dailyForecast.sunRise != 0 && dailyForecast.sunSet != 0) {
|
||||
epochSunrise = dailyForecast.sunRise;
|
||||
epochSunset = dailyForecast.sunSet;
|
||||
} else {
|
||||
final Location lastKnownLocation = new CurrentPosition().getLastKnownLocation();
|
||||
|
||||
final GregorianCalendar[] sunriseTransitSet = SPA.calculateSunriseTransitSet(
|
||||
date,
|
||||
lastKnownLocation.getLatitude(),
|
||||
lastKnownLocation.getLongitude(),
|
||||
DeltaT.estimate(date)
|
||||
);
|
||||
|
||||
epochSunrise = (int) (sunriseTransitSet[0].getTime().getTime() / 1000);
|
||||
epochSunset = (int) (sunriseTransitSet[2].getTime().getTime() / 1000);
|
||||
}
|
||||
|
||||
wind = new Wind(new WeatherValue(dailyForecast.windSpeed * 3.6, "METERS_PER_SECOND"), dailyForecast.windDirection);
|
||||
humidity = dailyForecast.humidity;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherForecastHour {
|
||||
public int epochSeconds;
|
||||
public String description;
|
||||
public WeatherValue temp;
|
||||
public Integer precipProb;
|
||||
public Wind wind;
|
||||
public Integer icon;
|
||||
public WeatherValue dewPoint;
|
||||
public Float uvIndex;
|
||||
public Integer relativeHumidity;
|
||||
public WeatherValue feelsLikeTemperature;
|
||||
public WeatherValue visibility;
|
||||
public WeatherValue pressure;
|
||||
public Object airQuality;
|
||||
public Integer cloudCover;
|
||||
|
||||
public WeatherForecastHour(final WeatherSpec.Hourly hourlyForecast) {
|
||||
epochSeconds = hourlyForecast.timestamp;
|
||||
description = "Unknown"; // TODO from conditionCode
|
||||
temp = new WeatherValue(hourlyForecast.temp - 273f, "CELSIUS");
|
||||
precipProb = hourlyForecast.precipProbability;
|
||||
wind = new Wind(new WeatherValue(hourlyForecast.windSpeed * 3.6, "METERS_PER_SECOND"), hourlyForecast.windDirection);
|
||||
icon = mapToCmfCondition(hourlyForecast.conditionCode);
|
||||
//dewPoint = new WeatherValue(hourlyForecast.temp - 273f, "CELSIUS"); // TODO dewPoint
|
||||
uvIndex = hourlyForecast.uvIndex;
|
||||
relativeHumidity = hourlyForecast.humidity;
|
||||
//feelsLikeTemperature = new WeatherValue(hourlyForecast.temp - 273f, "CELSIUS"); // TODO feelsLikeTemperature
|
||||
//visibility = new WeatherValue(0, "METER"); // TODO visibility
|
||||
//pressure = new WeatherValue(0f, "INCHES_OF_MERCURY"); // TODO pressure
|
||||
//airQuality = null; // TODO airQuality
|
||||
//cloudCover = 0; // TODO cloudCover
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherForecastCurrent {
|
||||
public Integer epochSeconds;
|
||||
public WeatherValue temperature;
|
||||
public String description;
|
||||
public Integer icon;
|
||||
public WeatherValue feelsLikeTemperature;
|
||||
public WeatherValue dewPoint;
|
||||
public Integer relativeHumidity;
|
||||
public Wind wind;
|
||||
public String locationName;
|
||||
public WeatherValue visibility;
|
||||
public WeatherValue pressure;
|
||||
public WeatherValue pressureChange;
|
||||
|
||||
public WeatherForecastCurrent(final WeatherSpec weatherSpec) {
|
||||
epochSeconds = weatherSpec.timestamp;
|
||||
temperature = new WeatherValue(weatherSpec.currentTemp - 273f, "CELSIUS");
|
||||
description = weatherSpec.currentCondition;
|
||||
icon = mapToCmfCondition(weatherSpec.currentConditionCode);
|
||||
feelsLikeTemperature = new WeatherValue(weatherSpec.currentTemp - 273f, "CELSIUS");
|
||||
dewPoint = new WeatherValue(weatherSpec.dewPoint - 273f, "CELSIUS");
|
||||
relativeHumidity = weatherSpec.currentHumidity;
|
||||
wind = new Wind(new WeatherValue(weatherSpec.windSpeed * 3.6, "METERS_PER_SECOND"), weatherSpec.windDirection);
|
||||
locationName = weatherSpec.location;
|
||||
visibility = new WeatherValue(weatherSpec.visibility, "METER");
|
||||
pressure = new WeatherValue(weatherSpec.pressure * 0.02953, "INCHES_OF_MERCURY");
|
||||
pressureChange = new WeatherValue(0f, "INCHES_OF_MERCURY");
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherValue {
|
||||
public Number value;
|
||||
public String units;
|
||||
|
||||
public WeatherValue(final Number value, final String units) {
|
||||
this.value = value;
|
||||
this.units = units;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Wind {
|
||||
public WeatherValue speed;
|
||||
public String directionString; // NW
|
||||
public Integer direction;
|
||||
|
||||
public Wind(final WeatherValue speed, final int direction) {
|
||||
this.speed = speed;
|
||||
this.direction = direction;
|
||||
}
|
||||
}
|
||||
|
||||
public static int mapToCmfCondition(int openWeatherMapCondition) {
|
||||
// Icons mapped from a Venu 3:
|
||||
// 0 1 2 unk
|
||||
// 3 4 5 6 sunny
|
||||
// 7 8 9 10 sun cloudy
|
||||
// 11 12 cloudy with dashes below
|
||||
// 13 14 sun cloud 2 clouds
|
||||
// 15 16 clouds
|
||||
// 17 rain
|
||||
// 18 19 20 21 rain with sun (or night at night?)
|
||||
// 22 rain
|
||||
// 23 24 unk
|
||||
// 25 26 thunder with rain and sun behind
|
||||
// 27 thunder with rain
|
||||
// 28 29 rain
|
||||
// 30 31 32 33 34 snow with clouds
|
||||
// 35 36 37 snowflake
|
||||
// 38 snow with clouds, with big flake
|
||||
// 39 snow with rain
|
||||
// 40 41 snow with rain
|
||||
// 42 43 44 rain with snow
|
||||
// 45 rain with snow
|
||||
// 46 wind
|
||||
// 47 48 foggy (dashes?)
|
||||
// 49 50 51 unk
|
||||
|
||||
switch (openWeatherMapCondition) {
|
||||
//Group 2xx: Thunderstorm
|
||||
case 210: //light thunderstorm:: //11d
|
||||
case 200: //thunderstorm with light rain: //11d
|
||||
case 201: //thunderstorm with rain: //11d
|
||||
case 202: //thunderstorm with heavy rain: //11d
|
||||
case 230: //thunderstorm with light drizzle: //11d
|
||||
case 231: //thunderstorm with drizzle: //11d
|
||||
case 232: //thunderstorm with heavy drizzle: //11d
|
||||
case 211: //thunderstorm: //11d
|
||||
case 212: //heavy thunderstorm: //11d
|
||||
case 221: //ragged thunderstorm: //11d
|
||||
return 27;
|
||||
|
||||
//Group 90x: Extreme
|
||||
case 901: //tropical storm
|
||||
//Group 7xx: Atmosphere
|
||||
case 781: //tornado: //[[file:50d.png]]
|
||||
//Group 90x: Extreme
|
||||
case 900: //tornado
|
||||
// Group 7xx: Atmosphere
|
||||
case 771: //squalls: //[[file:50d.png]]
|
||||
//Group 9xx: Additional
|
||||
case 960: //storm
|
||||
case 961: //violent storm
|
||||
case 902: //hurricane
|
||||
case 962: //hurricane
|
||||
return 46;
|
||||
|
||||
//Group 3xx: Drizzle
|
||||
case 300: //light intensity drizzle: //09d
|
||||
case 301: //drizzle: //09d
|
||||
case 302: //heavy intensity drizzle: //09d
|
||||
case 310: //light intensity drizzle rain: //09d
|
||||
case 311: //drizzle rain: //09d
|
||||
case 312: //heavy intensity drizzle rain: //09d
|
||||
case 313: //shower rain and drizzle: //09d
|
||||
case 314: //heavy shower rain and drizzle: //09d
|
||||
case 321: //shower drizzle: //09d
|
||||
//Group 5xx: Rain
|
||||
case 500: //light rain: //10d
|
||||
case 501: //moderate rain: //10d
|
||||
case 502: //heavy intensity rain: //10d
|
||||
case 503: //very heavy rain: //10d
|
||||
case 504: //extreme rain: //10d
|
||||
case 520: //light intensity shower rain: //09d
|
||||
case 521: //shower rain: //09d
|
||||
case 522: //heavy intensity shower rain: //09d
|
||||
case 531: //ragged shower rain: //09d
|
||||
return 17;
|
||||
|
||||
//Group 90x: Extreme
|
||||
case 906: //hail
|
||||
case 615: //light rain and snow: //[[file:13d.png]]
|
||||
case 616: //rain and snow: //[[file:13d.png]]
|
||||
case 511: //freezing rain: //13d
|
||||
return 40;
|
||||
|
||||
//Group 6xx: Snow
|
||||
case 611: //sleet: //[[file:13d.png]]
|
||||
case 612: //shower sleet: //[[file:13d.png]]
|
||||
//Group 6xx: Snow
|
||||
case 600: //light snow: //[[file:13d.png]]
|
||||
case 601: //snow: //[[file:13d.png]]
|
||||
//Group 6xx: Snow
|
||||
case 602: //heavy snow: //[[file:13d.png]]
|
||||
//Group 6xx: Snow
|
||||
case 620: //light shower snow: //[[file:13d.png]]
|
||||
case 621: //shower snow: //[[file:13d.png]]
|
||||
case 622: //heavy shower snow: //[[file:13d.png]]
|
||||
return 38;
|
||||
|
||||
|
||||
//Group 7xx: Atmosphere
|
||||
case 701: //mist: //[[file:50d.png]]
|
||||
case 711: //smoke: //[[file:50d.png]]
|
||||
case 721: //haze: //[[file:50d.png]]
|
||||
case 731: //sandcase dust whirls: //[[file:50d.png]]
|
||||
case 741: //fog: //[[file:50d.png]]
|
||||
case 751: //sand: //[[file:50d.png]]
|
||||
case 761: //dust: //[[file:50d.png]]
|
||||
case 762: //volcanic ash: //[[file:50d.png]]
|
||||
return 47;
|
||||
|
||||
//Group 800: Clear
|
||||
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
|
||||
return 5;
|
||||
|
||||
//Group 90x: Extreme
|
||||
case 904: //hot
|
||||
return 5;
|
||||
|
||||
//Group 80x: Clouds
|
||||
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
|
||||
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
|
||||
return 8;
|
||||
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
|
||||
return 15;
|
||||
|
||||
//Group 80x: Clouds
|
||||
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
|
||||
return 15;
|
||||
|
||||
//Group 9xx: Additional
|
||||
case 905: //windy
|
||||
case 951: //calm
|
||||
case 952: //light breeze
|
||||
case 953: //gentle breeze
|
||||
case 954: //moderate breeze
|
||||
case 955: //fresh breeze
|
||||
case 956: //strong breeze
|
||||
case 957: //high windcase near gale
|
||||
case 958: //gale
|
||||
case 959: //severe gale
|
||||
return 46;
|
||||
|
||||
default:
|
||||
//Group 90x: Extreme
|
||||
case 903: //cold
|
||||
return 35;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
|
||||
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 Set<GarminCapability> capabilities;
|
||||
private final byte[] ourConfigurationPayload = GarminCapability.setToBinary(OUR_CAPABILITIES);
|
||||
|
||||
public ConfigurationMessage(GarminMessage garminMessage, byte[] configurationPayload) {
|
||||
this.garminMessage = garminMessage;
|
||||
if (configurationPayload.length > 255)
|
||||
throw new IllegalArgumentException("Too long payload");
|
||||
this.incomingConfigurationPayload = configurationPayload;
|
||||
this.capabilities = GarminCapability.setFromBinary(configurationPayload);
|
||||
LOG.info("Received configuration message; capabilities: {}", GarminCapability.setToString(capabilities));
|
||||
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
}
|
||||
|
||||
public static ConfigurationMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final int endOfPayload = reader.readByte();
|
||||
ConfigurationMessage configurationMessage = new ConfigurationMessage(garminMessage, reader.readBytes(endOfPayload - reader.getPosition()));
|
||||
return configurationMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
final Set<Object> capabilitiesPref = new HashSet<>();
|
||||
for (final GarminCapability capability : capabilities) {
|
||||
capabilitiesPref.add(capability.name());
|
||||
}
|
||||
return Collections.singletonList(
|
||||
new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_GARMIN_CAPABILITIES, capabilitiesPref)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // placeholder for packet size
|
||||
writer.writeShort(garminMessage.getId());
|
||||
writer.writeByte(ourConfigurationPayload.length);
|
||||
writer.writeBytes(ourConfigurationPayload);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
|
||||
|
||||
public class CreateFileMessage extends GFDIMessage {
|
||||
|
||||
private final int fileSize;
|
||||
private final FileType.FILETYPE filetype;
|
||||
private final boolean generateOutgoing;
|
||||
|
||||
public CreateFileMessage(GarminMessage garminMessage, int fileSize, FileType.FILETYPE filetype) {
|
||||
this.fileSize = fileSize;
|
||||
this.filetype = filetype;
|
||||
this.garminMessage = garminMessage;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
this.generateOutgoing = false;
|
||||
}
|
||||
|
||||
public CreateFileMessage(int fileSize, FileType.FILETYPE filetype) {
|
||||
this.garminMessage = GarminMessage.CREATE_FILE;
|
||||
this.fileSize = fileSize;
|
||||
this.filetype = filetype;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
this.generateOutgoing = true;
|
||||
}
|
||||
|
||||
public static CreateFileMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
|
||||
final int fileSize = reader.readInt();
|
||||
final int dataType = reader.readByte(); //SupportedFileTypesStatusMessage.FileTypeInfo.type
|
||||
final int subType = reader.readByte();//SupportedFileTypesStatusMessage.FileTypeInfo.subtypetype
|
||||
final FileType.FILETYPE filetype = FileType.FILETYPE.fromDataTypeSubType(dataType, subType);
|
||||
final int fileIndex = reader.readShort(); //???
|
||||
reader.readByte(); //unk
|
||||
final int subTypeMask = reader.readByte(); //???
|
||||
final int numberMask = reader.readShort(); //???
|
||||
|
||||
return new CreateFileMessage(garminMessage, fileSize, filetype);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() { //TODO: adjust variables
|
||||
Random random = new Random();
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
writer.writeInt(this.fileSize);
|
||||
writer.writeByte(this.filetype.getType());
|
||||
writer.writeByte(this.filetype.getSubType());
|
||||
writer.writeShort(0); //fileIndex
|
||||
writer.writeByte(0); //reserved
|
||||
writer.writeByte(0); //subtypemask
|
||||
writer.writeShort(65535); //numbermask
|
||||
writer.writeShort(0); ///???
|
||||
writer.writeLong(random.nextLong());
|
||||
|
||||
return generateOutgoing;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import org.threeten.bp.Instant;
|
||||
import org.threeten.bp.ZoneId;
|
||||
import org.threeten.bp.zone.ZoneOffsetTransition;
|
||||
import org.threeten.bp.zone.ZoneRules;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils;
|
||||
|
||||
public class CurrentTimeRequestMessage extends GFDIMessage {
|
||||
private final int referenceID;
|
||||
|
||||
public CurrentTimeRequestMessage(int referenceID, GarminMessage garminMessage) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.referenceID = referenceID;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
}
|
||||
|
||||
public static CurrentTimeRequestMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final int referenceID = reader.readInt();
|
||||
|
||||
return new CurrentTimeRequestMessage(referenceID, garminMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
|
||||
final Instant now = Instant.now();
|
||||
final ZoneRules zoneRules = ZoneId.systemDefault().getRules();
|
||||
final int dstOffset = (int) zoneRules.getDaylightSavings(now).getSeconds();
|
||||
final int timeZoneOffset = TimeZone.getDefault().getOffset(now.toEpochMilli()) / 1000;
|
||||
final int garminTimestamp = GarminTimeUtils.unixTimeToGarminTimestamp((int) now.getEpochSecond());
|
||||
final ZoneOffsetTransition nextTransitionStart = zoneRules.nextTransition(now);
|
||||
|
||||
final int nextTransitionStartsTs = (int) nextTransitionStart.toEpochSecond();
|
||||
final int nextTransitionEndsTs = (int) zoneRules.nextTransition(nextTransitionStart.getInstant()).toEpochSecond();
|
||||
|
||||
|
||||
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(this.garminMessage.getId());
|
||||
writer.writeByte(Status.ACK.ordinal());
|
||||
writer.writeInt(referenceID);
|
||||
writer.writeInt(garminTimestamp);
|
||||
writer.writeInt(timeZoneOffset);
|
||||
writer.writeInt(GarminTimeUtils.unixTimeToGarminTimestamp(nextTransitionEndsTs));
|
||||
writer.writeInt(GarminTimeUtils.unixTimeToGarminTimestamp(nextTransitionStartsTs));
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
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 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(GarminMessage garminMessage, int protocolVersion, int productNumber, String unitNumber, int softwareVersion, int maxPacketSize, String bluetoothFriendlyName, String deviceName, String deviceModel) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.incomingProtocolVersion = protocolVersion;
|
||||
this.incomingProductNumber = productNumber;
|
||||
this.incomingUnitNumber = unitNumber;
|
||||
this.incomingSoftwareVersion = softwareVersion;
|
||||
this.incomingMaxPacketSize = maxPacketSize;
|
||||
this.bluetoothFriendlyName = bluetoothFriendlyName;
|
||||
this.deviceName = deviceName;
|
||||
this.deviceModel = deviceModel;
|
||||
|
||||
GFDIMessage.setMaxPacketSize(maxPacketSize);
|
||||
this.statusMessage = getStatusMessage();
|
||||
}
|
||||
|
||||
public static DeviceInformationMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
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();
|
||||
|
||||
return new DeviceInformationMessage(garminMessage, protocolVersion, productNumber, unitNumber, softwareVersion, maxPacketSize, bluetoothFriendlyName, deviceName, deviceModel);
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@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(this.garminMessage.getId());
|
||||
writer.writeByte(Status.ACK.ordinal());
|
||||
writer.writeShort(ourProtocolVersion);
|
||||
writer.writeShort(ourProductNumber);
|
||||
writer.writeInt(ourUnitNumber);
|
||||
writer.writeShort(ourSoftwareVersion);
|
||||
writer.writeShort(ourMaxPacketSize);
|
||||
String bluetoothName;
|
||||
try {
|
||||
bluetoothName = BluetoothAdapter.getDefaultAdapter().getName();
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to get bluetooth name", e);
|
||||
bluetoothName = "Unknown";
|
||||
}
|
||||
writer.writeString(bluetoothName);
|
||||
writer.writeString(Build.MANUFACTURER);
|
||||
writer.writeString(Build.DEVICE);
|
||||
writer.writeByte(protocolFlags);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GBDeviceEvent> 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 Collections.singletonList(versionCmd);
|
||||
}
|
||||
|
||||
private String getSoftwareVersionStr() {
|
||||
int softwareVersionMajor = incomingSoftwareVersion / 100;
|
||||
int softwareVersionMinor = incomingSoftwareVersion % 100;
|
||||
return String.format(Locale.ROOT, "%d.%02d", softwareVersionMajor, softwareVersionMinor);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class DownloadRequestMessage extends GFDIMessage {
|
||||
|
||||
private final int fileIndex;
|
||||
private final REQUEST_TYPE requestType;
|
||||
private final int crcSeed;
|
||||
private final int dataSize;
|
||||
|
||||
private final int dataOffset;
|
||||
|
||||
public DownloadRequestMessage(GarminMessage garminMessage, int fileIndex, int size, REQUEST_TYPE requestType, int crcSeed, int dataSize, int dataOffset) {
|
||||
this.requestType = requestType;
|
||||
this.crcSeed = crcSeed;
|
||||
this.dataSize = dataSize;
|
||||
this.dataOffset = dataOffset;
|
||||
this.garminMessage = garminMessage;
|
||||
this.fileIndex = fileIndex;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
}
|
||||
|
||||
public DownloadRequestMessage(int fileIndex, int dataSize, REQUEST_TYPE requestType, int crcSeed, int dataOffset) {
|
||||
this(GarminMessage.DOWNLOAD_REQUEST, fileIndex, dataSize, requestType, crcSeed, dataSize, dataOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
writer.writeShort(this.fileIndex);
|
||||
writer.writeInt(this.dataOffset);
|
||||
writer.writeByte(this.requestType.ordinal());
|
||||
writer.writeShort(this.crcSeed);
|
||||
writer.writeInt(this.dataSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum REQUEST_TYPE {
|
||||
CONTINUE,
|
||||
NEW,
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.FileTransferDataStatusMessage;
|
||||
|
||||
public class FileTransferDataMessage extends GFDIMessage {
|
||||
|
||||
private final byte[] message;
|
||||
private final int dataOffset;
|
||||
private final boolean sendOutgoing;
|
||||
private final int crc;
|
||||
|
||||
public FileTransferDataMessage(byte[] message, int dataOffset, int crc) {
|
||||
this(message, dataOffset, crc, true);
|
||||
}
|
||||
|
||||
public FileTransferDataMessage(byte[] message, int dataOffset, int crc, boolean sendOutgoing) {
|
||||
this.garminMessage = GarminMessage.FILE_TRANSFER_DATA;
|
||||
this.dataOffset = dataOffset;
|
||||
this.crc = crc;
|
||||
this.message = message;
|
||||
|
||||
this.statusMessage = new FileTransferDataStatusMessage(GarminMessage.FILE_TRANSFER_DATA, Status.ACK, FileTransferDataStatusMessage.TransferStatus.OK, dataOffset);
|
||||
this.sendOutgoing = sendOutgoing;
|
||||
}
|
||||
|
||||
public static FileTransferDataMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
|
||||
final int flags = reader.readByte();
|
||||
final int crc = reader.readShort();
|
||||
final int dataOffset = reader.readInt();
|
||||
final byte[] message = reader.readBytes(reader.remaining());
|
||||
|
||||
return new FileTransferDataMessage(message, dataOffset, crc, false);
|
||||
}
|
||||
|
||||
public byte[] getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public int getCrc() {
|
||||
return crc;
|
||||
}
|
||||
|
||||
public int getDataOffset() {
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(garminMessage.getId());
|
||||
writer.writeByte(0); //flags?
|
||||
writer.writeShort(crc);
|
||||
writer.writeInt(dataOffset);
|
||||
writer.writeBytes(message);
|
||||
return sendOutgoing;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
||||
|
||||
public class FindMyPhoneCancelMessage extends GFDIMessage {
|
||||
public FindMyPhoneCancelMessage(GarminMessage garminMessage) {
|
||||
this.garminMessage = garminMessage;
|
||||
|
||||
this.statusMessage = getStatusMessage();
|
||||
}
|
||||
|
||||
public static FindMyPhoneCancelMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
return new FindMyPhoneCancelMessage(garminMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
|
||||
return Collections.singletonList(findPhoneEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
|
||||
|
||||
public class FindMyPhoneRequestMessage extends GFDIMessage {
|
||||
private final int duration;
|
||||
|
||||
public FindMyPhoneRequestMessage(GarminMessage garminMessage, int duration) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.duration = duration;
|
||||
|
||||
this.statusMessage = getStatusMessage();
|
||||
}
|
||||
|
||||
public static FindMyPhoneRequestMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final int duration = reader.readByte();
|
||||
|
||||
return new FindMyPhoneRequestMessage(garminMessage, duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
|
||||
return Collections.singletonList(findPhoneEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.PredefinedLocalMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
||||
|
||||
public class FitDataMessage extends GFDIMessage {
|
||||
private final List<RecordData> recordDataList;
|
||||
|
||||
public FitDataMessage(List<RecordData> recordDataList, GarminMessage garminMessage) {
|
||||
this.recordDataList = recordDataList;
|
||||
this.garminMessage = garminMessage;
|
||||
}
|
||||
|
||||
public FitDataMessage(List<RecordData> recordDataList) {
|
||||
this.recordDataList = recordDataList;
|
||||
this.garminMessage = GarminMessage.FIT_DATA;
|
||||
}
|
||||
|
||||
public static FitDataMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final List<RecordData> recordDataList = new ArrayList<>();
|
||||
|
||||
while (reader.remaining() > 0) {
|
||||
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte(), true);
|
||||
if (recordHeader.isDefinition())
|
||||
return null;
|
||||
PredefinedLocalMessage predefinedLocalMessage = recordHeader.getPredefinedLocalMessage();
|
||||
if (predefinedLocalMessage == null) {
|
||||
LOG.warn("Local message is null");
|
||||
|
||||
return null;
|
||||
}
|
||||
RecordData recordData = new RecordData(predefinedLocalMessage.getRecordDefinition());
|
||||
recordData.parseDataMessage(reader);
|
||||
recordDataList.add(recordData);
|
||||
}
|
||||
|
||||
return new FitDataMessage(recordDataList, garminMessage);
|
||||
}
|
||||
|
||||
public List<RecordData> getRecordDataList() {
|
||||
return recordDataList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
for (RecordData recordData : recordDataList) {
|
||||
recordData.generateOutgoingDataPayload(writer);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
|
||||
|
||||
public class FitDefinitionMessage extends GFDIMessage {
|
||||
|
||||
private final List<RecordDefinition> recordDefinitions;
|
||||
|
||||
public FitDefinitionMessage(List<RecordDefinition> recordDefinitions, GarminMessage garminMessage) {
|
||||
this.recordDefinitions = recordDefinitions;
|
||||
this.garminMessage = garminMessage;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
}
|
||||
|
||||
public FitDefinitionMessage(List<RecordDefinition> recordDefinitions) {
|
||||
this.recordDefinitions = recordDefinitions;
|
||||
this.garminMessage = GarminMessage.FIT_DEFINITION;
|
||||
}
|
||||
|
||||
public static FitDefinitionMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
List<RecordDefinition> recordDefinitions = new ArrayList<>();
|
||||
|
||||
while (reader.remaining() > 0) {
|
||||
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte(), true);
|
||||
recordDefinitions.add(RecordDefinition.parseIncoming(reader, recordHeader));
|
||||
}
|
||||
|
||||
return new FitDefinitionMessage(recordDefinitions, garminMessage);
|
||||
}
|
||||
|
||||
public List<RecordDefinition> getRecordDefinitions() {
|
||||
return recordDefinitions;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(garminMessage.getId());
|
||||
for (RecordDefinition recordDefinition : recordDefinitions) {
|
||||
recordDefinition.generateOutgoingPayload(writer);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
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 java.nio.ByteOrder;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GFDIStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public abstract class GFDIMessage {
|
||||
public static final int MESSAGE_REQUEST = 5001;
|
||||
public static final int MESSAGE_DIRECTORY_FILE_FILTER_REQUEST = 5007;
|
||||
public static final int MESSAGE_FILE_READY = 5009;
|
||||
public static final int MESSAGE_BATTERY_STATUS = 5023;
|
||||
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_AUTH_NEGOTIATION = 5101;
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(GFDIMessage.class);
|
||||
private static int maxPacketSize = 375; //safe default?
|
||||
protected final ByteBuffer response = ByteBuffer.allocate(1000);
|
||||
protected GFDIStatusMessage statusMessage;
|
||||
protected GarminMessage garminMessage;
|
||||
|
||||
public static int getMaxPacketSize() {
|
||||
return maxPacketSize;
|
||||
}
|
||||
|
||||
public static void setMaxPacketSize(int maxPacketSize) {
|
||||
GFDIMessage.maxPacketSize = maxPacketSize;
|
||||
}
|
||||
|
||||
public static GFDIMessage parseIncoming(byte[] message) {
|
||||
final MessageReader messageReader = new MessageReader(message);
|
||||
|
||||
int messageType = messageReader.readShort();
|
||||
if (messageType > 0x8000) {
|
||||
messageType = (messageType & 0xff) + 5000;
|
||||
}
|
||||
try {
|
||||
final GarminMessage garminMessage = GarminMessage.fromId(messageType);
|
||||
if (garminMessage == null) {
|
||||
LOG.warn("Unknown message type {}, message {}", messageType, message);
|
||||
return new UnhandledMessage(messageType);
|
||||
}
|
||||
final Method m = garminMessage.objectClass.getMethod("parseIncoming", MessageReader.class, GarminMessage.class);
|
||||
return garminMessage.objectClass.cast(m.invoke(null, messageReader, garminMessage));
|
||||
} catch (final Exception e) {
|
||||
LOG.error("UNHANDLED GFDI MESSAGE TYPE {}, MESSAGE {}", messageType, message, e);
|
||||
return new UnhandledMessage(messageType);
|
||||
} finally {
|
||||
messageReader.warnIfLeftover(messageType);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract boolean generateOutgoing();
|
||||
|
||||
public byte[] getOutgoingMessage() {
|
||||
response.clear();
|
||||
boolean toSend = generateOutgoing();
|
||||
response.order(ByteOrder.LITTLE_ENDIAN);
|
||||
if (!toSend)
|
||||
return null;
|
||||
addLengthAndChecksum();
|
||||
response.flip();
|
||||
|
||||
final byte[] packet = new byte[response.limit()];
|
||||
response.get(packet);
|
||||
return packet;
|
||||
}
|
||||
|
||||
protected GFDIStatusMessage getStatusMessage() {
|
||||
return new GenericStatusMessage(garminMessage, Status.ACK);
|
||||
}
|
||||
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
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?
|
||||
DOWNLOAD_REQUEST(5002, DownloadRequestMessage.class),
|
||||
UPLOAD_REQUEST(5003, UploadRequestMessage.class),
|
||||
FILE_TRANSFER_DATA(5004, FileTransferDataMessage.class),
|
||||
CREATE_FILE(5005, CreateFileMessage.class),
|
||||
SET_FILE_FLAG(5008, SetFileFlagsMessage.class),
|
||||
FIT_DEFINITION(5011, FitDefinitionMessage.class),
|
||||
FIT_DATA(5012, FitDataMessage.class),
|
||||
WEATHER_REQUEST(5014, WeatherMessage.class),
|
||||
DEVICE_INFORMATION(5024, DeviceInformationMessage.class),
|
||||
DEVICE_SETTINGS(5026, SetDeviceSettingsMessage.class),
|
||||
SYSTEM_EVENT(5030, SystemEventMessage.class),
|
||||
SUPPORTED_FILE_TYPES_REQUEST(5031, SupportedFileTypesMessage.class),
|
||||
NOTIFICATION_UPDATE(5033, NotificationUpdateMessage.class),
|
||||
NOTIFICATION_CONTROL(5034, NotificationControlMessage.class),
|
||||
NOTIFICATION_DATA(5035, NotificationDataMessage.class),
|
||||
NOTIFICATION_SUBSCRIPTION(5036, NotificationSubscriptionMessage.class),
|
||||
FIND_MY_PHONE_REQUEST(5039, FindMyPhoneRequestMessage.class),
|
||||
FIND_MY_PHONE_CANCEL(5040, FindMyPhoneCancelMessage.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> getClassFromId(final int id) {
|
||||
for (final GarminMessage garminMessage : GarminMessage.values()) {
|
||||
if (garminMessage.getId() == id) {
|
||||
return garminMessage.getObjectClass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static GarminMessage fromId(final int id) {
|
||||
for (final GarminMessage garminMessage : GarminMessage.values()) {
|
||||
if (garminMessage.getId() == id) {
|
||||
return garminMessage;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
public static Status fromCode(final int code) {
|
||||
for (final Status status : Status.values()) {
|
||||
if (status.ordinal() == code) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown status code " + code);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class MessageReader extends GarminByteBufferReader {
|
||||
|
||||
private final int payloadSize;
|
||||
|
||||
public MessageReader(byte[] data) {
|
||||
super(data);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
this.payloadSize = readShort(); //includes CRC
|
||||
checkSize();
|
||||
checkCRC();
|
||||
this.byteBuffer.limit(payloadSize - 2); //remove CRC
|
||||
}
|
||||
|
||||
public void skip(int offset) {
|
||||
if (remaining() < offset) throw new IllegalStateException();
|
||||
byteBuffer.position(byteBuffer.position() + offset);
|
||||
}
|
||||
|
||||
private void checkSize() {
|
||||
if (payloadSize != byteBuffer.capacity()) {
|
||||
LOG.error("Received GFDI packet with invalid length: {} vs {}", payloadSize, byteBuffer.capacity());
|
||||
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(int messageType) {
|
||||
if (byteBuffer.hasRemaining() && byteBuffer.position() < (byteBuffer.limit())) {
|
||||
int pos = byteBuffer.position();
|
||||
int numBytes = (byteBuffer.limit()) - byteBuffer.position();
|
||||
byte[] leftover = new byte[numBytes];
|
||||
byteBuffer.get(leftover);
|
||||
byteBuffer.position(pos);
|
||||
LOG.warn("Leftover bytes when parsing message type {}. Bytes: {}, complete message: {}", messageType, GB.hexdump(leftover), GB.hexdump(byteBuffer.array()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class MessageWriter {
|
||||
private static final int DEFAULT_BUFFER_SIZE = 16384;
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public MessageWriter() {
|
||||
this(DEFAULT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
public MessageWriter(int bufferSize) {
|
||||
this.byteBuffer = ByteBuffer.allocate(bufferSize);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
public MessageWriter(ByteBuffer byteBuffer) {
|
||||
this.byteBuffer = byteBuffer;
|
||||
this.byteBuffer.clear();
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
public void setByteOrder(ByteOrder byteOrder) {
|
||||
this.byteBuffer.order(byteOrder);
|
||||
}
|
||||
|
||||
public void writeByte(int value) {
|
||||
byteBuffer.put((byte) value);
|
||||
}
|
||||
|
||||
public void writeShort(int value) {
|
||||
byteBuffer.putShort((short) value);
|
||||
}
|
||||
|
||||
public void writeInt(int value) {
|
||||
byteBuffer.putInt(value);
|
||||
}
|
||||
|
||||
public void writeLong(long value) {
|
||||
byteBuffer.putLong(value);
|
||||
}
|
||||
|
||||
public void writeFloat32(float value) {
|
||||
byteBuffer.putFloat(value);
|
||||
}
|
||||
|
||||
public void writeFloat64(double value) {
|
||||
byteBuffer.putDouble(value);
|
||||
}
|
||||
|
||||
public void writeString(String value) {
|
||||
final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
final int size = bytes.length;
|
||||
if (size > 255) throw new IllegalArgumentException("Too long string");
|
||||
|
||||
if (byteBuffer.position() + 1 + size > byteBuffer.capacity())
|
||||
throw new IllegalStateException();
|
||||
byteBuffer.put((byte) size);
|
||||
byteBuffer.put(bytes);
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
//TODO: implement the correct flip()/compat() logic
|
||||
return byteBuffer.hasRemaining() ? Arrays.copyOf(byteBuffer.array(), byteBuffer.position()) : byteBuffer.array();
|
||||
}
|
||||
|
||||
public byte[] peekBytes() {
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
public void writeBytes(byte[] bytes) {
|
||||
writeBytes(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public void writeBytes(byte[] bytes, int offset, int size) {
|
||||
if (byteBuffer.position() + size > byteBuffer.capacity()) throw new IllegalStateException();
|
||||
byteBuffer.put(Arrays.copyOfRange(bytes, offset, offset + size));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class MusicControlCapabilitiesMessage extends GFDIMessage {
|
||||
|
||||
private final int supportedCapabilities;
|
||||
private final GarminMusicControlCommand[] commands = GarminMusicControlCommand.values();
|
||||
|
||||
public MusicControlCapabilitiesMessage(GarminMessage garminMessage, int supportedCapabilities) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.supportedCapabilities = supportedCapabilities;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
}
|
||||
|
||||
public static MusicControlCapabilitiesMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final int supportedCapabilities = reader.readByte();
|
||||
|
||||
return new MusicControlCapabilitiesMessage(garminMessage, 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(this.garminMessage.getId());
|
||||
writer.writeByte(Status.ACK.ordinal());
|
||||
writer.writeByte(commands.length);
|
||||
for (GarminMusicControlCommand command : commands) {
|
||||
writer.writeByte(command.ordinal());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum GarminMusicControlCommand {
|
||||
TOGGLE_PLAY_PAUSE,
|
||||
SKIP_TO_NEXT_ITEM,
|
||||
SKIP_TO_PREVIOUS_ITEM,
|
||||
VOLUME_UP,
|
||||
VOLUME_DOWN,
|
||||
PLAY,
|
||||
PAUSE,
|
||||
SKIP_FORWARD,
|
||||
SKIP_BACKWARDS
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
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;
|
||||
this.garminMessage = GarminMessage.MUSIC_CONTROL_ENTITY_UPDATE;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
|
||||
for (Map.Entry<MusicEntity, String> entry : attributes.entrySet()) {
|
||||
MusicEntity a = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (null == value)
|
||||
value = "";
|
||||
byte[] v = value.getBytes(StandardCharsets.UTF_8);
|
||||
if (v.length > 252) throw new IllegalArgumentException("Too long value");
|
||||
|
||||
writer.writeByte((v.length + 3) & 0xff); //the three following bytes
|
||||
writer.writeByte(a.getEntityId());
|
||||
writer.writeByte(a.ordinal());
|
||||
writer.writeByte(0);//TODO what is this?
|
||||
writer.writeBytes(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum PLAYER implements MusicEntity {
|
||||
NAME,
|
||||
PLAYBACK_INFO,
|
||||
VOLUME;
|
||||
|
||||
@Override
|
||||
public int getEntityId() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public enum QUEUE implements MusicEntity {
|
||||
INDEX,
|
||||
COUNT,
|
||||
SHUFFLE,
|
||||
REPEAT;
|
||||
|
||||
@Override
|
||||
public int getEntityId() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public enum TRACK implements MusicEntity {
|
||||
ARTIST,
|
||||
ALBUM,
|
||||
TITLE,
|
||||
DURATION;
|
||||
|
||||
@Override
|
||||
public int getEntityId() {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public interface MusicEntity {
|
||||
int getEntityId();
|
||||
|
||||
int ordinal();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
|
||||
public class MusicControlMessage extends GFDIMessage {
|
||||
|
||||
private static final MusicControlCapabilitiesMessage.GarminMusicControlCommand[] commands = MusicControlCapabilitiesMessage.GarminMusicControlCommand.values();
|
||||
private final GBDeviceEventMusicControl event;
|
||||
|
||||
public MusicControlMessage(GarminMessage garminMessage, MusicControlCapabilitiesMessage.GarminMusicControlCommand command) {
|
||||
this.event = new GBDeviceEventMusicControl();
|
||||
this.garminMessage = garminMessage;
|
||||
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;
|
||||
case VOLUME_UP:
|
||||
event.event = GBDeviceEventMusicControl.Event.VOLUMEUP;
|
||||
break;
|
||||
case VOLUME_DOWN:
|
||||
event.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN;
|
||||
break;
|
||||
}
|
||||
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
}
|
||||
|
||||
public static MusicControlMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
MusicControlCapabilitiesMessage.GarminMusicControlCommand command = commands[reader.readByte()];
|
||||
|
||||
return new MusicControlMessage(garminMessage, command);
|
||||
}
|
||||
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.singletonList(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationControlStatusMessage;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler.NotificationCommand.GET_NOTIFICATION_ATTRIBUTES;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler.NotificationCommand.PERFORM_LEGACY_NOTIFICATION_ACTION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler.NotificationCommand.PERFORM_NOTIFICATION_ACTION;
|
||||
|
||||
public class NotificationControlMessage extends GFDIMessage {
|
||||
|
||||
private final NotificationsHandler.NotificationCommand command;
|
||||
private final int notificationId;
|
||||
private Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap;
|
||||
private NotificationsHandler.LegacyNotificationAction legacyNotificationAction;
|
||||
private NotificationsHandler.NotificationAction notificationAction;
|
||||
private String actionString;
|
||||
private GBDeviceEvent deviceEvent;
|
||||
|
||||
public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, NotificationsHandler.NotificationAction notificationAction, String actionString) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.command = command;
|
||||
this.notificationId = notificationId;
|
||||
this.notificationAction = notificationAction;
|
||||
this.actionString = actionString;
|
||||
|
||||
this.statusMessage = new NotificationControlStatusMessage(garminMessage, GFDIMessage.Status.ACK, NotificationControlStatusMessage.NotificationChunkStatus.OK, NotificationControlStatusMessage.NotificationStatusCode.NO_ERROR);
|
||||
|
||||
}
|
||||
|
||||
public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, NotificationsHandler.LegacyNotificationAction legacyNotificationAction) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.command = command;
|
||||
this.notificationId = notificationId;
|
||||
this.legacyNotificationAction = legacyNotificationAction;
|
||||
|
||||
this.statusMessage = new NotificationControlStatusMessage(garminMessage, GFDIMessage.Status.ACK, NotificationControlStatusMessage.NotificationChunkStatus.OK, NotificationControlStatusMessage.NotificationStatusCode.NO_ERROR);
|
||||
}
|
||||
|
||||
public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.command = command;
|
||||
this.notificationId = notificationId;
|
||||
this.notificationAttributesMap = notificationAttributesMap;
|
||||
|
||||
this.statusMessage = new NotificationControlStatusMessage(garminMessage, GFDIMessage.Status.ACK, NotificationControlStatusMessage.NotificationChunkStatus.OK, NotificationControlStatusMessage.NotificationStatusCode.NO_ERROR);
|
||||
}
|
||||
|
||||
//TODO: the fact that we return three versions of this object is really ugly
|
||||
public static NotificationControlMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
|
||||
final NotificationsHandler.NotificationCommand command = NotificationsHandler.NotificationCommand.fromCode(reader.readByte());
|
||||
|
||||
final int notificationId = reader.readInt();
|
||||
if (command == GET_NOTIFICATION_ATTRIBUTES) {
|
||||
final Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = createGetNotificationAttributesCommand(reader);
|
||||
return new NotificationControlMessage(garminMessage, command, notificationId, notificationAttributesMap);
|
||||
} else if (command == PERFORM_LEGACY_NOTIFICATION_ACTION) {
|
||||
NotificationsHandler.LegacyNotificationAction[] values = NotificationsHandler.LegacyNotificationAction.values();
|
||||
final NotificationsHandler.LegacyNotificationAction legacyNotificationAction = values[reader.readByte()];
|
||||
return new NotificationControlMessage(garminMessage, command, notificationId, legacyNotificationAction);
|
||||
} else if (command == PERFORM_NOTIFICATION_ACTION) {
|
||||
final int actionId = reader.readByte();
|
||||
final NotificationsHandler.NotificationAction notificationAction = NotificationsHandler.NotificationAction.fromCode(actionId);
|
||||
final String actionString = reader.readNullTerminatedString();
|
||||
return new NotificationControlMessage(garminMessage, command, notificationId, notificationAction, actionString);
|
||||
}
|
||||
LOG.warn("Unknown NotificationCommand in NotificationControlMessage");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<NotificationsHandler.NotificationAttribute, Integer> createGetNotificationAttributesCommand(MessageReader reader) {
|
||||
final Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = new HashMap<>();
|
||||
while (reader.remaining() > 0) {
|
||||
final int attributeID = reader.readByte();
|
||||
|
||||
final NotificationsHandler.NotificationAttribute attribute = NotificationsHandler.NotificationAttribute.getByCode(attributeID);
|
||||
LOG.info("Requested attribute: {}", attribute);
|
||||
if (attribute == null) {
|
||||
LOG.error("Unknown notification attribute {}", attributeID);
|
||||
return null;
|
||||
}
|
||||
final int maxLength;
|
||||
if (attribute.hasLengthParam) {
|
||||
maxLength = reader.readShort();
|
||||
|
||||
} else if (attribute.hasAdditionalParams) {
|
||||
maxLength = reader.readShort(); //TODO this is wrong
|
||||
// TODO: What is this??
|
||||
reader.readByte();
|
||||
|
||||
} else {
|
||||
maxLength = 0;
|
||||
}
|
||||
notificationAttributesMap.put(attribute, maxLength);
|
||||
}
|
||||
return notificationAttributesMap;
|
||||
}
|
||||
|
||||
public String getActionString() {
|
||||
return actionString;
|
||||
}
|
||||
|
||||
public void setDeviceEvent(GBDeviceEvent deviceEvent) {
|
||||
this.deviceEvent = deviceEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.singletonList(deviceEvent);
|
||||
}
|
||||
|
||||
public NotificationsHandler.LegacyNotificationAction getLegacyNotificationAction() {
|
||||
return legacyNotificationAction;
|
||||
}
|
||||
|
||||
public NotificationsHandler.NotificationAction getNotificationAction() {
|
||||
return notificationAction;
|
||||
}
|
||||
|
||||
public NotificationsHandler.NotificationCommand getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public int getNotificationId() {
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
public Map<NotificationsHandler.NotificationAttribute, Integer> getNotificationAttributesMap() {
|
||||
return notificationAttributesMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class NotificationDataMessage extends GFDIMessage {
|
||||
private final byte[] chunk;
|
||||
private final int dataOffset;
|
||||
private final int messageSize;
|
||||
private final boolean sendOutgoing;
|
||||
private final int crc;
|
||||
|
||||
public NotificationDataMessage(byte[] chunk, int messageSize, int dataOffset, int crc) {
|
||||
this(chunk, messageSize, dataOffset, crc, true);
|
||||
}
|
||||
|
||||
public NotificationDataMessage(byte[] chunk, int messageSize, int dataOffset, int crc, boolean sendOutgoing) {
|
||||
this.garminMessage = GarminMessage.NOTIFICATION_DATA;
|
||||
this.dataOffset = dataOffset;
|
||||
this.crc = crc;
|
||||
this.chunk = chunk;
|
||||
this.messageSize = messageSize;
|
||||
|
||||
this.sendOutgoing = sendOutgoing;
|
||||
}
|
||||
|
||||
public int getCrc() {
|
||||
return crc;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(garminMessage.getId());
|
||||
writer.writeShort(messageSize);
|
||||
writer.writeShort(crc);
|
||||
writer.writeShort(dataOffset);
|
||||
writer.writeBytes(chunk);
|
||||
return sendOutgoing;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationSubscriptionStatusMessage;
|
||||
|
||||
public class NotificationSubscriptionMessage extends GFDIMessage {
|
||||
|
||||
private final boolean enable;
|
||||
private final int unk;
|
||||
|
||||
public NotificationSubscriptionMessage(GarminMessage garminMessage, boolean enable, int unk) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.enable = enable;
|
||||
this.unk = unk;
|
||||
|
||||
this.statusMessage = new NotificationSubscriptionStatusMessage(Status.ACK, NotificationSubscriptionStatusMessage.NotificationStatus.OK, enable, unk);
|
||||
}
|
||||
|
||||
public static NotificationSubscriptionMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final boolean enable = reader.readByte() == 1;
|
||||
final int unk = reader.readByte();
|
||||
|
||||
return new NotificationSubscriptionMessage(garminMessage, enable, unk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
|
||||
notificationSubscriptionDeviceEvent.enable = this.enable;
|
||||
return Collections.singletonList(notificationSubscriptionDeviceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import org.apache.commons.lang3.EnumUtils;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
|
||||
public class NotificationUpdateMessage extends GFDIMessage {
|
||||
|
||||
final private NotificationUpdateType notificationUpdateType;
|
||||
final private NotificationType notificationType;
|
||||
final private int count; //how many notifications of the same type are present
|
||||
final private int notificationId;
|
||||
final private boolean hasActions;
|
||||
final private boolean useLegacyActions = false;
|
||||
|
||||
public NotificationUpdateMessage(NotificationUpdateType notificationUpdateType, NotificationType notificationType, int count, int notificationId, boolean hasActions) {
|
||||
this.garminMessage = GarminMessage.NOTIFICATION_UPDATE;
|
||||
this.notificationUpdateType = notificationUpdateType;
|
||||
this.notificationType = notificationType;
|
||||
this.count = count;
|
||||
this.notificationId = notificationId;
|
||||
this.hasActions = hasActions;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
writer.writeByte(this.notificationUpdateType.ordinal());
|
||||
writer.writeByte(getCategoryFlags(this.notificationType));
|
||||
writer.writeByte(getCategoryValue(this.notificationType));
|
||||
writer.writeByte(this.count);
|
||||
writer.writeInt(this.notificationId);
|
||||
writer.writeByte(this.useLegacyActions ? 0x00 : 0x03);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int getCategoryFlags(NotificationType notificationType) {
|
||||
EnumSet<NotificationFlag> flags = EnumSet.noneOf(NotificationFlag.class);
|
||||
if (this.hasActions && this.useLegacyActions) { //only needed for legacy actions
|
||||
flags.add(NotificationFlag.ACTION_ACCEPT);
|
||||
flags.add(NotificationFlag.ACTION_DECLINE);
|
||||
}
|
||||
|
||||
switch (notificationType.getGenericType()) {
|
||||
case "generic_phone":
|
||||
case "generic_email":
|
||||
case "generic_sms":
|
||||
case "generic_chat":
|
||||
flags.add(NotificationFlag.FOREGROUND);
|
||||
break;
|
||||
case "generic_navigation":
|
||||
case "generic_social":
|
||||
case "generic_alarm_clock":
|
||||
case "generic":
|
||||
flags.add(NotificationFlag.BACKGROUND);
|
||||
}
|
||||
return (int) EnumUtils.generateBitVector(NotificationFlag.class, flags);
|
||||
}
|
||||
|
||||
private int getCategoryValue(NotificationType notificationType) {
|
||||
switch (notificationType.getGenericType()) {
|
||||
case "generic_phone":
|
||||
return NotificationCategory.INCOMING_CALL.ordinal();
|
||||
case "generic_email":
|
||||
return NotificationCategory.EMAIL.ordinal();
|
||||
case "generic_sms":
|
||||
case "generic_chat":
|
||||
return NotificationCategory.SMS.ordinal();
|
||||
case "generic_navigation":
|
||||
return NotificationCategory.LOCATION.ordinal();
|
||||
case "generic_social":
|
||||
return NotificationCategory.SOCIAL.ordinal();
|
||||
case "generic_alarm_clock":
|
||||
case "generic":
|
||||
return NotificationCategory.OTHER.ordinal();
|
||||
}
|
||||
return NotificationCategory.OTHER.ordinal();
|
||||
}
|
||||
|
||||
public enum NotificationUpdateType {
|
||||
ADD,
|
||||
MODIFY,
|
||||
REMOVE,
|
||||
}
|
||||
|
||||
enum NotificationFlag { //was AncsEventFlag
|
||||
BACKGROUND,
|
||||
FOREGROUND,
|
||||
UNK,
|
||||
ACTION_ACCEPT, //only needed for legacy actions
|
||||
ACTION_DECLINE, //only needed for legacy actions
|
||||
|
||||
}
|
||||
|
||||
enum NotificationCategory { //was AncsCategory
|
||||
OTHER,
|
||||
INCOMING_CALL,
|
||||
MISSED_CALL,
|
||||
VOICEMAIL,
|
||||
SOCIAL,
|
||||
SCHEDULE,
|
||||
EMAIL,
|
||||
NEWS,
|
||||
HEALTH_AND_FITNESS,
|
||||
BUSINESS_AND_FINANCE,
|
||||
LOCATION,
|
||||
ENTERTAINMENT,
|
||||
SMS
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage.ProtobufChunkStatus.KEPT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage.ProtobufStatusCode.NO_ERROR;
|
||||
|
||||
public class ProtobufMessage extends GFDIMessage {
|
||||
|
||||
|
||||
private final int requestId;
|
||||
private final int dataOffset;
|
||||
private final int totalProtobufLength;
|
||||
private final int protobufDataLength;
|
||||
private final byte[] messageBytes;
|
||||
private final boolean sendOutgoing;
|
||||
|
||||
public ProtobufMessage(GarminMessage garminMessage, int requestId, int dataOffset, int totalProtobufLength, int protobufDataLength, byte[] messageBytes, boolean sendOutgoing) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.requestId = requestId;
|
||||
this.dataOffset = dataOffset;
|
||||
this.totalProtobufLength = totalProtobufLength;
|
||||
this.protobufDataLength = protobufDataLength;
|
||||
this.messageBytes = messageBytes;
|
||||
this.sendOutgoing = sendOutgoing;
|
||||
|
||||
if (isComplete()) {
|
||||
this.statusMessage = new GenericStatusMessage(garminMessage, GFDIMessage.Status.ACK);
|
||||
} else {
|
||||
this.statusMessage = new ProtobufStatusMessage(garminMessage, GFDIMessage.Status.ACK, requestId, dataOffset, KEPT, NO_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public ProtobufMessage(GarminMessage garminMessage, int requestId, int dataOffset, int totalProtobufLength, int protobufDataLength, byte[] messageBytes) {
|
||||
this(garminMessage, requestId, dataOffset, totalProtobufLength, protobufDataLength, messageBytes, true);
|
||||
}
|
||||
|
||||
public void setStatusMessage(ProtobufStatusMessage protobufStatusMessage) {
|
||||
this.statusMessage = protobufStatusMessage;
|
||||
}
|
||||
|
||||
public static ProtobufMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
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);
|
||||
|
||||
return new ProtobufMessage(garminMessage, requestID, dataOffset, totalProtobufLength, protobufDataLength, messageBytes, false);
|
||||
}
|
||||
|
||||
public int getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public GarminMessage getMessageType() {
|
||||
return garminMessage;
|
||||
}
|
||||
|
||||
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(garminMessage.getId());
|
||||
writer.writeShort(requestId);
|
||||
writer.writeInt(dataOffset);
|
||||
writer.writeInt(totalProtobufLength);
|
||||
writer.writeInt(protobufDataLength);
|
||||
writer.writeBytes(messageBytes);
|
||||
return sendOutgoing;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
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.garminMessage = GarminMessage.DEVICE_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(this.garminMessage.getId());
|
||||
writer.writeByte(settings.size());
|
||||
for (Map.Entry<GarminDeviceSetting, Object> settingPair : settings.entrySet()) {
|
||||
final GarminDeviceSetting setting = settingPair.getKey();
|
||||
writer.writeByte(setting.ordinal());
|
||||
final Object value = settingPair.getValue();
|
||||
if (value instanceof String) {
|
||||
writer.writeString((String) value);
|
||||
} else if (value instanceof Integer) {
|
||||
writer.writeByte(4);
|
||||
writer.writeInt((Integer) value);
|
||||
} else if (value instanceof Boolean) {
|
||||
writer.writeByte(1);
|
||||
writer.writeByte(Boolean.TRUE.equals(value) ? 1 : 0);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported setting value type " + value);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum GarminDeviceSetting {
|
||||
DEVICE_NAME,
|
||||
CURRENT_TIME,
|
||||
DAYLIGHT_SAVINGS_TIME_OFFSET,
|
||||
TIME_ZONE_OFFSET,
|
||||
NEXT_DAYLIGHT_SAVINGS_START,
|
||||
NEXT_DAYLIGHT_SAVINGS_END,
|
||||
AUTO_UPLOAD_ENABLED,
|
||||
WEATHER_CONDITIONS_ENABLED,
|
||||
WEATHER_ALERTS_ENABLED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import org.apache.commons.lang3.EnumUtils;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class SetFileFlagsMessage extends GFDIMessage {
|
||||
|
||||
private final int fileIndex;
|
||||
private final FileFlags flags;
|
||||
|
||||
public SetFileFlagsMessage(int fileIndex, FileFlags flags) {
|
||||
this.garminMessage = GarminMessage.SET_FILE_FLAG;
|
||||
this.fileIndex = fileIndex;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
writer.writeShort(this.fileIndex);
|
||||
writer.writeByte((int) EnumUtils.generateBitVector(FileFlags.class, this.flags));
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum FileFlags {
|
||||
UNK_00000001,
|
||||
UNK_00000010,
|
||||
UNK_00000100,
|
||||
UNK_00001000,
|
||||
ARCHIVE,
|
||||
;
|
||||
|
||||
public static EnumSet<FileFlags> fromBitMask(final int code) {
|
||||
return EnumUtils.processBitVector(FileFlags.class, code);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class SupportedFileTypesMessage extends GFDIMessage {
|
||||
|
||||
public SupportedFileTypesMessage() {
|
||||
this.garminMessage = GarminMessage.SUPPORTED_FILE_TYPES_REQUEST;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
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;
|
||||
this.garminMessage = GarminMessage.SYSTEM_EVENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
writer.writeByte(eventType.ordinal());
|
||||
if (value instanceof String) {
|
||||
writer.writeString((String) value);
|
||||
} else if (value instanceof Integer) {
|
||||
writer.writeByte((Integer) value);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported event value type " + value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public enum GarminSystemEventType {
|
||||
SYNC_COMPLETE,
|
||||
SYNC_FAIL,
|
||||
FACTORY_RESET,
|
||||
PAIR_START,
|
||||
PAIR_COMPLETE,
|
||||
PAIR_FAIL,
|
||||
HOST_DID_ENTER_FOREGROUND,
|
||||
HOST_DID_ENTER_BACKGROUND,
|
||||
SYNC_READY,
|
||||
NEW_DOWNLOAD_AVAILABLE,
|
||||
DEVICE_SOFTWARE_UPDATE,
|
||||
DEVICE_DISCONNECT,
|
||||
TUTORIAL_COMPLETE,
|
||||
SETUP_WIZARD_START,
|
||||
SETUP_WIZARD_COMPLETE,
|
||||
SETUP_WIZARD_SKIPPED,
|
||||
TIME_UPDATED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
public class UploadRequestMessage extends GFDIMessage {
|
||||
|
||||
private final int fileIndex;
|
||||
private final int size;
|
||||
private final boolean generateOutgoing;
|
||||
private final int dataOffset;
|
||||
private final int crcSeed;
|
||||
|
||||
|
||||
public UploadRequestMessage(GarminMessage garminMessage, int fileIndex, int size, int dataOffset, int crcSeed) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.fileIndex = fileIndex;
|
||||
this.size = size;
|
||||
this.dataOffset = dataOffset;
|
||||
this.crcSeed = crcSeed;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
this.generateOutgoing = false;
|
||||
}
|
||||
|
||||
public UploadRequestMessage(int fileIndex, int size) {
|
||||
this.garminMessage = GarminMessage.UPLOAD_REQUEST;
|
||||
this.fileIndex = fileIndex;
|
||||
this.size = size;
|
||||
this.dataOffset = 0;
|
||||
this.crcSeed = 0;
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
this.generateOutgoing = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
final MessageWriter writer = new MessageWriter(response);
|
||||
writer.writeShort(0); // packet size will be filled below
|
||||
writer.writeShort(this.garminMessage.getId());
|
||||
writer.writeShort(this.fileIndex);
|
||||
writer.writeInt(this.size);
|
||||
writer.writeInt(this.dataOffset);
|
||||
writer.writeShort(this.crcSeed);
|
||||
|
||||
return generateOutgoing;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent;
|
||||
|
||||
public class WeatherMessage extends GFDIMessage {
|
||||
private final WeatherRequestDeviceEvent weatherRequestDeviceEvent;
|
||||
public WeatherMessage(int format, int latitude, int longitude, int hoursOfForecast, GarminMessage garminMessage) {
|
||||
|
||||
this.garminMessage = garminMessage;
|
||||
weatherRequestDeviceEvent = new WeatherRequestDeviceEvent(format, latitude, longitude, hoursOfForecast);
|
||||
this.statusMessage = this.getStatusMessage();
|
||||
|
||||
}
|
||||
|
||||
public static WeatherMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final int format = reader.readByte();
|
||||
final int latitude = reader.readInt();
|
||||
final int longitude = reader.readInt();
|
||||
final int hoursOfForecast = reader.readByte();
|
||||
|
||||
return new WeatherMessage(format, latitude, longitude, hoursOfForecast, garminMessage);
|
||||
}
|
||||
|
||||
public List<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.singletonList(weatherRequestDeviceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
|
||||
|
||||
public class CreateFileStatusMessage extends GFDIStatusMessage {
|
||||
private final Status status;
|
||||
private final CreateStatus createStatus;
|
||||
private final FileType.FILETYPE filetype;
|
||||
private final int fileIndex;
|
||||
private final int fileNumber;
|
||||
|
||||
public CreateFileStatusMessage(GarminMessage garminMessage, Status status, CreateStatus createStatus, int fileIndex, FileType.FILETYPE filetype, int fileNumber) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.status = status;
|
||||
this.createStatus = createStatus;
|
||||
this.fileIndex = fileIndex;
|
||||
this.filetype = filetype;
|
||||
this.fileNumber = fileNumber;
|
||||
}
|
||||
|
||||
public static CreateFileStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
|
||||
if (!status.equals(Status.ACK)) {
|
||||
return null;
|
||||
}
|
||||
final CreateStatus createStatus = CreateStatus.fromId(reader.readByte());
|
||||
int fileIndex = reader.readShort();
|
||||
final int dataType = reader.readByte();
|
||||
final int subType = reader.readByte();
|
||||
final FileType.FILETYPE filetype = FileType.FILETYPE.fromDataTypeSubType(dataType, subType);
|
||||
final int fileNumber = reader.readShort();
|
||||
if (!CreateStatus.OK.equals(createStatus)) {
|
||||
LOG.warn("Received {} / {} for message {}", status, createStatus, garminMessage);
|
||||
} else {
|
||||
LOG.info("Received {} / {} for message {}", status, createStatus, garminMessage);
|
||||
}
|
||||
return new CreateFileStatusMessage(garminMessage, status, createStatus, fileIndex, filetype, fileNumber);
|
||||
}
|
||||
|
||||
public int getFileIndex() {
|
||||
return fileIndex;
|
||||
}
|
||||
|
||||
public int getFileNumber() {
|
||||
return fileNumber;
|
||||
}
|
||||
|
||||
public boolean canProceed() {
|
||||
return status.equals(Status.ACK) && createStatus.equals(CreateStatus.OK);
|
||||
}
|
||||
|
||||
public enum CreateStatus {
|
||||
OK,
|
||||
DUPLICATE,
|
||||
NO_SPACE,
|
||||
UNSUPPORTED,
|
||||
NO_SLOTS,
|
||||
NO_SPACE_FOR_TYPE,
|
||||
;
|
||||
|
||||
public static CreateStatus fromId(int id) {
|
||||
for (CreateStatus createStatus :
|
||||
CreateStatus.values()) {
|
||||
if (createStatus.ordinal() == id) {
|
||||
return createStatus;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
public class DownloadRequestStatusMessage extends GFDIStatusMessage {
|
||||
private final Status status;
|
||||
private final DownloadStatus downloadStatus;
|
||||
private final int maxFileSize;
|
||||
|
||||
public DownloadRequestStatusMessage(GarminMessage garminMessage, Status status, DownloadStatus downloadStatus, int maxFileSize) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.status = status;
|
||||
this.downloadStatus = downloadStatus;
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
public static DownloadRequestStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
|
||||
if (!status.equals(Status.ACK)) {
|
||||
return null;
|
||||
}
|
||||
final DownloadStatus downloadStatus = DownloadStatus.fromId(reader.readByte());
|
||||
final int maxFileSize = reader.readInt();
|
||||
|
||||
if (!DownloadStatus.OK.equals(downloadStatus)) {
|
||||
LOG.warn("Received {} / {} for message {}", status, downloadStatus, garminMessage);
|
||||
} else {
|
||||
LOG.info("Received {} / {} for message {}", status, downloadStatus, garminMessage);
|
||||
}
|
||||
return new DownloadRequestStatusMessage(garminMessage, status, downloadStatus, maxFileSize);
|
||||
}
|
||||
|
||||
public int getMaxFileSize() {
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
public boolean canProceed() {
|
||||
return status.equals(Status.ACK) && downloadStatus.equals(DownloadStatus.OK);
|
||||
}
|
||||
|
||||
public enum DownloadStatus { //was DownloadRequestResponseMessage
|
||||
OK,
|
||||
INDEX_UNKNOWN,
|
||||
INDEX_NOT_READABLE,
|
||||
NO_SPACE_LEFT,
|
||||
INVALID,
|
||||
NOT_READY,
|
||||
CRC_INCORRECT,
|
||||
;
|
||||
|
||||
public static DownloadStatus fromId(int id) {
|
||||
for (DownloadStatus downloadStatus :
|
||||
DownloadStatus.values()) {
|
||||
if (downloadStatus.ordinal() == id) {
|
||||
return downloadStatus;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
|
||||
public class FileTransferDataStatusMessage extends GFDIStatusMessage {
|
||||
private final Status status;
|
||||
private final TransferStatus transferStatus;
|
||||
private final int dataOffset;
|
||||
private final boolean sendOutgoing;
|
||||
public FileTransferDataStatusMessage(GarminMessage garminMessage, Status status, TransferStatus transferStatus, int dataOffset) {
|
||||
this(garminMessage, status, transferStatus, dataOffset, true);
|
||||
}
|
||||
|
||||
|
||||
public FileTransferDataStatusMessage(GarminMessage garminMessage, Status status, TransferStatus transferStatus, int dataOffset, boolean sendOutgoing) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.status = status;
|
||||
this.transferStatus = transferStatus;
|
||||
this.dataOffset = dataOffset;
|
||||
this.sendOutgoing = sendOutgoing;
|
||||
}
|
||||
|
||||
public static FileTransferDataStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
|
||||
if (!status.equals(Status.ACK)) {
|
||||
return null;
|
||||
}
|
||||
final TransferStatus transferStatus = TransferStatus.fromId(reader.readByte());
|
||||
final int dataOffset = reader.readInt();
|
||||
|
||||
if (!TransferStatus.OK.equals(transferStatus)) {
|
||||
LOG.warn("Received {} / {} for message {}", status, transferStatus, garminMessage);
|
||||
} else {
|
||||
LOG.info("Received {} / {} for message {}", status, transferStatus, garminMessage);
|
||||
}
|
||||
return new FileTransferDataStatusMessage(garminMessage, status, transferStatus, dataOffset, false);
|
||||
}
|
||||
|
||||
public int getDataOffset() {
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
public boolean canProceed() {
|
||||
return status.equals(Status.ACK) && transferStatus.equals(TransferStatus.OK);
|
||||
}
|
||||
|
||||
@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(garminMessage.getId());
|
||||
writer.writeByte(status.ordinal());
|
||||
writer.writeByte(transferStatus.ordinal());
|
||||
writer.writeInt(dataOffset);
|
||||
|
||||
return sendOutgoing;
|
||||
}
|
||||
|
||||
public enum TransferStatus {
|
||||
OK,
|
||||
RESEND,
|
||||
ABORT,
|
||||
CRC_MISMATCH,
|
||||
OFFSET_MISMATCH,
|
||||
SYNC_PAUSED,
|
||||
;
|
||||
|
||||
public static TransferStatus fromId(int id) {
|
||||
for (TransferStatus transferStatus :
|
||||
TransferStatus.values()) {
|
||||
if (transferStatus.ordinal() == id) {
|
||||
return transferStatus;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class FitDataStatusMessage extends GFDIStatusMessage {
|
||||
|
||||
private final Status status;
|
||||
private final FitDataStatusCode fitDataStatusCode;
|
||||
|
||||
public FitDataStatusMessage(GarminMessage garminMessage, Status status, FitDataStatusCode fitDataStatusCode) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.status = status;
|
||||
this.fitDataStatusCode = fitDataStatusCode;
|
||||
switch (fitDataStatusCode) {
|
||||
case APPLIED:
|
||||
LOG.info("FIT DATA RETURNED STATUS: {}", fitDataStatusCode.name());
|
||||
break;
|
||||
default:
|
||||
LOG.warn("FIT DATA RETURNED STATUS: {}", fitDataStatusCode.name());
|
||||
}
|
||||
}
|
||||
|
||||
public static FitDataStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
final int fitDataStatusCodeByte = reader.readByte();
|
||||
final FitDataStatusCode fitDataStatusCode = FitDataStatusCode.fromCode(fitDataStatusCodeByte);
|
||||
if (fitDataStatusCode == null) {
|
||||
LOG.warn("Unknown fit data status code {}", fitDataStatusCodeByte);
|
||||
return null;
|
||||
}
|
||||
return new FitDataStatusMessage(garminMessage, status, fitDataStatusCode);
|
||||
}
|
||||
|
||||
public enum FitDataStatusCode {
|
||||
APPLIED,
|
||||
NO_DEFINITION,
|
||||
MISMATCH,
|
||||
NOT_READY,
|
||||
;
|
||||
|
||||
@Nullable
|
||||
public static FitDataStatusCode fromCode(final int code) {
|
||||
for (final FitDataStatusCode fitDataStatusCode : FitDataStatusCode.values()) {
|
||||
if (fitDataStatusCode.ordinal() == code) {
|
||||
return fitDataStatusCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class FitDefinitionStatusMessage extends GFDIStatusMessage {
|
||||
|
||||
private final Status status;
|
||||
private final FitDefinitionStatusCode fitDefinitionStatusCode;
|
||||
|
||||
public FitDefinitionStatusMessage(GarminMessage garminMessage, Status status, FitDefinitionStatusCode fitDefinitionStatusCode) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.status = status;
|
||||
this.fitDefinitionStatusCode = fitDefinitionStatusCode;
|
||||
switch (fitDefinitionStatusCode) {
|
||||
case APPLIED:
|
||||
LOG.info("FIT DEFINITION RETURNED STATUS: {}", fitDefinitionStatusCode.name());
|
||||
break;
|
||||
default:
|
||||
LOG.warn("FIT DEFINITION RETURNED STATUS: {}", fitDefinitionStatusCode.name());
|
||||
}
|
||||
}
|
||||
|
||||
public static FitDefinitionStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
final int fitDefinitionStatusCodeByte = reader.readByte();
|
||||
final FitDefinitionStatusCode fitDefinitionStatusCode = FitDefinitionStatusCode.fromCode(fitDefinitionStatusCodeByte);
|
||||
if (fitDefinitionStatusCode == null) {
|
||||
LOG.warn("Unknown fit definition status code {}", fitDefinitionStatusCodeByte);
|
||||
return null;
|
||||
}
|
||||
return new FitDefinitionStatusMessage(garminMessage, status, fitDefinitionStatusCode);
|
||||
}
|
||||
|
||||
public enum FitDefinitionStatusCode {
|
||||
APPLIED,
|
||||
NOT_UNIQUE,
|
||||
OUT_OF_RANGE,
|
||||
NOT_READY,
|
||||
;
|
||||
|
||||
@Nullable
|
||||
public static FitDefinitionStatusCode fromCode(final int code) {
|
||||
for (final FitDefinitionStatusCode fitDefinitionStatusCode : FitDefinitionStatusCode.values()) {
|
||||
if (fitDefinitionStatusCode.ordinal() == code) {
|
||||
return fitDefinitionStatusCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
|
||||
|
||||
public abstract class GFDIStatusMessage extends GFDIMessage {
|
||||
private Status status;
|
||||
|
||||
public static GFDIStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
int originalMessageType = reader.readShort();
|
||||
final GarminMessage originalGarminMessage = GFDIMessage.GarminMessage.fromId(originalMessageType);
|
||||
|
||||
if (GarminMessage.PROTOBUF_REQUEST.equals(originalGarminMessage) || GarminMessage.PROTOBUF_RESPONSE.equals(originalGarminMessage)) {
|
||||
return ProtobufStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else if (GarminMessage.NOTIFICATION_DATA.equals(originalGarminMessage)) {
|
||||
return NotificationDataStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else if (GarminMessage.UPLOAD_REQUEST.equals(originalGarminMessage)) {
|
||||
return UploadRequestStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else if (GarminMessage.DOWNLOAD_REQUEST.equals(originalGarminMessage)) {
|
||||
return DownloadRequestStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else if (GarminMessage.FILE_TRANSFER_DATA.equals(originalGarminMessage)) {
|
||||
return FileTransferDataStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else if (GarminMessage.CREATE_FILE.equals(originalGarminMessage)) {
|
||||
return CreateFileStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else if (GarminMessage.SUPPORTED_FILE_TYPES_REQUEST.equals(originalGarminMessage)) {
|
||||
SupportedFileTypesStatusMessage supportedFileTypesStatusMessage = SupportedFileTypesStatusMessage.parseIncoming(reader, garminMessage);
|
||||
LOG.info("{}", supportedFileTypesStatusMessage);
|
||||
return supportedFileTypesStatusMessage;
|
||||
} else if (GarminMessage.SET_FILE_FLAG.equals(originalGarminMessage)) {
|
||||
return SetFileFlagsStatusMessage.parseIncoming(reader, garminMessage);
|
||||
} else if (GarminMessage.FIT_DEFINITION.equals(originalGarminMessage)) {
|
||||
return FitDefinitionStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else if (GarminMessage.FIT_DATA.equals(originalGarminMessage)) {
|
||||
return FitDataStatusMessage.parseIncoming(reader, originalGarminMessage);
|
||||
} else {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
|
||||
if (Status.ACK == status) {
|
||||
LOG.info("Received ACK for message {}", originalGarminMessage);
|
||||
} else {
|
||||
LOG.warn("Received {} for message {}", status, (null == originalGarminMessage) ? originalMessageType : originalGarminMessage.name());
|
||||
}
|
||||
|
||||
return new GenericStatusMessage(garminMessage, status);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
|
||||
public class GenericStatusMessage extends GFDIStatusMessage {
|
||||
|
||||
private final Status status;
|
||||
private int messageType; // for unsupported message types
|
||||
|
||||
public GenericStatusMessage(GarminMessage originalMessage, Status status) {
|
||||
this.garminMessage = originalMessage;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public GenericStatusMessage(int messageType, Status status) {
|
||||
this.messageType = messageType;
|
||||
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 != 0 ? messageType : garminMessage.getId());
|
||||
writer.writeByte(status.ordinal());
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
|
||||
public class NotificationControlStatusMessage extends GFDIStatusMessage {
|
||||
private final Status status;
|
||||
private final NotificationChunkStatus notificationChunkStatus;
|
||||
private final NotificationStatusCode notificationStatusCode;
|
||||
private final boolean sendOutgoing;
|
||||
|
||||
public NotificationControlStatusMessage(GarminMessage garminMessage, Status status, NotificationChunkStatus notificationChunkStatus, NotificationStatusCode notificationStatusCode) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.status = status;
|
||||
this.notificationChunkStatus = notificationChunkStatus;
|
||||
this.notificationStatusCode = notificationStatusCode;
|
||||
this.sendOutgoing = true;
|
||||
}
|
||||
|
||||
|
||||
@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(garminMessage.getId());
|
||||
writer.writeByte(status.ordinal());
|
||||
writer.writeByte(notificationChunkStatus.ordinal());
|
||||
writer.writeByte(notificationStatusCode.getCode());
|
||||
|
||||
return this.sendOutgoing;
|
||||
}
|
||||
|
||||
public enum NotificationChunkStatus {
|
||||
OK,
|
||||
ERROR,
|
||||
;
|
||||
|
||||
public static NotificationChunkStatus fromId(int id) {
|
||||
for (NotificationChunkStatus notificationChunkStatus :
|
||||
NotificationChunkStatus.values()) {
|
||||
if (notificationChunkStatus.ordinal() == id) {
|
||||
return notificationChunkStatus;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum NotificationStatusCode {
|
||||
NO_ERROR(0),
|
||||
UNKNOWN_COMMAND(160),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
NotificationStatusCode(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static NotificationStatusCode fromCode(final int code) {
|
||||
for (final NotificationStatusCode notificationStatusCode : NotificationStatusCode.values()) {
|
||||
if (notificationStatusCode.getCode() == code) {
|
||||
return notificationStatusCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
public class NotificationDataStatusMessage extends GFDIStatusMessage {
|
||||
private final Status status;
|
||||
private final TransferStatus transferStatus;
|
||||
|
||||
public NotificationDataStatusMessage(GarminMessage garminMessage, Status status, TransferStatus transferStatus) {
|
||||
this.garminMessage = garminMessage;
|
||||
this.status = status;
|
||||
this.transferStatus = transferStatus;
|
||||
}
|
||||
|
||||
public static NotificationDataStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
|
||||
final Status status = Status.fromCode(reader.readByte());
|
||||
|
||||
if (!status.equals(Status.ACK)) {
|
||||
return null;
|
||||
}
|
||||
final TransferStatus transferStatus = TransferStatus.fromId(reader.readByte());
|
||||
|
||||
if (!TransferStatus.OK.equals(transferStatus)) {
|
||||
LOG.warn("Received {} / {} for message {}", status, transferStatus, garminMessage);
|
||||
} else {
|
||||
LOG.info("Received {} / {} for message {}", status, transferStatus, garminMessage);
|
||||
}
|
||||
return new NotificationDataStatusMessage(garminMessage, status, transferStatus);
|
||||
}
|
||||
|
||||
public boolean canProceed() {
|
||||
return status.equals(Status.ACK) && transferStatus.equals(TransferStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean generateOutgoing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public enum TransferStatus {
|
||||
OK,
|
||||
RESEND,
|
||||
ABORT,
|
||||
CRC_MISMATCH,
|
||||
OFFSET_MISMATCH,
|
||||
;
|
||||
|
||||
public static TransferStatus fromId(int id) {
|
||||
for (TransferStatus transferStatus :
|
||||
TransferStatus.values()) {
|
||||
if (transferStatus.ordinal() == id) {
|
||||
return transferStatus;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
|
||||
|
||||
public class NotificationSubscriptionStatusMessage extends GFDIStatusMessage {
|
||||
private final Status status;
|
||||
private final NotificationStatus notificationStatus;
|
||||
private final int enableRaw;
|
||||
private final int unk;
|
||||
private final boolean sendOutgoing;
|
||||
|
||||
public NotificationSubscriptionStatusMessage(Status status, NotificationStatus notificationStatus, boolean enable, int unk) {
|
||||
this.garminMessage = GarminMessage.NOTIFICATION_SUBSCRIPTION;
|
||||
this.status = status;
|
||||
this.notificationStatus = notificationStatus;
|
||||
this.enableRaw = enable ? 1 : 0;
|
||||
this.unk = unk;
|
||||
this.sendOutgoing = true;
|
||||
}
|
||||
|
||||
@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(garminMessage.getId());
|
||||
writer.writeByte(status.ordinal());
|
||||
writer.writeByte(notificationStatus.ordinal());
|
||||
writer.writeByte(this.enableRaw);
|
||||
writer.writeByte(this.unk);
|
||||
|
||||
return this.sendOutgoing;
|
||||
}
|
||||
|
||||
public enum NotificationStatus {
|
||||
OK,
|
||||
;
|
||||
|
||||
public static NotificationStatus fromId(int id) {
|
||||
for (NotificationStatus notificationStatus :
|
||||
NotificationStatus.values()) {
|
||||
if (notificationStatus.ordinal() == id) {
|
||||
return notificationStatus;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue