Bluetooth LE: implemented cycling cadence and speed sensors.

Cadence is working while speed still requires the wheel size.
This commit is contained in:
Dennis Guse
2020-04-17 07:33:00 +02:00
parent bbe182cf82
commit ddb17f61cb
32 changed files with 1099 additions and 338 deletions
@@ -45,6 +45,7 @@ import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
@@ -575,7 +576,7 @@ public class TrackRecordingService extends Service {
return;
}
TrackPoint trackPoint = new TrackPoint(location, getSensorDataSet());
TrackPoint trackPoint = new TrackPoint(location, getTrackPointSensorDataSet());
notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy);
if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) {
@@ -708,10 +709,20 @@ public class TrackRecordingService extends Service {
}
SensorDataSet getSensorDataSet() {
if (remoteSensorManager == null || !remoteSensorManager.isEnabled() || !remoteSensorManager.isSensorDataSetValid()) {
if (remoteSensorManager == null || !remoteSensorManager.isEnabled()) {
return null;
}
return remoteSensorManager.getSensorDataSet();
return remoteSensorManager.getSensorData();
}
TrackPointSensorDataSet getTrackPointSensorDataSet() {
SensorDataSet sensorData = getSensorDataSet();
if (sensorData != null) {
return sensorData.createTrackPointSensorDataSet();
}
return null;
}
private void registerLocationListener() {
@@ -95,8 +95,13 @@ public interface TrackRecordingServiceInterface {
long insertWaypoint(String name, String category, String description, String photoUrl);
/**
* ONLY FOR TESTING
* Gets the current sensor data. Returns null if there is no data.
*
* @return SensorDataSet object.
*/
SensorDataSet getSensorData();
/**
* Inserts a location in the current recording track.
* <p>
* When recording a track, GPS locations are automatically inserted.
@@ -107,13 +112,6 @@ public interface TrackRecordingServiceInterface {
@VisibleForTesting
void insertLocation(Location location);
/**
* Gets the current sensor data. Returns null if there is no data.
*
* @return SensorDataSet object.
*/
SensorDataSet getSensorData();
/**
* Disables processing of location updates from {@link android.location.LocationManager}.
*/
@@ -30,27 +30,24 @@ import androidx.annotation.NonNull;
import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.util.BluetoothUtils;
/**
* Manages connection to Bluetooth LE heart rate monitor.
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
* Also parses the transferred data into {@link SensorDataObserver}.
*/
public class BluetoothConnectionManager {
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
public abstract class BluetoothConnectionManager {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final Context context;
private final EventCallback observer;
private SensorState sensorState;
private final SensorDataObserver observer;
private final UUID serviceUUUID;
private final UUID measurementUUID;
private BluetoothGatt bluetoothGatt;
private final BluetoothDevice bluetoothDevice;
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override
@@ -58,80 +55,70 @@ public class BluetoothConnectionManager {
switch (newState) {
case BluetoothProfile.STATE_CONNECTING:
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
setState(SensorState.CONNECTING);
observer.connecting(gatt.getDevice().getName());
case BluetoothProfile.STATE_CONNECTED:
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
setState(SensorState.CONNECTED);
gatt.discoverServices();
observer.connected(gatt.getDevice().getName());
break;
case BluetoothProfile.STATE_DISCONNECTING:
Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice());
setState(SensorState.DISCONNECTING);
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
setState(SensorState.DISCONNECTED);
observer.disconnected(gatt.getDevice().getName());
}
}
@Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothGattService service = gatt.getService(BluetoothUtils.HEART_RATE_SERVICE_UUID);
BluetoothGattService service = gatt.getService(serviceUUUID);
if (service == null) {
Log.e(TAG, "Could not get heart rate service for " + gatt.getDevice().getAddress());
Log.e(TAG, "Could not get service for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID);
return;
}
BluetoothGattCharacteristic characteristic = service.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
BluetoothGattCharacteristic characteristic = service.getCharacteristic(measurementUUID);
if (characteristic == null) {
Log.e(TAG, "Could not get BluetoothCharacteristic for " + gatt.getDevice().getAddress());
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID + " characteristicUUID=" + measurementUUID);
return;
}
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
// Register for updates.
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
String deviceName = gatt.getDevice().getName();
String sensorName = gatt.getDevice().getName();
Log.d(TAG, "Received data from " + sensorName);
Log.d(TAG, "Received heart beat rate " + deviceName + ": " + heartRate);
observer.onSensorDataReceived(new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress()));
SensorData sensorData = parsePayload(sensorName, gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
observer.onChanged(sensorData);
}
}
};
BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, EventCallback observer) {
this.context = context;
this.bluetoothDevice = bluetoothDevice;
this.sensorState = SensorState.NONE;
BluetoothConnectionManager(UUID serviceUUUID, UUID measurementUUID, SensorDataObserver observer) {
this.serviceUUUID = serviceUUUID;
this.measurementUUID = measurementUUID;
this.observer = observer;
}
public synchronized void connect() {
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
if (bluetoothGatt != null) {
Log.w(TAG, "Already connected; ignoring.");
}
Log.d(TAG, "Connecting to: " + bluetoothDevice);
Log.d(TAG, "Connecting to: " + device);
bluetoothGatt = bluetoothDevice.connectGatt(this.context, true, this.connectCallback);
bluetoothGatt = device.connectGatt(context, true, this.connectCallback);
setState(SensorState.CONNECTING);
}
public synchronized void disconnect() {
synchronized void disconnect() {
if (bluetoothGatt == null) {
Log.w(TAG, "Cannot disconnect if not connected.");
return;
@@ -140,27 +127,78 @@ public class BluetoothConnectionManager {
bluetoothGatt = null;
}
public synchronized boolean isSameBluetoothDevice(String address) {
return this.bluetoothDevice.getAddress().equals(address);
synchronized boolean isSameBluetoothDevice(String address) {
if (bluetoothGatt == null) {
return false;
}
return address.equals(bluetoothGatt.getDevice().getAddress());
}
synchronized SensorState getSensorState() {
return sensorState;
/**
* @return null if data could not be parsed.
*/
protected abstract de.dennisguse.opentracks.content.sensor.SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
public static class HeartRate extends BluetoothConnectionManager {
HeartRate(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataHeartRate parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
return heartRate != null ? new SensorDataHeartRate(address, sensorName, heartRate) : null;
}
}
private synchronized void setState(SensorState sensorState) {
this.sensorState = sensorState;
public static class CyclingCadence extends BluetoothConnectionManager {
CyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataCycling.Cadence parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getCadence() != null) {
return cadenceAndSpeed.getCadence();
}
//Workaround for Wahoo CADENCE: this sensor reports speed (instead of cadence)
if (cadenceAndSpeed.getSpeed() != null) {
return new SensorDataCycling.Cadence(cadenceAndSpeed.getSpeed());
}
return null;
}
}
public static class CyclingSpeed extends BluetoothConnectionManager {
interface EventCallback {
CyclingSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
void connecting(String sensorName);
@Override
protected SensorDataCycling.Speed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed != null) {
return cadenceAndSpeed.getSpeed();
}
return null;
}
}
void connected(String sensorName);
interface SensorDataObserver {
void onSensorDataReceived(SensorDataSet sensorDataSet);
void onChanged(SensorData sensorData);
void disconnected(String sensorName);
}
}
@@ -21,53 +21,72 @@ import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.widget.Toast;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Bluetooth LE sensor manager.
* Should only be instantiated once!
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
*
* Note: should only be instantiated once.
*
* TODO: listen for Bluetooth enabled/disabled events.
*
* TODO: In case, a cycling (Cadence and Speed) sensor reports both values, testing is required.
* We establish two GATT separate GATT connections (as if two different sensors were used).
* However, it is not clear if this is allowed.
* Even if this works, it is not clear what happens if a user (while recording) changes one of the sensors in the settings as this will trigger a disconnect of one GATT.
*
* @author Sandor Dornbush
*/
public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.EventCallback {
public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.SensorDataObserver {
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5000;
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5 * UnitConversions.S_TO_MS;
private static final BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
private final Context context;
private final SharedPreferences sharedPreferences;
private boolean started = false;
private final BluetoothConnectionManager heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
if (!started) return;
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
if (PreferencesUtils.isBluetoothHeartRateSensorAddressNone(context)) {
stop();
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
connect(heartRate, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
if (PreferencesUtils.isBluetoothCyclingCadenceSensorAddressNone(context)) {
stop();
return;
}
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress(context);
disconnect();
startCurrentSensor();
connect(cyclingCadence, address);
}
}
};
private SensorDataSet sensorDataSet = null;
private BluetoothConnectionManager bluetoothConnectionManager;
/**
* @param context the context
*/
@@ -77,84 +96,76 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
}
public void start() {
started = true;
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
startCurrentSensor();
//Trigger connection startup
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
public void stop() {
disconnect();
public synchronized void stop() {
// Disconnecting
heartRate.disconnect();
cyclingCadence.disconnect();
sensorDataSet.clear();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
started = false;
}
public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
public boolean isSensorDataSetValid() {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet == null) {
return false;
}
return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE_MS);
}
private void startCurrentSensor() {
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
Log.w(TAG, "No bluetooth address.");
if (PreferencesUtils.isBluetoothSensorAddressNone(context, address)) {
Log.w(TAG, "No Bluetooth address.");
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
// Check if there is an ongoing connection; if yes, check if the address changed.
if (connectionManager.isSameBluetoothDevice(address)) {
return;
} else {
connectionManager.disconnect();
}
Log.i(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device;
try {
device = bluetoothAdapter.getRemoteDevice(address);
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
connectionManager.connect(context, device);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Unable to get remote device for: " + address, e);
String toastMessage = context.getString(R.string.sensor_not_known, address);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
return;
}
disconnect();
bluetoothConnectionManager = new BluetoothConnectionManager(context, device, this);
bluetoothConnectionManager.connect();
}
private void disconnect() {
if (bluetoothConnectionManager != null) {
bluetoothConnectionManager.disconnect();
bluetoothConnectionManager = null;
Log.e(TAG, "Unable to get remote device for: " + address, e);
}
}
@Override
public void connecting(String sensorName) {
public SensorDataSet getSensorData() {
return sensorDataSet;
}
@Override
public void connected(String sensorName) {
}
public synchronized void onChanged(de.dennisguse.opentracks.content.sensor.SensorData sensorData) {
if (sensorData instanceof SensorDataCycling.Cadence) {
if (sensorData.equals(sensorDataSet.getCyclingCadence())) {
Log.d(TAG, "onChanged: cadence data repeated.");
return;
}
((SensorDataCycling.Cadence) sensorData).compute(sensorDataSet.getCyclingCadence());
}
if (sensorData instanceof SensorDataCycling.Speed) {
if (sensorData.equals(sensorDataSet.getCyclingSpeed())) {
Log.d(TAG, "onChanged: speed data repeated.");
return;
}
((SensorDataCycling.Speed) sensorData).compute(sensorDataSet.getCyclingSpeed());
}
@Override
public void onSensorDataReceived(SensorDataSet sensorDataSet) {
this.sensorDataSet = sensorDataSet;
}
@Override
public void disconnected(String sensorName) {
sensorDataSet.set(sensorData);
}
}