mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-28 12:56:49 +01:00
HPlus: Improved support for storing and displaying data.
This commit is contained in:
parent
970c6960ea
commit
4cf872664c
@ -28,8 +28,8 @@ public final class HPlusConstants {
|
||||
public static final byte ARG_HEARTRATE_MEASURE_ON = 11;
|
||||
public static final byte ARG_HEARTRATE_MEASURE_OFF = 22;
|
||||
|
||||
public static final byte ARG_HEARTRATE_ALLDAY_ON = 10;
|
||||
public static final byte ARG_HEARTRATE_ALLDAY_OFF = -1;
|
||||
public static final byte ARG_HEARTRATE_ALLDAY_ON = 0x0A;
|
||||
public static final byte ARG_HEARTRATE_ALLDAY_OFF = (byte) 0xff;
|
||||
|
||||
public static final byte INCOMING_CALL_STATE_DISABLED_THRESHOLD = 0x7B;
|
||||
public static final byte INCOMING_CALL_STATE_ENABLED = (byte) 0xAA;
|
||||
@ -64,9 +64,7 @@ public final class HPlusConstants {
|
||||
public static final byte CMD_SET_CONF_END = 0x4f;
|
||||
public static final byte CMD_SET_PREFS = 0x50;
|
||||
public static final byte CMD_SET_SIT_INTERVAL = 0x51;
|
||||
|
||||
public static final byte[] COMMAND_FACTORY_RESET = new byte[] {-74, 90};
|
||||
|
||||
public static final byte CMD_SET_HEARTRATE_STATE = 0x32;
|
||||
|
||||
//Actions to device
|
||||
public static final byte CMD_GET_ACTIVE_DAY = 0x27;
|
||||
@ -76,19 +74,17 @@ public final class HPlusConstants {
|
||||
public static final byte CMD_GET_DEVICE_ID = 0x24;
|
||||
|
||||
public static final byte CMD_ACTION_INCOMING_SOCIAL = 0x31;
|
||||
//public static final byte COMMAND_ACTION_INCOMING_SMS = 0x40;
|
||||
//public static final byte COMMAND_ACTION_INCOMING_SMS = 0x40; //Unknown
|
||||
public static final byte CMD_ACTION_DISPLAY_TEXT = 0x43;
|
||||
|
||||
public static final byte CMD_ACTION_DISPLAY_TEXT_NAME = 0x3F;
|
||||
public static final byte CMD_ACTION_DISPLAY_TEXT_NAME_CN = 0x3E; //Text in GB2312?
|
||||
public static final byte[] CMD_ACTION_HELLO = new byte[]{0x01, 0};
|
||||
public static final byte CMD_SHUTDOWN = 91;
|
||||
public static final byte ARG_SHUTDOWN_EN = 90;
|
||||
public static final byte[] CMD_ACTION_HELLO = new byte[]{0x01, 0x00};
|
||||
public static final byte CMD_SHUTDOWN = 0x5B;
|
||||
public static final byte ARG_SHUTDOWN_EN = 0x5A;
|
||||
|
||||
public static final byte CMD_FACTORY_RESET = -74;
|
||||
public static final byte ARG_FACTORY_RESET_EN = 90;
|
||||
|
||||
|
||||
public static final byte ARG_FACTORY_RESET_EN = 0x5A;
|
||||
|
||||
public static final byte CMD_SET_INCOMING_MESSAGE = 0x07;
|
||||
public static final byte CMD_SET_INCOMING_CALL = 0x06;
|
||||
@ -103,15 +99,9 @@ public final class HPlusConstants {
|
||||
public static final byte DATA_SLEEP = 0x1A;
|
||||
public static final byte DATA_VERSION = 0x18;
|
||||
|
||||
|
||||
public static final byte DB_TYPE_DAY_SLOT_SUMMARY = 1;
|
||||
public static final byte DB_TYPE_DAY_SUMMARY = 2;
|
||||
public static final byte DB_TYPE_INSTANT_STATS = 3;
|
||||
public static final byte DB_TYPE_SLEEP_STATS = 4;
|
||||
|
||||
|
||||
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";
|
||||
public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr";
|
||||
public static final String PREF_HPLUS_HR = "hplus_hr_enable";
|
||||
public static final String PREF_HPLUS_UNIT = "hplus_unit";
|
||||
public static final String PREF_HPLUS_TIMEMODE = "hplus_timemode";
|
||||
public static final String PREF_HPLUS_WRIST = "hplus_wrist";
|
||||
@ -120,5 +110,4 @@ public final class HPlusConstants {
|
||||
public static final String PREF_HPLUS_SIT_START_TIME = "hplus_sit_start_time";
|
||||
public static final String PREF_HPLUS_SIT_END_TIME = "hplus_sit_end_time";
|
||||
public static final String PREF_HPLUS_COUNTRY = "hplus_country";
|
||||
|
||||
}
|
||||
|
@ -198,6 +198,10 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
|
||||
return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_ALLDAYHR + "_" + address, HPlusConstants.ARG_HEARTRATE_ALLDAY_ON) & 0xFF);
|
||||
}
|
||||
|
||||
public static byte getHRState(String address) {
|
||||
return (byte) (prefs.getInt(HPlusConstants.PREF_HPLUS_HR + "_" + address, HPlusConstants.ARG_HEARTRATE_MEASURE_ON) & 0xFF);
|
||||
}
|
||||
|
||||
public static byte getSocial(String address) {
|
||||
//TODO: Figure what this is. Returning the default value
|
||||
|
||||
|
@ -5,9 +5,12 @@ package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
|
||||
*/
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
@ -25,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySampleDa
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusDataRecord;
|
||||
|
||||
public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealthActivitySample> {
|
||||
|
||||
@ -44,13 +48,28 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
|
||||
}
|
||||
|
||||
public int normalizeType(int rawType) {
|
||||
|
||||
return rawType;
|
||||
switch (rawType){
|
||||
case HPlusDataRecord.TYPE_DAY_SLOT:
|
||||
case HPlusDataRecord.TYPE_DAY_SUMMARY:
|
||||
case HPlusDataRecord.TYPE_REALTIME:
|
||||
case HPlusDataRecord.TYPE_SLEEP:
|
||||
case HPlusDataRecord.TYPE_UNKNOWN:
|
||||
return ActivityKind.TYPE_UNKNOWN;
|
||||
default:
|
||||
return rawType;
|
||||
}
|
||||
}
|
||||
|
||||
public int toRawActivityKind(int activityKind) {
|
||||
switch (activityKind){
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
return ActivityKind.TYPE_DEEP_SLEEP;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
return ActivityKind.TYPE_LIGHT_SLEEP;
|
||||
default:
|
||||
return HPlusDataRecord.TYPE_DAY_SLOT;
|
||||
}
|
||||
|
||||
return activityKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -85,6 +104,15 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
|
||||
return getSession().getHPlusHealthActivitySampleDao();
|
||||
}
|
||||
|
||||
|
||||
public List<HPlusHealthActivitySample> getActivityamples(int timestamp_from, int timestamp_to) {
|
||||
return getAllActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
public List<HPlusHealthActivitySample> getSleepSamples(int timestamp_from, int timestamp_to) {
|
||||
return getAllActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<HPlusHealthActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to) {
|
||||
@ -97,16 +125,48 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
|
||||
|
||||
QueryBuilder<HPlusHealthActivityOverlay> qb = getSession().getHPlusHealthActivityOverlayDao().queryBuilder();
|
||||
|
||||
qb.where(HPlusHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()), HPlusHealthActivityOverlayDao.Properties.TimestampFrom.ge(timestamp_from))
|
||||
.where(HPlusHealthActivityOverlayDao.Properties.TimestampTo.le(timestamp_to));
|
||||
qb.where(HPlusHealthActivityOverlayDao.Properties.DeviceId.eq(dbDevice.getId()),
|
||||
HPlusHealthActivityOverlayDao.Properties.TimestampFrom.ge(timestamp_from - 3600 * 24),
|
||||
HPlusHealthActivityOverlayDao.Properties.TimestampTo.le(timestamp_to),
|
||||
HPlusHealthActivityOverlayDao.Properties.TimestampTo.ge(timestamp_from));
|
||||
|
||||
List<HPlusHealthActivityOverlay> overlayRecords = qb.build().list();
|
||||
|
||||
//Todays sample steps will come from the Day Slots messages
|
||||
//Historical steps will be provided by Day Summaries messages
|
||||
//This will allow both week and current day results to be consistent
|
||||
Calendar today = GregorianCalendar.getInstance();
|
||||
today.set(Calendar.HOUR_OF_DAY, 0);
|
||||
today.set(Calendar.MINUTE, 0);
|
||||
today.set(Calendar.SECOND, 0);
|
||||
today.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
int stepsToday = 0;
|
||||
for(HPlusHealthActivitySample sample: samples){
|
||||
if(sample.getTimestamp() >= today.getTimeInMillis() / 1000){
|
||||
//Only consider these for the current day as a single message is enough for steps
|
||||
//HR and Overlays will still benefit from the full set of samples
|
||||
if(sample.getRawKind() == HPlusDataRecord.TYPE_REALTIME) {
|
||||
int aux = sample.getSteps();
|
||||
sample.setSteps(sample.getSteps() - stepsToday);
|
||||
stepsToday = aux;
|
||||
}else
|
||||
sample.setSteps(ActivitySample.NOT_MEASURED);
|
||||
}else{
|
||||
if (sample.getRawKind() != HPlusDataRecord.TYPE_DAY_SUMMARY) {
|
||||
sample.setSteps(ActivityKind.TYPE_NOT_MEASURED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
|
||||
|
||||
//Create fake events to improve activity counters if there are no events around the overlay
|
||||
//timestamp boundaries
|
||||
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId());
|
||||
insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId());
|
||||
|
||||
for (HPlusHealthActivitySample sample : samples) {
|
||||
|
||||
if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) {
|
||||
sample.setRawKind(overlay.getRawKind());
|
||||
}
|
||||
@ -124,7 +184,7 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
|
||||
return samples;
|
||||
}
|
||||
|
||||
private List<HPlusHealthActivitySample> insertVirtualItem(List<HPlusHealthActivitySample> samples, int timestamp, long deviceId, long userId){
|
||||
private List<HPlusHealthActivitySample> insertVirtualItem(List<HPlusHealthActivitySample> samples, int timestamp, long deviceId, long userId) {
|
||||
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
|
||||
timestamp, // ts
|
||||
deviceId,
|
||||
@ -143,5 +203,5 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,13 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
|
||||
public class HPlusDataRecord {
|
||||
public final static int TYPE_SLEEP = 1;
|
||||
public final static int TYPE_UNKNOWN = 0;
|
||||
public final static int TYPE_SLEEP = 100;
|
||||
public final static int TYPE_DAY_SUMMARY = 101;
|
||||
public final static int TYPE_DAY_SLOT = 102;
|
||||
public final static int TYPE_REALTIME = 103;
|
||||
|
||||
public int type = TYPE_UNKNOWN;
|
||||
public int activityKind = ActivityKind.TYPE_UNKNOWN;
|
||||
|
||||
/**
|
||||
@ -21,8 +27,13 @@ public class HPlusDataRecord {
|
||||
*/
|
||||
public byte[] rawData;
|
||||
|
||||
public HPlusDataRecord(byte[] data){
|
||||
rawData = data;
|
||||
protected HPlusDataRecord(){
|
||||
|
||||
}
|
||||
|
||||
protected HPlusDataRecord(byte[] data, int type){
|
||||
this.rawData = data;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public byte[] getRawData() {
|
||||
|
@ -4,6 +4,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
* @author João Paulo Barraca <jpbarraca@gmail.com>
|
||||
*/
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
|
||||
@ -35,7 +36,7 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
|
||||
public int heartRate;
|
||||
|
||||
public HPlusDataRecordDaySlot(byte[] data) {
|
||||
super(data);
|
||||
super(data, TYPE_DAY_SLOT);
|
||||
|
||||
int a = (data[4] & 0xFF) * 256 + (data[5] & 0xFF);
|
||||
if (a >= 144) {
|
||||
@ -50,14 +51,34 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
|
||||
|
||||
steps = (data[2] & 0xFF) * 256 + data[3] & 0xFF;
|
||||
|
||||
//?? data[6];
|
||||
//?? data[6]; atemp?? always 0
|
||||
secondsInactive = data[7] & 0xFF; // ?
|
||||
|
||||
int now = (int) (GregorianCalendar.getInstance().getTimeInMillis() / (3600 * 24 * 1000L));
|
||||
timestamp = now * 3600 * 24 + (slot / 6 * 3600 + slot % 6 * 10);
|
||||
Calendar slotTime = GregorianCalendar.getInstance();
|
||||
|
||||
slotTime.set(Calendar.MINUTE, (slot % 6) * 10);
|
||||
slotTime.set(Calendar.HOUR_OF_DAY, slot / 6);
|
||||
slotTime.set(Calendar.SECOND, 0);
|
||||
|
||||
timestamp = (int) (slotTime.getTimeInMillis() / 1000L);
|
||||
}
|
||||
|
||||
public String toString(){
|
||||
return String.format(Locale.US, "Slot: %d, Steps: %d, InactiveSeconds: %d, HeartRate: %d", slot, steps, secondsInactive, heartRate);
|
||||
Calendar slotTime = GregorianCalendar.getInstance();
|
||||
slotTime.setTimeInMillis(timestamp * 1000L);
|
||||
return String.format(Locale.US, "Slot: %d, Time: %s, Steps: %d, InactiveSeconds: %d, HeartRate: %d", slot, slotTime.getTime(), steps, secondsInactive, heartRate);
|
||||
}
|
||||
|
||||
public void add(HPlusDataRecordDaySlot other){
|
||||
if(other == null)
|
||||
return;
|
||||
|
||||
steps += other.steps;
|
||||
secondsInactive += other.secondsInactive;
|
||||
if(heartRate == -1)
|
||||
heartRate = other.heartRate;
|
||||
else if(other.heartRate != -1) {
|
||||
heartRate = (heartRate + other.heartRate) / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ class HPlusDataRecordDaySummary extends HPlusDataRecord{
|
||||
public int calories;
|
||||
|
||||
HPlusDataRecordDaySummary(byte[] data) {
|
||||
super(data);
|
||||
super(data, TYPE_DAY_SUMMARY);
|
||||
|
||||
year = (data[10] & 0xFF) * 256 + (data[9] & 0xFF);
|
||||
month = data[11] & 0xFF;
|
||||
@ -75,7 +75,7 @@ class HPlusDataRecordDaySummary extends HPlusDataRecord{
|
||||
throw new IllegalArgumentException("Invalid record date "+year+"-"+month+"-"+day);
|
||||
}
|
||||
steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
|
||||
distance = (data[4] & 0xFF) * 256 + (data[3] & 0xFF);
|
||||
distance = ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)) * 10;
|
||||
activeTime = (data[14] & 0xFF) * 256 + (data[13] & 0xFF);
|
||||
calories = (data[6] & 0xFF) * 256 + (data[5] & 0xFF);
|
||||
calories += (data[8] & 0xFF) * 256 + (data[7] & 0xFF);
|
||||
|
@ -33,6 +33,11 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
|
||||
*/
|
||||
public byte battery;
|
||||
|
||||
/**
|
||||
* Number of steps today
|
||||
*/
|
||||
public int steps;
|
||||
|
||||
/**
|
||||
* Time active (To be determined how it works)
|
||||
*/
|
||||
@ -45,7 +50,7 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
|
||||
public int intensity;
|
||||
|
||||
public HPlusDataRecordRealtime(byte[] data) {
|
||||
super(data);
|
||||
super(data, TYPE_REALTIME);
|
||||
|
||||
if (data.length < 15) {
|
||||
throw new IllegalArgumentException("Invalid data packet");
|
||||
@ -53,7 +58,7 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
|
||||
|
||||
timestamp = (int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000);
|
||||
distance = 10 * ((data[4] & 0xFF) * 256 + (data[3] & 0xFF)); // meters
|
||||
|
||||
steps = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
|
||||
int x = (data[6] & 0xFF) * 256 + data[5] & 0xFF;
|
||||
int y = (data[8] & 0xFF) * 256 + data[7] & 0xFF;
|
||||
|
||||
@ -63,10 +68,14 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
|
||||
|
||||
heartRate = data[11] & 0xFF; // BPM
|
||||
activeTime = (data[14] & 0xFF * 256) + (data[13] & 0xFF);
|
||||
if(heartRate == 255)
|
||||
if(heartRate == 255) {
|
||||
intensity = 0;
|
||||
else
|
||||
activityKind = ActivityKind.TYPE_NOT_MEASURED;
|
||||
}
|
||||
else {
|
||||
intensity = (int) (100 * Math.max(0, Math.min((heartRate - 60) / 120.0, 1))); // TODO: Calculate a proper value
|
||||
activityKind = ActivityKind.TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public void computeActivity(HPlusDataRecordRealtime prev){
|
||||
@ -93,11 +102,11 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
|
||||
if(other == null)
|
||||
return false;
|
||||
|
||||
return distance == other.distance && calories == other.calories && heartRate == other.heartRate && battery == other.battery;
|
||||
return steps == other.steps && distance == other.distance && calories == other.calories && heartRate == other.heartRate && battery == other.battery;
|
||||
}
|
||||
|
||||
public String toString(){
|
||||
return String.format(Locale.US, "Distance: %d Calories: %d HeartRate: %d Battery: %d ActiveTime: %d Intensity: %d", distance, calories, heartRate, battery, activeTime, intensity);
|
||||
return String.format(Locale.US, "Distance: %d Steps: %d Calories: %d HeartRate: %d Battery: %d ActiveTime: %d Intensity: %d", distance, steps, calories, heartRate, battery, activeTime, intensity);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
@ -67,7 +68,7 @@ public class HPlusDataRecordSleep extends HPlusDataRecord {
|
||||
public int wakeupCount;
|
||||
|
||||
public HPlusDataRecordSleep(byte[] data) {
|
||||
super(data);
|
||||
super(data, TYPE_SLEEP);
|
||||
|
||||
int year = (data[2] & 0xFF) * 256 + (data[1] & 0xFF);
|
||||
int month = data[3] & 0xFF;
|
||||
@ -91,6 +92,7 @@ public class HPlusDataRecordSleep extends HPlusDataRecord {
|
||||
int minute = data[18] & 0xFF;
|
||||
|
||||
Calendar sleepStart = GregorianCalendar.getInstance();
|
||||
sleepStart.clear();
|
||||
sleepStart.set(Calendar.YEAR, year);
|
||||
sleepStart.set(Calendar.MONTH, month - 1);
|
||||
sleepStart.set(Calendar.DAY_OF_MONTH, day);
|
||||
@ -114,4 +116,14 @@ public class HPlusDataRecordSleep extends HPlusDataRecord {
|
||||
intervals.add(new RecordInterval(ts, bedTimeEnd, ActivityKind.TYPE_DEEP_SLEEP));
|
||||
return intervals;
|
||||
}
|
||||
|
||||
public String toString(){
|
||||
Calendar s = GregorianCalendar.getInstance();
|
||||
s.setTimeInMillis(bedTimeStart * 1000L);
|
||||
|
||||
Calendar end = GregorianCalendar.getInstance();
|
||||
end.setTimeInMillis(bedTimeEnd * 1000L);
|
||||
|
||||
return String.format(Locale.US, "Sleep start: %s end: %s enter: %d spindles: %d rem: %d deep: %d wake: %d-%d", s.getTime(), end.getTime(), enterSleepMinutes, spindleMinutes, remSleepMinutes, deepSleepMinutes, wakeupMinutes, wakeupCount);
|
||||
}
|
||||
}
|
||||
|
@ -6,14 +6,21 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.util.Log;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
@ -28,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
|
||||
@ -35,20 +43,22 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HPlusHandlerThread.class);
|
||||
|
||||
private int CURRENT_DAY_SYNC_PERIOD = 60 * 10;
|
||||
private int CURRENT_DAY_SYNC_PERIOD = 24 * 60 * 60 * 365; //Never
|
||||
private int CURRENT_DAY_SYNC_RETRY_PERIOD = 10;
|
||||
|
||||
private int SLEEP_SYNC_PERIOD = 12 * 60 * 60;
|
||||
private int SLEEP_SYNC_RETRY_PERIOD = 30;
|
||||
|
||||
private int DAY_SUMMARY_SYNC_PERIOD = 24 * 60 * 60;
|
||||
private int DAY_SUMMARY_SYNC_RETRY_PERIOD = 30;
|
||||
|
||||
private int HELLO_INTERVAL = 60;
|
||||
|
||||
private boolean mQuit = false;
|
||||
private HPlusSupport mHPlusSupport;
|
||||
private int mLastSlotReceived = 0;
|
||||
|
||||
private int mLastSlotReceived = -1;
|
||||
private int mLastSlotRequested = 0;
|
||||
private int mSlotsToRequest = 6;
|
||||
|
||||
private Calendar mLastSleepDayReceived = GregorianCalendar.getInstance();
|
||||
private Calendar mHelloTime = GregorianCalendar.getInstance();
|
||||
@ -56,10 +66,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
private Calendar mGetSleepTime = GregorianCalendar.getInstance();
|
||||
private Calendar mGetDaySummaryTime = GregorianCalendar.getInstance();
|
||||
|
||||
private boolean mSlotsInitialSync = true;
|
||||
|
||||
private HPlusDataRecordRealtime prevRealTimeRecord = null;
|
||||
|
||||
private final Object waitObject = new Object();
|
||||
List<HPlusHealthActivitySample> mDaySlotSamples = new ArrayList<>();
|
||||
|
||||
List<HPlusDataRecordDaySlot> mDaySlotSamples = new ArrayList<>();
|
||||
|
||||
public HPlusHandlerThread(GBDevice gbDevice, Context context, HPlusSupport hplusSupport) {
|
||||
super(gbDevice, context);
|
||||
@ -78,7 +91,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
|
||||
long waitTime = 0;
|
||||
while (!mQuit) {
|
||||
//LOG.debug("Waiting " + (waitTime));
|
||||
|
||||
if (waitTime > 0) {
|
||||
synchronized (waitObject) {
|
||||
try {
|
||||
@ -88,10 +101,16 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mQuit) {
|
||||
break;
|
||||
}
|
||||
|
||||
if(!mHPlusSupport.getDevice().isConnected()){
|
||||
quit();
|
||||
break;
|
||||
}
|
||||
|
||||
Calendar now = GregorianCalendar.getInstance();
|
||||
|
||||
if (now.compareTo(mHelloTime) > 0) {
|
||||
@ -111,7 +130,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
}
|
||||
|
||||
now = GregorianCalendar.getInstance();
|
||||
waitTime = Math.min(Math.min(mGetDaySlotsTime.getTimeInMillis(), mGetSleepTime.getTimeInMillis()), mHelloTime.getTimeInMillis()) - now.getTimeInMillis();
|
||||
waitTime = Math.min(mGetDaySummaryTime.getTimeInMillis(), Math.min(Math.min(mGetDaySlotsTime.getTimeInMillis(), mGetSleepTime.getTimeInMillis()), mHelloTime.getTimeInMillis())) - now.getTimeInMillis();
|
||||
}
|
||||
|
||||
}
|
||||
@ -128,151 +147,153 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
mGetSleepTime.setTimeInMillis(0);
|
||||
mGetDaySlotsTime.setTimeInMillis(0);
|
||||
mGetDaySummaryTime.setTimeInMillis(0);
|
||||
mLastSleepDayReceived.setTimeInMillis(0);
|
||||
|
||||
mSlotsInitialSync = true;
|
||||
mLastSlotReceived = -1;
|
||||
mLastSlotRequested = 0;
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder("startSyncDayStats");
|
||||
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY});
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DEVICE_ID});
|
||||
builder.wait(400);
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_VERSION});
|
||||
builder.wait(400);
|
||||
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
|
||||
builder.wait(400);
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
|
||||
builder.wait(400);
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY});
|
||||
builder.wait(400);
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA});
|
||||
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALLDAY_HRM, HPlusConstants.ARG_HEARTRATE_ALLDAY_ON});
|
||||
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
scheduleHello();
|
||||
|
||||
synchronized (waitObject) {
|
||||
waitObject.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Hello/Null Packet to keep connection
|
||||
*/
|
||||
private void sendHello() {
|
||||
TransactionBuilder builder = new TransactionBuilder("hello");
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, HPlusConstants.CMD_ACTION_HELLO);
|
||||
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, HPlusConstants.CMD_ACTION_HELLO);
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
|
||||
scheduleHello();
|
||||
|
||||
synchronized (waitObject) {
|
||||
waitObject.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an Hello Packet in the future
|
||||
*/
|
||||
public void scheduleHello(){
|
||||
mHelloTime = GregorianCalendar.getInstance();
|
||||
mHelloTime.add(Calendar.SECOND, HELLO_INTERVAL);
|
||||
}
|
||||
|
||||
public void requestDaySummaryData(){
|
||||
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
|
||||
mGetDaySummaryTime = GregorianCalendar.getInstance();
|
||||
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a message containing information regarding a day slot
|
||||
* A slot summarizes 10 minutes of data
|
||||
*
|
||||
* @param data the message from the device
|
||||
* @return boolean indicating success or fail
|
||||
*/
|
||||
public boolean processIncomingDaySlotData(byte[] data) {
|
||||
|
||||
HPlusDataRecordDaySlot record;
|
||||
|
||||
try{
|
||||
record = new HPlusDataRecordDaySlot(data);
|
||||
} catch(IllegalArgumentException e){
|
||||
LOG.debug((e.getMessage()));
|
||||
return false;
|
||||
}
|
||||
|
||||
//Ignore real time messages as they are still not understood
|
||||
if(!mSlotsInitialSync){
|
||||
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
|
||||
return true;
|
||||
}
|
||||
mLastSlotReceived = record.slot;
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
|
||||
Calendar now = GregorianCalendar.getInstance();
|
||||
int nowSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + (now.get(Calendar.MINUTE) / 10);
|
||||
|
||||
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
|
||||
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
|
||||
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
|
||||
record.timestamp, // ts
|
||||
deviceId, userId, // User id
|
||||
record.getRawData(), // Raw Data
|
||||
ActivityKind.TYPE_UNKNOWN,
|
||||
0, // Intensity
|
||||
record.steps, // Steps
|
||||
record.heartRate, // HR
|
||||
ActivitySample.NOT_MEASURED, // Distance
|
||||
ActivitySample.NOT_MEASURED // Calories
|
||||
);
|
||||
sample.setProvider(provider);
|
||||
mDaySlotSamples.add(sample);
|
||||
|
||||
Calendar now = GregorianCalendar.getInstance();
|
||||
|
||||
//Dump buffered samples to DB
|
||||
if ((record.timestamp + (60*100) >= (now.getTimeInMillis() / 1000L) )) {
|
||||
|
||||
provider.getSampleDao().insertOrReplaceInTx(mDaySlotSamples);
|
||||
|
||||
mGetDaySlotsTime.setTimeInMillis(0);
|
||||
mDaySlotSamples.clear();
|
||||
mSlotsToRequest = 144 - mLastSlotReceived;
|
||||
}
|
||||
} catch (GBException ex) {
|
||||
LOG.debug((ex.getMessage()));
|
||||
} catch (Exception ex) {
|
||||
LOG.debug(ex.getMessage());
|
||||
//If the slot is in the future, actually it is from the previous day
|
||||
//Subtract a day of seconds
|
||||
if(record.slot >= nowSlot){
|
||||
record.timestamp -= 3600 * 24;
|
||||
}
|
||||
|
||||
//Request next slot
|
||||
if(mLastSlotReceived == mLastSlotRequested){
|
||||
//Ignore out of order messages
|
||||
if(record.slot == mLastSlotReceived + 1) {
|
||||
mLastSlotReceived = record.slot;
|
||||
}
|
||||
|
||||
if(record.slot < 143){
|
||||
mDaySlotSamples.add(record);
|
||||
}else {
|
||||
|
||||
//Sort the samples
|
||||
Collections.sort(mDaySlotSamples, new Comparator<HPlusDataRecordDaySlot>() {
|
||||
public int compare(HPlusDataRecordDaySlot one, HPlusDataRecordDaySlot other) {
|
||||
return one.timestamp - other.timestamp;
|
||||
}
|
||||
});
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
|
||||
List<HPlusHealthActivitySample> samples = new ArrayList<>();
|
||||
|
||||
for(HPlusDataRecordDaySlot storedRecord : mDaySlotSamples) {
|
||||
HPlusHealthActivitySample sample = createSample(dbHandler, storedRecord.timestamp);
|
||||
|
||||
sample.setRawHPlusHealthData(record.getRawData());
|
||||
sample.setSteps(record.steps);
|
||||
sample.setHeartRate(record.heartRate);
|
||||
sample.setRawKind(record.type);
|
||||
|
||||
sample.setProvider(provider);
|
||||
samples.add(sample);
|
||||
}
|
||||
|
||||
provider.getSampleDao().insertOrReplaceInTx(samples);
|
||||
mDaySlotSamples.clear();
|
||||
|
||||
} catch (GBException ex) {
|
||||
LOG.debug((ex.getMessage()));
|
||||
} catch (Exception ex) {
|
||||
LOG.debug(ex.getMessage());
|
||||
}
|
||||
}
|
||||
//Still fetching ring buffer. Request the next slots
|
||||
if (record.slot == mLastSlotRequested) {
|
||||
mGetDaySlotsTime.clear();
|
||||
synchronized (waitObject) {
|
||||
mGetDaySlotsTime.setTimeInMillis(0);
|
||||
waitObject.notify();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void requestNextDaySlots() {
|
||||
|
||||
Calendar now = GregorianCalendar.getInstance();
|
||||
|
||||
if (mLastSlotReceived >= 144 + 6) { // 24 * 6 + 6
|
||||
LOG.debug("Reached End of the Day");
|
||||
mLastSlotReceived = 0;
|
||||
mSlotsToRequest = 6; // 1h
|
||||
mGetDaySlotsTime = now;
|
||||
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
|
||||
mLastSlotRequested = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
//Sync Day Stats
|
||||
mLastSlotRequested = Math.min(mLastSlotReceived + mSlotsToRequest, 144);
|
||||
|
||||
LOG.debug("Requesting slot " + mLastSlotRequested);
|
||||
|
||||
byte nextHour = (byte) (mLastSlotRequested / 6);
|
||||
byte nextMinute = (byte) ((mLastSlotRequested % 6) * 10);
|
||||
|
||||
if (nextHour == (byte) now.get(Calendar.HOUR_OF_DAY)) {
|
||||
nextMinute = (byte) now.get(Calendar.MINUTE);
|
||||
}
|
||||
|
||||
byte hour = (byte) (mLastSlotReceived / 6);
|
||||
byte minute = (byte) ((mLastSlotReceived % 6) * 10);
|
||||
|
||||
byte[] msg = new byte[]{39, hour, minute, nextHour, nextMinute};
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
|
||||
mGetDaySlotsTime = now;
|
||||
if(mSlotsToRequest == 6) {
|
||||
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_RETRY_PERIOD);
|
||||
}else{
|
||||
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
|
||||
}
|
||||
LOG.debug("Requesting next slot " + mLastSlotRequested+ " at " + mGetDaySlotsTime.getTime());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Process sleep data from the device
|
||||
* Devices send a single sleep message for each sleep period
|
||||
* This message contains the duration of the sub-intervals (rem, deep, etc...)
|
||||
*
|
||||
* @param data the message from the device
|
||||
* @return boolean indicating success or fail
|
||||
*/
|
||||
public boolean processIncomingSleepData(byte[] data){
|
||||
HPlusDataRecordSleep record;
|
||||
|
||||
@ -280,7 +301,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
record = new HPlusDataRecordSleep(data);
|
||||
} catch(IllegalArgumentException e){
|
||||
LOG.debug((e.getMessage()));
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
mLastSleepDayReceived.setTimeInMillis(record.bedTimeStart * 1000L);
|
||||
@ -293,9 +314,10 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
HPlusHealthActivityOverlayDao overlayDao = session.getHPlusHealthActivityOverlayDao();
|
||||
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
|
||||
|
||||
//Insert the Overlays
|
||||
//Get the individual Sleep overlays and insert them
|
||||
List<HPlusHealthActivityOverlay> overlayList = new ArrayList<>();
|
||||
List<HPlusDataRecord.RecordInterval> intervals = record.getIntervals();
|
||||
|
||||
for(HPlusDataRecord.RecordInterval interval : intervals){
|
||||
overlayList.add(new HPlusHealthActivityOverlay(interval.timestampFrom, interval.timestampTo, interval.activityKind, deviceId, userId, null));
|
||||
}
|
||||
@ -303,23 +325,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
overlayDao.insertOrReplaceInTx(overlayList);
|
||||
|
||||
//Store the data
|
||||
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
|
||||
record.timestamp, // ts
|
||||
deviceId, userId, // User id
|
||||
record.getRawData(), // Raw Data
|
||||
record.activityKind,
|
||||
0, // Intensity
|
||||
ActivitySample.NOT_MEASURED, // Steps
|
||||
ActivitySample.NOT_MEASURED, // HR
|
||||
ActivitySample.NOT_MEASURED, // Distance
|
||||
ActivitySample.NOT_MEASURED // Calories
|
||||
);
|
||||
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
|
||||
sample.setRawHPlusHealthData(record.getRawData());
|
||||
sample.setRawKind(record.activityKind);
|
||||
|
||||
sample.setProvider(provider);
|
||||
|
||||
provider.addGBActivitySample(sample);
|
||||
|
||||
|
||||
} catch (Exception ex) {
|
||||
LOG.debug(ex.getMessage());
|
||||
}
|
||||
@ -330,16 +342,12 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void requestNextSleepData() {
|
||||
mGetSleepTime = GregorianCalendar.getInstance();
|
||||
mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_RETRY_PERIOD);
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a message containing real time information
|
||||
*
|
||||
* @param data the message from the device
|
||||
* @return boolean indicating success or fail
|
||||
*/
|
||||
public boolean processRealtimeStats(byte[] data) {
|
||||
HPlusDataRecordRealtime record;
|
||||
|
||||
@ -347,49 +355,59 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
record = new HPlusDataRecordRealtime(data);
|
||||
} catch(IllegalArgumentException e){
|
||||
LOG.debug((e.getMessage()));
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(record.same(prevRealTimeRecord))
|
||||
//Skip duplicated messages as the device seems to send the same record multiple times
|
||||
//This can be used to detect the user is moving (not sleeping)
|
||||
if(prevRealTimeRecord != null && record.same(prevRealTimeRecord))
|
||||
return true;
|
||||
|
||||
prevRealTimeRecord = record;
|
||||
|
||||
getDevice().setBatteryLevel(record.battery);
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
|
||||
//Skip when measuring
|
||||
if(record.heartRate == 255) {
|
||||
//Skip when measuring heart rate
|
||||
//Calories and Distance are updated and these values will be lost.
|
||||
//Because a message with a valid Heart Rate will be provided, this loss very limited
|
||||
if(record.heartRate == ActivityKind.TYPE_NOT_MEASURED) {
|
||||
getDevice().setFirmwareVersion2("---");
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
return true;
|
||||
}else {
|
||||
getDevice().setFirmwareVersion2("" + record.heartRate);
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
|
||||
getDevice().setFirmwareVersion2("" + record.heartRate);
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
|
||||
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
|
||||
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
|
||||
|
||||
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
|
||||
record.timestamp, // ts
|
||||
deviceId, userId, // User id
|
||||
record.getRawData(), // Raw Data
|
||||
record.activityKind,
|
||||
record.intensity, // Intensity
|
||||
ActivitySample.NOT_MEASURED, // Steps
|
||||
record.heartRate, // HR
|
||||
record.distance, // Distance
|
||||
record.calories // Calories
|
||||
);
|
||||
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
|
||||
sample.setRawKind(record.type);
|
||||
sample.setRawIntensity(record.intensity);
|
||||
sample.setHeartRate(record.heartRate);
|
||||
sample.setDistance(record.distance);
|
||||
sample.setCalories(record.calories);
|
||||
sample.setSteps(record.steps);
|
||||
|
||||
sample.setRawHPlusHealthData(record.getRawData());
|
||||
sample.setProvider(provider);
|
||||
|
||||
if (sample.getSteps() != ActivitySample.NOT_MEASURED && sample.getSteps() - prevRealTimeRecord.steps > 0) {
|
||||
Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS)
|
||||
.putExtra(DeviceService.EXTRA_REALTIME_STEPS, sample.getSteps() - prevRealTimeRecord.steps)
|
||||
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
if (sample.getHeartRate() != ActivitySample.NOT_MEASURED) {
|
||||
Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT)
|
||||
.putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, sample.getHeartRate())
|
||||
.putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis());
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
provider.addGBActivitySample(sample);
|
||||
|
||||
//TODO: Handle Active Time. With Overlay?
|
||||
|
||||
} catch (GBException ex) {
|
||||
LOG.debug((ex.getMessage()));
|
||||
} catch (Exception ex) {
|
||||
@ -398,57 +416,35 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a day summary message
|
||||
* This message includes aggregates regarding an entire day
|
||||
*
|
||||
* @param data the message from the device
|
||||
* @return boolean indicating success or fail
|
||||
*/
|
||||
public boolean processDaySummary(byte[] data) {
|
||||
LOG.debug("Process Day Summary");
|
||||
HPlusDataRecordDaySummary record;
|
||||
|
||||
try{
|
||||
record = new HPlusDataRecordDaySummary(data);
|
||||
} catch(IllegalArgumentException e){
|
||||
LOG.debug((e.getMessage()));
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
LOG.debug("Received: " + record);
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
|
||||
|
||||
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
|
||||
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
|
||||
HPlusHealthActivitySample sample = createSample(dbHandler, record.timestamp);
|
||||
|
||||
//Hugly (?) fix.
|
||||
//This message returns the day summary, but the DB already has some detailed entries with steps and distance.
|
||||
//However DB data is probably incomplete as some update messages could be missing
|
||||
//Proposed fix: Calculate the total steps and distance and store a new sample with the remaining data
|
||||
//Existing data will reflect user activity with the issue of a potencially large number of steps at midnight.
|
||||
//Steps counters by day will be OK with this
|
||||
|
||||
List<HPlusHealthActivitySample> samples = provider.getActivitySamples(record.timestamp - 3600 * 24 + 1, record.timestamp);
|
||||
|
||||
int missingDistance = record.distance;
|
||||
int missingSteps = record.steps;
|
||||
|
||||
for(HPlusHealthActivitySample sample : samples){
|
||||
if(sample.getSteps() > 0) {
|
||||
missingSteps -= sample.getSteps();
|
||||
}
|
||||
if(sample.getDistance() > 0){
|
||||
missingDistance -= sample.getDistance();
|
||||
}
|
||||
}
|
||||
|
||||
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
|
||||
record.timestamp, // ts
|
||||
deviceId, userId, // User id
|
||||
record.getRawData(), // Raw Data
|
||||
ActivityKind.TYPE_UNKNOWN,
|
||||
0, // Intensity
|
||||
Math.max( missingSteps, 0), // Steps
|
||||
ActivitySample.NOT_MEASURED, // HR
|
||||
Math.max( missingDistance, 0), // Distance
|
||||
ActivitySample.NOT_MEASURED // Calories
|
||||
);
|
||||
sample.setRawKind(record.type);
|
||||
sample.setSteps(record.steps);
|
||||
sample.setDistance(record.distance);
|
||||
sample.setCalories(record.calories);
|
||||
sample.setDistance(record.distance);
|
||||
sample.setHeartRate((record.maxHeartRate - record.minHeartRate) / 2); //TODO: Find an alternative approach for Day Summary Heart Rate
|
||||
sample.setRawHPlusHealthData(record.getRawData());
|
||||
|
||||
sample.setProvider(provider);
|
||||
provider.addGBActivitySample(sample);
|
||||
@ -458,9 +454,17 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
LOG.debug(ex.getMessage());
|
||||
}
|
||||
|
||||
mGetDaySummaryTime = GregorianCalendar.getInstance();
|
||||
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_PERIOD);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a message containing information regarding firmware version
|
||||
*
|
||||
* @param data the message from the device
|
||||
* @return boolean indicating success or fail
|
||||
*/
|
||||
public boolean processVersion(byte[] data) {
|
||||
int major = data[2] & 0xFF;
|
||||
int minor = data[1] & 0xFF;
|
||||
@ -471,4 +475,102 @@ class HPlusHandlerThread extends GBDeviceIoThread {
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a message requesting the next batch of sleep data
|
||||
*/
|
||||
private void requestNextSleepData() {
|
||||
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
|
||||
|
||||
mGetSleepTime = GregorianCalendar.getInstance();
|
||||
mGetSleepTime.add(GregorianCalendar.SECOND, SLEEP_SYNC_RETRY_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a message requesting the next set of slots
|
||||
* The process will sync 1h at a time until the device is in sync
|
||||
* Then it will request samples until the end of the day in order to minimize data loss
|
||||
* Messages will be provided every 10 minutes after they are available
|
||||
*/
|
||||
private void requestNextDaySlots() {
|
||||
|
||||
Calendar now = GregorianCalendar.getInstance();
|
||||
int currentSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + now.get(Calendar.MINUTE) / 10;
|
||||
|
||||
//Finished dumping the entire ring buffer
|
||||
//Sync to current time
|
||||
mGetDaySlotsTime = now;
|
||||
|
||||
if(mSlotsInitialSync) {
|
||||
if(mLastSlotReceived == 143) {
|
||||
mSlotsInitialSync = false;
|
||||
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); //Sync complete. Delay timer forever
|
||||
mLastSlotReceived = -1;
|
||||
mLastSlotRequested = mLastSlotReceived + 1;
|
||||
return;
|
||||
}else {
|
||||
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_RETRY_PERIOD);
|
||||
}
|
||||
}else{
|
||||
//Sync complete. Delay timer forever
|
||||
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
|
||||
return;
|
||||
}
|
||||
|
||||
if(mLastSlotReceived == 143)
|
||||
mLastSlotReceived = -1;
|
||||
|
||||
byte hour = (byte) ((mLastSlotReceived + 1)/ 6);
|
||||
byte minute = (byte) (((mLastSlotReceived + 1) % 6) * 10);
|
||||
|
||||
byte nextHour = hour;
|
||||
byte nextMinute = 59;
|
||||
|
||||
mLastSlotRequested = nextHour * 6 + (nextMinute / 10);
|
||||
|
||||
byte[] msg = new byte[]{HPlusConstants.CMD_GET_ACTIVE_DAY, hour, minute, nextHour, nextMinute};
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
}
|
||||
/**
|
||||
* Request a batch of data with the summary of the previous days
|
||||
*/
|
||||
public void requestDaySummaryData(){
|
||||
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
|
||||
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
|
||||
builder.queue(mHPlusSupport.getQueue());
|
||||
|
||||
mGetDaySummaryTime = GregorianCalendar.getInstance();
|
||||
mGetDaySummaryTime.add(Calendar.SECOND, DAY_SUMMARY_SYNC_RETRY_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a sample
|
||||
* @param dbHandler The database handler
|
||||
* @param timestamp The sample timestamp
|
||||
* @return The sample just created
|
||||
*/
|
||||
private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp){
|
||||
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
|
||||
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
|
||||
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(
|
||||
timestamp, // ts
|
||||
deviceId, userId, // User id
|
||||
null, // Raw Data
|
||||
ActivityKind.TYPE_UNKNOWN,
|
||||
0, // Intensity
|
||||
ActivitySample.NOT_MEASURED, // Steps
|
||||
ActivitySample.NOT_MEASURED, // HR
|
||||
ActivitySample.NOT_MEASURED, // Distance
|
||||
ActivitySample.NOT_MEASURED // Calories
|
||||
);
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@ -344,14 +345,23 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
private HPlusSupport setAllDayHeart(TransactionBuilder transaction) {
|
||||
LOG.info("Attempting to set All Day HR...");
|
||||
|
||||
byte value = HPlusCoordinator.getAllDayHR(getDevice().getAddress());
|
||||
byte value = HPlusCoordinator.getHRState(getDevice().getAddress());
|
||||
|
||||
transaction.write(ctrlCharacteristic, new byte[]{
|
||||
HPlusConstants.CMD_SET_HEARTRATE_STATE,
|
||||
value
|
||||
});
|
||||
|
||||
|
||||
value = HPlusCoordinator.getAllDayHR(getDevice().getAddress());
|
||||
|
||||
transaction.write(ctrlCharacteristic, new byte[]{
|
||||
HPlusConstants.CMD_SET_ALLDAY_HRM,
|
||||
value
|
||||
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -415,6 +425,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
//TODO: Show different notifications according to source as Band supports this
|
||||
//LOG.debug("OnNotification: Title: "+notificationSpec.title+" Body: "+notificationSpec.body+" Source: "+notificationSpec.sourceName+" Sender: "+notificationSpec.sender+" Subject: "+notificationSpec.subject);
|
||||
showText(notificationSpec.title, notificationSpec.body);
|
||||
}
|
||||
|
||||
@ -534,7 +545,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder("HeartRateTest");
|
||||
|
||||
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_ALLDAY_HRM, 0x0A}); //Set Real Time... ?
|
||||
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_HEARTRATE_STATE, HPlusConstants.ARG_HEARTRATE_MEASURE_ON}); //Set Real Time... ?
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
@ -629,20 +640,14 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_ACTION_INCOMING_CALL, 1});
|
||||
|
||||
//Show Call Icon
|
||||
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_ACTION_INCOMING_CALL, HPlusConstants.ARG_INCOMING_CALL});
|
||||
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_CALL, HPlusConstants.ARG_INCOMING_CALL});
|
||||
|
||||
builder.queue(getQueue());
|
||||
|
||||
//TODO: Use WaitAction
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
byte[] msg = new byte[13];
|
||||
|
||||
builder = performInitialized("incomingCallNumber");
|
||||
builder.wait(200);
|
||||
|
||||
//Show call number
|
||||
for (int i = 0; i < msg.length; i++)
|
||||
@ -657,13 +662,8 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
builder.write(ctrlCharacteristic, msg);
|
||||
builder.queue(getQueue());
|
||||
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
builder = performInitialized("incomingCallText");
|
||||
builder.wait(200);
|
||||
|
||||
//Show call name
|
||||
//Must call twice, otherwise nothing happens
|
||||
@ -676,11 +676,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME;
|
||||
builder.write(ctrlCharacteristic, msg);
|
||||
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
builder.wait(200);
|
||||
|
||||
msg[0] = HPlusConstants.CMD_ACTION_DISPLAY_TEXT_NAME_CN;
|
||||
builder.write(ctrlCharacteristic, msg);
|
||||
@ -693,6 +689,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
private void showText(String title, String body) {
|
||||
LOG.debug("Show Notification: "+title+" --> "+body);
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("notification");
|
||||
|
||||
@ -720,6 +717,8 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_ACTION_INCOMING_SOCIAL, (byte) 255});
|
||||
|
||||
builder.write(ctrlCharacteristic, new byte[]{HPlusConstants.CMD_SET_INCOMING_MESSAGE, HPlusConstants.ARG_INCOMING_MESSAGE});
|
||||
|
||||
int remaining;
|
||||
|
||||
if (message.length() % 17 > 0)
|
||||
@ -801,4 +800,5 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user