From 82cd06f4c13f8c0e10fe8f56988abae1d8feca18 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Fri, 18 Nov 2016 21:14:04 +0100 Subject: [PATCH] Mi2: WIP: initial support for activity data (#323) --- .../freeyourgadget/gadgetbridge/Logging.java | 12 + .../devices/miband/MiBand2Coordinator.java | 4 +- .../devices/miband/MiBand2Service.java | 10 +- .../service/btle/BLETypeConversions.java | 36 +++ .../service/btle/actions/WriteAction.java | 9 + .../devices/miband/MiBand2Support.java | 86 ++++--- .../service/devices/miband/MiBandSupport.java | 1 + .../operations/AbstractMiBand1Operation.java | 17 ++ .../operations/AbstractMiBandOperation.java | 28 ++- .../operations/FetchActivityOperation.java | 7 +- .../operations/UpdateFirmwareOperation.java | 6 +- .../miband2/AbstractMiBand2Operation.java | 20 ++ .../operations/FetchActivityOperation.java | 233 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 14 files changed, 420 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/AbstractMiBand2Operation.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java index 75504d790..3d71b91fb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Logging.java @@ -126,6 +126,18 @@ public abstract class Logging { return false; } + public static String formatBytes(byte[] bytes) { + if (bytes == null) { + return "(null)"; + } + StringBuilder builder = new StringBuilder(bytes.length * 5); + for (byte b : bytes) { + builder.append(String.format("0x%2x", b)); + builder.append(" "); + } + return builder.toString().trim(); + } + public static void logBytes(Logger logger, byte[] value) { if (value != null) { for (byte b : value) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java index 6cf308f8e..84bc5d8a9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java @@ -41,7 +41,7 @@ public class MiBand2Coordinator extends MiBandCoordinator { @Override public boolean supportsHeartRateMeasurement(GBDevice device) { - return false; // not yet + return true; } @Override @@ -51,7 +51,7 @@ public class MiBand2Coordinator extends MiBandCoordinator { @Override public boolean supportsActivityDataFetching() { - return false; // not yet + return true; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java index c29451e9e..51eead049 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java @@ -18,7 +18,7 @@ public class MiBand2Service { public static final UUID UUID_UNKNOWN_CHARACTERISTIC2 = UUID.fromString("00000002-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC3 = UUID.fromString("00000003-0000-3512-2118-0009af100700"); // Alarm related public static final UUID UUID_UNKNOWN_CHARACTERISTIC4 = UUID.fromString("00000004-0000-3512-2118-0009af100700"); - public static final UUID UUID_UNKNOWN_CHARACTERISTIC5 = UUID.fromString("00000005-0000-3512-2118-0009af100700"); + public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString("00000005-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC6 = UUID.fromString("00000006-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC7 = UUID.fromString("00000007-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC8 = UUID.fromString("00000008-0000-3512-2118-0009af100700"); @@ -279,6 +279,9 @@ public class MiBand2Service { */ public static final byte AUTH_BYTE = 0x8; + // maybe not really activity data, but steps? + public static final byte COMMAND_FETCH_ACTIVITY_DATA = 0x02; + public static byte COMMAND_DATEFORMAT = 0x06; public static final byte[] DATEFORMAT_DATE_TIME = new byte[] { COMMAND_DATEFORMAT, 0x0a, 0x0, 0x03 }; @@ -286,10 +289,15 @@ public class MiBand2Service { public static final byte RESPONSE = 0x10; + public static final byte SUCCESS = 0x01; + public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01; + + public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS }; /** * Received in response to any dateformat configuration request (byte 0 in the byte[] value. */ public static final byte[] RESPONSE_DATEFORMAT_SUCCESS = new byte[] { RESPONSE, COMMAND_DATEFORMAT, 0x0a, 0x0, 0x01 }; + public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS}; public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 }; public static final byte[] WEAR_LOCATION_RIGHT_WRIST = new byte[] { 0x20, 0x00, 0x00, (byte) 0x82}; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java index 727e6c14c..bc61b0f33 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java @@ -52,6 +52,42 @@ public class BLETypeConversions { }; } + /** + * Similar to calendarToRawBytes, but only up to (and including) the MINUTES. + * @param timestamp + * @param honorDeviceTimeOffset + * @return + */ + public static byte[] shortCalendarToRawBytes(Calendar timestamp, boolean honorDeviceTimeOffset) { + + // The mi-band device currently records sleep + // only if it happens after 10pm and before 7am. + // The offset is used to trick the device to record sleep + // in non-standard hours. + // If you usually sleep, say, from 6am to 2pm, set the + // shift to -8, so at 6am the device thinks it's still 10pm + // of the day before. + if (honorDeviceTimeOffset) { + int offsetInHours = MiBandCoordinator.getDeviceTimeOffsetHours(); + if (offsetInHours != 0) { + timestamp.add(Calendar.HOUR_OF_DAY, offsetInHours); + } + } + + // MiBand2: + // year,year,month,dayofmonth,hour,minute,second,dayofweek,0,0,tz + + byte[] year = fromUint16(timestamp.get(Calendar.YEAR)); + return new byte[] { + year[0], + year[1], + fromUint8(timestamp.get(Calendar.MONTH) + 1), + fromUint8(timestamp.get(Calendar.DATE)), + fromUint8(timestamp.get(Calendar.HOUR_OF_DAY)), + fromUint8(timestamp.get(Calendar.MINUTE)) + }; + } + private static int getMiBand2TimeZone(int rawOffset) { int offsetMinutes = rawOffset / 1000 / 60; rawOffset = offsetMinutes < 0 ? -1 : 1; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java index 5cecee06d..b64c17bb9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java @@ -4,6 +4,11 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; /** @@ -12,6 +17,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; * {@link BluetoothGattCallback} */ public class WriteAction extends BtLEAction { + private static final Logger LOG = LoggerFactory.getLogger(WriteAction.class); private final byte[] value; @@ -32,6 +38,9 @@ public class WriteAction extends BtLEAction { } protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { + if (LOG.isDebugEnabled()) { + LOG.debug("writing to characteristic: " + characteristic.getUuid() + ": " + Logging.formatBytes(value)); + } if (characteristic.setValue(value)) { return gatt.writeCharacteristic(characteristic); } 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 10e6de684..c4714b89f 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 @@ -26,6 +26,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; @@ -57,8 +58,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateA import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2NotificationStrategy; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -176,13 +177,23 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { // return this; // } - public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) { - GregorianCalendar now = BLETypeConversions.createCalendar(); - byte[] bytes = BLETypeConversions.calendarToRawBytes(now, true); - byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(now.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone + 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 // byte[] tail = new byte[] { 0x2 }; // reason byte[] all = BLETypeConversions.join(bytes, tail); - builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), all); + return all; + } + + public Calendar fromTimeBytes(byte[] bytes) { + GregorianCalendar timestamp = BLETypeConversions.rawBytesToCalendar(bytes, true); + return timestamp; + } + + public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) { + GregorianCalendar now = BLETypeConversions.createCalendar(); + byte[] bytes = getTimeBytes(now); + builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytes); // byte[] localtime = BLETypeConversions.calendarToLocalTimeBytes(now); // builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_LOCAL_TIME_INFORMATION), localtime); // builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), new byte[] {0x2, 0x00}); @@ -412,7 +423,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { builder.write(characteristic, MiBand2Service.WEAR_LOCATION_RIGHT_WRIST); break; } - builder.notify(characteristic, false); // TODO: this should actually be in some kind of finally-block in the queue + builder.notify(characteristic, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. } return this; } @@ -455,7 +466,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { LOG.info("Disabling heartrate sleep support..."); builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_DISABLE_HR_SLEEP_MEASUREMENT); } - builder.notify(characteristicHRControlPoint, false); // TODO: this should run in some kind of finally-block in the queue + builder.notify(characteristicHRControlPoint, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. } return this; } @@ -678,18 +689,18 @@ 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("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); +// } } @Override @@ -714,28 +725,27 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void onFetchActivityData() { -// TODO: onFetchActivityData -// try { -// new FetchActivityOperation(this).perform(); -// } catch (IOException ex) { -// LOG.error("Unable to fetch MI activity data", ex); -// } + try { + new FetchActivityOperation(this).perform(); + } catch (IOException ex) { + LOG.error("Unable to fetch MI activity data", ex); + } } @Override public void onEnableRealtimeSteps(boolean enable) { - try { - BluetoothGattCharacteristic controlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); - if (enable) { - TransactionBuilder builder = performInitialized("Read realtime steps"); - builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS)).queue(getQueue()); - } - 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()); - } catch (IOException e) { - LOG.error("Unable to change realtime steps notification to: " + enable, e); - } +// try { +// BluetoothGattCharacteristic controlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); +// if (enable) { +// TransactionBuilder builder = performInitialized("Read realtime steps"); +// builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS)).queue(getQueue()); +// } +// 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()); +// } catch (IOException e) { +// LOG.error("Unable to change realtime steps notification to: " + enable, e); +// } } private byte[] getHighLatency() { 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 fafab86fe..7df10ef7c 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 @@ -55,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.Transaction; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ConditionalWriteAction; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java new file mode 100644 index 000000000..00c1d7baa --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBand1Operation.java @@ -0,0 +1,17 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; + +public abstract class AbstractMiBand1Operation extends AbstractMiBandOperation { + protected AbstractMiBand1Operation(MiBandSupport support) { + super(support); + } + + @Override + protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java index 971a63d40..93fd84024 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java @@ -4,14 +4,16 @@ import android.widget.Toast; import java.io.IOException; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; -public abstract class AbstractMiBandOperation extends AbstractBTLEOperation { - protected AbstractMiBandOperation(MiBandSupport support) { +public abstract class AbstractMiBandOperation extends AbstractBTLEOperation { + protected AbstractMiBandOperation(T support) { super(support); } @@ -21,6 +23,7 @@ public abstract class AbstractMiBandOperation extends AbstractBTLEOperation { + protected AbstractMiBand2Operation(MiBand2Support support) { + super(support); + } + + @Override + protected void enableOtherNotifications(TransactionBuilder builder, boolean enable) { + // TODO: check which notifications we should disable and re-enable here +// builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) +// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java new file mode 100644 index 000000000..bb4b87bce --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java @@ -0,0 +1,233 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider; +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.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * An operation that fetches activity data. For every fetch, a new operation must + * be created, i.e. an operation may not be reused for multiple fetches. + */ +public class FetchActivityOperation extends AbstractMiBand2Operation { + private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class); + + private List samples = new ArrayList<>(60*24); // 1day per default + + private byte lastPacketCounter = -1; + private Calendar startTimestamp; + + public FetchActivityOperation(MiBand2Support support) { + super(support); + } + + @Override + protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { + if (!enable) { + // dynamically enabled, but always disabled on finish + builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable); + } + } + + @Override + protected void doPerform() throws IOException { + TransactionBuilder builder = performInitialized("fetching activity data"); + getSupport().setLowLatency(builder); + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); + BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4); + builder.notify(characteristicFetch, true); + BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA); + + GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime(); + builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen))); + builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply + builder.notify(characteristicActivityData, true); + builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_ACTIVITY_DATA }); + builder.queue(getQueue()); + } + + // TODO: use last synchronized sample for the timestamp! + // and what do we do if there was no sync? timestamp from first connectionn? + // or just now - 20d? + private GregorianCalendar getLastSuccessfulSynchronizedTime() { + GregorianCalendar calendar = BLETypeConversions.createCalendar(); + calendar.add(Calendar.DAY_OF_MONTH, -4); + return calendar; + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) { + handleActivityNotif(characteristic.getValue()); + return true; + } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { + handleActivityMetadata(characteristic.getValue()); + return true; + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + private void handleActivityFetchFinish() { + LOG.info("Fetching activity data has finished."); + saveSamples(); + operationFinished(); + unsetBusy(); + } + + private void saveSamples() { + if (samples.size() > 0) { + // save all the samples that we got + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + SampleProvider sampleProvider = new MiBandSampleProvider(getDevice(), session); + Device device = DBHelper.getDevice(getDevice(), session); + User user = DBHelper.getUser(session); + + GregorianCalendar timestamp = (GregorianCalendar) startTimestamp.clone(); + for (MiBandActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000)); + sample.setProvider(sampleProvider); + + if (LOG.isDebugEnabled()) { +// LOG.debug("sample: " + sample); + } + + timestamp.add(Calendar.MINUTE, 1); + } + sampleProvider.addGBActivitySamples(samples.toArray(new MiBandActivitySample[0])); + + } catch (Exception ex) { + GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR); + } finally { + samples.clear(); + } + } + } + + /** + * Method to handle the incoming activity data. + * There are two kind of messages we currently know: + * - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.) + * - the second one is 20 bytes long and contains the actual activity data + *

+ * The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called. + * + * @param value + */ + private void handleActivityNotif(byte[] value) { + if (!isOperationRunning()) { + LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length); + getSupport().logMessageContent(value); + return; + } + + if (value.length == 17) { + if ((byte) (lastPacketCounter + 1) == value[0] ) { + lastPacketCounter++; + bufferActivityData(value); + } else { + GB.toast("Error fetching activity data, invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR); + handleActivityFetchFinish(); + return; + } + handleActivityMetadata(value); + } else { + GB.toast("Error fetching activity data, unexpected package length: " + value.length, Toast.LENGTH_LONG, GB.ERROR); + } + } + + /** + * Creates samples from the given 17-length array + * @param value + */ + private void bufferActivityData(byte[] value) { + int len = value.length; + + if (len % 4 != 1) { + throw new AssertionError("Unexpected activity array size: " + value); + } + + for (int i = 1; i < len; i++) { + if (i % 4 == 1) { + MiBandActivitySample sample = createSample(value[i], value[i + 1], value[i + 2], value[i + 3]); + samples.add(sample); + } + } + } + + private MiBandActivitySample createSample(byte category, byte intensity, byte steps, byte heartrate) { + MiBandActivitySample sample = new MiBandActivitySample(); + sample.setRawKind(category & 0xff); + sample.setRawIntensity(intensity & 0xff); + sample.setSteps(steps & 0xff); + sample.setHeartRate(heartrate & 0xff); + + return sample; + } + + private void handleActivityMetadata(byte[] value) { + if (value.length == 15) { + // first two bytes are whether our request was accepted + if (ArrayUtils.equals(MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, value, 0, 2)) { + // the third byte (0x01 on success) = ? + // the 4th - 7th bytes probably somehow represent the number of bytes/packets to expect + + // last 8 bytes are the start date + Calendar startTimestamp = getSupport().fromTimeBytes(org.apache.commons.lang3.ArrayUtils.subarray(value, 7, value.length)); + setStartTimestamp(startTimestamp); + + GB.toast(getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since, + DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), Toast.LENGTH_LONG, GB.INFO); + } else { + LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); + } + } else if (value.length == 3) { + if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) { + handleActivityFetchFinish(); + } else { + LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); + } + } + } + + private void setStartTimestamp(Calendar startTimestamp) { + this.startTimestamp = startTimestamp; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd30af060..c4d609535 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -282,6 +282,7 @@ Time Activate display upon lift + About to transfer data since %1$s waiting for reconnect