From 242147e49122563d35eaf0105fbc6bf79f75edc3 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Wed, 21 Jul 2021 22:26:57 +0200 Subject: [PATCH] Sensor: adding Running Speed and Cadence. Fixes #891. --- README.md | 2 + README_TESTED_SENSORS.md | 5 + .../opentracks/util/BluetoothUtilsTest.java | 17 +++ .../opentracks/content/data/Distance.java | 10 +- .../content/sensor/SensorDataRunning.java | 109 ++++++++++++++++++ .../content/sensor/SensorDataSet.java | 24 +++- .../sensors/BluetoothConnectionManager.java | 34 ++++-- .../sensors/BluetoothRemoteSensorManager.java | 28 ++++- .../opentracks/settings/SettingsActivity.java | 3 + ...othLeRunningSpeedAndCadencePreference.java | 23 ++++ .../opentracks/util/BluetoothUtils.java | 45 ++++++++ .../opentracks/util/PreferencesUtils.java | 4 + .../opentracks/util/UnitConversions.java | 2 - src/main/res/values/settings.xml | 1 + src/main/res/values/strings.xml | 1 + src/main/res/xml/settings.xml | 4 + 16 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataRunning.java create mode 100644 src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java diff --git a/README.md b/README.md index d66ced1342..577766bb13 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva * cycling: speed and distance * cycling: cadence * cycling: power meter + * running: speed and cadence + An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md) ### Gadgetbridge integration diff --git a/README_TESTED_SENSORS.md b/README_TESTED_SENSORS.md index 1035dd9f19..d70769721f 100644 --- a/README_TESTED_SENSORS.md +++ b/README_TESTED_SENSORS.md @@ -50,3 +50,8 @@ Also the distance is not computed. * Wahoo Kickr v4.0 * Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061) +## RSC: running cadence and speed + +We do not support type of movement (i.e., walking vs running). + +* Polar Stride \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java index 58dcf0c6b1..30ec4aaab6 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java @@ -4,7 +4,10 @@ import org.junit.Test; +import de.dennisguse.opentracks.content.data.Distance; +import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.sensor.SensorDataCycling; +import de.dennisguse.opentracks.content.sensor.SensorDataRunning; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -87,4 +90,18 @@ public void parseCyclingPower_power() { // then assertEquals(40, power_w); } + + @Test + public void parseRunningSpeedAndCadence_with_distance() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, 0, 0); + characteristic.setValue(new byte[]{2, 0, 5, 80, 12, 1}); + + // when + SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic); + + // then + assertEquals(Speed.of(5), sensor.getSpeed()); + assertEquals(80, sensor.getCadence(), 0.01); + assertEquals(Distance.of(268), sensor.getTotalDistance()); + } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Distance.java b/src/main/java/de/dennisguse/opentracks/content/data/Distance.java index b323c90e8e..ee610454c0 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/Distance.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/Distance.java @@ -25,7 +25,15 @@ public static Distance ofKilometer(double distance_km) { } public static Distance ofMM(double distance_mm) { - return of(distance_mm * UnitConversions.MM_TO_M); + return of(0.001 * distance_mm); + } + + public static Distance ofCM(double distance_cm) { + return of(0.01 * distance_cm); + } + + public static Distance ofDM(double distance_dm) { + return of(0.1 * distance_dm); } public static Distance one(boolean metricUnit) { diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataRunning.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataRunning.java new file mode 100644 index 0000000000..403dff6cd6 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataRunning.java @@ -0,0 +1,109 @@ +package de.dennisguse.opentracks.content.sensor; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import de.dennisguse.opentracks.content.data.Distance; +import de.dennisguse.opentracks.content.data.Speed; + +/** + * Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors. + */ +public final class SensorDataRunning extends SensorData { + + private static final String TAG = SensorDataRunning.class.getSimpleName(); + + private final Speed speed; + + private final Float cadence; + + private final Distance totalDistance; + + public SensorDataRunning(String sensorAddress) { + super(sensorAddress); + this.speed = null; + this.cadence = null; + this.totalDistance = null; + } + + public SensorDataRunning(String sensorAddress, String sensorName, Speed speed, Float cadence, Distance totalDistance) { + super(sensorAddress, sensorName); + this.speed = speed; + this.cadence = cadence; + this.totalDistance = totalDistance; + } + + private boolean hasTotalDistance() { + return totalDistance != null; + } + + + public Float getCadence() { + return cadence; + } + + @VisibleForTesting + public Speed getSpeed() { + return speed; + } + + @VisibleForTesting + public Distance getTotalDistance() { + return totalDistance; + } + + public void compute(SensorDataRunning previous) { + Distance overallDistance = null; + if (hasTotalDistance() && previous != null && previous.hasTotalDistance()) { + overallDistance = this.totalDistance.minus(previous.totalDistance); + if (previous.hasValue() && previous.getValue().getDistance() != null) { + overallDistance = overallDistance.plus(previous.getValue().getDistance()); + } + } + + value = new Data(speed, cadence, overallDistance); + } + + @Override + public void reset() { + if (value != null) { + value = new Data(value.speed, value.cadence, Distance.of(0)); + } + } + + public static class Data { + private final Speed speed; + private final Float cadence; + + @Nullable + private final Distance distance; + + public Data(Speed speed, Float cadence, @Nullable Distance distance) { + this.speed = speed; + this.cadence = cadence; + this.distance = distance; + } + + public Speed getSpeed() { + return speed; + } + + public Float getCadence() { + return cadence; + } + + public Distance getDistance() { + return distance; + } + + @Override + public String toString() { + return "Data{" + + "speed=" + speed + + ", cadence=" + cadence + + ", distance=" + distance + + '}'; + } + } +} + diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java index 22eb527f1b..c28e2d2f63 100644 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java @@ -14,6 +14,8 @@ public final class SensorDataSet { private SensorDataCyclingPower cyclingPower; + private SensorDataRunning runningDistanceSpeedCadence; + public SensorDataSet() { } @@ -22,6 +24,7 @@ public SensorDataSet(SensorDataSet toCopy) { this.cyclingCadence = toCopy.cyclingCadence; this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed; this.cyclingPower = toCopy.cyclingPower; + this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence; } public SensorDataHeartRate getHeartRate() { @@ -40,6 +43,10 @@ public SensorDataCyclingPower getCyclingPower() { return cyclingPower; } + public SensorDataRunning getRunningDistanceSpeedCadence() { + return runningDistanceSpeedCadence; + } + public void set(SensorData data) { set(data, data); } @@ -53,6 +60,7 @@ public void clear() { this.cyclingCadence = null; this.cyclingDistanceSpeed = null; this.cyclingPower = null; + this.runningDistanceSpeedCadence = null; } public void fillTrackPoint(TrackPoint trackPoint) { @@ -72,6 +80,12 @@ public void fillTrackPoint(TrackPoint trackPoint) { if (cyclingPower != null && cyclingPower.hasValue()) { trackPoint.setPower(cyclingPower.getValue()); } + + if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) { + trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().getDistance()); + trackPoint.setSpeed(runningDistanceSpeedCadence.getValue().getSpeed()); + trackPoint.setCyclingCadence_rpm(runningDistanceSpeedCadence.getValue().getCadence()); + } } public void reset() { @@ -79,6 +93,8 @@ public void reset() { if (cyclingCadence != null) cyclingCadence.reset(); if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset(); if (cyclingPower != null) cyclingPower.reset(); + + if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset(); } @NonNull @@ -87,7 +103,8 @@ public String toString() { return (getHeartRate() != null ? "" + getHeartRate() : "") + (getCyclingCadence() != null ? " " + getCyclingCadence() : "") + (getCyclingDistanceSpeed() != null ? " " + getCyclingDistanceSpeed() : "") - + (getCyclingPower() != null ? " " + getCyclingPower() : ""); + + (getCyclingPower() != null ? " " + getCyclingPower() : "") + + (getRunningDistanceSpeedCadence() != null ? " " + getRunningDistanceSpeedCadence() : ""); } private void set(@NonNull SensorData type, SensorData data) { @@ -110,6 +127,11 @@ private void set(@NonNull SensorData type, SensorData data) { return; } + if (type instanceof SensorDataRunning) { + this.runningDistanceSpeedCadence = (SensorDataRunning) data; + return; + } + throw new UnsupportedOperationException(type.getClass().getCanonicalName()); } } diff --git a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java index fa44df1ef8..a8f93ea4af 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java @@ -34,13 +34,14 @@ import de.dennisguse.opentracks.content.sensor.SensorDataCycling; import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower; import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate; +import de.dennisguse.opentracks.content.sensor.SensorDataRunning; import de.dennisguse.opentracks.util.BluetoothUtils; /** * Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications. * Also parses the transferred data into {@link SensorDataObserver}. */ -public abstract class BluetoothConnectionManager { +public abstract class BluetoothConnectionManager { private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); @@ -103,7 +104,7 @@ public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) { public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) { Log.d(TAG, "Received data from " + gatt.getDevice().getAddress()); - SensorData sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic); + SensorData sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic); if (sensorData != null) { Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData); observer.onChanged(sensorData); @@ -148,14 +149,14 @@ synchronized boolean isSameBluetoothDevice(String address) { return address.equals(bluetoothGatt.getDevice().getAddress()); } - protected abstract SensorData createEmptySensorData(String address); + protected abstract SensorData createEmptySensorData(String address); /** * @return null if data could not be parsed. */ - protected abstract SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic); + protected abstract SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic); - public static class HeartRate extends BluetoothConnectionManager { + public static class HeartRate extends BluetoothConnectionManager { HeartRate(@NonNull SensorDataObserver observer) { super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer); @@ -174,7 +175,7 @@ protected SensorDataHeartRate parsePayload(String sensorName, String address, Bl } } - public static class CyclingCadence extends BluetoothConnectionManager { + public static class CyclingCadence extends BluetoothConnectionManager { CyclingCadence(SensorDataObserver observer) { super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer); @@ -205,7 +206,7 @@ protected SensorDataCycling.Cadence parsePayload(String sensorName, String addre } } - public static class CyclingDistanceSpeed extends BluetoothConnectionManager { + public static class CyclingDistanceSpeed extends BluetoothConnectionManager { CyclingDistanceSpeed(SensorDataObserver observer) { super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer); @@ -232,7 +233,7 @@ protected SensorDataCycling.DistanceSpeed parsePayload(String sensorName, String } } - public static class CyclingPower extends BluetoothConnectionManager { + public static class CyclingPower extends BluetoothConnectionManager { CyclingPower(@NonNull SensorDataObserver observer) { super(BluetoothUtils.CYCLING_POWER_UUID, BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, observer); @@ -251,6 +252,23 @@ protected SensorDataCyclingPower parsePayload(String sensorName, String address, } } + public static class RunningSpeedAndCadence extends BluetoothConnectionManager { + + RunningSpeedAndCadence(@NonNull SensorDataObserver observer) { + super(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_UUID, BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, observer); + } + + @Override + protected SensorDataRunning createEmptySensorData(String address) { + return new SensorDataRunning(address); + } + + @Override + protected SensorDataRunning parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) { + return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic); + } + } + interface SensorDataObserver { void onChanged(SensorData sensorData); diff --git a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java index ae9d17c41e..182b2fa4e7 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java @@ -31,6 +31,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.sensor.SensorData; import de.dennisguse.opentracks.content.sensor.SensorDataCycling; +import de.dennisguse.opentracks.content.sensor.SensorDataRunning; import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.util.BluetoothUtils; import de.dennisguse.opentracks.util.PreferencesUtils; @@ -67,6 +68,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager. private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this); private final BluetoothConnectionManager.CyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingDistanceSpeed(this); private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this); + private final BluetoothConnectionManager.RunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionManager.RunningSpeedAndCadence(this); private final SensorDataSet sensorDataSet = new SensorDataSet(); @@ -85,11 +87,14 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin connect(cyclingCadence, address); } - if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) { + if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_speed_key, key)) { String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress(sharedPreferences, context); connect(cyclingSpeed, address); } + if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, key)) { + preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(sharedPreferences, context); + } if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) { String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress(sharedPreferences, context); @@ -97,8 +102,11 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin connect(cyclingPower, address); } - if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, key)) { - preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(sharedPreferences, context); + + if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_running_speed_and_cadence_key, key)) { + String address = PreferencesUtils.getBluetoothRunningSpeedAndCadenceAddress(sharedPreferences, context); + + connect(runningSpeedAndCadence, address); } } }; @@ -122,6 +130,7 @@ public synchronized void stop() { cyclingCadence.disconnect(); cyclingSpeed.disconnect(); cyclingPower.disconnect(); + runningSpeedAndCadence.disconnect(); sensorDataSet.clear(); @@ -133,7 +142,7 @@ public boolean isEnabled() { return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); } - private synchronized void connect(BluetoothConnectionManager connectionManager, String address) { + private synchronized void connect(BluetoothConnectionManager connectionManager, String address) { if (!isEnabled()) { Log.w(TAG, "Bluetooth not enabled."); return; @@ -186,11 +195,20 @@ public synchronized void onChanged(SensorData sensorData) { SensorDataCycling.DistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed(); Log.d(TAG, "Previous: " + previous + "; Current" + sensorData); if (sensorData.equals(previous)) { - Log.d(TAG, "onChanged: speed data repeated."); + Log.d(TAG, "onChanged: cycling speed data repeated."); return; } ((SensorDataCycling.DistanceSpeed) sensorData).compute(previous, preferenceWheelCircumference); } + if (sensorData instanceof SensorDataRunning) { + SensorDataRunning previous = sensorDataSet.getRunningDistanceSpeedCadence(); + Log.d(TAG, "Previous: " + previous + "; Current" + sensorData); + if (sensorData.equals(previous)) { + Log.d(TAG, "onChanged: running speed data repeated."); + return; + } + ((SensorDataRunning) sensorData).compute(previous); + } sensorDataSet.set(sensorData); } diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java index 9a2d49788b..8911f1929c 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java @@ -31,6 +31,7 @@ import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference; +import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeRunningSpeedAndCadencePreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference; import de.dennisguse.opentracks.util.ActivityUtils; import de.dennisguse.opentracks.util.BluetoothUtils; @@ -252,6 +253,8 @@ public void onDisplayPreferenceDialog(Preference preference) { dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID); } else if (preference instanceof BluetoothLeCyclingPowerPreference) { dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_POWER_UUID); + } else if (preference instanceof BluetoothLeRunningSpeedAndCadencePreference) { + dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_UUID); } if (dialogFragment != null) { diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java new file mode 100644 index 0000000000..055fcb29f1 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java @@ -0,0 +1,23 @@ +package de.dennisguse.opentracks.settings.bluetooth; + +import android.content.Context; +import android.util.AttributeSet; + +public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference { + + public BluetoothLeRunningSpeedAndCadencePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public BluetoothLeRunningSpeedAndCadencePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public BluetoothLeRunningSpeedAndCadencePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BluetoothLeRunningSpeedAndCadencePreference(Context context) { + super(context); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java b/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java index 730c6c9653..4a9b72687e 100644 --- a/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java @@ -28,7 +28,10 @@ import java.util.List; import java.util.UUID; +import de.dennisguse.opentracks.content.data.Distance; +import de.dennisguse.opentracks.content.data.Speed; import de.dennisguse.opentracks.content.sensor.SensorDataCycling; +import de.dennisguse.opentracks.content.sensor.SensorDataRunning; /** * Utilities for dealing with bluetooth devices. @@ -54,6 +57,9 @@ public class BluetoothUtils { public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL); public static final UUID CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL); + public static final UUID RUNNING_RUNNING_SPEED_CADENCE_UUID = new UUID(0x181400001000L, 0x800000805f9b34fbL); + public static final UUID RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID = new UUID(0x2A5300001000L, 0x800000805f9b34fbL); + private static final String TAG = BluetoothUtils.class.getSimpleName(); private BluetoothUtils() { @@ -136,4 +142,43 @@ public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed); } + + public static SensorDataRunning parseRunningSpeedAndCadence(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) { + // DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml + int valueLength = characteristic.getValue().length; + if (valueLength == 0) { + return null; + } + + int flags = characteristic.getValue()[0]; + boolean hasStrideLength = (flags & 0x01) > 0; + boolean hasTotalDistance = (flags & 0x02) > 0; + boolean hasStatus = (flags & 0x03) > 0; // walking vs running + + Speed speed = null; + Float cadence = null; + Distance totalDistance = null; + + int index = 1; + if (valueLength - index >= 2) { + speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f); + } + + index = 3; + if (valueLength - index >= 1) { + cadence = Float.valueOf(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index)); + } + + index = 4; + if (hasStrideLength && valueLength - index >= 2) { + Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index)); + index += 2; + } + + if (hasTotalDistance && valueLength - index >= 2) { + totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index)); + } + + return new SensorDataRunning(address, sensorName, speed, cadence, totalDistance); + } } diff --git a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java index ec022bd247..365a838a75 100644 --- a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java @@ -177,6 +177,10 @@ public static String getBluetoothCyclingPowerSensorAddress(SharedPreferences sha return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_power_key, getBluetoothSensorAddressNone(context)); } + public static String getBluetoothRunningSpeedAndCadenceAddress(SharedPreferences sharedPreferences, Context context) { + return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_running_speed_and_cadence_key, getBluetoothSensorAddressNone(context)); + } + public static boolean shouldShowStatsOnLockscreen(SharedPreferences sharedPreferences, Context context) { final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default); return getBoolean(sharedPreferences, context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT); diff --git a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java index 8fa1ea0482..427e72d92a 100644 --- a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java +++ b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java @@ -42,8 +42,6 @@ public class UnitConversions { // multiplication factor to convert kilometers to miles public static final double KM_TO_MI = 0.621371192; - public static final double MM_TO_M = 0.001; - // Distance //TODO Make private to Distance class! // multiplication factor to convert miles to feet private static final double MI_TO_FT = 5280.0; diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index f5c30914d2..4e130b4b3a 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -22,6 +22,7 @@ bluetoothCyclingCadenceSensor bluetoothCyclingSpeedSensor bluetoothCyclingPowerSensor + bluetoothRunningSpeedAndCadenceSensor NONE bluetoothCyclingSpeedWheelCircumference diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 1aa6e3b314..98459e458d 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -369,6 +369,7 @@ limitations under the License. Cycling: distance and speed Cycling: cadence + Running: distance, speed, and cadence Wheel circumference (mm) diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml index 8bc6938f15..99d1a208a1 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -138,6 +138,10 @@ limitations under the License. android:defaultValue="@string/sensor_type_value_none" android:key="@string/settings_sensor_bluetooth_cycling_power_key" android:title="@string/sensor_state_power" /> +