mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-09-30 16:52:21 +02:00
initial support for Bosch ebike BLE power sensor
This commit is contained in:
@@ -112,3 +112,15 @@ Here is a list of the smartphones tested:
|
|||||||
| Google | Nexus 4 | LineageOS 18.1 | Ok |
|
| Google | Nexus 4 | LineageOS 18.1 | Ok |
|
||||||
| Samsung | S4 | Android 11 | Ok |
|
| Samsung | S4 | Android 11 | Ok |
|
||||||
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
|
| Sony | Xperia XZ1 Compact | LineageOS 16.1 | Failure |
|
||||||
|
|
||||||
|
## Bosch Smart System eBike
|
||||||
|
|
||||||
|
OpenTracks also supports Bosch Smart System eBikes via Bosch's custom BLE service `00000010-eaa2-11e9-81b4-2a2ae2dbcce4`.
|
||||||
|
|
||||||
|
Tested with:
|
||||||
|
|
||||||
|
* Bosch Perfomance Line CX (SW-Version 16.9.0) / Kiox 300 (SW-Version 16.4.0)
|
||||||
|
|
||||||
|
Currently supported measurements:
|
||||||
|
|
||||||
|
* Human power
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
package de.dennisguse.opentracks.sensors.driver;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class BoschEbikeParserTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_multipleFrames_returnsCombinedMeasurements() {
|
||||||
|
byte[] raw = new byte[] {
|
||||||
|
// Bosch cadence frame
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
|
||||||
|
// Bosch human power frame
|
||||||
|
0x30, 0x05, (byte) 0x98, 0x5B, 0x08, (byte) 0x83, 0x01,
|
||||||
|
// Bosch speed frame
|
||||||
|
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0x94, 0x0B, 0x10, 0x01
|
||||||
|
};
|
||||||
|
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
|
||||||
|
|
||||||
|
assertNotNull(data);
|
||||||
|
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||||
|
assertEquals(131.0f, data.humanPower().getW(), 0.01f);
|
||||||
|
assertEquals(14.28 / 3.6, data.speed().toMPS(), 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_multiByteVarint_returnsScaledSpeed() {
|
||||||
|
// Bosch speed frame using a multi-byte varint
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
0x30, 0x07, (byte) 0x98, 0x2D, 0x08, (byte) 0xE1, 0x0E, 0x10, 0x01
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(data);
|
||||||
|
assertEquals(18.89 / 3.6, data.speed().toMPS(), 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_zeroValueFrame_returnsZeroCadence() {
|
||||||
|
// Bosch zero-value cadence frame
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
0x30, 0x02, (byte) 0x98, 0x5A
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(data);
|
||||||
|
assertEquals(0.0f, data.cadence().getRPM(), 0.01f);
|
||||||
|
assertNull(data.speed());
|
||||||
|
assertNull(data.humanPower());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_unsupportedMeasurementsOnly_returnsNull() {
|
||||||
|
// Bosch frame for message 0x9865, which the parser does not map to a measurement.
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
0x30, 0x02, (byte) 0x98, 0x65
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNull(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_unimplementedMeasurementsOnly_returnsNull() {
|
||||||
|
// Bosch motor power frame: message 0x985D with value 0x42, currently ignored.
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x5D, 0x08, 0x42
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNull(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_malformedVarint_returnsNull() {
|
||||||
|
// Truncated Bosch speed frame: starts like a real 0x982D frame but the varint is incomplete.
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNull(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_varintLargerThan32Bit_returnsNull() {
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
0x30, 0x08, (byte) 0x98, 0x2D, 0x08,
|
||||||
|
(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNull(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_validMeasurementBeforeMalformedFrame_returnsValidMeasurement() {
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
// Bosch cadence frame: message 0x985A with value 0x76
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76,
|
||||||
|
// Truncated Bosch speed frame: malformed trailing frame should not discard the valid cadence above.
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(data);
|
||||||
|
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||||
|
assertNull(data.speed());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_malformedVarintDoesNotConsumeNextFrameStartByte() {
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(new byte[] {
|
||||||
|
// Truncated Bosch speed frame: the varint continuation bit is set, but the frame ends
|
||||||
|
// before the next varint byte. The following frame start byte must not be consumed.
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x2D, 0x08, (byte) 0x94,
|
||||||
|
// Bosch cadence frame: message 0x985A with value 0x76
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
|
||||||
|
});
|
||||||
|
|
||||||
|
assertNotNull(data);
|
||||||
|
assertNull(data.speed());
|
||||||
|
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parse_leadingNoise_ignored() {
|
||||||
|
byte[] raw = new byte[] {
|
||||||
|
// Noise bytes to verify the parser resynchronizes on the Bosch frame start byte 0x30.
|
||||||
|
0x01, 0x02, 0x03,
|
||||||
|
// Bosch cadence frame: message 0x985A with value 0x76
|
||||||
|
0x30, 0x04, (byte) 0x98, 0x5A, 0x08, 0x76
|
||||||
|
};
|
||||||
|
|
||||||
|
BoschEbikeParser.Data data = BoschEbikeParser.parse(raw);
|
||||||
|
|
||||||
|
assertNotNull(data);
|
||||||
|
assertEquals(59.0f, data.cadence().getRPM(), 0.01f);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-1
@@ -1,6 +1,7 @@
|
|||||||
package de.dennisguse.opentracks.sensors.driver;
|
package de.dennisguse.opentracks.sensors.driver;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
import android.bluetooth.BluetoothGattCharacteristic;
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
|
||||||
@@ -35,4 +36,27 @@ public class CyclingPowerBluetoothTest {
|
|||||||
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
|
assertEquals(17125, powerCadence.crank().crankRevolutionsTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@Test
|
||||||
|
public void parsePayload_boschPower() {
|
||||||
|
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
|
||||||
|
// Bosch human power frame: message 0x985B with value 0x7B => 123 W.
|
||||||
|
characteristic.setValue(new byte[] { 0x30, 0x04, (byte) 0x98, 0x5B, 0x08, 0x7B });
|
||||||
|
|
||||||
|
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
|
||||||
|
|
||||||
|
assertEquals(123, power.power().getW(), 0.01);
|
||||||
|
assertNull(power.crank());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parsePayload_boschCadenceOnly_returnsNull() {
|
||||||
|
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BoschEbikeParser.BOSCH_EBIKE.measurementUUID(), 0, 0);
|
||||||
|
// Bosch cadence-only frame: message 0x985A with value 0x01B8 => 220 / 2 = 110 rpm, so power parsing should ignore it.
|
||||||
|
characteristic.setValue(new byte[] { 0x30, 0x05, (byte) 0x98, 0x5A, 0x08, (byte) 0xB8, 0x01 });
|
||||||
|
|
||||||
|
CyclingPowerBluetooth.Data power = new CyclingPowerBluetooth().parsePayload(BoschEbikeParser.BOSCH_EBIKE, null, characteristic);
|
||||||
|
|
||||||
|
assertNull(power);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package de.dennisguse.opentracks.sensors.driver;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.data.models.Cadence;
|
||||||
|
import de.dennisguse.opentracks.data.models.Power;
|
||||||
|
import de.dennisguse.opentracks.data.models.Speed;
|
||||||
|
import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID;
|
||||||
|
|
||||||
|
// Implementation of metrics from a Bosch Smart System eBike via BLE.
|
||||||
|
// as described and implemented here: https://github.com/RobbyPee/Bosch-Smart-System-Ebike-Garmin-Android
|
||||||
|
//
|
||||||
|
// The bike exposes a single custom BLE service with one notification characteristic onto
|
||||||
|
// which all measurements are multiplexed as concatenated frames.
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// 30 <length> <id-hi> <id-lo> [08 <varint...>]
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// Frame format
|
||||||
|
//
|
||||||
|
// - `30` : start byte
|
||||||
|
// - `<length>` - number of payload bytes following (min 2, which means value = 0)
|
||||||
|
// - two-byte big-endian message ID
|
||||||
|
// - `08` : varint type tag (absent when value is 0)
|
||||||
|
// - varint : strip MSB from each byte, 7-bit groups, little-endian concatenation
|
||||||
|
//
|
||||||
|
// Multiple frames may be packed into a single characteristic notification.
|
||||||
|
//
|
||||||
|
|
||||||
|
public final class BoschEbikeParser {
|
||||||
|
|
||||||
|
private static final String TAG = BoschEbikeParser.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int MESSAGE_ID_CADENCE = 0x985A;
|
||||||
|
private static final int MESSAGE_ID_HUMAN_POWER = 0x985B;
|
||||||
|
private static final int MESSAGE_ID_SPEED = 0x982D;
|
||||||
|
private static final int MESSAGE_ID_MOTOR_POWER = 0x985D;
|
||||||
|
private static final int MESSAGE_ID_BATTERY = 0x8088;
|
||||||
|
private static final int MESSAGE_ID_ASSIST_MODE = 0x9809;
|
||||||
|
|
||||||
|
private static final int START_BYTE = 0x30;
|
||||||
|
private static final int VARINT_TAG = 0x08;
|
||||||
|
private static final int MAX_VARINT_32_BIT_BYTES = 5;
|
||||||
|
|
||||||
|
public static final ServiceMeasurementUUID BOSCH_EBIKE = new ServiceMeasurementUUID(
|
||||||
|
UUID.fromString("00000010-eaa2-11e9-81b4-2a2ae2dbcce4"),
|
||||||
|
UUID.fromString("00000011-eaa2-11e9-81b4-2a2ae2dbcce4"));
|
||||||
|
|
||||||
|
private BoschEbikeParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a single varint from {@code data}. Uses little-endian 7-bit groups; MSB=1 means another byte follows.
|
||||||
|
*/
|
||||||
|
private static Integer decodeVarint(byte[] data) {
|
||||||
|
if (data == null || data.length == 0) {
|
||||||
|
Log.d(TAG, "Malformed varint: empty payload");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < data.length; i++) {
|
||||||
|
if (i >= MAX_VARINT_32_BIT_BYTES) {
|
||||||
|
Log.d(TAG, "Malformed varint: exceeds 32-bit limit after " + i + " bytes");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int byteValue = data[i] & 0x7F;
|
||||||
|
result |= byteValue << (7 * i);
|
||||||
|
|
||||||
|
if ((data[i] & 0x80) == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Malformed varint: truncated after " + data.length + " bytes");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses one Bosch notification payload, which may contain multiple concatenated frames, and returns the measurements recognized in that payload.
|
||||||
|
*/
|
||||||
|
static Data parse(byte[] raw) {
|
||||||
|
if (raw == null || raw.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Data data = splitFrames(raw).stream()
|
||||||
|
.filter(BoschEbikeParser::isValidFrame)
|
||||||
|
.filter(BoschEbikeParser::isSupportedFrame)
|
||||||
|
.map(BoschEbikeParser::decodeFrame)
|
||||||
|
.filter(decodedFrame -> decodedFrame != null)
|
||||||
|
.reduce(null,
|
||||||
|
BoschEbikeParser::processMessage,
|
||||||
|
(left, right) -> right);
|
||||||
|
Log.d(TAG, "Parsed Bosch data: " + data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<byte[]> splitFrames(byte[] raw) {
|
||||||
|
List<byte[]> frames = new ArrayList<>();
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (i < raw.length) {
|
||||||
|
if (raw[i] != START_BYTE) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i + 1 >= raw.length) {
|
||||||
|
Log.d(TAG, "Malformed frame at offset " + i + ": missing length byte (raw length=" + raw.length + ")");
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = raw[i + 1] & 0xFF;
|
||||||
|
int frameEnd = i + length + 2;
|
||||||
|
frames.add(Arrays.copyOfRange(raw, i, Math.min(frameEnd, raw.length)));
|
||||||
|
i = frameEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidFrame(byte[] frame) {
|
||||||
|
int length = getFrameLength(frame);
|
||||||
|
if (length < 2) {
|
||||||
|
Log.d(TAG, "Malformed frame: payload length " + length + " is shorter than minimum 2 bytes");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.length != length + 2) {
|
||||||
|
Log.d(TAG, "Malformed frame: expected " + (length + 2) + " bytes, got " + frame.length);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSupportedFrame(byte[] frame) {
|
||||||
|
boolean supported = getFrameLength(frame) == 2 || (frame[4] & 0xFF) == VARINT_TAG;
|
||||||
|
if (!supported) {
|
||||||
|
Log.d(TAG, "Unsupported frame: tag 0x" + String.format("%02X", frame[4] & 0xFF));
|
||||||
|
}
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static DecodedFrame decodeFrame(byte[] frame) {
|
||||||
|
if (getFrameLength(frame) == 2) {
|
||||||
|
return new DecodedFrame(getMessageId(frame), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer varintResult = decodeVarint(Arrays.copyOfRange(frame, 5, frame.length));
|
||||||
|
if (varintResult == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DecodedFrame(getMessageId(frame), varintResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getFrameLength(byte[] frame) {
|
||||||
|
return frame[1] & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getMessageId(byte[] frame) {
|
||||||
|
return (frame[2] & 0xFF) << 8 | (frame[3] & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a message ID and value, updating the data with any measurements.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static Data processMessage(@Nullable Data data, DecodedFrame decodedFrame) {
|
||||||
|
switch (decodedFrame.messageId()) {
|
||||||
|
case MESSAGE_ID_CADENCE:
|
||||||
|
return new Data(null, Cadence.of(decodedFrame.value() / 2.0f), null, data);
|
||||||
|
case MESSAGE_ID_HUMAN_POWER:
|
||||||
|
return new Data(null, null, Power.of((float) decodedFrame.value()), data);
|
||||||
|
case MESSAGE_ID_SPEED:
|
||||||
|
double speed_kmh = decodedFrame.value() / 100.0;
|
||||||
|
return new Data(Speed.ofKMH(speed_kmh), null, null, data);
|
||||||
|
case MESSAGE_ID_MOTOR_POWER:
|
||||||
|
case MESSAGE_ID_BATTERY:
|
||||||
|
case MESSAGE_ID_ASSIST_MODE:
|
||||||
|
Log.d(TAG, "Ignoring unimplemented message: raw=" + decodedFrame.value());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Ignoring unknown message: raw=" + decodedFrame.value());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Data(
|
||||||
|
@Nullable Speed speed,
|
||||||
|
@Nullable Cadence cadence,
|
||||||
|
@Nullable Power humanPower) {
|
||||||
|
|
||||||
|
public Data(@Nullable Speed speed, @Nullable Cadence cadence, @Nullable Power humanPower, @Nullable Data oldData) {
|
||||||
|
this(
|
||||||
|
speed != null ? speed : oldData != null ? oldData.speed() : null,
|
||||||
|
cadence != null ? cadence : oldData != null ? oldData.cadence() : null,
|
||||||
|
humanPower != null ? humanPower : oldData != null ? oldData.humanPower() : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DecodedFrame(int messageId, int value) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,14 +18,28 @@ public class CyclingPowerBluetooth implements BluetoothDriver.BluetoothParser<Cy
|
|||||||
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
new UUID(0x2A6300001000L, 0x800000805f9b34fbL)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static final List<ServiceMeasurementUUID> SUPPORTED_SERVICES = List.of(
|
||||||
|
CYCLING_POWER,
|
||||||
|
BoschEbikeParser.BOSCH_EBIKE
|
||||||
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ServiceMeasurementUUID> getServices() {
|
public List<ServiceMeasurementUUID> getServices() {
|
||||||
return List.of(CYCLING_POWER);
|
return SUPPORTED_SERVICES;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CyclingPowerBluetooth.Data parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
public CyclingPowerBluetooth.Data parsePayload(@NonNull ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, BluetoothGattCharacteristic characteristic) {
|
||||||
return parseCyclingPower(characteristic);
|
if (serviceMeasurementUUID.equals(CYCLING_POWER)) {
|
||||||
|
return parseCyclingPower(characteristic);
|
||||||
|
}
|
||||||
|
if (serviceMeasurementUUID.equals(BoschEbikeParser.BOSCH_EBIKE)) {
|
||||||
|
BoschEbikeParser.Data boschData = BoschEbikeParser.parse(characteristic.getValue());
|
||||||
|
if (boschData != null && boschData.humanPower() != null) {
|
||||||
|
return new Data(boschData.humanPower(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -4,9 +4,6 @@ import android.content.Context;
|
|||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
|
import de.dennisguse.opentracks.sensors.driver.CyclingPowerBluetooth;
|
||||||
|
|
||||||
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
|
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
|
||||||
@@ -29,6 +26,8 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PreferenceDialogFragmentCompat createInstance() {
|
public PreferenceDialogFragmentCompat createInstance() {
|
||||||
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(CyclingPowerBluetooth.CYCLING_POWER));
|
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(
|
||||||
|
getKey(),
|
||||||
|
CyclingPowerBluetooth.SUPPORTED_SERVICES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user