From 114f6fcbf03954b93ff82fca3145839b9b5b4955 Mon Sep 17 00:00:00 2001 From: Mormegil Date: Thu, 15 Jun 2023 15:38:25 +0200 Subject: [PATCH] First piece of Garmin Vivomove HR support basics - `DeviceType` definition - Definition of activity sample table - Coordinator and sample provider --- .../gadgetbridge/daogen/GBDaoGenerator.java | 16 +- README.md | 3 +- .../devices/vivomovehr/GarminCapability.java | 142 +++++++++++ .../devices/vivomovehr/VivomoveConstants.java | 82 +++++++ .../vivomovehr/VivomoveHrCoordinator.java | 119 +++++++++ .../vivomovehr/VivomoveHrSampleProvider.java | 229 ++++++++++++++++++ .../gadgetbridge/model/DeviceType.java | 1 + .../gadgetbridge/util/DeviceHelper.java | 2 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 593 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveConstants.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrSampleProvider.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index efe479298..8c357d15a 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -90,8 +90,9 @@ public class GBDaoGenerator { addCasioGBX100Sample(schema, user, device); addFitProActivitySample(schema, user, device); addPineTimeActivitySample(schema, user, device); - addHybridHRActivitySample(schema, user, device); + addVivomoveHrActivitySample(schema, user, device); + addCalendarSyncState(schema, device); addAlarms(schema, user, device); addReminders(schema, user, device); @@ -461,6 +462,19 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addVivomoveHrActivitySample(Schema schema, Entity user, Entity device) { + final Entity activitySample = addEntity(schema, "VivomoveHrActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty("caloriesBurnt"); + activitySample.addIntProperty("floorsClimbed"); + return activitySample; + } + private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "WatchXPlusActivitySample"); activitySample.implementsSerializable(); diff --git a/README.md b/README.md index 2fef8477a..21d67d2d6 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ vendor's servers. - [Galaxy Buds Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#galaxy-buds-pro) - [Galaxy Buds2](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#galaxy-buds2) - [Galaxy Buds2 Pro](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Galaxy-Buds#galaxy-buds2-pro) - +- Garmin + - Vívomove HR - [HPlus Devices (e.g. ZeBand)](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus) - ID115 - [iTag](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/iTag) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java new file mode 100644 index 000000000..93e395cbc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java @@ -0,0 +1,142 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public enum GarminCapability { + CONNECT_MOBILE_FIT_LINK, + GOLF_FIT_LINK, + VIVOKID_JR_FIT_LINK, + SYNC, + DEVICE_INITIATES_SYNC, + HOST_INITIATED_SYNC_REQUESTS, + GNCS, + ADVANCED_MUSIC_CONTROLS, + FIND_MY_PHONE, + FIND_MY_WATCH, + CONNECTIQ_HTTP, + CONNECTIQ_SETTINGS, + CONNECTIQ_WATCH_APP_DOWNLOAD, + CONNECTIQ_WIDGET_DOWNLOAD, + CONNECTIQ_WATCH_FACE_DOWNLOAD, + CONNECTIQ_DATA_FIELD_DOWNLOAD, + CONNECTIQ_APP_MANAGEMENT, + COURSE_DOWNLOAD, + WORKOUT_DOWNLOAD, + GOLF_COURSE_DOWNLOAD, + DELTA_SOFTWARE_UPDATE_FILES, + FITPAY, + LIVETRACK, + LIVETRACK_AUTO_START, + LIVETRACK_MESSAGING, + GROUP_LIVETRACK, + WEATHER_CONDITIONS, + WEATHER_ALERTS, + GPS_EPHEMERIS_DOWNLOAD, + EXPLICIT_ARCHIVE, + SWING_SENSOR, + SWING_SENSOR_REMOTE, + INCIDENT_DETECTION, + TRUEUP, + INSTANT_INPUT, + SEGMENTS, + AUDIO_PROMPT_LAP, + AUDIO_PROMPT_PACE_SPEED, + AUDIO_PROMPT_HEART_RATE, + AUDIO_PROMPT_POWER, + AUDIO_PROMPT_NAVIGATION, + AUDIO_PROMPT_CADENCE, + SPORT_GENERIC, + SPORT_RUNNING, + SPORT_CYCLING, + SPORT_TRANSITION, + SPORT_FITNESS_EQUIPMENT, + SPORT_SWIMMING, + STOP_SYNC_AFTER_SOFTWARE_UPDATE, + CALENDAR, + WIFI_SETUP, + SMS_NOTIFICATIONS, + BASIC_MUSIC_CONTROLS, + AUDIO_PROMPTS_SPEECH, + DELTA_SOFTWARE_UPDATES, + GARMIN_DEVICE_INFO_FILE_TYPE, + SPORT_PROFILE_SETUP, + HSA_SUPPORT, + SPORT_STRENGTH, + SPORT_CARDIO, + UNION_PAY, + IPASS, + CIQ_AUDIO_CONTENT_PROVIDER, + UNION_PAY_INTERNATIONAL, + REQUEST_PAIR_FLOW, + LOCATION_UPDATE, + LTE_SUPPORT, + DEVICE_DRIVEN_LIVETRACK_SUPPORT, + CUSTOM_CANNED_TEXT_LIST_SUPPORT, + EXPLORE_SYNC, + INCIDENT_DETECT_AND_ASSISTANCE, + CURRENT_TIME_REQUEST_SUPPORT, + CONTACTS_SUPPORT, + LAUNCH_REMOTE_CIQ_APP_SUPPORT, + DEVICE_MESSAGES, + WAYPOINT_TRANSFER, + MULTI_LINK_SERVICE, + OAUTH_CREDENTIALS, + GOLF_9_PLUS_9, + ANTI_THEFT_ALARM, + INREACH, + EVENT_SHARING; + + public static final Set ALL_CAPABILITIES = new HashSet<>(values().length); + private static final Map FROM_ORDINAL = new HashMap<>(values().length); + + static { + for (GarminCapability cap : values()) { + FROM_ORDINAL.put(cap.ordinal(), cap); + ALL_CAPABILITIES.add(cap); + } + } + + public static Set setFromBinary(byte[] bytes) { + final Set result = new HashSet<>(GarminCapability.values().length); + int current = 0; + for (int b : bytes) { + for (int curr = 1; curr < 0x100; curr <<= 1) { + if ((b & curr) != 0) { + result.add(FROM_ORDINAL.get(current)); + } + ++current; + } + } + return result; + } + + public static byte[] setToBinary(Set capabilities) { + final GarminCapability[] values = values(); + final byte[] result = new byte[(values.length + 7) / 8]; + int bytePos = 0; + int bitPos = 0; + for (int i = 0; i < values.length; ++i) { + if (capabilities.contains(FROM_ORDINAL.get(i))) { + result[bytePos] |= (1 << bitPos); + } + ++bitPos; + if (bitPos >= 8) { + bitPos = 0; + ++bytePos; + } + } + return result; + } + + public static String setToString(Set capabilities) { + final StringBuilder result = new StringBuilder(); + for (GarminCapability cap : capabilities) { + if (result.length() > 0) result.append(", "); + result.append(cap.name()); + } + return result.toString(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveConstants.java new file mode 100644 index 000000000..ac33e5c42 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveConstants.java @@ -0,0 +1,82 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr; + +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +public class VivomoveConstants { + public static final UUID UUID_SERVICE_GARMIN_1 = UUID.fromString("6A4E2401-667B-11E3-949A-0800200C9A66"); + public static final UUID UUID_SERVICE_GARMIN_2 = UUID.fromString("6A4E2500-667B-11E3-949A-0800200C9A66"); + + public static final UUID UUID_CHARACTERISTIC_GARMIN_GFDI_SEND = UUID.fromString("6a4e4c80-667b-11e3-949a-0800200c9a66"); + public static final UUID UUID_CHARACTERISTIC_GARMIN_GFDI_RECEIVE = UUID.fromString("6a4ecd28-667b-11e3-949a-0800200c9a66"); + + public static final UUID UUID_CHARACTERISTIC_GARMIN_HEART_RATE = UUID.fromString("6a4e2501-667b-11e3-949a-0800200c9a66"); + public static final UUID UUID_CHARACTERISTIC_GARMIN_STEPS = UUID.fromString("6a4e2502-667b-11e3-949a-0800200c9a66"); + public static final UUID UUID_CHARACTERISTIC_GARMIN_CALORIES = UUID.fromString("6a4e2503-667b-11e3-949a-0800200c9a66"); + public static final UUID UUID_CHARACTERISTIC_GARMIN_STAIRS = UUID.fromString("6a4e2504-667b-11e3-949a-0800200c9a66"); + public static final UUID UUID_CHARACTERISTIC_GARMIN_INTENSITY = UUID.fromString("6a4e2505-667b-11e3-949a-0800200c9a66"); + public static final UUID UUID_CHARACTERISTIC_GARMIN_HEART_RATE_VARIATION = UUID.fromString("6a4e2507-667b-11e3-949a-0800200c9a66"); + // public static final UUID UUID_CHARACTERISTIC_GARMIN_STRESS = UUID.fromString("6a4e2508-667b-11e3-949a-0800200c9a66"); + public static final UUID UUID_CHARACTERISTIC_GARMIN_2_9 = UUID.fromString("6a4e2509-667b-11e3-949a-0800200c9a66"); + // public static final UUID UUID_CHARACTERISTIC_GARMIN_SPO2 = UUID.fromString("6a4e250c-667b-11e3-949a-0800200c9a66"); + // public static final UUID UUID_CHARACTERISTIC_GARMIN_RESPIRATION = UUID.fromString("6a4e250e-667b-11e3-949a-0800200c9a66"); + + // public static final UUID UUID_CLIENT_CHARACTERISTIC_CONFIGURATION = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + + public static final int STATUS_ACK = 0; + public static final int STATUS_NAK = 1; + public static final int STATUS_UNSUPPORTED = 2; + public static final int STATUS_DECODE_ERROR = 3; + public static final int STATUS_CRC_ERROR = 4; + public static final int STATUS_LENGTH_ERROR = 5; + + public static final int GADGETBRIDGE_UNIT_NUMBER = 22222; + + public static final int GARMIN_DEVICE_XML_FILE_INDEX = 65533; + + // TODO: Better capability management/configuration + // public static final Set OUR_CAPABILITIES = new HashSet<>(Arrays.asList(GarminCapability.SYNC, GarminCapability.GNCS, GarminCapability.ADVANCED_MUSIC_CONTROLS, GarminCapability.FIND_MY_PHONE, GarminCapability.WEATHER_CONDITIONS, GarminCapability.WEATHER_ALERTS, GarminCapability.DEVICE_MESSAGES, GarminCapability.SMS_NOTIFICATIONS, GarminCapability.SYNC, GarminCapability.DEVICE_INITIATES_SYNC, GarminCapability.HOST_INITIATED_SYNC_REQUESTS, GarminCapability.CALENDAR, GarminCapability.CURRENT_TIME_REQUEST_SUPPORT)); + public static final Set OUR_CAPABILITIES = GarminCapability.ALL_CAPABILITIES; + + public static final int MAX_WRITE_SIZE = 20; + + /** + * Garmin zero time in seconds since Epoch: 1989-12-31T00:00:00Z + */ + public static final int GARMIN_TIME_EPOCH = 631065600; + + public static final SimpleDateFormat ANCS_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT); + + public static final int MESSAGE_RESPONSE = 5000; + public static final int MESSAGE_REQUEST = 5001; + public static final int MESSAGE_DOWNLOAD_REQUEST = 5002; + public static final int MESSAGE_UPLOAD_REQUEST = 5003; + public static final int MESSAGE_FILE_TRANSFER_DATA = 5004; + public static final int MESSAGE_CREATE_FILE_REQUEST = 5005; + public static final int MESSAGE_DIRECTORY_FILE_FILTER_REQUEST = 5007; + public static final int MESSAGE_FILE_READY = 5009; + public static final int MESSAGE_FIT_DEFINITION = 5011; + public static final int MESSAGE_FIT_DATA = 5012; + public static final int MESSAGE_WEATHER_REQUEST = 5014; + public static final int MESSAGE_BATTERY_STATUS = 5023; + public static final int MESSAGE_DEVICE_INFORMATION = 5024; + public static final int MESSAGE_DEVICE_SETTINGS = 5026; + public static final int MESSAGE_SYSTEM_EVENT = 5030; + public static final int MESSAGE_SUPPORTED_FILE_TYPES_REQUEST = 5031; + public static final int MESSAGE_NOTIFICATION_SOURCE = 5033; + public static final int MESSAGE_GNCS_CONTROL_POINT_REQUEST = 5034; + public static final int MESSAGE_GNCS_DATA_SOURCE = 5035; + public static final int MESSAGE_NOTIFICATION_SERVICE_SUBSCRIPTION = 5036; + public static final int MESSAGE_SYNC_REQUEST = 5037; + public static final int MESSAGE_FIND_MY_PHONE = 5039; + public static final int MESSAGE_CANCEL_FIND_MY_PHONE = 5040; + public static final int MESSAGE_MUSIC_CONTROL_CAPABILITIES = 5042; + public static final int MESSAGE_PROTOBUF_REQUEST = 5043; + public static final int MESSAGE_PROTOBUF_RESPONSE = 5044; + public static final int MESSAGE_MUSIC_CONTROL_ENTITY_UPDATE = 5049; + public static final int MESSAGE_CONFIGURATION = 5050; + public static final int MESSAGE_CURRENT_TIME_REQUEST = 5052; + public static final int MESSAGE_AUTH_NEGOTIATION = 5101; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrCoordinator.java new file mode 100644 index 000000000..d52b5612a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrCoordinator.java @@ -0,0 +1,119 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class VivomoveHrCoordinator extends AbstractDeviceCoordinator { + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + if ("vívomove HR".equals(candidate.getName())) return DeviceType.VIVOMOVE_HR; + + final boolean hasServiceUuids = candidate.getServiceUuids().length > 0; + return hasServiceUuids && candidate.supportsService(VivomoveConstants.UUID_SERVICE_GARMIN_2) ? DeviceType.VIVOMOVE_HR : DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.VIVOMOVE_HR; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new VivomoveHrSampleProvider(device, session); + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public int getAlarmSlotCount() { + return 0; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public String getManufacturer() { + return "Garmin"; + } + + @Override + public boolean supportsAppsManagement(GBDevice device) { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return true; + } + + @Override + public boolean supportsWeather() { + return true; + } + + @Override + public boolean supportsFindDevice() { + return true; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + // nothing to delete, yet + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrSampleProvider.java new file mode 100644 index 000000000..3ffa549c2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/VivomoveHrSampleProvider.java @@ -0,0 +1,229 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.VivomoveHrActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.VivomoveHrActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class VivomoveHrSampleProvider extends AbstractSampleProvider { + public static final int RAW_TYPE_KIND_MASK = 0x0F000000; + public static final int RAW_TYPE_KIND_ACTIVITY = 0x00000000; + public static final int RAW_TYPE_KIND_SLEEP = 0x01000000; + // public static final int RAW_TYPE_KIND_NOT_WORN = 0x0F000000; + public static final int RAW_NOT_WORN = 0x0F000000; + + public VivomoveHrSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public int normalizeType(int rawType) { + if (rawType == RAW_NOT_WORN) { + return ActivityKind.TYPE_NOT_WORN; + } + + switch (rawType & RAW_TYPE_KIND_MASK) { + case RAW_TYPE_KIND_ACTIVITY: + return normalizeActivityType(rawType & ~RAW_TYPE_KIND_MASK); + case RAW_TYPE_KIND_SLEEP: + return normalizeSleepType(rawType & ~RAW_TYPE_KIND_MASK); + default: + // ??? + return ActivityKind.TYPE_UNKNOWN; + } + } + + private static int normalizeActivityType(int rawType) { + switch (rawType) { + case 0: + // generic + return ActivityKind.TYPE_ACTIVITY; + case 1: + // running + return ActivityKind.TYPE_RUNNING; + case 2: + // cycling + return ActivityKind.TYPE_CYCLING; + case 3: + // transition + return ActivityKind.TYPE_ACTIVITY | ActivityKind.TYPE_RUNNING | ActivityKind.TYPE_WALKING | ActivityKind.TYPE_EXERCISE | ActivityKind.TYPE_SWIMMING; + case 4: + // fitness_equipment + return ActivityKind.TYPE_EXERCISE; + case 5: + // swimming + return ActivityKind.TYPE_SWIMMING; + case 6: + // walking + return ActivityKind.TYPE_WALKING; + case 8: + // sedentary + // TODO? + return ActivityKind.TYPE_ACTIVITY; + + default: + return ActivityKind.TYPE_UNKNOWN; + } + } + + private static int normalizeSleepType(int rawType) { + switch (rawType) { + case 0: + // deep_sleep + return ActivityKind.TYPE_DEEP_SLEEP; + case 1: + // light_sleep + return ActivityKind.TYPE_LIGHT_SLEEP; + case 2: + // awake + case 3: + // more_awake + return ActivityKind.TYPE_ACTIVITY; + default: + // ? + return ActivityKind.TYPE_UNKNOWN; + } + } + + @Override + public int toRawActivityKind(int activityKind) { + switch (activityKind) { + case ActivityKind.TYPE_NOT_WORN: + return RAW_NOT_WORN; + + case ActivityKind.TYPE_ACTIVITY: + // generic + //noinspection PointlessBitwiseExpression + return RAW_TYPE_KIND_ACTIVITY | 0; + case ActivityKind.TYPE_RUNNING: + // running + return RAW_TYPE_KIND_ACTIVITY | 1; + case ActivityKind.TYPE_CYCLING: + // cycling + return RAW_TYPE_KIND_ACTIVITY | 2; + case ActivityKind.TYPE_ACTIVITY | ActivityKind.TYPE_RUNNING | ActivityKind.TYPE_WALKING | ActivityKind.TYPE_EXERCISE | ActivityKind.TYPE_SWIMMING: + return RAW_TYPE_KIND_ACTIVITY | 3; + case ActivityKind.TYPE_EXERCISE: + // fitness_equipment + return RAW_TYPE_KIND_ACTIVITY | 4; + case ActivityKind.TYPE_SWIMMING: + // swimming + return RAW_TYPE_KIND_ACTIVITY | 5; + case ActivityKind.TYPE_WALKING: + // walking + return RAW_TYPE_KIND_ACTIVITY | 6; + case ActivityKind.TYPE_LIGHT_SLEEP: + return RAW_TYPE_KIND_SLEEP | 1; + case ActivityKind.TYPE_DEEP_SLEEP: + //noinspection PointlessBitwiseExpression + return RAW_TYPE_KIND_SLEEP | 0; + default: + if ((activityKind & ActivityKind.TYPE_SWIMMING) != 0) return RAW_TYPE_KIND_ACTIVITY | 5; + if ((activityKind & ActivityKind.TYPE_CYCLING) != 0) return RAW_TYPE_KIND_ACTIVITY | 2; + if ((activityKind & ActivityKind.TYPE_RUNNING) != 0) return RAW_TYPE_KIND_ACTIVITY | 1; + if ((activityKind & ActivityKind.TYPE_EXERCISE) != 0) return RAW_TYPE_KIND_ACTIVITY | 4; + if ((activityKind & ActivityKind.TYPE_WALKING) != 0) return RAW_TYPE_KIND_ACTIVITY | 6; + if ((activityKind & ActivityKind.TYPE_SLEEP) != 0) return RAW_TYPE_KIND_SLEEP | 1; + if ((activityKind & ActivityKind.TYPE_ACTIVITY) != 0) { + //noinspection PointlessBitwiseExpression + return RAW_TYPE_KIND_ACTIVITY | 0; + } + return 0; + } + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity / 255.0f; + } + + @Override + public VivomoveHrActivitySample createActivitySample() { + return new VivomoveHrActivitySample(); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getVivomoveHrActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return VivomoveHrActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return VivomoveHrActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return VivomoveHrActivitySampleDao.Properties.DeviceId; + } + + public static String rawKindToString(int rawType) { + if (rawType == RAW_NOT_WORN) { + return "not worn"; + } + + switch (rawType & RAW_TYPE_KIND_MASK) { + case RAW_TYPE_KIND_ACTIVITY: + return activityTypeToString(rawType & ~RAW_TYPE_KIND_MASK); + case RAW_TYPE_KIND_SLEEP: + return sleepTypeToString(rawType & ~RAW_TYPE_KIND_MASK); + default: + // ??? + return "unknown " + rawType; + } + } + + private static String activityTypeToString(int rawType) { + switch (rawType) { + case 0: + return "generic"; + case 1: + return "running"; + case 2: + return "cycling"; + case 3: + return "transition"; + case 4: + return "fitness equipment"; + case 5: + return "swimming"; + case 6: + return "walking"; + case 8: + return "sedentary"; + default: + return "unknown activity " + rawType; + } + } + + private static String sleepTypeToString(int rawType) { + switch (rawType) { + case 0: + return "deep sleep"; + case 1: + return "light sleep"; + case 2: + // awake + return "awake"; + case 3: + // more_awake + return "more awake"; + default: + return "unknown sleep " + rawType; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index c459c712e..54a625de5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -106,6 +106,7 @@ public enum DeviceType { FITPRO(230, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_fitpro), ITAG(250, R.drawable.ic_device_itag, R.drawable.ic_device_itag_disabled, R.string.devicetype_itag), NUTMINI(251, R.drawable.ic_device_itag, R.drawable.ic_device_itag_disabled, R.string.devicetype_nut_mini), + VIVOMOVE_HR(260, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_vivomove_hr), VIBRATISSIMO(300, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_vibratissimo), SONY_SWR12(310, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_sonyswr12), LIVEVIEW(320, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_liveview), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index adae99dfe..88f7b0939 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -27,6 +27,7 @@ import android.bluetooth.BluetoothDevice; import android.content.Context; import android.widget.Toast; +import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveHrCoordinator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -373,6 +374,7 @@ public class DeviceHelper { result.add(new SuperCarsCoordinator()); result.add(new AsteroidOSDeviceCoordinator()); result.add(new SoFlowCoordinator()); + result.add(new VivomoveHrCoordinator()); return result; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e689897da..289a3f7eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1269,6 +1269,7 @@ Amazfit GTS 2e Amazfit X Zepp E + Garmin Vivomove HR Vibratissimo UM-25 LiveView