diff --git a/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java b/src/androidTest/java/de/dennisguse/opentracks/TrackStubUtils.java index 0a6420038..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.sensor.SensorDataSet; /** * 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 SensorDataSet(Float.NaN, Float.NaN)); + 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/content/sensor/SensorDataCyclingTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java new file mode 100644 index 000000000..ca8f7959d --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/content/sensor/SensorDataCyclingTest.java @@ -0,0 +1,92 @@ +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("sensorAddress", "sensorName", 1, 1024); // 1s + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 2048); // 2s + + // when + current.compute(previous); + + // then + Assert.assertEquals(60, current.getCadence_rpm(), 0.01); + } + + @Test + public void compute_cadence_sameCount() { + // given + SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 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("sensorAddress", "sensorName", 1, 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 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("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024); + SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 0); + + // when + current.compute(previous); + + // then + Assert.assertEquals(60, current.getCadence_rpm(), 0.01); + } + + @Test + public void compute_cadence_rollOverCount() { + // given + 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(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/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java index 22c45f79e..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.sensor.SensorDataSet; 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. - SensorDataSet sensorDataSet = new SensorDataSet(SensorDataSet.DATA_UNAVAILABLE, SensorDataSet.DATA_UNAVAILABLE); - 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,20 +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. - SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102); - - // 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 eaeda2d6c..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; @@ -244,13 +245,10 @@ public class TrackRecordingServiceTestLocation { } @Override - public boolean isSensorDataSetValid() { - return true; - } - - @Override - public SensorDataSet getSensorDataSet() { - return new SensorDataSet(1, 2); + public SensorDataSet getSensorData() { + SensorDataSet sensorDataSet = new SensorDataSet(); + sensorDataSet.set(new SensorDataHeartRate("sensorName", "sensorAddress", 5f)); + return 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..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.sensor.SensorDataSet; +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, SensorDataSet 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(); + if (trackPoint != null) { + if (trackPoint.hasHeartRate()) { + heartRate = trackPoint.getHeartRate_bpm(); } - if (sensorDataSet.hasCadence()) { - cadence = sensorDataSet.getCadence(); + 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 a3f230bb8..959011eef 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. * @@ -31,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(""); } @@ -39,17 +41,13 @@ public class TrackPoint { this.location = location; } - public TrackPoint(@NonNull Location location, SensorDataSet sensorDataSet) { - this.location = location; - this.sensorDataSet = sensorDataSet; - } - - public TrackPoint(@NonNull TrackPoint trackPoint, SensorDataSet sensorDataSet) { + public TrackPoint(@NonNull TrackPoint trackPoint) { this.location = trackPoint.getLocation(); - this.sensorDataSet = sensorDataSet; - } - private SensorDataSet 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); @@ -90,14 +88,6 @@ public class TrackPoint { return location; } - public SensorDataSet getSensorDataSet() { - return sensorDataSet; - } - - public void setSensorDataSet(SensorDataSet sensorDataSet) { - this.sensorDataSet = sensorDataSet; - } - public double getLatitude() { return location.getLatitude(); } @@ -182,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/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index 409b3cab2..53a054881 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -40,7 +40,6 @@ 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,11 +620,15 @@ 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); - - trackPoint.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime())); + if (!cursor.isNull(indexes.sensorHeartRateIndex)) { + trackPoint.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex)); + } + if (!cursor.isNull(indexes.sensorCadenceIndex)) { + trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex)); + } + if (!cursor.isNull(indexes.sensorPowerIndex)) { + trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex)); + } return trackPoint; } @@ -805,16 +808,14 @@ public class ContentProviderUtils { values.put(TrackPointsColumns.BEARING, trackPoint.getBearing()); } - //SensorData - SensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); - if (sensorDataSet != null && sensorDataSet.hasHeartRate()) { - values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getSensorDataSet().getHeartRate()); + if (trackPoint.hasHeartRate()) { + values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate_bpm()); } - if (sensorDataSet != null && sensorDataSet.hasCadence()) { - values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getSensorDataSet().getCadence()); + if (trackPoint.hasCyclingCadence()) { + values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm()); } - if (sensorDataSet != null && sensorDataSet.hasPower()) { - values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getSensorDataSet().getPower()); + if (trackPoint.hasPower()) { + values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.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..231d0285e --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataCycling.java @@ -0,0 +1,175 @@ +package de.dennisguse.opentracks.content.sensor; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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()); + } + + 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()) { + 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(); + } + } + + public static class Speed extends SensorData { + + private int wheelRevolutionsCount; // UINT16 + private int wheelRevolutionsTime; // UINT16; 1/1024s + private float speed_mps = 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_mps() { + return !Float.isNaN(speed_mps); + } + + public float getSpeed_mps() { + return speed_mps; + } + + 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; + if (timeDiff_ms <= 0) { + Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); + speed_mps = INVALID_VALUE_FLOAT; + } else { + 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); + } + } + } + + @NonNull + @Override + public String toString() { + return "speed=" + getSpeed_mps() + "_" + 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..0d05c9c9d 100644 --- a/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/content/sensor/SensorDataSet.java @@ -2,101 +2,85 @@ package de.dennisguse.opentracks.content.sensor; import androidx.annotation.NonNull; +import de.dennisguse.opentracks.content.data.TrackPoint; + +/** + * + */ 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) { - this.heartRate = heartRate; - this.cadence = cadence; - this.power = power; - this.batteryLevel = batteryLevel; - this.time = time; + private SensorDataCycling.Speed cyclingSpeed; + + public SensorDataSet() { } - 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 void fillTrackPoint(TrackPoint trackPoint) { + if (heartRate != null) { + trackPoint.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()) { + trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm()); + } - public boolean hasBatteryLevel() { - return !Float.isNaN(batteryLevel); - } - - public float getBatteryLevel() { - return batteryLevel; - } - - public String getSensorName() { - return sensorName; - } - - public String getSensorAddress() { - return sensorAddress; + if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) { + trackPoint.setSpeed(cyclingSpeed.getSpeed_mps()); + } } @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/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 647d44390..6d1110caa 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatsFragment.java @@ -43,7 +43,6 @@ 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 +113,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 +164,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 +275,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 +435,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(); @@ -435,33 +444,65 @@ 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()); + setSpeedSensorData(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) { + 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); + } + } + + 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()); + } + } } } @@ -568,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 @@ -603,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/io/file/exporter/KmlTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java index 4ed5314f7..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 @@ -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; @@ -31,7 +30,6 @@ import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.TrackPoint; 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 +71,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,17 +267,14 @@ public class KmlTrackWriter implements TrackWriter { printWriter.println("" + getCoordinates(trackPoint.getLocation(), " ") + ""); if (exportSensorData) { - SensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); - if (sensorDataSet != null) { - if (sensorDataSet.hasHeartRate()) { - heartRateList.add(sensorDataSet.getHeartRate()); - } - if (sensorDataSet.hasCadence()) { - cadenceList.add(sensorDataSet.getCadence()); - } - 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()); } } } @@ -453,7 +448,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..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 @@ -28,7 +28,6 @@ import java.util.ArrayList; import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter; /** @@ -223,30 +222,17 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { for (int i = 0; i < trackPoints.size(); i++) { TrackPoint trackPoint = trackPoints.get(i); - boolean hasSensorData = false; - float heartrate = SensorDataSet.DATA_UNAVAILABLE; - float cadence = SensorDataSet.DATA_UNAVAILABLE; - float power = SensorDataSet.DATA_UNAVAILABLE; - if (i < heartRateList.size()) { - heartrate = heartRateList.get(i); - hasSensorData = true; + trackPoint.setHeartRate_bpm(heartRateList.get(i)); } if (i < cadenceList.size()) { - cadence = cadenceList.get(i); - hasSensorData = true; + trackPoint.setCyclingCadence_rpm(cadenceList.get(i)); } if (i < powerList.size()) { - power = powerList.get(i); - hasSensorData = true; + trackPoint.setPower(cadenceList.get(i)); } - if (!hasSensorData) { - insertTrackPoint(trackPoint); - } else { - TrackPoint sensorDataSetLocation = new TrackPoint(trackPoint, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime())); - insertTrackPoint(sensorDataSetLocation); - } + 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..82b5265b6 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -575,7 +575,9 @@ public class TrackRecordingService extends Service { return; } - TrackPoint trackPoint = new TrackPoint(location, getSensorDataSet()); + TrackPoint trackPoint = new TrackPoint(location); + fillWithSensorDataSet(trackPoint); + notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy); if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) { @@ -626,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; @@ -708,10 +710,18 @@ 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(); + } + + void fillWithSensorDataSet(TrackPoint trackPoint) { + SensorDataSet sensorData = getSensorDataSet(); + if (sensorData != null) { + sensorData.fillTrackPoint(trackPoint); + } } 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 98913593c..fbd4c4240 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothConnectionManager.java @@ -24,40 +24,30 @@ 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; 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 { - - // 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); +public abstract class BluetoothConnectionManager { private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); - private final Context context; - private final Handler handler; - - 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 @@ -65,86 +55,70 @@ public class BluetoothConnectionManager { switch (newState) { case BluetoothProfile.STATE_CONNECTING: Log.d(TAG, "Connecting to sensor: " + gatt.getDevice()); - setState(SensorState.CONNECTING); - - handler.obtainMessage(MESSAGE_CONNECTING, gatt.getDevice().getName()).sendToTarget(); 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(); 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); - - handler.obtainMessage(MESSAGE_DISCONNECTED, gatt.getDevice().getName()).sendToTarget(); } } @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); - SensorDataSet sensorDataSet = new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress()); - handler.obtainMessage(MESSAGE_READ, sensorDataSet).sendToTarget(); + SensorData sensorData = parsePayload(sensorName, gatt.getDevice().getAddress(), characteristic); + if (sensorData != null) { + observer.onChanged(sensorData); + } } }; - /** - * Constructor. - * - * @param handler a handler for sending messages back to the UI activity - */ - BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, @NonNull Handler handler) { - this.context = context; - this.bluetoothDevice = bluetoothDevice; - this.handler = handler; - 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; @@ -153,15 +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 { + + CyclingSpeed(SensorDataObserver observer) { + super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer); + } + + @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; + } + } + + interface SensorDataObserver { + + void onChanged(SensorData sensorData); + } } diff --git a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java index 1a1779335..cbffa48f1 100644 --- a/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/BluetoothRemoteSensorManager.java @@ -20,90 +20,71 @@ 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; 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 { +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; - // 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 boolean started = false; + + 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(); + 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)) { - stop(); - return; - } - String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context); - if (bluetoothConnectionManager.isSameBluetoothDevice(address)) { - return; - } + if (!started) return; - disconnect(); - startCurrentSensor(); + if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) { + String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context); + connect(heartRate, address); + } + + if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) { + 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); } } }; - private SensorDataSet sensorDataSet = null; - private BluetoothConnectionManager bluetoothConnectionManager; - /** * @param context the context */ @@ -113,67 +94,76 @@ public class BluetoothRemoteSensorManager { } 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; + Log.e(TAG, "Unable to get remote device for: " + address, e); } - - disconnect(); - - bluetoothConnectionManager = new BluetoothConnectionManager(context, device, messageHandler); - bluetoothConnectionManager.connect(); } - private void disconnect() { - if (bluetoothConnectionManager != null) { - bluetoothConnectionManager.disconnect(); - bluetoothConnectionManager = null; + public SensorDataSet getSensorData() { + return sensorDataSet; + } + + @Override + 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(), PreferencesUtils.getWheelCircumference(context)); + } + + 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..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) { @@ -215,20 +215,44 @@ 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 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) { 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/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/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..8ec4230e1 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -16,8 +16,25 @@ settingsReset bluetoothSensor + bluetoothCyclingCadenceSensor + 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 d86b8dffe..48486a545 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -54,8 +54,8 @@ limitations under the License. - + + +