From 09ff95eb343bbe38e07b52cd3fa38d82037ead71 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Tue, 29 Nov 2016 23:22:36 +0100 Subject: [PATCH] Support for continuous hr readings (live activity) #323 --- .../profiles/heartrate/HeartRateProfile.java | 8 +- .../devices/miband/MiBand2Support.java | 140 +++++++++++++----- 2 files changed, 106 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java index f5495124..ec195fce 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/heartrate/HeartRateProfile.java @@ -44,12 +44,6 @@ public class HeartRateProfile extends Abstr } - // TODO: I didn't find anything in the spec to request heart rate readings, so probably this - // should be done in a device specific way. - public void requestHeartRateMeasurement(TransactionBuilder builder) { - writeToControlPoint(new byte[] { 0x15, 0x02, 0x01}, builder); - } - @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { @@ -61,7 +55,7 @@ public class HeartRateProfile extends Abstr format = BluetoothGattCharacteristic.FORMAT_UINT8; } final int heartRate = characteristic.getIntValue(format, 1); - GB.toast(getContext(), "Heart rate: " + heartRate, Toast.LENGTH_LONG, GB.INFO); + LOG.info("Heart rate: " + heartRate, Toast.LENGTH_LONG, GB.INFO); } return false; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java index 963a34b5..62016c4d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java @@ -15,7 +15,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -26,19 +25,28 @@ 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.DateTimeDisplay; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; 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.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.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; 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; @@ -109,6 +117,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + private RealtimeSamplesSupport realtimeSamplesSupport; public MiBand2Support() { super(LOG); @@ -124,7 +133,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { deviceInfoProfile = new DeviceInfoProfile<>(this); addSupportedProfile(deviceInfoProfile); - heartRateProfile = new HeartRateProfile(this); + heartRateProfile = new HeartRateProfile<>(this); addSupportedProfile(heartRateProfile); LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); @@ -173,12 +182,6 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return builder; } -// private MiBand2Support maybeAuth(TransactionBuilder builder) { -// builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x20, 0x00}); -// builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x03,0x00,(byte)0x8e,(byte)0xce,0x5a,0x09,(byte)0xb3,(byte)0xd8,0x55,0x57,0x10,0x2a,(byte)0xed,0x7d,0x6b,0x78,(byte)0xc5,(byte)0xd2}); -// return this; -// } - public byte[] getTimeBytes(Calendar calendar) { byte[] bytes = BLETypeConversions.shortCalendarToRawBytes(calendar, true); byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone @@ -678,11 +681,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { public void onHeartRateTest() { try { TransactionBuilder builder = performInitialized("HeartRateTest"); - heartRateProfile.requestHeartRateMeasurement(builder); -// profile.resetEnergyExpended(builder); -// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); -// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); -// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementManual); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementManual); builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to read HearRate with MI2", ex); @@ -691,18 +692,19 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void onEnableRealtimeHeartRateMeasurement(boolean enable) { -// try { -// TransactionBuilder builder = performInitialized("EnableRealtimeHeartRateMeasurement"); -// if (enable) { -// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); -// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementContinuous); -// } else { -// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); -// } -// builder.queue(getQueue()); -// } catch (IOException ex) { -// LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex); -// } + try { + TransactionBuilder builder = performInitialized("Enable realtime heart rateM measurement"); + if (enable) { + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementContinuous); + } else { + 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); + } } @Override @@ -745,6 +747,7 @@ public class MiBand2Support 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); // } @@ -922,7 +925,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { public void logHeartrate(byte[] value, int status) { if (status == BluetoothGatt.GATT_SUCCESS && value != null) { LOG.info("Got heartrate:"); - if (value.length == 2 && value[0] == 6) { + if (value.length == 2 && value[0] == 0) { int hrValue = (value[1] & 0xff); GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); } @@ -932,15 +935,17 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { } private void handleHeartrate(byte[] value) { - if (value.length == 2 && value[0] == 6) { + if (value.length == 2 && value[0] == 0) { int hrValue = (value[1] & 0xff); 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(); + } } } @@ -949,10 +954,75 @@ public class MiBand2Support 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); + MiBand2SampleProvider provider = new MiBand2SampleProvider(gbDevice, session); + MiBandActivitySample sample = createActivitySample(device, user, ts, provider); + sample.setHeartRate(getHeartrateBpm()); + sample.setSteps(getSteps()); + sample.setRawIntensity(ActivitySample.NOT_MEASURED); + sample.setRawKind(MiBand2SampleProvider.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; } /**