diff --git a/build.gradle b/build.gradle index 89c375594..0b84b40c8 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ android { buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" - minSdk 24 + minSdk 26 targetSdk 34 testInstrumentationRunner "de.dennisguse.opentracks.TestRunner" diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java index 7065be17d..97b61dda0 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java @@ -48,6 +48,7 @@ import de.dennisguse.opentracks.TimezoneRule; import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.ActivityType; +import de.dennisguse.opentracks.data.models.AltitudeGainLoss; import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.HeartRate; @@ -58,11 +59,12 @@ import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.exporter.TrackExporter; -import de.dennisguse.opentracks.sensors.AltitudeSumManager; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate; +import de.dennisguse.opentracks.sensors.sensorData.Aggregator; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.handlers.TrackPointCreator; @@ -71,14 +73,12 @@ import de.dennisguse.opentracks.stats.TrackStatistics; /** * Export a track to {@link TrackFileFormat} and verify that the import is identical. *

- * Note: those tests are affected by {@link de.dennisguse.opentracks.sensors.sensorData.SensorData}.isRecent(). + * Note: those tests are affected by {@link Aggregator}.isRecent(). * If the test device is too slow (like in a CI) these are likely to fail as the sensor data will be omitted from actual. */ @RunWith(AndroidJUnit4.class) public class ExportImportTest { - private static final String TAG = ExportImportTest.class.getSimpleName(); - @Rule public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS); @@ -139,23 +139,22 @@ public class ExportImportTest { Distance sensorDistance = Distance.of(10); // recording distance interval - sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1); + sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1f); service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null); // A sensor-only TrackPoint trackPointCreator.setClock("2020-02-02T02:02:04Z"); - mockAltitudeChange(trackPointCreator, 1); - mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f); + mockSensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f, 1f); trackPointCreator.setClock("2020-02-02T02:02:14Z"); - mockBLESensorData(trackPointCreator, 15f, null, 67f, 3f, 50f); + mockSensorData(trackPointCreator, 15f, null, 67f, 3f, 50f, null); trackPointCreator.setClock("2020-02-02T02:02:15Z"); - mockBLESensorData(trackPointCreator, null, null, 68f, 3f, 50f); + mockSensorData(trackPointCreator, null, null, 68f, 3f, 50f, null); trackPointCreator.setClock("2020-02-02T02:02:16Z"); - mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f); // Distance will be added to next TrackPoint + mockSensorData(trackPointCreator, 5f, Distance.of(2), 69f, 3f, 50f, null); // Distance will be added to next TrackPoint - sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0); + sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0f); service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null); trackPointCreator.setClock("2020-02-02T02:02:18Z"); @@ -165,15 +164,16 @@ public class ExportImportTest { trackPointCreator.setClock("2020-02-02T02:03:20Z"); service.resumeTrack(trackId); - sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0); + sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0f); - sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0); + sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0f); trackPointCreator.setClock("2020-02-02T02:03:30Z"); service.getTrackRecordingManager().onIdle(); - sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0); + sendLocation(trackPointCreator, "2020-02-02T02:03:50Z", 3, 16.001, 10, 27, 15, 10, 0f); + trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet(); trackPointCreator.setClock("2020-02-02T02:04:00Z"); service.endCurrentTrack(); @@ -271,7 +271,9 @@ public class ExportImportTest { .setAltitudeLoss(0f) .setAltitudeGain(0f) .setHorizontalAccuracy(Distance.of(10)), - new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z")), + new TrackPoint(TrackPoint.Type.IDLE, Instant.parse("2020-02-02T02:03:30Z")) + .setAltitudeLoss(0f) + .setAltitudeGain(0f), new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:50Z")) .setLatitude(3) .setLongitude(16.001) @@ -281,7 +283,6 @@ public class ExportImportTest { .setAltitudeGain(0f) .setHorizontalAccuracy(Distance.of(10)), new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse("2020-02-02T02:04:00Z")) - ), actual); } @@ -545,40 +546,51 @@ public class ExportImportTest { } } - private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) { + private void mockSensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power, Float altitudeGain) { + SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet; - SensorDataSet sensorDataSet = new SensorDataSet(); - sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power))); - sensorDataSet.set(new SensorDataHeartRate("heartRate", "heartRate", HeartRate.of(heartRate))); + AggregatorCyclingPower cyclingPower = Mockito.mock(AggregatorCyclingPower.class); + Mockito.when(cyclingPower.hasValue()).thenReturn(true); + Mockito.when(cyclingPower.getValue()).thenReturn(Power.of(power)); + sensorDataSet.add(cyclingPower); - SensorDataCyclingCadence cyclingCadence = Mockito.mock(SensorDataCyclingCadence.class); + AggregatorHeartRate agheartRate = Mockito.mock(AggregatorHeartRate.class); + Mockito.when(agheartRate.getValue()).thenReturn(HeartRate.of(heartRate)); + sensorDataSet.add(agheartRate); + + AggregatorCyclingCadence cyclingCadence = Mockito.mock(AggregatorCyclingCadence.class); Mockito.when(cyclingCadence.hasValue()).thenReturn(true); Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence)); - sensorDataSet.set(cyclingCadence); + sensorDataSet.add(cyclingCadence); if (distance != null && speed != null) { - SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class); - Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance); - Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed)); - SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class); + AggregatorCyclingDistanceSpeed distanceSpeed = Mockito.mock(AggregatorCyclingDistanceSpeed.class); Mockito.when(distanceSpeed.hasValue()).thenReturn(true); - Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData); - sensorDataSet.set(distanceSpeed); + Mockito.when(distanceSpeed.getValue()).thenReturn(new AggregatorCyclingDistanceSpeed.Data(null, distance, Speed.of(speed))); + sensorDataSet.add(distanceSpeed); + } else { + sensorDataSet.add(new AggregatorCyclingDistanceSpeed("", "")); } - trackPointCreator.getSensorManager().sensorDataSet = sensorDataSet; + mockAltitudeChange(trackPointCreator, altitudeGain); + trackPointCreator.onChange(sensorDataSet); - - //TODO Might require: getSensorManager().sensorDataSet = new SensorDataSet() } - private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) { - AltitudeSumManager altitudeSumManager = trackPointCreator.getSensorManager().getAltitudeSumManager(); - altitudeSumManager.setAltitudeGain_m(altitudeGain); - altitudeSumManager.setAltitudeLoss_m(altitudeGain); + private void mockAltitudeChange(TrackPointCreator trackPointCreator, Float altitudeGain) { + SensorDataSet sensorDataSet = trackPointCreator.getSensorManager().sensorDataSet; + + if (altitudeGain != null) { + AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class); + Mockito.when(barometer.hasValue()).thenReturn(true); + Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain)); + sensorDataSet.add(barometer); + } else { + sensorDataSet.add(new AggregatorBarometer("")); + } } - private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, float altitudeGain) { + private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, Float altitudeGain) { Location location = new Location("mock"); location.setLatitude(latitude); location.setLongitude(longitude); diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/AltitudeSumManagerTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/AltitudeSumManagerTest.java deleted file mode 100644 index 557d3ff71..000000000 --- a/src/androidTest/java/de/dennisguse/opentracks/sensors/AltitudeSumManagerTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import static org.mockito.Mockito.when; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import de.dennisguse.opentracks.data.models.AtmosphericPressure; -import de.dennisguse.opentracks.sensors.driver.BarometerInternal; - -/** - * NOTE: Test data is completely artificial. - */ -@RunWith(MockitoJUnitRunner.class) -public class AltitudeSumManagerTest { - - private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) { - for (float f : values) { - altitudeSumManager.onSensorValueChanged(AtmosphericPressure.ofHPA(f)); - } - } - - @Mock - BarometerInternal driver; - - @Test - public void getAltitudeGainLoss_downhill() { - // given - AltitudeSumManager subject = new AltitudeSumManager(driver); - - when(driver.isConnected()).thenReturn(true); - - // then - addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f}); - - // then - Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01); - Assert.assertEquals(15f, subject.getAltitudeLoss_m(), 0.01); - } - - @Test - public void sensorUnavailable() { - // given - AltitudeSumManager subject = new AltitudeSumManager(driver); - - when(driver.isConnected()).thenReturn(true); - - // then - subject.onSensorValueChanged(AtmosphericPressure.ofHPA(999f)); - - // then - Assert.assertNull(subject.getAltitudeGain_m()); - Assert.assertNull(subject.getAltitudeLoss_m()); - } -} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressureTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressureTest.java new file mode 100644 index 000000000..03f513ff6 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressureTest.java @@ -0,0 +1,25 @@ +package de.dennisguse.opentracks.sensors; + +import static org.junit.Assert.assertEquals; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.junit.Test; + +import de.dennisguse.opentracks.data.models.AtmosphericPressure; + +public class BluetoothHandlerBarometricPressureTest { + + @Test + public void parseEnvironmentalSensing_Pa() { + // given + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00}); + + // when + AtmosphericPressure pressure = BluetoothHandlerBarometricPressure.parseEnvironmentalSensing(characteristic); + + // then + assertEquals(AtmosphericPressure.ofPA(100165), pressure); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingDistanceSpeedTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingDistanceSpeedTest.java new file mode 100644 index 000000000..58bbbfacd --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingDistanceSpeedTest.java @@ -0,0 +1,51 @@ +package de.dennisguse.opentracks.sensors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Pair; + +import org.junit.Test; + +public class BluetoothHandlerCyclingDistanceSpeedTest { + @Test + public void parseCyclingSpeedCadence_crankOnly() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99}); + + // when + Pair sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + assertNull(sensor.first); + assertEquals(200, sensor.second.crankRevolutionsCount()); + } + + @Test + public void parseCyclingSpeedCadence_wheelOnly() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99}); + + // when + Pair sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + assertEquals(65535 + 16777216, sensor.first.wheelRevolutionsCount()); + assertNull(sensor.second); + } + + @Test + public void parseCyclingSpeedCadence_crankWheel() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99}); + + // when + Pair sensor = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + assertEquals(200 + 16777216, sensor.first.wheelRevolutionsCount()); + assertEquals(225, sensor.second.crankRevolutionsCount()); + } + +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingPowerTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingPowerTest.java new file mode 100644 index 000000000..2f4d56e14 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingPowerTest.java @@ -0,0 +1,38 @@ +package de.dennisguse.opentracks.sensors; + +import static org.junit.Assert.assertEquals; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.junit.Test; + +public class BluetoothHandlerCyclingPowerTest { + + @Test + public void parseCyclingPower_power() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0, 0, 40, 0}); + + // when + BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic); + + // then + assertEquals(40, powerCadence.power().getW(), 0.01); + } + + @Test + public void parseCyclingPower_power_with_cadence() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42}); + + // when + BluetoothHandlerManagerCyclingPower.Data powerCadence = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic); + + // then + assertEquals(0, powerCadence.power().getW(), 0.01); + + assertEquals(12, powerCadence.crank().crankRevolutionsCount()); + assertEquals(17125, powerCadence.crank().crankRevolutionsTime()); + } + +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerHeartRateTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerHeartRateTest.java new file mode 100644 index 000000000..55fecb1d5 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerHeartRateTest.java @@ -0,0 +1,38 @@ +package de.dennisguse.opentracks.sensors; + +import static org.junit.Assert.assertEquals; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.junit.Test; + +import de.dennisguse.opentracks.data.models.HeartRate; + +public class BluetoothHandlerManagerHeartRateTest { + + @Test + public void parseHeartRate_uint8() { + // given + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x02, 0x3C}); + + // when + HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic); + + // then + assertEquals(HeartRate.of(60), heartRate); + } + + @Test + public void parseHeartRate_uint16() { + // given + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x01, 0x01, 0x01}); + + // when + HeartRate heartRate = BluetoothHandlerManagerHeartRate.parseHeartRate(characteristic); + + // then + assertEquals(HeartRate.of(257), heartRate); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerRunningSpeedAndCadenceTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerRunningSpeedAndCadenceTest.java new file mode 100644 index 000000000..7ca2507e3 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerRunningSpeedAndCadenceTest.java @@ -0,0 +1,28 @@ +package de.dennisguse.opentracks.sensors; + +import static org.junit.Assert.assertEquals; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.junit.Test; + +import de.dennisguse.opentracks.data.models.Cadence; +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Speed; + +public class BluetoothHandlerRunningSpeedAndCadenceTest { + + @Test + public void parseRunningSpeedAndCadence_with_distance() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1}); + + // when + BluetoothHandlerRunningSpeedAndCadence.Data sensor = BluetoothHandlerRunningSpeedAndCadence.parseRunningSpeedAndCadence("sensorName", characteristic); + + // then + assertEquals(Speed.of(5), sensor.speed()); + assertEquals(Cadence.of(80), sensor.cadence()); + assertEquals(Distance.of(6553.5 + 1677721.6), sensor.totalDistance()); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothUtilsTest.java deleted file mode 100644 index 92dc59f21..000000000 --- a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothUtilsTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import android.bluetooth.BluetoothGattCharacteristic; - -import org.junit.Test; - -import de.dennisguse.opentracks.data.models.AtmosphericPressure; -import de.dennisguse.opentracks.data.models.Cadence; -import de.dennisguse.opentracks.data.models.Distance; -import de.dennisguse.opentracks.data.models.HeartRate; -import de.dennisguse.opentracks.data.models.Speed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning; - -public class BluetoothUtilsTest { - - @Test - public void parseHeartRate_uint8() { - // given - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{0x02, 0x3C}); - - // when - HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic); - - // then - assertEquals(HeartRate.of(60), heartRate); - } - - @Test - public void parseHeartRate_uint16() { - // given - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEARTRATE.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{0x01, 0x01, 0x01}); - - // when - HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic); - - // then - assertEquals(HeartRate.of(257), heartRate); - } - - @Test - public void parseEnvironmentalSensing_Pa() { - // given - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.BAROMETRIC_PRESSURE.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{(byte) 0xB2, (byte) 0x48, (byte) 0x0F, (byte) 0x00}); - - // when - AtmosphericPressure pressure = BluetoothUtils.parseEnvironmentalSensing(characteristic); - - // then - assertEquals(AtmosphericPressure.ofPA(100165), pressure); - } - - @Test - public void parseCyclingSpeedCadence_crankOnly() { - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99}); - - // when - SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic); - - // then - assertNull(sensor.getDistanceSpeed()); - assertEquals(200, sensor.getCadence().getCrankRevolutionsCount()); - } - - @Test - public void parseCyclingSpeedCadence_wheelOnly() { - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99}); - - // when - SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic); - - // then - assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount()); - assertNull(sensor.getCadence()); - } - - @Test - public void parseCyclingSpeedCadence_crankWheel() { - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x01, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99}); - - // when - SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic); - - // then - assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount()); - assertEquals(225, sensor.getCadence().getCrankRevolutionsCount()); - } - - @Test - public void parseCyclingPower_power() { - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{0, 0, 40, 0}); - - // when - SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic); - - // then - assertEquals(40, powerCadence.power().getValue().getW(), 0.01); - } - - @Test - public void parseCyclingPower_power_with_cadence() { - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{0x2C, 0x00, 0x00, 0x00, (byte) 0x9F, 0x00, 0x0C, 0x00, (byte) 0xE5, 0x42}); - - // when - SensorDataCyclingPower.Data powerCadence = BluetoothUtils.parseCyclingPower("", "", characteristic); - - // then - assertEquals(0, powerCadence.power().getValue().getW(), 0.01); - - assertEquals(12, powerCadence.cadence().getCrankRevolutionsCount()); - assertEquals(17125, powerCadence.cadence().getCrankRevolutionsTime()); - } - - @Test - public void parseRunningSpeedAndCadence_with_distance() { - BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0); - characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1}); - - // when - SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic); - - // then - assertEquals(Speed.of(5), sensor.getSpeed()); - assertEquals(Cadence.of(80), sensor.getCadence()); - assertEquals(Distance.of(6553.5 + 1677721.6), sensor.getTotalDistance()); - } -} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometerTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometerTest.java new file mode 100644 index 000000000..5ef2b51c6 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometerTest.java @@ -0,0 +1,28 @@ +package de.dennisguse.opentracks.sensors.sensorData; + +import org.junit.Assert; +import org.junit.Test; + +import de.dennisguse.opentracks.data.models.AtmosphericPressure; + +public class AggregatorBarometerTest { + + private static void addSensorValue(AggregatorBarometer aggregatorBarometer, float[] values) { + for (float f : values) { + aggregatorBarometer.add(new Raw<>(AtmosphericPressure.ofHPA(f))); + } + } + + @Test + public void getAltitudeGainLoss_downhill() { + // given + AggregatorBarometer subject = new AggregatorBarometer(""); + + // then + addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f}); + + // then + Assert.assertEquals(0f, subject.value.gain_m(), 0.01); + Assert.assertEquals(15f, subject.value.loss_m(), 0.01); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingTest.java index 4dd5ca5ba..ca3e18949 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingTest.java @@ -2,17 +2,17 @@ package de.dennisguse.opentracks.sensors.sensorData; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed; import de.dennisguse.opentracks.sensors.UintUtils; @RunWith(AndroidJUnit4.class) @@ -20,12 +20,11 @@ public class SensorDataCyclingTest { @Test public void compute_cadence_1() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); // 1s - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 2048); // 2s + AggregatorCyclingCadence current = new AggregatorCyclingCadence("", ""); // when - current.compute(previous); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024))); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 2048))); // then assertEquals(60, current.getValue().getRPM(), 0.01); @@ -33,12 +32,11 @@ public class SensorDataCyclingTest { @Test public void compute_cadence_2() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 6184); - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 8016); + AggregatorCyclingCadence current = new AggregatorCyclingCadence("", ""); // when - current.compute(previous); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 6184))); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 8016))); // then assertEquals(33.53, current.getValue().getRPM(), 0.01); @@ -46,12 +44,11 @@ public class SensorDataCyclingTest { @Test public void compute_cadence_sameCount() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 2048); + AggregatorCyclingCadence current = new AggregatorCyclingCadence("", ""); // when - current.compute(previous); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024))); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 2048))); // then assertEquals(Cadence.of(0), current.getValue()); @@ -60,126 +57,72 @@ public class SensorDataCyclingTest { @Test public void compute_cadence_sameTime() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, 1024); - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 1024); + AggregatorCyclingCadence current = new AggregatorCyclingCadence("", ""); // when - current.compute(previous); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, 1024))); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 1024))); // then - assertFalse(current.hasValue()); + assertFalse(current.hasValue()); //TODO Cadence should be 0? } @Test public void compute_cadence_rollOverTime() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024); - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 2, 0); + AggregatorCyclingCadence current = new AggregatorCyclingCadence("", ""); // when - current.compute(previous); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(1, UintUtils.UINT16_MAX - 1024))); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(2, 0))); // then assertEquals(60, current.getValue().getRPM(), 0.01); } - @Ignore("Disabled from #953") @Test @Deprecated public void compute_cadence_rollOverCount() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024); - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048); + AggregatorCyclingCadence current = new AggregatorCyclingCadence("", ""); // when - current.compute(previous); - - // then - assertEquals(60, current.getValue().getRPM(), 0.01); - } - - @Test - public void compute_cadence_overflow() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024); - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048); - - // when - current.compute(previous); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(UintUtils.UINT32_MAX - 1, 1024))); + current.add(new Raw<>(new BluetoothHandlerCyclingCadence.CrankData(0, 2048))); // then + // TODO See #953 +// assertEquals(60, current.getValue().getRPM(), 0.01); assertNull(current.getValue()); } @Test public void compute_speed() { - // given - SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 1, 6184); - SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 2, 8016); + AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", ""); + current.setWheelCircumference(Distance.ofMM(2150)); // when - current.compute(previous, Distance.ofMM(2150)); + current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(1, 6184))); + current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(2, 8016))); // then - assertEquals(2.15, current.getValue().getDistance().toM(), 0.01); - assertEquals(1.20, current.getValue().getSpeed().toMPS(), 0.01); + assertEquals(2.15, current.getValue().distance().toM(), 0.01); + assertEquals(1.20, current.getValue().speed().toMPS(), 0.01); } - @Ignore("Disabled from #953") @Test @Deprecated public void compute_speed_rollOverCount() { - // given - SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024); - SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048); + AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", ""); + current.setWheelCircumference(Distance.ofMM(2000)); // when - current.compute(previous, Distance.ofMM(2000)); - - // then - assertEquals(2, current.getValue().getDistance().toM(), 0.01); - assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01); - } - - @Test - public void compute_speed_overflow() { - // given - SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024); - SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048); - - // when - current.compute(previous, Distance.ofMM(2000)); + current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(UintUtils.UINT32_MAX - 1, 1024))); + current.add(new Raw<>(new BluetoothHandlerCyclingDistanceSpeed.WheelData(0, 2048))); + // then + // TODO See #953 +// assertEquals(2, current.getValue().getDistance().toM(), 0.01); +// assertEquals(2, current.getValue().getSpeed().toMPS(), 0.01); assertNull(current.getValue()); } - - @Test - public void equals_speed_with_no_data() { - // given - SensorDataCyclingDistanceSpeed previous = new SensorDataCyclingDistanceSpeed("sensorAddress"); - SensorDataCyclingDistanceSpeed current = new SensorDataCyclingDistanceSpeed("sensorAddress", "sensorName", 0, 2048); - - // when - previous.toString(); - - // then - assertNotEquals(previous, current); - assertNotEquals(previous, previous); - } - - @Test - public void equals_cadence_with_no_data() { - // given - SensorDataCyclingCadence previous = new SensorDataCyclingCadence("sensorAddress"); - SensorDataCyclingCadence current = new SensorDataCyclingCadence("sensorAddress", "sensorName", 0, 2048); - - // when - previous.toString(); - - // then - assertNotEquals(previous, current); - assertNotEquals(previous, previous); - } } \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceRecordingTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceRecordingTest.java index 95e35f88e..82c75ce8e 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceRecordingTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/TrackRecordingServiceRecordingTest.java @@ -7,7 +7,6 @@ import android.content.Context; import android.content.Intent; import android.os.Looper; -import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; @@ -19,6 +18,7 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import java.time.Instant; import java.util.List; @@ -28,24 +28,25 @@ import java.util.concurrent.TimeoutException; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.data.ContentProviderUtils; +import de.dennisguse.opentracks.data.models.AltitudeGainLoss; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.io.file.importer.TrackPointAssert; -import de.dennisguse.opentracks.sensors.AltitudeSumManager; -import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning; +import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence; +import de.dennisguse.opentracks.sensors.SensorManager; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning; +import de.dennisguse.opentracks.sensors.sensorData.Raw; import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.stats.TrackStatistics; /** * Tests insert location. - * Note: on API23, the TrackRecordingService may not be stopped properly before the next test. - * So, if something fails, subsequent tests may be affected. */ @RunWith(AndroidJUnit4.class) public class TrackRecordingServiceRecordingTest { @@ -58,14 +59,6 @@ public class TrackRecordingServiceRecordingTest { private TrackRecordingService service; - private final AltitudeSumManager altitudeSumManager = new AltitudeSumManager() { - @Override - public void fill(@NonNull TrackPoint trackPoint) { - trackPoint.setAltitudeGain(0f); - trackPoint.setAltitudeLoss(0f); - } - }; - @BeforeClass public static void preSetUp() { // Prepare looper for Android's message queue @@ -105,7 +98,8 @@ public class TrackRecordingServiceRecordingTest { String startTime = "2020-02-02T02:02:02Z"; trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + // then assertEquals(new TrackStatistics(startTime, startTime, 0, 0, 0, 0, null, null) @@ -142,7 +136,8 @@ public class TrackRecordingServiceRecordingTest { trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + // when String pauseTime = "2020-02-02T02:02:03Z"; @@ -188,7 +183,8 @@ public class TrackRecordingServiceRecordingTest { String starTime = "2020-02-02T02:02:02Z"; trackPointCreator.setClock(starTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + String pauseTime = "2020-02-02T02:02:03Z"; trackPointCreator.setClock(pauseTime); @@ -220,7 +216,8 @@ public class TrackRecordingServiceRecordingTest { trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + String stopTime = "2020-02-02T02:02:03Z"; trackPointCreator.setClock(stopTime); @@ -230,7 +227,8 @@ public class TrackRecordingServiceRecordingTest { String resumeTime = "2020-02-02T02:02:04Z"; trackPointCreator.setClock(resumeTime); service.resumeTrack(trackId); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + // then new TrackPointAssert().assertEquals(List.of( @@ -251,18 +249,19 @@ public class TrackRecordingServiceRecordingTest { trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); - BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager(); + mockAltitudeChange(trackPointCreator, 0); + SensorManager sensorManager = trackPointCreator.getSensorManager(); + sensorManager.sensorDataSet.add(new AggregatorHeartRate("", "")); // when String sensor1 = "2020-02-02T02:02:03Z"; trackPointCreator.setClock(sensor1); - remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored + sensorManager.onChanged(new Raw<>(HeartRate.of(5))); //Should be ignored String sensor3 = "2020-02-02T02:02:13Z"; trackPointCreator.setClock(sensor3); - remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(7))); + sensorManager.onChanged(new Raw<>(HeartRate.of(7))); String stopTime = "2020-02-02T02:02:15Z"; trackPointCreator.setClock(stopTime); @@ -282,7 +281,6 @@ public class TrackRecordingServiceRecordingTest { ), TestDataUtil.getTrackPoints(contentProviderUtils, trackId)); } - @MediumTest @Test public void testRecording_gpsOnly_recordingDistance_above() { @@ -291,7 +289,7 @@ public class TrackRecordingServiceRecordingTest { TrackPointCreator trackPointCreator = service.getTrackPointCreator(); trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); // when @@ -365,7 +363,8 @@ public class TrackRecordingServiceRecordingTest { TrackPointCreator trackPointCreator = service.getTrackPointCreator(); trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + // when String gps1 = "2020-02-02T02:02:03Z"; @@ -429,7 +428,8 @@ public class TrackRecordingServiceRecordingTest { TrackPointCreator trackPointCreator = service.getTrackPointCreator(); trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + // when String gps1 = "2020-02-02T02:02:03Z"; @@ -479,7 +479,8 @@ public class TrackRecordingServiceRecordingTest { TrackPointCreator trackPointCreator = service.getTrackPointCreator(); trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); + // when String gps1 = "2020-02-02T02:02:03Z"; @@ -525,7 +526,7 @@ public class TrackRecordingServiceRecordingTest { TrackPointCreator trackPointCreator = service.getTrackPointCreator(); trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); + mockAltitudeChange(trackPointCreator, 0); // when String gps1 = "2020-02-02T02:02:03Z"; @@ -581,25 +582,25 @@ public class TrackRecordingServiceRecordingTest { @Test public void testRecording_gpsAndSensor_gpsIdleMoving_sensorMoving() { // TODO Check TrackStatistics - AltitudeSumManager altitudeSumManager = new AltitudeSumManager(); - // given String startTime = "2020-02-02T02:02:02Z"; TrackPointCreator trackPointCreator = service.getTrackPointCreator(); trackPointCreator.setClock(startTime); Track.Id trackId = service.startNewTrack(); - trackPointCreator.getSensorManager().setAltitudeSumManager(altitudeSumManager); - BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getSensorManager().getBluetoothSensorManager(); + + SensorManager sensorManager = trackPointCreator.getSensorManager(); + sensorManager.sensorDataSet.add(new AggregatorRunning("", "")); + sensorManager.sensorDataSet.barometer = null; // when String sensor1 = "2020-02-02T02:02:03Z"; trackPointCreator.setClock(sensor1); - remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(0))); //Should be ignored + sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(0)))); //Should be ignored // when String sensor2 = "2020-02-02T02:02:04Z"; trackPointCreator.setClock(sensor2); - remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(2))); + sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(2)))); // when String gps1 = "2020-02-02T02:02:05Z"; @@ -608,12 +609,12 @@ public class TrackRecordingServiceRecordingTest { // when String sensor3 = "2020-02-02T02:02:06Z"; trackPointCreator.setClock(sensor3); - remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(12))); + sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(12)))); // when String sensor4 = "2020-02-02T02:02:07Z"; trackPointCreator.setClock(sensor4); - remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(14))); //Should be ignored + sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(14)))); //Should be ignored // when String gps2 = "2020-02-02T02:02:08Z"; @@ -622,7 +623,7 @@ public class TrackRecordingServiceRecordingTest { // when String sensor5 = "2020-02-02T02:02:10Z"; trackPointCreator.setClock(sensor5); - remoteSensorManager.onChanged(new SensorDataRunning("", "", Speed.of(5), null, Distance.of(16))); //Should be ignored + sensorManager.onChanged(new Raw<>(new BluetoothHandlerRunningSpeedAndCadence.Data(Speed.of(5), null, Distance.of(16)))); //Should be ignored // when String gps3 = "2020-02-02T02:02:12Z"; @@ -671,4 +672,12 @@ public class TrackRecordingServiceRecordingTest { .setSensorDistance(Distance.of(0)) ), TestDataUtil.getTrackPoints(contentProviderUtils, trackId)); } + + private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) { + AggregatorBarometer barometer = Mockito.mock(AggregatorBarometer.class); + Mockito.when(barometer.hasValue()).thenReturn(true); + Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain)); + + trackPointCreator.getSensorManager().sensorDataSet.barometer = barometer; + } } diff --git a/src/androidTest/res/raw/csv_export.csv b/src/androidTest/res/raw/csv_export.csv index cdefb81a1..f4344cd20 100644 --- a/src/androidTest/res/raw/csv_export.csv +++ b/src/androidTest/res/raw/csv_export.csv @@ -8,6 +8,6 @@ "2020-02-02T03:03:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,, "2020-02-02T03:03:21+01:00","TRACKPOINT",3,14.002,10,10,,54,0,0,,,, "2020-02-02T03:03:22+01:00","SEGMENT_START_AUTOMATIC",3,16,10,10,,54,0,0,,,, -"2020-02-02T03:03:30+01:00","IDLE",,,,,,,,,,,, +"2020-02-02T03:03:30+01:00","IDLE",,,,,,,0,0,,,, "2020-02-02T03:03:50+01:00","TRACKPOINT",3,16.001,10,10,,54,0,0,,,, "2020-02-02T03:04:00+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,, \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/data/models/AltitudeGainLoss.java b/src/main/java/de/dennisguse/opentracks/data/models/AltitudeGainLoss.java new file mode 100644 index 000000000..a33047393 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/data/models/AltitudeGainLoss.java @@ -0,0 +1,3 @@ +package de.dennisguse.opentracks.data.models; + +public record AltitudeGainLoss(float gain_m, float loss_m) {} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/AltitudeSumManager.java b/src/main/java/de/dennisguse/opentracks/sensors/AltitudeSumManager.java deleted file mode 100644 index 63587e033..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/AltitudeSumManager.java +++ /dev/null @@ -1,108 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import android.content.Context; -import android.os.Handler; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import de.dennisguse.opentracks.data.models.AtmosphericPressure; -import de.dennisguse.opentracks.data.models.TrackPoint; -import de.dennisguse.opentracks.sensors.driver.BarometerInternal; - -/** - * Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer). - */ -public class AltitudeSumManager implements SensorConnector { - - private static final String TAG = AltitudeSumManager.class.getSimpleName(); - - private final BarometerInternal driver; - - private AtmosphericPressure lastAcceptedSensorValue; - - private AtmosphericPressure lastSeenSensorValue; - - private Float altitudeGain_m; - private Float altitudeLoss_m; - - public AltitudeSumManager() { - driver = new BarometerInternal(); - } - - @VisibleForTesting - public AltitudeSumManager(BarometerInternal mock) { - this.driver = mock; - } - - public void start(Context context, Handler handler) { - driver.connect(context, handler, this); - - lastAcceptedSensorValue = null; - reset(); - } - - public void stop(Context context) { - Log.d(TAG, "Stop"); - - driver.disconnect(context); - reset(); - } - - public void fill(@NonNull TrackPoint trackPoint) { - trackPoint.setAltitudeGain(altitudeGain_m); - trackPoint.setAltitudeLoss(altitudeLoss_m); - } - - @Nullable - public Float getAltitudeGain_m() { - return driver.isConnected() ? altitudeGain_m : null; - } - - @VisibleForTesting - public void setAltitudeGain_m(float altitudeGain_m) { - this.altitudeGain_m = altitudeGain_m; - } - - @Nullable - public Float getAltitudeLoss_m() { - return driver.isConnected() ? altitudeLoss_m : null; - } - - @VisibleForTesting - public void setAltitudeLoss_m(float altitudeLoss_m) { - this.altitudeLoss_m = altitudeLoss_m; - } - - public void reset() { - Log.d(TAG, "Reset"); - altitudeGain_m = null; - altitudeLoss_m = null; - } - - public void onSensorValueChanged(AtmosphericPressure currentSensorValue) { - if (lastAcceptedSensorValue == null) { - lastAcceptedSensorValue = currentSensorValue; - lastSeenSensorValue = currentSensorValue; - return; - } - - altitudeGain_m = altitudeGain_m != null ? altitudeGain_m : 0; - altitudeLoss_m = altitudeLoss_m != null ? altitudeLoss_m : 0; - - PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, lastSeenSensorValue, currentSensorValue); - if (altitudeChange != null) { - altitudeGain_m += altitudeChange.getAltitudeGain_m(); - - altitudeLoss_m += altitudeChange.getAltitudeLoss_m(); - - lastAcceptedSensorValue = altitudeChange.currentSensorValue(); - } - - lastSeenSensorValue = currentSensorValue; - - Log.v(TAG, "altitude gain: " + altitudeGain_m + ", altitude loss: " + altitudeLoss_m); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/AbstractBluetoothConnectionManager.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManager.java similarity index 66% rename from src/main/java/de/dennisguse/opentracks/sensors/AbstractBluetoothConnectionManager.java rename to src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManager.java index 7a7185b68..bbb483d15 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/AbstractBluetoothConnectionManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManager.java @@ -16,6 +16,7 @@ package de.dennisguse.opentracks.sensors; +import android.annotation.SuppressLint; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; @@ -24,29 +25,27 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; import android.content.Context; -import android.os.Build; import android.os.Handler; import android.util.Log; import androidx.annotation.NonNull; -import java.util.List; import java.util.Optional; import java.util.UUID; -import de.dennisguse.opentracks.sensors.sensorData.SensorData; +import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; /** * Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications. - * Also parses the transferred data into {@link SensorDataObserver}. */ -public abstract class AbstractBluetoothConnectionManager { +@SuppressLint("MissingPermission") +public class BluetoothConnectionManager { - private static final String TAG = AbstractBluetoothConnectionManager.class.getSimpleName(); + private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); - private final SensorDataObserver observer; + private final SensorManager.SensorDataChangedObserver observer; - private final List serviceMeasurementUUIDs; + private final SensorHandlerInterface sensorHandler; private BluetoothGatt bluetoothGatt; private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() { @@ -76,7 +75,7 @@ public abstract class AbstractBluetoothConnectionManager { public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) { BluetoothGattService gattService = null; ServiceMeasurementUUID serviceMeasurement = null; - for (ServiceMeasurementUUID s : serviceMeasurementUUIDs) { + for (ServiceMeasurementUUID s : sensorHandler.getServices()) { gattService = gatt.getService(s.serviceUUID()); if (gattService != null) { serviceMeasurement = s; @@ -112,37 +111,25 @@ public abstract class AbstractBluetoothConnectionManager { public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) { UUID serviceUUID = characteristic.getService().getUuid(); Log.d(TAG, "Received data from " + gatt.getDevice().getAddress() + " with service " + serviceUUID + " and characteristics " + characteristic.getUuid()); - Optional serviceMeasurementUUID = serviceMeasurementUUIDs.stream() - .filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid())).findFirst(); + Optional serviceMeasurementUUID = sensorHandler.getServices() + .stream() + .filter(s -> s.serviceUUID().equals(characteristic.getService().getUuid())) + .findFirst(); if (serviceMeasurementUUID.isEmpty()) { Log.e(TAG, "Unknown service UUID; not supported?"); return; } - SensorData sensorData = parsePayload(serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic); - if (sensorData != null) { - Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - observer.onChanged(sensorData); - } else { - //TODO This might lead to NPEs in case of race conditions due to shutdown. - observer.getHandler().post(() -> observer.onChanged(sensorData)); - } - } + sensorHandler.handlePayload(observer, serviceMeasurementUUID.get(), gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic); } }; - AbstractBluetoothConnectionManager(ServiceMeasurementUUID serviceUUUID, SensorDataObserver observer) { - this.serviceMeasurementUUIDs = List.of(serviceUUUID); + BluetoothConnectionManager(SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) { this.observer = observer; + this.sensorHandler = sensorHandler; } - AbstractBluetoothConnectionManager(List serviceUUUID, SensorDataObserver observer) { - this.serviceMeasurementUUIDs = serviceUUUID; - this.observer = observer; - } - - synchronized void connect(Context context, @NonNull BluetoothDevice device) { + synchronized void connect(Context context, Handler handler, @NonNull BluetoothDevice device) { if (bluetoothGatt != null) { Log.w(TAG, "Already connected; ignoring."); return; @@ -150,20 +137,15 @@ public abstract class AbstractBluetoothConnectionManager { Log.d(TAG, "Connecting to: " + device); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler()); - } else { - bluetoothGatt = device.connectGatt(context, false, connectCallback); - } - SensorData sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress()); - observer.onChanged(sensorData); + bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler); + + observer.onConnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress())); } private synchronized void clearData() { - observer.onDisconnecting(createEmptySensorData(bluetoothGatt.getDevice().getAddress())); + observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress())); } - synchronized void disconnect() { if (bluetoothGatt == null) { Log.w(TAG, "Cannot disconnect if not connected."); @@ -181,22 +163,4 @@ public abstract class AbstractBluetoothConnectionManager { return address.equals(bluetoothGatt.getDevice().getAddress()); } - - protected abstract SensorData createEmptySensorData(String address); - - /** - * @return null if data could not be parsed. - */ - protected abstract SensorData parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, @NonNull BluetoothGattCharacteristic characteristic); - - @Deprecated //TODO REMOVE - interface SensorDataObserver { - - void onChanged(SensorData sensorData); - - void onDisconnecting(SensorData sensorData); - - @NonNull - Handler getHandler(); - } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingCadence.java deleted file mode 100644 index 58980afa7..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingCadence.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import android.bluetooth.BluetoothGattCharacteristic; -import android.util.Log; - -import de.dennisguse.opentracks.data.models.Cadence; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; - -public class BluetoothConnectionManagerCyclingCadence extends AbstractBluetoothConnectionManager { - - private static final String TAG = BluetoothConnectionManagerCyclingCadence.class.getSimpleName(); - - BluetoothConnectionManagerCyclingCadence(SensorDataObserver observer) { - super(BluetoothUtils.CYCLING_CADENCE, observer); - } - - @Override - protected SensorDataCyclingCadence createEmptySensorData(String address) { - return new SensorDataCyclingCadence(address); - } - - @Override - protected SensorDataCyclingCadence parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - - //TODO Implement to ServiceMeasurement.parse()? - if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_POWER)) { - SensorDataCyclingPower.Data data = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic); - if (data!= null) { - return data.cadence(); - } - } else if (serviceMeasurementUUID.equals(BluetoothUtils.CYCLING_SPEED_CADENCE)) { - SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic); - if (cadenceAndSpeed == null) { - return null; - } - - if (cadenceAndSpeed.getCadence() != null) { - return cadenceAndSpeed.getCadence(); - } - } - - Log.e(TAG, "Don't know how to decode this payload."); - return null; - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeed.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeed.java deleted file mode 100644 index 538183ea6..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeed.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import android.bluetooth.BluetoothGattCharacteristic; - -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed; - -public class BluetoothConnectionManagerCyclingDistanceSpeed extends AbstractBluetoothConnectionManager { - - BluetoothConnectionManagerCyclingDistanceSpeed(SensorDataObserver observer) { - super(BluetoothUtils.CYCLING_SPEED_CADENCE, observer); - } - - @Override - protected SensorDataCyclingDistanceSpeed createEmptySensorData(String address) { - return new SensorDataCyclingDistanceSpeed(address); - } - - @Override - protected SensorDataCyclingDistanceSpeed parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic); - if (cadenceAndSpeed == null) { - return null; - } - - if (cadenceAndSpeed.getDistanceSpeed() != null) { - return cadenceAndSpeed.getDistanceSpeed(); - } - - return null; - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPower.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPower.java deleted file mode 100644 index b5b5f8649..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPower.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import android.bluetooth.BluetoothGattCharacteristic; - -import androidx.annotation.NonNull; - -import de.dennisguse.opentracks.data.models.Power; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; - -public class BluetoothConnectionManagerCyclingPower extends AbstractBluetoothConnectionManager { - - BluetoothConnectionManagerCyclingPower(@NonNull SensorDataObserver observer) { - super(BluetoothUtils.CYCLING_POWER, observer); - } - - @Override - protected SensorDataCyclingPower createEmptySensorData(String address) { - return new SensorDataCyclingPower(address); - } - - @Override - protected SensorDataCyclingPower parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - SensorDataCyclingPower.Data cyclingPower = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic); - - return cyclingPower != null ? cyclingPower.power() : null; - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRate.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRate.java deleted file mode 100644 index b8271920b..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRate.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import android.bluetooth.BluetoothGattCharacteristic; - -import androidx.annotation.NonNull; - -import de.dennisguse.opentracks.data.models.HeartRate; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate; - -public class BluetoothConnectionManagerHeartRate extends AbstractBluetoothConnectionManager { - - BluetoothConnectionManagerHeartRate(@NonNull SensorDataObserver observer) { - super(BluetoothUtils.HEARTRATE, observer); - } - - @Override - protected SensorDataHeartRate createEmptySensorData(String address) { - return new SensorDataHeartRate(address); - } - - @Override - protected SensorDataHeartRate parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic); - - return heartRate != null ? new SensorDataHeartRate(address, sensorName, heartRate) : null; - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadence.java deleted file mode 100644 index 42c952628..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadence.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.dennisguse.opentracks.sensors; - -import android.bluetooth.BluetoothGattCharacteristic; - -import androidx.annotation.NonNull; - -import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning; - -public class BluetoothConnectionRunningSpeedAndCadence extends AbstractBluetoothConnectionManager { - - BluetoothConnectionRunningSpeedAndCadence(@NonNull SensorDataObserver observer) { - super(BluetoothUtils.RUNNING_SPEED_CADENCE, observer); - } - - @Override - protected SensorDataRunning createEmptySensorData(String address) { - return new SensorDataRunning(address); - } - - @Override - protected SensorDataRunning parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java new file mode 100644 index 000000000..44adebbc9 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java @@ -0,0 +1,45 @@ +package de.dennisguse.opentracks.sensors; + +import android.bluetooth.BluetoothGattCharacteristic; + +import java.util.List; +import java.util.UUID; + +import de.dennisguse.opentracks.data.models.AtmosphericPressure; +import de.dennisguse.opentracks.sensors.sensorData.Aggregator; +import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; + +public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface { + private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL); + public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID( + ENVIRONMENTAL_SENSING_SERVICE, + new UUID(0x2A6D00001000L, 0x800000805f9b34fbL) + ); + + @Override + public List getServices() { + return List.of(BAROMETRIC_PRESSURE); + } + + @Override + public Aggregator createEmptySensorData(String address) { + return null; //TODO + } + + @Override + public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { + //TODO + } + + public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) { + byte[] raw = characteristic.getValue(); + + if (raw.length < 4) { + return null; + } + + Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0); + return AtmosphericPressure.ofPA(pressure / 10f); + } + +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingCadence.java new file mode 100644 index 000000000..74ad56d33 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingCadence.java @@ -0,0 +1,57 @@ +package de.dennisguse.opentracks.sensors; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Log; +import android.util.Pair; + +import java.util.List; + +import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence; +import de.dennisguse.opentracks.sensors.sensorData.Raw; +import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; + +public class BluetoothHandlerCyclingCadence implements SensorHandlerInterface { + private static final String TAG = BluetoothHandlerCyclingCadence.class.getSimpleName(); + + public static final List CYCLING_CADENCE = List.of( + BluetoothHandlerManagerCyclingPower.CYCLING_POWER, + BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE + ); + + @Override + public List getServices() { + return CYCLING_CADENCE; + } + + @Override + public AggregatorCyclingCadence createEmptySensorData(String address) { + return new AggregatorCyclingCadence(address); + } + + @Override + public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { + if (serviceMeasurementUUID.equals(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)) { + BluetoothHandlerManagerCyclingPower.Data data = BluetoothHandlerManagerCyclingPower.parseCyclingPower(characteristic); + if (data != null && data.crank() != null) { + observer.onChange(new Raw<>(data.crank())); + } + return; + } + + if (serviceMeasurementUUID.equals(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)) { + Pair data = BluetoothHandlerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic); + + if (data != null && data.second != null) { + observer.onChange(new Raw<>(data.second)); + } + return; + } + + Log.e(TAG, "Don't know how to decode this payload."); + } + + public record CrankData( + long crankRevolutionsCount, // UINT32 + int crankRevolutionsTime // UINT16; 1/1024s + ) {} +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingDistanceSpeed.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingDistanceSpeed.java new file mode 100644 index 000000000..a0cb87a48 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingDistanceSpeed.java @@ -0,0 +1,82 @@ +package de.dennisguse.opentracks.sensors; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.util.List; +import java.util.UUID; + +import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed; +import de.dennisguse.opentracks.sensors.sensorData.Raw; +import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; + +public class BluetoothHandlerCyclingDistanceSpeed implements SensorHandlerInterface { + + public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID( + new UUID(0x181600001000L, 0x800000805f9b34fbL), + new UUID(0x2A5B00001000L, 0x800000805f9b34fbL) + ); + + @Override + public List getServices() { + return List.of(CYCLING_SPEED_CADENCE); + } + + @Override + public AggregatorCyclingDistanceSpeed createEmptySensorData(String address) { + return new AggregatorCyclingDistanceSpeed(address); + } + + @Override + public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { + Pair data = parseCyclingCrankAndWheel(address, sensorName, characteristic); + if (data.first != null) { + observer.onChange(new Raw<>(data.first)); + } + } + + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public static Pair parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) { + // DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml + int valueLength = characteristic.getValue().length; + if (valueLength == 0) { + return null; + } + + int flags = characteristic.getValue()[0]; + boolean hasWheel = (flags & 0x01) > 0; + boolean hasCrank = (flags & 0x02) > 0; + + int index = 1; + WheelData wheelData = null; + if (hasWheel && valueLength - index >= 6) { + long wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index); + index += 4; + int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s + wheelData = new WheelData(wheelTotalRevolutionCount, wheelTime); + index += 2; + } + + BluetoothHandlerCyclingCadence.CrankData crankData = null; + if (hasCrank && valueLength - index >= 4) { + long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); + index += 2; + + int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s + crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime); + } + + return new Pair<>(wheelData, crankData); + } + + public record WheelData( + + long wheelRevolutionsCount, // UINT32 + + int wheelRevolutionsTime // UINT16; 1/1024s + ) {} +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerCyclingPower.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerCyclingPower.java new file mode 100644 index 000000000..149ec9207 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerCyclingPower.java @@ -0,0 +1,87 @@ +package de.dennisguse.opentracks.sensors; + +import android.bluetooth.BluetoothGattCharacteristic; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.util.List; +import java.util.UUID; + +import de.dennisguse.opentracks.data.models.Power; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower; +import de.dennisguse.opentracks.sensors.sensorData.Raw; +import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; + +public class BluetoothHandlerManagerCyclingPower implements SensorHandlerInterface { + + public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID( + new UUID(0x181800001000L, 0x800000805f9b34fbL), + new UUID(0x2A6300001000L, 0x800000805f9b34fbL) + ); + + @Override + public List getServices() { + return List.of(CYCLING_POWER); + } + + @Override + public AggregatorCyclingPower createEmptySensorData(String address) { + return new AggregatorCyclingPower(address); + } + + @Override + public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { + Data cyclingPower = parseCyclingPower(characteristic); + + if (cyclingPower != null) { + observer.onChange(new Raw<>(cyclingPower)); + } + } + + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public static Data parseCyclingPower(BluetoothGattCharacteristic characteristic) { + // DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml + int valueLength = characteristic.getValue().length; + if (valueLength == 0) { + return null; + } + + int index = 0; + int flags1 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++); + int flags2 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++); + boolean hasPedalPowerBalance = (flags1 & 0x01) > 0; + boolean hasAccumulatedTorque = (flags1 & 0x04) > 0; + boolean hasWheel = (flags1 & 16) > 0; + boolean hasCrank = (flags1 & 32) > 0; + + Integer instantaneousPower = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, index); + index += 2; + + if (hasPedalPowerBalance) { + index += 1; + } + if (hasAccumulatedTorque) { + index += 2; + } + if (hasWheel) { + index += 2 + 2; + } + + BluetoothHandlerCyclingCadence.CrankData crankData = null; + if (hasCrank && valueLength - index >= 4) { + long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); + index += 2; + + int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s + + crankData = new BluetoothHandlerCyclingCadence.CrankData(crankCount, crankTime); + } + + return new Data(Power.of(instantaneousPower), crankData); + } + + public record Data(Power power, BluetoothHandlerCyclingCadence.CrankData crank) {} + +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerHeartRate.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerHeartRate.java new file mode 100644 index 000000000..10ace7bc0 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerManagerHeartRate.java @@ -0,0 +1,70 @@ +package de.dennisguse.opentracks.sensors; + +import android.bluetooth.BluetoothGattCharacteristic; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.util.List; +import java.util.UUID; + +import de.dennisguse.opentracks.data.models.HeartRate; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate; +import de.dennisguse.opentracks.sensors.sensorData.Raw; +import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; + +public class BluetoothHandlerManagerHeartRate implements SensorHandlerInterface { + + public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID( + new UUID(0x180D00001000L, 0x800000805f9b34fbL), + new UUID(0x2A3700001000L, 0x800000805f9b34fbL) + ); + + // Used for device discovery in preferences + public static final List HEART_RATE_SUPPORTING_DEVICES = List.of( + HEARTRATE, + //Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery). + new ServiceMeasurementUUID( + UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3 + HEARTRATE.measurementUUID() + ) + ); + + @Override + public List getServices() { + return HEART_RATE_SUPPORTING_DEVICES; + } + + @Override + public AggregatorHeartRate createEmptySensorData(String address) { + return new AggregatorHeartRate(address); + } + + @Override + public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { + HeartRate heartRate = parseHeartRate(characteristic); + + if (heartRate != null) { + observer.onChange(new Raw<>(heartRate)); + } + } + + @VisibleForTesting + public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) { + //DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml + byte[] raw = characteristic.getValue(); + if (raw.length == 0) { + return null; + } + + boolean formatUINT16 = ((raw[0] & 0x1) == 1); + if (formatUINT16 && raw.length >= 3) { + return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1)); + } + if (!formatUINT16 && raw.length >= 2) { + return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1)); + } + + return null; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerRunningSpeedAndCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerRunningSpeedAndCadence.java new file mode 100644 index 000000000..1a7f8a6f4 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerRunningSpeedAndCadence.java @@ -0,0 +1,88 @@ +package de.dennisguse.opentracks.sensors; + +import android.bluetooth.BluetoothGattCharacteristic; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.util.List; +import java.util.UUID; + +import de.dennisguse.opentracks.data.models.Cadence; +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Speed; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorRunning; +import de.dennisguse.opentracks.sensors.sensorData.Raw; +import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; + +public class BluetoothHandlerRunningSpeedAndCadence implements SensorHandlerInterface { + + + public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID( + new UUID(0x181400001000L, 0x800000805f9b34fbL), + new UUID(0x2A5300001000L, 0x800000805f9b34fbL) + ); + + @Override + public List getServices() { + return List.of(RUNNING_SPEED_CADENCE); + } + + @Override + public AggregatorRunning createEmptySensorData(String address) { + return new AggregatorRunning(address); + } + + @Override + public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { + Data data = parseRunningSpeedAndCadence(sensorName, characteristic); + observer.onChange(new Raw<>(data)); + } + + @VisibleForTesting + public static Data parseRunningSpeedAndCadence(String sensorName, @NonNull BluetoothGattCharacteristic characteristic) { + // DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml + int valueLength = characteristic.getValue().length; + if (valueLength == 0) { + return null; + } + + int flags = characteristic.getValue()[0]; + boolean hasStrideLength = (flags & 0x01) > 0; + boolean hasTotalDistance = (flags & 0x02) > 0; + boolean hasStatus = (flags & 0x03) > 0; // walking vs running + + Speed speed = null; + Cadence cadence = null; + Distance totalDistance = null; + + int index = 1; + if (valueLength - index >= 2) { + speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f); + } + + index = 3; + if (valueLength - index >= 1) { + cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index)); + + // Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard. + if (sensorName != null && sensorName.startsWith("TICKR X")) { + cadence = Cadence.of(cadence.getRPM() / 2); + } + } + + index = 4; + if (hasStrideLength && valueLength - index >= 2) { + Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index)); + index += 2; + } + + if (hasTotalDistance && valueLength - index >= 4) { + totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index)); + } + + return new Data(speed, cadence, totalDistance); + } + + public record Data(Speed speed, Cadence cadence, Distance totalDistance) {} +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java index 3e6ddc54f..96f62779b 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java @@ -29,7 +29,6 @@ import androidx.annotation.Nullable; import java.time.Duration; import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.sensors.sensorData.SensorData; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.util.PermissionRequester; @@ -47,29 +46,34 @@ import de.dennisguse.opentracks.util.PermissionRequester; * * @author Sandor Dornbush */ -public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBluetoothConnectionManager.SensorDataObserver, SharedPreferences.OnSharedPreferenceChangeListener { +public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName(); - public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5); + public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(50); //TODO private final BluetoothAdapter bluetoothAdapter; private final Context context; private final Handler handler; - private final SensorManager.SensorDataChangedObserver observer; private boolean started = false; - private final BluetoothConnectionManagerHeartRate heartRate = new BluetoothConnectionManagerHeartRate(this); - private final BluetoothConnectionManagerCyclingCadence cyclingCadence = new BluetoothConnectionManagerCyclingCadence(this); - private final BluetoothConnectionManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManagerCyclingDistanceSpeed(this); - private final BluetoothConnectionManagerCyclingPower cyclingPower = new BluetoothConnectionManagerCyclingPower(this); - private final BluetoothConnectionRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionRunningSpeedAndCadence(this); + private final BluetoothConnectionManager heartRate; + private final BluetoothConnectionManager cyclingCadence; + private final BluetoothConnectionManager cyclingSpeed; + private final BluetoothConnectionManager cyclingPower; + private final BluetoothConnectionManager runningSpeedAndCadence; public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) { this.context = context; this.handler = handler; - this.observer = observer; bluetoothAdapter = BluetoothUtils.getAdapter(context); + + this.heartRate = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerHeartRate()); + this.cyclingCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed()); + this.cyclingSpeed = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed()); + this.cyclingPower = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerCyclingPower()); + this.runningSpeedAndCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerRunningSpeedAndCadence()); + } @Override @@ -95,7 +99,7 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); } - private synchronized void connect(AbstractBluetoothConnectionManager connectionManager, String address) { + private synchronized void connect(BluetoothConnectionManager connectionManager, String address) { if (!isEnabled()) { Log.w(TAG, "Bluetooth not enabled."); return; @@ -120,28 +124,12 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl Log.i(TAG, "Connecting to bluetooth address: " + address); try { BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address); - connectionManager.connect(context, device); + connectionManager.connect(context, handler, device); } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to get remote device for: " + address, e); } } - @Override - public synchronized void onChanged(SensorData sensorData) { - observer.onChange(sensorData); - } - - @Override - public void onDisconnecting(SensorData sensorData) { - observer.onDisconnect(sensorData); - } - - @NonNull - @Override - public Handler getHandler() { - return handler; - } - @Override public void onSharedPreferenceChanged(SharedPreferences unused, @Nullable String key) { if (!started) return; diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java index 48e0bb296..c0f2f2d1f 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java @@ -21,23 +21,9 @@ import android.bluetooth.BluetoothManager; import android.content.Context; import android.util.Log; -import androidx.annotation.NonNull; - -import java.util.List; import java.util.UUID; -import de.dennisguse.opentracks.data.models.AtmosphericPressure; import de.dennisguse.opentracks.data.models.BatteryLevel; -import de.dennisguse.opentracks.data.models.Cadence; -import de.dennisguse.opentracks.data.models.Distance; -import de.dennisguse.opentracks.data.models.HeartRate; -import de.dennisguse.opentracks.data.models.Power; -import de.dennisguse.opentracks.data.models.Speed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; -import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning; /** * Utilities for dealing with bluetooth devices. @@ -53,52 +39,9 @@ public class BluetoothUtils { new UUID(0x2A1900001000L, 0x800000805f9b34fbL) ); - public static final ServiceMeasurementUUID HEARTRATE = new ServiceMeasurementUUID( - new UUID(0x180D00001000L, 0x800000805f9b34fbL), - new UUID(0x2A3700001000L, 0x800000805f9b34fbL) - ); - - // Used for device discovery in preferences - public static final List HEART_RATE_SUPPORTING_DEVICES = List.of( - HEARTRATE, - //Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery). - new ServiceMeasurementUUID( - UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"), //Miband3 - HEARTRATE.measurementUUID() - ) - ); - - private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL); - public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID( - ENVIRONMENTAL_SENSING_SERVICE, - new UUID(0x2A6D00001000L, 0x800000805f9b34fbL) - ); - - public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID( - new UUID(0x181800001000L, 0x800000805f9b34fbL), - new UUID(0x2A6300001000L, 0x800000805f9b34fbL) - ); - - public static final ServiceMeasurementUUID CYCLING_SPEED_CADENCE = new ServiceMeasurementUUID( - new UUID(0x181600001000L, 0x800000805f9b34fbL), - new UUID(0x2A5B00001000L, 0x800000805f9b34fbL) - ); - - public static final List CYCLING_CADENCE = List.of( - CYCLING_POWER, - CYCLING_SPEED_CADENCE - ); - - public static final ServiceMeasurementUUID RUNNING_SPEED_CADENCE = new ServiceMeasurementUUID( - new UUID(0x181400001000L, 0x800000805f9b34fbL), - new UUID(0x2A5300001000L, 0x800000805f9b34fbL) - ); private static final String TAG = BluetoothUtils.class.getSimpleName(); - private BluetoothUtils() { - } - public static BluetoothAdapter getAdapter(Context context) { BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager == null) { @@ -123,152 +66,4 @@ public class BluetoothUtils { final int batteryLevel = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); return BatteryLevel.of(batteryLevel); } - - public static HeartRate parseHeartRate(BluetoothGattCharacteristic characteristic) { - //DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml - byte[] raw = characteristic.getValue(); - if (raw.length == 0) { - return null; - } - - boolean formatUINT16 = ((raw[0] & 0x1) == 1); - if (formatUINT16 && raw.length >= 3) { - return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1)); - } - if (!formatUINT16 && raw.length >= 2) { - return HeartRate.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1)); - } - - return null; - } - - public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) { - byte[] raw = characteristic.getValue(); - - if (raw.length < 4) { - return null; - } - - Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0); - return AtmosphericPressure.ofPA(pressure / 10f); - } - - public static SensorDataCyclingPower.Data parseCyclingPower(String address, String sensorName, BluetoothGattCharacteristic characteristic) { - // DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml - int valueLength = characteristic.getValue().length; - if (valueLength == 0) { - return null; - } - - int index = 0; - int flags1 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++); - int flags2 = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index++); - boolean hasPedalPowerBalance = (flags1 & 0x01) > 0; - boolean hasAccumulatedTorque = (flags1 & 0x04) > 0; - boolean hasWheel = (flags1 & 16) > 0; - boolean hasCrank = (flags1 & 32) > 0; - - Integer instantaneousPower = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, index); - index += 2; - - if (hasPedalPowerBalance) { - index += 1; - } - if (hasAccumulatedTorque) { - index += 2; - } - if (hasWheel) { - index += 2 + 2; - } - - SensorDataCyclingCadence cadence = null; - if (hasCrank && valueLength - index >= 4) { - long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); - index += 2; - - int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s - - cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime); - } - - - return new SensorDataCyclingPower.Data(new SensorDataCyclingPower(sensorName, address, Power.of(instantaneousPower)), cadence); - } - - public static SensorDataCyclingCadenceAndDistanceSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) { - // DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml - int valueLength = characteristic.getValue().length; - if (valueLength == 0) { - return null; - } - - int flags = characteristic.getValue()[0]; - boolean hasWheel = (flags & 0x01) > 0; - boolean hasCrank = (flags & 0x02) > 0; - - int index = 1; - SensorDataCyclingDistanceSpeed speed = null; - if (hasWheel && valueLength - index >= 6) { - int wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index); - index += 4; - int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s - speed = new SensorDataCyclingDistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime); - index += 2; - } - - SensorDataCyclingCadence cadence = null; - if (hasCrank && valueLength - index >= 4) { - long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); - index += 2; - - int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s - cadence = new SensorDataCyclingCadence(address, sensorName, crankCount, crankTime); - } - - return new SensorDataCyclingCadenceAndDistanceSpeed(address, sensorName, cadence, speed); - } - - public static SensorDataRunning parseRunningSpeedAndCadence(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) { - // DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml - int valueLength = characteristic.getValue().length; - if (valueLength == 0) { - return null; - } - - int flags = characteristic.getValue()[0]; - boolean hasStrideLength = (flags & 0x01) > 0; - boolean hasTotalDistance = (flags & 0x02) > 0; - boolean hasStatus = (flags & 0x03) > 0; // walking vs running - - Speed speed = null; - Cadence cadence = null; - Distance totalDistance = null; - - int index = 1; - if (valueLength - index >= 2) { - speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f); - } - - index = 3; - if (valueLength - index >= 1) { - cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index)); - - // Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard. - if (sensorName != null && sensorName.startsWith("TICKR X")) { - cadence = Cadence.of(cadence.getRPM() / 2); - } - } - - index = 4; - if (hasStrideLength && valueLength - index >= 2) { - Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index)); - index += 2; - } - - if (hasTotalDistance && valueLength - index >= 4) { - totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index)); - } - - return new SensorDataRunning(address, sensorName, speed, cadence, totalDistance); - } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/GainManager.java b/src/main/java/de/dennisguse/opentracks/sensors/GainManager.java new file mode 100644 index 000000000..40e1749f9 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/GainManager.java @@ -0,0 +1,46 @@ +package de.dennisguse.opentracks.sensors; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; + +import de.dennisguse.opentracks.data.models.AtmosphericPressure; +import de.dennisguse.opentracks.sensors.driver.BarometerInternal; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer; +import de.dennisguse.opentracks.sensors.sensorData.Raw; + +/** + * Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer). + */ +public class GainManager implements SensorConnector { + + private static final String TAG = GainManager.class.getSimpleName(); + + private final BarometerInternal driver; + + private final SensorManager.SensorDataChangedObserver listener; + + public GainManager(SensorManager.SensorDataChangedObserver listener) { + this.listener = listener; + driver = new BarometerInternal(); + } + + public void start(Context context, Handler handler) { + driver.connect(context, handler, this); + + if (driver.isConnected()) { + listener.onConnect(new AggregatorBarometer("internal")); + } + } + + public void stop(Context context) { + Log.d(TAG, "Stop"); + + driver.disconnect(context); + listener.onDisconnect(new AggregatorBarometer("internal")); + } + + public void onSensorValueChanged(AtmosphericPressure currentSensorValue) { + listener.onChange(new Raw<>(currentSensorValue)); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java b/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java index 789bee4be..385d69ca7 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/SensorManager.java @@ -9,7 +9,8 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import de.dennisguse.opentracks.data.models.TrackPoint; -import de.dennisguse.opentracks.sensors.sensorData.SensorData; +import de.dennisguse.opentracks.sensors.sensorData.Aggregator; +import de.dennisguse.opentracks.sensors.sensorData.Raw; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.services.handlers.GPSManager; import de.dennisguse.opentracks.services.handlers.TrackPointCreator; @@ -27,13 +28,19 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange private final SensorDataChangedObserver listener = new SensorDataChangedObserver() { @Override - public void onChange(SensorData sensorData) { - sensorDataSet.set(sensorData); + public void onConnect(Aggregator sensorData) { + sensorDataSet.add(sensorData); observer.onChange(new SensorDataSet(sensorDataSet)); } @Override - public void onDisconnect(SensorData sensorData) { + public void onChange(Raw sensorData) { + sensorDataSet.update(sensorData); + observer.onChange(new SensorDataSet(sensorDataSet)); + } + + @Override + public void onDisconnect(Aggregator sensorData) { sensorDataSet.remove(sensorData); observer.onChange(new SensorDataSet(sensorDataSet)); } @@ -41,7 +48,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange private BluetoothRemoteSensorManager bluetoothSensorManager; - private AltitudeSumManager altitudeSumManager; + private GainManager altitudeSumManager; private GPSManager gpsManager; @@ -51,7 +58,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange public void start(Context context, Handler handler) { gpsManager = new GPSManager(observer); //TODO Pass listener - altitudeSumManager = new AltitudeSumManager(); + altitudeSumManager = new GainManager(listener); bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener); onSharedPreferenceChanged(null, null); @@ -75,7 +82,6 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange } public SensorDataSet fill(TrackPoint trackPoint) { - altitudeSumManager.fill(trackPoint); sensorDataSet.fillTrackPoint(trackPoint); return new SensorDataSet(sensorDataSet); } @@ -86,13 +92,12 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange return; } sensorDataSet.reset(); - altitudeSumManager.reset(); } @Deprecated @VisibleForTesting - public BluetoothRemoteSensorManager getBluetoothSensorManager() { - return bluetoothSensorManager; + public void onChanged(Raw data) { + listener.onChange(data); } public GPSManager getGpsManager() { @@ -101,13 +106,13 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange @Deprecated @VisibleForTesting - public AltitudeSumManager getAltitudeSumManager() { + public GainManager getAltitudeSumManager() { return altitudeSumManager; } @Deprecated @VisibleForTesting - public void setAltitudeSumManager(AltitudeSumManager altitudeSumManager) { + public void setAltitudeSumManager(GainManager altitudeSumManager) { this.altitudeSumManager = altitudeSumManager; } @@ -120,8 +125,10 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange } public interface SensorDataChangedObserver { - void onChange(SensorData sensorData); - void onDisconnect(SensorData sensorData); + void onConnect(Aggregator sensorData); + void onChange(Raw sensorData); + + void onDisconnect(Aggregator sensorData); } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java b/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java index 8a405f720..4fc47b116 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java @@ -11,7 +11,7 @@ import android.util.Log; import java.util.concurrent.TimeUnit; import de.dennisguse.opentracks.data.models.AtmosphericPressure; -import de.dennisguse.opentracks.sensors.AltitudeSumManager; +import de.dennisguse.opentracks.sensors.GainManager; public class BarometerInternal { @@ -19,7 +19,7 @@ public class BarometerInternal { private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5); - private AltitudeSumManager observer; + private GainManager observer; private final SensorEventListener listener = new SensorEventListener() { @Override @@ -38,14 +38,12 @@ public class BarometerInternal { } }; - public void connect(Context context, Handler handler, AltitudeSumManager observer) { - this.observer = observer; + public void connect(Context context, Handler handler, GainManager observer) { SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE); if (pressureSensor == null) { Log.w(TAG, "No pressure sensor available."); this.observer = null; - return; } if (sensorManager.registerListener(listener, pressureSensor, SAMPLING_PERIOD, handler)) { @@ -59,6 +57,7 @@ public class BarometerInternal { public void disconnect(Context context) { SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); sensorManager.unregisterListener(listener); + observer = null; } public boolean isConnected() { diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorData.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/Aggregator.java similarity index 59% rename from src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorData.java rename to src/main/java/de/dennisguse/opentracks/sensors/sensorData/Aggregator.java index 365d6bc48..82ae79c2f 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorData.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/Aggregator.java @@ -1,48 +1,48 @@ package de.dennisguse.opentracks.sensors.sensorData; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import java.time.Instant; import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager; -public abstract class SensorData { +public abstract class Aggregator { - protected T value; + protected Raw previous; + + protected Output value; private final String sensorAddress; private final String sensorName; - private final Instant time; - - SensorData(String sensorAddress) { + Aggregator(String sensorAddress) { this(sensorAddress, null); } - SensorData(String sensorAddress, String sensorName) { - this(sensorAddress, sensorName, Instant.now()); - } - - @VisibleForTesting - SensorData(String sensorAddress, String sensorName, Instant time) { + Aggregator(String sensorAddress, String sensorName) { this.sensorAddress = sensorAddress; this.sensorName = sensorName; - this.time = time; } public String getSensorNameOrAddress() { return sensorName != null ? sensorName : sensorAddress; } + public final void add(Raw current) { + computeValue(current); + previous = current; + } + + protected abstract void computeValue(Raw current); + public boolean hasValue() { return value != null; } @NonNull - protected abstract T getNoneValue(); + protected abstract Output getNoneValue(); - public T getValue() { + public Output getValue() { if (!hasValue()) { return null; } @@ -55,20 +55,23 @@ public abstract class SensorData { /** * Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance. */ - public void reset() { - } + public void reset() {}; /** * Is the data recent considering the current time. */ private boolean isRecent() { + if (previous == null) { + return false; + } + return Instant.now() - .isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE)); + .isBefore(previous.time().plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE)); } @NonNull @Override public String toString() { - return "sensorAddress='" + sensorAddress; + return "sensorAddress=" + sensorAddress + " data=" + value; } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometer.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometer.java new file mode 100644 index 000000000..7e5ed7c57 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometer.java @@ -0,0 +1,46 @@ +package de.dennisguse.opentracks.sensors.sensorData; + +import androidx.annotation.NonNull; + +import de.dennisguse.opentracks.data.models.Altitude; +import de.dennisguse.opentracks.data.models.AltitudeGainLoss; +import de.dennisguse.opentracks.data.models.AtmosphericPressure; +import de.dennisguse.opentracks.sensors.PressureSensorUtils; + +public class AggregatorBarometer extends Aggregator { + + private AtmosphericPressure lastAcceptedSensorValue; + + public AggregatorBarometer(String sensorAddress) { + super(sensorAddress); + } + + @Override + protected void computeValue(Raw current) { + if (previous == null) { + lastAcceptedSensorValue = current.value(); + value = getNoneValue(); + return; + } + + PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, previous.value(), current.value()); + if (altitudeChange != null) { + value = new AltitudeGainLoss(value.gain_m() + altitudeChange.getAltitudeGain_m(), value.loss_m() + altitudeChange.getAltitudeLoss_m()); + + lastAcceptedSensorValue = altitudeChange.currentSensorValue(); + } + } + + @NonNull + @Override + protected AltitudeGainLoss getNoneValue() { + return new AltitudeGainLoss(0f, 0f); + } + + @Override + public void reset() { + value = getNoneValue(); + } + + public record Data(Altitude gain, Altitude loss) {} +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingCadence.java new file mode 100644 index 000000000..61a348354 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingCadence.java @@ -0,0 +1,52 @@ +package de.dennisguse.opentracks.sensors.sensorData; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.time.Duration; + +import de.dennisguse.opentracks.data.models.Cadence; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence; +import de.dennisguse.opentracks.sensors.UintUtils; + +public class AggregatorCyclingCadence extends Aggregator { + + private final String TAG = AggregatorCyclingCadence.class.getSimpleName(); + + public AggregatorCyclingCadence(String sensorAddress) { + super(sensorAddress); + } + + public AggregatorCyclingCadence(String sensorAddress, String sensorName) { + super(sensorAddress, sensorName); + } + + @Override + protected void computeValue(Raw current) { + if (previous != null) { + float timeDiff_ms = UintUtils.diff(current.value().crankRevolutionsTime(), previous.value().crankRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000; + Duration timeDiff = Duration.ofMillis((long) timeDiff_ms); + if (timeDiff.isZero() || timeDiff.isNegative()) { + Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); + value = null; + return; + } + + // TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268 + if (current.value().crankRevolutionsCount() < previous.value().crankRevolutionsCount()) { + Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence."); + return; + } + + long crankDiff = UintUtils.diff(current.value().crankRevolutionsCount(), previous.value().crankRevolutionsCount(), UintUtils.UINT32_MAX); + value = Cadence.of(crankDiff, timeDiff); + } + } + + @NonNull + @Override + protected Cadence getNoneValue() { + return Cadence.of(0); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingDistanceSpeed.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingDistanceSpeed.java new file mode 100644 index 000000000..71374c019 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingDistanceSpeed.java @@ -0,0 +1,77 @@ +package de.dennisguse.opentracks.sensors.sensorData; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.time.Duration; + +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Speed; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed; +import de.dennisguse.opentracks.sensors.UintUtils; + +public class AggregatorCyclingDistanceSpeed extends Aggregator { + + private final String TAG = AggregatorCyclingDistanceSpeed.class.getSimpleName(); + + private Distance wheelCircumference; + + public AggregatorCyclingDistanceSpeed(String sensorAddress) { + super(sensorAddress); + } + + public AggregatorCyclingDistanceSpeed(String sensorAddress, String sensorName) { + super(sensorAddress, sensorName); + } + + @Override + protected void computeValue(Raw current) { + if (previous != null) { + float timeDiff_ms = UintUtils.diff(current.value().wheelRevolutionsTime(), previous.value().wheelRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000; + Duration timeDiff = Duration.ofMillis((long) timeDiff_ms); + if (timeDiff.isZero() || timeDiff.isNegative()) { + Log.e(TAG, "Timestamps difference is invalid: cannot compute speed."); + value = null; + return; + } + + if (current.value().wheelRevolutionsCount() < previous.value().wheelRevolutionsCount()) { + Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed."); + return; + } + long wheelDiff = UintUtils.diff(current.value().wheelRevolutionsCount(), previous.value().wheelRevolutionsCount(), UintUtils.UINT32_MAX); + + Distance distance = wheelCircumference.multipliedBy(wheelDiff); + Distance distanceOverall = distance; + if (value != null) { + distanceOverall = distance.plus(value.distanceOverall); + } + Speed speed_mps = Speed.of(distance, timeDiff); + value = new Data(distance, distanceOverall, speed_mps); + } + } + + @Override + public void reset() { + if (value != null) { + value = new Data(value.distance, Distance.of(0), value.speed); + } + } + + @NonNull + @Override + protected Data getNoneValue() { + if (value != null) { + return new Data(value.distance, value.distanceOverall, Speed.zero()); + } else { + return new Data(Distance.of(0), Distance.of(0), Speed.zero()); + } + } + + public void setWheelCircumference(Distance wheelCircumference) { + this.wheelCircumference = wheelCircumference; + } + + public record Data(Distance distance, Distance distanceOverall, Speed speed) {} +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingPower.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingPower.java new file mode 100644 index 000000000..e60e08ee6 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorCyclingPower.java @@ -0,0 +1,27 @@ +package de.dennisguse.opentracks.sensors.sensorData; + +import androidx.annotation.NonNull; + +import de.dennisguse.opentracks.data.models.Power; + +public class AggregatorCyclingPower extends Aggregator { + + public AggregatorCyclingPower(String address) { + super(address); + } + + public AggregatorCyclingPower(String name, String address) { + super(name, address); + } + + @Override + public void computeValue(Raw current) { + this.value = current.value(); + } + + @NonNull + @Override + protected Power getNoneValue() { + return Power.of(0f); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataHeartRate.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorHeartRate.java similarity index 50% rename from src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataHeartRate.java rename to src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorHeartRate.java index 3f02eca7d..68879cb59 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataHeartRate.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorHeartRate.java @@ -4,21 +4,19 @@ import androidx.annotation.NonNull; import de.dennisguse.opentracks.data.models.HeartRate; -public class SensorDataHeartRate extends SensorData { +public class AggregatorHeartRate extends Aggregator { - public SensorDataHeartRate(String address) { + public AggregatorHeartRate(String address) { super(address); } - public SensorDataHeartRate(String name, String address, @NonNull HeartRate heartRate) { + public AggregatorHeartRate(String name, String address) { super(name, address); - this.value = heartRate; } - @NonNull @Override - public String toString() { - return super.toString() + " heart=" + value; + protected void computeValue(Raw current) { + this.value = current.value(); } @NonNull diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorRunning.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorRunning.java new file mode 100644 index 000000000..425f20705 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorRunning.java @@ -0,0 +1,61 @@ +package de.dennisguse.opentracks.sensors.sensorData; + +import androidx.annotation.NonNull; + +import de.dennisguse.opentracks.data.models.Cadence; +import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Speed; +import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence; + +/** + * Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors. + */ +public final class AggregatorRunning extends Aggregator { + + private static final String TAG = AggregatorRunning.class.getSimpleName(); + + public AggregatorRunning(String sensorAddress) { + super(sensorAddress); + } + + public AggregatorRunning(String sensorAddress, String sensorName) { + super(sensorAddress, sensorName); + } + + @Override + public void computeValue(Raw current) { + if (previous != null) { + + Distance distance = null; + if (previous.value().totalDistance() != null && current.value().totalDistance() != null) { + distance = current.value().totalDistance().minus(previous.value().totalDistance()); + if (value != null) { + distance = distance.plus(value.distance); + } + } + + value = new Data(current.value().speed(), current.value().cadence(), distance); + } + } + + @Override + public void reset() { + if (value != null) { + value = new Data(value.speed, value.cadence, Distance.of(0)); + } + } + + @NonNull + @Override + protected Data getNoneValue() { + if (value != null) { + return new Data(Speed.zero(), Cadence.of(0f), value.distance); + } else { + return new Data(Speed.zero(), Cadence.of(0f), Distance.of(0)); + } + } + + public record Data(Speed speed, Cadence cadence, @NonNull Distance distance) { + } +} + diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/Raw.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/Raw.java new file mode 100644 index 000000000..5f88bb229 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/Raw.java @@ -0,0 +1,16 @@ +package de.dennisguse.opentracks.sensors.sensorData; + + +import androidx.annotation.NonNull; + +import java.time.Instant; + +public record Raw( + @NonNull T value, + + @NonNull Instant time +) { + public Raw(@NonNull T value) { + this(value, Instant.now()); //TODO We should be using the MonotonicClock + } +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingCadence.java deleted file mode 100644 index 0fbdb193e..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingCadence.java +++ /dev/null @@ -1,87 +0,0 @@ -package de.dennisguse.opentracks.sensors.sensorData; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.time.Duration; - -import de.dennisguse.opentracks.data.models.Cadence; -import de.dennisguse.opentracks.sensors.UintUtils; - -public class SensorDataCyclingCadence extends SensorData { - - private final String TAG = SensorDataCyclingCadence.class.getSimpleName(); - - private final Long crankRevolutionsCount; // UINT32 - private final Integer crankRevolutionsTime; // UINT16; 1/1024s - - public SensorDataCyclingCadence(String sensorAddress) { - super(sensorAddress); - this.crankRevolutionsCount = null; - this.crankRevolutionsTime = null; - } - - public SensorDataCyclingCadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) { - super(sensorAddress, sensorName); - this.crankRevolutionsCount = crankRevolutionsCount; - this.crankRevolutionsTime = crankRevolutionsTime; - } - - public boolean hasData() { - return crankRevolutionsCount != null && crankRevolutionsTime != null; - } - - public long getCrankRevolutionsCount() { - return crankRevolutionsCount; - } - - public int getCrankRevolutionsTime() { - return crankRevolutionsTime; - } - - @NonNull - @Override - protected Cadence getNoneValue() { - return Cadence.of(0); - } - - public void compute(SensorDataCyclingCadence previous) { - if (hasData() && previous != null && previous.hasData()) { - float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000; - Duration timeDiff = Duration.ofMillis((long) timeDiff_ms); - if (timeDiff.isZero() || timeDiff.isNegative()) { - Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); - value = null; - return; - } - - // TODO We have to treat with overflow according to the documentation: read https://github.com/OpenTracksApp/OpenTracks/pull/953#discussion_r711625268 - if (crankRevolutionsCount < previous.crankRevolutionsCount) { - Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence."); - return; - } - - long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX); - value = Cadence.of(crankDiff, timeDiff); - } - } - - @NonNull - @Override - public String toString() { - return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof SensorDataCyclingCadence comp)) return false; - - if (hasData() && comp.hasData() == hasData()) { - return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime(); - } else { - return false; - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingCadenceAndDistanceSpeed.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingCadenceAndDistanceSpeed.java deleted file mode 100644 index 26d4cd9c2..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingCadenceAndDistanceSpeed.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.dennisguse.opentracks.sensors.sensorData; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; - -public class SensorDataCyclingCadenceAndDistanceSpeed extends SensorData> { - - public SensorDataCyclingCadenceAndDistanceSpeed(String sensorAddress, String sensorName, @Nullable SensorDataCyclingCadence cadence, @Nullable SensorDataCyclingDistanceSpeed distanceSpeed) { - super(sensorAddress, sensorName); - this.value = new Pair<>(cadence, distanceSpeed); - } - - public SensorDataCyclingCadence getCadence() { - return this.value != null ? this.value.first : null; - } - - public SensorDataCyclingDistanceSpeed getDistanceSpeed() { - return this.value != null ? this.value.second : null; - } - - @NonNull - @Override - protected Pair getNoneValue() { - return new Pair<>(null, null); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingDistanceSpeed.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingDistanceSpeed.java deleted file mode 100644 index 0e76713ac..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingDistanceSpeed.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.dennisguse.opentracks.sensors.sensorData; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.time.Duration; - -import de.dennisguse.opentracks.data.models.Distance; -import de.dennisguse.opentracks.data.models.Speed; -import de.dennisguse.opentracks.sensors.UintUtils; - -public class SensorDataCyclingDistanceSpeed extends SensorData { - - private final String TAG = SensorDataCyclingDistanceSpeed.class.getSimpleName(); - - private final Long wheelRevolutionsCount; // UINT32 - private final Integer wheelRevolutionsTime; // UINT16; 1/1024s - - public SensorDataCyclingDistanceSpeed(String sensorAddress) { - super(sensorAddress); - this.wheelRevolutionsCount = null; - this.wheelRevolutionsTime = null; - } - - public SensorDataCyclingDistanceSpeed(String sensorAddress, String sensorName, long wheelRevolutionsCount, int wheelRevolutionsTime) { - super(sensorAddress, sensorName); - this.wheelRevolutionsCount = wheelRevolutionsCount; - this.wheelRevolutionsTime = wheelRevolutionsTime; - } - - public boolean hasData() { - return wheelRevolutionsCount != null && wheelRevolutionsTime != null; - } - - public long getWheelRevolutionsCount() { - return wheelRevolutionsCount; - } - - public int getWheelRevolutionsTime() { - return wheelRevolutionsTime; - } - - @NonNull - @Override - protected Data getNoneValue() { - if (value != null) { - return new Data(value.distance, value.distanceOverall, Speed.zero()); - } else { - return new Data(Distance.of(0), Distance.of(0), Speed.zero()); - } - } - - public void compute(SensorDataCyclingDistanceSpeed previous, Distance wheelCircumference) { - if (hasData() && previous != null && previous.hasData()) { - float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * 1000; - Duration timeDiff = Duration.ofMillis((long) timeDiff_ms); - if (timeDiff.isZero() || timeDiff.isNegative()) { - Log.e(TAG, "Timestamps difference is invalid: cannot compute speed."); - value = null; - return; - } - - if (wheelRevolutionsCount < previous.wheelRevolutionsCount) { - Log.e(TAG, "Wheel revolutions count difference is invalid: cannot compute speed."); - return; - } - long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX); - - Distance distance = wheelCircumference.multipliedBy(wheelDiff); - Distance distanceOverall = distance; - if (previous.hasValue()) { - distanceOverall = distance.plus(previous.getValue().distanceOverall); - } - Speed speed_mps = Speed.of(distance, timeDiff); - value = new Data(distance, distanceOverall, speed_mps); - } - } - - @Override - public void reset() { - if (value != null) { - value = new Data(value.distance, Distance.of(0), value.speed); - } - } - - @NonNull - @Override - public String toString() { - return super.toString() + " data=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof SensorDataCyclingDistanceSpeed comp)) return false; - - if (!(hasData() && comp.hasData())) { - return false; - } - - return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime(); - } - - public static class Data { - private final Distance distance; - private final Distance distanceOverall; - private final Speed speed; - - private Data(Distance distance, Distance distanceOverall, Speed speed) { - this.distance = distance; - this.distanceOverall = distanceOverall; - this.speed = speed; - } - - public Distance getDistance() { - return distance; - } - - public Distance getDistanceOverall() { - return distanceOverall; - } - - public Speed getSpeed() { - return speed; - } - - @NonNull - @Override - public String toString() { - return "Data{" + - "distance=" + getDistance() + - ", distance_overall=" + getDistanceOverall() + - ", speed=" + getSpeed() + - '}'; - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingPower.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingPower.java deleted file mode 100644 index 6cf450da0..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataCyclingPower.java +++ /dev/null @@ -1,46 +0,0 @@ -package de.dennisguse.opentracks.sensors.sensorData; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import de.dennisguse.opentracks.data.models.Power; - -public class SensorDataCyclingPower extends SensorData { - - public SensorDataCyclingPower(String address) { - super(address); - } - - public SensorDataCyclingPower(String name, String address, Power power) { - super(name, address); - this.value = power; - } - - @NonNull - @Override - protected Power getNoneValue() { - return Power.of(0f); - } - - @NonNull - @Override - public String toString() { - return super.toString() + " data=" + value; - } - - public record Data(SensorDataCyclingPower power, SensorDataCyclingCadence cadence) { - public Data(SensorDataCyclingPower power, @Nullable SensorDataCyclingCadence cadence) { - this.power = power; - this.cadence = cadence; - } - - @NonNull - @Override - public String toString() { - return "Data{" + - "power=" + power + - ", cadence=" + cadence + - '}'; - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataRunning.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataRunning.java deleted file mode 100644 index 58ef3abd4..000000000 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataRunning.java +++ /dev/null @@ -1,89 +0,0 @@ -package de.dennisguse.opentracks.sensors.sensorData; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import de.dennisguse.opentracks.data.models.Cadence; -import de.dennisguse.opentracks.data.models.Distance; -import de.dennisguse.opentracks.data.models.Speed; - -/** - * Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors. - */ -public final class SensorDataRunning extends SensorData { - - private static final String TAG = SensorDataRunning.class.getSimpleName(); - - private final Speed speed; - - private final Cadence cadence; - - private final Distance totalDistance; - - public SensorDataRunning(String sensorAddress) { - super(sensorAddress); - this.speed = null; - this.cadence = null; - this.totalDistance = null; - } - - public SensorDataRunning(String sensorAddress, String sensorName, Speed speed, Cadence cadence, Distance totalDistance) { - super(sensorAddress, sensorName); - this.speed = speed; - this.cadence = cadence; - this.totalDistance = totalDistance; - } - - private boolean hasTotalDistance() { - return totalDistance != null; - } - - - public Cadence getCadence() { - return cadence; - } - - public Speed getSpeed() { - return speed; - } - - @VisibleForTesting - public Distance getTotalDistance() { - return totalDistance; - } - - @NonNull - @Override - protected Data getNoneValue() { - if (value != null) { - return new Data(Speed.zero(), Cadence.of(0f), value.distance); - } else { - return new Data(Speed.zero(), Cadence.of(0f), Distance.of(0)); - } - } - - public void compute(SensorDataRunning previous) { - if (speed != null && hasTotalDistance()) { - Distance overallDistance = null; - if (previous != null && previous.hasTotalDistance()) { - overallDistance = this.totalDistance.minus(previous.totalDistance); - if (previous.hasValue() && previous.getValue().distance() != null) { - overallDistance = overallDistance.plus(previous.getValue().distance()); - } - } - - value = new Data(speed, cadence, overallDistance); - } - } - - @Override - public void reset() { - if (value != null) { - value = new Data(value.speed, value.cadence, Distance.of(0)); - } - } - - public record Data(Speed speed, Cadence cadence, @Nullable Distance distance) {} -} - diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java index 1522f056e..485760817 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorDataSet.java @@ -5,27 +5,41 @@ import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import de.dennisguse.opentracks.data.models.AtmosphericPressure; import de.dennisguse.opentracks.data.models.Cadence; -import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.HeartRate; +import de.dennisguse.opentracks.data.models.Power; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.TrackPoint; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed; +import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower; +import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence; import de.dennisguse.opentracks.settings.PreferencesUtils; public final class SensorDataSet { private static final String TAG = SensorDataSet.class.getSimpleName(); - private SensorDataHeartRate heartRate; + @VisibleForTesting + public AggregatorHeartRate heartRate; - private SensorDataCyclingCadence cyclingCadence; + @VisibleForTesting + public AggregatorCyclingCadence cyclingCadence; - private SensorDataCyclingDistanceSpeed cyclingDistanceSpeed; + @VisibleForTesting + public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed; - private SensorDataCyclingPower cyclingPower; + @VisibleForTesting + public AggregatorCyclingPower cyclingPower; - private SensorDataRunning runningDistanceSpeedCadence; + @VisibleForTesting + public AggregatorRunning runningDistanceSpeedCadence; + + @VisibleForTesting + public AggregatorBarometer barometer; public SensorDataSet() { } @@ -36,6 +50,7 @@ public final class SensorDataSet { this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed; this.cyclingPower = toCopy.cyclingPower; this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence; + this.barometer = toCopy.barometer; } public Pair getHeartRate() { @@ -51,55 +66,79 @@ public final class SensorDataSet { return new Pair<>(cyclingCadence.getValue(), cyclingCadence.getSensorNameOrAddress()); } - if (runningDistanceSpeedCadence != null) { - return new Pair<>(runningDistanceSpeedCadence.getCadence(), runningDistanceSpeedCadence.getSensorNameOrAddress()); + if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.value.cadence() != null) { + return new Pair<>(runningDistanceSpeedCadence.value.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress()); } return null; } public Pair getSpeed() { - if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().getSpeed() != null) { - return new Pair<>(cyclingDistanceSpeed.getValue().getSpeed(), cyclingDistanceSpeed.getSensorNameOrAddress()); + if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue() && cyclingDistanceSpeed.getValue().speed() != null) { + return new Pair<>(cyclingDistanceSpeed.getValue().speed(), cyclingDistanceSpeed.getSensorNameOrAddress()); } if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue() && runningDistanceSpeedCadence.getValue().speed() != null) { - return new Pair<>(runningDistanceSpeedCadence.getSpeed(), runningDistanceSpeedCadence.getSensorNameOrAddress()); + return new Pair<>(runningDistanceSpeedCadence.value.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress()); } return null; } - public SensorDataCyclingCadence getCyclingCadence() { - return cyclingCadence; - } - - public SensorDataCyclingDistanceSpeed getCyclingDistanceSpeed() { - return cyclingDistanceSpeed; - } - - public SensorDataCyclingPower getCyclingPower() { + public AggregatorCyclingPower getCyclingPower() { return cyclingPower; } - public SensorDataRunning getRunningDistanceSpeedCadence() { - return runningDistanceSpeedCadence; - } - - public void set(SensorData data) { + public void add(@NonNull Aggregator data) { set(data, data); } - public void remove(SensorData type) { + public void update(@NonNull Raw data) { + Record value = data.value(); + + if (value instanceof HeartRate) { + this.heartRate.add((Raw) data); + return; + } + + if (value instanceof BluetoothHandlerCyclingCadence.CrankData) { + this.cyclingCadence.add((Raw) data); + return; + } + if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData ) { + this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes. + this.cyclingDistanceSpeed.add((Raw) data); + return; + } + if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) { + this.runningDistanceSpeedCadence.add((Raw) data); + + return; + } + if (value instanceof BluetoothHandlerManagerCyclingPower.Data) { + this.cyclingPower.add((Raw) data); + return; + } + if (value instanceof AtmosphericPressure) { + this.barometer.add((Raw) data); + return; + } + + throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName()); + } + + public void remove(@NonNull Aggregator type) { set(type, null); } public void clear() { + Log.i(TAG, "Removing all aggregators"); this.heartRate = null; this.cyclingCadence = null; this.cyclingDistanceSpeed = null; this.cyclingPower = null; this.runningDistanceSpeedCadence = null; + this.barometer = null; } public void fillTrackPoint(TrackPoint trackPoint) { @@ -116,7 +155,7 @@ public final class SensorDataSet { } if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) { - trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().getDistanceOverall()); + trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall()); } if (cyclingPower != null && cyclingPower.hasValue()) { @@ -126,81 +165,49 @@ public final class SensorDataSet { if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) { trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().distance()); } + + if (barometer != null && barometer.hasValue()) { + trackPoint.setAltitudeGain(barometer.getValue().gain_m()); + trackPoint.setAltitudeLoss(barometer.getValue().loss_m()); + } } public void reset() { + Log.i(TAG, "Resetting data"); + if (heartRate != null) heartRate.reset(); if (cyclingCadence != null) cyclingCadence.reset(); if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset(); if (cyclingPower != null) cyclingPower.reset(); if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset(); + if (barometer != null) barometer.reset(); } - @NonNull - @Override - public String toString() { - return (heartRate != null ? "" + heartRate : "") - + (cyclingCadence != null ? " " + cyclingCadence : "") - + (cyclingDistanceSpeed != null ? " " + cyclingDistanceSpeed : "") - + (cyclingPower != null ? " " + cyclingPower : "") - + (runningDistanceSpeedCadence != null ? " " + runningDistanceSpeedCadence : ""); - } + private void set(@NonNull Aggregator type, @Nullable Aggregator sensorData) { + Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName()); - private void set(@NonNull SensorData type, @Nullable SensorData sensorData) { - if (type instanceof SensorDataHeartRate) { - this.heartRate = (SensorDataHeartRate) sensorData; + if (type instanceof AggregatorHeartRate) { + heartRate = (AggregatorHeartRate) sensorData; return; } - - if (type instanceof SensorDataCyclingCadence) { - SensorDataCyclingCadence previous = getCyclingCadence(); - Log.d(TAG, "Previous: " + previous + "; current: " + sensorData); - - if (sensorData != null && sensorData.equals(previous)) { - Log.d(TAG, "onChanged: cadence data repeated."); - return; - } - - this.cyclingCadence = (SensorDataCyclingCadence) sensorData; - if (this.cyclingCadence != null) { - this.cyclingCadence.compute(previous); - } + if (type instanceof AggregatorCyclingCadence) { + cyclingCadence = (AggregatorCyclingCadence) sensorData; return; } - - if (type instanceof SensorDataCyclingDistanceSpeed) { - SensorDataCyclingDistanceSpeed previous = getCyclingDistanceSpeed(); - Log.d(TAG, "Previous: " + previous + "; Current" + sensorData); - if (sensorData != null && sensorData.equals(previous)) { - Log.d(TAG, "onChanged: cycling speed data repeated."); - return; - } - Distance preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(); //TODO Fetch once and then listen for changes. - - this.cyclingDistanceSpeed = (SensorDataCyclingDistanceSpeed) sensorData; - if (this.cyclingDistanceSpeed != null) { - this.cyclingDistanceSpeed.compute(previous, preferenceWheelCircumference); - } + if (type instanceof AggregatorCyclingDistanceSpeed) { + cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData; return; } - - if (type instanceof SensorDataCyclingPower) { - this.cyclingPower = (SensorDataCyclingPower) sensorData; + if (type instanceof AggregatorCyclingPower) { + cyclingPower = (AggregatorCyclingPower) sensorData; return; } - - if (type instanceof SensorDataRunning) { - SensorDataRunning previous = getRunningDistanceSpeedCadence(); - Log.d(TAG, "Previous: " + previous + "; Current" + sensorData); - if (sensorData != null && sensorData.equals(previous)) { - Log.d(TAG, "onChanged: running speed data repeated."); - return; - } - - this.runningDistanceSpeedCadence = (SensorDataRunning) sensorData; - if (this.runningDistanceSpeedCadence != null) { - this.runningDistanceSpeedCadence.compute(previous); - } + if (type instanceof AggregatorRunning) { + runningDistanceSpeedCadence = (AggregatorRunning) sensorData; + return; + } + if (type instanceof AggregatorBarometer) { + barometer = (AggregatorBarometer) sensorData; return; } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorHandlerInterface.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorHandlerInterface.java new file mode 100644 index 000000000..89e409f5a --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/SensorHandlerInterface.java @@ -0,0 +1,17 @@ +package de.dennisguse.opentracks.sensors.sensorData; + +import android.bluetooth.BluetoothGattCharacteristic; + +import java.util.List; + +import de.dennisguse.opentracks.sensors.SensorManager; +import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID; + +public interface SensorHandlerInterface { + + List getServices(); + + Aggregator createEmptySensorData(String address); + + void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic); +} diff --git a/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java b/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java index 49384b291..f779e6ddc 100644 --- a/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java +++ b/src/main/java/de/dennisguse/opentracks/services/handlers/TrackPointCreator.java @@ -34,7 +34,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh @NonNull private Clock clock = new MonotonicClock(); - private SensorManager sensorManager; + private final SensorManager sensorManager; public TrackPointCreator(Callback service) { this.service = service; @@ -64,7 +64,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh return sensorManager.fill(trackPoint); } - public synchronized void stop() { + public void stop() { sensorManager.stop(context); this.context = null; } @@ -139,11 +139,6 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh return sensorManager; } - @VisibleForTesting - public void setSensorManager(SensorManager sensorManager) { - this.sensorManager = sensorManager; - } - @VisibleForTesting public void setClock(@NonNull String time) { this.clock = Clock.fixed(Instant.parse(time), ZoneId.of("CET")); diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadencePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadencePreference.java index bf9577621..ce0d9a2f7 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadencePreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingCadencePreference.java @@ -5,7 +5,7 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; -import de.dennisguse.opentracks.sensors.BluetoothUtils; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence; public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPreference { @@ -27,6 +27,6 @@ public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPrefer @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_CADENCE); + return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingCadence.CYCLING_CADENCE); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java index af020ce07..025251bb0 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java @@ -5,7 +5,7 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; -import de.dennisguse.opentracks.sensors.BluetoothUtils; +import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower; public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference { @@ -27,6 +27,6 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_POWER); + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerCyclingPower.CYCLING_POWER); } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java index db918d97d..5b37a9ca3 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java @@ -5,7 +5,7 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; -import de.dennisguse.opentracks.sensors.BluetoothUtils; +import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed; public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreference { @@ -27,6 +27,6 @@ public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreferen @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE); + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java index db6df1979..17183bf53 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeHeartRatePreference.java @@ -5,7 +5,7 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; -import de.dennisguse.opentracks.sensors.BluetoothUtils; +import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerHeartRate; public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference { @@ -27,6 +27,6 @@ public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.HEART_RATE_SUPPORTING_DEVICES); + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerHeartRate.HEART_RATE_SUPPORTING_DEVICES); } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java index 856c79b92..d857ace9e 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java @@ -5,7 +5,7 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; -import de.dennisguse.opentracks.sensors.BluetoothUtils; +import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence; public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference { @@ -27,6 +27,6 @@ public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSens @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothUtils.RUNNING_SPEED_CADENCE); + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE); } }