diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java index 366d920da..41bf6a90f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandService.java @@ -13,8 +13,9 @@ public class MiBandService { public static final String MAC_ADDRESS_FILTER_1S = "C8:0F:10"; public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0")); - + public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1")); public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D")); + public static final String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700"; public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01")); @@ -53,8 +54,6 @@ public class MiBandService { /* FURTHER UUIDS that were mixed with the other params below. The base UUID for these is unknown */ - public static final String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700"; - public static final byte ALIAS_LEN = 0xa; /*NOTIFICATIONS: usually received on the UUID_CHARACTERISTIC_NOTIFICATION characteristic */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index 03918682d..bfa9f59bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -9,6 +9,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.AbstractCollection; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -17,6 +19,7 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; /** * Abstract base class for all devices connected through Bluetooth Low Energy (LE) aka @@ -35,6 +38,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im private BtLEQueue mQueue; private HashMap mAvailableCharacteristics; private final Set mSupportedServices = new HashSet<>(4); + private final List> mSupportedProfiles = new ArrayList<>(); public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb"; //this is common for all BTLE devices. see http://stackoverflow.com/questions/18699251/finding-out-android-bluetooth-le-gatt-profiles @@ -131,6 +135,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im mSupportedServices.add(aSupportedService); } + protected void addSupportedProfile(AbstractBleProfile profile) { + mSupportedProfiles.add(profile); + } + /** * Returns the characteristic matching the given UUID. Only characteristics * are returned whose service is marked as supported. @@ -155,7 +163,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im mAvailableCharacteristics = new HashMap<>(); for (BluetoothGattService service : discoveredGattServices) { if (supportedServices.contains(service.getUuid())) { - LOG.debug("discovered supported service: " + service.getUuid()); + LOG.debug("discovered supported service: " + BleNamesResolver.resolveServiceName(service.getUuid().toString()) + ": " + service.getUuid()); List characteristics = service.getCharacteristics(); if (characteristics == null || characteristics.isEmpty()) { LOG.warn("Supported LE service " + service.getUuid() + "did not return any characteristics"); @@ -164,10 +172,11 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im HashMap intmAvailableCharacteristics = new HashMap<>(characteristics.size()); for (BluetoothGattCharacteristic characteristic : characteristics) { intmAvailableCharacteristics.put(characteristic.getUuid(), characteristic); + LOG.info(" characteristic: " + BleNamesResolver.resolveCharacteristicName(characteristic.getUuid().toString()) + ": " + characteristic.getUuid()); } mAvailableCharacteristics.putAll(intmAvailableCharacteristics); } else { - LOG.debug("discovered unsupported service: " + service.getUuid()); + LOG.debug("discovered unsupported service: " + BleNamesResolver.resolveServiceName(service.getUuid().toString()) + ": " + service.getUuid()); } } } @@ -179,6 +188,9 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im // default implementations of event handler methods (gatt callbacks) @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onConnectionStateChange(gatt, status, newState); + } } @Override @@ -190,27 +202,45 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onCharacteristicRead(gatt, characteristic, status); + } } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onCharacteristicWrite(gatt, characteristic, status); + } } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onDescriptorRead(gatt, descriptor, status); + } } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onDescriptorWrite(gatt, descriptor, status); + } } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onCharacteristicChanged(gatt, characteristic); + } } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + for (AbstractBleProfile profile : mSupportedProfiles) { + profile.onReadRemoteRssi(gatt, rssi, status); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java new file mode 100644 index 000000000..39713b384 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java @@ -0,0 +1,203 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle; + +import java.util.HashMap; + +import android.util.SparseArray; + +public class BleNamesResolver { + private static HashMap mServices = new HashMap(); + private static HashMap mCharacteristics = new HashMap(); + private static SparseArray mValueFormats = new SparseArray(); + private static SparseArray mAppearance = new SparseArray(); + private static SparseArray mHeartRateSensorLocation = new SparseArray(); + + static public String resolveServiceName(final String uuid) + { + String result = mServices.get(uuid); + if(result == null) result = "Unknown Service"; + return result; + } + + static public String resolveValueTypeDescription(final int format) + { + Integer tmp = Integer.valueOf(format); + return mValueFormats.get(tmp, "Unknown Format"); + } + + static public String resolveCharacteristicName(final String uuid) + { + String result = mCharacteristics.get(uuid); + if(result == null) result = "Unknown Characteristic"; + return result; + } + + static public String resolveUuid(final String uuid) { + String result = mServices.get(uuid); + if(result != null) return "Service: " + result; + + result = mCharacteristics.get(uuid); + if(result != null) return "Characteristic: " + result; + + result = "Unknown UUID"; + return result; + } + + static public String resolveAppearance(int key) { + Integer tmp = Integer.valueOf(key); + return mAppearance.get(tmp, "Unknown Appearance"); + } + + static public String resolveHeartRateSensorLocation(int key) { + Integer tmp = Integer.valueOf(key); + return mHeartRateSensorLocation.get(tmp, "Other"); + } + + static public boolean isService(final String uuid) { + return mServices.containsKey(uuid); + } + + static public boolean isCharacteristic(final String uuid) { + return mCharacteristics.containsKey(uuid); + } + + static { + mServices.put("00001811-0000-1000-8000-00805f9b34fb", "Alert Notification Service"); + mServices.put("0000180f-0000-1000-8000-00805f9b34fb", "Battery Service"); + mServices.put("00001810-0000-1000-8000-00805f9b34fb", "Blood Pressure"); + mServices.put("00001805-0000-1000-8000-00805f9b34fb", "Current Time Service"); + mServices.put("00001818-0000-1000-8000-00805f9b34fb", "Cycling Power"); + mServices.put("00001816-0000-1000-8000-00805f9b34fb", "Cycling Speed and Cadence"); + mServices.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information"); + mServices.put("00001800-0000-1000-8000-00805f9b34fb", "Generic Access"); + mServices.put("00001801-0000-1000-8000-00805f9b34fb", "Generic Attribute"); + mServices.put("00001808-0000-1000-8000-00805f9b34fb", "Glucose"); + mServices.put("00001809-0000-1000-8000-00805f9b34fb", "Health Thermometer"); + mServices.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate"); + mServices.put("00001812-0000-1000-8000-00805f9b34fb", "Human Interface Device"); + mServices.put("00001802-0000-1000-8000-00805f9b34fb", "Immediate Alert"); + mServices.put("00001803-0000-1000-8000-00805f9b34fb", "Link Loss"); + mServices.put("00001819-0000-1000-8000-00805f9b34fb", "Location and Navigation"); + mServices.put("00001807-0000-1000-8000-00805f9b34fb", "Next DST Change Service"); + mServices.put("0000180e-0000-1000-8000-00805f9b34fb", "Phone Alert Status Service"); + mServices.put("00001806-0000-1000-8000-00805f9b34fb", "Reference Time Update Service"); + mServices.put("00001814-0000-1000-8000-00805f9b34fb", "Running Speed and Cadence"); + mServices.put("00001813-0000-1000-8000-00805f9b34fb", "Scan Parameters"); + mServices.put("00001804-0000-1000-8000-00805f9b34fb", "Tx Power"); + mServices.put("0000fee0-0000-3512-2118-0009af100700", "(Propr: Xiaomi MiLi Service)"); + mServices.put("00001530-0000-3512-2118-0009af100700", "(Propr: Xiaomi Weight Service)"); + + + mCharacteristics.put("00002a43-0000-1000-8000-00805f9b34fb", "Alert Category ID"); + mCharacteristics.put("00002a42-0000-1000-8000-00805f9b34fb", "Alert Category ID Bit Mask"); + mCharacteristics.put("00002a06-0000-1000-8000-00805f9b34fb", "Alert Level"); + mCharacteristics.put("00002a44-0000-1000-8000-00805f9b34fb", "Alert Notification Control Point"); + mCharacteristics.put("00002a3f-0000-1000-8000-00805f9b34fb", "Alert Status"); + mCharacteristics.put("00002a01-0000-1000-8000-00805f9b34fb", "Appearance"); + mCharacteristics.put("00002a19-0000-1000-8000-00805f9b34fb", "Battery Level"); + mCharacteristics.put("00002a49-0000-1000-8000-00805f9b34fb", "Blood Pressure Feature"); + mCharacteristics.put("00002a35-0000-1000-8000-00805f9b34fb", "Blood Pressure Measurement"); + mCharacteristics.put("00002a38-0000-1000-8000-00805f9b34fb", "Body Sensor Location"); + mCharacteristics.put("00002a22-0000-1000-8000-00805f9b34fb", "Boot Keyboard Input Report"); + mCharacteristics.put("00002a32-0000-1000-8000-00805f9b34fb", "Boot Keyboard Output Report"); + mCharacteristics.put("00002a33-0000-1000-8000-00805f9b34fb", "Boot Mouse Input Report"); + mCharacteristics.put("00002a5c-0000-1000-8000-00805f9b34fb", "CSC Feature"); + mCharacteristics.put("00002a5b-0000-1000-8000-00805f9b34fb", "CSC Measurement"); + mCharacteristics.put("00002a2b-0000-1000-8000-00805f9b34fb", "Current Time"); + mCharacteristics.put("00002a66-0000-1000-8000-00805f9b34fb", "Cycling Power Control Point"); + mCharacteristics.put("00002a65-0000-1000-8000-00805f9b34fb", "Cycling Power Feature"); + mCharacteristics.put("00002a63-0000-1000-8000-00805f9b34fb", "Cycling Power Measurement"); + mCharacteristics.put("00002a64-0000-1000-8000-00805f9b34fb", "Cycling Power Vector"); + mCharacteristics.put("00002a08-0000-1000-8000-00805f9b34fb", "Date Time"); + mCharacteristics.put("00002a0a-0000-1000-8000-00805f9b34fb", "Day Date Time"); + mCharacteristics.put("00002a09-0000-1000-8000-00805f9b34fb", "Day of Week"); + mCharacteristics.put("00002a00-0000-1000-8000-00805f9b34fb", "Device Name"); + mCharacteristics.put("00002a0d-0000-1000-8000-00805f9b34fb", "DST Offset"); + mCharacteristics.put("00002a0c-0000-1000-8000-00805f9b34fb", "Exact Time 256"); + mCharacteristics.put("00002a26-0000-1000-8000-00805f9b34fb", "Firmware Revision String"); + mCharacteristics.put("00002a51-0000-1000-8000-00805f9b34fb", "Glucose Feature"); + mCharacteristics.put("00002a18-0000-1000-8000-00805f9b34fb", "Glucose Measurement"); + mCharacteristics.put("00002a34-0000-1000-8000-00805f9b34fb", "Glucose Measurement Context"); + mCharacteristics.put("00002a27-0000-1000-8000-00805f9b34fb", "Hardware Revision String"); + mCharacteristics.put("00002a39-0000-1000-8000-00805f9b34fb", "Heart Rate Control Point"); + mCharacteristics.put("00002a37-0000-1000-8000-00805f9b34fb", "Heart Rate Measurement"); + mCharacteristics.put("00002a4c-0000-1000-8000-00805f9b34fb", "HID Control Point"); + mCharacteristics.put("00002a4a-0000-1000-8000-00805f9b34fb", "HID Information"); + mCharacteristics.put("00002a2a-0000-1000-8000-00805f9b34fb", "IEEE 11073-20601 Regulatory Certification Data List"); + mCharacteristics.put("00002a36-0000-1000-8000-00805f9b34fb", "Intermediate Cuff Pressure"); + mCharacteristics.put("00002a1e-0000-1000-8000-00805f9b34fb", "Intermediate Temperature"); + mCharacteristics.put("00002a6b-0000-1000-8000-00805f9b34fb", "LN Control Point"); + mCharacteristics.put("00002a6a-0000-1000-8000-00805f9b34fb", "LN Feature"); + mCharacteristics.put("00002a0f-0000-1000-8000-00805f9b34fb", "Local Time Information"); + mCharacteristics.put("00002a67-0000-1000-8000-00805f9b34fb", "Location and Speed"); + mCharacteristics.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String"); + mCharacteristics.put("00002a21-0000-1000-8000-00805f9b34fb", "Measurement Interval"); + mCharacteristics.put("00002a24-0000-1000-8000-00805f9b34fb", "Model Number String"); + mCharacteristics.put("00002a68-0000-1000-8000-00805f9b34fb", "Navigation"); + mCharacteristics.put("00002a46-0000-1000-8000-00805f9b34fb", "New Alert"); + mCharacteristics.put("00002a04-0000-1000-8000-00805f9b34fb", "Peripheral Preferred Connection Parameters"); + mCharacteristics.put("00002a02-0000-1000-8000-00805f9b34fb", "Peripheral Privacy Flag"); + mCharacteristics.put("00002a50-0000-1000-8000-00805f9b34fb", "PnP ID"); + mCharacteristics.put("00002a69-0000-1000-8000-00805f9b34fb", "Position Quality"); + mCharacteristics.put("00002a4e-0000-1000-8000-00805f9b34fb", "Protocol Mode"); + mCharacteristics.put("00002a03-0000-1000-8000-00805f9b34fb", "Reconnection Address"); + mCharacteristics.put("00002a52-0000-1000-8000-00805f9b34fb", "Record Access Control Point"); + mCharacteristics.put("00002a14-0000-1000-8000-00805f9b34fb", "Reference Time Information"); + mCharacteristics.put("00002a4d-0000-1000-8000-00805f9b34fb", "Report"); + mCharacteristics.put("00002a4b-0000-1000-8000-00805f9b34fb", "Report Map"); + mCharacteristics.put("00002a40-0000-1000-8000-00805f9b34fb", "Ringer Control Point"); + mCharacteristics.put("00002a41-0000-1000-8000-00805f9b34fb", "Ringer Setting"); + mCharacteristics.put("00002a54-0000-1000-8000-00805f9b34fb", "RSC Feature"); + mCharacteristics.put("00002a53-0000-1000-8000-00805f9b34fb", "RSC Measurement"); + mCharacteristics.put("00002a55-0000-1000-8000-00805f9b34fb", "SC Control Point"); + mCharacteristics.put("00002a4f-0000-1000-8000-00805f9b34fb", "Scan Interval Window"); + mCharacteristics.put("00002a31-0000-1000-8000-00805f9b34fb", "Scan Refresh"); + mCharacteristics.put("00002a5d-0000-1000-8000-00805f9b34fb", "Sensor Location"); + mCharacteristics.put("00002a25-0000-1000-8000-00805f9b34fb", "Serial Number String"); + mCharacteristics.put("00002a05-0000-1000-8000-00805f9b34fb", "Service Changed"); + mCharacteristics.put("00002a28-0000-1000-8000-00805f9b34fb", "Software Revision String"); + mCharacteristics.put("00002a47-0000-1000-8000-00805f9b34fb", "Supported New Alert Category"); + mCharacteristics.put("00002a48-0000-1000-8000-00805f9b34fb", "Supported Unread Alert Category"); + mCharacteristics.put("00002a23-0000-1000-8000-00805f9b34fb", "System ID"); + mCharacteristics.put("00002a1c-0000-1000-8000-00805f9b34fb", "Temperature Measurement"); + mCharacteristics.put("00002a1d-0000-1000-8000-00805f9b34fb", "Temperature Type"); + mCharacteristics.put("00002a12-0000-1000-8000-00805f9b34fb", "Time Accuracy"); + mCharacteristics.put("00002a13-0000-1000-8000-00805f9b34fb", "Time Source"); + mCharacteristics.put("00002a16-0000-1000-8000-00805f9b34fb", "Time Update Control Point"); + mCharacteristics.put("00002a17-0000-1000-8000-00805f9b34fb", "Time Update State"); + mCharacteristics.put("00002a11-0000-1000-8000-00805f9b34fb", "Time with DST"); + mCharacteristics.put("00002a0e-0000-1000-8000-00805f9b34fb", "Time Zone"); + mCharacteristics.put("00002a07-0000-1000-8000-00805f9b34fb", "Tx Power Level"); + mCharacteristics.put("00002a45-0000-1000-8000-00805f9b34fb", "Unread Alert Status"); + + mValueFormats.put(Integer.valueOf(52), "32bit float"); + mValueFormats.put(Integer.valueOf(50), "16bit float"); + mValueFormats.put(Integer.valueOf(34), "16bit signed int"); + mValueFormats.put(Integer.valueOf(36), "32bit signed int"); + mValueFormats.put(Integer.valueOf(33), "8bit signed int"); + mValueFormats.put(Integer.valueOf(18), "16bit unsigned int"); + mValueFormats.put(Integer.valueOf(20), "32bit unsigned int"); + mValueFormats.put(Integer.valueOf(17), "8bit unsigned int"); + + // lets add also couple appearance string description + // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml + mAppearance.put(Integer.valueOf(833), "Heart Rate Sensor: Belt"); + mAppearance.put(Integer.valueOf(832), "Generic Heart Rate Sensor"); + mAppearance.put(Integer.valueOf(0), "Unknown"); + mAppearance.put(Integer.valueOf(64), "Generic Phone"); + mAppearance.put(Integer.valueOf(1157), "Cycling: Speed and Cadence Sensor"); + mAppearance.put(Integer.valueOf(1152), "General Cycling"); + mAppearance.put(Integer.valueOf(1153), "Cycling Computer"); + mAppearance.put(Integer.valueOf(1154), "Cycling: Speed Sensor"); + mAppearance.put(Integer.valueOf(1155), "Cycling: Cadence Sensor"); + mAppearance.put(Integer.valueOf(1156), "Cycling: Speed and Cadence Sensor"); + mAppearance.put(Integer.valueOf(1157), "Cycling: Power Sensor"); + + mHeartRateSensorLocation.put(Integer.valueOf(0), "Other"); + mHeartRateSensorLocation.put(Integer.valueOf(1), "Chest"); + mHeartRateSensorLocation.put(Integer.valueOf(2), "Wrist"); + mHeartRateSensorLocation.put(Integer.valueOf(3), "Finger"); + mHeartRateSensorLocation.put(Integer.valueOf(4), "Hand"); + mHeartRateSensorLocation.put(Integer.valueOf(5), "Ear Lobe"); + mHeartRateSensorLocation.put(Integer.valueOf(6), "Foot"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java index afbd9ee6d..6f9f0f51d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattCharacteristic.java @@ -1,5 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.btle; +import android.bluetooth.BluetoothGattCharacteristic; + import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -241,4 +243,7 @@ public class GattCharacteristic { return name; } + public static String toString(BluetoothGattCharacteristic characteristic) { + return characteristic.getUuid() + " (" + lookup(characteristic.getUuid(), "unknown") + ")"; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java new file mode 100644 index 000000000..50122b41f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java @@ -0,0 +1,73 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.LocalBroadcastManager; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractGattCallback; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; + +/** + * Base class for all BLE profiles, with things that all impplementations are + * expected to use. + * + * Instances are used in the context of a concrete AbstractBTLEDeviceSupport instance, + * i.e. a concrete device. + * + * @see nodomain.freeyourgadget.gadgetbridge.service.btle.GattService + * @see nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic + * @see https://www.bluetooth.com/specifications/assigned-numbers + */ +public abstract class AbstractBleProfile extends AbstractGattCallback { + private final T mSupport; + + public AbstractBleProfile(T support) { + this.mSupport = support; + } + + /** + * All notifications should be sent through this methods to make them testable. + * @param intent the intent to broadcast + */ + protected void notify(Intent intent) { + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + + /** + * Delegates to the DeviceSupport instance and additionally sets this instance as the Gatt + * callback for the transaction. + * + * @param taskName + * @return + * @throws IOException + */ + public TransactionBuilder performInitialized(String taskName) throws IOException { + TransactionBuilder builder = mSupport.performInitialized(taskName); + builder.setGattCallback(this); + return builder; + } + + public Context getContext() { + return mSupport.getContext(); + } + + protected GBDevice getDevice() { + return mSupport.getDevice(); + } + + protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) { + return mSupport.getCharacteristic(uuid); + } + + protected BtLEQueue getQueue() { + return mSupport.getQueue(); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java new file mode 100644 index 000000000..1e7d2bca3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/ValueDecoder.java @@ -0,0 +1,21 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; + +public class ValueDecoder { + private static final Logger LOG = LoggerFactory.getLogger(ValueDecoder.class); + + public static int decodePercent(BluetoothGattCharacteristic characteristic) { + int percent = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + if (percent > 100 || percent < 0) { + LOG.warn("Unexpected percent value: " + percent + ": " + GattCharacteristic.toString(characteristic)); + percent = Math.max(100, Math.min(0, percent)); + } + return percent; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java new file mode 100644 index 000000000..f13c47bf4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfo.java @@ -0,0 +1,46 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery; + +import android.os.Parcel; +import android.os.Parcelable; + +public class BatteryInfo implements Parcelable{ + + private int percentCharged; + + public BatteryInfo() { + } + + protected BatteryInfo(Parcel in) { + percentCharged = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(percentCharged); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public BatteryInfo createFromParcel(Parcel in) { + return new BatteryInfo(in); + } + + @Override + public BatteryInfo[] newArray(int size) { + return new BatteryInfo[size]; + } + }; + + public int getPercentCharged() { + return percentCharged; + } + + public void setPercentCharged(int percentCharged) { + this.percentCharged = percentCharged; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java new file mode 100644 index 000000000..d471609b5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/battery/BatteryInfoProfile.java @@ -0,0 +1,71 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.ValueDecoder; + +public class BatteryInfoProfile extends AbstractBleProfile { + private static final Logger LOG = LoggerFactory.getLogger(BatteryInfoProfile.class); + + private static final String ACTION_PREFIX = BatteryInfoProfile.class.getName() + "_"; + + public static final String ACTION_BATTERY_INFO = ACTION_PREFIX + "BATTERY_INFO"; + public static final String EXTRA_BATTERY_INFO = "BATTERY_INFO"; + + public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_BATTERY_SERVICE; + + public static final UUID UUID_CHARACTERISTIC_BATTERY_LEVEL = GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL; + private BatteryInfo batteryInfo; + + public BatteryInfoProfile(T support) { + super(support); + } + + public void requestBatteryInfo(TransactionBuilder builder) { + builder.read(getCharacteristic(UUID_CHARACTERISTIC_BATTERY_LEVEL)); + } + + public void enableNotifiy() { + // TODO: notification + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + UUID charUuid = characteristic.getUuid(); + if (charUuid.equals(UUID_CHARACTERISTIC_BATTERY_LEVEL)) { + handleBatteryLevel(gatt, characteristic); + } else { + LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic)); + } + } else { + LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic)); + } + } + + private void handleBatteryLevel(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + int percent = ValueDecoder.decodePercent(characteristic); + batteryInfo.setPercentCharged(percent); + + notify(createIntent(batteryInfo)); + } + + private Intent createIntent(BatteryInfo batteryInfo) { + Intent intent = new Intent(ACTION_BATTERY_INFO); + intent.putExtra(EXTRA_BATTERY_INFO, batteryInfo); + return intent; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java new file mode 100644 index 000000000..2dac1fb9e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfo.java @@ -0,0 +1,133 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo; + +import android.os.Parcel; +import android.os.Parcelable; + +public class DeviceInfo implements Parcelable{ + private String manufacturerName; + private String modelNumber; + private String serialNumber; + private String hardwareRevision; + private String firmwareRevision; + private String softwareRevision; + private String systemId; + private String regulatoryCertificationDataList; + private String pnpId; + + public DeviceInfo() { + } + + protected DeviceInfo(Parcel in) { + manufacturerName = in.readString(); + modelNumber = in.readString(); + serialNumber = in.readString(); + hardwareRevision = in.readString(); + firmwareRevision = in.readString(); + softwareRevision = in.readString(); + systemId = in.readString(); + regulatoryCertificationDataList = in.readString(); + pnpId = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(manufacturerName); + dest.writeString(modelNumber); + dest.writeString(serialNumber); + dest.writeString(hardwareRevision); + dest.writeString(firmwareRevision); + dest.writeString(softwareRevision); + dest.writeString(systemId); + dest.writeString(regulatoryCertificationDataList); + dest.writeString(pnpId); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public DeviceInfo createFromParcel(Parcel in) { + return new DeviceInfo(in); + } + + @Override + public DeviceInfo[] newArray(int size) { + return new DeviceInfo[size]; + } + }; + + public String getManufacturerName() { + return manufacturerName; + } + + public void setManufacturerName(String manufacturerName) { + this.manufacturerName = manufacturerName; + } + + public String getModelNumber() { + return modelNumber; + } + + public void setModelNumber(String modelNumber) { + this.modelNumber = modelNumber; + } + + public String getSerialNumber() { + return serialNumber; + } + + public void setSerialNumber(String serialNumber) { + this.serialNumber = serialNumber; + } + + public String getHardwareRevision() { + return hardwareRevision; + } + + public void setHardwareRevision(String hardwareRevision) { + this.hardwareRevision = hardwareRevision; + } + + public String getFirmwareRevision() { + return firmwareRevision; + } + + public void setFirmwareRevision(String firmwareRevision) { + this.firmwareRevision = firmwareRevision; + } + + public String getSoftwareRevision() { + return softwareRevision; + } + + public void setSoftwareRevision(String softwareRevision) { + this.softwareRevision = softwareRevision; + } + + public String getSystemId() { + return systemId; + } + + public void setSystemId(String systemId) { + this.systemId = systemId; + } + + public String getRegulatoryCertificationDataList() { + return regulatoryCertificationDataList; + } + + public void setRegulatoryCertificationDataList(String regulatoryCertificationDataList) { + this.regulatoryCertificationDataList = regulatoryCertificationDataList; + } + + public String getPnpId() { + return pnpId; + } + + public void setPnpId(String pnpId) { + this.pnpId = pnpId; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java new file mode 100644 index 000000000..7ae8b6092 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/deviceinfo/DeviceInfoProfile.java @@ -0,0 +1,148 @@ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo; + +public class DeviceInfoProfile extends AbstractBleProfile { + private static final Logger LOG = LoggerFactory.getLogger(DeviceInfoProfile.class); + + private static final String ACTION_PREFIX = DeviceInfoProfile.class.getName() + "_"; + + public static final String ACTION_DEVICE_INFO = ACTION_PREFIX + "DEVICE_INFO"; + public static final String EXTRA_DEVICE_INFO = "DEVICE_INFO"; + + public static final UUID SERVICE_UUID = GattService.UUID_SERVICE_DEVICE_INFORMATION; + + public static final UUID UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING = GattCharacteristic.UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING; + public static final UUID UUID_CHARACTERISTIC_MODEL_NUMBER_STRING = GattCharacteristic.UUID_CHARACTERISTIC_MODEL_NUMBER_STRING; + public static final UUID UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING = GattCharacteristic.UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING; + public static final UUID UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING; + public static final UUID UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING; + public static final UUID UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING = GattCharacteristic.UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING; + public static final UUID UUID_CHARACTERISTIC_SYSTEM_ID = GattCharacteristic.UUID_CHARACTERISTIC_SYSTEM_ID; + public static final UUID UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST = GattCharacteristic.UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST; + public static final UUID UUID_CHARACTERISTIC_PNP_ID = GattCharacteristic.UUID_CHARACTERISTIC_PNP_ID; + private final DeviceInfo deviceInfo = new DeviceInfo(); + + public DeviceInfoProfile(T support) { + super(support); + } + + public void requestDeviceInfo(TransactionBuilder builder) { + builder.read(getCharacteristic(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING)) + .read(getCharacteristic(UUID_CHARACTERISTIC_SYSTEM_ID)) + .read(getCharacteristic(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST)) + .read(getCharacteristic(UUID_CHARACTERISTIC_PNP_ID)); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + UUID charUuid = characteristic.getUuid(); + if (charUuid.equals(UUID_CHARACTERISTIC_MANUFACTURER_NAME_STRING)) { + handleManufacturerName(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_MODEL_NUMBER_STRING)) { + handleModelNumber(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING)) { + handleSerialNumber(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING)) { + handleHardwareRevision(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING)) { + handleFirmwareRevision(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_SOFTWARE_REVISION_STRING)) { + handleSoftwareRevision(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_SYSTEM_ID)) { + handleSystemId(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_IEEE_11073_20601_REGULATORY_CERTIFICATION_DATA_LIST)) { + handleRegulatoryCertificationData(gatt, characteristic); + } else if (charUuid.equals(UUID_CHARACTERISTIC_PNP_ID)) { + handlePnpId(gatt, characteristic); + } else { + LOG.info("Unexpected onCharacteristicRead: " + GattCharacteristic.toString(characteristic)); + } + } else { + LOG.warn("error reading from characteristic:" + GattCharacteristic.toString(characteristic)); + } + } + + + private void handleManufacturerName(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String name = characteristic.getStringValue(0); + deviceInfo.setManufacturerName(name); + notify(createIntent(deviceInfo)); + } + + private void handleModelNumber(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String modelNumber = characteristic.getStringValue(0); + deviceInfo.setModelNumber(modelNumber); + notify(createIntent(deviceInfo)); + } + private void handleSerialNumber(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String serialNumber = characteristic.getStringValue(0); + deviceInfo.setSerialNumber(serialNumber); + notify(createIntent(deviceInfo)); + } + + private void handleHardwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String hardwareRevision = characteristic.getStringValue(0); + deviceInfo.setHardwareRevision(hardwareRevision); + notify(createIntent(deviceInfo)); + } + + private void handleFirmwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String firmwareRevision = characteristic.getStringValue(0); + deviceInfo.setFirmwareRevision(firmwareRevision); + notify(createIntent(deviceInfo)); + } + + private void handleSoftwareRevision(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String softwareRevision = characteristic.getStringValue(0); + deviceInfo.setSoftwareRevision(softwareRevision); + notify(createIntent(deviceInfo)); + } + + private void handleSystemId(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String systemId = characteristic.getStringValue(0); + deviceInfo.setSystemId(systemId); + notify(createIntent(deviceInfo)); + } + + private void handleRegulatoryCertificationData(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + // TODO: regulatory certification data list not supported yet +// String regulatoryCertificationData = characteristic.getStringValue(0); +// deviceInfo.setRegulatoryCertificationDataList(regulatoryCertificationData); +// notify(createIntent(deviceInfo)); + } + + private void handlePnpId(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + String pnpId = characteristic.getStringValue(0); + deviceInfo.setPnpId(pnpId); + notify(createIntent(deviceInfo)); + } + + private Intent createIntent(DeviceInfo deviceInfo) { + Intent intent = new Intent(ACTION_DEVICE_INFO); + intent.putExtra(EXTRA_DEVICE_INFO, deviceInfo); // TODO: broadcast a clone of the info + return intent; + } + +} 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 new file mode 100644 index 000000000..6f0539299 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java @@ -0,0 +1,1185 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.support.v4.content.LocalBroadcastManager; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.UUID; + +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.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.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ConditionalWriteAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PROFILE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_GENERIC; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_K9MAIL; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_PEBBLEMSG; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_SMS; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue; + +public class MiBand2Support extends AbstractBTLEDeviceSupport { + + private static final Logger LOG = LoggerFactory.getLogger(MiBand2Support.class); + private final DeviceInfoProfile deviceInfoProfile; + private final BatteryInfoProfile batteryInfoProfile; + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String s = intent.getAction(); + if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { + handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + + } else if (s.equals(BatteryInfoProfile.ACTION_BATTERY_INFO)) { + handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); + } + } + }; + + private volatile boolean telephoneRinging; + private volatile boolean isLocatingDevice; + + private DeviceInfo mDeviceInfo; + + private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + + public MiBand2Support() { + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(GattService.UUID_SERVICE_HEART_RATE); + addSupportedService(GattService.UUID_SERVICE_IMMEDIATE_ALERT); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(GattService.UUID_SERVICE_ALERT_NOTIFICATION); + + addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); + addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE); + + deviceInfoProfile = new DeviceInfoProfile<>(this); + batteryInfoProfile = new BatteryInfoProfile<>(this); + addSupportedProfile(deviceInfoProfile); + addSupportedProfile(batteryInfoProfile); + + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO); + intentFilter.addAction(BatteryInfoProfile.ACTION_BATTERY_INFO); + broadcastManager.registerReceiver(mReceiver, intentFilter); + } + + @Override + public void dispose() { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + broadcastManager.unregisterReceiver(mReceiver); + super.dispose(); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZING, getContext())); + enableNotifications(builder, true) + .setLowLatency(builder) + .readDate(builder) // without reading the data, we get sporadic connection problems, especially directly after turning on BT +// this is apparently not needed anymore, and actually causes problems when bonding is not used/does not work +// so we simply not use the UUID_PAIR characteristic. +// .pair(builder) + .requestDeviceInfo(builder) + .requestBatteryInfo(builder); +// .sendUserInfo(builder) +// .checkAuthenticationNeeded(builder, getDevice()) +// .setWearLocation(builder) +// .setHeartrateSleepSupport(builder) +// .setFitnessGoal(builder) +// .enableFurtherNotifications(builder, true) +// .setCurrentTime(builder) +// .requestBatteryInfo(builder) +// .setHighLatency(builder) +// .setInitialized(builder); + return builder; + } + + private MiBand2Support readDate(TransactionBuilder builder) { + // NAVL +// builder.read(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DATE_TIME)); + // TODO: handle result + builder.read(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME)); + return this; + } + + // NAVL + public MiBand2Support setLowLatency(TransactionBuilder builder) { +// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getLowLatency()); + return this; + } + // NAVL + public MiBand2Support setHighLatency(TransactionBuilder builder) { +// builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_LE_PARAMS), getHighLatency()); + return this; + } + + private MiBand2Support checkAuthenticationNeeded(TransactionBuilder builder, GBDevice device) { + builder.add(new CheckAuthenticationNeededAction(device)); + return this; + } + + /** + * Last action of initialization sequence. Sets the device to initialized. + * It is only invoked if all other actions were successfully run, so the device + * must be initialized, then. + * + * @param builder + */ + private void setInitialized(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZED, getContext())); + } + + // MB2: AVL + // TODO: tear down the notifications on quit + private MiBand2Support enableNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable); + return this; + } + + private MiBand2Support enableFurtherNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); + // cannot use supportsHeartrate() here because we don't have that information yet + BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT); + if (heartrateCharacteristic != null) { + builder.notify(heartrateCharacteristic, enable); + } + + return this; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void pair() { + for (int i = 0; i < 5; i++) { + if (connect()) { + return; + } + } + } + + public DeviceInfo getDeviceInfo() { + return mDeviceInfo; + } + + private MiBand2Support sendDefaultNotification(TransactionBuilder builder, short repeat, BtLEAction extraAction) { + LOG.info("Sending notification to MiBand: (" + repeat + " times)"); + NotificationStrategy strategy = getNotificationStrategy(); + for (short i = 0; i < repeat; i++) { + strategy.sendDefaultNotification(builder, extraAction); + } + return this; + } + + /** + * Adds a custom notification to the given transaction builder + * + * @param vibrationProfile specifies how and how often the Band shall vibrate. + * @param flashTimes + * @param flashColour + * @param originalColour + * @param flashDuration + * @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example. + * @param builder + */ + private MiBand2Support sendCustomNotification(VibrationProfile vibrationProfile, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { + getNotificationStrategy().sendCustomNotification(vibrationProfile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); + LOG.info("Sending notification to MiBand"); + return this; + } + + private NotificationStrategy getNotificationStrategy() { + if (mDeviceInfo == null) { + // not initialized yet? + return new NoNotificationStrategy(); + } + if (mDeviceInfo.getFirmwareVersion() < MiBandFWHelper.FW_16779790) { + return new V1NotificationStrategy(this); + } else { + //use the new alert characteristic + return new V2NotificationStrategy(this); + } + } + + static final byte[] reboot = new byte[]{MiBandService.COMMAND_REBOOT}; + + static final byte[] startHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 1}; + static final byte[] stopHeartMeasurementManual = new byte[]{0x15, MiBandService.COMMAND_SET_HR_MANUAL, 0}; + static final byte[] startHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 1}; + static final byte[] stopHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 0}; + static final byte[] startHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 1}; + static final byte[] stopHeartMeasurementSleep = new byte[]{0x15, MiBandService.COMMAND_SET_HR_SLEEP, 0}; + + static final byte[] startRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 1}; + static final byte[] stopRealTimeStepsNotifications = new byte[]{MiBandService.COMMAND_SET_REALTIME_STEPS_NOTIFICATION, 0}; + + /** + * Part of device initialization process. Do not call manually. + * + * @param builder + * @return + */ + private MiBand2Support sendUserInfo(TransactionBuilder builder) { + LOG.debug("Writing User Info!"); + // Use a custom action instead of just builder.write() because mDeviceInfo + // is set by handleDeviceInfo *after* this action is created. + builder.add(new BtLEAction(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_USER_INFO)) { + @Override + public boolean expectsResult() { + return true; + } + + @Override + public boolean run(BluetoothGatt gatt) { + // at this point, mDeviceInfo should be set + return new WriteAction(getCharacteristic(), + MiBandCoordinator.getAnyUserInfo(getDevice().getAddress()).getData(mDeviceInfo) + ).run(gatt); + } + }); + return this; + } + + private MiBand2Support requestBatteryInfo(TransactionBuilder builder) { + LOG.debug("Requesting Battery Info!"); + batteryInfoProfile.requestBatteryInfo(builder); + return this; + } + + private MiBand2Support requestDeviceInfo(TransactionBuilder builder) { + LOG.debug("Requesting Device Info!"); + deviceInfoProfile.requestDeviceInfo(builder); + return this; + } + + /* private MiBandSupport requestHRInfo(TransactionBuilder builder) { + LOG.debug("Requesting HR Info!"); + BluetoothGattCharacteristic HRInfo = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT); + builder.read(HRInfo); + BluetoothGattCharacteristic HR_Point = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); + builder.read(HR_Point); + return this; + } + *//** + * Part of HR test. Do not call manually. + * + * @param transaction + * @return + *//* + private MiBandSupport heartrate(TransactionBuilder transaction) { + LOG.info("Attempting to read HR ..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHAR_HEART_RATE_MEASUREMENT); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{MiBandService.COMMAND_SET__HR_CONTINUOUS}); + } else { + LOG.info("Unable to read HR from MI device -- characteristic not available"); + } + return this; + }*/ + + /** + * Part of device initialization process. Do not call manually. + * + * @param transaction + * @return + */ + private MiBand2Support pair(TransactionBuilder transaction) { + LOG.info("Attempting to pair MI device..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{2}); + } else { + LOG.info("Unable to pair MI device -- characteristic not available"); + } + return this; + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param transaction + * @return + */ + + private MiBand2Support setFitnessGoal(TransactionBuilder transaction) { + LOG.info("Attempting to set Fitness Goal..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + if (characteristic != null) { + int fitnessGoal = MiBandCoordinator.getFitnessGoal(getDevice().getAddress()); + transaction.write(characteristic, new byte[]{ + MiBandService.COMMAND_SET_FITNESS_GOAL, + 0, + (byte) (fitnessGoal & 0xff), + (byte) ((fitnessGoal >>> 8) & 0xff) + }); + } else { + LOG.info("Unable to set Fitness Goal"); + } + return this; + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param transaction + * @return + */ + private MiBand2Support setWearLocation(TransactionBuilder transaction) { + LOG.info("Attempting to set wear location..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + if (characteristic != null) { + transaction.add(new ConditionalWriteAction(characteristic) { + @Override + protected byte[] checkCondition() { + if (getDeviceInfo() != null && getDeviceInfo().isAmazFit()) { + return null; + } + int location = MiBandCoordinator.getWearLocation(getDevice().getAddress()); + return new byte[]{ + MiBandService.COMMAND_SET_WEAR_LOCATION, + (byte) location + }; + } + }); + } else { + LOG.info("Unable to set Wear Location"); + } + return this; + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + try { + TransactionBuilder builder = performInitialized("enable heart rate sleep support: " + enable); + setHeartrateSleepSupport(builder); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error toggling heart rate sleep support: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + // not supported + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + // not supported + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param builder + */ + private MiBand2Support setHeartrateSleepSupport(TransactionBuilder builder) { + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); + if (characteristic != null) { + builder.add(new ConditionalWriteAction(characteristic) { + @Override + protected byte[] checkCondition() { + if (!supportsHeartRate()) { + return null; + } + if (MiBandCoordinator.getHeartrateSleepSupport(getDevice().getAddress())) { + LOG.info("Enabling heartrate sleep support..."); + return startHeartMeasurementSleep; + } else { + LOG.info("Disabling heartrate sleep support..."); + return stopHeartMeasurementSleep; + } + } + }); + } + return this; + } + + private void performDefaultNotification(String task, short repeat, BtLEAction extraAction) { + try { + TransactionBuilder builder = performInitialized(task); + sendDefaultNotification(builder, repeat, extraAction); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send notification to MI device", ex); + } + } + + private void performPreferredNotification(String task, String notificationOrigin, BtLEAction extraAction) { + try { + TransactionBuilder builder = performInitialized(task); + Prefs prefs = GBApplication.getPrefs(); + int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs); + int vibratePause = getPreferredVibratePause(notificationOrigin, prefs); + short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs); + VibrationProfile profile = getPreferredVibrateProfile(notificationOrigin, prefs, vibrateTimes); + + int flashTimes = getPreferredFlashCount(notificationOrigin, prefs); + int flashColour = getPreferredFlashColour(notificationOrigin, prefs); + int originalColour = getPreferredOriginalColour(notificationOrigin, prefs); + int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs); + + sendCustomNotification(profile, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); +// sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send notification to MI device", ex); + } + } + + private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION); + } + + private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR); + } + + private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR); + } + + private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT); + } + + private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE); + } + + private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) { + return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT)); + } + + private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) { + return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION); + } + + private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) { + String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE); + return VibrationProfile.getProfile(profileId, repeat); + } + + @Override + public void onSetAlarms(ArrayList alarms) { + try { + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + TransactionBuilder builder = performInitialized("Set alarm"); + boolean anyAlarmEnabled = false; + for (Alarm alarm : alarms) { + anyAlarmEnabled |= alarm.isEnabled(); + queueAlarm(alarm, builder, characteristic); + } + builder.queue(getQueue()); + if (anyAlarmEnabled) { + GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO); + } else { + GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO); + } + } catch (IOException ex) { + GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_failed), Toast.LENGTH_LONG, GB.ERROR, ex); + } + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + // FIXME: these ORIGIN contants do not really make sense anymore + switch (notificationSpec.type) { + case SMS: + performPreferredNotification("sms received", ORIGIN_SMS, null); + break; + case EMAIL: + performPreferredNotification("email received", ORIGIN_K9MAIL, null); + break; + case CHAT: + performPreferredNotification("chat message received", ORIGIN_PEBBLEMSG, null); + break; + default: + performPreferredNotification("generic notification received", ORIGIN_GENERIC, null); + } + } + + @Override + public void onSetTime() { + try { + TransactionBuilder builder = performInitialized("Set date and time"); + setCurrentTime(builder); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to set time on MI device", ex); + } + //TODO: once we have a common strategy for sending events (e.g. EventHandler), remove this call from here. Meanwhile it does no harm. + sendCalendarEvents(); + } + + /** + * Sets the current time to the Mi device using the given builder. + * + * @param builder + */ + private MiBand2Support setCurrentTime(TransactionBuilder builder) { + Calendar now = GregorianCalendar.getInstance(); + Date date = now.getTime(); + LOG.info("Sending current time to Mi Band: " + DateTimeUtils.formatDate(date) + " (" + date.toGMTString() + ")"); + byte[] nowBytes = MiBandDateConverter.calendarToRawBytes(now); + byte[] time = new byte[]{ + nowBytes[0], + nowBytes[1], + nowBytes[2], + nowBytes[3], + nowBytes[4], + nowBytes[5], + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f + }; + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DATE_TIME); + if (characteristic != null) { + builder.write(characteristic, time); + } else { + LOG.info("Unable to set time -- characteristic not available"); + } + return this; + } + + @Override + public void onSetCallState(CallSpec callSpec) { + if (callSpec.command == CallSpec.CALL_INCOMING) { + telephoneRinging = true; + AbortTransactionAction abortAction = new AbortTransactionAction() { + @Override + protected boolean shouldAbort() { + return !isTelephoneRinging(); + } + }; + performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, abortAction); + } else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) { + telephoneRinging = false; + } + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + } + + private boolean isTelephoneRinging() { + // don't synchronize, this is not really important + return telephoneRinging; + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + // not supported + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + // not supported + } + + @Override + public void onReboot() { + try { + TransactionBuilder builder = performInitialized("Reboot"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), reboot); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to reboot MI", ex); + } + } + + @Override + public void onHeartRateTest() { + if (supportsHeartRate()) { + try { + TransactionBuilder builder = performInitialized("HeartRateTest"); + 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 in MI1S", ex); + } + } else { + GB.toast(getContext(), "Heart rate is not supported on this device", Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + if (supportsHeartRate()) { + 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); + } + } + } + + public boolean supportsHeartRate() { + return getDeviceInfo() != null && getDeviceInfo().supportsHeartrate(); + } + + @Override + public void onFindDevice(boolean start) { + isLocatingDevice = start; + + if (start) { + AbortTransactionAction abortAction = new AbortTransactionAction() { + @Override + protected boolean shouldAbort() { + return !isLocatingDevice; + } + }; + performDefaultNotification("locating device", (short) 255, abortAction); + } + } + + @Override + public void onFetchActivityData() { +// TODO: onFetchActivityData +// 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); + } + } + + private byte[] getHighLatency() { + int minConnectionInterval = 460; + int maxConnectionInterval = 500; + int latency = 0; + int timeout = 500; + int advertisementInterval = 0; + + return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval); + } + + private byte[] getLatency(int minConnectionInterval, int maxConnectionInterval, int latency, int timeout, int advertisementInterval) { + byte result[] = new byte[12]; + result[0] = (byte) (minConnectionInterval & 0xff); + result[1] = (byte) (0xff & minConnectionInterval >> 8); + result[2] = (byte) (maxConnectionInterval & 0xff); + result[3] = (byte) (0xff & maxConnectionInterval >> 8); + result[4] = (byte) (latency & 0xff); + result[5] = (byte) (0xff & latency >> 8); + result[6] = (byte) (timeout & 0xff); + result[7] = (byte) (0xff & timeout >> 8); + result[8] = 0; + result[9] = 0; + result[10] = (byte) (advertisementInterval & 0xff); + result[11] = (byte) (0xff & advertisementInterval >> 8); + + return result; + } + + private byte[] getLowLatency() { + int minConnectionInterval = 39; + int maxConnectionInterval = 49; + int latency = 0; + int timeout = 500; + int advertisementInterval = 0; + + return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval); + } + + @Override + public void onInstallApp(Uri uri) { +// TODO: onInstallApp (firmware update) +// try { +// new UpdateFirmwareOperation(uri, this).perform(); +// } catch (IOException ex) { +// GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); +// } + } + + @Override + public void onAppInfoReq() { + // not supported + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + // not supported + } + + @Override + public void onAppDelete(UUID uuid) { + // not supported + } + + @Override + public void onAppConfiguration(UUID uuid, String config) { + // not supported + } + + @Override + public void onAppReorder(UUID[] uuids) { + // not supported + } + + @Override + public void onScreenshotReq() { + // not supported + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { + handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS); + } else if (MiBandService.UUID_CHARACTERISTIC_NOTIFICATION.equals(characteristicUUID)) { + handleNotificationNotif(characteristic.getValue()); + } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { + handleRealtimeSteps(characteristic.getValue()); + } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { + handleRealtimeSteps(characteristic.getValue()); + } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { + handleHeartrate(characteristic.getValue()); + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + logMessageContent(characteristic.getValue()); + } + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicRead(gatt, characteristic, status); + + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_DEVICE_INFO.equals(characteristicUUID)) { + handleDeviceInfo(characteristic.getValue(), status); + } else if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) { + handleDeviceName(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { + handleBatteryInfo(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { + logHeartrate(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_DATE_TIME.equals(characteristicUUID)) { + logDate(characteristic.getValue(), status); + } else { + LOG.info("Unhandled characteristic read: " + characteristicUUID); + logMessageContent(characteristic.getValue()); + } + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_PAIR.equals(characteristicUUID)) { + handlePairResult(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_USER_INFO.equals(characteristicUUID)) { + handleUserInfoResult(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT.equals(characteristicUUID)) { + handleControlPointResult(characteristic.getValue(), status); + } + } + + /** + * Utility method that may be used to log incoming messages when we don't know how to deal with them yet. + * + * @param value + */ + public void logMessageContent(byte[] value) { + LOG.info("RECEIVED DATA WITH LENGTH: " + ((value != null) ? value.length : "(null)")); + if (value != null) { + for (byte b : value) { + LOG.warn("DATA: " + String.format("0x%2x", b)); + } + } + } + + public void logDate(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + GregorianCalendar calendar = MiBandDateConverter.rawBytesToCalendar(value); + LOG.info("Got Mi Band Date: " + DateTimeUtils.formatDateTime(calendar.getTime())); + } else { + logMessageContent(value); + } + } + + 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) { + int hrValue = (value[1] & 0xff); + GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); + } + return; + } + logMessageContent(value); + } + + private void handleHeartrate(byte[] value) { + if (value.length == 2 && value[0] == 6) { + 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); + } + } + + private void handleRealtimeSteps(byte[] value) { + int steps = 0xff & value[0] | (0xff & value[1]) << 8; + 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); + } + + /** + * React to unsolicited messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION + * characteristic, + * These messages appear to be always 1 byte long, with values that are listed in MiBandService. + * It is not excluded that there are further values which are still unknown. + *

+ * Upon receiving known values that request further action by GB, the appropriate method is called. + * + * @param value + */ + private void handleNotificationNotif(byte[] value) { + if (value.length != 1) { + LOG.error("Notifications should be 1 byte long."); + LOG.info("RECEIVED DATA WITH LENGTH: " + value.length); + for (byte b : value) { + LOG.warn("DATA: " + String.format("0x%2x", b)); + } + return; + } + switch (value[0]) { + case MiBandService.NOTIFY_AUTHENTICATION_FAILED: + // we get first FAILED, then NOTIFY_STATUS_MOTOR_AUTH (0x13) + // which means, we need to authenticate by tapping + getDevice().setState(State.AUTHENTICATION_REQUIRED); + getDevice().sendDeviceUpdateIntent(getContext()); + GB.toast(getContext(), "Band needs pairing", Toast.LENGTH_LONG, GB.ERROR); + break; + case MiBandService.NOTIFY_AUTHENTICATION_SUCCESS: // fall through -- not sure which one we get + case MiBandService.NOTIFY_RESET_AUTHENTICATION_SUCCESS: // for Mi 1A + case MiBandService.NOTIFY_STATUS_MOTOR_AUTH_SUCCESS: + LOG.info("Band successfully authenticated"); + // maybe we can perform the rest of the initialization from here + doInitialize(); + break; + + case MiBandService.NOTIFY_STATUS_MOTOR_AUTH: + LOG.info("Band needs authentication (MOTOR_AUTH)"); + getDevice().setState(State.AUTHENTICATING); + getDevice().sendDeviceUpdateIntent(getContext()); + break; + + case MiBandService.NOTIFY_SET_LATENCY_SUCCESS: + LOG.info("Setting latency succeeded."); + break; + default: + for (byte b : value) { + LOG.warn("DATA: " + String.format("0x%2x", b)); + } + } + } + + private void doInitialize() { + try { + TransactionBuilder builder = performInitialized("just initializing after authentication"); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to initialize device after authentication", ex); + } + } + + private void handleDeviceInfo(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + mDeviceInfo = new DeviceInfo(value); + if (getDeviceInfo().supportsHeartrate()) { + getDevice().addDeviceInfo(new GenericItem( + getContext().getString(R.string.DEVINFO_HR_VER), + MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getHeartrateFirmwareVersion()))); + } + LOG.warn("Device info: " + mDeviceInfo); + versionCmd.hwVersion = mDeviceInfo.getHwVersion(); + versionCmd.fwVersion = MiBandFWHelper.formatFirmwareVersion(mDeviceInfo.getFirmwareVersion()); + handleGBDeviceEvent(versionCmd); + } + } + + private void handleDeviceName(byte[] value, int status) { +// if (status == BluetoothGatt.GATT_SUCCESS) { +// versionCmd.hwVersion = new String(value); +// handleGBDeviceEvent(versionCmd); +// } + } + + /** + * Convert an alarm from the GB internal structure to a Mi Band message and put on the specified + * builder queue as a write message for the passed characteristic + * + * @param alarm + * @param builder + * @param characteristic + */ + private void queueAlarm(Alarm alarm, TransactionBuilder builder, BluetoothGattCharacteristic characteristic) { + byte[] alarmCalBytes = MiBandDateConverter.calendarToRawBytes(alarm.getAlarmCal()); + + byte[] alarmMessage = new byte[]{ + MiBandService.COMMAND_SET_TIMER, + (byte) alarm.getIndex(), + (byte) (alarm.isEnabled() ? 1 : 0), + alarmCalBytes[0], + alarmCalBytes[1], + alarmCalBytes[2], + alarmCalBytes[3], + alarmCalBytes[4], + alarmCalBytes[5], + (byte) (alarm.isSmartWakeup() ? 30 : 0), + (byte) alarm.getRepetitionMask() + }; + builder.write(characteristic, alarmMessage); + } + + private void handleControlPointResult(byte[] value, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + LOG.warn("Could not write to the control point."); + } + LOG.info("handleControlPoint write status:" + status + "; length: " + (value != null ? value.length : "(null)")); + + if (value != null) { + for (byte b : value) { + LOG.info("handleControlPoint WROTE DATA:" + String.format("0x%8x", b)); + } + } else { + LOG.warn("handleControlPoint WROTE null"); + } + } + + private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { +// if (getDeviceInfo().supportsHeartrate()) { +// getDevice().addDeviceInfo(new GenericItem( +// getContext().getString(R.string.DEVINFO_HR_VER), +// info.getSoftwareRevision())); +// } + LOG.warn("Device info: " + info); + versionCmd.hwVersion = info.getHardwareRevision(); + versionCmd.fwVersion = info.getFirmwareRevision(); + handleGBDeviceEvent(versionCmd); + } + + private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) { + batteryCmd.level = (short) info.getPercentCharged(); +// batteryCmd.state = info.getState(); +// batteryCmd.lastChargeTime = info.getLastChargeTime(); +// batteryCmd.numCharges = info.getNumCharges(); + handleGBDeviceEvent(batteryCmd); + } + + + private void handleBatteryInfo(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + BatteryInfo info = new BatteryInfo(value); + batteryCmd.level = ((short) info.getLevelInPercent()); + batteryCmd.state = info.getState(); + batteryCmd.lastChargeTime = info.getLastChargeTime(); + batteryCmd.numCharges = info.getNumCharges(); + handleGBDeviceEvent(batteryCmd); + } + } + + private void handleUserInfoResult(byte[] value, int status) { + // successfully transferred user info means we're initialized +// commented out, because we have SetDeviceStateAction which sets initialized +// state on every successful initialization. +// if (status == BluetoothGatt.GATT_SUCCESS) { +// setConnectionState(State.INITIALIZED); +// } + } + + private void setConnectionState(State newState) { + getDevice().setState(newState); + getDevice().sendDeviceUpdateIntent(getContext()); + } + + private void handlePairResult(byte[] pairResult, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + LOG.info("Pairing MI device failed: " + status); + return; + } + + String value = null; + if (pairResult != null) { + if (pairResult.length == 1) { + try { + if (pairResult[0] == 2) { + LOG.info("Successfully paired MI device"); + return; + } + } catch (Exception ex) { + LOG.warn("Error identifying pairing result", ex); + return; + } + } + value = Arrays.toString(pairResult); + } + LOG.info("MI Band pairing result: " + value); + } + + /** + * Fetch the events from the android device calendars and set the alarms on the miband. + */ + private void sendCalendarEvents() { + try { + TransactionBuilder builder = performInitialized("Send upcoming events"); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + + Prefs prefs = GBApplication.getPrefs(); + int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0); + + if (availableSlots > 0) { + CalendarEvents upcomingEvents = new CalendarEvents(); + List mEvents = upcomingEvents.getCalendarEventList(getContext()); + + int iteration = 0; + for (CalendarEvents.CalendarEvent mEvt : mEvents) { + if (iteration >= availableSlots || iteration > 2) { + break; + } + int slotToUse = 2 - iteration; + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(mEvt.getBegin()); + byte[] calBytes = MiBandDateConverter.calendarToRawBytes(calendar); + + byte[] alarmMessage = new byte[]{ + MiBandService.COMMAND_SET_TIMER, + (byte) slotToUse, + (byte) 1, + calBytes[0], + calBytes[1], + calBytes[2], + calBytes[3], + calBytes[4], + calBytes[5], + (byte) 0, + (byte) 0 + }; + builder.write(characteristic, alarmMessage); + iteration++; + } + builder.queue(getQueue()); + } + } catch (IOException ex) { + LOG.error("Unable to send Events to MI device", ex); + } + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java index 132ec182b..e9f23097b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V1NotificationStrategy.java @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; @@ -16,9 +17,9 @@ public class V1NotificationStrategy implements NotificationStrategy { static final byte[] startVibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, 1}; static final byte[] stopVibrate = new byte[]{MiBandService.COMMAND_STOP_MOTOR_VIBRATE}; - private final MiBandSupport support; + private final AbstractBTLEDeviceSupport support; - public V1NotificationStrategy(MiBandSupport support) { + public V1NotificationStrategy(AbstractBTLEDeviceSupport support) { this.support = support; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java index 32ed1275f..c97303608 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/V2NotificationStrategy.java @@ -3,14 +3,15 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; import android.bluetooth.BluetoothGattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; public class V2NotificationStrategy implements NotificationStrategy { - private final MiBandSupport support; + private final AbstractBTLEDeviceSupport support; - public V2NotificationStrategy(MiBandSupport support) { + public V2NotificationStrategy(AbstractBTLEDeviceSupport support) { this.support = support; }