Migrating HRS to use new BleManager API

This commit is contained in:
Aleksander Nowakowski
2018-05-23 17:39:11 +02:00
parent 06da68c3b7
commit 21cd638758
6 changed files with 109 additions and 147 deletions

View File

@@ -28,11 +28,13 @@ import android.graphics.Point;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import android.widget.TextView;
import org.achartengine.GraphicalView;
import java.util.List;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManager;
@@ -53,8 +55,6 @@ public class HRSActivity extends BleProfileActivity implements HRSManagerCallbac
private final static String GRAPH_COUNTER = "graph_counter";
private final static String HR_VALUE = "hr_value";
private final static int MAX_HR_VALUE = 65535;
private final static int MIN_POSITIVE_VALUE = 0;
private final static int REFRESH_INTERVAL = 1000; // 1 second interval
private Handler mHandler = new Handler();
@@ -64,6 +64,7 @@ public class HRSActivity extends BleProfileActivity implements HRSManagerCallbac
private GraphicalView mGraphView;
private LineGraphView mLineGraph;
private TextView mHRSValue, mHRSPosition;
private TextView mBatteryLevelView;
private int mHrmValue = 0;
private int mCounter = 0;
@@ -78,6 +79,7 @@ public class HRSActivity extends BleProfileActivity implements HRSManagerCallbac
mLineGraph = LineGraphView.getLineGraphView();
mHRSValue = findViewById(R.id.text_hrs_value);
mHRSPosition = findViewById(R.id.text_hrs_position);
mBatteryLevelView = findViewById(R.id.battery);
showGraph();
}
@@ -183,26 +185,6 @@ public class HRSActivity extends BleProfileActivity implements HRSManagerCallbac
return manager;
}
private void setHRSValueOnView(final int value) {
runOnUiThread(() -> {
if (value >= MIN_POSITIVE_VALUE && value <= MAX_HR_VALUE) {
mHRSValue.setText(Integer.toString(value));
} else {
mHRSValue.setText(R.string.not_available_value);
}
});
}
private void setHRSPositionOnView(final String position) {
runOnUiThread(() -> {
if (position != null) {
mHRSPosition.setText(position);
} else {
mHRSPosition.setText(R.string.not_available);
}
});
}
@Override
public void onServicesDiscovered(final BluetoothDevice device, final boolean optionalServicesFound) {
// this may notify user or show some views
@@ -214,14 +196,28 @@ public class HRSActivity extends BleProfileActivity implements HRSManagerCallbac
}
@Override
public void onHRSensorPositionFound(final BluetoothDevice device, final String position) {
setHRSPositionOnView(position);
public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
runOnUiThread(() -> mBatteryLevelView.setText(getString(R.string.battery, batteryLevel)));
}
@Override
public void onHRValueReceived(final BluetoothDevice device, int value) {
mHrmValue = value;
setHRSValueOnView(mHrmValue);
public void onBodySensorLocationReceived(@NonNull final BluetoothDevice device, final int sensorLocation) {
runOnUiThread(() -> {
if (sensorLocation >= SENSOR_LOCATION_FIRST && sensorLocation <= SENSOR_LOCATION_LAST) {
mHRSPosition.setText(getResources().getStringArray(R.array.hrs_locations)[sensorLocation]);
} else {
mHRSPosition.setText(R.string.hrs_location_other);
}
});
}
@Override
public void onHeartRateMeasurementReceived(@NonNull final BluetoothDevice device, final int heartRate,
@Nullable final Boolean contactDetected,
@Nullable final Integer energyExpanded,
@Nullable final List<Integer> rrIntervals) {
mHrmValue = heartRate;
runOnUiThread(() -> mHRSValue.setText(getString(R.string.hrs_value, heartRate)));
}
@Override
@@ -230,6 +226,7 @@ public class HRSActivity extends BleProfileActivity implements HRSManagerCallbac
runOnUiThread(() -> {
mHRSValue.setText(R.string.not_available_value);
mHRSPosition.setText(R.string.not_available);
mBatteryLevelView.setText(R.string.not_available);
stopShowGraph();
});
}
@@ -238,6 +235,7 @@ public class HRSActivity extends BleProfileActivity implements HRSManagerCallbac
protected void setDefaultUI() {
mHRSValue.setText(R.string.not_available_value);
mHRSPosition.setText(R.string.not_available);
mBatteryLevelView.setText(R.string.not_available);
clearGraph();
}

View File

@@ -21,33 +21,37 @@
*/
package no.nordicsemi.android.nrftoolbox.hrs;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.Request;
import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallback;
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.log.LogContract;
import no.nordicsemi.android.nrftoolbox.R;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManager;
import no.nordicsemi.android.nrftoolbox.parser.BodySensorLocationParser;
import no.nordicsemi.android.nrftoolbox.parser.HeartRateMeasurementParser;
/**
* HRSManager class performs BluetoothGatt operations for connection, service discovery, enabling notification and reading characteristics. All operations required to connect to device with BLE HR
* Service and reading heart rate values are performed here. HRSActivity implements HRSManagerCallbacks in order to receive callbacks of BluetoothGatt operations
* HRSManager class performs BluetoothGatt operations for connection, service discovery,
* enabling notification and reading characteristics.
* All operations required to connect to device with BLE Heart Rate Service and reading
* heart rate values are performed here.
*/
public class HRSManager extends BleManager<HRSManagerCallbacks> {
public final static UUID HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb");
public class HRSManager extends BatteryManager<HRSManagerCallbacks> {
public static final UUID HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb");
private static final UUID HR_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb");
private static final UUID HR_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb");
private BluetoothGattCharacteristic mHRCharacteristic, mHRLocationCharacteristic;
private BluetoothGattCharacteristic mHeartRateCharacteristic, mBodySensorLocationCharacteristic;
private static HRSManager managerInstance = null;
@@ -61,93 +65,83 @@ public class HRSManager extends BleManager<HRSManagerCallbacks> {
return managerInstance;
}
public HRSManager(final Context context) {
private HRSManager(final Context context) {
super(context);
}
@NonNull
@Override
protected BleManagerGattCallback getGattCallback() {
protected BatteryManagerGattCallback getGattCallback() {
return mGattCallback;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery, receiving notification, etc
*/
private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {
private final BatteryManagerGattCallback mGattCallback = new BatteryManagerGattCallback() {
@Override
protected Deque<Request> initGatt(@NonNull final BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
if (mHRLocationCharacteristic != null)
requests.add(Request.newReadRequest(mHRLocationCharacteristic));
requests.add(Request.newEnableNotificationsRequest(mHRCharacteristic));
return requests;
protected void initialize() {
super.initialize();
readCharacteristic(mBodySensorLocationCharacteristic)
.with(new BodySensorLocationDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + BodySensorLocationParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onBodySensorLocationReceived(@NonNull final BluetoothDevice device,
final int sensorLocation) {
mCallbacks.onBodySensorLocationReceived(device, sensorLocation);
}
})
.fail((device, status) -> log(LogContract.Log.Level.WARNING, "Body Sensor Location characteristic not found"));
setNotificationCallback(mHeartRateCharacteristic)
.with(new HeartRateMeasurementDataCallback() {
@Override
public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) {
log(LogContract.Log.Level.APPLICATION, "\"" + HeartRateMeasurementParser.parse(data) + "\" received");
super.onDataReceived(device, data);
}
@Override
public void onHeartRateMeasurementReceived(@NonNull final BluetoothDevice device,
final int heartRate,
@Nullable final Boolean contactDetected,
@Nullable final Integer energyExpanded,
@Nullable final List<Integer> rrIntervals) {
mCallbacks.onHeartRateMeasurementReceived(device, heartRate, contactDetected, energyExpanded, rrIntervals);
}
});
enableNotifications(mHeartRateCharacteristic);
}
@Override
protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
final BluetoothGattService service = gatt.getService(HR_SERVICE_UUID);
if (service != null) {
mHRCharacteristic = service.getCharacteristic(HR_CHARACTERISTIC_UUID);
mHeartRateCharacteristic = service.getCharacteristic(HR_CHARACTERISTIC_UUID);
}
return mHRCharacteristic != null;
return mHeartRateCharacteristic != null;
}
@Override
protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
super.isOptionalServiceSupported(gatt);
final BluetoothGattService service = gatt.getService(HR_SERVICE_UUID);
if (service != null) {
mHRLocationCharacteristic = service.getCharacteristic(HR_SENSOR_LOCATION_CHARACTERISTIC_UUID);
mBodySensorLocationCharacteristic = service.getCharacteristic(HR_SENSOR_LOCATION_CHARACTERISTIC_UUID);
}
return mHRLocationCharacteristic != null;
}
@Override
public void onCharacteristicRead(@NonNull final BluetoothGatt gatt, @NonNull final BluetoothGattCharacteristic characteristic) {
log(LogContract.Log.Level.APPLICATION, "\"" + BodySensorLocationParser.parse(characteristic) + "\" received");
final String sensorPosition = getBodySensorPosition(characteristic.getValue()[0]);
//This will send callback to HRSActivity when HR sensor position on body is found in HR device
mCallbacks.onHRSensorPositionFound(gatt.getDevice(), sensorPosition);
return mBodySensorLocationCharacteristic != null;
}
@Override
protected void onDeviceDisconnected() {
mHRLocationCharacteristic = null;
mHRCharacteristic = null;
}
@Override
public void onCharacteristicNotified(@NonNull final BluetoothGatt gatt, @NonNull final BluetoothGattCharacteristic characteristic) {
log(LogContract.Log.Level.APPLICATION, "\"" + HeartRateMeasurementParser.parse(characteristic) + "\" received");
int hrValue;
if (isHeartRateInUINT16(characteristic.getValue()[0])) {
hrValue = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1);
} else {
hrValue = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1);
}
//This will send callback to HRSActivity when new HR value is received from HR device
mCallbacks.onHRValueReceived(gatt.getDevice(), hrValue);
super.onDeviceDisconnected();
mBodySensorLocationCharacteristic = null;
mHeartRateCharacteristic = null;
}
};
/**
* This method will decode and return Heart rate sensor position on body
*/
private String getBodySensorPosition(final byte bodySensorPositionValue) {
final String[] locations = getContext().getResources().getStringArray(R.array.hrs_locations);
if (bodySensorPositionValue > locations.length)
return getContext().getString(R.string.hrs_location_other);
return locations[bodySensorPositionValue];
}
/**
* This method will check if Heart rate value is in 8 bits or 16 bits
*/
private boolean isHeartRateInUINT16(final byte value) {
return ((value & 0x01) != 0);
}
}

View File

@@ -21,27 +21,10 @@
*/
package no.nordicsemi.android.nrftoolbox.hrs;
import android.bluetooth.BluetoothDevice;
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocationCallback;
import no.nordicsemi.android.ble.common.profile.hr.HeartRateMeasurementCallback;
import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks;
import no.nordicsemi.android.ble.BleManagerCallbacks;
interface HRSManagerCallbacks extends BatteryManagerCallbacks, BodySensorLocationCallback, HeartRateMeasurementCallback {
public interface HRSManagerCallbacks extends BleManagerCallbacks {
/**
* Called when the sensor position information has been obtained from the sensor
*
* @param device the bluetooth device from which the value was obtained
* @param position
* the sensor position
*/
void onHRSensorPositionFound(final BluetoothDevice device, String position);
/**
* Called when new Heart Rate value has been obtained from the sensor
*
* @param device the bluetooth device from which the value was obtained
* @param value
* the new value
*/
void onHRValueReceived(final BluetoothDevice device, int value);
}

View File

@@ -21,36 +21,22 @@
*/
package no.nordicsemi.android.nrftoolbox.parser;
import android.bluetooth.BluetoothGattCharacteristic;
import no.nordicsemi.android.ble.data.Data;
public class BodySensorLocationParser {
public static String parse(final BluetoothGattCharacteristic characteristic) {
final int value = unsignedByteToInt(characteristic.getValue()[0]);
public static String parse(final Data data) {
final int value = data.getIntValue(Data.FORMAT_UINT8, 0);
switch (value) {
case 6:
return "Foot";
case 5:
return "Ear Lobe";
case 4:
return "Hand";
case 3:
return "Finger";
case 2:
return "Wrist";
case 1:
return "Chest";
case 0:
default:
return "Other";
case 6: return "Foot";
case 5: return "Ear Lobe";
case 4: return "Hand";
case 3: return "Finger";
case 2: return "Wrist";
case 1: return "Chest";
case 0:
default: return "Other";
}
}
/**
* Convert a signed byte to an unsigned int.
*/
private static int unsignedByteToInt(byte b) {
return b & 0xFF;
}
}

View File

@@ -21,21 +21,21 @@
*/
package no.nordicsemi.android.nrftoolbox.parser;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import no.nordicsemi.android.ble.data.Data;
public class HeartRateMeasurementParser {
private static final byte HEART_RATE_VALUE_FORMAT = 0x01; // 1 bit
private static final byte SENSOR_CONTACT_STATUS = 0x06; // 2 bits
private static final byte ENERGY_EXPANDED_STATUS = 0x08; // 1 bit
private static final byte RR_INTERVAL = 0x10; // 1 bit
public static String parse(final BluetoothGattCharacteristic characteristic) {
public static String parse(final Data data) {
int offset = 0;
final int flags = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, offset++);
final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++);
/*
* false Heart Rate Value Format is set to UINT8. Units: beats per minute (bpm)
@@ -64,21 +64,21 @@ public class HeartRateMeasurementParser {
final boolean rrIntervalStatus = (flags & RR_INTERVAL) > 0;
// heart rate value is 8 or 16 bit long
int heartRateValue = characteristic.getIntValue(value16bit ? BluetoothGattCharacteristic.FORMAT_UINT16 : BluetoothGattCharacteristic.FORMAT_UINT8, offset++); // bits per minute
int heartRateValue = data.getIntValue(value16bit ? Data.FORMAT_UINT16 : Data.FORMAT_UINT8, offset++); // bits per minute
if (value16bit)
offset++;
// energy expanded value is present if a flag was set
int energyExpanded = -1;
if (energyExpandedStatus)
energyExpanded = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, offset);
energyExpanded = data.getIntValue(Data.FORMAT_UINT16, offset);
offset += 2;
// RR-interval is set when a flag is set
final List<Float> rrIntervals = new ArrayList<>();
if (rrIntervalStatus) {
for (int o = offset; o < characteristic.getValue().length; o += 2) {
final int units = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, o);
for (int o = offset; o < data.getValue().length; o += 2) {
final int units = data.getIntValue(Data.FORMAT_UINT16, o);
rrIntervals.add(units * 1000.0f / 1024.0f); // RR interval is in [1/1024s]
}
}

View File

@@ -27,6 +27,7 @@
<dimen name="hrs_feature_title_long_margin">-152dp</dimen>
<string name="hrs_default_name">DEFAULT HRM</string>
<string name="hrs_value">%d</string>
<string name="hrs_value_unit">bpm</string>
<string name="hrs_position_label">sensor position</string>