From 125c0092cbb8f653a6cab2579ce698ddd61e9ea7 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Sun, 4 Sep 2016 00:02:58 +0200 Subject: [PATCH] Storage and improved way of realtime data (hr, steps so far) --- .../gadgetbridge/daogen/GBDaoGenerator.java | 1 + .../entities/AbstractActivitySample.java | 3 + .../gadgetbridge/model/DeviceService.java | 14 +++ .../service/devices/miband/MiBandSupport.java | 96 +++++++++++++++++-- .../miband/RealtimeSamplesSupport.java | 86 +++++++++++++++++ .../operations/FetchActivityOperation.java | 21 ++-- 6 files changed, 202 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 499ebc0ae..9a834ca48 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -162,6 +162,7 @@ public class GBDaoGenerator { private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "MiBandActivitySample"); + activitySample.implementsSerializable(); addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java index 06718116d..08f09c116 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractActivitySample.java @@ -40,6 +40,9 @@ public abstract class AbstractActivitySample implements ActivitySample { public void setSteps(int steps) { } + /** + * Unix timestamp of the sample, i.e. the number of seconds since 1970-01-01 00:00:00 UTC. + */ public abstract void setTimestamp(int timestamp); public abstract void setUserId(long userId); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index 0b664fcf6..6f75d01e8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -37,6 +37,11 @@ public interface DeviceService extends EventHandler { String ACTION_SET_CONSTANT_VIBRATION = PREFIX + ".action.set_constant_vibration"; String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms"; String ACTION_ENABLE_REALTIME_STEPS = PREFIX + ".action.enable_realtime_steps"; + String ACTION_REALTIME_SAMPLES = PREFIX + ".action.realtime_samples"; + /** + * Use EXTRA_REALTIME_SAMPLE instead + */ + @Deprecated String ACTION_REALTIME_STEPS = PREFIX + ".action.realtime_steps"; String ACTION_ENABLE_REALTIME_HEARTRATE_MEASUREMENT = PREFIX + ".action.realtime_hr_measurement"; String ACTION_ENABLE_HEARTRATE_SLEEP_SUPPORT = PREFIX + ".action.enable_heartrate_sleep_support"; @@ -77,8 +82,17 @@ public interface DeviceService extends EventHandler { String EXTRA_ALARMS = "alarms"; String EXTRA_PERFORM_PAIR = "perform_pair"; String EXTRA_BOOLEAN_ENABLE = "enable_realtime_steps"; + /** + * Use EXTRA_REALTIME_SAMPLE instead + */ + @Deprecated String EXTRA_REALTIME_STEPS = "realtime_steps"; + String EXTRA_REALTIME_SAMPLE = "realtime_sample"; String EXTRA_TIMESTAMP = "timestamp"; + /** + * Use EXTRA_REALTIME_SAMPLE instead + */ + @Deprecated String EXTRA_HEART_RATE_VALUE = "hr_value"; String EXTRA_CALENDAREVENT_ID = "calendarevent_id"; String EXTRA_CALENDAREVENT_TYPE = "calendarevent_type"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java index f2b4de9e3..ab76abbe6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBandSupport.java @@ -21,16 +21,26 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents; @@ -93,6 +103,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + private RealtimeSamplesSupport realtimeSamplesSupport; public MiBandSupport() { addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); @@ -663,6 +674,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); } builder.queue(getQueue()); + enableRealtimeSamplesTimer(enable); } catch (IOException ex) { LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex); } @@ -713,6 +725,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications") .write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), enable ? getLowLatency() : getHighLatency()) .write(controlPoint, enable ? startRealTimeStepsNotifications : stopRealTimeStepsNotifications).queue(getQueue()); + enableRealtimeSamplesTimer(enable); } catch (IOException e) { LOG.error("Unable to change realtime steps notification to: " + enable, e); } @@ -909,10 +922,12 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { if (LOG.isDebugEnabled()) { LOG.debug("heart rate: " + hrValue); } - Intent intent = new Intent(DeviceService.ACTION_HEARTRATE_MEASUREMENT) - .putExtra(DeviceService.EXTRA_HEART_RATE_VALUE, hrValue) - .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); - LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport(); + realtimeSamplesSupport.setHeartrateBpm(hrValue); + if (!realtimeSamplesSupport.isRunning()) { + // single shot measurement, manually invoke storage and result publishing + realtimeSamplesSupport.triggerCurrentSample(); + } } } @@ -921,10 +936,75 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { if (LOG.isDebugEnabled()) { LOG.debug("realtime steps: " + steps); } - Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS) - .putExtra(DeviceService.EXTRA_REALTIME_STEPS, steps) - .putExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()); - LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + getRealtimeSamplesSupport().setSteps(steps); + } + + private void enableRealtimeSamplesTimer(boolean enable) { + if (enable) { + getRealtimeSamplesSupport().start(); + } else { + if (realtimeSamplesSupport != null) { + realtimeSamplesSupport.stop(); + } + } + } + + public MiBandActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) { + MiBandActivitySample sample = new MiBandActivitySample(); + sample.setDevice(device); + sample.setUser(user); + sample.setTimestamp(timestampInSeconds); + sample.setProvider(provider); + + return sample; + } + + private RealtimeSamplesSupport getRealtimeSamplesSupport() { + if (realtimeSamplesSupport == null) { + realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) { + @Override + public void doCurrentSample() { + + 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); + MiBandSampleProvider provider = new MiBandSampleProvider(gbDevice, session); + MiBandActivitySample sample = createActivitySample(device, user, ts, provider); + sample.setHeartRate(getHeartrateBpm()); + sample.setSteps(getSteps()); + sample.setRawIntensity(ActivitySample.NOT_MEASURED); + sample.setRawKind(MiBandSampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? + + // TODO: remove this once fully ported to REALTIME_SAMPLES + if (sample.getSteps() != ActivitySample.NOT_MEASURED) { + Intent intent = new Intent(DeviceService.ACTION_REALTIME_STEPS) + .putExtra(DeviceService.EXTRA_REALTIME_STEPS, sample.getSteps()) + .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); + } + +// Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) +// .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample); +// LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + + LOG.debug("Storing realtime sample: " + sample); + provider.addGBActivitySample(sample); + } catch (Exception e) { + LOG.warn("Unable to acquire db for saving realtime samples", e); + } + } + }; + } + return realtimeSamplesSupport; } /** diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java new file mode 100644 index 000000000..a160af8c6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/RealtimeSamplesSupport.java @@ -0,0 +1,86 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import java.util.Timer; +import java.util.TimerTask; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; + +/** + * Basic support for aggregating different sources of realtime data that comes in in a mostly + * fixed interval. The aggregated data will be stored together. + * + * start() and stop() may be called multiple times, but the first stop() call will really + * stop the timer. + * manner. + * + * Subclasses must implement #doCurrentSample() and should override #resetCurrentValues() + * (but call super!). + */ +public abstract class RealtimeSamplesSupport { + private final long delay; + private final long period; + + protected int steps; + protected int heartrateBpm; + // subclasses may add more + + private Timer realtimeStorageTimer; + + public RealtimeSamplesSupport(long delay, long period) { + this.delay = delay; + this.period = period; + } + + public synchronized void start() { + if (isRunning()) { + return; // already running + } + realtimeStorageTimer = new Timer("Mi Band Realtime Storage Timer"); + realtimeStorageTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + triggerCurrentSample(); + } + }, delay, period); + } + + public synchronized void stop() { + if (realtimeStorageTimer != null) { + realtimeStorageTimer.cancel(); + realtimeStorageTimer.purge(); + realtimeStorageTimer = null; + } + } + + public synchronized boolean isRunning() { + return realtimeStorageTimer != null; + } + + public void setSteps(int stepsPerMinute) { + this.steps = stepsPerMinute; + } + + public int getSteps() { + return steps; + } + + public void setHeartrateBpm(int hrBpm) { + this.heartrateBpm = hrBpm; + } + + public int getHeartrateBpm() { + return heartrateBpm; + } + + public void triggerCurrentSample() { + doCurrentSample(); + resetCurrentValues(); + } + + protected void resetCurrentValues() { + steps = ActivitySample.NOT_MEASURED; + heartrateBpm = ActivitySample.NOT_MEASURED; + } + + protected abstract void doCurrentSample(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/FetchActivityOperation.java index 9fba71ff3..d57b9c458 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/FetchActivityOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/FetchActivityOperation.java @@ -22,7 +22,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; @@ -307,8 +309,8 @@ public class FetchActivityOperation extends AbstractMiBandOperation { try (DBHandler dbHandler = GBApplication.acquireDB()){ MiBandSampleProvider provider = new MiBandSampleProvider(getDevice(), dbHandler.getDaoSession()); - Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); - Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()); int minutes = 0; try { int timestampInSeconds = (int) (activityStruct.activityDataTimestampProgress.getTimeInMillis() / 1000); @@ -327,15 +329,12 @@ public class FetchActivityOperation extends AbstractMiBandOperation { LOG.debug("heartrate received: " + (heartrate & 0xff)); } - samples[minutes] = new MiBandActivitySample( - timestampInSeconds, - deviceId, - userId, - intensity & 0xff, - steps & 0xff, - category & 0xff, - heartrate & 0xff); - samples[minutes].setProvider(provider); + MiBandActivitySample sample = getSupport().createActivitySample(device, user, timestampInSeconds, provider); + sample.setRawIntensity(intensity & 0xff); + sample.setSteps(steps & 0xff); + sample.setRawKind(category & 0xff); + sample.setHeartRate(heartrate & 0xff); + samples[minutes] = sample; if (LOG.isDebugEnabled()) { LOG.debug("sample: " + samples[minutes]);