Sensor: add temperature.

Fixes #2241.
This commit is contained in:
Dennis Guse
2026-01-04 09:56:44 +01:00
parent 738201c1c2
commit 54844b9a9a
36 changed files with 435 additions and 44 deletions
@@ -49,6 +49,7 @@ import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Statistics;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
@@ -538,6 +539,7 @@ public class ContentProviderUtils {
int speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
int bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
int sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
int temperatureIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_TEMPERATURE);
int sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
int sensorDistanceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_DISTANCE);
int sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
@@ -564,6 +566,7 @@ public class ContentProviderUtils {
),
!cursor.isNull(sensorDistanceIndex) ? Distance.of(cursor.getFloat(sensorDistanceIndex)) : null,
!cursor.isNull(sensorHeartRateIndex) ? HeartRate.of(cursor.getFloat(sensorHeartRateIndex)) : null,
!cursor.isNull(temperatureIndex) ? Temperature.of(cursor.getFloat(temperatureIndex)) : null,
!cursor.isNull(sensorCadenceIndex) ? Cadence.of(cursor.getFloat(sensorCadenceIndex)) : null,
!cursor.isNull(sensorPowerIndex) ? Power.of(cursor.getFloat(sensorPowerIndex)) : null,
altitudeGainLoss
@@ -652,6 +655,9 @@ public class ContentProviderUtils {
if (trackPoint.heartRate() != null) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.heartRate().getBPM());
}
if (trackPoint.temperature() != null) {
values.put(TrackPointsColumns.SENSOR_TEMPERATURE, trackPoint.temperature().getCelsius());
}
if (trackPoint.cadence() != null) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.cadence().getRPM());
}
@@ -29,7 +29,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 40;
private static final int DATABASE_VERSION = 41;
private final Context context;
@@ -84,6 +84,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 38 -> upgradeFrom37to38(db);
case 39 -> upgradeFrom38to39(db);
case 40 -> upgradeFrom39to40(db);
case 41 -> upgradeFrom40to41(db);
default -> throw new RuntimeException("Not implemented: upgrade to " + toVersion);
}
}
@@ -111,6 +112,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 37 -> downgradeFrom38to37(db);
case 38 -> downgradeFrom39to38(db);
case 39 -> downgradeFrom40to39(db);
case 40 -> downgradeFrom41to40(db);
default -> throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
}
@@ -734,4 +736,32 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
private void upgradeFrom40to41(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_temperature FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1, 3)), sensor_distance FLOAT, accuracy_vertical FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, null, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance, accuracy_vertical FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom41to40(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1, 3)), sensor_distance FLOAT, accuracy_vertical FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance, accuracy_vertical FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -0,0 +1,18 @@
package de.dennisguse.opentracks.data.models;
public record Temperature(
float value_celsius
) {
public static Temperature of(float value_celsius) {
return new Temperature(value_celsius);
}
public static Temperature ofCentiCelsius(int value_centi_celsius) {
return Temperature.of((float) (value_centi_celsius / 100.0));
}
public float getCelsius() {
return value_celsius;
}
}
@@ -43,6 +43,7 @@ public record TrackPoint(
Distance sensorDistance,
HeartRate heartRate,
Temperature temperature,
Cadence cadence,
Power power,
AltitudeGainLoss altitudeGainLoss
@@ -57,6 +58,7 @@ public record TrackPoint(
null,
null,
null,
null,
null
);
}
@@ -71,6 +73,7 @@ public record TrackPoint(
null,
null,
null,
null,
null
);
}
@@ -95,6 +98,7 @@ public record TrackPoint(
),
trackPoint.sensorDistance,
trackPoint.heartRate,
null,
trackPoint.cadence,
trackPoint.power,
trackPoint.altitudeGainLoss
@@ -108,6 +112,7 @@ public record TrackPoint(
position,
sensorDistance,
heartRate,
null,
cadence,
power,
altitudeGainLoss
@@ -121,6 +126,7 @@ public record TrackPoint(
newPosition,
sensorDistance,
heartRate,
null,
cadence,
power,
altitudeGainLoss
@@ -150,6 +156,7 @@ public record TrackPoint(
position,
newSensorDistance,
heartRate,
null,
cadence,
power,
newAltitudeGainLoss
@@ -50,6 +50,7 @@ public interface TrackPointsColumns extends BaseColumns {
String SPEED = "speed";
String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate";
String SENSOR_TEMPERATURE = "sensor_temperature";
String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_DISTANCE = "sensor_distance"; //DISTANCE from previous TrackPoint
String SENSOR_POWER = "sensor_power";
@@ -75,6 +76,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ SPEED + " FLOAT, "
+ BEARING + " FLOAT, "
+ SENSOR_HEARTRATE + " FLOAT, "
+ SENSOR_TEMPERATURE + " FLOAT, "
+ SENSOR_CADENCE + " FLOAT, "
+ SENSOR_POWER + " FLOAT, "
+ ALTITUDE_GAIN + " FLOAT, "
@@ -61,6 +61,7 @@ public class GPXTrackExporter implements TrackExporter {
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat TEMPERATURE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -82,6 +83,9 @@ public class GPXTrackExporter implements TrackExporter {
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
HEARTRATE_FORMAT.setGroupingUsed(false);
TEMPERATURE_FORMAT.setMaximumFractionDigits(2);
TEMPERATURE_FORMAT.setGroupingUsed(false);
CADENCE_FORMAT.setMaximumFractionDigits(0);
CADENCE_FORMAT.setGroupingUsed(false);
@@ -321,6 +325,10 @@ public class GPXTrackExporter implements TrackExporter {
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.heartRate().getBPM()) + "</gpxtpx:hr>\n";
}
if (trackPoint.temperature() != null) {
trackPointExtensionV2Content += "<gpxtpx:atemp>" + TEMPERATURE_FORMAT.format(trackPoint.temperature().getCelsius()) + "</gpxtpx:atemp>\n";
}
if (trackPoint.cadence() != null) {
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.cadence().getRPM()) + "</gpxtpx:cad>\n";
}
@@ -67,8 +67,9 @@ public class KMLTrackExporter implements TrackExporter {
public static final String EXTENDED_DATA_TYPE_TRACKPOINT = "trackpoint_type";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_HEARTRATE = "heartrate";
public static final String EXTENDED_DATA_TYPE_TEMPERATURE = "temperature";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_POWER = "power";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_GAIN = "elevation_gain";
public static final String EXTENDED_DATA_TYPE_ALTITUDE_LOSS = "elevation_loss";
@@ -95,6 +96,7 @@ public class KMLTrackExporter implements TrackExporter {
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
private final List<Float> temperatureList = new ArrayList<>();
private final List<Float> altitudeGainList = new ArrayList<>(); //TODO Join with altitudeLossList
private final List<Float> altitudeLossList = new ArrayList<>();
private final List<Float> accuracyHorizontal = new ArrayList<>();
@@ -237,7 +239,7 @@ public class KMLTrackExporter implements TrackExporter {
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd
http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
"""); //TODO ADD xsi:schemaLocation for atom
"""); //NOTE: There is no XSD for Atom
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
@@ -254,6 +256,7 @@ public class KMLTrackExporter implements TrackExporter {
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_POWER, context.getString(R.string.description_sensor_power));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_HEARTRATE, context.getString(R.string.description_sensor_heart_rate));
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_TEMPERATURE, context.getString(R.string.description_sensor_temperature));
printWriter.println("</Schema>");
}
@@ -321,6 +324,7 @@ public class KMLTrackExporter implements TrackExporter {
powerList.clear();
cadenceList.clear();
heartRateList.clear();
temperatureList.clear();
altitudeGainList.clear();
altitudeLossList.clear();
accuracyHorizontal.clear();
@@ -349,6 +353,9 @@ public class KMLTrackExporter implements TrackExporter {
if (heartRateList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(heartRateList, EXTENDED_DATA_TYPE_HEARTRATE);
}
if (temperatureList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(temperatureList, EXTENDED_DATA_TYPE_TEMPERATURE);
}
if (altitudeGainList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArraySensorData(altitudeGainList, EXTENDED_DATA_TYPE_ALTITUDE_GAIN);
}
@@ -381,6 +388,7 @@ public class KMLTrackExporter implements TrackExporter {
distanceList.add(trackPoint.sensorDistance() != null ? (float) trackPoint.sensorDistance().toM() : null);
heartRateList.add(trackPoint.heartRate() != null ? trackPoint.heartRate().getBPM() : null);
temperatureList.add(trackPoint.temperature() != null ? trackPoint.temperature().getCelsius() : null);
cadenceList.add(trackPoint.cadence() != null ? trackPoint.cadence().getRPM() : null);
powerList.add(trackPoint.power() != null ? trackPoint.power().getW() : null);
if (trackPoint.altitudeGainLoss() != null) {
@@ -40,6 +40,7 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
@@ -83,6 +84,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
*/
private static final String TAG_EXTENSION_SPEED_COMPAT = "speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_TEMPERATURE = "gpxtpx:atemp";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
@@ -113,6 +115,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
private String time;
private String speed;
private String heartrate;
private String temperature;
private String cadence;
private String power;
private String markerType;
@@ -218,6 +221,11 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
heartrate = content.trim();
}
}
case TAG_EXTENSION_TEMPERATURE -> {
if (content != null) {
temperature = content.trim();
}
}
case TAG_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
@@ -354,6 +362,15 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
}
Temperature parsedTemperature = null;
if (temperature != null) {
try {
parsedTemperature = Temperature.of(Float.parseFloat(temperature));
} catch (NumberFormatException e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse temperature: %s", temperature)), e);
}
}
Cadence parsedCadence = null;
if (cadence != null) {
try {
@@ -396,6 +413,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
),
parsedSensorDistance,
parsedHeartRate,
parsedTemperature,
parsedCadence,
parsedPower,
altitudeGainLoss
@@ -415,9 +433,10 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorDistance = null;
accuracyHorizontal = null;
accuracyVertical = null;
power = null;
heartrate = null;
temperature = null;
cadence = null;
power = null;
}
private void onMarkerStart(Attributes attributes) {
@@ -42,6 +42,7 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
@@ -109,6 +110,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
private final ArrayList<Float> sensorCadenceList = new ArrayList<>();
private final ArrayList<Float> sensorHeartRateList = new ArrayList<>();
private final ArrayList<Float> sensorTemperatureList = new ArrayList<>();
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
@@ -312,6 +314,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
sensorSpeedList.clear();
sensorDistanceList.clear();
sensorHeartRateList.clear();
sensorTemperatureList.clear();
sensorCadenceList.clear();
sensorPowerList.clear();
altitudeGainList.clear();
@@ -334,6 +337,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
Speed speed = null;
Distance sensorDistance = null;
HeartRate heartRate = null;
Temperature temperature = null;
Cadence cadence = null;
Power power = null;
AltitudeGainLoss altitudeGainLoss = null;
@@ -352,6 +356,9 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
if (i < sensorHeartRateList.size()) {
heartRate = HeartRate.ofOrNull(sensorHeartRateList.get(i));
}
if (i < sensorTemperatureList.size() && sensorTemperatureList.get(i) != null) {
temperature = Temperature.of(sensorTemperatureList.get(i));
}
if (i < sensorCadenceList.size() && sensorCadenceList.get(i) != null) {
cadence = Cadence.of(sensorCadenceList.get(i));
}
@@ -399,6 +406,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
position,
sensorDistance,
heartRate,
temperature,
cadence,
power,
altitudeGainLoss
@@ -467,6 +475,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER -> sensorPowerList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_HEARTRATE,
EXTENDED_DATA_TYPE_HEART_RATE_LEGACY -> sensorHeartRateList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_TEMPERATURE -> sensorTemperatureList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE -> sensorCadenceList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN -> altitudeGainList.add(value);
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS -> altitudeLossList.add(value);
@@ -12,10 +12,10 @@ import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface {
private static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
static final UUID ENVIRONMENTAL_SENSING_SERVICE = new UUID(0x181A00001000L, 0x800000805f9b34fbL);
public static final ServiceMeasurementUUID BAROMETRIC_PRESSURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL)
new UUID(0x2A6D00001000L, 0x800000805f9b34fbL) //TODO extract leastSigBits
);
@Override
@@ -37,9 +37,7 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac
}
/**
* Decoding:
* org.bluetooth.service.environmental_sensing.xml
* org.bluetooth.characteristic.pressure.xml
* Decoding: org.bluetooth.characteristic.pressure.xml
*/
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
@@ -0,0 +1,53 @@
package de.dennisguse.opentracks.sensors;
import static de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure.ENVIRONMENTAL_SENSING_SERVICE;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorTemperature;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
public class BluetoothHandlerTemperature implements SensorHandlerInterface {
public static final ServiceMeasurementUUID TEMPERATURE = new ServiceMeasurementUUID(
ENVIRONMENTAL_SENSING_SERVICE,
new UUID(0x2A6E00001000L, 0x800000805f9b34fbL)
);
@Override
public List<ServiceMeasurementUUID> getServices() {
return List.of(TEMPERATURE);
}
@Override
public Aggregator<?, ?> createEmptySensorData(String address, String name) {
return new AggregatorTemperature(address, name);
}
@Override
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Temperature value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
observer.onChange(new Raw<>(observer.getNow(), value));
}
/**
* Decoding: org.bluetooth.characteristic.temperature.xml
*/
public static Temperature parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
if (raw.length < 2) {
return null;
}
Integer pressure = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 0);
return Temperature.ofCentiCelsius(pressure);
}
}
@@ -24,8 +24,6 @@ import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -40,13 +38,12 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
private final Context context;
private final Handler handler;
private boolean started = false;
private final BluetoothConnectionManager heartRate;
private final BluetoothConnectionManager temperature;
private final BluetoothConnectionManager cyclingCadence;
private final BluetoothConnectionManager cyclingSpeed;
private final BluetoothConnectionManager cyclingPower;
@@ -58,6 +55,7 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerHeartRate());
this.temperature = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerTemperature());
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingPower());
@@ -75,6 +73,7 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
@Override
public synchronized void stop(Context context) {
heartRate.disconnect();
temperature.disconnect();
cyclingCadence.disconnect();
cyclingSpeed.disconnect();
cyclingPower.disconnect();
@@ -96,6 +95,11 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
connect(heartRate, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_temperature_key, key)) {
String address = PreferencesUtils.getBluetoothTemperatureSensorAddress();
connect(temperature, address);
}
if (PreferencesUtils.isKey(R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress();
connect(cyclingCadence, address);
@@ -2,12 +2,13 @@ package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
public abstract class Aggregator<Input, Output> {
private static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
protected Raw<Input> previous;
protected Output aggregatedValue;
@@ -43,6 +44,7 @@ public abstract class Aggregator<Input, Output> {
return aggregatedValue != null;
}
@Deprecated //TODO Remove: for sensor there is no default value. Make getAggregatedValue() @Nullable
@NonNull
protected abstract Output getNoneValue();
@@ -84,10 +86,14 @@ public abstract class Aggregator<Input, Output> {
return now
.isAfter(
previous.time()
.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE)
.plus(getMaxSensorAge())
);
}
protected Duration getMaxSensorAge() {
return MAX_SENSOR_DATE_SET_AGE;
}
@NonNull
@Override
public String toString() {
@@ -0,0 +1,34 @@
package de.dennisguse.opentracks.sensors.sensorData;
import androidx.annotation.NonNull;
import java.time.Duration;
import de.dennisguse.opentracks.data.models.Temperature;
public class AggregatorTemperature extends Aggregator<Temperature, Temperature> {
public AggregatorTemperature(String sensorAddress, String sensorName) {
super(sensorAddress, sensorName);
}
protected void computeValue(Raw<Temperature> current) {
this.aggregatedValue = current.value();
}
@NonNull
protected Temperature getNoneValue() {
return Temperature.ofCentiCelsius(0);
}
protected void resetImmediate() {
aggregatedValue = getNoneValue();
}
public void resetAggregated() {};
@Override
protected Duration getMaxSensorAge() {
return Duration.ofMinutes(1);
}
}
@@ -14,6 +14,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingPower;
@@ -27,6 +28,9 @@ public class SensorDataAggregator {
@VisibleForTesting
public AggregatorHeartRate heartRate;
@VisibleForTesting
public AggregatorTemperature temperature;
@VisibleForTesting
public AggregatorCyclingCadence cyclingCadence;
@@ -97,6 +101,7 @@ public class SensorDataAggregator {
set(data, data);
}
//TODO Refactor generic "fun" here: go explicit.
public void update(@NonNull Raw<?> data) {
Object value = data.value();
@@ -105,6 +110,11 @@ public class SensorDataAggregator {
return;
}
if (value instanceof Temperature) {
this.temperature.add((Raw<Temperature>) data);
return;
}
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
return;
@@ -142,6 +152,7 @@ public class SensorDataAggregator {
public void clear() {
Log.i(TAG, "Removing all aggregators");
this.heartRate = null;
this.temperature = null;
this.cyclingCadence = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
@@ -156,6 +167,7 @@ public class SensorDataAggregator {
getSpeed(now),
getDistance(now),
getHeartRate(now),
temperature != null && temperature.hasReceivedData() ? temperature.getAggregatedValueWithSensorName(now) : null,
getCadence(now),
cyclingPower != null && cyclingPower.hasReceivedData() ? cyclingPower.getAggregatedValueWithSensorName(now) : null,
barometer != null && barometer.hasReceivedData() ? barometer.getAggregatedValueWithSensorName(now) : null
@@ -166,6 +178,7 @@ public class SensorDataAggregator {
Log.i(TAG, "Resetting data");
if (heartRate != null) heartRate.resetAggregated();
if (temperature != null) temperature.resetAggregated();
if (cyclingCadence != null) cyclingCadence.resetAggregated();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.resetAggregated();
if (cyclingPower != null) cyclingPower.resetAggregated();
@@ -182,6 +195,10 @@ public class SensorDataAggregator {
heartRate = (AggregatorHeartRate) sensorData;
return;
}
if (type instanceof AggregatorTemperature) {
temperature = (AggregatorTemperature) sensorData;
return;
}
if (type instanceof AggregatorCyclingCadence) {
cyclingCadence = (AggregatorCyclingCadence) sensorData;
return;
@@ -10,6 +10,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Temperature;
public record SensorDataSet(
@NonNull
@@ -21,6 +22,7 @@ public record SensorDataSet(
@Nullable
SensorData<HeartRate> heartRate,
@Nullable
SensorData<Temperature> temperature,
SensorData<Cadence> cadence,
@Nullable
SensorData<Power> power,
@@ -113,6 +113,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
position,
sensorDataSet.distance() != null ? sensorDataSet.distance().data() : null,
sensorDataSet.heartRate() != null ? sensorDataSet.heartRate().data() : null,
sensorDataSet.temperature() != null ? sensorDataSet.temperature().data() : null,
sensorDataSet.cadence() != null ? sensorDataSet.cadence().data() : null,
sensorDataSet.power() != null ? sensorDataSet.power().data() : null,
sensorDataSet.altitudeGainLoss() != null ? sensorDataSet.altitudeGainLoss().data() : null
@@ -138,6 +139,7 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
position,
sensorDataSet.distance() != null ? sensorDataSet.distance().data() : null,
sensorDataSet.heartRate() != null ? sensorDataSet.heartRate().data() : null,
sensorDataSet.temperature() != null ? sensorDataSet.temperature().data() : null,
sensorDataSet.cadence() != null ? sensorDataSet.cadence().data() : null,
sensorDataSet.power() != null ? sensorDataSet.power().data() : null,
sensorDataSet.altitudeGainLoss() != null ? sensorDataSet.altitudeGainLoss().data() : null
@@ -274,6 +274,10 @@ public class PreferencesUtils {
return getString(R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothTemperatureSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_temperature_key, getBluetoothSensorAddressNone());
}
public static String getBluetoothCyclingCadenceSensorAddress() {
return getString(R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone());
}
@@ -830,7 +834,8 @@ public class PreferencesUtils {
R.array.stats_custom_layout_fields_default_value_17,
R.array.stats_custom_layout_fields_default_value_18,
R.array.stats_custom_layout_fields_default_value_19,
R.array.stats_custom_layout_fields_default_value_20
R.array.stats_custom_layout_fields_default_value_20,
R.array.stats_custom_layout_fields_default_value_21
).map(id -> resources.obtainTypedArray(id))
.toList();
}
@@ -0,0 +1,35 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerTemperature;
public class BluetoothLeTemperaturePreference extends BluetoothLeSensorPreference {
public BluetoothLeTemperaturePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLeTemperaturePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLeTemperaturePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLeTemperaturePreference(Context context) {
super(context);
}
@Override
public PreferenceDialogFragmentCompat createInstance() {
return BluetoothLeSensorPreferenceDialog
.newInstance(getKey(), List.of(BluetoothHandlerTemperature.TEMPERATURE));
}
}
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.util;
import static java.time.temporal.ChronoUnit.DAYS;
import android.content.Context;
import android.location.Location;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
@@ -47,6 +46,7 @@ import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Position;
import de.dennisguse.opentracks.data.models.Power;
import de.dennisguse.opentracks.data.models.Temperature;
import de.dennisguse.opentracks.settings.UnitSystem;
/**
@@ -156,6 +156,15 @@ public class StringUtils {
return new Pair<>(value, context.getString(R.string.sensor_unit_beats_per_minute));
}
public static Pair<String, String> getTemperatureParts(Context context, Temperature temperature) {
String value = context.getString(R.string.value_unknown);
if (temperature != null) {
value = StringUtils.formatDecimal(temperature.getCelsius(), 1);
}
return new Pair<>(value, context.getString(R.string.sensor_unit_temperature));
}
public static Pair<String, String> getCadenceParts(Context context, Cadence cadence) {
String value = context.getString(R.string.value_unknown);
if (cadence != null) {
@@ -34,6 +34,7 @@ public class Mapping {
m.put(context.getString(R.string.stats_custom_layout_coordinates_key), GenericStatisticsViewHolder.Coordinates::new);
m.put(context.getString(R.string.stats_custom_layout_heart_rate_key), SensorStatisticsViewHolder.SensorHeartRate::new);
m.put(context.getString(R.string.stats_custom_layout_temperature_key), SensorStatisticsViewHolder.SensorTemperature::new);
m.put(context.getString(R.string.stats_custom_layout_cadence_key), SensorStatisticsViewHolder.SensorCadence::new);
m.put(context.getString(R.string.stats_custom_layout_power_key), SensorStatisticsViewHolder.SensorPower::new);
m.put(context.getString(R.string.stats_custom_layout_clock_key), ClockViewHolder::new);
@@ -60,6 +60,29 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
}
}
public static class SensorTemperature extends SensorStatisticsViewHolder {
@Override
public void onChanged(UnitSystem unitSystem, RecordingData data) {
SensorDataSet sensorDataSet = data.sensorDataSet();
String sensorName = getContext().getString(R.string.value_unknown);
Pair<String, String> valueAndUnit;
if (sensorDataSet != null && sensorDataSet.temperature() != null) {
valueAndUnit = StringUtils.getTemperatureParts(getContext(), sensorDataSet.temperature().data());
sensorName = sensorDataSet.temperature().sensorNameOrAddress();
} else {
valueAndUnit = StringUtils.getTemperatureParts(getContext(), null);
}
getBinding().statsValue.setText(valueAndUnit.first);
getBinding().statsUnit.setText(valueAndUnit.second);
getBinding().statsDescriptionMain.setText(R.string.stats_sensors_temperature);
getBinding().statsDescriptionSecondary.setText(sensorName);
}
}
public static class SensorCadence extends SensorStatisticsViewHolder {
@Override