Preparing for use BLE Environmental Sensing sensor for elevation gain/loss
This commit is contained in:
Dennis Guse
2023-06-08 06:38:49 +02:00
committed by GitHub
18 changed files with 342 additions and 303 deletions
@@ -57,7 +57,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter; import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.sensors.AltitudeSumManager; import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower;
@@ -165,8 +164,7 @@ public class ExportImportTest {
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null); service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:18Z"); trackPointCreator.setClock("2020-02-02T02:02:18Z");
BluetoothRemoteSensorManager mockRemoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class); trackPointCreator.getSensorManager().sensorDataSet = new SensorDataSet();
trackPointCreator.getSensorManager().setBluetoothSensorManager(mockRemoteSensorManager);
service.endCurrentTrack(); service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:03:20Z"); trackPointCreator.setClock("2020-02-02T02:03:20Z");
@@ -179,7 +177,6 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0); sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:03:24Z"); trackPointCreator.setClock("2020-02-02T02:03:24Z");
trackPointCreator.getSensorManager().setBluetoothSensorManager(mockRemoteSensorManager);
service.endCurrentTrack(); service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
@@ -495,9 +492,6 @@ public class ExportImportTest {
} }
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) { private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
BluetoothRemoteSensorManager remoteSensorManager = Mockito.mock(BluetoothRemoteSensorManager.class);
Mockito.when(remoteSensorManager.fill(Mockito.any())).thenAnswer(invocation -> {
TrackPoint trackPoint = invocation.getArgument(0);
SensorDataSet sensorDataSet = new SensorDataSet(); SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power))); sensorDataSet.set(new SensorDataCyclingPower("power", "power", Power.of(power)));
@@ -508,21 +502,20 @@ public class ExportImportTest {
Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence)); Mockito.when(cyclingCadence.getValue()).thenReturn(Cadence.of(cadence));
sensorDataSet.set(cyclingCadence); sensorDataSet.set(cyclingCadence);
if (distance != null && speed != null) { if (distance != null && speed != null) {
SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class); SensorDataCyclingDistanceSpeed.Data distanceSpeedData = Mockito.mock(SensorDataCyclingDistanceSpeed.Data.class);
Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance); Mockito.when(distanceSpeedData.getDistanceOverall()).thenReturn(distance);
Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed)); Mockito.when(distanceSpeedData.getSpeed()).thenReturn(Speed.of(speed));
SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class); SensorDataCyclingDistanceSpeed distanceSpeed = Mockito.mock(SensorDataCyclingDistanceSpeed.class);
Mockito.when(distanceSpeed.hasValue()).thenReturn(true); Mockito.when(distanceSpeed.hasValue()).thenReturn(true);
Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData); Mockito.when(distanceSpeed.getValue()).thenReturn(distanceSpeedData);
sensorDataSet.set(distanceSpeed); sensorDataSet.set(distanceSpeed);
} }
sensorDataSet.fillTrackPoint(trackPoint); trackPointCreator.getSensorManager().sensorDataSet = sensorDataSet;
return sensorDataSet; trackPointCreator.onChange(sensorDataSet);
});
trackPointCreator.getSensorManager().setBluetoothSensorManager(remoteSensorManager); //TODO Might require: getSensorManager().sensorDataSet = new SensorDataSet()
trackPointCreator.onChange(new SensorDataSet());
} }
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) { private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
@@ -545,6 +538,6 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, altitudeGain); mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time); trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location); trackPointCreator.onChange(location);
} }
} }
@@ -1,47 +1,55 @@
package de.dennisguse.opentracks.sensors; package de.dennisguse.opentracks.sensors;
import static org.mockito.Mockito.when;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before;
import org.junit.Test; 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. * NOTE: Test data is completely artificial.
*/ */
@RunWith(MockitoJUnitRunner.class)
public class AltitudeSumManagerTest { public class AltitudeSumManagerTest {
private final AltitudeSumManager subject = new AltitudeSumManager();
private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) { private static void addSensorValue(AltitudeSumManager altitudeSumManager, float[] values) {
for (float f : values) { for (float f : values) {
altitudeSumManager.onSensorValueChanged(f); altitudeSumManager.onSensorValueChanged(AtmosphericPressure.ofHPA(f));
} }
} }
@Before @Mock
public void setUp() { BarometerInternal driver;
subject.reset();
}
@Test @Test
public void getAltitudeGainLoss_downhill() { public void getAltitudeGainLoss_downhill() {
// given // given
subject.setConnected(true); AltitudeSumManager subject = new AltitudeSumManager(driver);
when(driver.isConnected()).thenReturn(true);
// then // 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, 1015.14f, 1015.15f}); 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 // then
Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01); Assert.assertEquals(0f, subject.getAltitudeGain_m(), 0.01);
Assert.assertEquals(48.0, subject.getAltitudeLoss_m(), 0.01); Assert.assertEquals(15f, subject.getAltitudeLoss_m(), 0.01);
} }
@Test @Test
public void sensorUnavailable() { public void sensorUnavailable() {
// given // given
subject.setConnected(false); AltitudeSumManager subject = new AltitudeSumManager(driver);
when(driver.isConnected()).thenReturn(true);
// then // then
subject.onSensorValueChanged(999f); subject.onSensorValueChanged(AtmosphericPressure.ofHPA(999f));
// then // then
Assert.assertNull(subject.getAltitudeGain_m()); Assert.assertNull(subject.getAltitudeGain_m());
@@ -7,6 +7,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Test; import org.junit.Test;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.HeartRate;
@@ -43,6 +44,19 @@ public class BluetoothUtilsTest {
assertEquals(HeartRate.of(257), heartRate); assertEquals(HeartRate.of(257), heartRate);
} }
@Test
public void parseEnvironmentalSensing_Pa() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.BAROMETRIC_PRESSURE.getServiceUUID(), 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 @Test
public void parseCyclingSpeedCadence_crankOnly() { public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0); BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE.getServiceUUID(), 0, 0);
@@ -5,6 +5,8 @@ import static org.junit.Assert.assertNull;
import org.junit.Test; import org.junit.Test;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
public class PressureSensorUtilsTest { public class PressureSensorUtilsTest {
// real data at 500Hz stationary // real data at 500Hz stationary
@@ -12,11 +14,11 @@ public class PressureSensorUtilsTest {
public void altitudeChanges_none() { public void altitudeChanges_none() {
// given // given
float[] sensorValues_hPa = new float[]{1015.6876f, 1015.699f, 1015.70905f, 1015.71075f, 1015.7224f, 1015.72f, 1015.7373f, 1015.7481f, 1015.74133f, 1015.73553f, 1015.7462f, 1015.74896f, 1015.752f, 1015.754f, 1015.7456f, 1015.73303f, 1015.7288f, 1015.7239f, 1015.7298f, 1015.7387f, 1015.72534f, 1015.72577f, 1015.72797f, 1015.71796f, 1015.7113f, 1015.7097f, 1015.7096f, 1015.7013f, 1015.7072f, 1015.71063f, 1015.7088f, 1015.7115f, 1015.713f, 1015.7121f, 1015.7062f, 1015.7062f, 1015.6994f, 1015.69617f, 1015.68945f, 1015.698f, 1015.6913f, 1015.69696f, 1015.69293f, 1015.6955f, 1015.6928f, 1015.6939f, 1015.68787f, 1015.6881f, 1015.68536f, 1015.68726f, 1015.6879f, 1015.6904f, 1015.6937f, 1015.69476f, 1015.7046f, 1015.7019f, 1015.7079f, 1015.71124f, 1015.71216f, 1015.71436f, 1015.7171f, 1015.7186f, 1015.7186f, 1015.7246f, 1015.72046f, 1015.7211f, 1015.72375f, 1015.7194f, 1015.72125f, 1015.7161f, 1015.71185f, 1015.7188f, 1015.721f, 1015.7252f, 1015.72266f, 1015.7236f, 1015.72614f, 1015.7266f, 1015.7344f, 1015.7376f, 1015.7337f, 1015.7253f, 1015.71875f, 1015.7219f, 1015.7219f, 1015.7317f, 1015.7284f, 1015.73444f, 1015.73584f, 1015.7369f, 1015.73254f, 1015.7369f, 1015.7336f, 1015.73254f, 1015.73676f, 1015.74176f, 1015.74506f, 1015.7478f, 1015.755f, 1015.7515f, 1015.7543f, 1015.7459f, 1015.7468f, 1015.7492f, 1015.74585f, 1015.74756f, 1015.74756f, 1015.75085f, 1015.754f, 1015.75336f, 1015.7568f, 1015.75165f, 1015.761f, 1015.7527f, 1015.75684f, 1015.7635f, 1015.7585f, 1015.7552f, 1015.7475f, 1015.74506f, 1015.7542f, 1015.75586f, 1015.74176f, 1015.74414f, 1015.7501f, 1015.751f, 1015.7478f, 1015.755f, 1015.75836f, 1015.7592f, 1015.75256f, 1015.7559f, 1015.76654f, 1015.76154f, 1015.766f, 1015.77014f, 1015.76904f, 1015.76013f, 1015.7674f, 1015.76514f, 1015.7674f, 1015.7635f, 1015.76404f, 1015.7607f, 1015.754f, 1015.7574f, 1015.7607f, 1015.7607f, 1015.7607f, 1015.754f, 1015.7567f, 1015.7616f, 1015.7643f, 1015.7666f, 1015.7609f, 1015.766f, 1015.76764f, 1015.755f, 1015.75757f, 1015.74927f, 1015.74927f, 1015.7426f, 1015.745f, 1015.745f, 1015.7409f, 1015.7376f, 1015.7326f, 1015.74164f, 1015.74664f, 1015.745f, 1015.7459f, 1015.7502f, 1015.7493f, 1015.7476f, 1015.7493f, 1015.751f, 1015.7601f, 1015.76434f, 1015.766f, 1015.756f, 1015.7617f, 1015.7623f, 1015.76404f, 1015.76733f, 1015.7607f, 1015.7623f, 1015.76337f, 1015.7584f, 1015.7573f, 1015.7502f, 1015.7534f, 1015.7534f, 1015.7601f, 1015.75684f, 1015.7518f, 1015.75574f, 1015.7524f, 1015.7507f, 1015.7484f, 1015.7502f, 1015.7534f, 1015.7623f, 1015.75903f, 1015.75574f, 1015.75574f, 1015.7573f, 1015.7524f, 1015.75574f, 1015.75574f, 1015.7607f, 1015.7573f, 1015.754f, 1015.74677f, 1015.754f, 1015.75574f, 1015.75903f, 1015.7573f, 1015.7551f, 1015.7573f, 1015.754f, 1015.7507f, 1015.7374f, 1015.739f, 1015.73566f, 1015.73566f, 1015.7367f, 1015.7417f, 1015.7417f, 1015.739f, 1015.74835f, 1015.74567f, 1015.7517f, 1015.74066f, 1015.7374f, 1015.739f, 1015.7417f, 1015.73065f, 1015.72894f, 1015.73505f, 1015.73334f, 1015.74005f, 1015.74005f, 1015.7367f, 1015.7434f, 1015.744f, 1015.7423f, 1015.7384f, 1015.739f, 1015.7374f, 1015.7374f, 1015.7374f, 1015.7341f, 1015.73505f, 1015.7341f, 1015.7267f, 1015.72833f, 1015.7234f, 1015.7167f, 1015.725f, 1015.7217f, 1015.7234f, 1015.73004f, 1015.7324f, 1015.7423f, 1015.74005f, 1015.73505f, 1015.73004f, 1015.739f, 1015.7417f, 1015.74567f, 1015.74066f, 1015.74146f, 1015.74146f, 1015.73816f, 1015.7274f, 1015.72906f, 1015.7274f, 1015.7341f, 1015.739f, 1015.744f, 1015.7417f, 1015.7374f, 1015.7324f, 1015.73816f, 1015.74146f, 1015.7374f, 1015.7341f, 1015.7365f, 1015.7365f, 1015.74146f, 1015.74414f, 1015.7475f, 1015.7448f, 1015.74146f, 1015.74585f, 1015.7475f, 1015.75146f, 1015.75146f, 1015.7591f, 1015.75476f, 1015.7614f, 1015.75476f, 1015.7492f, 1015.74817f, 1015.7498f, 1015.7515f, 1015.75586f, 1015.7509f, 1015.7448f, 1015.7525f, 1015.7581f, 1015.7592f}; float[] sensorValues_hPa = new float[]{1015.6876f, 1015.699f, 1015.70905f, 1015.71075f, 1015.7224f, 1015.72f, 1015.7373f, 1015.7481f, 1015.74133f, 1015.73553f, 1015.7462f, 1015.74896f, 1015.752f, 1015.754f, 1015.7456f, 1015.73303f, 1015.7288f, 1015.7239f, 1015.7298f, 1015.7387f, 1015.72534f, 1015.72577f, 1015.72797f, 1015.71796f, 1015.7113f, 1015.7097f, 1015.7096f, 1015.7013f, 1015.7072f, 1015.71063f, 1015.7088f, 1015.7115f, 1015.713f, 1015.7121f, 1015.7062f, 1015.7062f, 1015.6994f, 1015.69617f, 1015.68945f, 1015.698f, 1015.6913f, 1015.69696f, 1015.69293f, 1015.6955f, 1015.6928f, 1015.6939f, 1015.68787f, 1015.6881f, 1015.68536f, 1015.68726f, 1015.6879f, 1015.6904f, 1015.6937f, 1015.69476f, 1015.7046f, 1015.7019f, 1015.7079f, 1015.71124f, 1015.71216f, 1015.71436f, 1015.7171f, 1015.7186f, 1015.7186f, 1015.7246f, 1015.72046f, 1015.7211f, 1015.72375f, 1015.7194f, 1015.72125f, 1015.7161f, 1015.71185f, 1015.7188f, 1015.721f, 1015.7252f, 1015.72266f, 1015.7236f, 1015.72614f, 1015.7266f, 1015.7344f, 1015.7376f, 1015.7337f, 1015.7253f, 1015.71875f, 1015.7219f, 1015.7219f, 1015.7317f, 1015.7284f, 1015.73444f, 1015.73584f, 1015.7369f, 1015.73254f, 1015.7369f, 1015.7336f, 1015.73254f, 1015.73676f, 1015.74176f, 1015.74506f, 1015.7478f, 1015.755f, 1015.7515f, 1015.7543f, 1015.7459f, 1015.7468f, 1015.7492f, 1015.74585f, 1015.74756f, 1015.74756f, 1015.75085f, 1015.754f, 1015.75336f, 1015.7568f, 1015.75165f, 1015.761f, 1015.7527f, 1015.75684f, 1015.7635f, 1015.7585f, 1015.7552f, 1015.7475f, 1015.74506f, 1015.7542f, 1015.75586f, 1015.74176f, 1015.74414f, 1015.7501f, 1015.751f, 1015.7478f, 1015.755f, 1015.75836f, 1015.7592f, 1015.75256f, 1015.7559f, 1015.76654f, 1015.76154f, 1015.766f, 1015.77014f, 1015.76904f, 1015.76013f, 1015.7674f, 1015.76514f, 1015.7674f, 1015.7635f, 1015.76404f, 1015.7607f, 1015.754f, 1015.7574f, 1015.7607f, 1015.7607f, 1015.7607f, 1015.754f, 1015.7567f, 1015.7616f, 1015.7643f, 1015.7666f, 1015.7609f, 1015.766f, 1015.76764f, 1015.755f, 1015.75757f, 1015.74927f, 1015.74927f, 1015.7426f, 1015.745f, 1015.745f, 1015.7409f, 1015.7376f, 1015.7326f, 1015.74164f, 1015.74664f, 1015.745f, 1015.7459f, 1015.7502f, 1015.7493f, 1015.7476f, 1015.7493f, 1015.751f, 1015.7601f, 1015.76434f, 1015.766f, 1015.756f, 1015.7617f, 1015.7623f, 1015.76404f, 1015.76733f, 1015.7607f, 1015.7623f, 1015.76337f, 1015.7584f, 1015.7573f, 1015.7502f, 1015.7534f, 1015.7534f, 1015.7601f, 1015.75684f, 1015.7518f, 1015.75574f, 1015.7524f, 1015.7507f, 1015.7484f, 1015.7502f, 1015.7534f, 1015.7623f, 1015.75903f, 1015.75574f, 1015.75574f, 1015.7573f, 1015.7524f, 1015.75574f, 1015.75574f, 1015.7607f, 1015.7573f, 1015.754f, 1015.74677f, 1015.754f, 1015.75574f, 1015.75903f, 1015.7573f, 1015.7551f, 1015.7573f, 1015.754f, 1015.7507f, 1015.7374f, 1015.739f, 1015.73566f, 1015.73566f, 1015.7367f, 1015.7417f, 1015.7417f, 1015.739f, 1015.74835f, 1015.74567f, 1015.7517f, 1015.74066f, 1015.7374f, 1015.739f, 1015.7417f, 1015.73065f, 1015.72894f, 1015.73505f, 1015.73334f, 1015.74005f, 1015.74005f, 1015.7367f, 1015.7434f, 1015.744f, 1015.7423f, 1015.7384f, 1015.739f, 1015.7374f, 1015.7374f, 1015.7374f, 1015.7341f, 1015.73505f, 1015.7341f, 1015.7267f, 1015.72833f, 1015.7234f, 1015.7167f, 1015.725f, 1015.7217f, 1015.7234f, 1015.73004f, 1015.7324f, 1015.7423f, 1015.74005f, 1015.73505f, 1015.73004f, 1015.739f, 1015.7417f, 1015.74567f, 1015.74066f, 1015.74146f, 1015.74146f, 1015.73816f, 1015.7274f, 1015.72906f, 1015.7274f, 1015.7341f, 1015.739f, 1015.744f, 1015.7417f, 1015.7374f, 1015.7324f, 1015.73816f, 1015.74146f, 1015.7374f, 1015.7341f, 1015.7365f, 1015.7365f, 1015.74146f, 1015.74414f, 1015.7475f, 1015.7448f, 1015.74146f, 1015.74585f, 1015.7475f, 1015.75146f, 1015.75146f, 1015.7591f, 1015.75476f, 1015.7614f, 1015.75476f, 1015.7492f, 1015.74817f, 1015.7498f, 1015.7515f, 1015.75586f, 1015.7509f, 1015.7448f, 1015.7525f, 1015.7581f, 1015.7592f};
float firstSensorValue = sensorValues_hPa[0]; AtmosphericPressure firstSensorValue = AtmosphericPressure.ofHPA(sensorValues_hPa[0]);
// when // then // when // then
for (float v : sensorValues_hPa) { for (float v : sensorValues_hPa) {
assertNull(PressureSensorUtils.computeChanges(firstSensorValue, v)); assertNull(PressureSensorUtils.computeChanges(firstSensorValue, AtmosphericPressure.ofHPA(v)));
} }
} }
@@ -25,25 +27,25 @@ public class PressureSensorUtilsTest {
@Test @Test
public void computeChanges_downhill() { public void computeChanges_downhill() {
float[] sensorValues_hPa = 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}; float[] sensorValues_hPa = 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};
float firstSensorValue = sensorValues_hPa[0]; AtmosphericPressure firstSensorValue = AtmosphericPressure.ofHPA(sensorValues_hPa[0]);
// when // when
float altitudeGain_m = 0; float altitudeGain_m = 0;
float altitudeLoss = 0; float altitudeLoss = 0;
float lastUsedPressureValue_hPa = firstSensorValue; AtmosphericPressure lastUsedPressureValue = firstSensorValue;
for (float v : sensorValues_hPa) { for (float v : sensorValues_hPa) {
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChanges(lastUsedPressureValue_hPa, v); PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChanges(lastUsedPressureValue, AtmosphericPressure.ofHPA(v));
if (altitudeChange != null) { if (altitudeChange != null) {
altitudeGain_m += altitudeChange.getAltitudeGain_m(); altitudeGain_m += altitudeChange.getAltitudeGain_m();
altitudeLoss += altitudeChange.getAltitudeLoss_m(); altitudeLoss += altitudeChange.getAltitudeLoss_m();
lastUsedPressureValue_hPa = altitudeChange.getCurrentSensorValue_hPa(); lastUsedPressureValue = altitudeChange.getCurrentSensorValue();
} }
} }
// then // then
assertEquals(0f, altitudeGain_m, 0.01); assertEquals(0f, altitudeGain_m, 0.01);
assertEquals(15.0f, altitudeLoss, 0.01); assertEquals(15.0f, altitudeLoss, 0.01);
assertEquals(1016.80f, lastUsedPressureValue_hPa, 0.01); //Expect exponential smoothing assertEquals(1016.80f, lastUsedPressureValue.getHPA(), 0.01); //Expect exponential smoothing
} }
} }
@@ -30,6 +30,6 @@ public class TrackRecordingServiceTestUtils {
location.setSpeed(speed); location.setSpeed(speed);
trackPointCreator.setClock(time); trackPointCreator.setClock(time);
trackPointCreator.getGpsHandler().onLocationChanged(location); trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
} }
} }
@@ -1,57 +0,0 @@
package de.dennisguse.opentracks.services.handlers;
import static org.mockito.Mockito.verify;
import android.content.Context;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
@RunWith(MockitoJUnitRunner.class)
public class TrackPointCreatorTest {
@Mock
private Context context;
@Mock
private TrackPointCreator.Callback server;
@Mock
private GPSManager locationHandler;
private TrackPointCreator subject;
@Before
public void setUp() {
subject = new TrackPointCreator(locationHandler, server);
subject.start(context, null);
}
@After
public void tearDown() {
subject.stop();
}
@Ignore("ServiceExecutor disabled for #822")
@Test
public void sendTrackPoint() throws InterruptedException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, null);
Distance horizontalAccuracyThreshold = Distance.of(50);
// when
subject.onNewTrackPoint(trackPoint);
// then
Thread.sleep(10); // Wait for executor service
verify(server).newTrackPoint(trackPoint, horizontalAccuracyThreshold);
}
}
@@ -0,0 +1,48 @@
package de.dennisguse.opentracks.data.models;
import java.util.Objects;
public class AtmosphericPressure {
public static AtmosphericPressure ofPA(float value_Pa) {
return new AtmosphericPressure(value_Pa * 100);
}
public static AtmosphericPressure ofHPA(float value_hPa) {
return new AtmosphericPressure(value_hPa);
}
private final float value;
private AtmosphericPressure(float value) {
this.value = value;
}
public float getPA() {
return value * 100;
}
public float getHPA() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AtmosphericPressure that = (AtmosphericPressure) o;
return Float.compare(that.value, value) == 0;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return "AtmosphericPressure{" +
"value=" + value +
'}';
}
}
@@ -189,6 +189,7 @@ public abstract class AbstractBluetoothConnectionManager<DataType> {
*/ */
protected abstract SensorData<DataType> parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, @NonNull BluetoothGattCharacteristic characteristic); protected abstract SensorData<DataType> parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, @NonNull BluetoothGattCharacteristic characteristic);
@Deprecated //TODO REMOVE
interface SensorDataObserver { interface SensorDataObserver {
void onChanged(SensorData<?> sensorData); void onChanged(SensorData<?> sensorData);
@@ -1,10 +1,6 @@
package de.dennisguse.opentracks.sensors; package de.dennisguse.opentracks.sensors;
import android.content.Context; import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler; import android.os.Handler;
import android.util.Log; import android.util.Log;
@@ -12,56 +8,49 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import java.util.concurrent.TimeUnit; import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.TrackPoint; 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). * Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
*/ */
public class AltitudeSumManager implements SensorConnector, SensorEventListener { public class AltitudeSumManager implements SensorConnector {
private static final String TAG = AltitudeSumManager.class.getSimpleName(); private static final String TAG = AltitudeSumManager.class.getSimpleName();
private boolean isConnected = false; private final BarometerInternal driver;
private float lastAcceptedPressureValue_hPa; private AtmosphericPressure lastAcceptedSensorValue;
private float lastSeenSensorValue_hPa; private AtmosphericPressure lastSeenSensorValue;
private Float altitudeGain_m; private Float altitudeGain_m;
private Float altitudeLoss_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) { public void start(Context context, Handler handler) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); driver.connect(context, handler, this);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE); lastAcceptedSensorValue = null;
if (pressureSensor == null) {
Log.w(TAG, "No pressure sensor available.");
isConnected = false;
} else {
isConnected = sensorManager.registerListener(this, pressureSensor, (int) TimeUnit.SECONDS.toMicros(5), handler);
}
lastAcceptedPressureValue_hPa = Float.NaN;
reset(); reset();
} }
public void stop(Context context) { public void stop(Context context) {
Log.d(TAG, "Stop"); Log.d(TAG, "Stop");
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); driver.disconnect(context);
sensorManager.unregisterListener(this);
isConnected = false;
reset(); reset();
} }
@VisibleForTesting
public void setConnected(boolean isConnected) {
this.isConnected = isConnected;
}
public void fill(@NonNull TrackPoint trackPoint) { public void fill(@NonNull TrackPoint trackPoint) {
trackPoint.setAltitudeGain(altitudeGain_m); trackPoint.setAltitudeGain(altitudeGain_m);
trackPoint.setAltitudeLoss(altitudeLoss_m); trackPoint.setAltitudeLoss(altitudeLoss_m);
@@ -69,7 +58,7 @@ public class AltitudeSumManager implements SensorConnector, SensorEventListener
@Nullable @Nullable
public Float getAltitudeGain_m() { public Float getAltitudeGain_m() {
return isConnected ? altitudeGain_m : null; return driver.isConnected() ? altitudeGain_m : null;
} }
@VisibleForTesting @VisibleForTesting
@@ -77,21 +66,9 @@ public class AltitudeSumManager implements SensorConnector, SensorEventListener
this.altitudeGain_m = altitudeGain_m; this.altitudeGain_m = altitudeGain_m;
} }
@VisibleForTesting
public void addAltitudeGain_m(float altitudeGain_m) {
this.altitudeGain_m = this.altitudeGain_m == null ? 0f : this.altitudeGain_m;
this.altitudeGain_m += altitudeGain_m;
}
@VisibleForTesting
public void addAltitudeLoss_m(Float altitudeLoss_m) {
this.altitudeLoss_m = this.altitudeLoss_m == null ? 0f : this.altitudeLoss_m;
this.altitudeLoss_m += altitudeLoss_m;
}
@Nullable @Nullable
public Float getAltitudeLoss_m() { public Float getAltitudeLoss_m() {
return isConnected ? altitudeLoss_m : null; return driver.isConnected() ? altitudeLoss_m : null;
} }
@VisibleForTesting @VisibleForTesting
@@ -105,41 +82,26 @@ public class AltitudeSumManager implements SensorConnector, SensorEventListener
altitudeLoss_m = null; altitudeLoss_m = null;
} }
@Override public void onSensorValueChanged(AtmosphericPressure currentSensorValue) {
public void onAccuracyChanged(Sensor sensor, int accuracy) { if (lastAcceptedSensorValue == null) {
Log.w(TAG, "Sensor accuracy changes are (currently) ignored."); lastAcceptedSensorValue = currentSensorValue;
} lastSeenSensorValue = currentSensorValue;
@Override
public void onSensorChanged(SensorEvent event) {
if (!isConnected) {
Log.w(TAG, "Not connected to sensor, cannot process data.");
return;
}
onSensorValueChanged(event.values[0]);
}
@VisibleForTesting
void onSensorValueChanged(float value_hPa) {
if (Float.isNaN(lastAcceptedPressureValue_hPa)) {
lastAcceptedPressureValue_hPa = value_hPa;
lastSeenSensorValue_hPa = value_hPa;
return; return;
} }
altitudeGain_m = altitudeGain_m != null ? altitudeGain_m : 0; altitudeGain_m = altitudeGain_m != null ? altitudeGain_m : 0;
altitudeLoss_m = altitudeLoss_m != null ? altitudeLoss_m : 0; altitudeLoss_m = altitudeLoss_m != null ? altitudeLoss_m : 0;
PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedPressureValue_hPa, lastSeenSensorValue_hPa, value_hPa); PressureSensorUtils.AltitudeChange altitudeChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedSensorValue, lastSeenSensorValue, currentSensorValue);
if (altitudeChange != null) { if (altitudeChange != null) {
altitudeGain_m += altitudeChange.getAltitudeGain_m(); altitudeGain_m += altitudeChange.getAltitudeGain_m();
altitudeLoss_m += altitudeChange.getAltitudeLoss_m(); altitudeLoss_m += altitudeChange.getAltitudeLoss_m();
lastAcceptedPressureValue_hPa = altitudeChange.getCurrentSensorValue_hPa(); lastAcceptedSensorValue = altitudeChange.getCurrentSensorValue();
} }
lastSeenSensorValue_hPa = value_hPa; lastSeenSensorValue = currentSensorValue;
Log.v(TAG, "altitude gain: " + altitudeGain_m + ", altitude loss: " + altitudeLoss_m); Log.v(TAG, "altitude gain: " + altitudeGain_m + ", altitude loss: " + altitudeLoss_m);
} }
@@ -24,17 +24,12 @@ import android.os.Handler;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorData; import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester; import de.dennisguse.opentracks.util.PermissionRequester;
@@ -61,20 +56,15 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
private final BluetoothAdapter bluetoothAdapter; private final BluetoothAdapter bluetoothAdapter;
private final Context context; private final Context context;
private final Handler handler; private final Handler handler;
private final SensorManager.SensorDataChangedObserver observer;
private boolean started = false; private boolean started = false;
private Distance preferenceWheelCircumference;
private final BluetoothConnectionManagerHeartRate heartRate = new BluetoothConnectionManagerHeartRate(this); private final BluetoothConnectionManagerHeartRate heartRate = new BluetoothConnectionManagerHeartRate(this);
private final BluetoothConnectionManagerCyclingCadence cyclingCadence = new BluetoothConnectionManagerCyclingCadence(this); private final BluetoothConnectionManagerCyclingCadence cyclingCadence = new BluetoothConnectionManagerCyclingCadence(this);
private final BluetoothConnectionManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManagerCyclingDistanceSpeed(this); private final BluetoothConnectionManagerCyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManagerCyclingDistanceSpeed(this);
private final BluetoothConnectionManagerCyclingPower cyclingPower = new BluetoothConnectionManagerCyclingPower(this); private final BluetoothConnectionManagerCyclingPower cyclingPower = new BluetoothConnectionManagerCyclingPower(this);
private final BluetoothConnectionRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionRunningSpeedAndCadence(this); private final BluetoothConnectionRunningSpeedAndCadence runningSpeedAndCadence = new BluetoothConnectionRunningSpeedAndCadence(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
private final SensorManager.SensorDataSetChangeObserver observer;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
@@ -95,9 +85,6 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
connect(cyclingSpeed, address); connect(cyclingSpeed, address);
} }
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, key)) {
preferenceWheelCircumference = PreferencesUtils.getWheelCircumference();
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_power_key, key)) { if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress(); String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress();
@@ -114,7 +101,7 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
} }
}; };
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @NonNull SensorManager.SensorDataSetChangeObserver observer) { public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
this.context = context; this.context = context;
this.handler = handler; this.handler = handler;
this.observer = observer; this.observer = observer;
@@ -137,8 +124,6 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
cyclingPower.disconnect(); cyclingPower.disconnect();
runningSpeedAndCadence.disconnect(); runningSpeedAndCadence.disconnect();
sensorDataSet.clear();
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
started = false; started = false;
} }
@@ -178,53 +163,14 @@ public class BluetoothRemoteSensorManager implements SensorConnector, AbstractBl
} }
} }
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
sensorDataSet.fillTrackPoint(trackPoint);
return new SensorDataSet(sensorDataSet);
}
public void reset() {
sensorDataSet.reset();
}
@Override @Override
public synchronized void onChanged(SensorData<?> sensorData) { public synchronized void onChanged(SensorData<?> sensorData) {
if (sensorData instanceof SensorDataCyclingCadence) { observer.onChange(sensorData);
SensorDataCyclingCadence previous = sensorDataSet.getCyclingCadence();
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cadence data repeated.");
return;
}
((SensorDataCyclingCadence) sensorData).compute(previous);
}
if (sensorData instanceof SensorDataCyclingDistanceSpeed) {
SensorDataCyclingDistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cycling speed data repeated.");
return;
}
((SensorDataCyclingDistanceSpeed) sensorData).compute(previous, preferenceWheelCircumference);
}
if (sensorData instanceof SensorDataRunning) {
SensorDataRunning previous = sensorDataSet.getRunningDistanceSpeedCadence();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: running speed data repeated.");
return;
}
((SensorDataRunning) sensorData).compute(previous);
}
sensorDataSet.set(sensorData);
observer.onChange(new SensorDataSet(sensorDataSet));
} }
@Override @Override
public void onDisconnecting(SensorData<?> sensorData) { public void onDisconnecting(SensorData<?> sensorData) {
sensorDataSet.remove(sensorData); observer.onDisconnect(sensorData);
} }
@NonNull @NonNull
@@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.BatteryLevel; import de.dennisguse.opentracks.data.models.BatteryLevel;
import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
@@ -69,6 +70,12 @@ public class BluetoothUtils {
)) ))
); );
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( public static final ServiceMeasurementUUID CYCLING_POWER = new ServiceMeasurementUUID(
new UUID(0x181800001000L, 0x800000805f9b34fbL), new UUID(0x181800001000L, 0x800000805f9b34fbL),
new UUID(0x2A6300001000L, 0x800000805f9b34fbL) new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
@@ -137,6 +144,17 @@ public class BluetoothUtils {
return null; 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) { 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 // 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; int valueLength = characteristic.getValue().length;
@@ -4,6 +4,8 @@ import android.hardware.SensorManager;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
public class PressureSensorUtils { public class PressureSensorUtils {
//Everything above is considered a meaningful change in altitude. //Everything above is considered a meaningful change in altitude.
@@ -18,17 +20,17 @@ public class PressureSensorUtils {
public static class AltitudeChange { public static class AltitudeChange {
private final float currentSensorValue_hPa; private final AtmosphericPressure currentSensorValue;
private final float altitudeChange_m; private final float altitudeChange_m;
public AltitudeChange(float currentSensorValue_hPa, float altitudeChange_m) { public AltitudeChange(AtmosphericPressure currentSensorValue, float altitudeChange_m) {
this.currentSensorValue_hPa = currentSensorValue_hPa; this.currentSensorValue = currentSensorValue;
this.altitudeChange_m = altitudeChange_m; this.altitudeChange_m = altitudeChange_m;
} }
public float getCurrentSensorValue_hPa() { public AtmosphericPressure getCurrentSensorValue() {
return currentSensorValue_hPa; return currentSensorValue;
} }
public float getAltitudeChange_m() { public float getAltitudeChange_m() {
@@ -47,16 +49,16 @@ public class PressureSensorUtils {
/** /**
* Applies exponential smoothing to sensor value before computation. * Applies exponential smoothing to sensor value before computation.
*/ */
public static AltitudeChange computeChangesWithSmoothing_m(float lastAcceptedSensorValue_hPa, float lastSeenSensorValue_hPa, float currentSensorValue_hPa) { public static AltitudeChange computeChangesWithSmoothing_m(AtmosphericPressure lastAcceptedSensorValue, AtmosphericPressure lastSeenSensorValue, AtmosphericPressure currentSensorValue) {
float nextSensorValue_hPa = EXPONENTIAL_SMOOTHING * currentSensorValue_hPa + (1 - EXPONENTIAL_SMOOTHING) * lastSeenSensorValue_hPa; AtmosphericPressure nextSensorValue = AtmosphericPressure.ofHPA(EXPONENTIAL_SMOOTHING * currentSensorValue.getHPA() + (1 - EXPONENTIAL_SMOOTHING) * lastSeenSensorValue.getHPA());
return computeChanges(lastAcceptedSensorValue_hPa, nextSensorValue_hPa); return computeChanges(lastAcceptedSensorValue, nextSensorValue);
} }
@VisibleForTesting @VisibleForTesting
public static AltitudeChange computeChanges(float lastAcceptedSensorValue_hPa, float currentSensorValue_hPa) { public static AltitudeChange computeChanges(AtmosphericPressure lastAcceptedSensorValue, AtmosphericPressure currentSensorValue) {
float lastSensorValue_m = SensorManager.getAltitude(p0, lastAcceptedSensorValue_hPa); float lastSensorValue_m = SensorManager.getAltitude(p0, lastAcceptedSensorValue.getHPA());
float currentSensorValue_m = SensorManager.getAltitude(p0, currentSensorValue_hPa); float currentSensorValue_m = SensorManager.getAltitude(p0, currentSensorValue.getHPA());
float altitudeChange_m = currentSensorValue_m - lastSensorValue_m; float altitudeChange_m = currentSensorValue_m - lastSensorValue_m;
if (Math.abs(altitudeChange_m) < ALTITUDE_CHANGE_DIFF_M) { if (Math.abs(altitudeChange_m) < ALTITUDE_CHANGE_DIFF_M) {
@@ -64,7 +66,7 @@ public class PressureSensorUtils {
} }
// Limit altitudeC change by ALTITUDE_CHANGE_DIFF and computes pressure value accordingly. // Limit altitudeC change by ALTITUDE_CHANGE_DIFF and computes pressure value accordingly.
AltitudeChange altitudeChange = new AltitudeChange(currentSensorValue_hPa, altitudeChange_m); AltitudeChange altitudeChange = new AltitudeChange(currentSensorValue, altitudeChange_m);
if (altitudeChange.getAltitudeChange_m() > 0) { if (altitudeChange.getAltitudeChange_m() > 0) {
return new AltitudeChange(getBarometricPressure(lastSensorValue_m + ALTITUDE_CHANGE_DIFF_M), ALTITUDE_CHANGE_DIFF_M); return new AltitudeChange(getBarometricPressure(lastSensorValue_m + ALTITUDE_CHANGE_DIFF_M), ALTITUDE_CHANGE_DIFF_M);
} else { } else {
@@ -78,7 +80,7 @@ public class PressureSensorUtils {
* {\color{White} p(h)} = p_0 \cdot \left( 1 - \frac{0{,}0065 \frac{\mathrm K}{\mathrm m} \cdot h}{T_0\ } \right)^{5{,}255} * {\color{White} p(h)} = p_0 \cdot \left( 1 - \frac{0{,}0065 \frac{\mathrm K}{\mathrm m} \cdot h}{T_0\ } \right)^{5{,}255}
*/ */
@VisibleForTesting @VisibleForTesting
public static float getBarometricPressure(float altitude_m) { public static AtmosphericPressure getBarometricPressure(float altitude_m) {
return (float) (p0 * Math.pow(1.0 - 0.0065 * altitude_m / 288.15, 5.255f)); return AtmosphericPressure.ofHPA((float) (p0 * Math.pow(1.0 - 0.0065 * altitude_m / 288.15, 5.255f)));
} }
} }
@@ -9,22 +9,52 @@ import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.sensorData.SensorData; import de.dennisguse.opentracks.sensors.sensorData.SensorData;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.GPSManager;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
public class SensorManager { public class SensorManager {
private static final String TAG = SensorManager.class.getSimpleName(); private static final String TAG = SensorManager.class.getSimpleName();
//TODO Should be final and not be visible for testing
@VisibleForTesting
public SensorDataSet sensorDataSet = new SensorDataSet();
private final TrackPointCreator observer;
private final SensorDataChangedObserver listener = new SensorDataChangedObserver() {
@Override
public void onChange(SensorData<?> sensorData) {
sensorDataSet.set(sensorData);
observer.onChange(new SensorDataSet(sensorDataSet));
}
@Override
public void onDisconnect(SensorData<?> sensorData) {
sensorDataSet.remove(sensorData);
observer.onChange(new SensorDataSet(sensorDataSet));
}
};
private BluetoothRemoteSensorManager bluetoothSensorManager; private BluetoothRemoteSensorManager bluetoothSensorManager;
private AltitudeSumManager altitudeSumManager; private AltitudeSumManager altitudeSumManager;
public SensorManager(Context context, Handler handler, SensorDataSetChangeObserver observer) { private GPSManager gpsManager;
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, observer);
altitudeSumManager = new AltitudeSumManager(); public SensorManager(TrackPointCreator observer) {
this.observer = observer;
} }
public void start(Context context, Handler handler) { public void start(Context context, Handler handler) {
gpsManager = new GPSManager(observer); //TODO Pass listener
gpsManager.start(context, handler);
bluetoothSensorManager = new BluetoothRemoteSensorManager(context, handler, listener);
bluetoothSensorManager.start(context, handler); bluetoothSensorManager.start(context, handler);
altitudeSumManager = new AltitudeSumManager();
altitudeSumManager.start(context, handler); altitudeSumManager.start(context, handler);
} }
@@ -38,11 +68,19 @@ public class SensorManager {
altitudeSumManager.stop(context); altitudeSumManager.stop(context);
altitudeSumManager = null; altitudeSumManager = null;
} }
if (gpsManager != null) {
gpsManager.stop(context);
gpsManager = null;
}
sensorDataSet.clear();
} }
public SensorDataSet fill(TrackPoint trackPoint) { public SensorDataSet fill(TrackPoint trackPoint) {
altitudeSumManager.fill(trackPoint); altitudeSumManager.fill(trackPoint);
return bluetoothSensorManager.fill(trackPoint); sensorDataSet.fillTrackPoint(trackPoint);
return new SensorDataSet(sensorDataSet);
} }
public void reset() { public void reset() {
@@ -50,7 +88,7 @@ public class SensorManager {
Log.d(TAG, "No recording running and no reset necessary."); Log.d(TAG, "No recording running and no reset necessary.");
return; return;
} }
bluetoothSensorManager.reset(); sensorDataSet.reset();
altitudeSumManager.reset(); altitudeSumManager.reset();
} }
@@ -60,10 +98,8 @@ public class SensorManager {
return bluetoothSensorManager; return bluetoothSensorManager;
} }
@Deprecated public GPSManager getGpsManager() {
@VisibleForTesting return gpsManager;
public void setBluetoothSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
this.bluetoothSensorManager = remoteSensorManager;
} }
@Deprecated @Deprecated
@@ -78,8 +114,9 @@ public class SensorManager {
this.altitudeSumManager = altitudeSumManager; this.altitudeSumManager = altitudeSumManager;
} }
@Deprecated public interface SensorDataChangedObserver {
public interface SensorDataSetChangeObserver { void onChange(SensorData<?> sensorData);
void onChange(SensorDataSet sensorDataSet);
void onDisconnect(SensorData<?> sensorData);
} }
} }
@@ -0,0 +1,67 @@
package de.dennisguse.opentracks.sensors.driver;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.util.Log;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
public class BarometerInternal {
private static final String TAG = BarometerInternal.class.getSimpleName();
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
private AltitudeSumManager observer;
private final SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (!isConnected()) {
Log.w(TAG, "Not connected to sensor, cannot process data.");
return;
}
observer.onSensorValueChanged(AtmosphericPressure.ofHPA(event.values[0]));
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
Log.w(TAG, "Sensor accuracy changes are (currently) ignored.");
}
};
public void connect(Context context, Handler handler, AltitudeSumManager observer) {
this.observer = 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)) {
this.observer = observer;
return;
}
disconnect(context);
}
public void disconnect(Context context) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(listener);
}
public boolean isConnected() {
return observer != null;
}
}
@@ -1,16 +1,21 @@
package de.dennisguse.opentracks.sensors.sensorData; package de.dennisguse.opentracks.sensors.sensorData;
import android.util.Log;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.Cadence; 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.HeartRate;
import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public final class SensorDataSet { public final class SensorDataSet {
private static final String TAG = SensorDataSet.class.getSimpleName();
private SensorDataHeartRate heartRate; private SensorDataHeartRate heartRate;
private SensorDataCyclingCadence cyclingCadence; private SensorDataCyclingCadence cyclingCadence;
@@ -140,28 +145,55 @@ public final class SensorDataSet {
+ (runningDistanceSpeedCadence != null ? " " + runningDistanceSpeedCadence : ""); + (runningDistanceSpeedCadence != null ? " " + runningDistanceSpeedCadence : "");
} }
private void set(@NonNull SensorData<?> type, SensorData<?> data) { private void set(@NonNull SensorData<?> type, SensorData<?> sensorData) {
if (type instanceof SensorDataHeartRate) { if (type instanceof SensorDataHeartRate) {
this.heartRate = (SensorDataHeartRate) data; this.heartRate = (SensorDataHeartRate) sensorData;
return; return;
} }
if (type instanceof SensorDataCyclingCadence) { if (type instanceof SensorDataCyclingCadence) {
this.cyclingCadence = (SensorDataCyclingCadence) data; SensorDataCyclingCadence previous = getCyclingCadence();
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cadence data repeated.");
return;
}
this.cyclingCadence = (SensorDataCyclingCadence) sensorData;
this.cyclingCadence.compute(previous);
return; return;
} }
if (type instanceof SensorDataCyclingDistanceSpeed) { if (type instanceof SensorDataCyclingDistanceSpeed) {
this.cyclingDistanceSpeed = (SensorDataCyclingDistanceSpeed) data; SensorDataCyclingDistanceSpeed previous = getCyclingDistanceSpeed();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (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;
this.cyclingDistanceSpeed.compute(previous, preferenceWheelCircumference);
return; return;
} }
if (type instanceof SensorDataCyclingPower) { if (type instanceof SensorDataCyclingPower) {
this.cyclingPower = (SensorDataCyclingPower) data; this.cyclingPower = (SensorDataCyclingPower) sensorData;
return; return;
} }
if (type instanceof SensorDataRunning) { if (type instanceof SensorDataRunning) {
this.runningDistanceSpeedCadence = (SensorDataRunning) data; SensorDataRunning previous = getRunningDistanceSpeedCadence();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: running speed data repeated.");
return;
}
this.runningDistanceSpeedCadence = (SensorDataRunning) sensorData;
this.runningDistanceSpeedCadence.compute(previous);
return; return;
} }
@@ -9,19 +9,15 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity; import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.DistanceFormatter; import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter; import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.settings.UnitSystem;
@@ -89,6 +85,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
DistanceFormatter formatter = DistanceFormatter.Builder() DistanceFormatter formatter = DistanceFormatter.Builder()
.setUnit(unitSystem) .setUnit(unitSystem)
.build(context); .build(context);
// The decision if a trackpoint is not accurate enough should happen in the sensor subsystem.
if (trackPoint.hasHorizontalAccuracy()) { if (trackPoint.hasHorizontalAccuracy()) {
formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy()); formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy());
@@ -107,26 +104,6 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
notificationBuilder.setOnlyAlertOnce(true); notificationBuilder.setOnlyAlertOnce(true);
} }
Notification setRecording(Context context, @NonNull Track.Id trackId) {
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = TaskStackBuilder.create(context)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, pendingIntentFlags);
updateContent(context.getString(R.string.gps_starting));
notificationBuilder.setContentIntent(pendingIntent);
updateNotification();
return getNotification();
}
Notification setGPSonlyStarted(Context context) { Notification setGPSonlyStarted(Context context) {
Intent intent = IntentUtils.newIntent(context, TrackListActivity.class); Intent intent = IntentUtils.newIntent(context, TrackListActivity.class);
@@ -30,7 +30,7 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
private final String TAG = GPSManager.class.getSimpleName(); private final String TAG = GPSManager.class.getSimpleName();
public static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER; private static final String LOCATION_PROVIDER = LocationManager.GPS_PROVIDER;
private final TrackPointCreator trackPointCreator; private final TrackPointCreator trackPointCreator;
private Context context; private Context context;
@@ -18,11 +18,12 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.SensorManager; import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/** /**
* Creates TrackPoints while recording by fusing data from different sensors (e.g., GNSS, barometer, BLE sensors). * Creates TrackPoints while recording by fusing data from different sensors (e.g., GNSS, barometer, BLE sensors).
*/ */
public class TrackPointCreator implements SensorManager.SensorDataSetChangeObserver { public class TrackPointCreator {
private static final String TAG = TrackPointCreator.class.getSimpleName(); private static final String TAG = TrackPointCreator.class.getSimpleName();
@@ -32,26 +33,21 @@ public class TrackPointCreator implements SensorManager.SensorDataSetChangeObser
@NonNull @NonNull
private Clock clock = new MonotonicClock(); private Clock clock = new MonotonicClock();
private final GPSManager gpsManager;
private SensorManager sensorManager; private SensorManager sensorManager;
public TrackPointCreator(Callback service, Context context, Handler handler) { public TrackPointCreator(Callback service, Context context, Handler handler) {
this.service = service; this.service = service;
this.gpsManager = new GPSManager(this); this.sensorManager = new SensorManager(this);
this.sensorManager = new SensorManager(context, handler, this);
} }
@VisibleForTesting @VisibleForTesting
TrackPointCreator(GPSManager gpsManager, Callback service) { TrackPointCreator(Callback service) {
this.service = service; this.service = service;
this.gpsManager = gpsManager;
} }
public synchronized void start(@NonNull Context context, @NonNull Handler handler) { public synchronized void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context; this.context = context;
gpsManager.start(context, handler);
sensorManager.start(context, handler); sensorManager.start(context, handler);
} }
@@ -73,8 +69,7 @@ public class TrackPointCreator implements SensorManager.SensorDataSetChangeObser
} }
public synchronized void stop() { public synchronized void stop() {
gpsManager.stop(context); sensorManager.stop(context);
this.context = null; this.context = null;
} }
@@ -85,7 +80,6 @@ public class TrackPointCreator implements SensorManager.SensorDataSetChangeObser
/** /**
* Got a new TrackPoint from Bluetooth only; contains no GPS location. * Got a new TrackPoint from Bluetooth only; contains no GPS location.
*/ */
@Override
public synchronized void onChange(@NonNull SensorDataSet unused) { public synchronized void onChange(@NonNull SensorDataSet unused) {
onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow())); onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow()));
} }
@@ -94,7 +88,7 @@ public class TrackPointCreator implements SensorManager.SensorDataSetChangeObser
public void onNewTrackPoint(@NonNull TrackPoint trackPoint) { public void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
addSensorData(trackPoint); addSensorData(trackPoint);
boolean stored = service.newTrackPoint(trackPoint, gpsManager.getThresholdHorizontalAccuracy()); boolean stored = service.newTrackPoint(trackPoint, PreferencesUtils.getThresholdHorizontalAccuracy()); //TODO Cache preference for performance
if (stored) { if (stored) {
reset(); reset();
} }
@@ -142,21 +136,16 @@ public class TrackPointCreator implements SensorManager.SensorDataSetChangeObser
return sensorManager; return sensorManager;
} }
@VisibleForTesting
public void setSensorManager(SensorManager sensorManager) {
this.sensorManager = sensorManager;
}
@VisibleForTesting @VisibleForTesting
public void setClock(@NonNull String time) { public void setClock(@NonNull String time) {
this.clock = Clock.fixed(Instant.parse(time), ZoneId.of("CET")); this.clock = Clock.fixed(Instant.parse(time), ZoneId.of("CET"));
} }
@VisibleForTesting
public void setClock(@NonNull Clock clock) {
this.clock = clock;
}
@VisibleForTesting
public GPSManager getGpsHandler() {
return gpsManager;
}
void sendGpsStatus(GpsStatusValue gpsStatusValue) { void sendGpsStatus(GpsStatusValue gpsStatusValue) {
service.newGpsStatus(gpsStatusValue); service.newGpsStatus(gpsStatusValue);
} }