From 09914e6da033135c35ea763ec8fa4d622b29d12c Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Sun, 19 Apr 2015 02:37:29 +0200 Subject: [PATCH] #15 Initial support for MI Band 1) The MAC address has to be added manually for now (Settings -> Debug) 2) Configure other notifications as desired 3) After doing that, restart Gadgetbridge, after that, the device should be visible. 4) Tap it to connect 5) Try the Debug activity via the Menu to send some dummy notifications => Your MI should vibrate and blink Also: add some extra files necessary to support Eclipse ADT. To be migrated to Andmore, later. --- app/src/main/.classpath | 11 + app/src/main/.project | 33 +++ .../main/.settings/org.eclipse.jdt.core.prefs | 12 + app/src/main/AndroidManifest.xml | 1 + .../AbstractBTLEDeviceSupport.java | 7 - .../gadgetbridge/AbstractDeviceSupport.java | 12 + .../BluetoothCommunicationService.java | 8 +- .../gadgetbridge/DeviceSupport.java | 17 +- .../freeyourgadget/gadgetbridge/GBDevice.java | 14 +- .../btle/AbstractBTLEDeviceSupport.java | 172 +++++++++++ .../gadgetbridge/btle/BtLEAction.java | 39 +++ .../gadgetbridge/btle/BtLEQueue.java | 273 ++++++++++++++++++ .../gadgetbridge/btle/GattCallback.java | 109 +++++++ .../gadgetbridge/btle/ReadAction.java | 22 ++ .../gadgetbridge/btle/Transaction.java | 42 +++ .../gadgetbridge/btle/TransactionBuilder.java | 54 ++++ .../gadgetbridge/btle/WaitAction.java | 22 ++ .../gadgetbridge/btle/WriteAction.java | 28 ++ .../gadgetbridge/miband/MiBandService.java | 271 +++++++++++++++++ .../gadgetbridge/miband/MiBandSupport.java | 145 +++++++++- .../gadgetbridge/miband/UserInfo.java | 91 ++++++ .../gadgetbridge/pebble/PebbleSupport.java | 5 + app/src/main/lint.xml | 3 + app/src/main/project.properties | 15 + 24 files changed, 1382 insertions(+), 24 deletions(-) create mode 100644 app/src/main/.classpath create mode 100644 app/src/main/.project create mode 100644 app/src/main/.settings/org.eclipse.jdt.core.prefs delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/AbstractBTLEDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEAction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEQueue.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/GattCallback.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/ReadAction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/Transaction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WaitAction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WriteAction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/UserInfo.java create mode 100644 app/src/main/lint.xml create mode 100644 app/src/main/project.properties diff --git a/app/src/main/.classpath b/app/src/main/.classpath new file mode 100644 index 00000000..60ab2ffa --- /dev/null +++ b/app/src/main/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/.project b/app/src/main/.project new file mode 100644 index 00000000..f8b13a0f --- /dev/null +++ b/app/src/main/.project @@ -0,0 +1,33 @@ + + + Gadgetbridge + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/app/src/main/.settings/org.eclipse.jdt.core.prefs b/app/src/main/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..d17b6724 --- /dev/null +++ b/app/src/main/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 583561c3..382f27b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + = State.CONNECTED.ordinal(); + } + + public boolean isInitialized() { + return state.ordinal() >= State.INITIALIZED.ordinal(); + } + public State getState() { return state; } @@ -65,6 +73,7 @@ public class GBDevice { return type; } + // TODO: this doesn't really belong here public void sendDeviceUpdateIntent(Context context) { Intent deviceUpdateIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST); deviceUpdateIntent.putExtra("device_address", getAddress()); @@ -75,9 +84,11 @@ public class GBDevice { } public enum State { + // Note: the order is important! NOT_CONNECTED, CONNECTING, - CONNECTED + CONNECTED, + INITIALIZED } public enum Type { @@ -85,5 +96,4 @@ public class GBDevice { PEBBLE, MIBAND } - } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java new file mode 100644 index 00000000..5f2cf205 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java @@ -0,0 +1,172 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.AbstractDeviceSupport; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.util.Log; + +/** + * + * @see TransactionBuilder + * @see BtLEQueue + */ +public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback { + private static final String TAG = "AbstractBTLEDeviceSupport"; + + private BtLEQueue mQueue; + private HashMap mAvailableCharacteristics; + private Set mSupportedServices = new HashSet<>(4); + + @Override + public boolean connect() { + if (mQueue == null) { + mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, getContext()); + } + return mQueue.connect(); + } + + /** + * Subclasses should populate the given builder to initialize the device (if necessary). + * @param builder + * @return the same builder as passed as the argument + */ + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + return builder; + } + + @Override + public void dispose() { + if (mQueue != null) { + mQueue.dispose(); + } + } + + /** + * Send commands like this to the device: + *

+ * perform("sms notification").write(someCharacteristic, someByteArray).queue(getQueue()); + *

+ * TODO: support orchestration of multiple reads and writes depending on returned values + * @see #performConnected(Transaction) + * @see #initializeDevice(TransactionBuilder) + */ + protected TransactionBuilder performInitialized(String taskName) throws IOException { + if (!isConnected()) { + if (!connect()) { + throw new IOException("1: Unable to connect to device: " + getDevice()); + } + } + if (!isInitialized()) { + // first, add a transaction that performs device initialization + TransactionBuilder builder = new TransactionBuilder("Initialize device"); + initializeDevice(builder).queue(getQueue()); + } + return new TransactionBuilder(taskName); + } + + /** + * + * @param transaction + * @throws IOException + * @see {@link #performInitialized(String)} + */ + protected void performConnected(Transaction transaction) throws IOException { + if (!isConnected()) { + if (!connect()) { + throw new IOException("2: Unable to connect to device: " + getDevice()); + } + } + getQueue().add(transaction); + } + + public BtLEQueue getQueue() { + return mQueue; + } + + /** + * Subclasses should call this method to add services they support. + * Only supported services will be queried for characteristics. + * @param aSupportedService + * @see #getCharacteristic(UUID) + */ + protected void addSupportedService(UUID aSupportedService) { + mSupportedServices.add(aSupportedService); + } + + /** + * Returns the characteristic matching the given UUID. Only characteristics + * are returned whose service is marked as supported. + * @param uuid + * @return the characteristic for the given UUID or null + * @see #addSupportedService(UUID) + */ + protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) { + if (mAvailableCharacteristics == null) { + return null; + } + return mAvailableCharacteristics.get(uuid); + } + + private void gattServicesDiscovered(List discoveredGattServices) { + mAvailableCharacteristics = null; + + if (discoveredGattServices == null) { + return; + } + Set supportedServices = getSupportedServices(); + + for (BluetoothGattService service : discoveredGattServices) { + if (supportedServices.contains(service.getUuid())) { + List characteristics = service.getCharacteristics(); + if (characteristics == null || characteristics.isEmpty()) { + Log.w(TAG, "Supported LE service " + service.getUuid() + "did not return any characteristics"); + continue; + } + mAvailableCharacteristics = new HashMap<>(characteristics.size()); + for (BluetoothGattCharacteristic characteristic : characteristics) { + mAvailableCharacteristics.put(characteristic.getUuid(), characteristic); + } + } + } + } + + protected Set getSupportedServices() { + return mSupportedServices; + } + + // default implementations of event handler methods (gatt callbacks) + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt) { + gattServicesDiscovered(getQueue().getSupportedGattServices()); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + } + + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEAction.java new file mode 100644 index 00000000..2e261e31 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEAction.java @@ -0,0 +1,39 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * The Bluedroid implementation only allows performing one GATT request at a time. + * As they are asynchronous anyway, we encapsulate every GATT request (read and write) + * inside a runnable action. + * + * These actions are then executed one after another, ensuring that every action's result + * has been posted before invoking the next action. + */ +public abstract class BtLEAction { + private final BluetoothGattCharacteristic characteristic; + + public BtLEAction() { + this(null); + } + + public BtLEAction(BluetoothGattCharacteristic characteristic) { + this.characteristic = characteristic; + } + + public abstract boolean run(BluetoothGatt gatt); + /** + * Returns the GATT characteristic being read/written/... + * @return the GATT characteristic, or null + */ + public BluetoothGattCharacteristic getCharacteristic() { + return characteristic; + } + + public String toString() { + BluetoothGattCharacteristic characteristic = getCharacteristic(); + String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString(); + return getClass().getSimpleName() + " on characteristic: " + uuid; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEQueue.java new file mode 100644 index 00000000..bbb42b81 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEQueue.java @@ -0,0 +1,273 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; + +import nodomain.freeyourgadget.gadgetbridge.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.GBDevice.State; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.util.Log; + +/** + * One queue/thread per connectable device. + */ +public final class BtLEQueue { + private static final String TAG = BtLEQueue.class.getSimpleName(); + + private GBDevice mGbDevice; + private BluetoothAdapter mBluetoothAdapter; + private BluetoothGatt mBluetoothGatt; + private volatile BlockingQueue mTransactions = new LinkedBlockingQueue(); + private volatile boolean mDisposed; + private volatile boolean mAbortTransaction; + + private Context mContext; + private CountDownLatch mWaitForActionResultLatch; + private CountDownLatch mConnectionLatch; + private BluetoothGattCharacteristic mWaitCharacteristic; + private GattCallback mExternalGattCallback; + + private Thread dispatchThread = new Thread("Bluetooth GATT Dispatcher") { + + public void run() { + while (!mDisposed) { + try { + Transaction transaction = mTransactions.take(); + if (!isConnected()) { + // TODO: request connection and initialization from the outside and wait until finished + + // wait until the connection succeeds before running the actions + // Note that no automatic connection is performed. This has to be triggered + // on the outside typically by the DeviceSupport. The reason is that + // devices have different kinds of initializations and this class has no + // idea about them. + mConnectionLatch = new CountDownLatch(1); + mConnectionLatch.await(); + mConnectionLatch = null; + } + + mAbortTransaction = false; + // Run all actions of the transaction until one doesn't succeed + for (BtLEAction action : transaction.getActions()) { + mWaitCharacteristic = action.getCharacteristic(); + if (action.run(mBluetoothGatt)) { + mWaitForActionResultLatch = new CountDownLatch(1); + mWaitForActionResultLatch.await(); + mWaitForActionResultLatch = null; + if (mAbortTransaction) { + break; + } + } else { + Log.e(TAG, "Action returned false: " + action); + break; // abort the transaction + } + } + } catch (InterruptedException ignored) { + mWaitForActionResultLatch = null; + mConnectionLatch = null; + } finally { + mWaitCharacteristic = null; + } + } + Log.i(TAG, "Queue Dispatch Thread terminated."); + } + }; + + public BtLEQueue(BluetoothAdapter bluetoothAdapter, GBDevice gbDevice, GattCallback externalGattCallback, Context context) { + mBluetoothAdapter = bluetoothAdapter; + mGbDevice = gbDevice; + mExternalGattCallback = externalGattCallback; + mContext = context; + + dispatchThread.start(); + } + + protected boolean isConnected() { + return mGbDevice.isConnected(); + } + + /** + * Connects to the given remote device. Note that this does not perform any device + * specific initialization. This should be done in the specific {@link DeviceSupport} + * class. + * + * @return true whether the connection attempt was successfully triggered + */ + public boolean connect() { + if (mBluetoothGatt != null) { + disconnect(); + } + BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(mGbDevice.getAddress()); + mBluetoothGatt = remoteDevice.connectGatt(mContext, false, internalGattCallback); + boolean result = mBluetoothGatt.connect(); + setDeviceConnectionState(result ? State.CONNECTING : State.NOT_CONNECTED); + return result; + } + + private void setDeviceConnectionState(State newState) { + mGbDevice.setState(newState); + mGbDevice.sendDeviceUpdateIntent(mContext); + if (mConnectionLatch != null) { + mConnectionLatch.countDown(); + } + } + + public void disconnect() { + if (mBluetoothGatt != null) { + Log.i(TAG, "Disconnecting BtLEQueue from GATT device"); + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + } + + private void handleDisconnected() { + mTransactions.clear(); + if (mWaitForActionResultLatch != null) { + mWaitForActionResultLatch.countDown(); + } + setDeviceConnectionState(State.NOT_CONNECTED); + } + + public void dispose() { + if (mDisposed) { + return; + } + mDisposed = true; +// try { + disconnect(); + dispatchThread.interrupt(); + dispatchThread = null; +// dispatchThread.join(); +// } catch (InterruptedException ex) { +// Log.e(TAG, "Exception while disposing BtLEQueue", ex); +// } + } + + /** + * Adds a transaction to the end of the queue. + * @param transaction + */ + public void add(Transaction transaction) { + if (!transaction.isEmpty()) { + mTransactions.add(transaction); + } + } + + public void clear() { + mTransactions.clear(); + } + + /** + * Retrieves a list of supported GATT services on the connected device. This should be + * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. + * + * @return A {@code List} of supported services. + */ + public List getSupportedGattServices() { + if (mBluetoothGatt == null) { + return Collections.emptyList(); + } + return mBluetoothGatt.getServices(); + } + + // Implements callback methods for GATT events that the app cares about. For example, + // connection change and services discovered. + private final BluetoothGattCallback internalGattCallback = new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + switch (newState) { + case BluetoothProfile.STATE_CONNECTED: + Log.i(TAG, "Connected to GATT server."); + setDeviceConnectionState(State.CONNECTED); + // Attempts to discover services after successful connection. + Log.i(TAG, "Attempting to start service discovery:" + + mBluetoothGatt.discoverServices()); + break; + case BluetoothProfile.STATE_DISCONNECTED: + Log.i(TAG, "Disconnected from GATT server."); + handleDisconnected(); + break; + case BluetoothProfile.STATE_CONNECTING: + Log.i(TAG, "Connecting to GATT server..."); + setDeviceConnectionState(State.CONNECTING); + break; + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + if (mExternalGattCallback != null) { + // only propagate the successful event + mExternalGattCallback.onServicesDiscovered(gatt); + } + } else { + Log.w(TAG, "onServicesDiscovered received: " + status); + } + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.e(TAG, "Writing characteristic " + characteristic.getUuid() + " succeeded."); + } else { + Log.e(TAG, "Writing characteristic " + characteristic.getUuid() + " failed: "+ status); + } + if (mExternalGattCallback != null) { + mExternalGattCallback.onCharacteristicWrite(gatt, characteristic, status); + } + checkWaitingCharacteristic(characteristic, status); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.e(TAG, "Reading characteristic " + characteristic.getUuid() + " failed: " + status); + } + if (mExternalGattCallback != null) { + mExternalGattCallback.onCharacteristicRead(gatt, characteristic, status); + } + checkWaitingCharacteristic(characteristic, status); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + if (mExternalGattCallback != null) { + mExternalGattCallback.onCharacteristicChanged(gatt, characteristic); + } + } + + @Override + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + if (mExternalGattCallback != null) { + mExternalGattCallback.onReadRemoteRssi(gatt, rssi, status); + } + } + + private void checkWaitingCharacteristic(BluetoothGattCharacteristic characteristic, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + mAbortTransaction = true; + } + if (characteristic != null && BtLEQueue.this.mWaitCharacteristic != null && characteristic.getUuid().equals(BtLEQueue.this.mWaitCharacteristic.getUuid())) { + if (mWaitForActionResultLatch != null) { + mWaitForActionResultLatch.countDown(); + } + } + } + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/GattCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/GattCallback.java new file mode 100644 index 00000000..ce20371d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/GattCallback.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * Callback interface handling gatt events. + * Pretty much the same as {@link BluetoothGattCallback}, except it's an interface + * instead of an abstract class. Some handlers commented out, because not used (yet). + */ +public interface GattCallback { + + /** + * @see BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int) + * @param gatt + * @param status + * @param newState + */ + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState); + + /** + * @see BluetoothGattCallback#onServicesDiscovered(BluetoothGatt, int) + * @param gatt + */ + public void onServicesDiscovered(BluetoothGatt gatt); + + /** + * @see BluetoothGattCallback#onCharacteristicRead(BluetoothGatt, BluetoothGattCharacteristic, int) + * @param gatt + * @param characteristic + * @param status + */ + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status); + + /** + * @see BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int) + * @param gatt + * @param characteristic + * @param status + */ + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status); + + /** + * @see BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic) + * @param gatt + * @param characteristic + */ + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic); + +// /** +// * @see BluetoothGattCallback#onDescriptorRead(BluetoothGatt, BluetoothGattDescriptor, int) +// * @param gatt +// * @param descriptor +// * @param status +// */ +// public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, +// int status); +// +// /** +// * @see BluetoothGattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int) +// * @param gatt +// * @param descriptor +// * @param status +// */ +// public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, +// int status); +// +// /** +// * @see BluetoothGattCallback#onReliableWriteCompleted(BluetoothGatt, int) +// * @param gatt +// * @param status +// */ +// public void onReliableWriteCompleted(BluetoothGatt gatt, int status); + + /** + * @see BluetoothGattCallback#onReadRemoteRssi(BluetoothGatt, int, int) + * @param gatt + * @param rssi + * @param status + */ + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status); + +// /** +// * @see BluetoothGattCallback#onMtuChanged(BluetoothGatt, int, int) +// * @param gatt +// * @param mtu +// * @param status +// */ +// public void onMtuChanged(BluetoothGatt gatt, int mtu, int status); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/ReadAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/ReadAction.java new file mode 100644 index 00000000..598bbb4b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/ReadAction.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * Invokes a read operation on a given GATT characteristic. + * The result will be made available asynchronously through the + * {@link BluetoothGattCallback} + */ +public class ReadAction extends BtLEAction { + + public ReadAction(BluetoothGattCharacteristic characteristic) { + super(characteristic); + } + + @Override + public boolean run(BluetoothGatt gatt) { + return gatt.readCharacteristic(getCharacteristic()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/Transaction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/Transaction.java new file mode 100644 index 00000000..8c4c4ce6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/Transaction.java @@ -0,0 +1,42 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Groups a bunch of {@link BtLEAction actions} together, making sure + * that upon failure of one action, all subsequent actions are discarded. + * + * @author TREND + */ +public class Transaction { + private String mName; + private List mActions = new ArrayList<>(4); + + public Transaction(String taskName) { + this.mName = taskName; + } + + public String getTaskName() { + return mName; + } + + public void add(BtLEAction action) { + mActions.add(action); + } + + public List getActions() { + return Collections.unmodifiableList(mActions); + } + + public boolean isEmpty() { + return mActions.isEmpty(); + } + + @Override + public String toString() { + return String.format(Locale.US, "Transaction task: %s with %d actions", getTaskName(), mActions.size()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java new file mode 100644 index 00000000..40ae8e0e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java @@ -0,0 +1,54 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Log; + +public class TransactionBuilder { + private static final String TAG = TransactionBuilder.class.getSimpleName(); + + private Transaction mTransaction; + + public TransactionBuilder(String taskName) { + mTransaction = new Transaction(taskName); + } + + public TransactionBuilder read(BluetoothGattCharacteristic characteristic) { + if (characteristic == null) { + Log.w(TAG, "Unable to read characteristic: null"); + return this; + } + ReadAction action = new ReadAction(characteristic); + return add(action); + } + + public TransactionBuilder write(BluetoothGattCharacteristic characteristic, byte[] data) { + if (characteristic == null) { + Log.w(TAG, "Unable to write characteristic: null"); + return this; + } + WriteAction action = new WriteAction(characteristic, data); + return add(action); + } + + public TransactionBuilder wait(int millis) { + WaitAction action = new WaitAction(millis); + return add(action); + } + + public TransactionBuilder add(BtLEAction action) { + mTransaction.add(action); + return this; + } + + /** + * To be used as the final step to execute the transaction by the given queue. + * @param queue + */ + public void queue(BtLEQueue queue) { + queue.add(mTransaction); + } + + public Transaction getTransaction() { + return mTransaction; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WaitAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WaitAction.java new file mode 100644 index 00000000..b0cf424d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WaitAction.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; + +public class WaitAction extends BtLEAction { + + private int mMillis; + + public WaitAction(int millis) { + mMillis = millis; + } + + @Override + public boolean run(BluetoothGatt gatt) { + try { + Thread.sleep(mMillis); + return true; + } catch (InterruptedException e) { + return false; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WriteAction.java new file mode 100644 index 00000000..4925a8ac --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WriteAction.java @@ -0,0 +1,28 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * Invokes a write operation on a given GATT characteristic. + * The result status will be made available asynchronously through the + * {@link BluetoothGattCallback} + */ +public class WriteAction extends BtLEAction { + + private byte[] value; + + public WriteAction(BluetoothGattCharacteristic characteristic, byte[] value) { + super(characteristic); + this.value = value; + } + + @Override + public boolean run(BluetoothGatt gatt) { + if (getCharacteristic().setValue(value)) { + return gatt.writeCharacteristic(getCharacteristic()); + } + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java new file mode 100644 index 00000000..6d11bcfd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java @@ -0,0 +1,271 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MiBandService { + + public static final String MAC_ADDRESS_FILTER = "88:0F:10"; + + public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb"; + + public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0")); + + public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01")); + + public static final UUID UUID_CHARACTERISTIC_DEVICE_NAME = UUID.fromString(String.format(BASE_UUID, "FF02")); + + public static final UUID UUID_CHARACTERISTIC_NOTIFICATION = UUID.fromString(String.format(BASE_UUID, "FF03")); + + public static final UUID UUID_CHARACTERISTIC_USER_INFO = UUID.fromString(String.format(BASE_UUID, "FF04")); + + public static final UUID UUID_CHARACTERISTIC_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "FF05")); + + public static final UUID UUID_CHARACTERISTIC_REALTIME_STEPS = UUID.fromString(String.format(BASE_UUID, "FF06")); + + public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString(String.format(BASE_UUID, "FF07")); + + public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString(String.format(BASE_UUID, "FF08")); + + public static final UUID UUID_CHARACTERISTIC_LE_PARAMS = UUID.fromString(String.format(BASE_UUID, "FF09")); + + public static final UUID UUID_CHARACTERISTIC_DATE_TIME = UUID.fromString(String.format(BASE_UUID, "FF0A")); + + public static final UUID UUID_CHARACTERISTIC_STATISTICS = UUID.fromString(String.format(BASE_UUID, "FF0B")); + + public static final UUID UUID_CHARACTERISTIC_BATTERY = UUID.fromString(String.format(BASE_UUID, "FF0C")); + + public static final UUID UUID_CHARACTERISTIC_TEST = UUID.fromString(String.format(BASE_UUID, "FF0D")); + + public static final UUID UUID_CHARACTERISTIC_SENSOR_DATA = UUID.fromString(String.format(BASE_UUID, "FF0E")); + + public static final UUID UUID_CHARACTERISTIC_PAIR = UUID.fromString(String.format(BASE_UUID, "FF0F")); + + public static final byte ALIAS_LEN = 0xa; + + public static final byte NOTIFY_AUTHENTICATION_FAILED = 0x6; + + public static final byte NOTIFY_AUTHENTICATION_SUCCESS = 0x5; + + public static final byte NOTIFY_CONN_PARAM_UPDATE_FAILED = 0x3; + + public static final byte NOTIFY_CONN_PARAM_UPDATE_SUCCESS = 0x4; + + public static final int NOTIFY_DEVICE_MALFUNCTION = 0xff; + + public static final byte NOTIFY_FIRMWARE_UPDATE_FAILED = 0x1; + + public static final byte NOTIFY_FIRMWARE_UPDATE_SUCCESS = 0x2; + + public static final byte NOTIFY_FITNESS_GOAL_ACHIEVED = 0x7; + + public static final byte NOTIFY_FW_CHECK_FAILED = 0xb; + + public static final byte NOTIFY_FW_CHECK_SUCCESS = 0xc; + + public static final byte NOTIFY_NORMAL = 0x0; + + public static final int NOTIFY_PAIR_CANCEL = 0xef; + + public static final byte NOTIFY_RESET_AUTHENTICATION_FAILED = 0x9; + + public static final byte NOTIFY_RESET_AUTHENTICATION_SUCCESS = 0xa; + + public static final byte NOTIFY_SET_LATENCY_SUCCESS = 0x8; + + public static final byte NOTIFY_STATUS_MOTOR_ALARM = 0x11; + + public static final byte NOTIFY_STATUS_MOTOR_AUTH = 0x13; + + public static final byte NOTIFY_STATUS_MOTOR_AUTH_SUCCESS = 0x15; + + public static final byte NOTIFY_STATUS_MOTOR_CALL = 0xe; + + public static final byte NOTIFY_STATUS_MOTOR_DISCONNECT = 0xf; + + public static final byte NOTIFY_STATUS_MOTOR_GOAL = 0x12; + + public static final byte NOTIFY_STATUS_MOTOR_NOTIFY = 0xd; + + public static final byte NOTIFY_STATUS_MOTOR_SHUTDOWN = 0x14; + + public static final byte NOTIFY_STATUS_MOTOR_SMART_ALARM = 0x10; + + public static final byte NOTIFY_STATUS_MOTOR_TEST = 0x16; + + public static final byte NOTIFY_UNKNOWN = -0x1; + + public static final String UUID_CHARACTERISTIC_FEATURE = "2A9E"; + + public static final String UUID_CHARACTERISTIC_MEASUREMENT = "2A9D"; + + public static final String UUID_SERVICE_WEIGHT_SCALE_SERVICE = "181D"; + + public static final String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700"; + + public static final byte MSG_CONNECTED = 0x0; + + public static final byte MSG_DISCONNECTED = 0x1; + + public static final byte MSG_CONNECTION_FAILED = 0x2; + + public static final byte MSG_INITIALIZATION_FAILED = 0x3; + + public static final byte MSG_INITIALIZATION_SUCCESS = 0x4; + + public static final byte MSG_STEPS_CHANGED = 0x5; + + public static final byte MSG_DEVICE_STATUS_CHANGED = 0x6; + + public static final byte MSG_BATTERY_STATUS_CHANGED = 0x7; + + /* + + public static final COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xat; + + public static final byte COMMAND_FACTORY_RESET = 0x9t; + + public static final byte COMMAND_FETCH_DATA = 0x6t; + + public static final byte COMMAND_GET_SENSOR_DATA = 0x12t + + public static final byte COMMAND_REBOOT = 0xct + + public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7t + + public static final COMMAND_SEND_NOTIFICATION = 0x8t + + public static final int COMMAND_SET_COLOR_THEME = et; + + public static final COMMAND_SET_FITNESS_GOAL = 0x5t + + public static final COMMAND_SET_REALTIME_STEP = 0x10t + + public static final COMMAND_SET_REALTIME_STEPS_NOTIFICATION = 0x3t + + public static final COMMAND_SET_TIMER = 0x4t + + public static final COMMAND_SET_WEAR_LOCATION = 0xft + + public static final COMMAND_STOP_MOTOR_VIBRATE = 0x13t + + public static final COMMAND_STOP_SYNC_DATA = 0x11t + + public static final COMMAND_SYNC = 0xbt + + public static final CONNECTION_LATENCY_LEVEL_HIGH = 0x2t; + + public static final CONNECTION_LATENCY_LEVEL_LOW = 0x0t; + + public static final CONNECTION_LATENCY_LEVEL_MEDIUM = 0x1t; + + public static final MODE_REGULAR_DATA_LEN_BYTE = 0x0t; + + public static final MODE_REGULAR_DATA_LEN_MINITE = 0x1t + + public static final PROFILE_STATE_AUTHENTICATION_FAILED:I = 0x4 + + public static final PROFILE_STATE_AUTHENTICATION_SUCCESS:I = 0x3 + + public static final PROFILE_STATE_INITIALIZATION_FAILED:I = 0x2 + + public static final PROFILE_STATE_INITIALIZATION_SUCCESS:I = 0x1 + + public static final PROFILE_STATE_UNKNOWN:I = 0x0 + + public static final TEST_DISCONNECTED_REMINDER = 0x5t + + public static final TEST_NOTIFICATION = 0x3t + + public static final TEST_REMOTE_DISCONNECT = 0x1t + + public static final TEST_SELFTEST = 0x2t + + */ + + private static Map MIBAND_DEBUG; + + static { + MIBAND_DEBUG = new HashMap<>(); + MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service"); + + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_INFO, "Device Info"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_NAME, "Device Name"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_NOTIFICATION, "Notification"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_USER_INFO, "User Info"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_CONTROL_POINT, "Control Point"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_REALTIME_STEPS, "Realtime Steps"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_ACTIVITY_DATA, "Activity Data"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_FIRMWARE_DATA, "Firmware Data"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_LE_PARAMS, "LE Params"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DATE_TIME, "Date/Time"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_STATISTICS, "Statistics"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_BATTERY, "Battery"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_TEST, "Test"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_SENSOR_DATA, "Sensor Data"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_PAIR, "Pair"); + + // extra: + MIBAND_DEBUG.put(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"), "Generic Access Service"); + MIBAND_DEBUG.put(UUID.fromString("00001801-0000-1000-8000-00805f9b34fb"), "Generic Attribute Service"); + MIBAND_DEBUG.put(UUID.fromString("00002a43-0000-1000-8000-00805f9b34fb"), "Alert Category ID"); + MIBAND_DEBUG.put(UUID.fromString("00002a42-0000-1000-8000-00805f9b34fb"), "Alert Category ID Bit Mask"); + MIBAND_DEBUG.put(UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb"), "Alert Level"); + MIBAND_DEBUG.put(UUID.fromString("00002a44-0000-1000-8000-00805f9b34fb"), "Alert Notification Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a3f-0000-1000-8000-00805f9b34fb"), "Alert Status"); + MIBAND_DEBUG.put(UUID.fromString("00002a01-0000-1000-8000-00805f9b34fb"), "Appearance"); + MIBAND_DEBUG.put(UUID.fromString("00002a49-0000-1000-8000-00805f9b34fb"), "Blood Pressure Feature"); + MIBAND_DEBUG.put(UUID.fromString("00002a35-0000-1000-8000-00805f9b34fb"), "Blood Pressure Measurement"); + MIBAND_DEBUG.put(UUID.fromString("00002a38-0000-1000-8000-00805f9b34fb"), "Body Sensor Location"); + MIBAND_DEBUG.put(UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb"), "Current Time"); + MIBAND_DEBUG.put(UUID.fromString("00002a08-0000-1000-8000-00805f9b34fb"), "Date Time"); + MIBAND_DEBUG.put(UUID.fromString("00002a0a-0000-1000-8000-00805f9b34fb"), "Day Date Time"); + MIBAND_DEBUG.put(UUID.fromString("00002a09-0000-1000-8000-00805f9b34fb"), "Day of Week"); + MIBAND_DEBUG.put(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb"), "Device Name"); + MIBAND_DEBUG.put(UUID.fromString("00002a0d-0000-1000-8000-00805f9b34fb"), "DST Offset"); + MIBAND_DEBUG.put(UUID.fromString("00002a0c-0000-1000-8000-00805f9b34fb"), "Exact Time 256"); + MIBAND_DEBUG.put(UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb"), "Firmware Revision String"); + MIBAND_DEBUG.put(UUID.fromString("00002a27-0000-1000-8000-00805f9b34fb"), "Hardware Revision String"); + MIBAND_DEBUG.put(UUID.fromString("00002a39-0000-1000-8000-00805f9b34fb"), "Heart Rate Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb"), "Heart Rate Measurement"); + MIBAND_DEBUG.put(UUID.fromString("00002a2a-0000-1000-8000-00805f9b34fb"), "IEEE 11073-20601 Regulatory"); + MIBAND_DEBUG.put(UUID.fromString("00002a36-0000-1000-8000-00805f9b34fb"), "Intermediate Cuff Pressure"); + MIBAND_DEBUG.put(UUID.fromString("00002a1e-0000-1000-8000-00805f9b34fb"), "Intermediate Temperature"); + MIBAND_DEBUG.put(UUID.fromString("00002a0f-0000-1000-8000-00805f9b34fb"), "Local Time Information"); + MIBAND_DEBUG.put(UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb"), "Manufacturer Name String"); + MIBAND_DEBUG.put(UUID.fromString("00002a21-0000-1000-8000-00805f9b34fb"), "Measurement Interval"); + MIBAND_DEBUG.put(UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb"), "Model Number String"); + MIBAND_DEBUG.put(UUID.fromString("00002a46-0000-1000-8000-00805f9b34fb"), "New Alert"); + MIBAND_DEBUG.put(UUID.fromString("00002a04-0000-1000-8000-00805f9b34fb"), "Peripheral Preferred Connection Parameters"); + MIBAND_DEBUG.put(UUID.fromString("00002a02-0000-1000-8000-00805f9b34fb"), "Peripheral Privacy Flag"); + MIBAND_DEBUG.put(UUID.fromString("00002a03-0000-1000-8000-00805f9b34fb"), "Reconnection Address"); + MIBAND_DEBUG.put(UUID.fromString("00002a14-0000-1000-8000-00805f9b34fb"), "Reference Time Information"); + MIBAND_DEBUG.put(UUID.fromString("00002a40-0000-1000-8000-00805f9b34fb"), "Ringer Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a41-0000-1000-8000-00805f9b34fb"), "Ringer Setting"); + MIBAND_DEBUG.put(UUID.fromString("00002a25-0000-1000-8000-00805f9b34fb"), "Serial Number String"); + MIBAND_DEBUG.put(UUID.fromString("00002a05-0000-1000-8000-00805f9b34fb"), "Service Changed"); + MIBAND_DEBUG.put(UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb"), "Software Revision String"); + MIBAND_DEBUG.put(UUID.fromString("00002a47-0000-1000-8000-00805f9b34fb"), "Supported New Alert Category"); + MIBAND_DEBUG.put(UUID.fromString("00002a48-0000-1000-8000-00805f9b34fb"), "Supported Unread Alert Category"); + MIBAND_DEBUG.put(UUID.fromString("00002a23-0000-1000-8000-00805f9b34fb"), "System ID"); + MIBAND_DEBUG.put(UUID.fromString("00002a1c-0000-1000-8000-00805f9b34fb"), "Temperature Measurement"); + MIBAND_DEBUG.put(UUID.fromString("00002a1d-0000-1000-8000-00805f9b34fb"), "Temperature Type"); + MIBAND_DEBUG.put(UUID.fromString("00002a12-0000-1000-8000-00805f9b34fb"), "Time Accuracy"); + MIBAND_DEBUG.put(UUID.fromString("00002a13-0000-1000-8000-00805f9b34fb"), "Time Source"); + MIBAND_DEBUG.put(UUID.fromString("00002a16-0000-1000-8000-00805f9b34fb"), "Time Update Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a17-0000-1000-8000-00805f9b34fb"), "Time Update State"); + MIBAND_DEBUG.put(UUID.fromString("00002a11-0000-1000-8000-00805f9b34fb"), "Time with DST"); + MIBAND_DEBUG.put(UUID.fromString("00002a0e-0000-1000-8000-00805f9b34fb"), "Time Zone"); + MIBAND_DEBUG.put(UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb"), "Tx Power Level"); + MIBAND_DEBUG.put(UUID.fromString("00002a45-0000-1000-8000-00805f9b34fb"), "Unread Alert Status"); + } + + public static String lookup(UUID uuid, String fallback) { + String name = MIBAND_DEBUG.get(uuid); + if (name == null) { + name = fallback; + } + return name; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java index 12c6661d..f652d64a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java @@ -1,32 +1,120 @@ package nodomain.freeyourgadget.gadgetbridge.miband; -import nodomain.freeyourgadget.gadgetbridge.AbstractBTLEDeviceSupport; +import java.io.IOException; +import java.util.UUID; + import nodomain.freeyourgadget.gadgetbridge.GBCommand; +import nodomain.freeyourgadget.gadgetbridge.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.btle.TransactionBuilder; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Log; public class MiBandSupport extends AbstractBTLEDeviceSupport { - @Override - public void dispose() { - // TODO Auto-generated method stub + private static final String TAG = MiBandSupport.class.getSimpleName(); + public MiBandSupport() { + addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); } @Override - public boolean connect() { - // TODO Auto-generated method stub - return false; + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + pair(builder).sendUserInfo(builder); + return builder; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + private byte[] getDefaultNotification() { + final int vibrateTimes = 1; + final long vibrateDuration = 250l; + final int flashTimes = 1; + final int flashColour = 0xFFFFFFFF; + final int originalColour = 0xFFFFFFFF; + final long flashDuration = 250l; + + return getNotification(vibrateDuration, vibrateTimes, flashTimes, flashColour, originalColour, flashDuration); + } + + private void sendDefaultNotification(TransactionBuilder builder) { + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + Log.i(TAG, "Sending notification to MiBand: " + characteristic); + builder.write(characteristic, getDefaultNotification()).queue(getQueue()); + } + + private byte[] getNotification(long vibrateDuration, int vibrateTimes, int flashTimes, int flashColour, int originalColour, long flashDuration) { + byte[] vibrate = new byte[]{ (byte) 8, (byte) 1 }; + byte r = 6; + byte g = 0; + byte b = 6; + boolean display = true; + // byte[] flashColor = new byte[]{ 14, r, g, b, display ? (byte) 1 : (byte) 0 }; + return vibrate; + } + + private UserInfo getUserInfo() { + // SharedPreferences mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext.getApplicationContext()); + // UserInfo mInfo = new UserInfo( + // mSharedPreferences.getString(MiBandConstants.PREFERENCE_MAC_ADDRESS, ""), + // "1550050550", + // (mSharedPreferences.getString(MiBandConstants.PREFERENCE_GENDER, "Male") == "Male") ? 1 : 0, + // Integer.parseInt(mSharedPreferences.getString(MiBandConstants.PREFERENCE_AGE, "25")), + // Integer.parseInt(mSharedPreferences.getString(MiBandConstants.PREFERENCE_HEIGHT, "175")), + // Integer.parseInt(mSharedPreferences.getString(MiBandConstants.PREFERENCE_WEIGHT, "60")), + // 0 + // ); + return UserInfo.getDefault(getDevice().getAddress()); + } + + /** + * Part of device initialization process. Do not call manually. + * @param builder + * @return + */ + private MiBandSupport sendUserInfo(TransactionBuilder builder) { + Log.d(TAG, "Writing User Info!"); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_USER_INFO); + builder.write(characteristic, getUserInfo().getData()); + return this; + } + + /** + * Part of device initialization process. Do not call manually. + * @param builder + * @return + */ + private MiBandSupport pair(TransactionBuilder transaction) { + Log.i(TAG, "Attempting to pair MI device..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{2}); + } else { + Log.i(TAG, "Unable to pair MI device -- characteristic not available"); + } + return this; + } + + private void performDefaultNotification(String task) { + try { + TransactionBuilder builder = performInitialized(task); + sendDefaultNotification(builder); + } catch (IOException ex) { + Log.e(TAG, "Unable to send notification to MI device", ex); + } } @Override public void onSMS(String from, String body) { - // TODO Auto-generated method stub - + performDefaultNotification("sms received"); } @Override public void onEmail(String from, String subject, String body) { - // TODO Auto-generated method stub - + performDefaultNotification("email received"); } @Override @@ -70,4 +158,39 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { // TODO Auto-generated method stub } + + @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); + } + } + + private void handlePairResult(byte[] pairResult, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "Pairing MI device failed: " + status); + return; + } + + Object value = null; + if (pairResult != null) { + if (pairResult.length == 1) { + try { + byte b = pairResult[0]; + Integer intValue = Integer.valueOf((int) b); + if (intValue.intValue() == 2) { + Log.i(TAG, "Successfully paired MI device"); + return; + } + } catch (Exception ex) { + Log.w(TAG, "Error identifying pairing result", ex); + return; + } + } + value = pairResult.toString(); + } + Log.i(TAG, "MI Band pairing result: " + value); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/UserInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/UserInfo.java new file mode 100644 index 00000000..dbca4804 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/UserInfo.java @@ -0,0 +1,91 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +/** + * Created by UgoRaffaele on 30/01/2015. + */ +public class UserInfo { + + private String btAddress; + private String alias; + private int gender; + private int age; + private int height; + private int weight; + private int type; + + private byte[] data = new byte[20]; + + /** + * Creates a default user info. + * @param btAddress the address of the MI Band to connect to. + */ + public static UserInfo getDefault(String btAddress) { + return new UserInfo(btAddress, "1550050550", 0, 25, 175, 70, 0); + } + + /** + * Creates a user info with the given data + * @param address the address of the MI Band to connect to. + */ + public UserInfo(String address, String alias, int gender, int age, int height, int weight, int type) { + + this.btAddress = address; + this.alias = alias; + this.gender = gender; + this.age = age; + this.height = height; + this.weight = weight; + this.type = type; + + byte[] sequence = new byte[20]; + + int uid = Integer.parseInt(alias); + + sequence[0] = (byte) uid; + sequence[1] = (byte) (uid >>> 8); + sequence[2] = (byte) (uid >>> 16); + sequence[3] = (byte) (uid >>> 24); + + sequence[4] = (byte) (gender & 0xff); + sequence[5] = (byte) (age & 0xff); + sequence[6] = (byte) (height & 0xff); + sequence[7] = (byte) (weight & 0xff); + sequence[8] = (byte) (type & 0xff); + + for (int u = 9; u < 19; u++) + sequence[u] = alias.getBytes()[u-9]; + + byte[] crcSequence = new byte[19]; + for (int u = 0; u < crcSequence.length; u++) + crcSequence[u] = sequence[u]; + + sequence[19] = (byte) ((getCRC8(crcSequence) ^ Integer.parseInt(address.substring(address.length() - 2), 16)) & 0xff); + + this.data = sequence; + } + + public byte[] getData() { + return this.data; + } + + protected int getCRC8(byte[] seq) { + int len = seq.length; + int i = 0; + byte crc = 0x00; + + while (len-- > 0) { + byte extract = seq[i++]; + for (byte tempI = 8; tempI != 0; tempI--) { + byte sum = (byte) ((crc & 0xff) ^ (extract & 0xff)); + sum = (byte) ((sum & 0xff) & 0x01); + crc = (byte) ((crc & 0xff) >>> 1); + if (sum != 0) { + crc = (byte)((crc & 0xff) ^ 0x8c); + } + extract = (byte) ((extract & 0xff) >>> 1); + } + } + return (crc & 0xff); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java index d2ad502f..c49e7ea4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java @@ -25,4 +25,9 @@ public class PebbleSupport extends AbstractBTDeviceSupport { protected GBDeviceIoThread createDeviceIOThread() { return new PebbleIoThread(getDevice(), getDeviceProtocol(), getBluetoothAdapter(), getContext()); } + + @Override + public boolean useAutoConnect() { + return false; + } } diff --git a/app/src/main/lint.xml b/app/src/main/lint.xml new file mode 100644 index 00000000..ee0eead5 --- /dev/null +++ b/app/src/main/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/project.properties b/app/src/main/project.properties new file mode 100644 index 00000000..7ad4d39c --- /dev/null +++ b/app/src/main/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-21 +android.library.reference.1=../../../../android-sdk/extras/android/support/v7/appcompat