From 4893a2b7432b87e11ef2de63c4782121a52ab160 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sat, 25 Apr 2020 19:59:18 +0200 Subject: [PATCH] Added cycling speed sensor. Fixed speed and cadence computation. Fixed that a sensor change in settings resulted in a disconnect for all connected sensors. --- .../content/sensor/SensorDataCyclingTest.java | 39 ++++++++++++------- .../content/sensor/SensorDataCycling.java | 35 +++++++---------- .../content/sensor/SensorDataSet.java | 4 +- .../opentracks/fragments/StatsFragment.java | 30 ++++++++++---- .../sensors/BluetoothRemoteSensorManager.java | 22 +++++------ .../opentracks/util/PreferencesUtils.java | 6 ++- .../opentracks/util/UnitConversions.java | 2 + src/main/res/values/settings.xml | 15 +++++++ src/main/res/values/strings.xml | 3 ++ src/main/res/xml/settings.xml | 17 +++++--- 10 files changed, 111 insertions(+), 62 deletions(-) diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java index b80b990a7..ca8f7959d 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java @@ -14,21 +14,21 @@ public class SensorDataCyclingTest { @Test public void compute_cadence() { // given - SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, 1024); // 1s - SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(2, 2048); // 2s + SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 1024); // 1s + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 2048); // 2s // when current.compute(previous); // then - Assert.assertEquals(57.2519, current.getCadence_rpm(), 0.01); + Assert.assertEquals(60, current.getCadence_rpm(), 0.01); } @Test public void compute_cadence_sameCount() { // given - SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, 1024); - SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(1, 2048); + SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 2048); // when current.compute(previous); @@ -41,8 +41,8 @@ public class SensorDataCyclingTest { @Test public void compute_cadence_sameTime() { // given - SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, 1024); - SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(2, 1024); + SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 1024); // when current.compute(previous); @@ -54,26 +54,39 @@ public class SensorDataCyclingTest { @Test public void compute_cadence_rollOverTime() { // given - SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, UintUtils.UINT16_MAX - 1024); - SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(2, 0); + SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 0); // when current.compute(previous); // then - Assert.assertEquals(57.2519, current.getCadence_rpm(), 0.01); + Assert.assertEquals(60, current.getCadence_rpm(), 0.01); } @Test public void compute_cadence_rollOverCount() { // given - SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(UintUtils.UINT32_MAX - 1, 1024); - SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(0, 2048); + SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 0, 2048); // when current.compute(previous); // then - Assert.assertEquals(57.2519, current.getCadence_rpm(), 0.01); + Assert.assertEquals(60, current.getCadence_rpm(), 0.01); + } + + @Test + public void compute_speed_rollOverCount() { + // given + SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024); + SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048); + + // when + current.compute(previous, 2000); + + // then + Assert.assertEquals(2, current.getSpeed_mps(), 0.01); } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java index 74dd70dd9..231d0285e 100644 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java @@ -4,7 +4,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import de.dennisguse.opentracks.util.UintUtils; import de.dennisguse.opentracks.util.UnitConversions; @@ -43,13 +42,6 @@ public final class SensorDataCycling { this(speed.getSensorAddress(), speed.getSensorName(), speed.getWheelRevolutionsCount(), speed.getWheelRevolutionsTime()); } - @VisibleForTesting - public Cadence(long crankRevolutionsCount, int crankRevolutionsTime) { - super("sensorAddress", "sensorName"); - this.crankRevolutionsCount = crankRevolutionsCount; - this.crankRevolutionsTime = crankRevolutionsTime; - } - public boolean hasData() { return crankRevolutionsCount != INVALID_VALUE_INT && crankRevolutionsTime != INVALID_VALUE_INT; } @@ -72,8 +64,7 @@ public final class SensorDataCycling { public void compute(Cadence previous) { if (hasData() && previous != null && previous.hasData()) { - Log.e(TAG, previous.getCrankRevolutionsTime() + " " + previous.getCrankRevolutionsCount() + " - " + this.getCrankRevolutionsTime() + " " + getCrankRevolutionsCount()); //TODO REMOVE - long timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) * 1024 / UnitConversions.S_TO_MS; + long timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024 * UnitConversions.S_TO_MS; if (timeDiff_ms <= 0) { Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); cadence_rpm = INVALID_VALUE_FLOAT; @@ -100,12 +91,11 @@ public final class SensorDataCycling { } } - //TODO Speed computation; needs wheel diameter / circumference public static class Speed extends SensorData { private int wheelRevolutionsCount; // UINT16 private int wheelRevolutionsTime; // UINT16; 1/1024s - private float speed_ms = INVALID_VALUE_FLOAT; + private float speed_mps = INVALID_VALUE_FLOAT; public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) { super(sensorAddress, sensorName); @@ -125,23 +115,24 @@ public final class SensorDataCycling { return wheelRevolutionsTime; } - public boolean hasSpeed() { - return !Float.isNaN(speed_ms); + public boolean hasSpeed_mps() { + return !Float.isNaN(speed_mps); } - public float getSpeed_ms() { - return speed_ms; + public float getSpeed_mps() { + return speed_mps; } - public void compute(Speed previous) { + public void compute(Speed previous, int wheel_circumference_mm) { if (hasData() && previous != null && previous.hasData()) { - long timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) * 1024 / UnitConversions.S_TO_MS; + long timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024 * UnitConversions.S_TO_MS; if (timeDiff_ms <= 0) { Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); - speed_ms = INVALID_VALUE_FLOAT; + speed_mps = INVALID_VALUE_FLOAT; } else { - long crankDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX); - speed_ms = crankDiff / (float) timeDiff_ms; + long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX); + double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S; + speed_mps = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s); } } } @@ -149,7 +140,7 @@ public final class SensorDataCycling { @NonNull @Override public String toString() { - return "speed=" + getSpeed_ms() + "_" + getWheelRevolutionsTime(); + return "speed=" + getSpeed_mps() + "_" + getWheelRevolutionsTime(); } @Override 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 ac0d11afa..0d05c9c9d 100644 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java @@ -71,8 +71,8 @@ public final class SensorDataSet { trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm()); } - if (cyclingSpeed != null && cyclingSpeed.hasSpeed()) { - trackPoint.setSpeed(cyclingSpeed.getSpeed_ms()); + if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) { + trackPoint.setSpeed(cyclingSpeed.getSpeed_mps()); } } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java index 9b1d49db3..6d1110caa 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java @@ -449,10 +449,10 @@ public class StatsFragment extends Fragment implements TrackDataListener { setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording()); setCadenceSensorData(sensorDataSet, isSelectedTrackRecording()); + setSpeedSensorData(sensorDataSet, isSelectedTrackRecording()); } private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) { - // heart rate int isVisible = View.VISIBLE; if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressNone(getContext())) { isVisible = View.INVISIBLE; @@ -475,7 +475,6 @@ public class StatsFragment extends Fragment implements TrackDataListener { } private void setCadenceSensorData(SensorDataSet sensorDataSet, boolean isRecording) { - // heart rate int isVisible = View.VISIBLE; if (!isRecording || PreferencesUtils.isBluetoothCyclingCadenceSensorAddressNone(getContext())) { isVisible = View.INVISIBLE; @@ -497,6 +496,16 @@ public class StatsFragment extends Fragment implements TrackDataListener { } } + private void setSpeedSensorData(SensorDataSet sensorDataSet, boolean isRecording) { + if (isRecording) { + if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) { + if (sensorDataSet.getCyclingSpeed().hasSpeed_mps() && sensorDataSet.getCyclingSpeed().isRecent()) { + setSpeed(sensorDataSet.getCyclingSpeed().getSpeed_mps()); + } + } + } + } + private void updateStats() { String trackIconValue = TrackIconUtils.getIconValue(getContext(), category); @@ -600,12 +609,8 @@ public class StatsFragment extends Fragment implements TrackDataListener { // Set speed/pace speedContainer.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE); if (isRecording) { - speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace); - double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN; - Pair parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed); - speedValue.setText(parts.first); - speedUnit.setText(parts.second); + setSpeed(speed); } // Set elevation @@ -635,4 +640,15 @@ public class StatsFragment extends Fragment implements TrackDataListener { longitudeValue.setText(longitudeText); } } + + private void setSpeed(double speed) { + boolean metricUnits = PreferencesUtils.isMetricUnits(getContext()); + boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext()); + + speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace); + + Pair parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed); + speedValue.setText(parts.first); + speedUnit.setText(parts.second); + } } 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 74f06457d..cbffa48f1 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java @@ -56,8 +56,9 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager. private boolean started = false; - private final BluetoothConnectionManager heartRate = new BluetoothConnectionManager.HeartRate(this); - private final BluetoothConnectionManager cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this); + private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this); + private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this); + private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this); private final SensorDataSet sensorDataSet = new SensorDataSet(); @@ -67,23 +68,20 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager. 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); 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); - connect(cyclingCadence, address); } + + if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) { + String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress(context); + + connect(cyclingSpeed, address); + } } }; @@ -163,7 +161,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager. Log.d(TAG, "onChanged: speed data repeated."); return; } - ((SensorDataCycling.Speed) sensorData).compute(sensorDataSet.getCyclingSpeed()); + ((SensorDataCycling.Speed) sensorData).compute(sensorDataSet.getCyclingSpeed(), PreferencesUtils.getWheelCircumference(context)); } sensorDataSet.set(sensorData); diff --git a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java index ed2b45443..4f8c671ce 100644 --- a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java @@ -49,7 +49,7 @@ public class PreferencesUtils { @Deprecated //NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly. - //NOTE: This was also used to recover from a reboot, but this data should not be exposed to the whole application. + //NOTE: This was also used to recover from a service restart, but this data should not be exposed to the whole application. public static final long RECORDING_TRACK_ID_DEFAULT = -1L; public static long getRecordingTrackId(Context context) { @@ -250,6 +250,10 @@ public class PreferencesUtils { return getString(context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context)); } + public static int getWheelCircumference(Context context) { + final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default)); + return getInt(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT); + } public static boolean shouldShowStatsOnLockscreen(Context context) { final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default); diff --git a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java index a3d9436b5..051ec63d3 100644 --- a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java +++ b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java @@ -46,6 +46,8 @@ 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 // multiplication factor to convert miles to feet public 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 aa27838a6..8ec4230e1 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -20,6 +20,21 @@ bluetoothCyclingSpeedSensor NONE + bluetoothCyclingSpeedWheelCircumference + 2135 + + 2100 + 2125 + @string/settings_sensor_bluetooth_cycling_speed_wheel_circumference_default + 2150 + + + 20-622 + 23-622 + 25-622 + 28-622 + + defaultActivity @string/activity_type_unknown diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d97edbf73..e3d67e16c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -397,6 +397,9 @@ limitations under the License. Connected to %1$s Connected to %1$s Disconnected from %1$s + + Wheel size (ETRTO) + Latitude/Longitude Preferred rate diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml index c1ee6185f..48486a545 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -54,8 +54,8 @@ limitations under the License. - - - - + +