mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-23 10:26:49 +01:00
Implement high res HR data
Specifically for: - The HR fragment - The sports activity graph Also adds support for Huawei high res HR, and high res SpO2.
This commit is contained in:
parent
1882ee947e
commit
82e3a86350
@ -152,6 +152,11 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
return getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<? extends ActivitySample> getSamplesHighRes(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return getAllSamplesHighRes(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart<?> chart) {
|
||||
List<LegendEntry> legendEntries = new ArrayList<>(5);
|
||||
@ -231,9 +236,12 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
|
||||
|
||||
private DefaultChartsData<LineData> buildChartFromSamples(DBHandler handler) {
|
||||
final List<? extends ActivitySample> samples = getAllSamples(handler, gbDevice, startTime, endTime);
|
||||
final List<? extends ActivitySample> highResSamples = getAllSamplesHighRes(handler, gbDevice, startTime, endTime);
|
||||
|
||||
try {
|
||||
if (highResSamples == null)
|
||||
return refresh(gbDevice, samples);
|
||||
return refresh(gbDevice, samples, highResSamples);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unable to get charts data right now", e);
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import com.github.mikephil.charting.data.LineDataSet;
|
||||
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -180,14 +181,28 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getAllSamplesHighRes(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
|
||||
// Only retrieve if the provider signals it has high res data, otherwise it is useless
|
||||
if (provider.hasHighResData())
|
||||
return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends AbstractActivitySample> provider = getProvider(db, device);
|
||||
return provider.getActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
public DefaultChartsData<LineData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples) {
|
||||
// If there is no high res samples, all the samples are high res samples
|
||||
return refresh(gbDevice, samples, samples);
|
||||
}
|
||||
|
||||
public DefaultChartsData<LineData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples, List<? extends ActivitySample> highResSamples) {
|
||||
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
LOG.info("{}: number of samples: {}", getTitle(), samples.size());
|
||||
LOG.info("{}: number of high res samples: {}", getTitle(), highResSamples.size());
|
||||
LineData lineData;
|
||||
|
||||
if (samples.isEmpty()) {
|
||||
@ -257,8 +272,15 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
}
|
||||
entries.get(index).add(createLineEntry(value, ts));
|
||||
|
||||
// heart rate line graph
|
||||
if (hr && type != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
last_type = type;
|
||||
last_value = value;
|
||||
}
|
||||
|
||||
// Currently only for HR
|
||||
if (hr) {
|
||||
for (ActivitySample sample : highResSamples) {
|
||||
if (sample.getKind() != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
int ts = tsTranslation.shorten(sample.getTimestamp());
|
||||
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
|
||||
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
|
||||
heartrateEntries.add(createLineEntry(0, ts - 1));
|
||||
@ -266,8 +288,7 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
|
||||
lastHrSampleIndex = ts;
|
||||
}
|
||||
last_type = type;
|
||||
last_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// convert Entry Lists to Datasets
|
||||
@ -364,15 +385,16 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
|
||||
/**
|
||||
* Implement this to supply the samples to be displayed.
|
||||
*
|
||||
* @param db
|
||||
* @param device
|
||||
* @param tsFrom
|
||||
* @param tsTo
|
||||
* @return
|
||||
*/
|
||||
protected abstract List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
|
||||
|
||||
/**
|
||||
* Implement this to supply high resolution data
|
||||
*/
|
||||
protected List<? extends ActivitySample> getSamplesHighRes(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
throw new NotImplementedException("High resolution samples have not been implemented for this chart.");
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
|
||||
int tsStart = getTSStart();
|
||||
int tsEnd = getTSEnd();
|
||||
@ -388,6 +410,12 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
|
||||
return samples;
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamplesHighRes(DBHandler db, GBDevice device) {
|
||||
int tsStart = getTSStart();
|
||||
int tsEnd = getTSEnd();
|
||||
return getSamplesHighRes(db, device, tsStart, tsEnd);
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamplesofSleep(DBHandler db, GBDevice device) {
|
||||
int SLEEP_HOUR_LIMIT = 12;
|
||||
|
||||
|
@ -92,7 +92,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
|
||||
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -76,6 +76,17 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
return getGBActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<T> getAllActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
return getGBActivitySamplesHighRes(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHighResData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@Deprecated // use getAllActivitySamples
|
||||
@ -138,7 +149,7 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity samples between two timestamps. Exactly one every minute.
|
||||
* Get the activity samples between two timestamps (inclusive). Exactly one every minute.
|
||||
* @param timestamp_from Start timestamp
|
||||
* @param timestamp_to End timestamp
|
||||
* @return Exactly one sample for every minute
|
||||
@ -162,6 +173,20 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activity samples between two timestamps (inclusive).
|
||||
* Differs from {@link #getGBActivitySamples(int, int)} in that it supplies as many samples as
|
||||
* available.
|
||||
* It assumes {@link #getGBActivitySamples(int, int)} returns the highest resolution data unless
|
||||
* this is overwritten.
|
||||
* @param timestamp_from Start timestamp
|
||||
* @param timestamp_to End timestamp
|
||||
* @return All the samples between start and end timestamp (inclusive)
|
||||
*/
|
||||
protected List<T> getGBActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
return getGBActivitySamples(timestamp_from, timestamp_to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches all samples of this type from the session. Changes to them may not be
|
||||
* written back to the database.
|
||||
|
@ -47,6 +47,7 @@ public interface SampleProvider<T extends AbstractActivitySample> {
|
||||
|
||||
/**
|
||||
* Returns the list of all samples, of any type, within the given time span.
|
||||
* This returns exactly one sample every minute.
|
||||
* @param timestamp_from the start timestamp
|
||||
* @param timestamp_to the end timestamp
|
||||
* @return the list of samples of any type
|
||||
@ -54,6 +55,19 @@ public interface SampleProvider<T extends AbstractActivitySample> {
|
||||
@NonNull
|
||||
List<T> getAllActivitySamples(int timestamp_from, int timestamp_to);
|
||||
|
||||
/**
|
||||
* Same as {@link #getAllActivitySamples(int, int)}}, but returns as many samples as possible.
|
||||
* Explicitly does not make a guarantee about how many samples there are per timeframe, which
|
||||
* can also change over time.
|
||||
*/
|
||||
List<T> getAllActivitySamplesHighRes(int timestamp_from, int timestamp_to);
|
||||
|
||||
/**
|
||||
* Specifies that the sample provider has higher resolution data. Set to true if the sample
|
||||
* provider can provide more than one sample a minute.
|
||||
*/
|
||||
boolean hasHighResData();
|
||||
|
||||
/**
|
||||
* Returns the list of all samples that represent user "activity", within
|
||||
* the given time span. This excludes samples of type sleep, for example.
|
||||
|
@ -21,6 +21,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
@ -64,6 +65,16 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractActivitySample> getAllActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHighResData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractActivitySample> getActivitySamples(int timestamp_from, int timestamp_to) {
|
||||
return null;
|
||||
|
@ -305,12 +305,24 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
|
||||
return processedSamples;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<HuaweiActivitySample> getGBActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
|
||||
List<HuaweiActivitySample> processedSamples = getRawOrderedActivitySamples(timestamp_from, timestamp_to);
|
||||
addWorkoutSamples(processedSamples, timestamp_from, timestamp_to);
|
||||
return processedSamples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHighResData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private HuaweiActivitySample createDummySample(int timestamp) {
|
||||
HuaweiActivitySample activitySample = new HuaweiActivitySample(
|
||||
timestamp,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
timestamp + 60, // Make sure the duration is 60
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
0,
|
||||
@ -438,7 +450,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
|
||||
List<HuaweiWorkoutDataSample> workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to);
|
||||
|
||||
for (int i = 0; i < workoutSamples.size(); i++) {
|
||||
// Look ahead to see if this is still the same workout
|
||||
// Look behind to see if this is still the same workout
|
||||
boolean inWorkout = i != 0 && workoutSamples.get(i).getWorkoutId() == workoutSamples.get(i - 1).getWorkoutId();
|
||||
|
||||
// Skip the processed sample that are before this workout sample
|
||||
@ -470,4 +482,53 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
|
||||
processedSamples.get(currentIndex).setRawIntensity(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void addWorkoutSamples(List<HuaweiActivitySample> processedSamples, int timestamp_from, int timestamp_to) {
|
||||
int currentIndex = 0;
|
||||
List<HuaweiWorkoutDataSample> workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to);
|
||||
|
||||
for (int i = 0; i < workoutSamples.size(); i++) {
|
||||
// Look behind to see if this is still the same workout
|
||||
boolean inWorkout = i != 0 && workoutSamples.get(i).getWorkoutId() == workoutSamples.get(i - 1).getWorkoutId();
|
||||
|
||||
// Skip the samples that are before this workout sample, and potentially clear the HR
|
||||
// and intensity - see #4126 for the reasoning
|
||||
while (currentIndex < processedSamples.size() && workoutSamples.get(i).getTimestamp() > processedSamples.get(currentIndex).getTimestamp()) {
|
||||
if (inWorkout) {
|
||||
processedSamples.get(currentIndex).setHeartRate(ActivitySample.NOT_MEASURED);
|
||||
processedSamples.get(currentIndex).setRawIntensity(0);
|
||||
}
|
||||
|
||||
currentIndex += 1;
|
||||
}
|
||||
|
||||
if (i < workoutSamples.size() - 1) {
|
||||
processedSamples.add(currentIndex, convertWorkoutSampleToActivitySample(workoutSamples.get(i), workoutSamples.get(i + 1).getTimestamp()));
|
||||
} else {
|
||||
// For the last workout sample we assume it is over 5 seconds
|
||||
processedSamples.add(currentIndex, convertWorkoutSampleToActivitySample(workoutSamples.get(i), workoutSamples.get(i).getTimestamp() + 5));
|
||||
}
|
||||
currentIndex += 1; // Prevent clearing the sample in the next loop
|
||||
}
|
||||
}
|
||||
|
||||
private HuaweiActivitySample convertWorkoutSampleToActivitySample(HuaweiWorkoutDataSample workoutSample, int nextTimestamp) {
|
||||
int hr = workoutSample.getHeartRate() & 0xFF;
|
||||
HuaweiActivitySample newSample = new HuaweiActivitySample(
|
||||
workoutSample.getTimestamp(),
|
||||
-1,
|
||||
-1,
|
||||
nextTimestamp - 1, // Just to prevent overlap causing issues
|
||||
(byte) 0x00,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
ActivitySample.NOT_MEASURED,
|
||||
hr
|
||||
);
|
||||
newSample.setProvider(this);
|
||||
return newSample;
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,8 @@ public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider<HuaweiS
|
||||
@NonNull
|
||||
@Override
|
||||
public List<HuaweiSpo2Sample> getAllSamples(long timestampFrom, long timestampTo) {
|
||||
List<HuaweiActivitySample> activitySamples = huaweiSampleProvider.getAllActivitySamples((int) (timestampFrom / 1000L), (int) (timestampTo / 1000L));
|
||||
// Using high res data is fine for the SpO2 sample provider at the time of writing
|
||||
List<HuaweiActivitySample> activitySamples = huaweiSampleProvider.getAllActivitySamplesHighRes((int) (timestampFrom / 1000L), (int) (timestampTo / 1000L));
|
||||
List<HuaweiSpo2Sample> spo2Samples = new ArrayList<>(activitySamples.size());
|
||||
for (HuaweiActivitySample sample : activitySamples) {
|
||||
if (sample.getSpo() == -1)
|
||||
|
Loading…
Reference in New Issue
Block a user