Skip to content

Commit

Permalink
Sensor: adding Running Speed and Cadence.
Browse files Browse the repository at this point in the history
  • Loading branch information
dennisguse committed Jul 31, 2021
1 parent eba7a7f commit 242147e
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 17 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* cycling: speed and distance
* cycling: cadence
* cycling: power meter
* running: speed and cadence
An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md)

### Gadgetbridge integration

Expand Down
5 changes: 5 additions & 0 deletions README_TESTED_SENSORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ Also the distance is not computed.
* Wahoo Kickr v4.0
* Rotor 2INpower DM Road (only supports power measurement, cadence measurement is propietary @ firmware v1.061)

## RSC: running cadence and speed

We do not support type of movement (i.e., walking vs running).

* Polar Stride
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

import org.junit.Test;

import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
Expand Down Expand Up @@ -87,4 +90,18 @@ public void parseCyclingPower_power() {
// then
assertEquals(40, power_w);
}

@Test
public void parseRunningSpeedAndCadence_with_distance() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, 0, 0);
characteristic.setValue(new byte[]{2, 0, 5, 80, 12, 1});

// when
SensorDataRunning sensor = BluetoothUtils.parseRunningSpeedAndCadence("address", "sensorName", characteristic);

// then
assertEquals(Speed.of(5), sensor.getSpeed());
assertEquals(80, sensor.getCadence(), 0.01);
assertEquals(Distance.of(268), sensor.getTotalDistance());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ public static Distance ofKilometer(double distance_km) {
}

public static Distance ofMM(double distance_mm) {
return of(distance_mm * UnitConversions.MM_TO_M);
return of(0.001 * distance_mm);
}

public static Distance ofCM(double distance_cm) {
return of(0.01 * distance_cm);
}

public static Distance ofDM(double distance_dm) {
return of(0.1 * distance_dm);
}

public static Distance one(boolean metricUnit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package de.dennisguse.opentracks.content.sensor;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;

/**
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Running Speed and Cadence sensors.
*/
public final class SensorDataRunning extends SensorData<SensorDataRunning.Data> {

private static final String TAG = SensorDataRunning.class.getSimpleName();

private final Speed speed;

private final Float cadence;

private final Distance totalDistance;

public SensorDataRunning(String sensorAddress) {
super(sensorAddress);
this.speed = null;
this.cadence = null;
this.totalDistance = null;
}

public SensorDataRunning(String sensorAddress, String sensorName, Speed speed, Float cadence, Distance totalDistance) {
super(sensorAddress, sensorName);
this.speed = speed;
this.cadence = cadence;
this.totalDistance = totalDistance;
}

private boolean hasTotalDistance() {
return totalDistance != null;
}


public Float getCadence() {
return cadence;
}

@VisibleForTesting
public Speed getSpeed() {
return speed;
}

@VisibleForTesting
public Distance getTotalDistance() {
return totalDistance;
}

public void compute(SensorDataRunning previous) {
Distance overallDistance = null;
if (hasTotalDistance() && previous != null && previous.hasTotalDistance()) {
overallDistance = this.totalDistance.minus(previous.totalDistance);
if (previous.hasValue() && previous.getValue().getDistance() != null) {
overallDistance = overallDistance.plus(previous.getValue().getDistance());
}
}

value = new Data(speed, cadence, overallDistance);
}

@Override
public void reset() {
if (value != null) {
value = new Data(value.speed, value.cadence, Distance.of(0));
}
}

public static class Data {
private final Speed speed;
private final Float cadence;

@Nullable
private final Distance distance;

public Data(Speed speed, Float cadence, @Nullable Distance distance) {
this.speed = speed;
this.cadence = cadence;
this.distance = distance;
}

public Speed getSpeed() {
return speed;
}

public Float getCadence() {
return cadence;
}

public Distance getDistance() {
return distance;
}

@Override
public String toString() {
return "Data{" +
"speed=" + speed +
", cadence=" + cadence +
", distance=" + distance +
'}';
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public final class SensorDataSet {

private SensorDataCyclingPower cyclingPower;

private SensorDataRunning runningDistanceSpeedCadence;

public SensorDataSet() {
}

Expand All @@ -22,6 +24,7 @@ public SensorDataSet(SensorDataSet toCopy) {
this.cyclingCadence = toCopy.cyclingCadence;
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
this.cyclingPower = toCopy.cyclingPower;
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
}

public SensorDataHeartRate getHeartRate() {
Expand All @@ -40,6 +43,10 @@ public SensorDataCyclingPower getCyclingPower() {
return cyclingPower;
}

public SensorDataRunning getRunningDistanceSpeedCadence() {
return runningDistanceSpeedCadence;
}

public void set(SensorData<?> data) {
set(data, data);
}
Expand All @@ -53,6 +60,7 @@ public void clear() {
this.cyclingCadence = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
this.runningDistanceSpeedCadence = null;
}

public void fillTrackPoint(TrackPoint trackPoint) {
Expand All @@ -72,13 +80,21 @@ public void fillTrackPoint(TrackPoint trackPoint) {
if (cyclingPower != null && cyclingPower.hasValue()) {
trackPoint.setPower(cyclingPower.getValue());
}

if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasValue()) {
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getValue().getDistance());
trackPoint.setSpeed(runningDistanceSpeedCadence.getValue().getSpeed());
trackPoint.setCyclingCadence_rpm(runningDistanceSpeedCadence.getValue().getCadence());
}
}

public void reset() {
if (heartRate != null) heartRate.reset();
if (cyclingCadence != null) cyclingCadence.reset();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset();
if (cyclingPower != null) cyclingPower.reset();

if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.reset();
}

@NonNull
Expand All @@ -87,7 +103,8 @@ public String toString() {
return (getHeartRate() != null ? "" + getHeartRate() : "")
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
+ (getCyclingDistanceSpeed() != null ? " " + getCyclingDistanceSpeed() : "")
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
+ (getCyclingPower() != null ? " " + getCyclingPower() : "")
+ (getRunningDistanceSpeedCadence() != null ? " " + getRunningDistanceSpeedCadence() : "");
}

private void set(@NonNull SensorData<?> type, SensorData<?> data) {
Expand All @@ -110,6 +127,11 @@ private void set(@NonNull SensorData<?> type, SensorData<?> data) {
return;
}

if (type instanceof SensorDataRunning) {
this.runningDistanceSpeedCadence = (SensorDataRunning) data;
return;
}

throw new UnsupportedOperationException(type.getClass().getCanonicalName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
import de.dennisguse.opentracks.util.BluetoothUtils;

/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
* Also parses the transferred data into {@link SensorDataObserver}.
*/
public abstract class BluetoothConnectionManager {
public abstract class BluetoothConnectionManager<DataType> {

private static final String TAG = BluetoothConnectionManager.class.getSimpleName();

Expand Down Expand Up @@ -103,7 +104,7 @@ public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());

SensorData<?> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
observer.onChanged(sensorData);
Expand Down Expand Up @@ -148,14 +149,14 @@ synchronized boolean isSameBluetoothDevice(String address) {
return address.equals(bluetoothGatt.getDevice().getAddress());
}

protected abstract SensorData<?> createEmptySensorData(String address);
protected abstract SensorData<DataType> createEmptySensorData(String address);

/**
* @return null if data could not be parsed.
*/
protected abstract SensorData<?> parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
protected abstract SensorData<DataType> parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);

public static class HeartRate extends BluetoothConnectionManager {
public static class HeartRate extends BluetoothConnectionManager<Float> {

HeartRate(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
Expand All @@ -174,7 +175,7 @@ protected SensorDataHeartRate parsePayload(String sensorName, String address, Bl
}
}

public static class CyclingCadence extends BluetoothConnectionManager {
public static class CyclingCadence extends BluetoothConnectionManager<Float> {

CyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
Expand Down Expand Up @@ -205,7 +206,7 @@ protected SensorDataCycling.Cadence parsePayload(String sensorName, String addre
}
}

public static class CyclingDistanceSpeed extends BluetoothConnectionManager {
public static class CyclingDistanceSpeed extends BluetoothConnectionManager<SensorDataCycling.DistanceSpeed.Data> {

CyclingDistanceSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
Expand All @@ -232,7 +233,7 @@ protected SensorDataCycling.DistanceSpeed parsePayload(String sensorName, String
}
}

public static class CyclingPower extends BluetoothConnectionManager {
public static class CyclingPower extends BluetoothConnectionManager<Float> {

CyclingPower(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_POWER_UUID, BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, observer);
Expand All @@ -251,6 +252,23 @@ protected SensorDataCyclingPower parsePayload(String sensorName, String address,
}
}

public static class RunningSpeedAndCadence extends BluetoothConnectionManager<SensorDataRunning.Data> {

RunningSpeedAndCadence(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_UUID, BluetoothUtils.RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID, observer);
}

@Override
protected SensorDataRunning createEmptySensorData(String address) {
return new SensorDataRunning(address);
}

@Override
protected SensorDataRunning parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
return BluetoothUtils.parseRunningSpeedAndCadence(address, sensorName, characteristic);
}
}

interface SensorDataObserver {

void onChanged(SensorData<?> sensorData);
Expand Down
Loading

0 comments on commit 242147e

Please sign in to comment.