forked from upstream-mirrors/OpenTracks
@@ -12,7 +12,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.util.PressureSensorUtils;
|
||||
|
||||
/**
|
||||
* Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer).
|
||||
|
||||
-1
@@ -35,7 +35,6 @@ import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
|
||||
/**
|
||||
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
|
||||
|
||||
-1
@@ -34,7 +34,6 @@ import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
|
||||
/**
|
||||
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package de.dennisguse.opentracks.services.sensors;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Distance;
|
||||
import de.dennisguse.opentracks.content.data.Speed;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataRunning;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with bluetooth devices.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
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 List<UUID> HEART_RATE_SUPPORTING_DEVICES = Collections.unmodifiableList(Arrays.asList(
|
||||
BluetoothUtils.HEART_RATE_SERVICE_UUID,
|
||||
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
|
||||
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb") //Miband3
|
||||
));
|
||||
|
||||
public static final UUID CYCLING_POWER_UUID = new UUID(0x181800001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID CYCLING_POWER_MEASUREMENT_CHAR_UUID = new UUID(0x2A6300001000L, 0x800000805f9b34fbL);
|
||||
|
||||
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
|
||||
|
||||
public static final UUID RUNNING_RUNNING_SPEED_CADENCE_UUID = new UUID(0x181400001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID RUNNING_RUNNING_SPEED_CADENCE_CHAR_UUID = new UUID(0x2A5300001000L, 0x800000805f9b34fbL);
|
||||
|
||||
private static final String TAG = BluetoothUtils.class.getSimpleName();
|
||||
|
||||
private BluetoothUtils() {
|
||||
}
|
||||
|
||||
public static BluetoothAdapter getAdapter(Context context) {
|
||||
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (bluetoothManager == null) {
|
||||
Log.i(TAG, "BluetoothManager not available.");
|
||||
return null;
|
||||
} else {
|
||||
return bluetoothManager.getAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasBluetooth(Context context) {
|
||||
return BluetoothUtils.getAdapter(context) != null;
|
||||
}
|
||||
|
||||
public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
||||
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
|
||||
byte[] raw = characteristic.getValue();
|
||||
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;
|
||||
}
|
||||
|
||||
public static Integer parseCyclingPower(BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 2);
|
||||
}
|
||||
|
||||
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int flags = characteristic.getValue()[0];
|
||||
boolean hasWheel = (flags & 0x01) > 0;
|
||||
boolean hasCrank = (flags & 0x02) > 0;
|
||||
|
||||
int index = 1;
|
||||
SensorDataCycling.DistanceSpeed speed = null;
|
||||
if (hasWheel && valueLength - index >= 6) {
|
||||
int wheelTotalRevolutionCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
|
||||
index += 4;
|
||||
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
speed = new SensorDataCycling.DistanceSpeed(address, sensorName, wheelTotalRevolutionCount, wheelTime);
|
||||
index += 2;
|
||||
}
|
||||
|
||||
SensorDataCycling.Cadence cadence = null;
|
||||
if (hasCrank && valueLength - index >= 4) {
|
||||
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
|
||||
index += 2;
|
||||
|
||||
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
|
||||
cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime);
|
||||
}
|
||||
|
||||
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
|
||||
}
|
||||
|
||||
public static SensorDataRunning parseRunningSpeedAndCadence(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.rsc_measurement.xml
|
||||
int valueLength = characteristic.getValue().length;
|
||||
if (valueLength == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int flags = characteristic.getValue()[0];
|
||||
boolean hasStrideLength = (flags & 0x01) > 0;
|
||||
boolean hasTotalDistance = (flags & 0x02) > 0;
|
||||
boolean hasStatus = (flags & 0x03) > 0; // walking vs running
|
||||
|
||||
Speed speed = null;
|
||||
Float cadence = null;
|
||||
Distance totalDistance = null;
|
||||
|
||||
int index = 1;
|
||||
if (valueLength - index >= 2) {
|
||||
speed = Speed.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index) / 256f);
|
||||
}
|
||||
|
||||
index = 3;
|
||||
if (valueLength - index >= 1) {
|
||||
cadence = Float.valueOf(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index));
|
||||
}
|
||||
|
||||
index = 4;
|
||||
if (hasStrideLength && valueLength - index >= 2) {
|
||||
Distance strideDistance = Distance.ofCM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index));
|
||||
index += 2;
|
||||
}
|
||||
|
||||
if (hasTotalDistance && valueLength - index >= 4) {
|
||||
totalDistance = Distance.ofDM(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index));
|
||||
}
|
||||
|
||||
return new SensorDataRunning(address, sensorName, speed, cadence, totalDistance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.dennisguse.opentracks.services.sensors;
|
||||
|
||||
import android.hardware.SensorManager;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
public class PressureSensorUtils {
|
||||
|
||||
//Everything above is considered a meaningful change in altitude.
|
||||
private static final float ALTITUDE_CHANGE_DIFF_M = 3.0f;
|
||||
|
||||
private static final float EXPONENTIAL_SMOOTHING = 0.3f;
|
||||
|
||||
private static final float p0 = SensorManager.PRESSURE_STANDARD_ATMOSPHERE;
|
||||
|
||||
private PressureSensorUtils() {
|
||||
}
|
||||
|
||||
public static class AltitudeChange {
|
||||
|
||||
private final float currentSensorValue_hPa;
|
||||
|
||||
private final float altitudeChange_m;
|
||||
|
||||
public AltitudeChange(float currentSensorValue_hPa, float altitudeChange_m) {
|
||||
this.currentSensorValue_hPa = currentSensorValue_hPa;
|
||||
this.altitudeChange_m = altitudeChange_m;
|
||||
}
|
||||
|
||||
public float getCurrentSensorValue_hPa() {
|
||||
return currentSensorValue_hPa;
|
||||
}
|
||||
|
||||
public float getAltitudeChange_m() {
|
||||
return altitudeChange_m;
|
||||
}
|
||||
|
||||
public float getAltitudeGain_m() {
|
||||
return altitudeChange_m > 0 ? altitudeChange_m : 0;
|
||||
}
|
||||
|
||||
public float getAltitudeLoss_m() {
|
||||
return altitudeChange_m < 0 ? -1 * altitudeChange_m : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies exponential smoothing to sensor value before computation.
|
||||
*/
|
||||
public static AltitudeChange computeChangesWithSmoothing_m(float lastAcceptedSensorValue_hPa, float lastSeenSensorValue_hPa, float currentSensorValue_hPa) {
|
||||
float nextSensorValue_hPa = EXPONENTIAL_SMOOTHING * currentSensorValue_hPa + (1 - EXPONENTIAL_SMOOTHING) * lastSeenSensorValue_hPa;
|
||||
|
||||
return computeChanges(lastAcceptedSensorValue_hPa, nextSensorValue_hPa);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static AltitudeChange computeChanges(float lastAcceptedSensorValue_hPa, float currentSensorValue_hPa) {
|
||||
float lastSensorValue_m = SensorManager.getAltitude(p0, lastAcceptedSensorValue_hPa);
|
||||
float currentSensorValue_m = SensorManager.getAltitude(p0, currentSensorValue_hPa);
|
||||
|
||||
float altitudeChange_m = currentSensorValue_m - lastSensorValue_m;
|
||||
if (Math.abs(altitudeChange_m) < ALTITUDE_CHANGE_DIFF_M) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Limit altitudeC change by ALTITUDE_CHANGE_DIFF and computes pressure value accordingly.
|
||||
AltitudeChange altitudeChange = new AltitudeChange(currentSensorValue_hPa, altitudeChange_m);
|
||||
if (altitudeChange.getAltitudeChange_m() > 0) {
|
||||
return new AltitudeChange(getBarometricPressure(lastSensorValue_m + ALTITUDE_CHANGE_DIFF_M), ALTITUDE_CHANGE_DIFF_M);
|
||||
} else {
|
||||
return new AltitudeChange(getBarometricPressure(lastSensorValue_m - ALTITUDE_CHANGE_DIFF_M), -1 * ALTITUDE_CHANGE_DIFF_M);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Barometeric pressure to altitude estimation; inverts of SensorManager.getAltitude(float, float)
|
||||
* https://de.wikipedia.org/wiki/Barometrische_H%C3%B6henformel#Internationale_H%C3%B6henformel
|
||||
* {\color{White} p(h)} = p_0 \cdot \left( 1 - \frac{0{,}0065 \frac{\mathrm K}{\mathrm m} \cdot h}{T_0\ } \right)^{5{,}255}
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static float getBarometricPressure(float altitude_m) {
|
||||
return (float) (p0 * Math.pow(1.0 - 0.0065 * altitude_m / 288.15, 5.255f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.dennisguse.opentracks.services.sensors;
|
||||
|
||||
public class UintUtils {
|
||||
|
||||
public static final int UINT16_MAX = 0xFFFF;
|
||||
public static final long UINT32_MAX = 0xFFFFFFFFL;
|
||||
|
||||
private UintUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a - b for UINT with overflow (b < a).
|
||||
*
|
||||
* @return diff
|
||||
*/
|
||||
public static long diff(long a, long b, final long UINT_MAX) {
|
||||
if (a < 0 || b < 0) {
|
||||
throw new RuntimeException("a or b cannot be less than zero.");
|
||||
}
|
||||
if (a > UINT_MAX || b > UINT_MAX) {
|
||||
throw new RuntimeException("a or b are outside of the allowed range.");
|
||||
}
|
||||
|
||||
if (a >= b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
return (UINT_MAX - b) + a;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user