diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 63e5d7139..e6cb8ddf8 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); + addHPlusHealthActivitySample(schema, user, device); new DaoGenerator().generateAll(schema, "app/src/main/java"); } @@ -221,6 +222,19 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addHPlusHealthActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "HPlusHealthActivitySample"); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addByteArrayProperty("rawHPlusHealthData"); + activitySample.addIntProperty("rawHPlusCalories").notNull(); + activitySample.addIntProperty("rawHPlusDistance").notNull(); + 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); + return activitySample; + } + 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/SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java index fca1710ec..3dafa35af 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java @@ -24,6 +24,7 @@ public interface SampleProvider { int PROVIDER_PEBBLE_MISFIT = 3; int PROVIDER_PEBBLE_HEALTH = 4; int PROVIDER_MIBAND2 = 5; + int PROVIDER_HPLUS = 6; int PROVIDER_UNKNOWN = 100; // TODO: can also be removed 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 new file mode 100644 index 000000000..f69bb4536 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusConstants.java @@ -0,0 +1,99 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.hplus; + +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"); + public static final UUID UUID_CHARACTERISTIC_MEASURE = UUID.fromString("14702853-620a-3973-7c78-9cfff0876abd"); + 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 CLOCK_24H = 0; + public static final byte CLOCK_12H = 1; + + public static final byte UNIT_METRIC = 0; + public static final byte UNIT_IMPERIAL = 1; + + public static final byte SEX_MALE = 0; + public static final byte SEX_FEMALE = 1; + + public static final byte HEARTRATE_MEASURE_ON = 11; + public static final byte HEARTRATE_MEASURE_OFF = 22; + + public static final byte HEARTRATE_ALLDAY_ON = 10; + public static final byte HEARTRATE_ALLDAY_OFF = -1; + + 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[] COMMAND_SET_PREF_START = new byte[]{0x4f, 0x5a}; + public static final byte[] COMMAND_SET_PREF_START1 = new byte[]{0x4d}; + + 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 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 COMMAND_SET_INCOMING_CALL = 0x41; + public static final byte[] COMMAND_FACTORY_RESET = new byte[] {-74, 90}; + + public static final byte COMMAND_SET_CONF_SAVE = 0x17; + public static final byte COMMAND_SET_CONF_END = 0x4f; + + public static final byte COMMAND_SET_PREFS = 0x50; + public static final byte COMMAND_SET_SIT_INTERVAL = 0x51; + + + + public static final byte DATA_STATS = 0x33; + public static final byte DATA_STEPS = 0x36; + + 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 String PREF_HPLUS_SCREENTIME = "hplus_screentime"; + public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr"; + 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"; + public static final String PREF_HPLUS_SWALERT = "hplus_sw_alert"; + public static final String PREF_HPLUS_ALERT_TIME = "hplus_alert_time"; + 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 new file mode 100644 index 000000000..d1cf8e251 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java @@ -0,0 +1,226 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + +import android.annotation.TargetApi; +import android.app.Activity; +import android.bluetooth.le.ScanFilter; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelUuid; +import android.support.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +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.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; + +public class HPlusCoordinator extends AbstractDeviceCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(HPlusCoordinator.class); + private static Prefs prefs = GBApplication.getPrefs(); + + @NonNull + @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(); + 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; + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.HPLUS; + } + + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public Class getPrimaryActivity() { + return ChartsActivity.class; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new HPlusSampleProvider(device, session); + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return true; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public int getTapString() { + return R.string.tap_connected_device_for_activity; + } + + @Override + public String getManufacturer() { + return "Zeblaze"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + // nothing to delete, yet + } + + public static int getFitnessGoal(String address) throws IllegalArgumentException { + ActivityUser activityUser = new ActivityUser(); + + return activityUser.getStepsGoal(); + } + + public static byte getCountry(String address) { + return (byte) prefs.getInt(HPlusConstants.PREF_HPLUS_COUNTRY + "_" + address, 10); + + } + + public static byte getTimeMode(String address) { + return (byte) prefs.getInt(HPlusConstants.PREF_HPLUS_TIMEMODE + "_" + address, 0); + } + + public static byte getUnit(String address) { + return (byte) prefs.getInt(HPlusConstants.PREF_HPLUS_UNIT + "_" + address, 0); + } + + public static byte getUserWeight(String address) { + ActivityUser activityUser = new ActivityUser(); + + return (byte) (activityUser.getWeightKg() & 0xFF); + } + + public static byte getUserHeight(String address) { + ActivityUser activityUser = new ActivityUser(); + + return (byte) (activityUser.getHeightCm() & 0xFF); + } + + public static byte getUserAge(String address) { + ActivityUser activityUser = new ActivityUser(); + + return (byte) (activityUser.getAge() & 0xFF); + } + + public static byte getUserSex(String address) { + ActivityUser activityUser = new ActivityUser(); + + return (byte) (activityUser.getGender() & 0xFF); + } + + public static int getGoal(String address) { + ActivityUser activityUser = new ActivityUser(); + + return activityUser.getStepsGoal(); + } + + public static byte getScreenTime(String address) { + return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_SCREENTIME + "_" + address, 5) & 0xFF); + } + + public static byte getAllDayHR(String address) { + return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + address, 10) & 0xFF); + } + + public static byte getSocial(String address) { + //TODO: Figure what this is. Returning the default value + + return (byte) 255; + } + + public static byte getUserWrist(String address) { + return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_WRIST + "_" + address, 10) & 0xFF); + } + + public static boolean getSWAlertTime(String address) { + return prefs.getBoolean(HPlusConstants.PREF_HPLUS_SWALERT + "_" + address, false); + } + + public static int getAlertTime(String address) { + return prefs.getInt(HPlusConstants.PREF_HPLUS_ALERT_TIME + "_" + address, 0); + } + + public static int getSITStartTime(String address) { + return prefs.getInt(HPlusConstants.PREF_HPLUS_SIT_START_TIME + "_" + address, 0); + } + + public static int getSITEndTime(String address) { + return prefs.getInt(HPlusConstants.PREF_HPLUS_SIT_END_TIME + "_" + address, 0); + } + +} 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 new file mode 100644 index 000000000..a26f6be2c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusSampleProvider.java @@ -0,0 +1,82 @@ +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/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 5c9d26636..882a82053 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -13,6 +13,7 @@ public enum DeviceType { MIBAND2(11), VIBRATISSIMO(20), LIVEVIEW(30), + HPLUS(40), TEST(1000); private final int key; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 71c3dc8d9..af25c8a73 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -15,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Suppo import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DeviceSupportFactory { @@ -96,6 +97,9 @@ public class DeviceSupportFactory { case LIVEVIEW: deviceSupport = new ServiceDeviceSupport(new LiveviewSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case HPLUS: + deviceSupport = new ServiceDeviceSupport(new HPlusSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); 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 new file mode 100644 index 000000000..bed5d581e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSleepRecord.java @@ -0,0 +1,86 @@ +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 new file mode 100644 index 000000000..c3704ffec --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java @@ -0,0 +1,1009 @@ +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; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.support.v4.content.LocalBroadcastManager; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +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; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.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.deviceinfo.DeviceInfoProfile; +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; + + private byte[] lastDataStats = null; + + private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String s = intent.getAction(); + if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { + handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + } + } + }; + + public HPlusSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(HPlusConstants.UUID_SERVICE_HP); + + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + IntentFilter intentFilter = new IntentFilter(); + + broadcastManager.registerReceiver(mReceiver, intentFilter); + } + + @Override + public void dispose() { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + broadcastManager.unregisterReceiver(mReceiver); + super.dispose(); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + + measureCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE); + ctrlCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_CONTROL); + + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + getDevice().setFirmwareVersion("0"); + getDevice().setFirmwareVersion2("0"); + + //Initialize device + setInitValues(builder); + setCurrentDate(builder); + setCurrentTime(builder); + syncPreferences(builder); + + 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); + + syncPreferences(builder); + + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_CONF_SAVE}); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_CONF_END}); + return this; + } + + private HPlusSupport syncPreferences(TransactionBuilder transaction) { + LOG.info("Attempting to sync preferences..."); + + byte sex = HPlusCoordinator.getUserSex(getDevice().getAddress()); + byte age = HPlusCoordinator.getUserAge(getDevice().getAddress()); + byte bodyHeight = HPlusCoordinator.getUserHeight(getDevice().getAddress()); + byte bodyWeight = HPlusCoordinator.getUserWeight(getDevice().getAddress()); + int goal = HPlusCoordinator.getGoal(getDevice().getAddress()); + byte displayTime = HPlusCoordinator.getScreenTime(getDevice().getAddress()); + byte country = HPlusCoordinator.getCountry(getDevice().getAddress()); + byte social = HPlusCoordinator.getSocial(getDevice().getAddress()); // ?? + byte allDayHeart = HPlusCoordinator.getAllDayHR(getDevice().getAddress()); + byte wrist = HPlusCoordinator.getUserWrist(getDevice().getAddress()); + byte alertTimeHour = 0; + byte alertTimeMinute = 0; + + if (HPlusCoordinator.getSWAlertTime(getDevice().getAddress())) { + int t = HPlusCoordinator.getAlertTime(getDevice().getAddress()); + + alertTimeHour = (byte) ((t / 256) & 0xff); + alertTimeMinute = (byte) (t % 256); + } + + byte unit = HPlusCoordinator.getUnit(getDevice().getAddress()); + byte timemode = HPlusCoordinator.getTimeMode((getDevice().getAddress())); + + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.COMMAND_SET_PREF_COUNTRY, + sex, + age, + bodyHeight, + bodyWeight, + 0, + 0, + (byte) ((goal / 256) & 0xff), + (byte) (goal % 256), + displayTime, + country, + 0, + social, + allDayHeart, + wrist, + alertTimeHour, + alertTimeMinute, + unit, + timemode + }); + return this; + } + + private HPlusSupport setCountry(TransactionBuilder transaction) { + LOG.info("Attempting to set country..."); + + byte value = HPlusCoordinator.getCountry(getDevice().getAddress()); + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.COMMAND_SET_PREF_COUNTRY, + value + }); + return this; + } + + + 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, + 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, + value + }); + return this; + } + + private HPlusSupport setCurrentDate(TransactionBuilder transaction) { + LOG.info("Attempting to set Current Date..."); + + Calendar c = Calendar.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, + (byte) ((year / 256) & 0xff), + (byte) (year % 256), + (byte) (month + 1), + (byte) (day) + + }); + return this; + } + + private HPlusSupport setCurrentTime(TransactionBuilder transaction) { + LOG.info("Attempting to set Current Time..."); + + Calendar c = Calendar.getInstance(); + + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.COMMAND_SET_PREF_TIME, + (byte) c.get(Calendar.HOUR_OF_DAY), + (byte) c.get(Calendar.MINUTE), + (byte) c.get(Calendar.SECOND) + + }); + return this; + } + + + private HPlusSupport setDayOfWeek(TransactionBuilder transaction) { + LOG.info("Attempting to set Day Of Week..."); + + Calendar c = Calendar.getInstance(); + + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.COMMAND_SET_PREF_WEEK, + (byte) c.get(Calendar.DAY_OF_WEEK) + }); + return this; + } + + + 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(); + + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.COMMAND_SET_SIT_INTERVAL, + (byte) ((startTime / 256) & 0xff), + (byte) (startTime % 256), + (byte) ((endTime / 256) & 0xff), + (byte) (endTime % 256), + 0, + 0, + (byte) ((now.get(Calendar.YEAR) / 256) & 0xff), + (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.MINUTE)), + (byte) (now.get(Calendar.SECOND)), + 0, + 0, + 0, + 0 + + }); + return this; + } + + 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, + value + + }); + return this; + } + + 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, + value + + }); + return this; + } + + + 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, + value + + }); + return this; + } + + private HPlusSupport setSex(TransactionBuilder transaction) { + LOG.info("Attempting to set Sex..."); + + byte value = HPlusCoordinator.getUserSex(getDevice().getAddress()); + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.COMMAND_SET_PREF_SEX, + value + + }); + return this; + } + + + 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, + (byte) ((value / 256) & 0xff), + (byte) (value % 256) + + }); + return this; + } + + + 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, + value + + }); + return this; + } + + private HPlusSupport setAllDayHeart(TransactionBuilder transaction) { + LOG.info("Attempting to set All Day HR..."); + + byte value = HPlusCoordinator.getAllDayHR(getDevice().getAddress()); + transaction.write(ctrlCharacteristic, new byte[]{ + HPlusConstants.COMMAND_SET_PREF_ALLDAYHR, + value + + }); + return this; + } + + + private HPlusSupport setAlarm(TransactionBuilder transaction) { + LOG.info("Attempting to set Alarm..."); + //TODO: Find how to set alarms + 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..."); + //TODO: Find how this works + return this; + } + + private HPlusSupport requestDeviceInfo(TransactionBuilder builder) { + LOG.debug("Requesting Device Info!"); + BluetoothGattCharacteristic deviceName = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME); + builder.read(deviceName); + return this; + } + + private void setInitialized(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + } + + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void pair() { + LOG.debug("Pair"); + } + + private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.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); + } + + + @Override + public void onSetTime() { + TransactionBuilder builder = new TransactionBuilder("time"); + + setCurrentDate(builder); + setCurrentTime(builder); + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(CallSpec callSpec) { + switch (callSpec.command) { + case CallSpec.CALL_INCOMING: { + showIncomingCall(callSpec.name, callSpec.number); + break; + } + } + + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + LOG.debug("Canned Messages: " + cannedMessagesSpec); + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchActivityData() { + + } + + @Override + public void onReboot() { + + } + + @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.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; + else + state = HPlusConstants.HEARTRATE_ALLDAY_OFF; + + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_PREF_ALLDAYHR, 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); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error toogling 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; + + for (int i = 0; i < msg.length - 1; i++) + msg[i + 1] = (byte) "GadgetBridge".charAt(i); + + builder.write(ctrlCharacteristic, msg); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error setting Vibration: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + LOG.debug("Send Configuration: " + config); + + } + + @Override + public void onTestNewFunction() { + LOG.debug("Test New Function"); + } + + + private void showIncomingCall(String name, String number){ + LOG.debug("Show Incoming Call"); + + try { + TransactionBuilder builder = performInitialized("incomingCallIcon"); + + //Enable call notifications + builder.write(ctrlCharacteristic, new byte[] {HPlusConstants.COMMAND_SET_INCOMING_CALL, 1 }); + + //Show Call Icon + builder.write(ctrlCharacteristic, HPlusConstants.COMMAND_ACTION_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"); + + //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++) + msg[i + 1] = (byte) number.charAt(i); + + + msg[0] = HPlusConstants.COMMAND_ACTION_DISPLAY_TEXT_CENTER; + + builder.write(ctrlCharacteristic, msg); + builder.queue(getQueue()); + + try { + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + builder = performInitialized("incomingCallText"); + + //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++) + msg[i + 1] = (byte) name.charAt(i); + + msg[0] = HPlusConstants.COMMAND_ACTION_DISPLAY_TEXT_NAME; + builder.write(ctrlCharacteristic, msg); + + try { + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + msg[0] = HPlusConstants.COMMAND_ACTION_DISPLAY_TEXT_NAME_CN; + builder.write(ctrlCharacteristic, msg); + + builder.queue(getQueue()); + }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"); + + 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; + + String message = ""; + + //TODO: Create StringUtils.pad and StringUtils.truncate + if (title != null) { + if (title.length() > 17) { + message = title.substring(0, 17); + } else { + message = title; + for (int i = message.length(); i < 17; i++) + message += " "; + } + } + message += body; + + int length = message.length() / 17; + + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_ACTION_INCOMING_SOCIAL, (byte) 255}); + + int remaining; + + if (message.length() % 17 > 0) + remaining = length + 1; + else + remaining = length; + + msg[1] = (byte) remaining; + int message_index = 0; + int i = 3; + + for (int j = 0; j < message.length(); j++) { + msg[i++] = (byte) message.charAt(j); + + if (i == msg.length) { + message_index++; + msg[2] = (byte) message_index; + builder.write(ctrlCharacteristic, msg); + + msg = msg.clone(); + for (i = 3; i < msg.length; i++) + msg[i] = ' '; + + if (message_index < remaining) + i = 3; + else + break; + } + } + + msg[2] = (byte) remaining; + + builder.write(ctrlCharacteristic, msg); + builder.queue(getQueue()); + }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() { + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + if (super.onCharacteristicChanged(gatt, characteristic)) { + return true; + } + + UUID characteristicUUID = characteristic.getUuid(); + byte[] data = characteristic.getValue(); + if (data.length == 0) + return true; + + switch (data[0]) { + case HPlusConstants.DATA_STATS: + return processDataStats(data); + case HPlusConstants.DATA_SLEEP: + return processSleepStats(data); + case HPlusConstants.DATA_STEPS: + return processStepStats(data); + + default: + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + + } + 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; + } +} 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 0b0c6a8d9..c57abb934 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -22,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; @@ -167,6 +168,7 @@ public class DeviceHelper { result.add(new PebbleCoordinator()); result.add(new VibratissimoCoordinator()); result.add(new LiveviewCoordinator()); + result.add(new HPlusCoordinator()); return result; }