diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index e6cb8ddf..d70679a1 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -60,6 +60,7 @@ public class GBDaoGenerator { addPebbleHealthActivityKindOverlay(schema, user, device); addPebbleMisfitActivitySample(schema, user, device); addPebbleMorpheuzActivitySample(schema, user, device); + addHPlusHealthActivityKindOverlay(schema, user, device); addHPlusHealthActivitySample(schema, user, device); new DaoGenerator().generateAll(schema, "app/src/main/java"); @@ -224,17 +225,34 @@ public class GBDaoGenerator { private static Entity addHPlusHealthActivitySample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "HPlusHealthActivitySample"); + activitySample.implementsSerializable(); addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); activitySample.addByteArrayProperty("rawHPlusHealthData"); - activitySample.addIntProperty("rawHPlusCalories").notNull(); - activitySample.addIntProperty("rawHPlusDistance").notNull(); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey(); activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); - activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); addHeartRateProperties(activitySample); + activitySample.addIntProperty("distance"); + activitySample.addIntProperty("calories"); + return activitySample; } + private static Entity addHPlusHealthActivityKindOverlay(Schema schema, Entity user, Entity device) { + Entity activityOverlay = addEntity(schema, "HPlusHealthActivityOverlay"); + + activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey(); + activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey(); + activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey(); + Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty(); + activityOverlay.addToOne(device, deviceId); + + Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty(); + activityOverlay.addToOne(user, userId); + activityOverlay.addByteArrayProperty("rawHPlusHealthData"); + return activityOverlay; + } + private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) { activitySample.setSuperclass(superClass); activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusConstants.java index f69bb453..5feaf6d7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusConstants.java @@ -1,12 +1,11 @@ package nodomain.freeyourgadget.gadgetbridge.devices.hplus; +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + import java.util.UUID; -/** - * Message constants reverse-engineered by João Paulo Barraca, jpbarraca@gmail.com. - * - * @author João Paulo Barraca <jpbarraca@gmail.com> - */ public final class HPlusConstants { public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("14702856-620a-3973-7c78-9cfff0876abd"); @@ -14,79 +13,95 @@ public final class HPlusConstants { public static final UUID UUID_SERVICE_HP = UUID.fromString("14701820-620a-3973-7c78-9cfff0876abd"); - public static final byte COUNTRY_CN = 1; - public static final byte COUNTRY_OTHER = 2; + public static final byte ARG_COUNTRY_CN = 1; + public static final byte ARG_COUNTRY_OTHER = 2; - public static final byte CLOCK_24H = 0; - public static final byte CLOCK_12H = 1; + public static final byte ARG_TIMEMODE_24H = 0; + public static final byte ARG_TIMEMODE_12H = 1; - public static final byte UNIT_METRIC = 0; - public static final byte UNIT_IMPERIAL = 1; + public static final byte ARG_UNIT_METRIC = 0; + public static final byte ARG_UNIT_IMPERIAL = 1; - public static final byte SEX_MALE = 0; - public static final byte SEX_FEMALE = 1; + public static final byte ARG_GENDER_MALE = 0; + public static final byte ARG_GENDER_FEMALE = 1; - public static final byte HEARTRATE_MEASURE_ON = 11; - public static final byte HEARTRATE_MEASURE_OFF = 22; + public static final byte ARG_HEARTRATE_MEASURE_ON = 11; + public static final byte ARG_HEARTRATE_MEASURE_OFF = 22; - public static final byte HEARTRATE_ALLDAY_ON = 10; - public static final byte HEARTRATE_ALLDAY_OFF = -1; + public static final byte ARG_HEARTRATE_ALLDAY_ON = 0x0A; + public static final byte ARG_HEARTRATE_ALLDAY_OFF = (byte) 0xff; - public static final byte[] COMMAND_SET_INIT1 = new byte[]{0x50,0x00,0x25,(byte) 0xb1,0x4a,0x00,0x00,0x27,0x10,0x05,0x02,0x00,(byte) 0xff,0x0a,(byte) 0xff,0x00,(byte) 0xff,(byte) 0xff,0x00,0x01}; - public static final byte[] COMMAND_SET_INIT2 = new byte[]{0x51,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,(byte) 0xe0,0x0c,0x12,0x16,0x0a,0x10,0x00,0x00,0x00,0x00}; + public static final byte INCOMING_CALL_STATE_DISABLED_THRESHOLD = 0x7B; + public static final byte INCOMING_CALL_STATE_ENABLED = (byte) 0xAA; - public static final byte[] COMMAND_SET_PREF_START = new byte[]{0x4f, 0x5a}; - public static final byte[] COMMAND_SET_PREF_START1 = new byte[]{0x4d}; + public static final byte[] CMD_SET_PREF_START = new byte[]{0x4f, 0x5a}; + public static final byte[] CMD_SET_PREF_START1 = new byte[]{0x4d}; + public static final byte CMD_SET_ALARM = 0x4c; + public static final byte CMD_SET_LANGUAGE = 0x22; + public static final byte CMD_SET_TIMEMODE = 0x47; + public static final byte CMD_SET_UNITS = 0x48; + public static final byte CMD_SET_GENDER = 0x2d; + public static final byte CMD_SET_DATE = 0x08; + public static final byte CMD_SET_TIME = 0x09; + public static final byte CMD_SET_WEEK = 0x2a; + public static final byte CMD_SET_PREF_SIT = 0x1e; + public static final byte CMD_SET_WEIGHT = 0x05; + public static final byte CMD_HEIGHT = 0x04; + public static final byte CMD_SET_AGE = 0x2c; + public static final byte CMD_SET_GOAL = 0x26; + public static final byte CMD_SET_SCREENTIME = 0x0b; + public static final byte CMD_SET_BLOOD = 0x4e; //?? - public static final byte COMMAND_SET_PREF_COUNTRY = 0x22; - public static final byte COMMAND_SET_PREF_TIMEMODE = 0x47; - public static final byte COMMAND_SET_PREF_UNIT = 0x48; - public static final byte COMMAND_SET_PREF_SEX = 0x2d; + public static final byte CMD_SET_FINDME = 0x0a; + public static final byte ARG_FINDME_ON = 0x01; + public static final byte ARG_FINDME_OFF = 0x02; - public static final byte COMMAND_SET_PREF_DATE = 0x08; - public static final byte COMMAND_SET_PREF_TIME = 0x09; - public static final byte COMMAND_SET_PREF_WEEK = 0x2a; - public static final byte COMMAND_SET_PREF_SIT = 0x1e; - public static final byte COMMAND_SET_PREF_WEIGHT = 0x05; - public static final byte COMMAND_SET_PREF_HEIGHT = 0x04; - public static final byte COMMAND_SET_PREF_AGE = 0x2c; - public static final byte COMMAND_SET_PREF_GOAL = 0x26; - public static final byte COMMAND_SET_PREF_SCREENTIME = 0x0b; - public static final byte COMMAND_SET_PREF_BLOOD = 0x4e; //?? - public static final byte COMMAND_SET_PREF_FINDME = 0x0a; - public static final byte COMMAND_SET_PREF_SAVE = 0x17; - public static final byte COMMAND_SET_PREF_END = 0x4f; - public static final byte COMMAND_SET_DISPLAY_ALERT = 0x23; - public static final byte COMMAND_SET_PREF_ALLDAYHR = 53; + public static final byte CMD_GET_VERSION = 0x17; + public static final byte CMD_SET_END = 0x4f; + public static final byte CMD_SET_INCOMING_CALL_NUMBER = 0x23; + public static final byte CMD_SET_ALLDAY_HRM = 0x35; + public static final byte CMD_ACTION_INCOMING_CALL = 0x41; + public static final byte CMD_SET_CONF_END = 0x4f; + public static final byte CMD_SET_PREFS = 0x50; + public static final byte CMD_SET_SIT_INTERVAL = 0x51; + public static final byte CMD_SET_HEARTRATE_STATE = 0x32; - public static final byte COMMAND_SET_INCOMING_CALL = 0x41; - public static final byte[] COMMAND_FACTORY_RESET = new byte[] {-74, 90}; + //Actions to device + public static final byte CMD_GET_ACTIVE_DAY = 0x27; + public static final byte CMD_GET_DAY_DATA = 0x15; + public static final byte CMD_GET_SLEEP = 0x19; + public static final byte CMD_GET_CURR_DATA = 0x16; + public static final byte CMD_GET_DEVICE_ID = 0x24; - public static final byte COMMAND_SET_CONF_SAVE = 0x17; - public static final byte COMMAND_SET_CONF_END = 0x4f; + public static final byte CMD_ACTION_INCOMING_SOCIAL = 0x31; + //public static final byte COMMAND_ACTION_INCOMING_SMS = 0x40; //Unknown + public static final byte CMD_ACTION_DISPLAY_TEXT = 0x43; - public static final byte COMMAND_SET_PREFS = 0x50; - public static final byte COMMAND_SET_SIT_INTERVAL = 0x51; + public static final byte CMD_ACTION_DISPLAY_TEXT_NAME = 0x3F; + public static final byte CMD_ACTION_DISPLAY_TEXT_NAME_CN = 0x3E; //Text in GB2312? + public static final byte[] CMD_ACTION_HELLO = new byte[]{0x01, 0x00}; + public static final byte CMD_SHUTDOWN = 0x5B; + public static final byte ARG_SHUTDOWN_EN = 0x5A; + public static final byte CMD_FACTORY_RESET = -74; + public static final byte ARG_FACTORY_RESET_EN = 0x5A; + public static final byte CMD_SET_INCOMING_MESSAGE = 0x07; + public static final byte CMD_SET_INCOMING_CALL = 0x06; + public static final byte ARG_INCOMING_CALL = (byte) -86; + public static final byte ARG_INCOMING_MESSAGE = (byte) -86; + //Incoming Messages public static final byte DATA_STATS = 0x33; public static final byte DATA_STEPS = 0x36; - + public static final byte DATA_DAY_SUMMARY = 0x38; + public static final byte DATA_DAY_SUMMARY_ALT = 0x39; public static final byte DATA_SLEEP = 0x1A; - - public static final byte COMMAND_ACTION_INCOMING_SOCIAL = 0x31; - public static final byte COMMAND_ACTION_INCOMING_SMS = 0x40; - public static final byte COMMAND_ACTION_DISPLAY_TEXT = 0x43; - public static final byte[] COMMAND_ACTION_INCOMING_CALL = new byte[] {6, -86}; - public static final byte COMMAND_ACTION_DISPLAY_TEXT_CENTER = 0x23; - public static final byte COMMAND_ACTION_DISPLAY_TEXT_NAME = 0x3F; - public static final byte COMMAND_ACTION_DISPLAY_TEXT_NAME_CN = 0x3E; //Text in GB2312? - - + public static final byte DATA_VERSION = 0x18; public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime"; public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr"; + public static final String PREF_HPLUS_HR = "hplus_hr_enable"; public static final String PREF_HPLUS_UNIT = "hplus_unit"; public static final String PREF_HPLUS_TIMEMODE = "hplus_timemode"; public static final String PREF_HPLUS_WRIST = "hplus_wrist"; @@ -95,5 +110,4 @@ public final class HPlusConstants { public static final String PREF_HPLUS_SIT_START_TIME = "hplus_sit_start_time"; public static final String PREF_HPLUS_SIT_END_TIME = "hplus_sit_end_time"; public static final String PREF_HPLUS_COUNTRY = "hplus_country"; - } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java index d1cf8e25..8a596d4c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java @@ -13,6 +13,7 @@ import android.os.Build; import android.os.ParcelUuid; import android.support.annotation.NonNull; +import de.greenrobot.dao.query.QueryBuilder; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; @@ -20,9 +21,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.UserInfo; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -44,20 +45,15 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator { @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Collection createBLEScanFilters() { - ParcelUuid mi2Service = new ParcelUuid(HPlusConstants.UUID_SERVICE_HP); - ScanFilter filter = new ScanFilter.Builder().setServiceUuid(mi2Service).build(); + ParcelUuid hpService = new ParcelUuid(HPlusConstants.UUID_SERVICE_HP); + ScanFilter filter = new ScanFilter.Builder().setServiceUuid(hpService).build(); return Collections.singletonList(filter); } @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { - if (candidate.supportsService(HPlusConstants.UUID_SERVICE_HP)) { - return DeviceType.HPLUS; - } - String name = candidate.getDevice().getName(); - LOG.debug("Looking for: " + name); if (name != null && name.startsWith("HPLUS")) { return DeviceType.HPLUS; } @@ -97,7 +93,7 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator { @Override public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { - return new HPlusSampleProvider(device, session); + return new HPlusHealthSampleProvider(device, session); } @Override @@ -137,7 +133,9 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator { @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { - // nothing to delete, yet + Long deviceId = device.getId(); + QueryBuilder qb = session.getHPlusHealthActivitySampleDao().queryBuilder(); + qb.where(HPlusHealthActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities(); } public static int getFitnessGoal(String address) throws IllegalArgumentException { @@ -177,10 +175,13 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator { return (byte) (activityUser.getAge() & 0xFF); } - public static byte getUserSex(String address) { + public static byte getUserGender(String address) { ActivityUser activityUser = new ActivityUser(); - return (byte) (activityUser.getGender() & 0xFF); + if (activityUser.getGender() == ActivityUser.GENDER_MALE) + return HPlusConstants.ARG_GENDER_MALE; + + return HPlusConstants.ARG_GENDER_FEMALE; } public static int getGoal(String address) { @@ -194,7 +195,11 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator { } public static byte getAllDayHR(String address) { - return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + address, 10) & 0xFF); + return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + address, HPlusConstants.ARG_HEARTRATE_ALLDAY_ON) & 0xFF); + } + + public static byte getHRState(String address) { + return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_HR + "_" + address, HPlusConstants.ARG_HEARTRATE_MEASURE_ON) & 0xFF); } public static byte getSocial(String address) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusHealthSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusHealthSampleProvider.java new file mode 100644 index 00000000..9a7fa8c1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusHealthSampleProvider.java @@ -0,0 +1,207 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + +import android.support.annotation.NonNull; +import android.util.Log; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.List; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import de.greenrobot.dao.query.QueryBuilder; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlay; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlayDao; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusDataRecord; + +public class HPlusHealthSampleProvider extends AbstractSampleProvider { + + private GBDevice mDevice; + private DaoSession mSession; + + public HPlusHealthSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + + mSession = session; + mDevice = device; + } + + public int getID() { + + return SampleProvider.PROVIDER_HPLUS; + } + + public int normalizeType(int rawType) { + switch (rawType){ + case HPlusDataRecord.TYPE_DAY_SLOT: + case HPlusDataRecord.TYPE_DAY_SUMMARY: + case HPlusDataRecord.TYPE_REALTIME: + case HPlusDataRecord.TYPE_SLEEP: + case HPlusDataRecord.TYPE_UNKNOWN: + return ActivityKind.TYPE_UNKNOWN; + default: + return rawType; + } + } + + public int toRawActivityKind(int activityKind) { + switch (activityKind){ + case ActivityKind.TYPE_DEEP_SLEEP: + return ActivityKind.TYPE_DEEP_SLEEP; + case ActivityKind.TYPE_LIGHT_SLEEP: + return ActivityKind.TYPE_LIGHT_SLEEP; + default: + return HPlusDataRecord.TYPE_DAY_SLOT; + } + + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return HPlusHealthActivitySampleDao.Properties.Timestamp; + } + + @Override + public HPlusHealthActivitySample createActivitySample() { + return new HPlusHealthActivitySample(); + } + + @Override + protected Property getRawKindSampleProperty() { + return null; // HPlusHealthActivitySampleDao.Properties.RawKind; + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity / (float) 100.0; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return HPlusHealthActivitySampleDao.Properties.DeviceId; + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getHPlusHealthActivitySampleDao(); + } + + + public List getActivityamples(int timestamp_from, int timestamp_to) { + return getAllActivitySamples(timestamp_from, timestamp_to); + } + + public List getSleepSamples(int timestamp_from, int timestamp_to) { + return getAllActivitySamples(timestamp_from, timestamp_to); + } + + @NonNull + @Override + public List getAllActivitySamples(int timestamp_from, int timestamp_to) { + List samples = super.getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL); + + Device dbDevice = DBHelper.findDevice(getDevice(), getSession()); + if (dbDevice == null) { + return Collections.emptyList(); + } + + QueryBuilder qb = getSession().getHPlusHealthActivityOverlayDao().queryBuilder(); + + qb.where(HPlusHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()), + HPlusHealthActivityOverlayDao.Properties.TimestampFrom.ge(timestamp_from - 3600 * 24), + HPlusHealthActivityOverlayDao.Properties.TimestampTo.le(timestamp_to), + HPlusHealthActivityOverlayDao.Properties.TimestampTo.ge(timestamp_from)); + + List overlayRecords = qb.build().list(); + + //Todays sample steps will come from the Day Slots messages + //Historical steps will be provided by Day Summaries messages + //This will allow both week and current day results to be consistent + Calendar today = GregorianCalendar.getInstance(); + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + + int stepsToday = 0; + for(HPlusHealthActivitySample sample: samples){ + if(sample.getTimestamp() >= today.getTimeInMillis() / 1000){ + //Only consider these for the current day as a single message is enough for steps + //HR and Overlays will still benefit from the full set of samples + if(sample.getRawKind() == HPlusDataRecord.TYPE_REALTIME) { + int aux = sample.getSteps(); + sample.setSteps(sample.getSteps() - stepsToday); + stepsToday = aux; + }else + sample.setSteps(ActivitySample.NOT_MEASURED); + }else{ + if (sample.getRawKind() != HPlusDataRecord.TYPE_DAY_SUMMARY) { + sample.setSteps(ActivityKind.TYPE_NOT_MEASURED); + } + } + } + + for (HPlusHealthActivityOverlay overlay : overlayRecords) { + + //Create fake events to improve activity counters if there are no events around the overlay + //timestamp boundaries + insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId()); + insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId()); + for (HPlusHealthActivitySample sample : samples) { + + if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) { + sample.setRawKind(overlay.getRawKind()); + } + } + } + + detachFromSession(); + + Collections.sort(samples, new Comparator() { + public int compare(HPlusHealthActivitySample one, HPlusHealthActivitySample other) { + return one.getTimestamp() - other.getTimestamp(); + } + }); + + return samples; + } + + private List insertVirtualItem(List samples, int timestamp, long deviceId, long userId) { + HPlusHealthActivitySample sample = new HPlusHealthActivitySample( + timestamp, // ts + deviceId, + userId, // User id + null, // Raw Data + ActivityKind.TYPE_UNKNOWN, + 0, // Intensity + ActivitySample.NOT_MEASURED, // Steps + ActivitySample.NOT_MEASURED, // HR + ActivitySample.NOT_MEASURED, // Distance + ActivitySample.NOT_MEASURED // Calories + ); + + sample.setProvider(this); + samples.add(sample); + + return samples; + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusSampleProvider.java deleted file mode 100644 index a26f6be2..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusSampleProvider.java +++ /dev/null @@ -1,82 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.devices.hplus; - - -/* -* @author João Paulo Barraca <jpbarraca@gmail.com> -*/ - -import android.content.Context; -import android.support.annotation.NonNull; - -import java.util.Collections; -import java.util.List; - -import de.greenrobot.dao.AbstractDao; -import de.greenrobot.dao.query.QueryBuilder; -import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; -import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.entities.Device; -import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample; -import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDao; -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; - -public class HPlusSampleProvider extends AbstractSampleProvider { - - - private GBDevice mDevice; - private DaoSession mSession; - - public HPlusSampleProvider(GBDevice device, DaoSession session) { - super(device, session); - - mSession = session; - mDevice = device;; - } - - public int getID() { - return SampleProvider.PROVIDER_HPLUS; - } - - public int normalizeType(int rawType) { - return rawType; - } - - public int toRawActivityKind(int activityKind) { - return activityKind; - } - - @NonNull - @Override - protected de.greenrobot.dao.Property getTimestampSampleProperty() { - return HPlusHealthActivitySampleDao.Properties.Timestamp; - } - - @Override - public HPlusHealthActivitySample createActivitySample() { - return new HPlusHealthActivitySample(); - } - - @Override - protected de.greenrobot.dao.Property getRawKindSampleProperty() { - return HPlusHealthActivitySampleDao.Properties.RawKind; - } - - @Override - public float normalizeIntensity(int rawIntensity) { - return rawIntensity; //TODO: Calculate actual value - } - - @NonNull - @Override - protected de.greenrobot.dao.Property getDeviceIdentifierSampleProperty() { - return HPlusHealthActivitySampleDao.Properties.DeviceId; - } - - @Override - public AbstractDao getSampleDao() { - return getSession().getHPlusHealthActivitySampleDao(); - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecord.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecord.java new file mode 100644 index 00000000..b116b00a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecord.java @@ -0,0 +1,66 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + + +public class HPlusDataRecord { + public final static int TYPE_UNKNOWN = 0; + public final static int TYPE_SLEEP = 100; + public final static int TYPE_DAY_SUMMARY = 101; + public final static int TYPE_DAY_SLOT = 102; + public final static int TYPE_REALTIME = 103; + + public int type = TYPE_UNKNOWN; + public int activityKind = ActivityKind.TYPE_UNKNOWN; + + /** + * Time of this record in seconds + */ + public int timestamp; + + /** + * Raw data as sent from the device + */ + public byte[] rawData; + + protected HPlusDataRecord(){ + + } + + protected HPlusDataRecord(byte[] data, int type){ + this.rawData = data; + this.type = type; + } + + public byte[] getRawData() { + + return rawData; + } + + public class RecordInterval { + /** + * Start time of this interval in seconds + */ + public int timestampFrom; + + /** + * End time of this interval in seconds + */ + public int timestampTo; + + /** + * Type of activity {@link ActivityKind} + */ + public int activityKind; + + RecordInterval(int timestampFrom, int timestampTo, int activityKind) { + this.timestampFrom = timestampFrom; + this.timestampTo = timestampTo; + this.activityKind = activityKind; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordDaySlot.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordDaySlot.java new file mode 100644 index 00000000..c742e046 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordDaySlot.java @@ -0,0 +1,84 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + + +public class HPlusDataRecordDaySlot extends HPlusDataRecord { + + /** + * The device reports data aggregated in slots. + * There are 144 slots in a given day, summarizing 10 minutes of data + * Integer with the slot number from 0 to 143 + */ + public int slot; + + /** + * Number of steps + */ + public int steps; + + /** + * Number of seconds without activity (TBC) + */ + public int secondsInactive; + + /** + * Average Heart Rate in Beats Per Minute + */ + public int heartRate; + + public HPlusDataRecordDaySlot(byte[] data) { + super(data, TYPE_DAY_SLOT); + + int a = (data[4] & 0xFF) * 256 + (data[5] & 0xFF); + if (a >= 144) { + throw new IllegalArgumentException("Invalid Slot Number"); + } + + slot = a; + heartRate = data[1] & 0xFF; + + if(heartRate == 255) + heartRate = ActivityKind.TYPE_NOT_MEASURED; + + steps = (data[2] & 0xFF) * 256 + data[3] & 0xFF; + + //?? data[6]; atemp?? always 0 + secondsInactive = data[7] & 0xFF; // ? + + Calendar slotTime = GregorianCalendar.getInstance(); + + slotTime.set(Calendar.MINUTE, (slot % 6) * 10); + slotTime.set(Calendar.HOUR_OF_DAY, slot / 6); + slotTime.set(Calendar.SECOND, 0); + + timestamp = (int) (slotTime.getTimeInMillis() / 1000L); + } + + public String toString(){ + Calendar slotTime = GregorianCalendar.getInstance(); + slotTime.setTimeInMillis(timestamp * 1000L); + return String.format(Locale.US, "Slot: %d, Time: %s, Steps: %d, InactiveSeconds: %d, HeartRate: %d", slot, slotTime.getTime(), steps, secondsInactive, heartRate); + } + + public void add(HPlusDataRecordDaySlot other){ + if(other == null) + return; + + steps += other.steps; + secondsInactive += other.secondsInactive; + if(heartRate == -1) + heartRate = other.heartRate; + else if(other.heartRate != -1) { + heartRate = (heartRate + other.heartRate) / 2; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordDaySummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordDaySummary.java new file mode 100644 index 00000000..e28960f9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordDaySummary.java @@ -0,0 +1,101 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; + + +class HPlusDataRecordDaySummary extends HPlusDataRecord{ + + /** + * Year of the record reported by the device + * Sometimes the device will report a low number (e.g, 116) which will be "corrected" + * by adding 1900 + */ + public int year; + + /** + * Month of the record reported by the device from 1 to 12 + */ + public int month; + + /** + * Day of the record reported by the device + */ + public int day; + + /** + * Number of steps accumulated in the day reported + */ + public int steps; + + /** + * Distance in meters accumulated in the day reported + */ + public int distance; + + /** + * Amount of time active in the day (Units are To Be Determined) + */ + public int activeTime; + + /** + * Max Heart Rate recorded in Beats Per Minute + */ + public int maxHeartRate; + + /** + * Min Heart Rate recorded in Beats Per Minute + */ + public int minHeartRate; + + /** + * Amount of estimated calories consumed during the day in KCalories + */ + public int calories; + + HPlusDataRecordDaySummary(byte[] data) { + super(data, TYPE_DAY_SUMMARY); + + year = (data[10] & 0xFF) * 256 + (data[9] & 0xFF); + month = data[11] & 0xFF; + day = data[12] & 0xFF; + + //Recover from bug in firmware where year is corrupted + //data[10] will be set to 0, effectively offsetting values by minus 1900 years + if(year < 1900) + year += 1900; + + if (year < 2000 || month > 12 || day > 31) { + throw new IllegalArgumentException("Invalid record date "+year+"-"+month+"-"+day); + } + steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF); + distance = ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)) * 10; + activeTime = (data[14] & 0xFF) * 256 + (data[13] & 0xFF); + calories = (data[6] & 0xFF) * 256 + (data[5] & 0xFF); + calories += (data[8] & 0xFF) * 256 + (data[7] & 0xFF); + + maxHeartRate = data[15] & 0xFF; + minHeartRate = data[16] & 0xFF; + + Calendar date = GregorianCalendar.getInstance(); + date.set(Calendar.YEAR, year); + date.set(Calendar.MONTH, month - 1); + date.set(Calendar.DAY_OF_MONTH, day); + date.set(Calendar.HOUR_OF_DAY, 23); + date.set(Calendar.MINUTE, 59); + date.set(Calendar.SECOND, 59); + date.set(Calendar.MILLISECOND, 999); + + timestamp = (int) (date.getTimeInMillis() / 1000); + } + + public String toString(){ + return String.format(Locale.US, "%s-%s-%s steps:%d distance:%d minHR:%d maxHR:%d calories:%d activeTime:%d", year, month, day, steps, distance,minHeartRate, maxHeartRate, calories, activeTime); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordRealtime.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordRealtime.java new file mode 100644 index 00000000..8b39f84f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordRealtime.java @@ -0,0 +1,112 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + + +import java.util.GregorianCalendar; +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + + +class HPlusDataRecordRealtime extends HPlusDataRecord { + + /** + * Distance accumulated during the day in meters + */ + public int distance; + + /** + * Calories consumed during the day in KCalories + */ + public int calories; + + /** + * Instantaneous Heart Rate measured in Beats Per Minute + */ + public int heartRate; + + /** + * Battery level from 0 to 100 + */ + public byte battery; + + /** + * Number of steps today + */ + public int steps; + + /** + * Time active (To be determined how it works) + */ + public int activeTime; + + /** + * Computing intensity + * To be calculated appropriately + */ + public int intensity; + + public HPlusDataRecordRealtime(byte[] data) { + super(data, TYPE_REALTIME); + + if (data.length < 15) { + throw new IllegalArgumentException("Invalid data packet"); + } + + timestamp = (int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000); + distance = 10 * ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)); // meters + steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF); + int x = (data[6] & 0xFF) * 256 + data[5] & 0xFF; + int y = (data[8] & 0xFF) * 256 + data[7] & 0xFF; + + battery = data[9]; + + calories = x + y; // KCal + + heartRate = data[11] & 0xFF; // BPM + activeTime = (data[14] & 0xFF * 256) + (data[13] & 0xFF); + if(heartRate == 255) { + intensity = 0; + activityKind = ActivityKind.TYPE_NOT_MEASURED; + } + else { + intensity = (int) (100 * Math.max(0, Math.min((heartRate - 60) / 120.0, 1))); // TODO: Calculate a proper value + activityKind = ActivityKind.TYPE_UNKNOWN; + } + } + + public void computeActivity(HPlusDataRecordRealtime prev){ + if(prev == null) + return; + + int deltaDistance = distance - prev.distance; + + if(deltaDistance <= 0) + return; + + int deltaTime = timestamp - prev.timestamp; + + if(deltaTime <= 0) + return; + + double speed = deltaDistance / deltaTime; + + if(speed >= 1.6) // ~6 KM/h + activityKind = ActivityKind.TYPE_ACTIVITY; + } + + public boolean same(HPlusDataRecordRealtime other){ + if(other == null) + return false; + + return steps == other.steps && distance == other.distance && calories == other.calories && heartRate == other.heartRate && battery == other.battery; + } + + public String toString(){ + return String.format(Locale.US, "Distance: %d Steps: %d Calories: %d HeartRate: %d Battery: %d ActiveTime: %d Intensity: %d", distance, steps, calories, heartRate, battery, activeTime, intensity); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordSleep.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordSleep.java new file mode 100644 index 00000000..f74db8fb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusDataRecordSleep.java @@ -0,0 +1,129 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class HPlusDataRecordSleep extends HPlusDataRecord { + + /** + * Time which the device determined to be the bed time in seconds + */ + public int bedTimeStart; + + /** + * Time which the device determined to be the end of this sleep period in seconds + */ + public int bedTimeEnd; + + /** + * Number of minutes in Deep Sleep + */ + public int deepSleepMinutes; + + /** + * Number of minutes in Light Sleep + * This is considered as Light Sleep + + */ + public int lightSleepMinutes; + + /** + * Number of minutes to start sleeping (??) + * This is considered as Light Sleep + */ + public int enterSleepMinutes; + + /** + * Number of minutes with Sleep Spindles (??) + * This is considered as Light Sleep + */ + public int spindleMinutes; + + /** + * Number of minutes in REM sleep + * This is considered as Light Sleep + + */ + public int remSleepMinutes; + + /** + * Number of wake up minutes during the sleep period + * This is not considered as a sleep activity + */ + public int wakeupMinutes; + + /** + * Number of times the user woke up + */ + public int wakeupCount; + + public HPlusDataRecordSleep(byte[] data) { + super(data, TYPE_SLEEP); + + int year = (data[2] & 0xFF) * 256 + (data[1] & 0xFF); + int month = data[3] & 0xFF; + int day = data[4] & 0xFF; + + if (year < 2000) //Attempt to recover from bug from device. + year += 1900; + + if (year < 2000 || month > 12 || day <= 0 || day > 31) { + throw new IllegalArgumentException("Invalid record date: " + year + "-" + month + "-" + day); + } + + enterSleepMinutes = ((data[6] & 0xFF) * 256 + (data[5] & 0xFF)); + spindleMinutes = ((data[8] & 0xFF) * 256 + (data[7] & 0xFF)); + deepSleepMinutes = ((data[10] & 0xFF) * 256 + (data[9] & 0xFF)); + remSleepMinutes = ((data[12] & 0xFF) * 256 + (data[11] & 0xFF)); + wakeupMinutes = ((data[14] & 0xFF) * 256 + (data[13] & 0xFF)); + wakeupCount = ((data[16] & 0xFF) * 256 + (data[15] & 0xFF)); + + int hour = data[17] & 0xFF; + int minute = data[18] & 0xFF; + + Calendar sleepStart = GregorianCalendar.getInstance(); + sleepStart.clear(); + sleepStart.set(Calendar.YEAR, year); + sleepStart.set(Calendar.MONTH, month - 1); + sleepStart.set(Calendar.DAY_OF_MONTH, day); + sleepStart.set(Calendar.HOUR_OF_DAY, hour); + sleepStart.set(Calendar.MINUTE, minute); + sleepStart.set(Calendar.SECOND, 0); + sleepStart.set(Calendar.MILLISECOND, 0); + + bedTimeStart = (int) (sleepStart.getTimeInMillis() / 1000); + bedTimeEnd = (enterSleepMinutes + spindleMinutes + deepSleepMinutes + remSleepMinutes + wakeupMinutes) * 60 + bedTimeStart; + lightSleepMinutes = enterSleepMinutes + spindleMinutes + remSleepMinutes; + + timestamp = bedTimeStart; + } + + public List getIntervals() { + List intervals = new ArrayList<>(); + + int ts = bedTimeStart + lightSleepMinutes * 60; + intervals.add(new RecordInterval(bedTimeStart, ts, ActivityKind.TYPE_LIGHT_SLEEP)); + intervals.add(new RecordInterval(ts, bedTimeEnd, ActivityKind.TYPE_DEEP_SLEEP)); + return intervals; + } + + public String toString(){ + Calendar s = GregorianCalendar.getInstance(); + s.setTimeInMillis(bedTimeStart * 1000L); + + Calendar end = GregorianCalendar.getInstance(); + end.setTimeInMillis(bedTimeEnd * 1000L); + + return String.format(Locale.US, "Sleep start: %s end: %s enter: %d spindles: %d rem: %d deep: %d wake: %d-%d", s.getTime(), end.getTime(), enterSleepMinutes, spindleMinutes, remSleepMinutes, deepSleepMinutes, wakeupMinutes, wakeupCount); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusHandlerThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusHandlerThread.java new file mode 100644 index 00000000..7b05775d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusHandlerThread.java @@ -0,0 +1,569 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + + +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusHealthSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlay; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlayDao; +import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; + + +class HPlusHandlerThread extends GBDeviceIoThread { + private static final Logger LOG = LoggerFactory.getLogger(HPlusHandlerThread.class); + + private int CURRENT_DAY_SYNC_PERIOD = 24 * 60 * 60 * 365; //Never + private int CURRENT_DAY_SYNC_RETRY_PERIOD = 10; + + private int SLEEP_SYNC_PERIOD = 12 * 60 * 60; + private int SLEEP_SYNC_RETRY_PERIOD = 30; + + private int DAY_SUMMARY_SYNC_PERIOD = 24 * 60 * 60; + private int DAY_SUMMARY_SYNC_RETRY_PERIOD = 30; + + private int HELLO_INTERVAL = 60; + + private boolean mQuit = false; + private HPlusSupport mHPlusSupport; + + private int mLastSlotReceived = -1; + private int mLastSlotRequested = 0; + + private Calendar mLastSleepDayReceived = GregorianCalendar.getInstance(); + private Calendar mHelloTime = GregorianCalendar.getInstance(); + private Calendar mGetDaySlotsTime = GregorianCalendar.getInstance(); + private Calendar mGetSleepTime = GregorianCalendar.getInstance(); + private Calendar mGetDaySummaryTime = GregorianCalendar.getInstance(); + + private boolean mSlotsInitialSync = true; + + private HPlusDataRecordRealtime prevRealTimeRecord = null; + + private final Object waitObject = new Object(); + + List mDaySlotSamples = new ArrayList<>(); + + public HPlusHandlerThread(GBDevice gbDevice, Context context, HPlusSupport hplusSupport) { + super(gbDevice, context); + + mQuit = false; + + mHPlusSupport = hplusSupport; + } + + + @Override + public void run() { + mQuit = false; + + sync(); + + long waitTime = 0; + while (!mQuit) { + + if (waitTime > 0) { + synchronized (waitObject) { + try { + waitObject.wait(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + if (mQuit) { + break; + } + + if(!mHPlusSupport.getDevice().isConnected()){ + quit(); + break; + } + + Calendar now = GregorianCalendar.getInstance(); + + if (now.compareTo(mHelloTime) > 0) { + sendHello(); + } + + if (now.compareTo(mGetDaySlotsTime) > 0) { + requestNextDaySlots(); + } + + if (now.compareTo(mGetSleepTime) > 0) { + requestNextSleepData(); + } + + if(now.compareTo(mGetDaySummaryTime) > 0) { + requestDaySummaryData(); + } + + now = GregorianCalendar.getInstance(); + waitTime = Math.min(mGetDaySummaryTime.getTimeInMillis(), Math.min(Math.min(mGetDaySlotsTime.getTimeInMillis(), mGetSleepTime.getTimeInMillis()), mHelloTime.getTimeInMillis())) - now.getTimeInMillis(); + } + + } + + @Override + public void quit() { + mQuit = true; + synchronized (waitObject) { + waitObject.notify(); + } + } + + public void sync() { + mGetSleepTime.setTimeInMillis(0); + mGetDaySlotsTime.setTimeInMillis(0); + mGetDaySummaryTime.setTimeInMillis(0); + mLastSleepDayReceived.setTimeInMillis(0); + + mSlotsInitialSync = true; + mLastSlotReceived = -1; + mLastSlotRequested = 0; + + TransactionBuilder builder = new TransactionBuilder("startSyncDayStats"); + + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID}); + builder.wait(400); + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION}); + builder.wait(400); + + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP}); + builder.wait(400); + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA}); + builder.wait(400); + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY}); + builder.wait(400); + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA}); + + builder.queue(mHPlusSupport.getQueue()); + scheduleHello(); + + synchronized (waitObject) { + waitObject.notify(); + } + } + + /** + * Send an Hello/Null Packet to keep connection + */ + private void sendHello() { + TransactionBuilder builder = new TransactionBuilder("hello"); + + builder.write(mHPlusSupport.ctrlCharacteristic, HPlusConstants.CMD_ACTION_HELLO); + builder.queue(mHPlusSupport.getQueue()); + + scheduleHello(); + + synchronized (waitObject) { + waitObject.notify(); + } + } + + /** + * Schedule an Hello Packet in the future + */ + public void scheduleHello(){ + mHelloTime = GregorianCalendar.getInstance(); + mHelloTime.add(Calendar.SECOND, HELLO_INTERVAL); + } + + /** + * Process a message containing information regarding a day slot + * A slot summarizes 10 minutes of data + * + * @param data the message from the device + * @return boolean indicating success or fail + */ + public boolean processIncomingDaySlotData(byte[] data) { + + HPlusDataRecordDaySlot record; + + try{ + record = new HPlusDataRecordDaySlot(data); + } catch(IllegalArgumentException e){ + LOG.debug((e.getMessage())); + return false; + } + + //Ignore real time messages as they are still not understood + if(!mSlotsInitialSync){ + mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); + return true; + } + + Calendar now = GregorianCalendar.getInstance(); + int nowSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + (now.get(Calendar.MINUTE) / 10); + + //If the slot is in the future, actually it is from the previous day + //Subtract a day of seconds + if(record.slot >= nowSlot){ + record.timestamp -= 3600 * 24; + } + + //Ignore out of order messages + if(record.slot == mLastSlotReceived + 1) { + mLastSlotReceived = record.slot; + } + + if(record.slot < 143){ + mDaySlotSamples.add(record); + }else { + + //Sort the samples + Collections.sort(mDaySlotSamples, new Comparator() { + public int compare(HPlusDataRecordDaySlot one, HPlusDataRecordDaySlot other) { + return one.timestamp - other.timestamp; + } + }); + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession()); + List samples = new ArrayList<>(); + + for(HPlusDataRecordDaySlot storedRecord : mDaySlotSamples) { + HPlusHealthActivitySample sample = createSample(dbHandler, storedRecord.timestamp); + + sample.setRawHPlusHealthData(record.getRawData()); + sample.setSteps(record.steps); + sample.setHeartRate(record.heartRate); + sample.setRawKind(record.type); + + sample.setProvider(provider); + samples.add(sample); + } + + provider.getSampleDao().insertOrReplaceInTx(samples); + mDaySlotSamples.clear(); + + } catch (GBException ex) { + LOG.debug((ex.getMessage())); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } + } + //Still fetching ring buffer. Request the next slots + if (record.slot == mLastSlotRequested) { + mGetDaySlotsTime.clear(); + synchronized (waitObject) { + waitObject.notify(); + } + } + + return true; + } + + + /** + * Process sleep data from the device + * Devices send a single sleep message for each sleep period + * This message contains the duration of the sub-intervals (rem, deep, etc...) + * + * @param data the message from the device + * @return boolean indicating success or fail + */ + public boolean processIncomingSleepData(byte[] data){ + HPlusDataRecordSleep record; + + try{ + record = new HPlusDataRecordSleep(data); + } catch(IllegalArgumentException e){ + LOG.debug((e.getMessage())); + return false; + } + + mLastSleepDayReceived.setTimeInMillis(record.bedTimeStart * 1000L); + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DaoSession session = dbHandler.getDaoSession(); + Long userId = DBHelper.getUser(session).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), session).getId(); + + HPlusHealthActivityOverlayDao overlayDao = session.getHPlusHealthActivityOverlayDao(); + HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession()); + + //Get the individual Sleep overlays and insert them + List overlayList = new ArrayList<>(); + List intervals = record.getIntervals(); + + for(HPlusDataRecord.RecordInterval interval : intervals){ + overlayList.add(new HPlusHealthActivityOverlay(interval.timestampFrom, interval.timestampTo, interval.activityKind, deviceId, userId, null)); + } + + overlayDao.insertOrReplaceInTx(overlayList); + + //Store the data + HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp); + sample.setRawHPlusHealthData(record.getRawData()); + sample.setRawKind(record.activityKind); + + sample.setProvider(provider); + + provider.addGBActivitySample(sample); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } + + mGetSleepTime = GregorianCalendar.getInstance(); + mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_PERIOD); + + return true; + } + + /** + * Process a message containing real time information + * + * @param data the message from the device + * @return boolean indicating success or fail + */ + public boolean processRealtimeStats(byte[] data) { + HPlusDataRecordRealtime record; + + try{ + record = new HPlusDataRecordRealtime(data); + } catch(IllegalArgumentException e){ + LOG.debug((e.getMessage())); + return false; + } + //Skip duplicated messages as the device seems to send the same record multiple times + //This can be used to detect the user is moving (not sleeping) + if(prevRealTimeRecord != null && record.same(prevRealTimeRecord)) + return true; + + prevRealTimeRecord = record; + + getDevice().setBatteryLevel(record.battery); + + //Skip when measuring heart rate + //Calories and Distance are updated and these values will be lost. + //Because a message with a valid Heart Rate will be provided, this loss very limited + if(record.heartRate == ActivityKind.TYPE_NOT_MEASURED) { + getDevice().setFirmwareVersion2("---"); + getDevice().sendDeviceUpdateIntent(getContext()); + }else { + getDevice().setFirmwareVersion2("" + record.heartRate); + getDevice().sendDeviceUpdateIntent(getContext()); + } + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession()); + + HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp); + sample.setRawKind(record.type); + sample.setRawIntensity(record.intensity); + sample.setHeartRate(record.heartRate); + sample.setDistance(record.distance); + sample.setCalories(record.calories); + sample.setSteps(record.steps); + + sample.setRawHPlusHealthData(record.getRawData()); + sample.setProvider(provider); + + sample.setSteps(sample.getSteps() - prevRealTimeRecord.steps); + + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) + .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + + provider.addGBActivitySample(sample); + + //TODO: Handle Active Time. With Overlay? + } catch (GBException ex) { + LOG.debug((ex.getMessage())); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } + return true; + } + + /** + * Process a day summary message + * This message includes aggregates regarding an entire day + * + * @param data the message from the device + * @return boolean indicating success or fail + */ + public boolean processDaySummary(byte[] data) { + HPlusDataRecordDaySummary record; + + try{ + record = new HPlusDataRecordDaySummary(data); + } catch(IllegalArgumentException e){ + LOG.debug((e.getMessage())); + return false; + } + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession()); + + HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp); + + sample.setRawKind(record.type); + sample.setSteps(record.steps); + sample.setDistance(record.distance); + sample.setCalories(record.calories); + sample.setDistance(record.distance); + sample.setHeartRate((record.maxHeartRate - record.minHeartRate) / 2); //TODO: Find an alternative approach for Day Summary Heart Rate + sample.setRawHPlusHealthData(record.getRawData()); + + sample.setProvider(provider); + provider.addGBActivitySample(sample); + } catch (GBException ex) { + LOG.debug((ex.getMessage())); + } catch (Exception ex) { + LOG.debug(ex.getMessage()); + } + + mGetDaySummaryTime = GregorianCalendar.getInstance(); + mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_PERIOD); + return true; + } + + /** + * Process a message containing information regarding firmware version + * + * @param data the message from the device + * @return boolean indicating success or fail + */ + public boolean processVersion(byte[] data) { + int major = data[2] & 0xFF; + int minor = data[1] & 0xFF; + + getDevice().setFirmwareVersion(major + "." + minor); + + getDevice().sendDeviceUpdateIntent(getContext()); + + return true; + } + + /** + * Issue a message requesting the next batch of sleep data + */ + private void requestNextSleepData() { + TransactionBuilder builder = new TransactionBuilder("requestSleepStats"); + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP}); + builder.queue(mHPlusSupport.getQueue()); + + + mGetSleepTime = GregorianCalendar.getInstance(); + mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_RETRY_PERIOD); + } + + /** + * Issue a message requesting the next set of slots + * The process will sync 1h at a time until the device is in sync + * Then it will request samples until the end of the day in order to minimize data loss + * Messages will be provided every 10 minutes after they are available + */ + private void requestNextDaySlots() { + + Calendar now = GregorianCalendar.getInstance(); + int currentSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + now.get(Calendar.MINUTE) / 10; + + //Finished dumping the entire ring buffer + //Sync to current time + mGetDaySlotsTime = now; + + if(mSlotsInitialSync) { + if(mLastSlotReceived == 143) { + mSlotsInitialSync = false; + mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); //Sync complete. Delay timer forever + mLastSlotReceived = -1; + mLastSlotRequested = mLastSlotReceived + 1; + return; + }else { + mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_RETRY_PERIOD); + } + }else{ + //Sync complete. Delay timer forever + mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); + return; + } + + if(mLastSlotReceived == 143) + mLastSlotReceived = -1; + + byte hour = (byte) ((mLastSlotReceived + 1)/ 6); + byte minute = (byte) (((mLastSlotReceived + 1) % 6) * 10); + + byte nextHour = hour; + byte nextMinute = 59; + + mLastSlotRequested = nextHour * 6 + (nextMinute / 10); + + byte[] msg = new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY, hour, minute, nextHour, nextMinute}; + + TransactionBuilder builder = new TransactionBuilder("getNextDaySlot"); + builder.write(mHPlusSupport.ctrlCharacteristic, msg); + builder.queue(mHPlusSupport.getQueue()); + } + /** + * Request a batch of data with the summary of the previous days + */ + public void requestDaySummaryData(){ + TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary"); + builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA}); + builder.queue(mHPlusSupport.getQueue()); + + mGetDaySummaryTime = GregorianCalendar.getInstance(); + mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_RETRY_PERIOD); + } + + /** + * Helper function to create a sample + * @param dbHandler The database handler + * @param timestamp The sample timestamp + * @return The sample just created + */ + private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp){ + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + HPlusHealthActivitySample sample = new HPlusHealthActivitySample( + timestamp, // ts + deviceId, userId, // User id + null, // Raw Data + ActivityKind.TYPE_UNKNOWN, + 0, // Intensity + ActivitySample.NOT_MEASURED, // Steps + ActivitySample.NOT_MEASURED, // HR + ActivitySample.NOT_MEASURED, // Distance + ActivitySample.NOT_MEASURED // Calories + ); + + return sample; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSleepRecord.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSleepRecord.java deleted file mode 100644 index bed5d581..00000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSleepRecord.java +++ /dev/null @@ -1,86 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Calendar; - - - -public class HPlusSleepRecord { - private long bedTimeStart; - private long bedTimeEnd; - private int deepSleepSeconds; - private int spindleSeconds; - private int remSleepSeconds; - private int wakeupTime; - private int wakeupCount; - private int enterSleepSeconds; - private byte[] rawData; - - HPlusSleepRecord(byte[] data) { - rawData = data; - int year = data[2] * 256 + data[1]; - int month = data[3]; - int day = data[4]; - - enterSleepSeconds = data[6] * 256 + data[5]; - spindleSeconds = data[8] * 256 + data[7]; - deepSleepSeconds = data[10] * 256 + data[9]; - remSleepSeconds = data[12] * 256 + data[11]; - wakeupTime = data[14] * 256 + data[13]; - wakeupCount = data[16] * 256 + data[15]; - int hour = data[17]; - int minute = data[18]; - - Calendar c = Calendar.getInstance(); - c.set(Calendar.YEAR, year); - c.set(Calendar.MONTH, month); - c.set(Calendar.DAY_OF_MONTH, day); - c.set(Calendar.HOUR, hour); - c.set(Calendar.MINUTE, minute); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - - bedTimeStart = (c.getTimeInMillis() / 1000L); - bedTimeEnd = bedTimeStart + enterSleepSeconds + spindleSeconds + deepSleepSeconds + remSleepSeconds + wakeupTime; - } - - byte[] getRawData() { - - return rawData; - } - - public long getBedTimeStart() { - return bedTimeStart; - } - - public long getBedTimeEnd() { - return bedTimeEnd; - } - - public int getDeepSleepSeconds() { - return deepSleepSeconds; - } - - public int getSpindleSeconds() { - return spindleSeconds; - } - - public int getRemSleepSeconds() { - return remSleepSeconds; - } - - public int getWakeupTime() { - return wakeupTime; - } - - public int getWakeupCount() { - return wakeupCount; - } - - public int getEnterSleepSeconds() { - return enterSleepSeconds; - } - -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java index c92c8736..4689155d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java @@ -4,7 +4,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; * @author João Paulo Barraca <jpbarraca@gmail.com> */ -import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.BroadcastReceiver; @@ -21,24 +20,12 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.UUID; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.GBException; -import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; -import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusConstants; import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.entities.Device; -import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample; -import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; @@ -48,10 +35,11 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -59,12 +47,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; public class HPlusSupport extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(HPlusSupport.class); - private BluetoothGattCharacteristic ctrlCharacteristic = null; - private BluetoothGattCharacteristic measureCharacteristic = null; + public BluetoothGattCharacteristic ctrlCharacteristic = null; + public BluetoothGattCharacteristic measureCharacteristic = null; - private byte[] lastDataStats = null; - - private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + private HPlusHandlerThread syncHelper; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override @@ -86,65 +72,65 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { IntentFilter intentFilter = new IntentFilter(); broadcastManager.registerReceiver(mReceiver, intentFilter); + } @Override public void dispose() { + LOG.debug("Dispose"); LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); broadcastManager.unregisterReceiver(mReceiver); + + close(); + super.dispose(); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + LOG.debug("Initializing"); + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); measureCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE); ctrlCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_CONTROL); + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); - builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); - - getDevice().setFirmwareVersion("0"); - getDevice().setFirmwareVersion2("0"); + syncHelper = new HPlusHandlerThread(getDevice(), getContext(), this); //Initialize device - setInitValues(builder); - setCurrentDate(builder); - setCurrentTime(builder); - syncPreferences(builder); + sendUserInfo(builder); //Sync preferences + setSIT(builder); //Sync SIT Interval + setCurrentDate(builder); // Sync Current Date + setCurrentTime(builder); // Sync Current Time + + requestDeviceInfo(builder); + + setInitialized(builder); + + syncHelper.start(); builder.notify(getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE), true); - builder.setGattCallback(this); builder.notify(measureCharacteristic, true); - setInitialized(builder); return builder; } - private HPlusSupport setInitValues(TransactionBuilder builder) { - LOG.debug("Set Init Values"); - - builder.write(ctrlCharacteristic, HPlusConstants.COMMAND_SET_INIT1); - builder.write(ctrlCharacteristic, HPlusConstants.COMMAND_SET_INIT2); - return this; - } - private HPlusSupport sendUserInfo(TransactionBuilder builder) { - builder.write(ctrlCharacteristic, HPlusConstants.COMMAND_SET_PREF_START); - builder.write(ctrlCharacteristic, HPlusConstants.COMMAND_SET_PREF_START1); + builder.write(ctrlCharacteristic, HPlusConstants.CMD_SET_PREF_START); + builder.write(ctrlCharacteristic, HPlusConstants.CMD_SET_PREF_START1); syncPreferences(builder); - builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_CONF_SAVE}); - builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_CONF_END}); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_CONF_END}); return this; } private HPlusSupport syncPreferences(TransactionBuilder transaction) { - LOG.info("Attempting to sync preferences..."); - - byte sex = HPlusCoordinator.getUserSex(getDevice().getAddress()); + byte gender = HPlusCoordinator.getUserGender(getDevice().getAddress()); byte age = HPlusCoordinator.getUserAge(getDevice().getAddress()); byte bodyHeight = HPlusCoordinator.getUserHeight(getDevice().getAddress()); byte bodyWeight = HPlusCoordinator.getUserWeight(getDevice().getAddress()); @@ -168,8 +154,8 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { byte timemode = HPlusCoordinator.getTimeMode((getDevice().getAddress())); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_COUNTRY, - sex, + HPlusConstants.CMD_SET_PREFS, + gender, age, bodyHeight, bodyWeight, @@ -183,20 +169,22 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { social, allDayHeart, wrist, + 0, alertTimeHour, alertTimeMinute, unit, timemode }); + + setAllDayHeart(transaction); + return this; } - private HPlusSupport setCountry(TransactionBuilder transaction) { - LOG.info("Attempting to set country..."); - + private HPlusSupport setLanguage(TransactionBuilder transaction) { byte value = HPlusCoordinator.getCountry(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_COUNTRY, + HPlusConstants.CMD_SET_LANGUAGE, value }); return this; @@ -204,38 +192,31 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { private HPlusSupport setTimeMode(TransactionBuilder transaction) { - LOG.info("Attempting to set Time Mode..."); - byte value = HPlusCoordinator.getTimeMode(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_TIMEMODE, + HPlusConstants.CMD_SET_TIMEMODE, value }); return this; } private HPlusSupport setUnit(TransactionBuilder transaction) { - LOG.info("Attempting to set Units..."); - - byte value = HPlusCoordinator.getUnit(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_UNIT, + HPlusConstants.CMD_SET_UNITS, value }); return this; } private HPlusSupport setCurrentDate(TransactionBuilder transaction) { - LOG.info("Attempting to set Current Date..."); - - Calendar c = Calendar.getInstance(); + Calendar c = GregorianCalendar.getInstance(); int year = c.get(Calendar.YEAR) - 1900; int month = c.get(Calendar.MONTH); int day = c.get(Calendar.DAY_OF_MONTH); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_DATE, + HPlusConstants.CMD_SET_DATE, (byte) ((year / 256) & 0xff), (byte) (year % 256), (byte) (month + 1), @@ -246,12 +227,10 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { } private HPlusSupport setCurrentTime(TransactionBuilder transaction) { - LOG.info("Attempting to set Current Time..."); - - Calendar c = Calendar.getInstance(); + Calendar c = GregorianCalendar.getInstance(); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_TIME, + HPlusConstants.CMD_SET_TIME, (byte) c.get(Calendar.HOUR_OF_DAY), (byte) c.get(Calendar.MINUTE), (byte) c.get(Calendar.SECOND) @@ -262,12 +241,10 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { private HPlusSupport setDayOfWeek(TransactionBuilder transaction) { - LOG.info("Attempting to set Day Of Week..."); - - Calendar c = Calendar.getInstance(); + Calendar c = GregorianCalendar.getInstance(); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_WEEK, + HPlusConstants.CMD_SET_WEEK, (byte) c.get(Calendar.DAY_OF_WEEK) }); return this; @@ -275,15 +252,13 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { private HPlusSupport setSIT(TransactionBuilder transaction) { - LOG.info("Attempting to set SIT..."); - int startTime = HPlusCoordinator.getSITStartTime(getDevice().getAddress()); int endTime = HPlusCoordinator.getSITEndTime(getDevice().getAddress()); - Calendar now = Calendar.getInstance(); + Calendar now = GregorianCalendar.getInstance(); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_SIT_INTERVAL, + HPlusConstants.CMD_SET_SIT_INTERVAL, (byte) ((startTime / 256) & 0xff), (byte) (startTime % 256), (byte) ((endTime / 256) & 0xff), @@ -294,7 +269,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { (byte) (now.get(Calendar.YEAR) % 256), (byte) (now.get(Calendar.MONTH) + 1), (byte) (now.get(Calendar.DAY_OF_MONTH)), - (byte) (now.get(Calendar.HOUR)), + (byte) (now.get(Calendar.HOUR_OF_DAY)), (byte) (now.get(Calendar.MINUTE)), (byte) (now.get(Calendar.SECOND)), 0, @@ -307,11 +282,9 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { } private HPlusSupport setWeight(TransactionBuilder transaction) { - LOG.info("Attempting to set Weight..."); - byte value = HPlusCoordinator.getUserWeight(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_WEIGHT, + HPlusConstants.CMD_SET_WEIGHT, value }); @@ -319,11 +292,9 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { } private HPlusSupport setHeight(TransactionBuilder transaction) { - LOG.info("Attempting to set Height..."); - byte value = HPlusCoordinator.getUserHeight(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_HEIGHT, + HPlusConstants.CMD_HEIGHT, value }); @@ -332,23 +303,19 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { private HPlusSupport setAge(TransactionBuilder transaction) { - LOG.info("Attempting to set Age..."); - byte value = HPlusCoordinator.getUserAge(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_AGE, + HPlusConstants.CMD_SET_AGE, value }); return this; } - private HPlusSupport setSex(TransactionBuilder transaction) { - LOG.info("Attempting to set Sex..."); - - byte value = HPlusCoordinator.getUserSex(getDevice().getAddress()); + private HPlusSupport setGender(TransactionBuilder transaction) { + byte value = HPlusCoordinator.getUserGender(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_SEX, + HPlusConstants.CMD_SET_GENDER, value }); @@ -357,11 +324,9 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { private HPlusSupport setGoal(TransactionBuilder transaction) { - LOG.info("Attempting to set Sex..."); - int value = HPlusCoordinator.getGoal(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_GOAL, + HPlusConstants.CMD_SET_GOAL, (byte) ((value / 256) & 0xff), (byte) (value % 256) @@ -371,11 +336,9 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { private HPlusSupport setScreenTime(TransactionBuilder transaction) { - LOG.info("Attempting to set Screentime..."); - byte value = HPlusCoordinator.getScreenTime(getDevice().getAddress()); transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_SCREENTIME, + HPlusConstants.CMD_SET_SCREENTIME, value }); @@ -383,41 +346,60 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { } private HPlusSupport setAllDayHeart(TransactionBuilder transaction) { - LOG.info("Attempting to set All Day HR..."); - byte value = HPlusCoordinator.getAllDayHR(getDevice().getAddress()); + byte value = HPlusCoordinator.getHRState(getDevice().getAddress()); + transaction.write(ctrlCharacteristic, new byte[]{ - HPlusConstants.COMMAND_SET_PREF_ALLDAYHR, + HPlusConstants.CMD_SET_HEARTRATE_STATE, + value + }); + + + value = HPlusCoordinator.getAllDayHR(getDevice().getAddress()); + + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.CMD_SET_ALLDAY_HRM, value }); + return this; } - private HPlusSupport setAlarm(TransactionBuilder transaction) { - LOG.info("Attempting to set Alarm..."); - //TODO: Find how to set alarms + private HPlusSupport setAlarm(TransactionBuilder transaction, Calendar t) { + + transaction.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALARM, + (byte) (t.get(Calendar.YEAR) / 256), + (byte) (t.get(Calendar.YEAR) % 256), + (byte) (t.get(Calendar.MONTH) + 1), + (byte) t.get(Calendar.HOUR_OF_DAY), + (byte) t.get(Calendar.MINUTE), + (byte) t.get(Calendar.SECOND)}); + return this; } - private HPlusSupport setBlood(TransactionBuilder transaction) { - LOG.info("Attempting to set Blood..."); - //TODO: Find what blood means for the band - return this; - } - - - private HPlusSupport setFindMe(TransactionBuilder transaction) { - LOG.info("Attempting to set Findme..."); + private HPlusSupport setFindMe(TransactionBuilder transaction, boolean state) { //TODO: Find how this works + + byte[] msg = new byte[2]; + msg[0] = HPlusConstants.CMD_SET_FINDME; + + if (state) + msg[1] = HPlusConstants.ARG_FINDME_ON; + else + msg[1] = HPlusConstants.ARG_FINDME_OFF; + + transaction.write(ctrlCharacteristic, msg); return this; } private HPlusSupport requestDeviceInfo(TransactionBuilder builder) { - LOG.debug("Requesting Device Info!"); - BluetoothGattCharacteristic deviceName = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME); - builder.read(deviceName); + // HPlus devices seem to report some information in an alternative manner + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID}); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION}); + return this; } @@ -433,18 +415,19 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { @Override public void pair() { + LOG.debug("Pair"); } - private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { + private void handleDeviceInfo(DeviceInfo info) { LOG.warn("Device info: " + info); } @Override public void onNotification(NotificationSpec notificationSpec) { - LOG.debug("Got Notification"); - //TODO: Show different notifications acccording to source as Band supports this - showText(notificationSpec.body); + //TODO: Show different notifications according to source as Band supports this + //LOG.debug("OnNotification: Title: "+notificationSpec.title+" Body: "+notificationSpec.body+" Source: "+notificationSpec.sourceName+" Sender: "+notificationSpec.sender+" Subject: "+notificationSpec.subject); + showText(notificationSpec.title, notificationSpec.body); } @Override @@ -452,17 +435,36 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { } - @Override public void onSetTime() { TransactionBuilder builder = new TransactionBuilder("time"); setCurrentDate(builder); setCurrentTime(builder); + + builder.queue(getQueue()); } @Override public void onSetAlarms(ArrayList alarms) { + if (alarms.size() == 0) + return; + + for (Alarm alarm : alarms) { + + if (!alarm.isEnabled()) + continue; + + if (alarm.isSmartWakeup()) //Not available + continue; + + Calendar t = alarm.getAlarmCal(); + TransactionBuilder builder = new TransactionBuilder("alarm"); + setAlarm(builder, t); + builder.queue(getQueue()); + + return; //Only first alarm + } } @@ -529,78 +531,69 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { @Override public void onFetchActivityData() { - + if (syncHelper != null) + syncHelper.sync(); } @Override public void onReboot() { + getQueue().clear(); + + TransactionBuilder builder = new TransactionBuilder("Shutdown"); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SHUTDOWN, HPlusConstants.ARG_SHUTDOWN_EN}); + builder.queue(getQueue()); } @Override public void onHeartRateTest() { - LOG.debug("On HeartRateTest"); - getQueue().clear(); TransactionBuilder builder = new TransactionBuilder("HeartRateTest"); - builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_PREF_ALLDAYHR, 0x10}); //Set Real Time... ? + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_HEARTRATE_STATE, HPlusConstants.ARG_HEARTRATE_MEASURE_ON}); //Set Real Time... ? builder.queue(getQueue()); } @Override public void onEnableRealtimeHeartRateMeasurement(boolean enable) { - LOG.debug("Set Real Time HR Measurement: " + enable); - getQueue().clear(); TransactionBuilder builder = new TransactionBuilder("realTimeHeartMeasurement"); byte state; if (enable) - state = HPlusConstants.HEARTRATE_ALLDAY_ON; + state = HPlusConstants.ARG_HEARTRATE_ALLDAY_ON; else - state = HPlusConstants.HEARTRATE_ALLDAY_OFF; + state = HPlusConstants.ARG_HEARTRATE_ALLDAY_OFF; - builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_PREF_ALLDAYHR, state}); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALLDAY_HRM, state}); builder.queue(getQueue()); + } @Override public void onFindDevice(boolean start) { - LOG.debug("Find Me"); - try { TransactionBuilder builder = performInitialized("findMe"); - byte[] msg = new byte[2]; - msg[0] = HPlusConstants.COMMAND_SET_PREF_FINDME; - - if (start) - msg[1] = 1; - else - msg[1] = 0; - - builder.write(ctrlCharacteristic, msg); + setFindMe(builder, start); builder.queue(getQueue()); } catch (IOException e) { - GB.toast(getContext(), "Error toogling Find Me: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.toast(getContext(), "Error toggling Find Me: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @Override public void onSetConstantVibration(int intensity) { - LOG.debug("Vibration Trigger"); - getQueue().clear(); try { TransactionBuilder builder = performInitialized("vibration"); byte[] msg = new byte[15]; - msg[0] = HPlusConstants.COMMAND_SET_DISPLAY_ALERT; + msg[0] = HPlusConstants.CMD_SET_INCOMING_CALL_NUMBER; for (int i = 0; i < msg.length - 1; i++) msg[i + 1] = (byte) "GadgetBridge".charAt(i); @@ -619,6 +612,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { @Override public void onEnableHeartRateSleepSupport(boolean enable) { + onEnableRealtimeHeartRateMeasurement(enable); } @@ -649,96 +643,72 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { } - private void showIncomingCall(String name, String number){ - LOG.debug("Show Incoming Call"); - + private void showIncomingCall(String name, String number) { try { TransactionBuilder builder = performInitialized("incomingCallIcon"); //Enable call notifications - builder.write(ctrlCharacteristic, new byte[] {HPlusConstants.COMMAND_SET_INCOMING_CALL, 1 }); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_ACTION_INCOMING_CALL, 1}); //Show Call Icon - builder.write(ctrlCharacteristic, HPlusConstants.COMMAND_ACTION_INCOMING_CALL); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_CALL, HPlusConstants.ARG_INCOMING_CALL}); - //builder = performInitialized("incomingCallText"); builder.queue(getQueue()); - //TODO: Use WaitAction - try { - Thread.sleep(200); - } catch (InterruptedException e) { - e.printStackTrace(); - } - byte[] msg = new byte[13]; builder = performInitialized("incomingCallNumber"); + builder.wait(200); //Show call number for (int i = 0; i < msg.length; i++) msg[i] = ' '; - for(int i = 0; i < number.length() && i < (msg.length - 1); i++) + for (int i = 0; i < number.length() && i < (msg.length - 1); i++) msg[i + 1] = (byte) number.charAt(i); - msg[0] = HPlusConstants.COMMAND_ACTION_DISPLAY_TEXT_CENTER; + msg[0] = HPlusConstants.CMD_SET_INCOMING_CALL_NUMBER; builder.write(ctrlCharacteristic, msg); builder.queue(getQueue()); - try { - Thread.sleep(200); - } catch (InterruptedException e) { - e.printStackTrace(); - } - builder = performInitialized("incomingCallText"); + builder.wait(200); //Show call name //Must call twice, otherwise nothing happens for (int i = 0; i < msg.length; i++) msg[i] = ' '; - for(int i = 0; i < name.length() && i < (msg.length - 1); i++) + for (int i = 0; i < name.length() && i < (msg.length - 1); i++) msg[i + 1] = (byte) name.charAt(i); - msg[0] = HPlusConstants.COMMAND_ACTION_DISPLAY_TEXT_NAME; + msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME; builder.write(ctrlCharacteristic, msg); - try { - Thread.sleep(200); - } catch (InterruptedException e) { - e.printStackTrace(); - } + builder.wait(200); - msg[0] = HPlusConstants.COMMAND_ACTION_DISPLAY_TEXT_NAME_CN; + msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME_CN; builder.write(ctrlCharacteristic, msg); builder.queue(getQueue()); - }catch(IOException e){ + } catch (IOException e) { GB.toast(getContext(), "Error showing incoming call: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } - private void showText(String message) { - showText(null, message); - } - private void showText(String title, String body) { - LOG.debug("Show Notification"); - + LOG.debug("Show Notification: "+title+" --> "+body); try { TransactionBuilder builder = performInitialized("notification"); - byte[] msg = new byte[20]; for (int i = 0; i < msg.length; i++) msg[i] = ' '; - msg[0] = HPlusConstants.COMMAND_ACTION_DISPLAY_TEXT; + msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT; String message = ""; @@ -756,7 +726,9 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { int length = message.length() / 17; - builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_ACTION_INCOMING_SOCIAL, (byte) 255}); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_ACTION_INCOMING_SOCIAL, (byte) 255}); + + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_MESSAGE, HPlusConstants.ARG_INCOMING_MESSAGE}); int remaining; @@ -792,17 +764,17 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { builder.write(ctrlCharacteristic, msg); builder.queue(getQueue()); - }catch(IOException e){ + } catch (IOException e) { GB.toast(getContext(), "Error showing device Notification: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } - public boolean isExpectedDevice(BluetoothDevice device) { - return true; - } - - public void close() { + private void close() { + if (syncHelper != null) { + syncHelper.quit(); + syncHelper = null; + } } @Override @@ -818,203 +790,26 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { return true; switch (data[0]) { + case HPlusConstants.DATA_VERSION: + return syncHelper.processVersion(data); + case HPlusConstants.DATA_STATS: - return processDataStats(data); + return syncHelper.processRealtimeStats(data); + case HPlusConstants.DATA_SLEEP: - return processSleepStats(data); + return syncHelper.processIncomingSleepData(data); + case HPlusConstants.DATA_STEPS: - return processStepStats(data); + return syncHelper.processDaySummary(data); + + case HPlusConstants.DATA_DAY_SUMMARY: + case HPlusConstants.DATA_DAY_SUMMARY_ALT: + return syncHelper.processIncomingDaySlotData(data); default: - LOG.info("Unhandled characteristic changed: " + characteristicUUID); - + LOG.debug("Unhandled characteristic changed: " + characteristicUUID); + return true; } - return false; } - /* - Receives a message containing the status of the day. - */ - private boolean processDayStats(byte[] data) { - int a = data[4] * 256 + data[5]; - if (a < 144) { - int slot = a * 2; // 10 minute slots as an offset from 0:00 AM - int avgHR = data[1]; //Average Heart Rate - int steps = data[2] * 256 + data[3]; // Steps in this period - - //?? data[6]; - int timeInactive = data[7]; - - LOG.debug("Day Stats: Slot: " + slot + " HR: " + avgHR + " Steps: " + steps + " TimeInactive: " + timeInactive); - - try (DBHandler handler = GBApplication.acquireDB()) { - DaoSession session = handler.getDaoSession(); - - Device device = DBHelper.getDevice(getDevice(), session); - User user = DBHelper.getUser(session); - int ts = (int) (System.currentTimeMillis() / 1000); - HPlusSampleProvider provider = new HPlusSampleProvider(gbDevice, session); - - - //TODO: Store Sample. How? - - //provider.addGBActivitySample(record); - - - } catch (GBException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - } else - LOG.error("Invalid day stats"); - - return true; - } - - private boolean processStepStats(byte[] data) { - LOG.debug("Process Step Stats"); - - if (data.length < 19) { - LOG.error("Invalid Steps Message Length " + data.length); - return false; - } - /* - This is a dump of the entire day. - */ - int year = data[9] + data[10] * 256; - short month = data[11]; - short day = data[12]; - int steps = data[2] * 256 + data[1]; - - float distance = ((float) (data[3] + data[4] * 256) / 100.0f); - - /* - unknown fields - short s12 = (short)(data[5] + data[6] * 256); - short s13 = (short)(data[7] + data[8] * 256); - short s16 = (short)(data[13]) + data[14] * 256); - short s17 = data[15]; - short s18 = data[16]; - */ - - - LOG.debug("Step Stats: Year: " + year + " Month: " + month + " Day:"); - try (DBHandler handler = GBApplication.acquireDB()) { - DaoSession session = handler.getDaoSession(); - - Device device = DBHelper.getDevice(getDevice(), session); - User user = DBHelper.getUser(session); - int ts = (int) (System.currentTimeMillis() / 1000); - HPlusSampleProvider provider = new HPlusSampleProvider(gbDevice, session); - - - //TODO: Store Sample. How? - - //provider.addGBActivitySample(record); - - - } catch (GBException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - return true; - } - - - private boolean processSleepStats(byte[] data) { - LOG.debug("Process Sleep Stats"); - - if (data.length < 19) { - LOG.error("Invalid Sleep Message Length " + data.length); - return false; - } - HPlusSleepRecord record = new HPlusSleepRecord(data); - - try (DBHandler handler = GBApplication.acquireDB()) { - DaoSession session = handler.getDaoSession(); - - Device device = DBHelper.getDevice(getDevice(), session); - User user = DBHelper.getUser(session); - int ts = (int) (System.currentTimeMillis() / 1000); - HPlusSampleProvider provider = new HPlusSampleProvider(gbDevice, session); - - //TODO: Store Sample. How? - - //provider.addGBActivitySample(record); - - - } catch (GBException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - return true; - } - - - private boolean processDataStats(byte[] data) { - //TODO: Store Calories and Distance. How? - - LOG.debug("Process Data Stats"); - - if (data.length < 15) { - LOG.error("Invalid Stats Message Length " + data.length); - return false; - } - - //Ignore duplicate packets - if(data.equals(lastDataStats)) - return true; - - lastDataStats = data.clone(); - - double distance = ((int) data[4] * 256 + data[3]) / 100.0; - - int x = (int) data[6] * 256 + data[5]; - int y = (int) data[8] * 256 + data[7]; - int calories = x + y; - - int bpm = (data[11] == -1) ? HPlusHealthActivitySample.NOT_MEASURED : data[11]; - - try (DBHandler handler = GBApplication.acquireDB()) { - DaoSession session = handler.getDaoSession(); - - Device device = DBHelper.getDevice(getDevice(), session); - User user = DBHelper.getUser(session); - int ts = (int) (System.currentTimeMillis() / 1000); - HPlusSampleProvider provider = new HPlusSampleProvider(gbDevice, session); - - - if (bpm != HPlusHealthActivitySample.NOT_MEASURED) { - HPlusHealthActivitySample sample = createActivitySample(device, user, ts, provider); - sample.setHeartRate(bpm); - sample.setSteps(0); - sample.setRawIntensity(ActivitySample.NOT_MEASURED); - sample.setRawKind(ActivityKind.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? - provider.addGBActivitySample(sample); - } - } catch (GBException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - - - return true; - } - - public HPlusHealthActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) { - HPlusHealthActivitySample sample = new HPlusHealthActivitySample(); - sample.setDevice(device); - sample.setUser(user); - sample.setTimestamp(timestampInSeconds); - sample.setProvider(provider); - - return sample; - } }