diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeedTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeedTest.java new file mode 100644 index 0000000000..189963e9d7 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeedTest.java @@ -0,0 +1,52 @@ +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.sensors.sensorData.SensorDataCyclingCadenceAndDistanceSpeed; + +public class BluetoothConnectionManagerCyclingDistanceSpeedTest { + @Test + public void parseCyclingSpeedCadence_crankOnly() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionManagerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x02, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99}); + + // when + SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothConnectionManagerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + assertNull(sensor.getDistanceSpeed()); + assertEquals(200, sensor.getCadence().getCrankRevolutionsCount()); + } + + @Test + public void parseCyclingSpeedCadence_wheelOnly() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionManagerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x01, (byte) 0xFF, (byte) 0xFF, 0, 1, 0x45, (byte) 0x99}); + + // when + SensorDataCyclingCadenceAndDistanceSpeed sensor = BluetoothConnectionManagerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + assertEquals(65535 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount()); + assertNull(sensor.getCadence()); + } + + @Test + public void parseCyclingSpeedCadence_crankWheel() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionManagerCyclingDistanceSpeed.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 = BluetoothConnectionManagerCyclingDistanceSpeed.parseCyclingCrankAndWheel("address", "sensorName", characteristic); + + // then + assertEquals(200 + 16777216, sensor.getDistanceSpeed().getWheelRevolutionsCount()); + assertEquals(225, sensor.getCadence().getCrankRevolutionsCount()); + } + +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPowerTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPowerTest.java new file mode 100644 index 0000000000..5ca6cfb524 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPowerTest.java @@ -0,0 +1,40 @@ +package de.dennisguse.opentracks.sensors; + +import static org.junit.Assert.assertEquals; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.junit.Test; + +import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; + +public class BluetoothConnectionManagerCyclingPowerTest { + + @Test + public void parseCyclingPower_power() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionManagerCyclingPower.CYCLING_POWER.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0, 0, 40, 0}); + + // when + SensorDataCyclingPower.Data powerCadence = BluetoothConnectionManagerCyclingPower.parseCyclingPower("", "", characteristic); + + // then + assertEquals(40, powerCadence.power().getValue().getW(), 0.01); + } + + @Test + public void parseCyclingPower_power_with_cadence() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionManagerCyclingPower.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 = BluetoothConnectionManagerCyclingPower.parseCyclingPower("", "", characteristic); + + // then + assertEquals(0, powerCadence.power().getValue().getW(), 0.01); + + assertEquals(12, powerCadence.cadence().getCrankRevolutionsCount()); + assertEquals(17125, powerCadence.cadence().getCrankRevolutionsTime()); + } + +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRateTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRateTest.java new file mode 100644 index 0000000000..42259ccc02 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRateTest.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 BluetoothConnectionManagerHeartRateTest { + + @Test + public void parseHeartRate_uint8() { + // given + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x02, 0x3C}); + + // when + HeartRate heartRate = BluetoothConnectionManagerHeartRate.parseHeartRate(characteristic); + + // then + assertEquals(HeartRate.of(60), heartRate); + } + + @Test + public void parseHeartRate_uint16() { + // given + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionManagerHeartRate.HEARTRATE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{0x01, 0x01, 0x01}); + + // when + HeartRate heartRate = BluetoothConnectionManagerHeartRate.parseHeartRate(characteristic); + + // then + assertEquals(HeartRate.of(257), heartRate); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadenceTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadenceTest.java new file mode 100644 index 0000000000..43f687ecfe --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadenceTest.java @@ -0,0 +1,29 @@ +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; +import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning; + +public class BluetoothConnectionRunningSpeedAndCadenceTest { + + @Test + public void parseRunningSpeedAndCadence_with_distance() { + BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothConnectionRunningSpeedAndCadence.RUNNING_SPEED_CADENCE.serviceUUID(), 0, 0); + characteristic.setValue(new byte[]{2, 0, 5, 80, (byte) 0xFF, (byte) 0xFF, 0, 1}); + + // when + SensorDataRunning sensor = BluetoothConnectionRunningSpeedAndCadence.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/BluetoothHandlerBarometricPressureTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressureTest.java new file mode 100644 index 0000000000..03f513ff6c --- /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/BluetoothUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/BluetoothUtilsTest.java deleted file mode 100644 index 73d5232490..0000000000 --- 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(BluetoothConnectionManagerHeartRate.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(BluetoothConnectionManagerHeartRate.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(BluetoothConnectionManagerCyclingDistanceSpeed.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(BluetoothConnectionManagerCyclingDistanceSpeed.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(BluetoothConnectionManagerCyclingDistanceSpeed.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(BluetoothConnectionManagerCyclingPower.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(BluetoothConnectionManagerCyclingPower.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(BluetoothConnectionRunningSpeedAndCadence.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/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeed.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeed.java index eb8dad423c..4dd666ad5e 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeed.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingDistanceSpeed.java @@ -2,9 +2,13 @@ import android.bluetooth.BluetoothGattCharacteristic; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + import java.util.List; import java.util.UUID; +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.SensorHandlerInterface; @@ -28,7 +32,7 @@ public SensorDataCyclingDistanceSpeed createEmptySensorData(String address) { @Override public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic); + SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = parseCyclingCrankAndWheel(address, sensorName, characteristic); if (cadenceAndSpeed == null) { return; } @@ -37,4 +41,39 @@ public void handlePayload(SensorManager.SensorDataChangedObserver observer, Serv observer.onChange(cadenceAndSpeed.getDistanceSpeed()); } } + + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + 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); + } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPower.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPower.java index e5b94ccfb1..5e1876e8f8 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPower.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerCyclingPower.java @@ -3,10 +3,13 @@ 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.SensorDataCyclingCadence; import de.dennisguse.opentracks.sensors.sensorData.SensorDataCyclingPower; import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; @@ -29,10 +32,55 @@ public SensorDataCyclingPower createEmptySensorData(String address) { @Override public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - SensorDataCyclingPower.Data cyclingPower = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic); + SensorDataCyclingPower.Data cyclingPower = parseCyclingPower(address, sensorName, characteristic); if (cyclingPower != null) { observer.onChange(cyclingPower.power()); } } + + + @VisibleForTesting + 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); + } + } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRate.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRate.java index ad13326c25..afe095b75b 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRate.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManagerHeartRate.java @@ -3,6 +3,7 @@ import android.bluetooth.BluetoothGattCharacteristic; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import java.util.List; import java.util.UUID; @@ -40,10 +41,29 @@ public SensorDataHeartRate createEmptySensorData(String address) { @Override public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - HeartRate heartRate = BluetoothUtils.parseHeartRate(characteristic); + HeartRate heartRate = parseHeartRate(characteristic); if (heartRate != null) { observer.onChange(new SensorDataHeartRate(address, sensorName, 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/BluetoothConnectionRunningSpeedAndCadence.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadence.java index 68fb1ff41e..83476c96d8 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadence.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionRunningSpeedAndCadence.java @@ -3,10 +3,14 @@ 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.SensorDataRunning; import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; @@ -30,6 +34,51 @@ public SensorDataRunning createEmptySensorData(String address) { @Override public void handlePayload(SensorManager.SensorDataChangedObserver observer, @NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - observer.onChange(BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic)); + observer.onChange(parseRunningSpeedAndCadence(address, sensorName, characteristic)); + } + + @VisibleForTesting + 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/BluetoothHandlerBarometricPressure.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java new file mode 100644 index 0000000000..e0f3cd320b --- /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.SensorData; +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 SensorData 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 index 625a686cb5..abc27a4e02 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingCadence.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerCyclingCadence.java @@ -33,12 +33,12 @@ public void handlePayload(SensorManager.SensorDataChangedObserver observer, Serv //TODO Implement to ServiceMeasurement.parse()? if (serviceMeasurementUUID.equals(BluetoothConnectionManagerCyclingPower.CYCLING_POWER)) { - SensorDataCyclingPower.Data data = BluetoothUtils.parseCyclingPower(address, sensorName, characteristic); + SensorDataCyclingPower.Data data = BluetoothConnectionManagerCyclingPower.parseCyclingPower(address, sensorName, characteristic); if (data!= null) { observer.onChange(data.cadence()); } } else if (serviceMeasurementUUID.equals(BluetoothConnectionManagerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)) { - SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic); + SensorDataCyclingCadenceAndDistanceSpeed cadenceAndSpeed = BluetoothConnectionManagerCyclingDistanceSpeed.parseCyclingCrankAndWheel(address, sensorName, characteristic); if (cadenceAndSpeed == null) { return; } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java index 33d0a712a4..c0f2f2d1fe 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothUtils.java @@ -21,22 +21,9 @@ import android.content.Context; import android.util.Log; -import androidx.annotation.NonNull; - 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. @@ -52,17 +39,9 @@ public class BluetoothUtils { new UUID(0x2A1900001000L, 0x800000805f9b34fbL) ); - 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) - ); 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) { @@ -87,152 +66,4 @@ public static BatteryLevel parseBatteryLevel(BluetoothGattCharacteristic charact 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); - } }