forked from upstream-mirrors/OpenTracks
Bluetooth LE: implemented cycling cadence and speed sensors.
Cadence is working while speed still requires the wheel size.
This commit is contained in:
@@ -19,7 +19,7 @@ package de.dennisguse.opentracks;
|
||||
import android.location.Location;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
|
||||
/**
|
||||
* Commons utilities for creating stubs of track, location.
|
||||
@@ -63,6 +63,6 @@ public class TrackStubUtils {
|
||||
location.setTime(INITIAL_TIME);
|
||||
location.setBearing(INITIAL_BEARING);
|
||||
|
||||
return new TrackPoint(location, new SensorDataSet(Float.NaN, Float.NaN));
|
||||
return new TrackPoint(location, new TrackPointSensorDataSet());
|
||||
}
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.util.UintUtils;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SensorDataCyclingTest {
|
||||
|
||||
@Test
|
||||
public void compute_cadence() {
|
||||
// given
|
||||
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, 1024); // 1s
|
||||
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(2, 2048); // 2s
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(57.2519, current.getCadence_rpm(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_cadence_sameCount() {
|
||||
// given
|
||||
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, 1024);
|
||||
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(1, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(0, current.getCadence_rpm(), 0.01);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void compute_cadence_sameTime() {
|
||||
// given
|
||||
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, 1024);
|
||||
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(2, 1024);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
Assert.assertFalse(current.hasCadence_rpm());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_cadence_rollOverTime() {
|
||||
// given
|
||||
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(1, UintUtils.UINT16_MAX - 1024);
|
||||
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(2, 0);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(57.2519, current.getCadence_rpm(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compute_cadence_rollOverCount() {
|
||||
// given
|
||||
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence(UintUtils.UINT32_MAX - 1, 1024);
|
||||
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence(0, 2048);
|
||||
|
||||
// when
|
||||
current.compute(previous);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(57.2519, current.getCadence_rpm(), 0.01);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import de.dennisguse.opentracks.TrackStubUtils;
|
||||
import de.dennisguse.opentracks.chart.ChartPoint;
|
||||
import de.dennisguse.opentracks.chart.ChartView;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
@@ -82,7 +82,7 @@ public class ChartFragmentTest {
|
||||
|
||||
// Input incorrect state.
|
||||
// Creates SensorData.
|
||||
SensorDataSet sensorDataSet = new SensorDataSet(SensorDataSet.DATA_UNAVAILABLE, SensorDataSet.DATA_UNAVAILABLE);
|
||||
TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet();
|
||||
trackPoint.setSensorDataSet(sensorDataSet);
|
||||
// Test.
|
||||
point = chartFragment.createPendingPoint(trackPoint);
|
||||
@@ -104,7 +104,10 @@ public class ChartFragmentTest {
|
||||
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
|
||||
|
||||
// Creates SensorData.
|
||||
SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102);
|
||||
TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet();
|
||||
sensorDataSet.setHeartRate_bpm(100f);
|
||||
sensorDataSet.setCyclingCadence(101);
|
||||
sensorDataSet.setPower(102f);
|
||||
|
||||
// Creates SensorDataSet.
|
||||
trackPoint.setSensorDataSet(sensorDataSet);
|
||||
|
||||
+2
-7
@@ -244,13 +244,8 @@ public class TrackRecordingServiceTestLocation {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSensorDataSetValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
return new SensorDataSet(1, 2);
|
||||
public SensorDataSet getSensorData() {
|
||||
return new SensorDataSet();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
|
||||
public class BluetoothUtilsTest {
|
||||
|
||||
@Test
|
||||
public void parseHeartRate_uint8() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, 0x3C});
|
||||
|
||||
// when
|
||||
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(60, heartRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseHeartRate_uint16() {
|
||||
// given
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
|
||||
|
||||
// when
|
||||
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(257, heartRate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
characteristic.setValue(new byte[]{0x01, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
|
||||
Assert.assertNull(sensor.getSpeed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_wheelOnly() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
characteristic.setValue(new byte[]{0x02, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
Assert.assertNull(sensor.getCadence());
|
||||
Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingSpeedCadence_crankWheel() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
|
||||
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
|
||||
|
||||
// when
|
||||
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
|
||||
Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package de.dennisguse.opentracks.chart;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
@@ -25,7 +25,7 @@ public class ChartPoint {
|
||||
this.elevation = elevation;
|
||||
}
|
||||
|
||||
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, SensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) {
|
||||
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPointSensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) {
|
||||
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
|
||||
|
||||
if (chartByDistance) {
|
||||
@@ -50,10 +50,10 @@ public class ChartPoint {
|
||||
pace = speed == 0 ? 0.0 : 60.0 / speed;
|
||||
if (sensorDataSet != null) {
|
||||
if (sensorDataSet.hasHeartRate()) {
|
||||
heartRate = sensorDataSet.getHeartRate();
|
||||
heartRate = sensorDataSet.getHeartRate_bpm();
|
||||
}
|
||||
if (sensorDataSet.hasCadence()) {
|
||||
cadence = sensorDataSet.getCadence();
|
||||
if (sensorDataSet.hasCyclingCadence()) {
|
||||
cadence = sensorDataSet.getCyclingCadence_rpm();
|
||||
}
|
||||
if (sensorDataSet.hasPower()) {
|
||||
power = sensorDataSet.getPower();
|
||||
|
||||
@@ -20,8 +20,6 @@ import android.location.LocationManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
|
||||
/**
|
||||
* This class extends the standard Android location with extra information.
|
||||
*
|
||||
@@ -39,17 +37,17 @@ public class TrackPoint {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Location location, SensorDataSet sensorDataSet) {
|
||||
public TrackPoint(@NonNull Location location, TrackPointSensorDataSet sensorDataSet) {
|
||||
this.location = location;
|
||||
this.sensorDataSet = sensorDataSet;
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull TrackPoint trackPoint, SensorDataSet sensorDataSet) {
|
||||
public TrackPoint(@NonNull TrackPoint trackPoint, TrackPointSensorDataSet sensorDataSet) {
|
||||
this.location = trackPoint.getLocation();
|
||||
this.sensorDataSet = sensorDataSet;
|
||||
}
|
||||
|
||||
private SensorDataSet sensorDataSet = null;
|
||||
private TrackPointSensorDataSet sensorDataSet = null;
|
||||
|
||||
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
|
||||
location = new Location(LocationManager.GPS_PROVIDER);
|
||||
@@ -90,11 +88,11 @@ public class TrackPoint {
|
||||
return location;
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
public TrackPointSensorDataSet getSensorDataSet() {
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
public void setSensorDataSet(SensorDataSet sensorDataSet) {
|
||||
public void setSensorDataSet(TrackPointSensorDataSet sensorDataSet) {
|
||||
this.sensorDataSet = sensorDataSet;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
//TODO Rename.
|
||||
public final class TrackPointSensorDataSet {
|
||||
private long time_ms;
|
||||
|
||||
private Float heartRate_bpm = null;
|
||||
private Float cyclingCadence = null;
|
||||
private Float cyclingSpeed = null;
|
||||
private Float power = null;
|
||||
|
||||
public TrackPointSensorDataSet() {
|
||||
this(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public TrackPointSensorDataSet(long time_ms) {
|
||||
this.time_ms = time_ms;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate() {
|
||||
return heartRate_bpm != null && heartRate_bpm > 0;
|
||||
}
|
||||
|
||||
public float getHeartRate_bpm() {
|
||||
return heartRate_bpm;
|
||||
}
|
||||
|
||||
public void setHeartRate_bpm(Float heartRate_bpm) {
|
||||
this.heartRate_bpm = heartRate_bpm;
|
||||
}
|
||||
|
||||
public boolean hasCyclingCadence() {
|
||||
return cyclingCadence != null;
|
||||
}
|
||||
|
||||
public Float getCyclingCadence_rpm() {
|
||||
return cyclingCadence;
|
||||
}
|
||||
|
||||
public void setCyclingCadence(Float cyclingCadence) {
|
||||
this.cyclingCadence = cyclingCadence;
|
||||
}
|
||||
|
||||
public void setCyclingCadence(Integer cyclingCadence) {
|
||||
this.cyclingCadence = cyclingCadence != null ? cyclingCadence.floatValue() : null;
|
||||
}
|
||||
|
||||
public boolean hasCyclingSpeed() {
|
||||
return cyclingSpeed != null;
|
||||
}
|
||||
|
||||
public Float getCyclingSpeed() {
|
||||
return cyclingSpeed;
|
||||
}
|
||||
|
||||
public void setCyclingSpeed(Float cyclingSpeed) {
|
||||
this.cyclingSpeed = cyclingSpeed;
|
||||
}
|
||||
|
||||
public boolean hasPower() {
|
||||
return power != null;
|
||||
}
|
||||
|
||||
public Float getPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public void setPower(Float power) {
|
||||
this.power = power;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time_ms;
|
||||
}
|
||||
|
||||
public void setTime(long time_ms) {
|
||||
this.time_ms = time_ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
*
|
||||
* @param maxAge the maximal age in milliseconds.
|
||||
*/
|
||||
public boolean isRecent(long maxAge) {
|
||||
return time_ms + maxAge > System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "time=" + getTime()
|
||||
+ (hasHeartRate() ? " heart=" + getHeartRate_bpm() : "")
|
||||
+ (hasCyclingCadence() ? " cyclingCad=" + getCyclingCadence_rpm() : "")
|
||||
+ (hasCyclingSpeed() ? " cyclingSpeed=" + getCyclingSpeed() : "");
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,11 @@ import de.dennisguse.opentracks.android.ContentResolverWrapper;
|
||||
import de.dennisguse.opentracks.android.IContentResolver;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
@@ -621,12 +621,19 @@ public class ContentProviderUtils {
|
||||
trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex));
|
||||
}
|
||||
|
||||
float heartRate = cursor.isNull(indexes.sensorHeartRateIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorHeartRateIndex);
|
||||
float cadence = cursor.isNull(indexes.sensorCadenceIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorCadenceIndex);
|
||||
float power = cursor.isNull(indexes.sensorPowerIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorPowerIndex);
|
||||
TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet();
|
||||
|
||||
trackPoint.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime()));
|
||||
if (cursor.isNull(indexes.sensorHeartRateIndex)) {
|
||||
sensorDataSet.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex));
|
||||
}
|
||||
if (cursor.isNull(indexes.sensorCadenceIndex)) {
|
||||
sensorDataSet.setCyclingCadence(cursor.getFloat(indexes.sensorCadenceIndex));
|
||||
}
|
||||
if (cursor.isNull(indexes.sensorPowerIndex)) {
|
||||
sensorDataSet.setCyclingSpeed(cursor.getFloat(indexes.sensorPowerIndex));
|
||||
}
|
||||
|
||||
trackPoint.setSensorDataSet(sensorDataSet);
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
@@ -806,15 +813,15 @@ public class ContentProviderUtils {
|
||||
}
|
||||
|
||||
//SensorData
|
||||
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
|
||||
TrackPointSensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
|
||||
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
|
||||
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getSensorDataSet().getHeartRate());
|
||||
values.put(TrackPointsColumns.SENSOR_HEARTRATE, sensorDataSet.getHeartRate_bpm());
|
||||
}
|
||||
if (sensorDataSet != null && sensorDataSet.hasCadence()) {
|
||||
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getSensorDataSet().getCadence());
|
||||
if (sensorDataSet != null && sensorDataSet.hasCyclingCadence()) {
|
||||
values.put(TrackPointsColumns.SENSOR_CADENCE, sensorDataSet.getCyclingCadence_rpm());
|
||||
}
|
||||
if (sensorDataSet != null && sensorDataSet.hasPower()) {
|
||||
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getSensorDataSet().getPower());
|
||||
values.put(TrackPointsColumns.SENSOR_POWER, sensorDataSet.getPower());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
|
||||
public class SensorData {
|
||||
|
||||
private String sensorAddress;
|
||||
private String sensorName;
|
||||
|
||||
private long timestamp_ms;
|
||||
|
||||
SensorData(String sensorAddress, String sensorName) {
|
||||
this(sensorAddress, sensorName, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
SensorData(String sensorAddress, String sensorName, long timestamp_ms) {
|
||||
this.sensorAddress = sensorAddress;
|
||||
this.sensorName = sensorName;
|
||||
this.timestamp_ms = timestamp_ms;
|
||||
}
|
||||
|
||||
public String getSensorAddress() {
|
||||
return sensorAddress;
|
||||
}
|
||||
|
||||
public String getSensorName() {
|
||||
return sensorName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
*/
|
||||
public boolean isRecent() {
|
||||
return timestamp_ms + BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS > System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.util.UintUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Cycling Cadence and Speed sensors.
|
||||
* <p>
|
||||
* https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
|
||||
*/
|
||||
public final class SensorDataCycling {
|
||||
|
||||
private static final String TAG = SensorDataCycling.class.getSimpleName();
|
||||
|
||||
private static final int INVALID_VALUE_INT = -1;
|
||||
private static final float INVALID_VALUE_FLOAT = Float.NaN;
|
||||
|
||||
private SensorDataCycling() {
|
||||
}
|
||||
|
||||
public static class Cadence extends SensorData {
|
||||
|
||||
private long crankRevolutionsCount; // UINT32
|
||||
private int crankRevolutionsTime; // UINT16; 1/1024s
|
||||
private float cadence_rpm = INVALID_VALUE_FLOAT;
|
||||
|
||||
public Cadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.crankRevolutionsCount = crankRevolutionsCount;
|
||||
this.crankRevolutionsTime = crankRevolutionsTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for Wahoo CADENCE: provides speed instead of cadence
|
||||
*/
|
||||
public Cadence(@NonNull SensorDataCycling.Speed speed) {
|
||||
this(speed.getSensorAddress(), speed.getSensorName(), speed.getWheelRevolutionsCount(), speed.getWheelRevolutionsTime());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Cadence(long crankRevolutionsCount, int crankRevolutionsTime) {
|
||||
super("sensorAddress", "sensorName");
|
||||
this.crankRevolutionsCount = crankRevolutionsCount;
|
||||
this.crankRevolutionsTime = crankRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return crankRevolutionsCount != INVALID_VALUE_INT && crankRevolutionsTime != INVALID_VALUE_INT;
|
||||
}
|
||||
|
||||
public long getCrankRevolutionsCount() {
|
||||
return crankRevolutionsCount;
|
||||
}
|
||||
|
||||
public int getCrankRevolutionsTime() {
|
||||
return crankRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasCadence_rpm() {
|
||||
return !Float.isNaN(cadence_rpm);
|
||||
}
|
||||
|
||||
public float getCadence_rpm() {
|
||||
return cadence_rpm;
|
||||
}
|
||||
|
||||
public void compute(Cadence previous) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
Log.e(TAG, previous.getCrankRevolutionsTime() + " " + previous.getCrankRevolutionsCount() + " - " + this.getCrankRevolutionsTime() + " " + getCrankRevolutionsCount()); //TODO REMOVE
|
||||
long timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) * 1024 / UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
cadence_rpm = INVALID_VALUE_FLOAT;
|
||||
} else {
|
||||
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
float cadence_ms = crankDiff / (float) timeDiff_ms;
|
||||
cadence_rpm = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "cadence=" + getCadence_rpm() + "_" + getCrankRevolutionsTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof Cadence)) return false;
|
||||
|
||||
Cadence comp = (Cadence) obj;
|
||||
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO Speed computation; needs wheel diameter / circumference
|
||||
public static class Speed extends SensorData {
|
||||
|
||||
private int wheelRevolutionsCount; // UINT16
|
||||
private int wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
private float speed_ms = INVALID_VALUE_FLOAT;
|
||||
|
||||
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.wheelRevolutionsCount = wheelRevolutionsCount;
|
||||
this.wheelRevolutionsTime = wheelRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return wheelRevolutionsCount != INVALID_VALUE_INT && wheelRevolutionsTime != INVALID_VALUE_INT;
|
||||
}
|
||||
|
||||
public int getWheelRevolutionsCount() {
|
||||
return wheelRevolutionsCount;
|
||||
}
|
||||
|
||||
public int getWheelRevolutionsTime() {
|
||||
return wheelRevolutionsTime;
|
||||
}
|
||||
|
||||
public boolean hasSpeed() {
|
||||
return !Float.isNaN(speed_ms);
|
||||
}
|
||||
|
||||
public float getSpeed_ms() {
|
||||
return speed_ms;
|
||||
}
|
||||
|
||||
public void compute(Speed previous) {
|
||||
if (hasData() && previous != null && previous.hasData()) {
|
||||
long timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) * 1024 / UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
speed_ms = INVALID_VALUE_FLOAT;
|
||||
} else {
|
||||
long crankDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
speed_ms = crankDiff / (float) timeDiff_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "speed=" + getSpeed_ms() + "_" + getWheelRevolutionsTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof Speed)) return false;
|
||||
|
||||
Speed comp = (Speed) obj;
|
||||
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
||||
}
|
||||
}
|
||||
|
||||
public static class CadenceAndSpeed extends SensorData {
|
||||
|
||||
private Cadence cadence;
|
||||
private Speed speed;
|
||||
|
||||
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
|
||||
super(sensorAddress, sensorName);
|
||||
this.cadence = cadence;
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public Cadence getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public Speed getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SensorDataHeartRate extends SensorData {
|
||||
|
||||
private float heartRate_bpm;
|
||||
|
||||
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
|
||||
super(name, address);
|
||||
this.heartRate_bpm = heartRate_bpm;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate_bpm() {
|
||||
return !Float.isNaN(heartRate_bpm);
|
||||
}
|
||||
|
||||
public float getHeartRate_bpm() {
|
||||
return heartRate_bpm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "heart=" + heartRate_bpm;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +1,97 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public final class SensorDataSet {
|
||||
|
||||
public static final float DATA_UNAVAILABLE = Float.NaN;
|
||||
private SensorDataHeartRate heartRate;
|
||||
|
||||
//TODO It might be necessary to consider: if sensor was connected as well.
|
||||
private String sensorName;
|
||||
private String sensorAddress;
|
||||
private float heartRate;
|
||||
private float cadence;
|
||||
private float power;
|
||||
private float batteryLevel;
|
||||
private long time;
|
||||
private SensorDataCycling.Cadence cyclingCadence;
|
||||
|
||||
public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel, long time) {
|
||||
private SensorDataCycling.Speed cyclingSpeed;
|
||||
|
||||
public SensorDataSet() {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public SensorDataSet(SensorDataHeartRate heartRate, SensorDataCycling.Cadence cyclingCadence, SensorDataCycling.Speed cyclingSpeed) {
|
||||
this.heartRate = heartRate;
|
||||
this.cadence = cadence;
|
||||
this.power = power;
|
||||
this.batteryLevel = batteryLevel;
|
||||
this.time = time;
|
||||
this.cyclingCadence = cyclingCadence;
|
||||
this.cyclingSpeed = cyclingSpeed;
|
||||
}
|
||||
|
||||
public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel) {
|
||||
this(heartRate, cadence, power, batteryLevel, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public SensorDataSet(float heartRate, float cadence, float power) {
|
||||
this(heartRate, cadence, power, DATA_UNAVAILABLE, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public SensorDataSet(float heartRate, float cadence) {
|
||||
this(heartRate, cadence, DATA_UNAVAILABLE, DATA_UNAVAILABLE, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public SensorDataSet(float heartRate, String sensorName, String sensorAddress) {
|
||||
this(heartRate, DATA_UNAVAILABLE, DATA_UNAVAILABLE, DATA_UNAVAILABLE, System.currentTimeMillis());
|
||||
this.sensorName = sensorName;
|
||||
this.sensorAddress = sensorAddress;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate() {
|
||||
return !Float.isNaN(heartRate) && heartRate > 0;
|
||||
}
|
||||
|
||||
public float getHeartRate() {
|
||||
public SensorDataHeartRate getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
public boolean hasCadence() {
|
||||
return !Float.isNaN(cadence);
|
||||
public SensorDataCycling.Cadence getCyclingCadence() {
|
||||
return cyclingCadence;
|
||||
}
|
||||
|
||||
public float getCadence() {
|
||||
return cadence;
|
||||
public SensorDataCycling.Speed getCyclingSpeed() {
|
||||
return cyclingSpeed;
|
||||
}
|
||||
|
||||
public boolean hasPower() {
|
||||
return !Float.isNaN(power);
|
||||
public void set(SensorData data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data instanceof SensorDataHeartRate) {
|
||||
this.heartRate = (SensorDataHeartRate) data;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data instanceof SensorDataCycling.Cadence) {
|
||||
this.cyclingCadence = (SensorDataCycling.Cadence) data;
|
||||
return;
|
||||
}
|
||||
if (data instanceof SensorDataCycling.Speed) {
|
||||
this.cyclingSpeed = (SensorDataCycling.Speed) data;
|
||||
return;
|
||||
}
|
||||
if (data instanceof SensorDataCycling.CadenceAndSpeed) {
|
||||
set(((SensorDataCycling.CadenceAndSpeed) data).getCadence());
|
||||
set(((SensorDataCycling.CadenceAndSpeed) data).getSpeed());
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public float getPower() {
|
||||
return power;
|
||||
public void clear() {
|
||||
this.heartRate = null;
|
||||
this.cyclingCadence = null;
|
||||
this.cyclingSpeed = null;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
public TrackPointSensorDataSet createTrackPointSensorDataSet() {
|
||||
TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet();
|
||||
if (heartRate != null) {
|
||||
sensorDataSet.setHeartRate_bpm(heartRate.getHeartRate_bpm());
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
*
|
||||
* @param maxAge the maximal age in milliseconds.
|
||||
*/
|
||||
public boolean isRecent(long maxAge) {
|
||||
return time + maxAge > System.currentTimeMillis();
|
||||
}
|
||||
if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) {
|
||||
sensorDataSet.setCyclingCadence(cyclingCadence.getCadence_rpm());
|
||||
}
|
||||
|
||||
public boolean hasBatteryLevel() {
|
||||
return !Float.isNaN(batteryLevel);
|
||||
}
|
||||
if (cyclingSpeed != null && cyclingSpeed.hasSpeed()) {
|
||||
sensorDataSet.setCyclingCadence(cyclingSpeed.getSpeed_ms());
|
||||
}
|
||||
|
||||
public float getBatteryLevel() {
|
||||
return batteryLevel;
|
||||
}
|
||||
|
||||
public String getSensorName() {
|
||||
return sensorName;
|
||||
}
|
||||
|
||||
public String getSensorAddress() {
|
||||
return sensorAddress;
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "time=" + getTime() + " sensor=" + getSensorAddress() + " heart=" + getHeartRate();
|
||||
return (getHeartRate() != null ? "" + getHeartRate() : "")
|
||||
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
|
||||
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public enum SensorState {
|
||||
NONE,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED;
|
||||
|
||||
public static String getStateAsString(SensorState state, Context c) {
|
||||
switch (state) {
|
||||
case NONE:
|
||||
return c.getString(R.string.value_none);
|
||||
case CONNECTING:
|
||||
return c.getString(R.string.sensor_state_connecting);
|
||||
case CONNECTED:
|
||||
return c.getString(R.string.sensor_state_connected);
|
||||
case DISCONNECTING:
|
||||
case DISCONNECTED:
|
||||
return c.getString(R.string.sensor_state_disconnected);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,11 @@ import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
@@ -114,9 +114,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
/* Views */
|
||||
private View sensorContainerView;
|
||||
private View heartRateContainer;
|
||||
private TextView heartRateValueView;
|
||||
private TextView heartRateSensorView;
|
||||
private View cadenceContainer;
|
||||
private TextView cadenceValueView;
|
||||
private TextView cadenceSensorView;
|
||||
|
||||
private TextView totalTimeValueView;
|
||||
private final Runnable updateUIeachSecond = new Runnable() {
|
||||
@@ -162,10 +165,14 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
sensorContainerView = view.findViewById(R.id.stats_sensor_container);
|
||||
heartRateContainer = view.findViewById(R.id.stats_sensor_heart_rate_container);
|
||||
heartRateValueView = view.findViewById(R.id.stats_sensor_heart_rate_value);
|
||||
heartRateSensorView = view.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
|
||||
|
||||
cadenceContainer = view.findViewById(R.id.stats_sensor_cadence_container);
|
||||
cadenceValueView = view.findViewById(R.id.stats_sensor_cadence_value);
|
||||
cadenceSensorView = view.findViewById(R.id.stats_sensor_cadence_sensor_value);
|
||||
|
||||
totalTimeValueView = view.findViewById(R.id.stats_total_time_value);
|
||||
|
||||
distanceValue = view.findViewById(R.id.stats_distance_value);
|
||||
@@ -269,9 +276,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
sensorContainerView = null;
|
||||
heartRateContainer = null;
|
||||
heartRateValueView = null;
|
||||
heartRateSensorView = null;
|
||||
cadenceContainer = null;
|
||||
cadenceValueView = null;
|
||||
cadenceSensorView = null;
|
||||
|
||||
totalTimeValueView = null;
|
||||
|
||||
@@ -426,7 +436,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
|
||||
* Tries to fetch most recent {@link TrackPointSensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
|
||||
*/
|
||||
private void updateSensorDataUI() {
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
@@ -435,33 +445,56 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
if (trackRecordingService == null) {
|
||||
Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service.");
|
||||
} else {
|
||||
//TODO sensorState = trackRecordingService.getSensorState();
|
||||
sensorDataSet = trackRecordingService.getSensorData();
|
||||
}
|
||||
|
||||
setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording());
|
||||
setCadenceSensorData(sensorDataSet, isSelectedTrackRecording());
|
||||
}
|
||||
|
||||
private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
|
||||
// heart rate
|
||||
int isVisible = View.VISIBLE;
|
||||
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext())) {
|
||||
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressNone(getContext())) {
|
||||
isVisible = View.INVISIBLE;
|
||||
}
|
||||
sensorContainerView.setVisibility(isVisible);
|
||||
heartRateContainer.setVisibility(isVisible);
|
||||
|
||||
if (isRecording) {
|
||||
String heartRate = getContext().getString(R.string.value_unknown);
|
||||
String sensorValue = getContext().getString(R.string.value_unknown);
|
||||
String sensorName = getContext().getString(R.string.value_unknown);
|
||||
if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) {
|
||||
sensorName = sensorDataSet.getSensorName();
|
||||
if (sensorDataSet.hasHeartRate()) {
|
||||
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
|
||||
if (sensorDataSet != null && sensorDataSet.getHeartRate() != null) {
|
||||
sensorName = sensorDataSet.getHeartRate().getSensorName();
|
||||
if (sensorDataSet.getHeartRate().hasHeartRate_bpm() && sensorDataSet.getHeartRate().isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(sensorDataSet.getHeartRate().getHeartRate_bpm(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
heartRateSensorView.setText(sensorName);
|
||||
heartRateValueView.setText(heartRate);
|
||||
heartRateValueView.setText(sensorValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCadenceSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
|
||||
// heart rate
|
||||
int isVisible = View.VISIBLE;
|
||||
if (!isRecording || PreferencesUtils.isBluetoothCyclingCadenceSensorAddressNone(getContext())) {
|
||||
isVisible = View.INVISIBLE;
|
||||
}
|
||||
cadenceContainer.setVisibility(isVisible);
|
||||
|
||||
if (isRecording) {
|
||||
String sensorValue = getContext().getString(R.string.value_unknown);
|
||||
String sensorName = getContext().getString(R.string.value_unknown);
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingCadence() != null) {
|
||||
sensorName = sensorDataSet.getCyclingCadence().getSensorName();
|
||||
if (sensorDataSet.getCyclingCadence().hasCadence_rpm() && sensorDataSet.getCyclingCadence().isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(sensorDataSet.getCyclingCadence().getCadence_rpm(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
cadenceSensorView.setText(sensorName);
|
||||
cadenceValueView.setText(sensorValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.io.file.exporter;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
@@ -29,9 +28,9 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.DescriptionGenerator;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
@@ -73,11 +72,11 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
private TrackPoint startTrackPoint;
|
||||
|
||||
/**
|
||||
* @param context the context
|
||||
* @param hasMultipleTracks should encode multiple tracks into one file?
|
||||
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
|
||||
* @param exportSensorData should {@link SensorDataSet} be exported?
|
||||
* @param exportPhotos should pictures be exported (if true: exports to KMZ)?
|
||||
* @param context the context
|
||||
* @param hasMultipleTracks should encode multiple tracks into one file?
|
||||
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
|
||||
* @param exportSensorData should {@link TrackPointSensorDataSet} be exported?
|
||||
* @param exportPhotos should pictures be exported (if true: exports to KMZ)?
|
||||
*/
|
||||
public KmlTrackWriter(Context context, boolean hasMultipleTracks, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) {
|
||||
this.context = context;
|
||||
@@ -269,13 +268,13 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
|
||||
|
||||
if (exportSensorData) {
|
||||
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
|
||||
TrackPointSensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
|
||||
if (sensorDataSet != null) {
|
||||
if (sensorDataSet.hasHeartRate()) {
|
||||
heartRateList.add(sensorDataSet.getHeartRate());
|
||||
heartRateList.add(sensorDataSet.getHeartRate_bpm());
|
||||
}
|
||||
if (sensorDataSet.hasCadence()) {
|
||||
cadenceList.add(sensorDataSet.getCadence());
|
||||
if (sensorDataSet.hasCyclingCadence()) {
|
||||
cadenceList.add(sensorDataSet.getCyclingCadence_rpm());
|
||||
}
|
||||
if (sensorDataSet.hasPower()) {
|
||||
powerList.add(sensorDataSet.getPower());
|
||||
@@ -453,7 +452,7 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
/**
|
||||
* Writes a sensor style.
|
||||
*
|
||||
* @param name the name of the sesnor
|
||||
* @param name the name of the sesnor
|
||||
* @param sensorType the sensor display name
|
||||
*/
|
||||
private void writeSensorStyle(String name, String sensorType) {
|
||||
|
||||
@@ -27,8 +27,8 @@ import org.xml.sax.SAXException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
|
||||
|
||||
/**
|
||||
@@ -224,29 +224,25 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
TrackPoint trackPoint = trackPoints.get(i);
|
||||
|
||||
boolean hasSensorData = false;
|
||||
float heartrate = SensorDataSet.DATA_UNAVAILABLE;
|
||||
float cadence = SensorDataSet.DATA_UNAVAILABLE;
|
||||
float power = SensorDataSet.DATA_UNAVAILABLE;
|
||||
TrackPointSensorDataSet sensorDataSet = new TrackPointSensorDataSet(trackPoint.getTime());
|
||||
|
||||
if (i < heartRateList.size()) {
|
||||
heartrate = heartRateList.get(i);
|
||||
sensorDataSet.setHeartRate_bpm(heartRateList.get(i));
|
||||
hasSensorData = true;
|
||||
}
|
||||
if (i < cadenceList.size()) {
|
||||
cadence = cadenceList.get(i);
|
||||
sensorDataSet.setCyclingCadence(cadenceList.get(i));
|
||||
hasSensorData = true;
|
||||
}
|
||||
if (i < powerList.size()) {
|
||||
power = powerList.get(i);
|
||||
sensorDataSet.setPower(cadenceList.get(i));
|
||||
hasSensorData = true;
|
||||
}
|
||||
|
||||
if (!hasSensorData) {
|
||||
insertTrackPoint(trackPoint);
|
||||
} else {
|
||||
TrackPoint sensorDataSetLocation = new TrackPoint(trackPoint, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime()));
|
||||
insertTrackPoint(sensorDataSetLocation);
|
||||
trackPoint.setSensorDataSet(sensorDataSet);
|
||||
}
|
||||
insertTrackPoint(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import de.dennisguse.opentracks.TrackDetailActivity;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointSensorDataSet;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
|
||||
@@ -575,7 +576,7 @@ public class TrackRecordingService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = new TrackPoint(location, getSensorDataSet());
|
||||
TrackPoint trackPoint = new TrackPoint(location, getTrackPointSensorDataSet());
|
||||
notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy);
|
||||
|
||||
if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) {
|
||||
@@ -708,10 +709,20 @@ public class TrackRecordingService extends Service {
|
||||
}
|
||||
|
||||
SensorDataSet getSensorDataSet() {
|
||||
if (remoteSensorManager == null || !remoteSensorManager.isEnabled() || !remoteSensorManager.isSensorDataSetValid()) {
|
||||
if (remoteSensorManager == null || !remoteSensorManager.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
return remoteSensorManager.getSensorDataSet();
|
||||
|
||||
return remoteSensorManager.getSensorData();
|
||||
}
|
||||
|
||||
TrackPointSensorDataSet getTrackPointSensorDataSet() {
|
||||
SensorDataSet sensorData = getSensorDataSet();
|
||||
if (sensorData != null) {
|
||||
return sensorData.createTrackPointSensorDataSet();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void registerLocationListener() {
|
||||
|
||||
@@ -95,8 +95,13 @@ public interface TrackRecordingServiceInterface {
|
||||
long insertWaypoint(String name, String category, String description, String photoUrl);
|
||||
|
||||
/**
|
||||
* ONLY FOR TESTING
|
||||
* Gets the current sensor data. Returns null if there is no data.
|
||||
*
|
||||
* @return SensorDataSet object.
|
||||
*/
|
||||
SensorDataSet getSensorData();
|
||||
|
||||
/**
|
||||
* Inserts a location in the current recording track.
|
||||
* <p>
|
||||
* When recording a track, GPS locations are automatically inserted.
|
||||
@@ -107,13 +112,6 @@ public interface TrackRecordingServiceInterface {
|
||||
@VisibleForTesting
|
||||
void insertLocation(Location location);
|
||||
|
||||
/**
|
||||
* Gets the current sensor data. Returns null if there is no data.
|
||||
*
|
||||
* @return SensorDataSet object.
|
||||
*/
|
||||
SensorDataSet getSensorData();
|
||||
|
||||
/**
|
||||
* Disables processing of location updates from {@link android.location.LocationManager}.
|
||||
*/
|
||||
|
||||
+90
-52
@@ -30,27 +30,24 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorState;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
|
||||
/**
|
||||
* Manages connection to Bluetooth LE heart rate monitor.
|
||||
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
|
||||
* Also parses the transferred data into {@link SensorDataObserver}.
|
||||
*/
|
||||
public class BluetoothConnectionManager {
|
||||
|
||||
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
|
||||
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
|
||||
public abstract class BluetoothConnectionManager {
|
||||
|
||||
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final EventCallback observer;
|
||||
|
||||
private SensorState sensorState;
|
||||
private final SensorDataObserver observer;
|
||||
|
||||
private final UUID serviceUUUID;
|
||||
private final UUID measurementUUID;
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
private final BluetoothDevice bluetoothDevice;
|
||||
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
@@ -58,80 +55,70 @@ public class BluetoothConnectionManager {
|
||||
switch (newState) {
|
||||
case BluetoothProfile.STATE_CONNECTING:
|
||||
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
|
||||
setState(SensorState.CONNECTING);
|
||||
|
||||
observer.connecting(gatt.getDevice().getName());
|
||||
case BluetoothProfile.STATE_CONNECTED:
|
||||
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
|
||||
setState(SensorState.CONNECTED);
|
||||
|
||||
gatt.discoverServices();
|
||||
|
||||
observer.connected(gatt.getDevice().getName());
|
||||
break;
|
||||
case BluetoothProfile.STATE_DISCONNECTING:
|
||||
Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice());
|
||||
setState(SensorState.DISCONNECTING);
|
||||
|
||||
case BluetoothProfile.STATE_DISCONNECTED:
|
||||
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
|
||||
setState(SensorState.DISCONNECTED);
|
||||
|
||||
observer.disconnected(gatt.getDevice().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
|
||||
BluetoothGattService service = gatt.getService(BluetoothUtils.HEART_RATE_SERVICE_UUID);
|
||||
BluetoothGattService service = gatt.getService(serviceUUUID);
|
||||
if (service == null) {
|
||||
Log.e(TAG, "Could not get heart rate service for " + gatt.getDevice().getAddress());
|
||||
Log.e(TAG, "Could not get service for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
BluetoothGattCharacteristic characteristic = service.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
|
||||
BluetoothGattCharacteristic characteristic = service.getCharacteristic(measurementUUID);
|
||||
if (characteristic == null) {
|
||||
Log.e(TAG, "Could not get BluetoothCharacteristic for " + gatt.getDevice().getAddress());
|
||||
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID + " characteristicUUID=" + measurementUUID);
|
||||
return;
|
||||
}
|
||||
gatt.setCharacteristicNotification(characteristic, true);
|
||||
|
||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
|
||||
// Register for updates.
|
||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
|
||||
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||
gatt.writeDescriptor(descriptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
String deviceName = gatt.getDevice().getName();
|
||||
String sensorName = gatt.getDevice().getName();
|
||||
Log.d(TAG, "Received data from " + sensorName);
|
||||
|
||||
Log.d(TAG, "Received heart beat rate " + deviceName + ": " + heartRate);
|
||||
observer.onSensorDataReceived(new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress()));
|
||||
SensorData sensorData = parsePayload(sensorName, gatt.getDevice().getAddress(), characteristic);
|
||||
if (sensorData != null) {
|
||||
observer.onChanged(sensorData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, EventCallback observer) {
|
||||
this.context = context;
|
||||
this.bluetoothDevice = bluetoothDevice;
|
||||
this.sensorState = SensorState.NONE;
|
||||
BluetoothConnectionManager(UUID serviceUUUID, UUID measurementUUID, SensorDataObserver observer) {
|
||||
this.serviceUUUID = serviceUUUID;
|
||||
this.measurementUUID = measurementUUID;
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
public synchronized void connect() {
|
||||
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
|
||||
if (bluetoothGatt != null) {
|
||||
Log.w(TAG, "Already connected; ignoring.");
|
||||
}
|
||||
|
||||
Log.d(TAG, "Connecting to: " + bluetoothDevice);
|
||||
Log.d(TAG, "Connecting to: " + device);
|
||||
|
||||
bluetoothGatt = bluetoothDevice.connectGatt(this.context, true, this.connectCallback);
|
||||
bluetoothGatt = device.connectGatt(context, true, this.connectCallback);
|
||||
|
||||
setState(SensorState.CONNECTING);
|
||||
}
|
||||
|
||||
public synchronized void disconnect() {
|
||||
synchronized void disconnect() {
|
||||
if (bluetoothGatt == null) {
|
||||
Log.w(TAG, "Cannot disconnect if not connected.");
|
||||
return;
|
||||
@@ -140,27 +127,78 @@ public class BluetoothConnectionManager {
|
||||
bluetoothGatt = null;
|
||||
}
|
||||
|
||||
public synchronized boolean isSameBluetoothDevice(String address) {
|
||||
return this.bluetoothDevice.getAddress().equals(address);
|
||||
synchronized boolean isSameBluetoothDevice(String address) {
|
||||
if (bluetoothGatt == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return address.equals(bluetoothGatt.getDevice().getAddress());
|
||||
}
|
||||
|
||||
synchronized SensorState getSensorState() {
|
||||
return sensorState;
|
||||
/**
|
||||
* @return null if data could not be parsed.
|
||||
*/
|
||||
protected abstract de.dennisguse.opentracks.content.sensor.SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
|
||||
|
||||
public static class HeartRate extends BluetoothConnectionManager {
|
||||
|
||||
HeartRate(@NonNull SensorDataObserver observer) {
|
||||
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataHeartRate parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||
|
||||
return heartRate != null ? new SensorDataHeartRate(address, sensorName, heartRate) : null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void setState(SensorState sensorState) {
|
||||
this.sensorState = sensorState;
|
||||
public static class CyclingCadence extends BluetoothConnectionManager {
|
||||
|
||||
CyclingCadence(SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SensorDataCycling.Cadence parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (cadenceAndSpeed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cadenceAndSpeed.getCadence() != null) {
|
||||
return cadenceAndSpeed.getCadence();
|
||||
}
|
||||
|
||||
//Workaround for Wahoo CADENCE: this sensor reports speed (instead of cadence)
|
||||
if (cadenceAndSpeed.getSpeed() != null) {
|
||||
return new SensorDataCycling.Cadence(cadenceAndSpeed.getSpeed());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CyclingSpeed extends BluetoothConnectionManager {
|
||||
|
||||
interface EventCallback {
|
||||
CyclingSpeed(SensorDataObserver observer) {
|
||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||
}
|
||||
|
||||
void connecting(String sensorName);
|
||||
@Override
|
||||
protected SensorDataCycling.Speed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||
if (cadenceAndSpeed != null) {
|
||||
return cadenceAndSpeed.getSpeed();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void connected(String sensorName);
|
||||
interface SensorDataObserver {
|
||||
|
||||
void onSensorDataReceived(SensorDataSet sensorDataSet);
|
||||
void onChanged(SensorData sensorData);
|
||||
|
||||
void disconnected(String sensorName);
|
||||
}
|
||||
}
|
||||
|
||||
+78
-67
@@ -21,53 +21,72 @@ import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* Bluetooth LE sensor manager.
|
||||
* Should only be instantiated once!
|
||||
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
|
||||
*
|
||||
* Note: should only be instantiated once.
|
||||
*
|
||||
* TODO: listen for Bluetooth enabled/disabled events.
|
||||
*
|
||||
* TODO: In case, a cycling (Cadence and Speed) sensor reports both values, testing is required.
|
||||
* We establish two GATT separate GATT connections (as if two different sensors were used).
|
||||
* However, it is not clear if this is allowed.
|
||||
* Even if this works, it is not clear what happens if a user (while recording) changes one of the sensors in the settings as this will trigger a disconnect of one GATT.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.EventCallback {
|
||||
public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.SensorDataObserver {
|
||||
|
||||
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5000;
|
||||
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
|
||||
|
||||
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
|
||||
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5 * UnitConversions.S_TO_MS;
|
||||
|
||||
private static final BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
private boolean started = false;
|
||||
|
||||
private final BluetoothConnectionManager heartRate = new BluetoothConnectionManager.HeartRate(this);
|
||||
private final BluetoothConnectionManager cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
|
||||
|
||||
private final SensorDataSet sensorDataSet = new SensorDataSet();
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
|
||||
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
|
||||
if (!started) return;
|
||||
|
||||
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
|
||||
if (PreferencesUtils.isBluetoothHeartRateSensorAddressNone(context)) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
|
||||
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
|
||||
connect(heartRate, address);
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
|
||||
if (PreferencesUtils.isBluetoothCyclingCadenceSensorAddressNone(context)) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress(context);
|
||||
|
||||
disconnect();
|
||||
startCurrentSensor();
|
||||
connect(cyclingCadence, address);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private SensorDataSet sensorDataSet = null;
|
||||
private BluetoothConnectionManager bluetoothConnectionManager;
|
||||
|
||||
/**
|
||||
* @param context the context
|
||||
*/
|
||||
@@ -77,84 +96,76 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
||||
}
|
||||
|
||||
public void start() {
|
||||
started = true;
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
startCurrentSensor();
|
||||
|
||||
//Trigger connection startup
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
disconnect();
|
||||
public synchronized void stop() {
|
||||
// Disconnecting
|
||||
heartRate.disconnect();
|
||||
cyclingCadence.disconnect();
|
||||
|
||||
sensorDataSet.clear();
|
||||
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
started = false;
|
||||
}
|
||||
|
||||
|
||||
public boolean isEnabled() {
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
public boolean isSensorDataSetValid() {
|
||||
SensorDataSet sensorDataSet = getSensorDataSet();
|
||||
if (sensorDataSet == null) {
|
||||
return false;
|
||||
}
|
||||
return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE_MS);
|
||||
}
|
||||
|
||||
private void startCurrentSensor() {
|
||||
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
|
||||
if (!isEnabled()) {
|
||||
Log.w(TAG, "Bluetooth not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
|
||||
Log.w(TAG, "No bluetooth address.");
|
||||
if (PreferencesUtils.isBluetoothSensorAddressNone(context, address)) {
|
||||
Log.w(TAG, "No Bluetooth address.");
|
||||
return;
|
||||
}
|
||||
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
|
||||
|
||||
// Check if there is an ongoing connection; if yes, check if the address changed.
|
||||
if (connectionManager.isSameBluetoothDevice(address)) {
|
||||
return;
|
||||
} else {
|
||||
connectionManager.disconnect();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Connecting to bluetooth address: " + address);
|
||||
|
||||
BluetoothDevice device;
|
||||
try {
|
||||
device = bluetoothAdapter.getRemoteDevice(address);
|
||||
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
|
||||
connectionManager.connect(context, device);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to get remote device for: " + address, e);
|
||||
|
||||
String toastMessage = context.getString(R.string.sensor_not_known, address);
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
disconnect();
|
||||
|
||||
bluetoothConnectionManager = new BluetoothConnectionManager(context, device, this);
|
||||
bluetoothConnectionManager.connect();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
if (bluetoothConnectionManager != null) {
|
||||
bluetoothConnectionManager.disconnect();
|
||||
bluetoothConnectionManager = null;
|
||||
Log.e(TAG, "Unable to get remote device for: " + address, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connecting(String sensorName) {
|
||||
public SensorDataSet getSensorData() {
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connected(String sensorName) {
|
||||
}
|
||||
public synchronized void onChanged(de.dennisguse.opentracks.content.sensor.SensorData sensorData) {
|
||||
if (sensorData instanceof SensorDataCycling.Cadence) {
|
||||
if (sensorData.equals(sensorDataSet.getCyclingCadence())) {
|
||||
Log.d(TAG, "onChanged: cadence data repeated.");
|
||||
return;
|
||||
}
|
||||
((SensorDataCycling.Cadence) sensorData).compute(sensorDataSet.getCyclingCadence());
|
||||
}
|
||||
if (sensorData instanceof SensorDataCycling.Speed) {
|
||||
if (sensorData.equals(sensorDataSet.getCyclingSpeed())) {
|
||||
Log.d(TAG, "onChanged: speed data repeated.");
|
||||
return;
|
||||
}
|
||||
((SensorDataCycling.Speed) sensorData).compute(sensorDataSet.getCyclingSpeed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorDataReceived(SensorDataSet sensorDataSet) {
|
||||
this.sensorDataSet = sensorDataSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected(String sensorName) {
|
||||
sensorDataSet.set(sensorData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
|
||||
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.util.HackUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
@@ -111,8 +114,10 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
} else if (preference instanceof ActivityTypePreference) {
|
||||
activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey());
|
||||
dialogFragment = activityPreferenceDialog;
|
||||
} else if (preference instanceof BluetoothLePreference) {
|
||||
dialogFragment = BluetoothLePreference.BluetoothLePreferenceDialog.newInstance(preference.getKey());
|
||||
} else if (preference instanceof BluetoothLeHeartRatePreference) {
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SERVICE_UUID);
|
||||
} else if (preference instanceof BluetoothLeCyclingCadenceAndSpeedPreference) {
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID);
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@@ -139,7 +144,7 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
|
||||
private void updateBluetooth() {
|
||||
// Disable Bluetooth preference if device does not have Bluetooth
|
||||
BluetoothLePreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key));
|
||||
BluetoothLeSensorPreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key));
|
||||
bluetoothPreference.setVisible(BluetoothUtils.hasBluetooth(TAG));
|
||||
}
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package de.dennisguse.opentracks.settings.bluetooth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class BluetoothLeCyclingCadenceAndSpeedPreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package de.dennisguse.opentracks.settings.bluetooth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference {
|
||||
|
||||
public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothLeHeartRatePreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+26
-16
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
package de.dennisguse.opentracks.settings.bluetooth;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.le.BluetoothLeScanner;
|
||||
@@ -21,8 +21,10 @@ import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.settings.BluetoothLeAdapter;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
@@ -30,25 +32,27 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
* Preference to select a discoverable Bluetooth LE device.
|
||||
* Based upon ListPreference.
|
||||
*/
|
||||
public class BluetoothLePreference extends DialogPreference {
|
||||
public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
|
||||
private static final String TAG = BluetoothLePreference.class.getSimpleName();
|
||||
private static final String TAG = BluetoothLeSensorPreference.class.getSimpleName();
|
||||
|
||||
private static final String ARG_BLUETOOTH_UUID = "bluetoothUUID";
|
||||
|
||||
private static final int DEVICE_NONE_RESOURCEID = R.string.value_none;
|
||||
|
||||
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public BluetoothLePreference(Context context, AttributeSet attrs) {
|
||||
public BluetoothLeSensorPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothLePreference(Context context) {
|
||||
public BluetoothLeSensorPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@@ -78,18 +82,20 @@ public class BluetoothLePreference extends DialogPreference {
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
if (getValue() == null || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext(), getValue())) {
|
||||
if (getValue() == null || PreferencesUtils.isBluetoothSensorAddressNone(getContext(), getValue())) {
|
||||
return getContext().getString(DEVICE_NONE_RESOURCEID);
|
||||
}
|
||||
|
||||
return getValue();
|
||||
}
|
||||
|
||||
public static class BluetoothLePreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
public static class BluetoothLeSensorPreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
|
||||
private int selectedEntryIndex;
|
||||
private final BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
|
||||
|
||||
private UUID sensorUUID;
|
||||
|
||||
private BluetoothLeScanner scanner = null;
|
||||
private final ScanCallback scanCallback = new ScanCallback() {
|
||||
@Override
|
||||
@@ -116,10 +122,11 @@ public class BluetoothLePreference extends DialogPreference {
|
||||
}
|
||||
};
|
||||
|
||||
static BluetoothLePreferenceDialog newInstance(String key) {
|
||||
final BluetoothLePreferenceDialog fragment = new BluetoothLePreferenceDialog();
|
||||
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, UUID sensorUUID) {
|
||||
final BluetoothLeSensorPreferenceDialog fragment = new BluetoothLeSensorPreferenceDialog();
|
||||
final Bundle b = new Bundle(1);
|
||||
b.putString(ARG_KEY, key);
|
||||
b.putString(ARG_KEY, preferenceKey);
|
||||
b.putSerializable(ARG_BLUETOOTH_UUID, sensorUUID);
|
||||
fragment.setArguments(b);
|
||||
return fragment;
|
||||
}
|
||||
@@ -128,6 +135,9 @@ public class BluetoothLePreference extends DialogPreference {
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
sensorUUID = (UUID) getArguments().getSerializable(ARG_BLUETOOTH_UUID);
|
||||
Log.i(TAG, "UUID: " + sensorUUID);
|
||||
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
|
||||
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
|
||||
Log.w(TAG, "Bluetooth adapter is present or not enabled.");
|
||||
@@ -152,14 +162,14 @@ public class BluetoothLePreference extends DialogPreference {
|
||||
listAdapter.add(getContext().getString(DEVICE_NONE_RESOURCEID), deviceNone);
|
||||
selectedEntryIndex = 0;
|
||||
|
||||
BluetoothLePreference preference = (BluetoothLePreference) getPreference();
|
||||
BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference();
|
||||
String deviceSelected = preference.getValue();
|
||||
if (deviceSelected != null && !deviceNone.equals(deviceSelected)) {
|
||||
listAdapter.add(preference.getValue(), preference.getValue());
|
||||
selectedEntryIndex = 1;
|
||||
}
|
||||
|
||||
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(BluetoothUtils.HEART_RATE_SERVICE_UUID));
|
||||
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(sensorUUID));
|
||||
List<ScanFilter> scanFilter = new ArrayList<>();
|
||||
scanFilter.add(scanFilterBuilder.build());
|
||||
|
||||
@@ -180,7 +190,7 @@ public class BluetoothLePreference extends DialogPreference {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
selectedEntryIndex = which;
|
||||
|
||||
BluetoothLePreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||
BluetoothLeSensorPreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
@@ -196,7 +206,7 @@ public class BluetoothLePreference extends DialogPreference {
|
||||
|
||||
if (positiveResult && selectedEntryIndex >= 0) {
|
||||
String value = listAdapter.get(selectedEntryIndex).getAddress();
|
||||
BluetoothLePreference preference = (BluetoothLePreference) getPreference();
|
||||
BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference();
|
||||
if (preference.callChangeListener(value)) {
|
||||
preference.setValue(value);
|
||||
}
|
||||
@@ -21,9 +21,13 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with bluetooth devices.
|
||||
*
|
||||
@@ -31,11 +35,21 @@ import java.util.UUID;
|
||||
*/
|
||||
public class BluetoothUtils {
|
||||
|
||||
public static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
|
||||
|
||||
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
|
||||
|
||||
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
|
||||
|
||||
private BluetoothUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* If called from UI: use a background thread to get the default Bluetooth adapter.
|
||||
* TODO Check if this is necessary.
|
||||
*/
|
||||
public static BluetoothAdapter getDefaultBluetoothAdapter(final String TAG) {
|
||||
// If from the main application thread, return directly
|
||||
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
|
||||
@@ -77,12 +91,56 @@ public class BluetoothUtils {
|
||||
return BluetoothUtils.getDefaultBluetoothAdapter(TAG) != null;
|
||||
}
|
||||
|
||||
public static int parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
||||
public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
||||
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
|
||||
byte[] raw = characteristic.getValue();
|
||||
int index = ((raw[0] & 0x1) == 1) ? 2 : 1;
|
||||
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
|
||||
return characteristic.getIntValue(format, index);
|
||||
if (raw.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean formatUINT16 = ((raw[0] & 0x1) == 1);
|
||||
if (formatUINT16 && raw.length >= 3) {
|
||||
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1);
|
||||
}
|
||||
if (!formatUINT16 && raw.length >= 2) {
|
||||
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Documentation: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
|
||||
*/
|
||||
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int flags = characteristic.getValue()[0];
|
||||
boolean hasCrank = (flags & 0x01) > 0;
|
||||
boolean hasWheel = (flags & 0x02) > 0;
|
||||
|
||||
SensorDataCycling.Cadence cadence = null;
|
||||
int index = 1;
|
||||
if (hasCrank && valueLength - index >= 6) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
|
||||
index += 4;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
index += 2;
|
||||
cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime);
|
||||
}
|
||||
|
||||
SensorDataCycling.Speed speed = null;
|
||||
if (hasWheel && valueLength - index >= 4) {
|
||||
int wheelCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
speed = new SensorDataCycling.Speed(address, sensorName, wheelCount, wheelTime);
|
||||
}
|
||||
|
||||
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,22 +215,42 @@ public class PreferencesUtils {
|
||||
setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
|
||||
}
|
||||
|
||||
private static String getBluetoothHeartRateSensorAddressNone(Context context) {
|
||||
private static String getBluetoothSensorAddressNone(Context context) {
|
||||
return context.getString(R.string.sensor_type_value_none);
|
||||
}
|
||||
|
||||
public static boolean isBluetoothHeartRateSensorAddressDefault(Context context) {
|
||||
return getBluetoothHeartRateSensorAddressNone(context).equals(getBluetoothHeartRateSensorAddress(context));
|
||||
public static boolean isBluetoothSensorAddressNone(Context context, String currentValue) {
|
||||
return getBluetoothSensorAddressNone(context).equals(currentValue);
|
||||
}
|
||||
|
||||
public static boolean isBluetoothHeartRateSensorAddressDefault(Context context, String currentValue) {
|
||||
return getBluetoothHeartRateSensorAddressNone(context).equals(currentValue);
|
||||
|
||||
public static boolean isBluetoothHeartRateSensorAddressNone(Context context) {
|
||||
return isBluetoothSensorAddressNone(context, getBluetoothHeartRateSensorAddress(context));
|
||||
}
|
||||
|
||||
public static String getBluetoothHeartRateSensorAddress(Context context) {
|
||||
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context));
|
||||
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone(context));
|
||||
}
|
||||
|
||||
|
||||
public static boolean isBluetoothCyclingCadenceSensorAddressNone(Context context) {
|
||||
return isBluetoothSensorAddressNone(context, getBluetoothCyclingCadenceSensorAddress(context));
|
||||
}
|
||||
|
||||
public static String getBluetoothCyclingCadenceSensorAddress(Context context) {
|
||||
return getString(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone(context));
|
||||
}
|
||||
|
||||
|
||||
public static boolean isBluetoothCyclingSpeedSensorAddressNone(Context context) {
|
||||
return isBluetoothSensorAddressNone(context, getBluetoothCyclingSpeedSensorAddress(context));
|
||||
}
|
||||
|
||||
public static String getBluetoothCyclingSpeedSensorAddress(Context context) {
|
||||
return getString(context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
|
||||
}
|
||||
|
||||
|
||||
public static boolean shouldShowStatsOnLockscreen(Context context) {
|
||||
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
|
||||
return getBoolean(context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
public class UintUtils {
|
||||
|
||||
public static int UINT16_MAX = 0xFFFF;
|
||||
public static long UINT32_MAX = 0xFFFFFFFFL;
|
||||
|
||||
private UintUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a - b for UINT with overflow (b < a).
|
||||
*
|
||||
* @return diff or -1 (invalid)
|
||||
*/
|
||||
public static long diff(long a, long b, final long UINT_MAX) {
|
||||
if (a < 0 || b < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (a > UINT_MAX && b > UINT_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a >= b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
return (UINT_MAX - b) + a;
|
||||
}
|
||||
}
|
||||
@@ -267,18 +267,24 @@ limitations under the License.
|
||||
android:id="@+id/stats_sensor_separator"
|
||||
style="@style/StatsHorizontalLine" />
|
||||
|
||||
<!-- Sensor data: heart rate-->
|
||||
<!-- Sensor data -->
|
||||
<LinearLayout
|
||||
android:id="@+id/stats_sensor_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout style="@style/StatsLargeItemContainer">
|
||||
<LinearLayout
|
||||
android:id="@+id/stats_sensor_heart_rate_container"
|
||||
style="@style/StatsLargeItemContainer">
|
||||
|
||||
<TextView
|
||||
style="@style/StatsLargeLabel"
|
||||
android:text="@string/sensor_state_heart_rate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_sensor_heart_rate_sensor_value"
|
||||
style="@style/StatsTinyValue"
|
||||
android:text="@string/value_unknown" />
|
||||
|
||||
<LinearLayout style="@style/StatsLargeValueContainer">
|
||||
|
||||
<TextView
|
||||
@@ -291,20 +297,36 @@ limitations under the License.
|
||||
android:text="@string/sensor_unit_beats_per_minute" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/StatsSmallGroupContainer">
|
||||
<LinearLayout
|
||||
android:id="@+id/stats_sensor_cadence_container"
|
||||
style="@style/StatsLargeItemContainer">
|
||||
|
||||
<TextView
|
||||
style="@style/StatsSmallLabel"
|
||||
android:text="@string/settings_sensor_bluetooth_sensor" />
|
||||
style="@style/StatsLargeLabel"
|
||||
android:text="@string/sensor_state_cadence" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_sensor_heart_rate_sensor_value"
|
||||
android:id="@+id/stats_sensor_cadence_sensor_value"
|
||||
style="@style/StatsTinyValue"
|
||||
android:value="@string/value_unknown" />
|
||||
android:text="@string/value_unknown" />
|
||||
|
||||
<LinearLayout style="@style/StatsLargeValueContainer">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_sensor_cadence_value"
|
||||
style="@style/StatsLargeValue"
|
||||
android:value="@string/value_unknown" />
|
||||
|
||||
<TextView
|
||||
style="@style/StatsUnit"
|
||||
android:text="@string/sensor_unit_rounds_per_minute" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -25,6 +25,7 @@ limitations under the License.
|
||||
<string name="marker_waypoint_icon_url" translatable="false">http://maps.google.com/mapfiles/ms/micons/blue-pushpin.png</string>
|
||||
|
||||
<string name="sensor_unit_beats_per_minute" translatable="false">BPM</string>
|
||||
<string name="sensor_unit_rounds_per_minute" translatable="false">RPM</string>
|
||||
|
||||
<string name="value_unknown" translatable="false">-</string>
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<string name="settings_reset_key" translatable="false">settingsReset</string>
|
||||
|
||||
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
|
||||
<string name="settings_sensor_bluetooth_cycling_cadence_key" translatable="false">bluetoothCyclingCadenceSensor</string>
|
||||
<string name="settings_sensor_bluetooth_cycling_speed_key" translatable="false">bluetoothCyclingSpeedSensor</string>
|
||||
<string name="sensor_type_value_none" translatable="false">NONE</string>
|
||||
|
||||
<string name="default_activity_key" translatable="false">defaultActivity</string>
|
||||
|
||||
@@ -111,10 +111,18 @@ limitations under the License.
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_sensor_bluetooth">
|
||||
<de.dennisguse.opentracks.settings.BluetoothLePreference
|
||||
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference
|
||||
android:defaultValue="@string/sensor_type_value_none"
|
||||
android:key="@string/settings_sensor_bluetooth_heart_rate_key"
|
||||
android:title="@string/sensor_state_heart_rate" />
|
||||
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference
|
||||
android:defaultValue="@string/sensor_type_value_none"
|
||||
android:key="@string/settings_sensor_bluetooth_cycling_cadence_key"
|
||||
android:title="@string/sensor_state_cadence" />
|
||||
<!-- <de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference-->
|
||||
<!-- android:defaultValue="@string/sensor_type_value_none"-->
|
||||
<!-- android:key="@string/settings_sensor_bluetooth_cycling_speed_key"-->
|
||||
<!-- android:title="@string/stats_speed" />-->
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/generic_tracks">
|
||||
|
||||
Reference in New Issue
Block a user