From b23eee27638eb5b4f173f6f29ed935dd129621f4 Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Thu, 24 May 2018 15:11:35 +0200 Subject: [PATCH] Template profile rewritten to use new BleManager API --- .../nrftoolbox/template/TemplateActivity.java | 52 +++-- .../nrftoolbox/template/TemplateManager.java | 195 ++++++++++++------ .../template/TemplateManagerCallbacks.java | 24 +-- .../nrftoolbox/template/TemplateService.java | 45 ++-- .../TemplateCharacteristicCallback.java | 21 ++ .../callback/TemplateDataCallback.java | 47 +++++ .../res/layout/activity_feature_template.xml | 10 + app/src/main/res/values/strings_template.xml | 1 + 8 files changed, 287 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java index eef66277..0894bdb1 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java @@ -27,8 +27,10 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.content.LocalBroadcastManager; import android.view.Menu; +import android.view.View; import android.widget.TextView; import java.util.UUID; @@ -47,7 +49,7 @@ public class TemplateActivity extends BleProfileServiceReadyActivity getService().performAction("Template")); } @Override @@ -77,16 +81,7 @@ public class TemplateActivity extends BleProfileServiceReadyActivity { - private static final String TAG = "TemplateManager"; - +public class TemplateManager extends BatteryManager { + // TODO Replace the services and characteristics below to match your device. /** The service UUID */ - public final static UUID SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb"); // TODO change the UUID to your match your service - /** The characteristic UUID */ - private static final UUID MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb"); // TODO change the UUID to your match your characteristic + static final UUID SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb"); // Heart Rate service + /** A UUID of a characteristic with notify property */ + private static final UUID MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb"); // Heart Rate Measurement + /** A UUID of a characteristic with read property */ + private static final UUID READABLE_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb"); // Body Sensor Location + /** Some other service UUID */ + private static final UUID OTHER_SERVICE_UUID = UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"); // Generic Access service + /** A UUID of a characteristic with write property */ + private static final UUID WRITABLE_CHARACTERISTIC_UUID = UUID.fromString("00002A00-0000-1000-8000-00805f9b34fb"); // Device Name - // TODO add more services and characteristics, if required - private BluetoothGattCharacteristic mCharacteristic; + // TODO Add more services and characteristics references. + private BluetoothGattCharacteristic mRequiredCharacteristic, mDeviceNameCharacteristic, mOptionalCharacteristic; public TemplateManager(final Context context) { super(context); @@ -56,83 +65,147 @@ public class TemplateManager extends BleManager { @NonNull @Override - protected BleManagerGattCallback getGattCallback() { + protected BatteryManagerGattCallback getGattCallback() { return mGattCallback; } /** * BluetoothGatt callbacks for connection/disconnection, service discovery, receiving indication, etc */ - private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() { + private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() { @Override - protected Deque initGatt(@NonNull final BluetoothGatt gatt) { - final LinkedList requests = new LinkedList<>(); - // TODO initialize your device, enable required notifications and indications, write what needs to be written to start working - requests.add(Request.newEnableNotificationsRequest(mCharacteristic)); - return requests; + protected void initialize() { + // Initialize the Battery Manager. It will enable Battery Level notifications. + // Remove it if you don't need this feature. + super.initialize(); + + // TODO Initialize your manager here. + // Initialization is done once, after the device is connected. Usually it should + // enable notifications or indications on some characteristics, write some data or + // read some features / version. + // After the initialization is complete, the onDeviceReady(...) method will be called. + + // Increase the MTU + requestMtu(43) + .with((device, mtu) -> log(LogContract.Log.Level.APPLICATION, "MTU changed to " + mtu)) + .done(device -> { + // You may do some logic in here that should be done when the request finished successfully. + // In case of MTU this method is called also when the MTU hasn't changed, or has changed + // to a different (lower) value. Use .with(...) to get the MTU value. + }) + .fail((device, status) -> log(LogContract.Log.Level.WARNING, "MTU change not supported")); + + // Set notification callback + setNotificationCallback(mRequiredCharacteristic) + // This callback will be called each time the notification is received + .with(new TemplateDataCallback() { + @Override + public void onSampleValueReceived(@NonNull final BluetoothDevice device, final int value) { + // Let's lass received data to the service + mCallbacks.onSampleValueReceived(device, value); + } + + @Override + public void onInvalidDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { + log(LogContract.Log.Level.WARNING, "Invalid data received: " + data); + } + }); + + // Enable notifications + enableNotifications(mRequiredCharacteristic) + // Method called after the data were sent (data will contain 0x0100 in this case) + .with((device, data) -> log(LogContract.Log.Level.DEBUG, "Data sent: " + data)) + // Method called when the request finished successfully. This will be called after .with(..) callback + .done(device -> log(LogContract.Log.Level.APPLICATION, "Notifications enabled successfully")) + // Methods called in case of an error, for example when the characteristic does not have Notify property + .fail((device, status) -> log(LogContract.Log.Level.WARNING, "Failed to enable notifications")); } @Override protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { + // TODO Initialize required characteristics. + // It should return true if all has been discovered (that is that device is supported). final BluetoothGattService service = gatt.getService(SERVICE_UUID); if (service != null) { - mCharacteristic = service.getCharacteristic(MEASUREMENT_CHARACTERISTIC_UUID); + mRequiredCharacteristic = service.getCharacteristic(MEASUREMENT_CHARACTERISTIC_UUID); } - return mCharacteristic != null; + final BluetoothGattService otherService = gatt.getService(OTHER_SERVICE_UUID); + if (otherService != null) { + mDeviceNameCharacteristic = otherService.getCharacteristic(WRITABLE_CHARACTERISTIC_UUID); + } + return mRequiredCharacteristic != null && mDeviceNameCharacteristic != null; + } + + @Override + protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) { + // Initialize Battery characteristic + super.isOptionalServiceSupported(gatt); + + // TODO If there are some optional characteristics, initialize them there. + final BluetoothGattService service = gatt.getService(SERVICE_UUID); + if (service != null) { + mOptionalCharacteristic = service.getCharacteristic(READABLE_CHARACTERISTIC_UUID); + } + return mOptionalCharacteristic != null; } @Override protected void onDeviceDisconnected() { - mCharacteristic = null; - } + // Release Battery Service + super.onDeviceDisconnected(); - // TODO implement data handlers. Methods below are called after the initialization is complete. + // TODO Release references to your characteristics. + mRequiredCharacteristic = null; + mDeviceNameCharacteristic = null; + mOptionalCharacteristic = null; + } @Override protected void onDeviceReady() { super.onDeviceReady(); - // TODO initialization is now ready. The activity is being notified using TemplateManagerCallbacks#onDeviceReady() method. - // This method may be removed from this class if not required as the super class implementation handles this event. - } + // Initialization is now ready. + // The service or activity has been notified with TemplateManagerCallbacks#onDeviceReady(). + // TODO Do some extra logic here, of remove onDeviceReady(). - @Override - protected void onCharacteristicNotified(@NonNull final BluetoothGatt gatt, @NonNull final BluetoothGattCharacteristic characteristic) { - // TODO this method is called when a notification has been received - // This method may be removed from this class if not required - - log(LogContract.Log.Level.APPLICATION, "\"" + TemplateParser.parse(characteristic) + "\" received"); - - int value; - final int flags = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); - if ((flags & 0x01) > 0) { - value = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1); - } else { - value = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1); - } - //This will send callback to the Activity when new value is received from HR device - mCallbacks.onSampleValueReceived(gatt.getDevice(), value); - } - - @Override - protected void onCharacteristicIndicated(@NonNull final BluetoothGatt gatt, @NonNull final BluetoothGattCharacteristic characteristic) { - // TODO this method is called when an indication has been received - // This method may be removed from this class if not required - } - - @Override - protected void onCharacteristicRead(@NonNull final BluetoothGatt gatt, @NonNull final BluetoothGattCharacteristic characteristic) { - // TODO this method is called when the characteristic has been read - // This method may be removed from this class if not required - } - - @Override - protected void onCharacteristicWrite(@NonNull final BluetoothGatt gatt, @NonNull final BluetoothGattCharacteristic characteristic) { - // TODO this method is called when the characteristic has been written - // This method may be removed from this class if not required + // Device is ready, let's read something here. Usually there is nothing else to be done + // here, as all had been done during initialization. + readCharacteristic(mOptionalCharacteristic) + .with((device, data) -> { + // Characteristic value has been read + // Let's do some magic with it. + if (data.size() > 0) { + final Integer value = data.getIntValue(Data.FORMAT_UINT8, 0); + log(LogContract.Log.Level.APPLICATION, "Value '" + value + "' has been read!"); + } else { + log(LogContract.Log.Level.WARNING, "Value is empty!"); + } + }); } }; + // TODO Define manager's API + /** + * This method will write important data to the device. + * + * @param parameter parameter to be written. + */ + void performAction(final String parameter) { + log(LogContract.Log.Level.VERBOSE, "Changing device name to \"" + parameter + "\""); + // Write some data to the characteristic. + writeCharacteristic(mDeviceNameCharacteristic, Data.from(parameter)) + // If data are longer than MTU-3, they will be chunked into multiple packets. + // Check out other split options, with .split(...). + .split() + // Callback called when data were sent, or added to outgoing queue is Write Without Request + // type has been chosen. + .with((device, data) -> log(LogContract.Log.Level.DEBUG, data.size() + " bytes were sent")) + // Callback called when data were sent, or added to outgoing queue is Write Without Request + // type has been chosen. + .done(device -> log(LogContract.Log.Level.APPLICATION, "Device name set to \"" + parameter + "\"")) + // Callback called when write has failed. + .fail((device, status) -> log(LogContract.Log.Level.WARNING, "Failed to change device name")); + } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java index fcbd285d..05b03d88 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java @@ -21,24 +21,18 @@ */ package no.nordicsemi.android.nrftoolbox.template; -import android.bluetooth.BluetoothDevice; - -import no.nordicsemi.android.ble.BleManagerCallbacks; +import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; +import no.nordicsemi.android.nrftoolbox.template.callback.TemplateCharacteristicCallback; /** - * Interface {@link TemplateManagerCallbacks} must be implemented by {@link TemplateActivity} in order to receive callbacks from {@link TemplateManager} + * Interface {@link TemplateManagerCallbacks} must be implemented by {@link TemplateService} + * in order to receive callbacks from {@link TemplateManager} */ -public interface TemplateManagerCallbacks extends BleManagerCallbacks { +interface TemplateManagerCallbacks extends BatteryManagerCallbacks, TemplateCharacteristicCallback { - // TODO add more callbacks. Callbacks are called when a data has been received/written to a remote device. This is the way how the manager notifies the activity about this event. - - /** - * Called when a value is received. - * - * @param device a device from which the value was obtained - * @param value - * the new value - */ - void onSampleValueReceived(final BluetoothDevice device, int value); + // Callbacks are called when a data has been received/written to a remote device. + // This is the way how the manager notifies the activity about this event. + // TODO add more callbacks. + // If you need, create more ...Callback interfaces and extend this interface with them. } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java index a92964e4..2f340f67 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java @@ -30,6 +30,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; @@ -44,6 +45,9 @@ public class TemplateService extends BleProfileService implements TemplateManage public static final String BROADCAST_TEMPLATE_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.template.BROADCAST_MEASUREMENT"; public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.template.EXTRA_DATA"; + public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; + public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; + private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.template.ACTION_DISCONNECT"; private final static int NOTIFICATION_ID = 864; @@ -55,16 +59,19 @@ public class TemplateService extends BleProfileService implements TemplateManage private final LocalBinder mBinder = new TemplateBinder(); /** - * This local binder is an interface for the bonded activity to operate with the HTS sensor + * This local binder is an interface for the bound activity to operate with the sensor. */ - public class TemplateBinder extends LocalBinder { - // empty - // TODO add some service API that mey be used from the Activity + class TemplateBinder extends LocalBinder { + // TODO Define service API that may be used by a bound Activity - // public void setLights(final boolean on) { - // Logger.v(getLogSession(), "Light set to: " + on); - // mManager.setLights(on); - // } + /** + * Sends some important data to the device. + * + * @param parameter some parameter. + */ + public void performAction(final String parameter) { + mManager.performAction(parameter); + } } @Override @@ -108,7 +115,7 @@ public class TemplateService extends BleProfileService implements TemplateManage } @Override - public void onSampleValueReceived(final BluetoothDevice device, final int value) { + public void onSampleValueReceived(@NonNull final BluetoothDevice device, final int value) { final Intent broadcast = new Intent(BROADCAST_TEMPLATE_MEASUREMENT); broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); broadcast.putExtra(EXTRA_DATA, value); @@ -120,14 +127,17 @@ public class TemplateService extends BleProfileService implements TemplateManage } } + @Override + public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { + + } + /** - * Creates the notification - * - * @param messageResId - * message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - * signals that will be used to notify the user + * Creates the notification. + * + * @param messageResId message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults signals that will be used to notify the user */ private void createNotification(final int messageResId, final int defaults) { final Intent parentIntent = new Intent(this, FeaturesActivity.class); @@ -138,7 +148,7 @@ public class TemplateService extends BleProfileService implements TemplateManage final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); + final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); builder.setContentIntent(pendingIntent); builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); @@ -172,5 +182,4 @@ public class TemplateService extends BleProfileService implements TemplateManage stopSelf(); } }; - } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java new file mode 100644 index 00000000..670788cb --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java @@ -0,0 +1,21 @@ +package no.nordicsemi.android.nrftoolbox.template.callback; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +/** + * This class defines your characteristic API. + * In this example (that is the HRM characteristic, which the template is based on), is notifying + * with a value (Heart Rate). The single method just returns the value and ignores other + * optional data from Heart Rate Measurement characteristic for simplicity. + */ +public interface TemplateCharacteristicCallback { + + /** + * Called when a value is received. + * + * @param device a device from which the value was obtained. + * @param value the new value. + */ + void onSampleValueReceived(@NonNull final BluetoothDevice device, int value); +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java new file mode 100644 index 00000000..dcc209be --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java @@ -0,0 +1,47 @@ +package no.nordicsemi.android.nrftoolbox.template.callback; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +import no.nordicsemi.android.ble.callback.profile.ProfileDataCallback; +import no.nordicsemi.android.ble.data.Data; + +/** + * This is a sample data callback, that's based on Heart Rate Measurement characteristic. + * It parses the HR value and ignores other optional data for simplicity. + * Check {@link no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback} + * for full implementation. + * + * TODO Modify the content to parse your data. + */ +@SuppressWarnings("ConstantConditions") +public abstract class TemplateDataCallback implements ProfileDataCallback, TemplateCharacteristicCallback { + + @Override + public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { + if (data.size() < 2) { + onInvalidDataReceived(device, data); + return; + } + + // Read flags + int offset = 0; + final int flags = data.getIntValue(Data.FORMAT_UINT8, offset); + final int hearRateType = (flags & 0x01) == 0 ? Data.FORMAT_UINT8 : Data.FORMAT_UINT16; + offset += 1; + + // Validate packet length. The type's lower nibble is its length. + if (data.size() < 1 + (hearRateType & 0x0F)) { + onInvalidDataReceived(device, data); + return; + } + + final int value = data.getIntValue(hearRateType, offset); + // offset += hearRateType & 0xF; + + // ... + + // Report the parsed value(s) + onSampleValueReceived(device, value); + } +} diff --git a/app/src/main/res/layout/activity_feature_template.xml b/app/src/main/res/layout/activity_feature_template.xml index ba9a2a01..2118c713 100644 --- a/app/src/main/res/layout/activity_feature_template.xml +++ b/app/src/main/res/layout/activity_feature_template.xml @@ -79,6 +79,7 @@ +