From bbe182cf82f95b620c2612b45aecc48f5d0727dc Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Fri, 17 Apr 2020 22:48:12 +0200 Subject: [PATCH 1/4] Removed handler between BluetoothConnectionManager and -RemoteSensorManager. Toast messages should rather be sent from activities. --- .../opentracks/fragments/StatsFragment.java | 2 +- .../sensors/BluetoothConnectionManager.java | 39 +++++++------ .../sensors/BluetoothRemoteSensorManager.java | 57 +++++++------------ 3 files changed, 39 insertions(+), 59 deletions(-) diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java index 647d44390..af45112d8 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java @@ -426,7 +426,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } /** - * Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}. + * Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}. */ private void updateSensorDataUI() { TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); 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 98913593c..bd9b31b3a 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java @@ -24,7 +24,6 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; import android.content.Context; -import android.os.Handler; import android.util.Log; import androidx.annotation.NonNull; @@ -40,19 +39,13 @@ import de.dennisguse.opentracks.util.BluetoothUtils; */ public class BluetoothConnectionManager { - // Message types sent to handler - static final int MESSAGE_CONNECTING = 1; - static final int MESSAGE_CONNECTED = 2; - static final int MESSAGE_READ = 3; - static final int MESSAGE_DISCONNECTED = 4; - 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); private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); private final Context context; - private final Handler handler; + private final EventCallback observer; private SensorState sensorState; @@ -67,14 +60,14 @@ public class BluetoothConnectionManager { Log.d(TAG, "Connecting to sensor: " + gatt.getDevice()); setState(SensorState.CONNECTING); - handler.obtainMessage(MESSAGE_CONNECTING, gatt.getDevice().getName()).sendToTarget(); + observer.connecting(gatt.getDevice().getName()); case BluetoothProfile.STATE_CONNECTED: Log.d(TAG, "Connected to sensor: " + gatt.getDevice()); setState(SensorState.CONNECTED); gatt.discoverServices(); - handler.obtainMessage(MESSAGE_CONNECTED, gatt.getDevice().getName()).sendToTarget(); + observer.connected(gatt.getDevice().getName()); break; case BluetoothProfile.STATE_DISCONNECTING: Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice()); @@ -84,7 +77,7 @@ public class BluetoothConnectionManager { Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice()); setState(SensorState.DISCONNECTED); - handler.obtainMessage(MESSAGE_DISCONNECTED, gatt.getDevice().getName()).sendToTarget(); + observer.disconnected(gatt.getDevice().getName()); } } @@ -115,21 +108,15 @@ public class BluetoothConnectionManager { String deviceName = gatt.getDevice().getName(); Log.d(TAG, "Received heart beat rate " + deviceName + ": " + heartRate); - SensorDataSet sensorDataSet = new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress()); - handler.obtainMessage(MESSAGE_READ, sensorDataSet).sendToTarget(); + observer.onSensorDataReceived(new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress())); } }; - /** - * Constructor. - * - * @param handler a handler for sending messages back to the UI activity - */ - BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, @NonNull Handler handler) { + BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, EventCallback observer) { this.context = context; this.bluetoothDevice = bluetoothDevice; - this.handler = handler; this.sensorState = SensorState.NONE; + this.observer = observer; } public synchronized void connect() { @@ -164,4 +151,16 @@ public class BluetoothConnectionManager { private synchronized void setState(SensorState sensorState) { this.sensorState = sensorState; } + + + interface EventCallback { + + void connecting(String sensorName); + + void connected(String sensorName); + + void onSensorDataReceived(SensorDataSet sensorDataSet); + + void disconnected(String sensorName); + } } 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 1a1779335..702cddda6 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java @@ -20,9 +20,6 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.SharedPreferences; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; import android.util.Log; import android.widget.Toast; @@ -37,7 +34,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils; * * @author Sandor Dornbush */ -public class BluetoothRemoteSensorManager { +public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.EventCallback { public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5000; @@ -49,39 +46,6 @@ public class BluetoothRemoteSensorManager { private final SharedPreferences sharedPreferences; - // Handler that gets information back from the bluetoothConnectionManager - private final Handler messageHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message message) { - String toastMessage; - switch (message.what) { - case BluetoothConnectionManager.MESSAGE_CONNECTING: - //Ignore for now. - toastMessage = context.getString(R.string.settings_sensor_connecting, message.obj); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - break; - case BluetoothConnectionManager.MESSAGE_CONNECTED: - toastMessage = context.getString(R.string.settings_sensor_connected, message.obj); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - break; - case BluetoothConnectionManager.MESSAGE_READ: - if (!(message.obj instanceof SensorDataSet)) { - Log.e(TAG, "Received message did not contain a SensorDataSet."); - sensorDataSet = null; - } else { - sensorDataSet = (SensorDataSet) message.obj; - } - break; - case BluetoothConnectionManager.MESSAGE_DISCONNECTED: - toastMessage = context.getString(R.string.settings_sensor_disconnected, message.obj); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - break; - default: - Log.e(TAG, "Got an undefined case. Please check."); - break; - } - } - }; private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { @@ -166,7 +130,7 @@ public class BluetoothRemoteSensorManager { disconnect(); - bluetoothConnectionManager = new BluetoothConnectionManager(context, device, messageHandler); + bluetoothConnectionManager = new BluetoothConnectionManager(context, device, this); bluetoothConnectionManager.connect(); } @@ -176,4 +140,21 @@ public class BluetoothRemoteSensorManager { bluetoothConnectionManager = null; } } + + @Override + public void connecting(String sensorName) { + } + + @Override + public void connected(String sensorName) { + } + + @Override + public void onSensorDataReceived(SensorDataSet sensorDataSet) { + this.sensorDataSet = sensorDataSet; + } + + @Override + public void disconnected(String sensorName) { + } } From ddb17f61cb93a244876538b00903af2d352d3d5e Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Fri, 17 Apr 2020 07:33:00 +0200 Subject: [PATCH 2/4] Bluetooth LE: implemented cycling cadence and speed sensors. Cadence is working while speed still requires the wheel size. --- .../dennisguse/opentracks/TrackStubUtils.java | 4 +- .../content/sensor/SensorDataCyclingTest.java | 79 ++++++++ .../fragments/ChartFragmentTest.java | 9 +- .../TrackRecordingServiceTestLocation.java | 9 +- .../opentracks/util/BluetoothUtilsTest.java | 76 ++++++++ .../opentracks/chart/ChartPoint.java | 10 +- .../opentracks/content/data/TrackPoint.java | 12 +- .../content/data/TrackPointSensorDataSet.java | 99 ++++++++++ .../provider/ContentProviderUtils.java | 27 ++- .../opentracks/content/sensor/SensorData.java | 39 ++++ .../content/sensor/SensorDataCycling.java | 184 ++++++++++++++++++ .../content/sensor/SensorDataHeartRate.java | 27 +++ .../content/sensor/SensorDataSet.java | 135 +++++++------ .../content/sensor/SensorState.java | 29 --- .../opentracks/fragments/StatsFragment.java | 61 ++++-- .../io/file/exporter/KmlTrackWriter.java | 23 ++- .../file/importer/KmlFileTrackImporter.java | 18 +- .../services/TrackRecordingService.java | 17 +- .../TrackRecordingServiceInterface.java | 14 +- .../sensors/BluetoothConnectionManager.java | 142 +++++++++----- .../sensors/BluetoothRemoteSensorManager.java | 145 +++++++------- .../opentracks/settings/SettingsActivity.java | 11 +- ...othLeCyclingCadenceAndSpeedPreference.java | 23 +++ .../BluetoothLeHeartRatePreference.java | 23 +++ .../BluetoothLeSensorPreference.java} | 42 ++-- .../opentracks/util/BluetoothUtils.java | 66 ++++++- .../opentracks/util/PreferencesUtils.java | 32 ++- .../dennisguse/opentracks/util/UintUtils.java | 30 +++ src/main/res/layout/stats.xml | 38 +++- src/main/res/values/do_not_translate.xml | 1 + src/main/res/values/settings.xml | 2 + src/main/res/xml/settings.xml | 10 +- 32 files changed, 1099 insertions(+), 338 deletions(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java create mode 100644 src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java create mode 100644 src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java create mode 100644 src/main/java/de/dennisguse/opentracks/content/sensor/SensorData.java create mode 100644 src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java create mode 100644 src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataHeartRate.java delete mode 100644 src/main/java/de/dennisguse/opentracks/content/sensor/SensorState.java create mode 100644 src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadenceAndSpeedPreference.java create mode 100644 src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java rename src/main/java/de/dennisguse/opentracks/settings/{BluetoothLePreference.java => bluetooth/BluetoothLeSensorPreference.java} (78%) create mode 100644 src/main/java/de/dennisguse/opentracks/util/UintUtils.java diff --git a/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java b/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java index 0a6420038..e258bce30 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java +++ b/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java @@ -19,7 +19,7 @@ package de.dennisguse.opentracks; import android.location.Location; import de.dennisguse.opentracks.content.data.TrackPoint; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; /** * Commons utilities for creating stubs of track, location. @@ -63,6 +63,6 @@ public class TrackStubUtils { location.setTime(INITIAL_TIME); location.setBearing(INITIAL_BEARING); - return new TrackPoint(location, new SensorDataSet(Float.NaN, Float.NaN)); + return new TrackPoint(location, new TrackPointSensorDataSet()); } } diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java new file mode 100644 index 000000000..b80b990a7 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java @@ -0,0 +1,79 @@ +package de.dennisguse.opentracks.content.sensor; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import de.dennisguse.opentracks.util.UintUtils; + +@RunWith(AndroidJUnit4.class) +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 + + // when + current.compute(previous); + + // then + Assert.assertEquals(57.2519, 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); + + // when + current.compute(previous); + + // then + Assert.assertEquals(0, current.getCadence_rpm(), 0.01); + } + + + @Test + public void compute_cadence_sameTime() { + // given + SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(2, 1024); + + // when + current.compute(previous); + + // then + Assert.assertFalse(current.hasCadence_rpm()); + } + + @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); + + // when + current.compute(previous); + + // then + Assert.assertEquals(57.2519, 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); + + // when + current.compute(previous); + + // then + Assert.assertEquals(57.2519, current.getCadence_rpm(), 0.01); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java index 22c45f79e..acddf2688 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java @@ -33,7 +33,7 @@ import de.dennisguse.opentracks.TrackStubUtils; import de.dennisguse.opentracks.chart.ChartPoint; import de.dennisguse.opentracks.chart.ChartView; import de.dennisguse.opentracks.content.data.TrackPoint; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; import de.dennisguse.opentracks.util.UnitConversions; /** @@ -82,7 +82,7 @@ public class ChartFragmentTest { // Input incorrect state. // Creates SensorData. - SensorDataSet sensorDataSet = new SensorDataSet(SensorDataSet.DATA_UNAVAILABLE, SensorDataSet.DATA_UNAVAILABLE); + TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); trackPoint.setSensorDataSet(sensorDataSet); // Test. point = chartFragment.createPendingPoint(trackPoint); @@ -104,7 +104,10 @@ public class ChartFragmentTest { Assert.assertEquals(Float.NaN, point.getPower(), 0.01); // Creates SensorData. - SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102); + TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); + sensorDataSet.setHeartRate_bpm(100f); + sensorDataSet.setCyclingCadence(101); + sensorDataSet.setPower(102f); // Creates SensorDataSet. trackPoint.setSensorDataSet(sensorDataSet); diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java index eaeda2d6c..81a64c416 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java @@ -244,13 +244,8 @@ public class TrackRecordingServiceTestLocation { } @Override - public boolean isSensorDataSetValid() { - return true; - } - - @Override - public SensorDataSet getSensorDataSet() { - return new SensorDataSet(1, 2); + public SensorDataSet getSensorData() { + return new SensorDataSet(); } }); diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java new file mode 100644 index 000000000..2ff661487 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/util/BluetoothUtilsTest.java @@ -0,0 +1,76 @@ +package de.dennisguse.opentracks.util; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.junit.Assert; +import org.junit.Test; + +import de.dennisguse.opentracks.content.sensor.SensorDataCycling; + +public class BluetoothUtilsTest { + + @Test + public void parseHeartRate_uint8() { + // given + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0); + characteristic.setValue(new byte[]{0x02, 0x3C}); + + // when + int heartRate = BluetoothUtils.parseHeartRate(characteristic); + + // then + Assert.assertEquals(60, heartRate); + } + + @Test + public void parseHeartRate_uint16() { + // given + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0); + characteristic.setValue(new byte[]{0x01, 0x01, 0x01}); + + // when + int heartRate = BluetoothUtils.parseHeartRate(characteristic); + + // then + Assert.assertEquals(257, heartRate); + } + + @Test + public void parseCyclingSpeedCadence_crankOnly() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0); + characteristic.setValue(new byte[]{0x01, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99}); + + // when + SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount()); + Assert.assertNull(sensor.getSpeed()); + } + + @Test + public void parseCyclingSpeedCadence_wheelOnly() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0); + characteristic.setValue(new byte[]{0x02, (byte) 0xE1, 0x00, 0x45, (byte) 0x99}); + + // when + SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + Assert.assertNull(sensor.getCadence()); + Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount()); + } + + @Test + public void parseCyclingSpeedCadence_crankWheel() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0); + characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99}); + + // when + SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount()); + Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount()); + } +} \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java index 940769378..262ad6231 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java @@ -3,7 +3,7 @@ package de.dennisguse.opentracks.chart; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.util.UnitConversions; @@ -25,7 +25,7 @@ public class ChartPoint { this.elevation = elevation; } - public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, SensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) { + public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPointSensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) { TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics(); if (chartByDistance) { @@ -50,10 +50,10 @@ public class ChartPoint { pace = speed == 0 ? 0.0 : 60.0 / speed; if (sensorDataSet != null) { if (sensorDataSet.hasHeartRate()) { - heartRate = sensorDataSet.getHeartRate(); + heartRate = sensorDataSet.getHeartRate_bpm(); } - if (sensorDataSet.hasCadence()) { - cadence = sensorDataSet.getCadence(); + if (sensorDataSet.hasCyclingCadence()) { + cadence = sensorDataSet.getCyclingCadence_rpm(); } if (sensorDataSet.hasPower()) { power = sensorDataSet.getPower(); diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java index a3f230bb8..992260471 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java @@ -20,8 +20,6 @@ import android.location.LocationManager; import androidx.annotation.NonNull; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; - /** * This class extends the standard Android location with extra information. * @@ -39,17 +37,17 @@ public class TrackPoint { this.location = location; } - public TrackPoint(@NonNull Location location, SensorDataSet sensorDataSet) { + public TrackPoint(@NonNull Location location, TrackPointSensorDataSet sensorDataSet) { this.location = location; this.sensorDataSet = sensorDataSet; } - public TrackPoint(@NonNull TrackPoint trackPoint, SensorDataSet sensorDataSet) { + public TrackPoint(@NonNull TrackPoint trackPoint, TrackPointSensorDataSet sensorDataSet) { this.location = trackPoint.getLocation(); this.sensorDataSet = sensorDataSet; } - private SensorDataSet sensorDataSet = null; + private TrackPointSensorDataSet sensorDataSet = null; public TrackPoint(double latitude, double longitude, Double altitude, long time) { location = new Location(LocationManager.GPS_PROVIDER); @@ -90,11 +88,11 @@ public class TrackPoint { return location; } - public SensorDataSet getSensorDataSet() { + public TrackPointSensorDataSet getSensorDataSet() { return sensorDataSet; } - public void setSensorDataSet(SensorDataSet sensorDataSet) { + public void setSensorDataSet(TrackPointSensorDataSet sensorDataSet) { this.sensorDataSet = sensorDataSet; } diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java new file mode 100644 index 000000000..e16118b9d --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java @@ -0,0 +1,99 @@ +package de.dennisguse.opentracks.content.data; + +import androidx.annotation.NonNull; + +//TODO Rename. +public final class TrackPointSensorDataSet { + private long time_ms; + + private Float heartRate_bpm = null; + private Float cyclingCadence = null; + private Float cyclingSpeed = null; + private Float power = null; + + public TrackPointSensorDataSet() { + this(System.currentTimeMillis()); + } + + public TrackPointSensorDataSet(long time_ms) { + this.time_ms = time_ms; + } + + public boolean hasHeartRate() { + return heartRate_bpm != null && heartRate_bpm > 0; + } + + public float getHeartRate_bpm() { + return heartRate_bpm; + } + + public void setHeartRate_bpm(Float heartRate_bpm) { + this.heartRate_bpm = heartRate_bpm; + } + + public boolean hasCyclingCadence() { + return cyclingCadence != null; + } + + public Float getCyclingCadence_rpm() { + return cyclingCadence; + } + + public void setCyclingCadence(Float cyclingCadence) { + this.cyclingCadence = cyclingCadence; + } + + public void setCyclingCadence(Integer cyclingCadence) { + this.cyclingCadence = cyclingCadence != null ? cyclingCadence.floatValue() : null; + } + + public boolean hasCyclingSpeed() { + return cyclingSpeed != null; + } + + public Float getCyclingSpeed() { + return cyclingSpeed; + } + + public void setCyclingSpeed(Float cyclingSpeed) { + this.cyclingSpeed = cyclingSpeed; + } + + public boolean hasPower() { + return power != null; + } + + public Float getPower() { + return power; + } + + public void setPower(Float power) { + this.power = power; + } + + public long getTime() { + return time_ms; + } + + public void setTime(long time_ms) { + this.time_ms = time_ms; + } + + /** + * Is the data recent considering the current time. + * + * @param maxAge the maximal age in milliseconds. + */ + public boolean isRecent(long maxAge) { + return time_ms + maxAge > System.currentTimeMillis(); + } + + @NonNull + @Override + public String toString() { + return "time=" + getTime() + + (hasHeartRate() ? " heart=" + getHeartRate_bpm() : "") + + (hasCyclingCadence() ? " cyclingCad=" + getCyclingCadence_rpm() : "") + + (hasCyclingSpeed() ? " cyclingSpeed=" + getCyclingSpeed() : ""); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index 409b3cab2..b429be852 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -36,11 +36,11 @@ import de.dennisguse.opentracks.android.ContentResolverWrapper; import de.dennisguse.opentracks.android.IContentResolver; 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.TrackPointsColumns; import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.content.data.WaypointsColumns; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.util.FileUtils; @@ -621,12 +621,19 @@ public class ContentProviderUtils { trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex)); } - float heartRate = cursor.isNull(indexes.sensorHeartRateIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorHeartRateIndex); - float cadence = cursor.isNull(indexes.sensorCadenceIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorCadenceIndex); - float power = cursor.isNull(indexes.sensorPowerIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorPowerIndex); + TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); - trackPoint.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime())); + if (cursor.isNull(indexes.sensorHeartRateIndex)) { + sensorDataSet.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex)); + } + if (cursor.isNull(indexes.sensorCadenceIndex)) { + sensorDataSet.setCyclingCadence(cursor.getFloat(indexes.sensorCadenceIndex)); + } + if (cursor.isNull(indexes.sensorPowerIndex)) { + sensorDataSet.setCyclingSpeed(cursor.getFloat(indexes.sensorPowerIndex)); + } + trackPoint.setSensorDataSet(sensorDataSet); return trackPoint; } @@ -806,15 +813,15 @@ public class ContentProviderUtils { } //SensorData - SensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); + TrackPointSensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); if (sensorDataSet != null && sensorDataSet.hasHeartRate()) { - values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getSensorDataSet().getHeartRate()); + values.put(TrackPointsColumns.SENSOR_HEARTRATE, sensorDataSet.getHeartRate_bpm()); } - if (sensorDataSet != null && sensorDataSet.hasCadence()) { - values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getSensorDataSet().getCadence()); + if (sensorDataSet != null && sensorDataSet.hasCyclingCadence()) { + values.put(TrackPointsColumns.SENSOR_CADENCE, sensorDataSet.getCyclingCadence_rpm()); } if (sensorDataSet != null && sensorDataSet.hasPower()) { - values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getSensorDataSet().getPower()); + values.put(TrackPointsColumns.SENSOR_POWER, sensorDataSet.getPower()); } return values; } diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorData.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorData.java new file mode 100644 index 000000000..b073df96e --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorData.java @@ -0,0 +1,39 @@ +package de.dennisguse.opentracks.content.sensor; + +import androidx.annotation.VisibleForTesting; + +import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; + +public class SensorData { + + private String sensorAddress; + private String sensorName; + + private long timestamp_ms; + + SensorData(String sensorAddress, String sensorName) { + this(sensorAddress, sensorName, System.currentTimeMillis()); + } + + @VisibleForTesting + SensorData(String sensorAddress, String sensorName, long timestamp_ms) { + this.sensorAddress = sensorAddress; + this.sensorName = sensorName; + this.timestamp_ms = timestamp_ms; + } + + public String getSensorAddress() { + return sensorAddress; + } + + public String getSensorName() { + return sensorName; + } + + /** + * Is the data recent considering the current time. + */ + public boolean isRecent() { + return timestamp_ms + BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS > System.currentTimeMillis(); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java new file mode 100644 index 000000000..74dd70dd9 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java @@ -0,0 +1,184 @@ +package de.dennisguse.opentracks.content.sensor; + +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; + +/** + * Provides cadence in rpm and speed in milliseconds from Bluetooth LE Cycling Cadence and Speed sensors. + *

+ * https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449 + */ +public final class SensorDataCycling { + + private static final String TAG = SensorDataCycling.class.getSimpleName(); + + private static final int INVALID_VALUE_INT = -1; + private static final float INVALID_VALUE_FLOAT = Float.NaN; + + private SensorDataCycling() { + } + + public static class Cadence extends SensorData { + + private long crankRevolutionsCount; // UINT32 + private int crankRevolutionsTime; // UINT16; 1/1024s + private float cadence_rpm = INVALID_VALUE_FLOAT; + + public Cadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) { + super(sensorAddress, sensorName); + this.crankRevolutionsCount = crankRevolutionsCount; + this.crankRevolutionsTime = crankRevolutionsTime; + } + + /** + * Workaround for Wahoo CADENCE: provides speed instead of cadence + */ + public Cadence(@NonNull SensorDataCycling.Speed speed) { + 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; + } + + public long getCrankRevolutionsCount() { + return crankRevolutionsCount; + } + + public int getCrankRevolutionsTime() { + return crankRevolutionsTime; + } + + public boolean hasCadence_rpm() { + return !Float.isNaN(cadence_rpm); + } + + public float getCadence_rpm() { + return cadence_rpm; + } + + 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; + if (timeDiff_ms <= 0) { + Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); + cadence_rpm = INVALID_VALUE_FLOAT; + } else { + long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX); + float cadence_ms = crankDiff / (float) timeDiff_ms; + cadence_rpm = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN); + } + } + } + + @NonNull + @Override + public String toString() { + return "cadence=" + getCadence_rpm() + "_" + getCrankRevolutionsTime(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof Cadence)) return false; + + Cadence comp = (Cadence) obj; + return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime(); + } + } + + //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; + + public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) { + super(sensorAddress, sensorName); + this.wheelRevolutionsCount = wheelRevolutionsCount; + this.wheelRevolutionsTime = wheelRevolutionsTime; + } + + public boolean hasData() { + return wheelRevolutionsCount != INVALID_VALUE_INT && wheelRevolutionsTime != INVALID_VALUE_INT; + } + + public int getWheelRevolutionsCount() { + return wheelRevolutionsCount; + } + + public int getWheelRevolutionsTime() { + return wheelRevolutionsTime; + } + + public boolean hasSpeed() { + return !Float.isNaN(speed_ms); + } + + public float getSpeed_ms() { + return speed_ms; + } + + public void compute(Speed previous) { + if (hasData() && previous != null && previous.hasData()) { + 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; + } else { + long crankDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX); + speed_ms = crankDiff / (float) timeDiff_ms; + } + } + } + + @NonNull + @Override + public String toString() { + return "speed=" + getSpeed_ms() + "_" + getWheelRevolutionsTime(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof Speed)) return false; + + Speed comp = (Speed) obj; + return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime(); + } + } + + public static class CadenceAndSpeed extends SensorData { + + private Cadence cadence; + private Speed speed; + + public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) { + super(sensorAddress, sensorName); + this.cadence = cadence; + this.speed = speed; + } + + public Cadence getCadence() { + return cadence; + } + + public Speed getSpeed() { + return speed; + } + } +} + diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataHeartRate.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataHeartRate.java new file mode 100644 index 000000000..c42c13f39 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataHeartRate.java @@ -0,0 +1,27 @@ +package de.dennisguse.opentracks.content.sensor; + +import androidx.annotation.NonNull; + +public class SensorDataHeartRate extends SensorData { + + private float heartRate_bpm; + + public SensorDataHeartRate(String name, String address, float heartRate_bpm) { + super(name, address); + this.heartRate_bpm = heartRate_bpm; + } + + public boolean hasHeartRate_bpm() { + return !Float.isNaN(heartRate_bpm); + } + + public float getHeartRate_bpm() { + return heartRate_bpm; + } + + @NonNull + @Override + public String toString() { + return "heart=" + heartRate_bpm; + } +} 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 60b1677ca..2ff83b011 100644 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java @@ -1,102 +1,97 @@ package de.dennisguse.opentracks.content.sensor; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; + +/** + * + */ public final class SensorDataSet { - public static final float DATA_UNAVAILABLE = Float.NaN; + private SensorDataHeartRate heartRate; - //TODO It might be necessary to consider: if sensor was connected as well. - private String sensorName; - private String sensorAddress; - private float heartRate; - private float cadence; - private float power; - private float batteryLevel; - private long time; + private SensorDataCycling.Cadence cyclingCadence; - public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel, long time) { + private SensorDataCycling.Speed cyclingSpeed; + + public SensorDataSet() { + } + + @VisibleForTesting + public SensorDataSet(SensorDataHeartRate heartRate, SensorDataCycling.Cadence cyclingCadence, SensorDataCycling.Speed cyclingSpeed) { this.heartRate = heartRate; - this.cadence = cadence; - this.power = power; - this.batteryLevel = batteryLevel; - this.time = time; + this.cyclingCadence = cyclingCadence; + this.cyclingSpeed = cyclingSpeed; } - public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel) { - this(heartRate, cadence, power, batteryLevel, System.currentTimeMillis()); - } - - public SensorDataSet(float heartRate, float cadence, float power) { - this(heartRate, cadence, power, DATA_UNAVAILABLE, System.currentTimeMillis()); - } - - public SensorDataSet(float heartRate, float cadence) { - this(heartRate, cadence, DATA_UNAVAILABLE, DATA_UNAVAILABLE, System.currentTimeMillis()); - } - - public SensorDataSet(float heartRate, String sensorName, String sensorAddress) { - this(heartRate, DATA_UNAVAILABLE, DATA_UNAVAILABLE, DATA_UNAVAILABLE, System.currentTimeMillis()); - this.sensorName = sensorName; - this.sensorAddress = sensorAddress; - } - - public boolean hasHeartRate() { - return !Float.isNaN(heartRate) && heartRate > 0; - } - - public float getHeartRate() { + public SensorDataHeartRate getHeartRate() { return heartRate; } - public boolean hasCadence() { - return !Float.isNaN(cadence); + public SensorDataCycling.Cadence getCyclingCadence() { + return cyclingCadence; } - public float getCadence() { - return cadence; + public SensorDataCycling.Speed getCyclingSpeed() { + return cyclingSpeed; } - public boolean hasPower() { - return !Float.isNaN(power); + public void set(SensorData data) { + if (data == null) { + return; + } + + if (data instanceof SensorDataHeartRate) { + this.heartRate = (SensorDataHeartRate) data; + return; + } + + if (data instanceof SensorDataCycling.Cadence) { + this.cyclingCadence = (SensorDataCycling.Cadence) data; + return; + } + if (data instanceof SensorDataCycling.Speed) { + this.cyclingSpeed = (SensorDataCycling.Speed) data; + return; + } + if (data instanceof SensorDataCycling.CadenceAndSpeed) { + set(((SensorDataCycling.CadenceAndSpeed) data).getCadence()); + set(((SensorDataCycling.CadenceAndSpeed) data).getSpeed()); + } + + throw new UnsupportedOperationException(); } - public float getPower() { - return power; + public void clear() { + this.heartRate = null; + this.cyclingCadence = null; + this.cyclingSpeed = null; } - public long getTime() { - return time; - } + public TrackPointSensorDataSet createTrackPointSensorDataSet() { + TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); + if (heartRate != null) { + sensorDataSet.setHeartRate_bpm(heartRate.getHeartRate_bpm()); + } - /** - * Is the data recent considering the current time. - * - * @param maxAge the maximal age in milliseconds. - */ - public boolean isRecent(long maxAge) { - return time + maxAge > System.currentTimeMillis(); - } + if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) { + sensorDataSet.setCyclingCadence(cyclingCadence.getCadence_rpm()); + } - public boolean hasBatteryLevel() { - return !Float.isNaN(batteryLevel); - } + if (cyclingSpeed != null && cyclingSpeed.hasSpeed()) { + sensorDataSet.setCyclingCadence(cyclingSpeed.getSpeed_ms()); + } - public float getBatteryLevel() { - return batteryLevel; - } - - public String getSensorName() { - return sensorName; - } - - public String getSensorAddress() { - return sensorAddress; + return sensorDataSet; } @NonNull @Override public String toString() { - return "time=" + getTime() + " sensor=" + getSensorAddress() + " heart=" + getHeartRate(); + return (getHeartRate() != null ? "" + getHeartRate() : "") + + (getCyclingCadence() != null ? " " + getCyclingCadence() : "") + + (getCyclingSpeed() != null ? " " + getCyclingSpeed() : ""); } } diff --git a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorState.java b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorState.java deleted file mode 100644 index 25da972e1..000000000 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorState.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.dennisguse.opentracks.content.sensor; - -import android.content.Context; - -import de.dennisguse.opentracks.R; - -public enum SensorState { - NONE, - CONNECTING, - CONNECTED, - DISCONNECTING, - DISCONNECTED; - - public static String getStateAsString(SensorState state, Context c) { - switch (state) { - case NONE: - return c.getString(R.string.value_none); - case CONNECTING: - return c.getString(R.string.sensor_state_connecting); - case CONNECTED: - return c.getString(R.string.sensor_state_connected); - case DISCONNECTING: - case DISCONNECTED: - return c.getString(R.string.sensor_state_disconnected); - default: - return ""; - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java index af45112d8..6696bd4c5 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java @@ -39,11 +39,11 @@ import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataListener; 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.sensor.SensorDataSet; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceInterface; -import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.PreferencesUtils; @@ -114,9 +114,12 @@ public class StatsFragment extends Fragment implements TrackDataListener { } /* Views */ - private View sensorContainerView; + private View heartRateContainer; private TextView heartRateValueView; private TextView heartRateSensorView; + private View cadenceContainer; + private TextView cadenceValueView; + private TextView cadenceSensorView; private TextView totalTimeValueView; private final Runnable updateUIeachSecond = new Runnable() { @@ -162,10 +165,14 @@ public class StatsFragment extends Fragment implements TrackDataListener { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - sensorContainerView = view.findViewById(R.id.stats_sensor_container); + heartRateContainer = view.findViewById(R.id.stats_sensor_heart_rate_container); heartRateValueView = view.findViewById(R.id.stats_sensor_heart_rate_value); heartRateSensorView = view.findViewById(R.id.stats_sensor_heart_rate_sensor_value); + cadenceContainer = view.findViewById(R.id.stats_sensor_cadence_container); + cadenceValueView = view.findViewById(R.id.stats_sensor_cadence_value); + cadenceSensorView = view.findViewById(R.id.stats_sensor_cadence_sensor_value); + totalTimeValueView = view.findViewById(R.id.stats_total_time_value); distanceValue = view.findViewById(R.id.stats_distance_value); @@ -269,9 +276,12 @@ public class StatsFragment extends Fragment implements TrackDataListener { @Override public void onDestroyView() { super.onDestroyView(); - sensorContainerView = null; + heartRateContainer = null; heartRateValueView = null; heartRateSensorView = null; + cadenceContainer = null; + cadenceValueView = null; + cadenceSensorView = null; totalTimeValueView = null; @@ -426,7 +436,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } /** - * Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}. + * Tries to fetch most recent {@link TrackPointSensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}. */ private void updateSensorDataUI() { TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); @@ -435,33 +445,56 @@ public class StatsFragment extends Fragment implements TrackDataListener { if (trackRecordingService == null) { Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service."); } else { - //TODO sensorState = trackRecordingService.getSensorState(); sensorDataSet = trackRecordingService.getSensorData(); } setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording()); + setCadenceSensorData(sensorDataSet, isSelectedTrackRecording()); } private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) { // heart rate int isVisible = View.VISIBLE; - if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext())) { + if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressNone(getContext())) { isVisible = View.INVISIBLE; } - sensorContainerView.setVisibility(isVisible); + heartRateContainer.setVisibility(isVisible); if (isRecording) { - String heartRate = getContext().getString(R.string.value_unknown); + String sensorValue = getContext().getString(R.string.value_unknown); String sensorName = getContext().getString(R.string.value_unknown); - if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) { - sensorName = sensorDataSet.getSensorName(); - if (sensorDataSet.hasHeartRate()) { - heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0); + if (sensorDataSet != null && sensorDataSet.getHeartRate() != null) { + sensorName = sensorDataSet.getHeartRate().getSensorName(); + if (sensorDataSet.getHeartRate().hasHeartRate_bpm() && sensorDataSet.getHeartRate().isRecent()) { + sensorValue = StringUtils.formatDecimal(sensorDataSet.getHeartRate().getHeartRate_bpm(), 0); } } heartRateSensorView.setText(sensorName); - heartRateValueView.setText(heartRate); + heartRateValueView.setText(sensorValue); + } + } + + private void setCadenceSensorData(SensorDataSet sensorDataSet, boolean isRecording) { + // heart rate + int isVisible = View.VISIBLE; + if (!isRecording || PreferencesUtils.isBluetoothCyclingCadenceSensorAddressNone(getContext())) { + isVisible = View.INVISIBLE; + } + cadenceContainer.setVisibility(isVisible); + + if (isRecording) { + String sensorValue = getContext().getString(R.string.value_unknown); + String sensorName = getContext().getString(R.string.value_unknown); + if (sensorDataSet != null && sensorDataSet.getCyclingCadence() != null) { + sensorName = sensorDataSet.getCyclingCadence().getSensorName(); + if (sensorDataSet.getCyclingCadence().hasCadence_rpm() && sensorDataSet.getCyclingCadence().isRecent()) { + sensorValue = StringUtils.formatDecimal(sensorDataSet.getCyclingCadence().getCadence_rpm(), 0); + } + } + + cadenceSensorView.setText(sensorName); + cadenceValueView.setText(sensorValue); } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java index 4ed5314f7..8b2e37bca 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java @@ -18,7 +18,6 @@ package de.dennisguse.opentracks.io.file.exporter; import android.content.Context; import android.database.Cursor; import android.location.Location; -import android.net.Uri; import java.io.OutputStream; import java.io.PrintWriter; @@ -29,9 +28,9 @@ import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.content.DescriptionGenerator; 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.sensor.SensorDataSet; import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.StringUtils; @@ -73,11 +72,11 @@ public class KmlTrackWriter implements TrackWriter { private TrackPoint startTrackPoint; /** - * @param context the context - * @param hasMultipleTracks should encode multiple tracks into one file? - * @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)? - * @param exportSensorData should {@link SensorDataSet} be exported? - * @param exportPhotos should pictures be exported (if true: exports to KMZ)? + * @param context the context + * @param hasMultipleTracks should encode multiple tracks into one file? + * @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)? + * @param exportSensorData should {@link TrackPointSensorDataSet} be exported? + * @param exportPhotos should pictures be exported (if true: exports to KMZ)? */ public KmlTrackWriter(Context context, boolean hasMultipleTracks, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) { this.context = context; @@ -269,13 +268,13 @@ public class KmlTrackWriter implements TrackWriter { printWriter.println("" + getCoordinates(trackPoint.getLocation(), " ") + ""); if (exportSensorData) { - SensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); + TrackPointSensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); if (sensorDataSet != null) { if (sensorDataSet.hasHeartRate()) { - heartRateList.add(sensorDataSet.getHeartRate()); + heartRateList.add(sensorDataSet.getHeartRate_bpm()); } - if (sensorDataSet.hasCadence()) { - cadenceList.add(sensorDataSet.getCadence()); + if (sensorDataSet.hasCyclingCadence()) { + cadenceList.add(sensorDataSet.getCyclingCadence_rpm()); } if (sensorDataSet.hasPower()) { powerList.add(sensorDataSet.getPower()); @@ -453,7 +452,7 @@ public class KmlTrackWriter implements TrackWriter { /** * Writes a sensor style. * - * @param name the name of the sesnor + * @param name the name of the sesnor * @param sensorType the sensor display name */ private void writeSensorStyle(String name, String sensorType) { diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java index f19fcd326..46d7aea6a 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java @@ -27,8 +27,8 @@ import org.xml.sax.SAXException; import java.util.ArrayList; import de.dennisguse.opentracks.content.data.TrackPoint; +import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter; /** @@ -224,29 +224,25 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { TrackPoint trackPoint = trackPoints.get(i); boolean hasSensorData = false; - float heartrate = SensorDataSet.DATA_UNAVAILABLE; - float cadence = SensorDataSet.DATA_UNAVAILABLE; - float power = SensorDataSet.DATA_UNAVAILABLE; + TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(trackPoint.getTime()); if (i < heartRateList.size()) { - heartrate = heartRateList.get(i); + sensorDataSet.setHeartRate_bpm(heartRateList.get(i)); hasSensorData = true; } if (i < cadenceList.size()) { - cadence = cadenceList.get(i); + sensorDataSet.setCyclingCadence(cadenceList.get(i)); hasSensorData = true; } if (i < powerList.size()) { - power = powerList.get(i); + sensorDataSet.setPower(cadenceList.get(i)); hasSensorData = true; } if (!hasSensorData) { - insertTrackPoint(trackPoint); - } else { - TrackPoint sensorDataSetLocation = new TrackPoint(trackPoint, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime())); - insertTrackPoint(sensorDataSetLocation); + trackPoint.setSensorDataSet(sensorDataSet); } + insertTrackPoint(trackPoint); } } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index 8ce1ec2de..f603cf1d8 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -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() { diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java index 531206a4a..951288a29 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java @@ -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. *

* 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}. */ 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 bd9b31b3a..fbd4c4240 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java @@ -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); } } 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 702cddda6..74f06457d 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java @@ -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); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java index e904e5f1c..5eccca161 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java +++ b/src/main/java/de/dennisguse/opentracks/settings/SettingsActivity.java @@ -13,6 +13,9 @@ import androidx.preference.PreferenceFragmentCompat; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; +import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference; +import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference; +import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference; import de.dennisguse.opentracks.util.BluetoothUtils; import de.dennisguse.opentracks.util.HackUtils; import de.dennisguse.opentracks.util.PreferencesUtils; @@ -111,8 +114,10 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit } else if (preference instanceof ActivityTypePreference) { activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey()); dialogFragment = activityPreferenceDialog; - } else if (preference instanceof BluetoothLePreference) { - dialogFragment = BluetoothLePreference.BluetoothLePreferenceDialog.newInstance(preference.getKey()); + } else if (preference instanceof BluetoothLeHeartRatePreference) { + dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SERVICE_UUID); + } else if (preference instanceof BluetoothLeCyclingCadenceAndSpeedPreference) { + dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID); } if (dialogFragment != null) { @@ -139,7 +144,7 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit private void updateBluetooth() { // Disable Bluetooth preference if device does not have Bluetooth - BluetoothLePreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key)); + BluetoothLeSensorPreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key)); bluetoothPreference.setVisible(BluetoothUtils.hasBluetooth(TAG)); } diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadenceAndSpeedPreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadenceAndSpeedPreference.java new file mode 100644 index 000000000..aba91ba79 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadenceAndSpeedPreference.java @@ -0,0 +1,23 @@ +package de.dennisguse.opentracks.settings.bluetooth; + +import android.content.Context; +import android.util.AttributeSet; + +public class BluetoothLeCyclingCadenceAndSpeedPreference extends BluetoothLeSensorPreference { + + public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BluetoothLeCyclingCadenceAndSpeedPreference(Context context) { + super(context); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java new file mode 100644 index 000000000..8021228a9 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java @@ -0,0 +1,23 @@ +package de.dennisguse.opentracks.settings.bluetooth; + +import android.content.Context; +import android.util.AttributeSet; + +public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference { + + public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BluetoothLeHeartRatePreference(Context context) { + super(context); + } +} \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/settings/BluetoothLePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeSensorPreference.java similarity index 78% rename from src/main/java/de/dennisguse/opentracks/settings/BluetoothLePreference.java rename to src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeSensorPreference.java index 4f8468408..275faf4c5 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/BluetoothLePreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeSensorPreference.java @@ -1,4 +1,4 @@ -package de.dennisguse.opentracks.settings; +package de.dennisguse.opentracks.settings.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.BluetoothLeScanner; @@ -21,8 +21,10 @@ import androidx.preference.PreferenceDialogFragmentCompat; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import de.dennisguse.opentracks.R; +import de.dennisguse.opentracks.settings.BluetoothLeAdapter; import de.dennisguse.opentracks.util.BluetoothUtils; import de.dennisguse.opentracks.util.PreferencesUtils; @@ -30,25 +32,27 @@ import de.dennisguse.opentracks.util.PreferencesUtils; * Preference to select a discoverable Bluetooth LE device. * Based upon ListPreference. */ -public class BluetoothLePreference extends DialogPreference { +public abstract class BluetoothLeSensorPreference extends DialogPreference { - private static final String TAG = BluetoothLePreference.class.getSimpleName(); + private static final String TAG = BluetoothLeSensorPreference.class.getSimpleName(); + + private static final String ARG_BLUETOOTH_UUID = "bluetoothUUID"; private static final int DEVICE_NONE_RESOURCEID = R.string.value_none; - public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } - public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr) { + public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - public BluetoothLePreference(Context context, AttributeSet attrs) { + public BluetoothLeSensorPreference(Context context, AttributeSet attrs) { super(context, attrs); } - public BluetoothLePreference(Context context) { + public BluetoothLeSensorPreference(Context context) { super(context); } @@ -78,18 +82,20 @@ public class BluetoothLePreference extends DialogPreference { @Override public CharSequence getSummary() { - if (getValue() == null || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext(), getValue())) { + if (getValue() == null || PreferencesUtils.isBluetoothSensorAddressNone(getContext(), getValue())) { return getContext().getString(DEVICE_NONE_RESOURCEID); } return getValue(); } - public static class BluetoothLePreferenceDialog extends PreferenceDialogFragmentCompat { + public static class BluetoothLeSensorPreferenceDialog extends PreferenceDialogFragmentCompat { private int selectedEntryIndex; private final BluetoothLeAdapter listAdapter = new BluetoothLeAdapter(); + private UUID sensorUUID; + private BluetoothLeScanner scanner = null; private final ScanCallback scanCallback = new ScanCallback() { @Override @@ -116,10 +122,11 @@ public class BluetoothLePreference extends DialogPreference { } }; - static BluetoothLePreferenceDialog newInstance(String key) { - final BluetoothLePreferenceDialog fragment = new BluetoothLePreferenceDialog(); + public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, UUID sensorUUID) { + final BluetoothLeSensorPreferenceDialog fragment = new BluetoothLeSensorPreferenceDialog(); final Bundle b = new Bundle(1); - b.putString(ARG_KEY, key); + b.putString(ARG_KEY, preferenceKey); + b.putSerializable(ARG_BLUETOOTH_UUID, sensorUUID); fragment.setArguments(b); return fragment; } @@ -128,6 +135,9 @@ public class BluetoothLePreference extends DialogPreference { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + sensorUUID = (UUID) getArguments().getSerializable(ARG_BLUETOOTH_UUID); + Log.i(TAG, "UUID: " + sensorUUID); + BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG); if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { Log.w(TAG, "Bluetooth adapter is present or not enabled."); @@ -152,14 +162,14 @@ public class BluetoothLePreference extends DialogPreference { listAdapter.add(getContext().getString(DEVICE_NONE_RESOURCEID), deviceNone); selectedEntryIndex = 0; - BluetoothLePreference preference = (BluetoothLePreference) getPreference(); + BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference(); String deviceSelected = preference.getValue(); if (deviceSelected != null && !deviceNone.equals(deviceSelected)) { listAdapter.add(preference.getValue(), preference.getValue()); selectedEntryIndex = 1; } - ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(BluetoothUtils.HEART_RATE_SERVICE_UUID)); + ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(sensorUUID)); List scanFilter = new ArrayList<>(); scanFilter.add(scanFilterBuilder.build()); @@ -180,7 +190,7 @@ public class BluetoothLePreference extends DialogPreference { public void onClick(DialogInterface dialog, int which) { selectedEntryIndex = which; - BluetoothLePreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + BluetoothLeSensorPreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE); dialog.dismiss(); } }); @@ -196,7 +206,7 @@ public class BluetoothLePreference extends DialogPreference { if (positiveResult && selectedEntryIndex >= 0) { String value = listAdapter.get(selectedEntryIndex).getAddress(); - BluetoothLePreference preference = (BluetoothLePreference) getPreference(); + BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference(); if (preference.callChangeListener(value)) { preference.setValue(value); } diff --git a/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java b/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java index d6a3df028..ed32d1c9f 100644 --- a/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/BluetoothUtils.java @@ -21,9 +21,13 @@ import android.os.Handler; import android.os.Looper; import android.util.Log; +import androidx.annotation.NonNull; + import java.util.ArrayList; import java.util.UUID; +import de.dennisguse.opentracks.content.sensor.SensorDataCycling; + /** * Utilities for dealing with bluetooth devices. * @@ -31,11 +35,21 @@ import java.util.UUID; */ public class BluetoothUtils { + public static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL); + public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL); + public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL); + + public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL); + public static final UUID CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL); private BluetoothUtils() { } + /** + * If called from UI: use a background thread to get the default Bluetooth adapter. + * TODO Check if this is necessary. + */ public static BluetoothAdapter getDefaultBluetoothAdapter(final String TAG) { // If from the main application thread, return directly if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) { @@ -77,12 +91,56 @@ public class BluetoothUtils { return BluetoothUtils.getDefaultBluetoothAdapter(TAG) != null; } - public static int parseHeartRate(BluetoothGattCharacteristic characteristic) { + public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) { //DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/ byte[] raw = characteristic.getValue(); - int index = ((raw[0] & 0x1) == 1) ? 2 : 1; - int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16; - return characteristic.getIntValue(format, index); + if (raw.length == 0) { + return null; + } + + boolean formatUINT16 = ((raw[0] & 0x1) == 1); + if (formatUINT16 && raw.length >= 3) { + return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1); + } + if (!formatUINT16 && raw.length >= 2) { + return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1); + } + + return null; } + /** + * Documentation: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449 + */ + public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) { + int valueLength = characteristic.getValue().length; + if (valueLength == 0) { + return null; + } + + int flags = characteristic.getValue()[0]; + boolean hasCrank = (flags & 0x01) > 0; + boolean hasWheel = (flags & 0x02) > 0; + + SensorDataCycling.Cadence cadence = null; + int index = 1; + if (hasCrank && valueLength - index >= 6) { + long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index); + index += 4; + + int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s + index += 2; + cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime); + } + + SensorDataCycling.Speed speed = null; + if (hasWheel && valueLength - index >= 4) { + int wheelCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); + index += 2; + int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s + speed = new SensorDataCycling.Speed(address, sensorName, wheelCount, wheelTime); + } + + return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed); + } } diff --git a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java index eb3d1c7e0..ed2b45443 100644 --- a/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/PreferencesUtils.java @@ -215,22 +215,42 @@ public class PreferencesUtils { setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED); } - private static String getBluetoothHeartRateSensorAddressNone(Context context) { + private static String getBluetoothSensorAddressNone(Context context) { return context.getString(R.string.sensor_type_value_none); } - public static boolean isBluetoothHeartRateSensorAddressDefault(Context context) { - return getBluetoothHeartRateSensorAddressNone(context).equals(getBluetoothHeartRateSensorAddress(context)); + public static boolean isBluetoothSensorAddressNone(Context context, String currentValue) { + return getBluetoothSensorAddressNone(context).equals(currentValue); } - public static boolean isBluetoothHeartRateSensorAddressDefault(Context context, String currentValue) { - return getBluetoothHeartRateSensorAddressNone(context).equals(currentValue); + + public static boolean isBluetoothHeartRateSensorAddressNone(Context context) { + return isBluetoothSensorAddressNone(context, getBluetoothHeartRateSensorAddress(context)); } public static String getBluetoothHeartRateSensorAddress(Context context) { - return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context)); + return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone(context)); } + + public static boolean isBluetoothCyclingCadenceSensorAddressNone(Context context) { + return isBluetoothSensorAddressNone(context, getBluetoothCyclingCadenceSensorAddress(context)); + } + + public static String getBluetoothCyclingCadenceSensorAddress(Context context) { + return getString(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone(context)); + } + + + public static boolean isBluetoothCyclingSpeedSensorAddressNone(Context context) { + return isBluetoothSensorAddressNone(context, getBluetoothCyclingSpeedSensorAddress(context)); + } + + public static String getBluetoothCyclingSpeedSensorAddress(Context context) { + return getString(context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context)); + } + + 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); return getBoolean(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/UintUtils.java b/src/main/java/de/dennisguse/opentracks/util/UintUtils.java new file mode 100644 index 000000000..3778c3255 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/UintUtils.java @@ -0,0 +1,30 @@ +package de.dennisguse.opentracks.util; + +public class UintUtils { + + public static int UINT16_MAX = 0xFFFF; + public static long UINT32_MAX = 0xFFFFFFFFL; + + private UintUtils() { + } + + /** + * Computes a - b for UINT with overflow (b < a). + * + * @return diff or -1 (invalid) + */ + public static long diff(long a, long b, final long UINT_MAX) { + if (a < 0 || b < 0) { + return -1; + } + if (a > UINT_MAX && b > UINT_MAX) { + return -1; + } + + if (a >= b) { + return a - b; + } + + return (UINT_MAX - b) + a; + } +} diff --git a/src/main/res/layout/stats.xml b/src/main/res/layout/stats.xml index a908a8208..dcd753d99 100644 --- a/src/main/res/layout/stats.xml +++ b/src/main/res/layout/stats.xml @@ -267,18 +267,24 @@ limitations under the License. android:id="@+id/stats_sensor_separator" style="@style/StatsHorizontalLine" /> - + - + + + + - + + style="@style/StatsLargeLabel" + android:text="@string/sensor_state_cadence" /> + android:text="@string/value_unknown" /> + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/do_not_translate.xml b/src/main/res/values/do_not_translate.xml index 014d775d0..bb53a8171 100644 --- a/src/main/res/values/do_not_translate.xml +++ b/src/main/res/values/do_not_translate.xml @@ -25,6 +25,7 @@ limitations under the License. http://maps.google.com/mapfiles/ms/micons/blue-pushpin.png BPM + RPM - diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index 5c9693d6f..aa27838a6 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -16,6 +16,8 @@ settingsReset bluetoothSensor + bluetoothCyclingCadenceSensor + bluetoothCyclingSpeedSensor NONE defaultActivity diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml index d86b8dffe..c1ee6185f 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -111,10 +111,18 @@ limitations under the License. - + + + + + From 3408ea97f4a214095114e584f29375bbf6204584 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Fri, 24 Apr 2020 23:32:12 +0200 Subject: [PATCH 3/4] Refactoring: move TrackPointDataSet directly into TrackPoint. --- .../dennisguse/opentracks/TrackStubUtils.java | 3 +- .../CustomContentProviderUtilsTest.java | 84 ++++++++++------ .../fragments/ChartFragmentTest.java | 35 ++----- .../services/TrackRecordingServiceTest.java | 4 +- .../TrackRecordingServiceTestLocation.java | 5 +- .../opentracks/chart/ChartPoint.java | 18 ++-- .../opentracks/content/data/TrackPoint.java | 67 +++++++++---- .../content/data/TrackPointSensorDataSet.java | 99 ------------------- .../provider/ContentProviderUtils.java | 30 +++--- .../content/sensor/SensorDataSet.java | 21 +--- .../opentracks/fragments/ChartFragment.java | 2 +- .../opentracks/fragments/StatsFragment.java | 3 +- .../io/file/exporter/KmlTrackWriter.java | 22 ++--- .../file/importer/KmlFileTrackImporter.java | 16 +-- .../services/TrackRecordingService.java | 13 ++- 15 files changed, 166 insertions(+), 256 deletions(-) delete mode 100644 src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java diff --git a/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java b/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java index e258bce30..2894dbcbc 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java +++ b/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java @@ -19,7 +19,6 @@ package de.dennisguse.opentracks; import android.location.Location; import de.dennisguse.opentracks.content.data.TrackPoint; -import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; /** * Commons utilities for creating stubs of track, location. @@ -63,6 +62,6 @@ public class TrackStubUtils { location.setTime(INITIAL_TIME); location.setBearing(INITIAL_BEARING); - return new TrackPoint(location, new TrackPointSensorDataSet()); + return new TrackPoint(location); } } diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java index f0aa1cdae..930af3470 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java @@ -514,7 +514,7 @@ public class CustomContentProviderUtilsTest { } /** - * Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(TrackPoint[], int, long)}. + * Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(TrackPoint[], long)}. */ @Test public void testBulkInsertTrackPoint() { @@ -533,44 +533,44 @@ public class CustomContentProviderUtilsTest { /** * Tests the method {@link ContentProviderUtils#createTrackPoint(Cursor)}. */ + //TODO incomplete @Test public void testCreateTrackPoint() { - // Set index. - int index = 1; - when(cursorMock.getColumnIndex(TrackPointsColumns._ID)).thenReturn(index++); - when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).thenReturn(index++); - when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).thenReturn(index++); - when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME)).thenReturn(index++); - when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).thenReturn(index++); - when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).thenReturn(index++); + // given + when(cursorMock.getColumnIndex(TrackPointsColumns._ID)).thenReturn(1); - // Set return value of isNull(). - index = 2; - when(cursorMock.isNull(index++)).thenReturn(false); - when(cursorMock.isNull(index++)).thenReturn(false); - when(cursorMock.isNull(index++)).thenReturn(false); - when(cursorMock.isNull(index++)).thenReturn(false); - when(cursorMock.isNull(index++)).thenReturn(false); - - // Set return value of getInt(). - index = 2; + when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).thenReturn(2); + when(cursorMock.isNull(2)).thenReturn(false); int longitude = 11; - when(cursorMock.getInt(index++)).thenReturn(longitude * 1000000); + when(cursorMock.getInt(2)).thenReturn(longitude * 1000000); + + when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).thenReturn(3); + when(cursorMock.isNull(3)).thenReturn(false); int latitude = 22; - when(cursorMock.getInt(index++)).thenReturn(latitude * 1000000); + when(cursorMock.getInt(3)).thenReturn(latitude * 1000000); + + when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME)).thenReturn(4); + when(cursorMock.isNull(4)).thenReturn(false); long time = System.currentTimeMillis(); - when(cursorMock.getLong(index++)).thenReturn(time); + when(cursorMock.getLong(4)).thenReturn(time); + + when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).thenReturn(5); + when(cursorMock.isNull(5)).thenReturn(false); float speed = 2.2f; - when(cursorMock.getFloat(index++)).thenReturn(speed); + when(cursorMock.getFloat(5)).thenReturn(speed); - byte[] sensor = "Sensor state".getBytes(); - when(cursorMock.getBlob(index++)).thenReturn(sensor); + when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).thenReturn(6); + when(cursorMock.isNull(6)).thenReturn(false); - TrackPoint location = contentProviderUtils.createTrackPoint(cursorMock); - Assert.assertEquals(longitude, location.getLongitude(), 0.01); - Assert.assertEquals(latitude, location.getLatitude(), 0.01); - Assert.assertEquals(time, location.getTime(), 0.01); - Assert.assertEquals(speed, location.getSpeed(), 0.01); + // when + TrackPoint trackPoint = contentProviderUtils.createTrackPoint(cursorMock); + + // then + Assert.assertEquals(longitude, trackPoint.getLongitude(), 0.01); + Assert.assertEquals(latitude, trackPoint.getLatitude(), 0.01); + Assert.assertEquals(time, trackPoint.getTime(), 0.01); + Assert.assertEquals(speed, trackPoint.getSpeed(), 0.01); + Assert.assertFalse(trackPoint.hasHeartRate()); } /** @@ -584,7 +584,29 @@ public class CustomContentProviderUtilsTest { Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10); contentProviderUtils.insertTrackPoint(TestDataUtil.createTrackPoint(22), trackId); - Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount()); + Assert.assertEquals(11, contentProviderUtils.getTrackPoints(trackId).size()); + } + + @Test + public void testInsertAndLoadTrackPoint() { + // given + long trackId = System.currentTimeMillis(); + Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10); + + TrackPoint trackPoint = TestDataUtil.createTrackPoint(5); + trackPoint.setHeartRate_bpm(1F); + trackPoint.setCyclingCadence_rpm(2F); + trackPoint.setPower(3F); + + // when + contentProviderUtils.insertTrackPoint(trackPoint, trackId); + + // then + List trackPoints = contentProviderUtils.getTrackPoints(trackId); + Assert.assertTrue(trackPoints.get(10).hasHeartRate()); + Assert.assertEquals(trackPoint.getHeartRate_bpm(), trackPoints.get(10).getHeartRate_bpm(), 0.01); + Assert.assertEquals(trackPoint.getCyclingCadence_rpm(), trackPoints.get(10).getCyclingCadence_rpm(), 0.01); + Assert.assertEquals(trackPoint.getPower(), trackPoints.get(10).getPower(), 0.01); } /** diff --git a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java index acddf2688..00d579ff5 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java @@ -33,7 +33,6 @@ import de.dennisguse.opentracks.TrackStubUtils; import de.dennisguse.opentracks.chart.ChartPoint; import de.dennisguse.opentracks.chart.ChartView; import de.dennisguse.opentracks.content.data.TrackPoint; -import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; import de.dennisguse.opentracks.util.UnitConversions; /** @@ -72,20 +71,13 @@ public class ChartFragmentTest { */ @Test public void testCreatePendingPoint_sensorIncorrect() { + // given TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); - // No input. + // when ChartPoint point = chartFragment.createPendingPoint(trackPoint); - Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01); - Assert.assertEquals(Float.NaN, point.getCadence(), 0.01); - Assert.assertEquals(Float.NaN, point.getPower(), 0.01); - // Input incorrect state. - // Creates SensorData. - TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); - trackPoint.setSensorDataSet(sensorDataSet); - // Test. - point = chartFragment.createPendingPoint(trackPoint); + // then Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01); Assert.assertEquals(Float.NaN, point.getCadence(), 0.01); Assert.assertEquals(Float.NaN, point.getPower(), 0.01); @@ -96,23 +88,16 @@ public class ChartFragmentTest { */ @Test public void testCreatePendingPoint_sensorCorrect() { + // given TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); - // No input. + trackPoint.setHeartRate_bpm(100f); + trackPoint.setCyclingCadence_rpm(101f); + trackPoint.setPower(102f); + + // when ChartPoint point = chartFragment.createPendingPoint(trackPoint); - Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01); - Assert.assertEquals(Float.NaN, point.getCadence(), 0.01); - Assert.assertEquals(Float.NaN, point.getPower(), 0.01); - // Creates SensorData. - TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); - sensorDataSet.setHeartRate_bpm(100f); - sensorDataSet.setCyclingCadence(101); - sensorDataSet.setPower(102f); - - // Creates SensorDataSet. - trackPoint.setSensorDataSet(sensorDataSet); - // Test. - point = chartFragment.createPendingPoint(trackPoint); + // then Assert.assertEquals(100.0, point.getHeartRate(), 0.01); Assert.assertEquals(101.0, point.getCadence(), 0.01); Assert.assertEquals(102.0, point.getPower(), 0.01); diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java index 7ebc2bb5c..ffe0ecb2b 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTest.java @@ -331,7 +331,7 @@ public class TrackRecordingServiceTest { addTrack(dummyTrack, isRecording); } - static void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException { + private static void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException { insertLocation(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis()); } @@ -342,7 +342,7 @@ public class TrackRecordingServiceTest { /** * Inserts a location and waits for 200ms. */ - static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException { + private static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException { Location location = new Location("gps"); location.setLongitude(longitude); location.setLatitude(latitude); diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java index 81a64c416..372e493ae 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceTestLocation.java @@ -27,6 +27,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPointsColumns; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.CustomContentProvider; +import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate; import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; import de.dennisguse.opentracks.util.PreferencesUtils; @@ -245,7 +246,9 @@ public class TrackRecordingServiceTestLocation { @Override public SensorDataSet getSensorData() { - return new SensorDataSet(); + SensorDataSet sensorDataSet = new SensorDataSet(); + sensorDataSet.set(new SensorDataHeartRate("sensorName", "sensorAddress", 5f)); + return sensorDataSet; } }); diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java index 262ad6231..34369efbb 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java @@ -3,7 +3,7 @@ package de.dennisguse.opentracks.chart; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; +import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.util.UnitConversions; @@ -25,7 +25,7 @@ public class ChartPoint { this.elevation = elevation; } - public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPointSensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) { + public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPoint trackPoint, boolean chartByDistance, boolean metricUnits) { TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics(); if (chartByDistance) { @@ -48,15 +48,15 @@ public class ChartPoint { speed *= UnitConversions.KM_TO_MI; } pace = speed == 0 ? 0.0 : 60.0 / speed; - if (sensorDataSet != null) { - if (sensorDataSet.hasHeartRate()) { - heartRate = sensorDataSet.getHeartRate_bpm(); + if (trackPoint != null) { + if (trackPoint.hasHeartRate()) { + heartRate = trackPoint.getHeartRate_bpm(); } - if (sensorDataSet.hasCyclingCadence()) { - cadence = sensorDataSet.getCyclingCadence_rpm(); + if (trackPoint.hasCyclingCadence()) { + cadence = trackPoint.getCyclingCadence_rpm(); } - if (sensorDataSet.hasPower()) { - power = sensorDataSet.getPower(); + if (trackPoint.hasPower()) { + power = trackPoint.getPower(); } } } diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java index 992260471..959011eef 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java @@ -29,6 +29,10 @@ public class TrackPoint { private final Location location; + private Float heartRate_bpm = null; + private Float cyclingCadence_rpm = null; + private Float power = null; + public TrackPoint() { this.location = new Location(""); } @@ -37,17 +41,13 @@ public class TrackPoint { this.location = location; } - public TrackPoint(@NonNull Location location, TrackPointSensorDataSet sensorDataSet) { - this.location = location; - this.sensorDataSet = sensorDataSet; - } - - public TrackPoint(@NonNull TrackPoint trackPoint, TrackPointSensorDataSet sensorDataSet) { + public TrackPoint(@NonNull TrackPoint trackPoint) { this.location = trackPoint.getLocation(); - this.sensorDataSet = sensorDataSet; - } - private TrackPointSensorDataSet sensorDataSet = null; + this.heartRate_bpm = trackPoint.getHeartRate_bpm(); + this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm(); + this.power = trackPoint.getPower(); + } public TrackPoint(double latitude, double longitude, Double altitude, long time) { location = new Location(LocationManager.GPS_PROVIDER); @@ -88,14 +88,6 @@ public class TrackPoint { return location; } - public TrackPointSensorDataSet getSensorDataSet() { - return sensorDataSet; - } - - public void setSensorDataSet(TrackPointSensorDataSet sensorDataSet) { - this.sensorDataSet = sensorDataSet; - } - public double getLatitude() { return location.getLatitude(); } @@ -180,6 +172,47 @@ public class TrackPoint { return location.bearingTo(dest); } + // Sensor data + public boolean hasSensorData() { + return hasHeartRate() || hasCyclingCadence() || hasPower(); + } + + public boolean hasHeartRate() { + return heartRate_bpm != null && heartRate_bpm > 0; + } + + public float getHeartRate_bpm() { + return heartRate_bpm; + } + + public void setHeartRate_bpm(Float heartRate_bpm) { + this.heartRate_bpm = heartRate_bpm; + } + + public boolean hasCyclingCadence() { + return cyclingCadence_rpm != null; + } + + public Float getCyclingCadence_rpm() { + return cyclingCadence_rpm; + } + + public void setCyclingCadence_rpm(Float cyclingCadence_rpm) { + this.cyclingCadence_rpm = cyclingCadence_rpm; + } + + public boolean hasPower() { + return power != null; + } + + public Float getPower() { + return power; + } + + public void setPower(Float power) { + this.power = power; + } + @NonNull @Override public String toString() { diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java deleted file mode 100644 index e16118b9d..000000000 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPointSensorDataSet.java +++ /dev/null @@ -1,99 +0,0 @@ -package de.dennisguse.opentracks.content.data; - -import androidx.annotation.NonNull; - -//TODO Rename. -public final class TrackPointSensorDataSet { - private long time_ms; - - private Float heartRate_bpm = null; - private Float cyclingCadence = null; - private Float cyclingSpeed = null; - private Float power = null; - - public TrackPointSensorDataSet() { - this(System.currentTimeMillis()); - } - - public TrackPointSensorDataSet(long time_ms) { - this.time_ms = time_ms; - } - - public boolean hasHeartRate() { - return heartRate_bpm != null && heartRate_bpm > 0; - } - - public float getHeartRate_bpm() { - return heartRate_bpm; - } - - public void setHeartRate_bpm(Float heartRate_bpm) { - this.heartRate_bpm = heartRate_bpm; - } - - public boolean hasCyclingCadence() { - return cyclingCadence != null; - } - - public Float getCyclingCadence_rpm() { - return cyclingCadence; - } - - public void setCyclingCadence(Float cyclingCadence) { - this.cyclingCadence = cyclingCadence; - } - - public void setCyclingCadence(Integer cyclingCadence) { - this.cyclingCadence = cyclingCadence != null ? cyclingCadence.floatValue() : null; - } - - public boolean hasCyclingSpeed() { - return cyclingSpeed != null; - } - - public Float getCyclingSpeed() { - return cyclingSpeed; - } - - public void setCyclingSpeed(Float cyclingSpeed) { - this.cyclingSpeed = cyclingSpeed; - } - - public boolean hasPower() { - return power != null; - } - - public Float getPower() { - return power; - } - - public void setPower(Float power) { - this.power = power; - } - - public long getTime() { - return time_ms; - } - - public void setTime(long time_ms) { - this.time_ms = time_ms; - } - - /** - * Is the data recent considering the current time. - * - * @param maxAge the maximal age in milliseconds. - */ - public boolean isRecent(long maxAge) { - return time_ms + maxAge > System.currentTimeMillis(); - } - - @NonNull - @Override - public String toString() { - return "time=" + getTime() - + (hasHeartRate() ? " heart=" + getHeartRate_bpm() : "") - + (hasCyclingCadence() ? " cyclingCad=" + getCyclingCadence_rpm() : "") - + (hasCyclingSpeed() ? " cyclingSpeed=" + getCyclingSpeed() : ""); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index b429be852..53a054881 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -36,7 +36,6 @@ import de.dennisguse.opentracks.android.ContentResolverWrapper; import de.dennisguse.opentracks.android.IContentResolver; 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.TrackPointsColumns; import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.data.Waypoint; @@ -621,19 +620,16 @@ public class ContentProviderUtils { trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex)); } - TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); - - if (cursor.isNull(indexes.sensorHeartRateIndex)) { - sensorDataSet.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex)); + if (!cursor.isNull(indexes.sensorHeartRateIndex)) { + trackPoint.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex)); } - if (cursor.isNull(indexes.sensorCadenceIndex)) { - sensorDataSet.setCyclingCadence(cursor.getFloat(indexes.sensorCadenceIndex)); + if (!cursor.isNull(indexes.sensorCadenceIndex)) { + trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex)); } - if (cursor.isNull(indexes.sensorPowerIndex)) { - sensorDataSet.setCyclingSpeed(cursor.getFloat(indexes.sensorPowerIndex)); + if (!cursor.isNull(indexes.sensorPowerIndex)) { + trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex)); } - trackPoint.setSensorDataSet(sensorDataSet); return trackPoint; } @@ -812,16 +808,14 @@ public class ContentProviderUtils { values.put(TrackPointsColumns.BEARING, trackPoint.getBearing()); } - //SensorData - TrackPointSensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); - if (sensorDataSet != null && sensorDataSet.hasHeartRate()) { - values.put(TrackPointsColumns.SENSOR_HEARTRATE, sensorDataSet.getHeartRate_bpm()); + if (trackPoint.hasHeartRate()) { + values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate_bpm()); } - if (sensorDataSet != null && sensorDataSet.hasCyclingCadence()) { - values.put(TrackPointsColumns.SENSOR_CADENCE, sensorDataSet.getCyclingCadence_rpm()); + if (trackPoint.hasCyclingCadence()) { + values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm()); } - if (sensorDataSet != null && sensorDataSet.hasPower()) { - values.put(TrackPointsColumns.SENSOR_POWER, sensorDataSet.getPower()); + if (trackPoint.hasPower()) { + values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower()); } return values; } 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 2ff83b011..ac0d11afa 100644 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java @@ -1,9 +1,8 @@ package de.dennisguse.opentracks.content.sensor; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; +import de.dennisguse.opentracks.content.data.TrackPoint; /** * @@ -19,13 +18,6 @@ public final class SensorDataSet { public SensorDataSet() { } - @VisibleForTesting - public SensorDataSet(SensorDataHeartRate heartRate, SensorDataCycling.Cadence cyclingCadence, SensorDataCycling.Speed cyclingSpeed) { - this.heartRate = heartRate; - this.cyclingCadence = cyclingCadence; - this.cyclingSpeed = cyclingSpeed; - } - public SensorDataHeartRate getHeartRate() { return heartRate; } @@ -70,21 +62,18 @@ public final class SensorDataSet { this.cyclingSpeed = null; } - public TrackPointSensorDataSet createTrackPointSensorDataSet() { - TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(); + public void fillTrackPoint(TrackPoint trackPoint) { if (heartRate != null) { - sensorDataSet.setHeartRate_bpm(heartRate.getHeartRate_bpm()); + trackPoint.setHeartRate_bpm(heartRate.getHeartRate_bpm()); } if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) { - sensorDataSet.setCyclingCadence(cyclingCadence.getCadence_rpm()); + trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm()); } if (cyclingSpeed != null && cyclingSpeed.hasSpeed()) { - sensorDataSet.setCyclingCadence(cyclingSpeed.getSpeed_ms()); + trackPoint.setSpeed(cyclingSpeed.getSpeed_ms()); } - - return sensorDataSet; } @NonNull diff --git a/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java index c5a13f0fe..4194f2b7e 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java @@ -313,7 +313,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { @VisibleForTesting ChartPoint createPendingPoint(@NonNull TrackPoint trackPoint) { trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); - return new ChartPoint(trackStatisticsUpdater, trackPoint.getSensorDataSet(), chartByDistance, chartView.getMetricUnits()); + return new ChartPoint(trackStatisticsUpdater, trackPoint, chartByDistance, chartView.getMetricUnits()); } @VisibleForTesting diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java index 6696bd4c5..9b1d49db3 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java @@ -39,7 +39,6 @@ import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataListener; 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.sensor.SensorDataSet; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; @@ -436,7 +435,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } /** - * Tries to fetch most recent {@link TrackPointSensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}. + * Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}. */ private void updateSensorDataUI() { TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java index 8b2e37bca..87d06c295 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java @@ -28,7 +28,6 @@ import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.content.DescriptionGenerator; 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.util.FileUtils; @@ -75,7 +74,7 @@ public class KmlTrackWriter implements TrackWriter { * @param context the context * @param hasMultipleTracks should encode multiple tracks into one file? * @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)? - * @param exportSensorData should {@link TrackPointSensorDataSet} be exported? + * @param exportSensorData should {@link TrackPointSensorDataSet} be exported? * @param exportPhotos should pictures be exported (if true: exports to KMZ)? */ public KmlTrackWriter(Context context, boolean hasMultipleTracks, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) { @@ -268,17 +267,14 @@ public class KmlTrackWriter implements TrackWriter { printWriter.println("" + getCoordinates(trackPoint.getLocation(), " ") + ""); if (exportSensorData) { - TrackPointSensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); - if (sensorDataSet != null) { - if (sensorDataSet.hasHeartRate()) { - heartRateList.add(sensorDataSet.getHeartRate_bpm()); - } - if (sensorDataSet.hasCyclingCadence()) { - cadenceList.add(sensorDataSet.getCyclingCadence_rpm()); - } - if (sensorDataSet.hasPower()) { - powerList.add(sensorDataSet.getPower()); - } + if (trackPoint.hasHeartRate()) { + heartRateList.add(trackPoint.getHeartRate_bpm()); + } + if (trackPoint.hasCyclingCadence()) { + cadenceList.add(trackPoint.getCyclingCadence_rpm()); + } + if (trackPoint.hasPower()) { + powerList.add(trackPoint.getPower()); } } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java index 46d7aea6a..f783f2df1 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java @@ -27,7 +27,6 @@ import org.xml.sax.SAXException; import java.util.ArrayList; import de.dennisguse.opentracks.content.data.TrackPoint; -import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter; @@ -223,25 +222,16 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { for (int i = 0; i < trackPoints.size(); i++) { TrackPoint trackPoint = trackPoints.get(i); - boolean hasSensorData = false; - TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(trackPoint.getTime()); - if (i < heartRateList.size()) { - sensorDataSet.setHeartRate_bpm(heartRateList.get(i)); - hasSensorData = true; + trackPoint.setHeartRate_bpm(heartRateList.get(i)); } if (i < cadenceList.size()) { - sensorDataSet.setCyclingCadence(cadenceList.get(i)); - hasSensorData = true; + trackPoint.setCyclingCadence_rpm(cadenceList.get(i)); } if (i < powerList.size()) { - sensorDataSet.setPower(cadenceList.get(i)); - hasSensorData = true; + trackPoint.setPower(cadenceList.get(i)); } - if (!hasSensorData) { - trackPoint.setSensorDataSet(sensorDataSet); - } insertTrackPoint(trackPoint); } } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index f603cf1d8..82b5265b6 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -45,7 +45,6 @@ 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; @@ -576,7 +575,9 @@ public class TrackRecordingService extends Service { return; } - TrackPoint trackPoint = new TrackPoint(location, getTrackPointSensorDataSet()); + TrackPoint trackPoint = new TrackPoint(location); + fillWithSensorDataSet(trackPoint); + notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy); if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) { @@ -627,7 +628,7 @@ public class TrackRecordingService extends Service { return; } - if (trackPoint.getSensorDataSet() != null || distanceToLastTrackLocation >= recordingDistanceInterval) { + if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) { insertTrackPointIfNewer(track, lastTrackPoint); insertTrackPoint(track, trackPoint); isIdle = false; @@ -716,13 +717,11 @@ public class TrackRecordingService extends Service { return remoteSensorManager.getSensorData(); } - TrackPointSensorDataSet getTrackPointSensorDataSet() { + void fillWithSensorDataSet(TrackPoint trackPoint) { SensorDataSet sensorData = getSensorDataSet(); if (sensorData != null) { - return sensorData.createTrackPointSensorDataSet(); + sensorData.fillTrackPoint(trackPoint); } - - return null; } private void registerLocationListener() { From 4893a2b7432b87e11ef2de63c4782121a52ab160 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sat, 25 Apr 2020 19:59:18 +0200 Subject: [PATCH 4/4] 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. - - - - + +