diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 63e5d7139..223404ce5 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,17 @@ 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(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..a24b1e862 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusConstants.java @@ -0,0 +1,82 @@ +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_INCOMMING_SOCIAL = 0x31; + public static final byte COMMAND_SET_INCOMMING_SMS = 0x40; + public static final byte COMMAND_SET_DISPLAY_TEXT = 0x43; + public static final byte COMMAND_SET_DISPLAY_ALERT = 0x23; + public static final byte COMMAND_SET_PREF_ALLDAYHR = 53; + + public static final byte COMMAND_SET_INCOMMING_CALL = 65; + 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 DATA_STATS = 0x33; + public static final byte DATA_SLEEP = 0x1A; + + + public static final String PREF_HPLUS_USER_ALIAS = "hplus_user_alias"; + public static final String PREF_HPLUS_FITNESS_GOAL = "hplus_fitness_goal"; + 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"; +} 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..4eceef13c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java @@ -0,0 +1,202 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.hplus; + +/* +* @author João Paulo Barraca <jpbarraca@gmail.com> +*/ + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +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; + +public class HPlusCoordinator extends AbstractDeviceCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(HPlusCoordinator.class); + private static Prefs prefs = GBApplication.getPrefs(); + + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + 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 { + Prefs prefs = GBApplication.getPrefs(); + return prefs.getInt(HPlusConstants.PREF_HPLUS_FITNESS_GOAL + "_" + address, 10000); + } + + /** + * Returns the user info from the user configured data in the preferences. + * + * @param hplusAddress + * @throws IllegalArgumentException when the user info can not be created + */ + public static UserInfo getConfiguredUserInfo(String hplusAddress) throws IllegalArgumentException { + ActivityUser activityUser = new ActivityUser(); + + UserInfo info = UserInfo.create( + hplusAddress, + prefs.getString(HPlusConstants.PREF_HPLUS_USER_ALIAS, null), + activityUser.getGender(), + activityUser.getAge(), + activityUser.getHeightCm(), + activityUser.getWeightKg(), + 0 + ); + return info; + } + + public static byte getCountry(String address) { + return (byte) prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + 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(); + } + + public static byte getUserHeight(String address) { + ActivityUser activityUser = new ActivityUser(); + + return (byte) activityUser.getHeightCm(); + } + + public static byte getUserAge(String address) { + ActivityUser activityUser = new ActivityUser(); + + return (byte) activityUser.getAge(); + } + + public static byte getUserSex(String address) { + ActivityUser activityUser = new ActivityUser(); + + int gender = activityUser.getGender(); + + return (byte) gender; + + } + + 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); + + } + + public static byte getAllDayHR(String address) { + return (byte) prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + address, 10); + + } +} 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..67b535a3b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusSampleProvider.java @@ -0,0 +1,140 @@ +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 { + + public static final int TYPE_DEEP_SLEEP = 4; + public static final int TYPE_LIGHT_SLEEP = 5; + public static final int TYPE_ACTIVITY = -1; + public static final int TYPE_UNKNOWN = -1; + public static final int TYPE_NONWEAR = 3; + public static final int TYPE_CHARGING = 6; + + 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) { + switch (rawType) { + case TYPE_DEEP_SLEEP: + return ActivityKind.TYPE_DEEP_SLEEP; + case TYPE_LIGHT_SLEEP: + return ActivityKind.TYPE_LIGHT_SLEEP; + case TYPE_ACTIVITY: + return ActivityKind.TYPE_ACTIVITY; + case TYPE_NONWEAR: + return ActivityKind.TYPE_NOT_WORN; + case TYPE_CHARGING: + return ActivityKind.TYPE_NOT_WORN; //I believe it's a safe assumption + default: +// case TYPE_UNKNOWN: // fall through + return ActivityKind.TYPE_UNKNOWN; + } + } + + public int toRawActivityKind(int activityKind) { + switch (activityKind) { + case ActivityKind.TYPE_ACTIVITY: + return TYPE_ACTIVITY; + case ActivityKind.TYPE_DEEP_SLEEP: + return TYPE_DEEP_SLEEP; + case ActivityKind.TYPE_LIGHT_SLEEP: + return TYPE_LIGHT_SLEEP; + case ActivityKind.TYPE_NOT_WORN: + return TYPE_NONWEAR; + case ActivityKind.TYPE_UNKNOWN: // fall through + default: + return TYPE_UNKNOWN; + } + } + + + @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) { + // no device, no samples + return Collections.emptyList(); + } + + QueryBuilder qb = getSession().getHPlusHealthActivitySampleDao().queryBuilder(); + + qb.where(HPlusHealthActivitySampleDao.Properties.DeviceId.eq(dbDevice.getId()), HPlusHealthActivitySampleDao.Properties.Timestamp.ge(timestamp_from)) + .where(HPlusHealthActivitySampleDao.Properties.Timestamp.le(timestamp_to)); + + List sampleList = qb.build().list(); + + for (HPlusHealthActivitySample sample : sampleList) { + if (timestamp_from <= sample.getTimestamp() && sample.getTimestamp() < timestamp_to) { + sample.setRawKind(sample.getRawKind()); + } + } + detachFromSession(); + return samples; + } + @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..7737b5198 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java @@ -0,0 +1,802 @@ +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.bluetooth.BluetoothGattDescriptor; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +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.ConditionalWriteAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; + + +public class HPlusSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(HPlusSupport.class); + + private BluetoothGattCharacteristic ctrlCharacteristic = null; + private BluetoothGattCharacteristic measureCharacteristic = 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())); + + //Fill device info + requestDeviceInfo(builder); + + getDevice().setFirmwareVersion("0"); + getDevice().setFirmwareVersion2("0"); + + //Initialize device + setInitValues(builder); + + builder.notify(getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_MEASURE), true); + + UUID uuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + BluetoothGattDescriptor descriptor = measureCharacteristic.getDescriptor(uuid); + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + + 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, new byte[]{HPlusConstants.COMMAND_SET_CONF_SAVE}); + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_CONF_END}); + return this; + } + + + private HPlusSupport setCountry(TransactionBuilder transaction) { + LOG.info("Attempting to set country..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getCountry(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_COUNTRY, + (byte) value + }; + } + }); + return this; + } + + + private HPlusSupport setTimeMode(TransactionBuilder transaction) { + LOG.info("Attempting to set Time Mode..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getTimeMode(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_TIMEMODE, + (byte) value + }; + } + }); + return this; + } + + private HPlusSupport setUnit(TransactionBuilder transaction) { + LOG.info("Attempting to set Units..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getUnit(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_UNIT, + (byte) value + }; + } + }); + return this; + } + + private HPlusSupport setCurrentDate(TransactionBuilder transaction) { + LOG.info("Attempting to set Current Date..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + 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); + + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_DATE, + (byte) (year / 256), + (byte) (year % 256), + (byte) (month), + (byte) (day) + }; + } + }); + return this; + } + + private HPlusSupport setCurrentTime(TransactionBuilder transaction) { + LOG.info("Attempting to set Current Time..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + Calendar c = Calendar.getInstance(); + + return 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..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + Calendar c = Calendar.getInstance(); + + return 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..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + Calendar c = Calendar.getInstance(); + + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_SIT, + 0, 0, 0, 0, 0 + }; + } + }); + return this; + } + + private HPlusSupport setWeight(TransactionBuilder transaction) { + LOG.info("Attempting to set Weight..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getUserWeight(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_WEIGHT, + (byte) value + }; + } + }); + return this; + } + + private HPlusSupport setHeight(TransactionBuilder transaction) { + LOG.info("Attempting to set Height..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getUserHeight(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_HEIGHT, + (byte) value + }; + } + }); + return this; + } + + + private HPlusSupport setAge(TransactionBuilder transaction) { + LOG.info("Attempting to set Age..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getUserAge(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_AGE, + (byte) value + }; + } + }); + return this; + } + + private HPlusSupport setSex(TransactionBuilder transaction) { + LOG.info("Attempting to set Sex..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getUserSex(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_SEX, + (byte) value + }; + } + }); + return this; + } + + + private HPlusSupport setGoal(TransactionBuilder transaction) { + LOG.info("Attempting to set Sex..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + int value = HPlusCoordinator.getGoal(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_GOAL, + (byte) (value / 256), + (byte) (value % 256) + }; + } + }); + return this; + } + + + private HPlusSupport setScreenTime(TransactionBuilder transaction) { + LOG.info("Attempting to set Screentime..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getScreenTime(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_SCREENTIME, + (byte) value + }; + } + }); + return this; + } + + private HPlusSupport setAllDayHeart(TransactionBuilder transaction) { + LOG.info("Attempting to set All Day HR..."); + transaction.add(new ConditionalWriteAction(ctrlCharacteristic) { + @Override + protected byte[] checkCondition() { + + byte value = HPlusCoordinator.getAllDayHR(getDevice().getAddress()); + return new byte[]{ + HPlusConstants.COMMAND_SET_PREF_ALLDAYHR, + (byte) value + }; + } + }); + return this; + } + + + private HPlusSupport setAlarm(TransactionBuilder transaction) { + LOG.info("Attempting to set Alarm..."); + return this; + } + + private HPlusSupport setBlood(TransactionBuilder transaction) { + LOG.info("Attempting to set Blood..."); + return this; + } + + + private HPlusSupport setFindMe(TransactionBuilder transaction) { + LOG.info("Attempting to set Findme..."); + 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"); + showText(notificationSpec.body); + } + + + @Override + public void onSetTime() { + TransactionBuilder builder = new TransactionBuilder("vibration"); + setCurrentDate(builder); + setCurrentTime(builder); + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(CallSpec callSpec) { + switch(callSpec.command){ + case CallSpec.CALL_INCOMING: { + showText(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"); + byte state = 0; + + 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 = 0; + + 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"); + + getQueue().clear(); + ctrlCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_CONTROL); + + TransactionBuilder builder = new TransactionBuilder("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()); + } + + @Override + public void onSetConstantVibration(int intensity) { + LOG.debug("Vibration Trigger"); + + getQueue().clear(); + + ctrlCharacteristic = getCharacteristic(HPlusConstants.UUID_CHARACTERISTIC_CONTROL); + + TransactionBuilder builder = new TransactionBuilder("vibration"); + + byte[] msg = new byte[15]; + msg[0] = HPlusConstants.COMMAND_SET_DISPLAY_ALERT; + + for(int i = 0;i 12) { + message = title.substring(0, 12); + }else { + message = title; + for(int i = message.length(); i < 12; i++) + message += ""; + } + } + message += body; + + int length = message.length() / 17; + + builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.COMMAND_SET_INCOMMING_SOCIAL, (byte) length}); + + int remaining = 0; + + 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] = 32; + + if(message_index < remaining) + i = 3; + else + break; + } + } + + msg[2] = (byte) remaining; + + builder.write(ctrlCharacteristic, msg); + builder.queue(getQueue()); + } + + 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); + default: + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + + } + return false; + } + + 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); + + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, record.getRawData() ) + .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); + + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + + }catch (GBException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + + return true; + } + + + private boolean processDataStats(byte[]data){ + LOG.debug("Process Data Stats"); + + if(data.length < 15) { + LOG.error("Invalid Stats Message Length " + data.length); + return false; + } + 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); + 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; }