added bosch cadence sensor

This commit is contained in:
Kilian Lackhove
2026-04-19 10:34:42 +02:00
parent f143aabfbf
commit 796fa216f6
6 changed files with 69 additions and 15 deletions
+2 -1
View File
@@ -123,4 +123,5 @@ Tested with:
Currently supported measurements: Currently supported measurements:
* Human power * Human power
* Cadence
@@ -96,6 +96,25 @@ public class SensorDataCyclingTest {
assertNull(current.getAggregatedValue(Instant.MIN)); assertNull(current.getAggregatedValue(Instant.MIN));
} }
@Test
public void compute_cadence_directCadence_withoutPrevious() {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
current.add(Instant.MIN, new CyclingCadenceBluetooth.DirectCadenceData(Cadence.of(92)));
assertEquals(92, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
}
@Test
public void compute_cadence_directCadence_winsOverCrankData() {
AggregatorCyclingCadence current = new AggregatorCyclingCadence("", "");
current.add(Instant.MIN, new CyclingCadenceBluetooth.CrankData(1L, 1024));
current.add(Instant.MIN, new CyclingCadenceBluetooth.DirectCadenceData(Cadence.of(92)));
assertEquals(92, current.getAggregatedValue(Instant.MIN).getRPM(), 0.01);
}
@Test @Test
public void compute_speed() { public void compute_speed() {
AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", ""); AggregatorCyclingDistanceSpeed current = new AggregatorCyclingDistanceSpeed("", "");
@@ -9,7 +9,7 @@ import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
import de.dennisguse.opentracks.sensors.driver.Driver; import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence; import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
public class CyclingCadenceHandler extends SensorHandler<CyclingCadenceBluetooth.CrankData, Cadence> { public class CyclingCadenceHandler extends SensorHandler<CyclingCadenceBluetooth.CyclingCadenceMeasurement, Cadence> {
protected CyclingCadenceHandler(SensorManager sensorManager) { protected CyclingCadenceHandler(SensorManager sensorManager) {
super(sensorManager); super(sensorManager);
@@ -6,23 +6,25 @@ import android.util.Pair;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID; import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
public class CyclingCadenceBluetooth implements BluetoothDriver.BluetoothParser<CyclingCadenceBluetooth.CrankData> { public class CyclingCadenceBluetooth implements BluetoothDriver.BluetoothParser<CyclingCadenceBluetooth.CyclingCadenceMeasurement> {
private static final String TAG = CyclingCadenceBluetooth.class.getSimpleName(); private static final String TAG = CyclingCadenceBluetooth.class.getSimpleName();
public static final List<ServiceMeasurementUUID> CYCLING_CADENCE = List.of( public static final List<ServiceMeasurementUUID> SUPPORTED_SERVICES = List.of(
CyclingPowerBluetooth.CYCLING_POWER, CyclingPowerBluetooth.CYCLING_POWER,
CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE CyclingDistanceSpeedBluetooth.CYCLING_SPEED_CADENCE,
BoschEbikeParser.BOSCH_EBIKE
); );
@Override @Override
public List<ServiceMeasurementUUID> getServices() { public List<ServiceMeasurementUUID> getServices() {
return CYCLING_CADENCE; return SUPPORTED_SERVICES;
} }
@Override @Override
public CrankData parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) { public CyclingCadenceMeasurement parsePayload(ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
if (serviceMeasurementUUID.equals(CyclingPowerBluetooth.CYCLING_POWER)) { if (serviceMeasurementUUID.equals(CyclingPowerBluetooth.CYCLING_POWER)) {
CyclingPowerBluetooth.Data data = CyclingPowerBluetooth.parseCyclingPower(characteristic); CyclingPowerBluetooth.Data data = CyclingPowerBluetooth.parseCyclingPower(characteristic);
if (data != null && data.crank() != null) { if (data != null && data.crank() != null) {
@@ -38,12 +40,28 @@ public class CyclingCadenceBluetooth implements BluetoothDriver.BluetoothParser<
} }
} }
if (serviceMeasurementUUID.equals(BoschEbikeParser.BOSCH_EBIKE)) {
BoschEbikeParser.Data boschData = BoschEbikeParser.parse(characteristic.getValue());
if (boschData != null && boschData.cadence() != null) {
return new DirectCadenceData(boschData.cadence());
}
}
Log.e(TAG, "Don't know how to decode this payload."); Log.e(TAG, "Don't know how to decode this payload.");
return null; return null;
} }
public sealed interface CyclingCadenceMeasurement permits CrankData, DirectCadenceData {
}
public record CrankData( public record CrankData(
long crankRevolutionsCount, // UINT32 long crankRevolutionsCount, // UINT32
int crankRevolutionsTime // UINT16; 1/1024s int crankRevolutionsTime // UINT16; 1/1024s
) {} ) implements CyclingCadenceMeasurement {
}
public record DirectCadenceData(
Cadence cadence
) implements CyclingCadenceMeasurement {
}
} }
@@ -10,7 +10,7 @@ import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.sensors.UintUtils; import de.dennisguse.opentracks.sensors.UintUtils;
import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth; import de.dennisguse.opentracks.sensors.driver.CyclingCadenceBluetooth;
public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth.CrankData, Cadence> { public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth.CyclingCadenceMeasurement, Cadence> {
private final String TAG = AggregatorCyclingCadence.class.getSimpleName(); private final String TAG = AggregatorCyclingCadence.class.getSimpleName();
@@ -19,12 +19,20 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
} }
@Override @Override
protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CrankData> current) { protected void computeValue(@NonNull Raw<CyclingCadenceBluetooth.CyclingCadenceMeasurement> current) {
if (previous == null) { if (current.value() instanceof CyclingCadenceBluetooth.DirectCadenceData directCadenceData) {
output = directCadenceData.cadence();
return; return;
} }
float timeDiff_ms = UintUtils.diff(current.value().crankRevolutionsTime(), previous.value().crankRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000; computeRawCadence((CyclingCadenceBluetooth.CrankData) current.value());
}
private void computeRawCadence(@NonNull CyclingCadenceBluetooth.CrankData currentRaw) {
CyclingCadenceBluetooth.CrankData previousRaw = getPreviousRaw();
if (previousRaw == null) return;
float timeDiff_ms = UintUtils.diff(currentRaw.crankRevolutionsTime(), previousRaw.crankRevolutionsTime(), UintUtils.UINT16_MAX) / 1024f * 1000;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms); Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero()) { if (timeDiff.isZero()) {
@@ -37,13 +45,21 @@ public class AggregatorCyclingCadence extends Aggregator<CyclingCadenceBluetooth
} }
// TODO We have to treat with overflow according to the documentation: read https://codeberg.org/OpenTracksApp/OpenTracks/pulls/953#issuecomment-6466930 // TODO We have to treat with overflow according to the documentation: read https://codeberg.org/OpenTracksApp/OpenTracks/pulls/953#issuecomment-6466930
if (current.value().crankRevolutionsCount() < previous.value().crankRevolutionsCount()) { if (currentRaw.crankRevolutionsCount() < previousRaw.crankRevolutionsCount()) {
Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence."); Log.e(TAG, "Crank revolutions count difference is invalid: cannot compute cadence.");
output = null; output = null;
return; return;
} }
long crankDiff = UintUtils.diff(current.value().crankRevolutionsCount(), previous.value().crankRevolutionsCount(), UintUtils.UINT32_MAX); long crankDiff = UintUtils.diff(currentRaw.crankRevolutionsCount(), previousRaw.crankRevolutionsCount(), UintUtils.UINT32_MAX);
output = Cadence.of(crankDiff, timeDiff); output = Cadence.of(crankDiff, timeDiff);
} }
private CyclingCadenceBluetooth.CrankData getPreviousRaw() {
if (previous == null) {
return null;
}
return previous.value() instanceof CyclingCadenceBluetooth.CrankData raw ? raw : null;
}
} }
@@ -27,6 +27,6 @@ public class BluetoothLeCyclingCadencePreference extends BluetoothLeSensorPrefer
@Override @Override
public PreferenceDialogFragmentCompat createInstance() { public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), CyclingCadenceBluetooth.CYCLING_CADENCE); return BluetoothLeSensorPreferenceDialog.newInstance(getKey(), CyclingCadenceBluetooth.SUPPORTED_SERVICES);
} }
} }