1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-27 04:16: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:
Martin.JM 2024-10-28 11:44:52 +01:00 committed by José Rebelo
parent 1882ee947e
commit 82e3a86350
8 changed files with 169 additions and 21 deletions

View File

@ -152,6 +152,11 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
return getAllSamples(db, device, tsFrom, tsTo); 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 @Override
protected void setupLegend(Chart<?> chart) { protected void setupLegend(Chart<?> chart) {
List<LegendEntry> legendEntries = new ArrayList<>(5); List<LegendEntry> legendEntries = new ArrayList<>(5);
@ -231,9 +236,12 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
private DefaultChartsData<LineData> buildChartFromSamples(DBHandler handler) { private DefaultChartsData<LineData> buildChartFromSamples(DBHandler handler) {
final List<? extends ActivitySample> samples = getAllSamples(handler, gbDevice, startTime, endTime); final List<? extends ActivitySample> samples = getAllSamples(handler, gbDevice, startTime, endTime);
final List<? extends ActivitySample> highResSamples = getAllSamplesHighRes(handler, gbDevice, startTime, endTime);
try { try {
if (highResSamples == null)
return refresh(gbDevice, samples); return refresh(gbDevice, samples);
return refresh(gbDevice, samples, highResSamples);
} catch (Exception e) { } catch (Exception e) {
LOG.error("Unable to get charts data right now", e); LOG.error("Unable to get charts data right now", e);
} }

View File

@ -27,6 +27,7 @@ import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
import org.apache.commons.lang3.NotImplementedException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -180,14 +181,28 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
return provider.getAllActivitySamples(tsFrom, tsTo); 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) { protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends AbstractActivitySample> provider = getProvider(db, device); SampleProvider<? extends AbstractActivitySample> provider = getProvider(db, device);
return provider.getActivitySamples(tsFrom, tsTo); return provider.getActivitySamples(tsFrom, tsTo);
} }
public DefaultChartsData<LineData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples) { 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(); TimestampTranslation tsTranslation = new TimestampTranslation();
LOG.info("{}: number of samples: {}", getTitle(), samples.size()); LOG.info("{}: number of samples: {}", getTitle(), samples.size());
LOG.info("{}: number of high res samples: {}", getTitle(), highResSamples.size());
LineData lineData; LineData lineData;
if (samples.isEmpty()) { if (samples.isEmpty()) {
@ -257,8 +272,15 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
} }
entries.get(index).add(createLineEntry(value, ts)); entries.get(index).add(createLineEntry(value, ts));
// heart rate line graph last_type = type;
if (hr && type != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { 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) { if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1)); heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
heartrateEntries.add(createLineEntry(0, ts - 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)); heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
lastHrSampleIndex = ts; lastHrSampleIndex = ts;
} }
last_type = type; }
last_value = value;
} }
// convert Entry Lists to Datasets // 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. * 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); 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) { protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
int tsStart = getTSStart(); int tsStart = getTSStart();
int tsEnd = getTSEnd(); int tsEnd = getTSEnd();
@ -388,6 +410,12 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
return samples; 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) { protected List<? extends ActivitySample> getSamplesofSleep(DBHandler db, GBDevice device) {
int SLEEP_HOUR_LIMIT = 12; int SLEEP_HOUR_LIMIT = 12;

View File

@ -92,7 +92,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider<? extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession()); SampleProvider<? extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
return provider.getAllActivitySamples(tsFrom, tsTo); return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
} }
@Override @Override

View File

@ -76,6 +76,17 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
return getGBActivitySamples(timestamp_from, timestamp_to); 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 @NonNull
@Override @Override
@Deprecated // use getAllActivitySamples @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_from Start timestamp
* @param timestamp_to End timestamp * @param timestamp_to End timestamp
* @return Exactly one sample for every minute * @return Exactly one sample for every minute
@ -162,6 +173,20 @@ public abstract class AbstractSampleProvider<T extends AbstractActivitySample> i
return samples; 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 * Detaches all samples of this type from the session. Changes to them may not be
* written back to the database. * written back to the database.

View File

@ -47,6 +47,7 @@ public interface SampleProvider<T extends AbstractActivitySample> {
/** /**
* Returns the list of all samples, of any type, within the given time span. * 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_from the start timestamp
* @param timestamp_to the end timestamp * @param timestamp_to the end timestamp
* @return the list of samples of any type * @return the list of samples of any type
@ -54,6 +55,19 @@ public interface SampleProvider<T extends AbstractActivitySample> {
@NonNull @NonNull
List<T> getAllActivitySamples(int timestamp_from, int timestamp_to); 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 * Returns the list of all samples that represent user "activity", within
* the given time span. This excludes samples of type sleep, for example. * the given time span. This excludes samples of type sleep, for example.

View File

@ -21,6 +21,7 @@ import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import java.util.Collections;
import java.util.List; import java.util.List;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
@ -64,6 +65,16 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return null; return null;
} }
@Override
public List<AbstractActivitySample> getAllActivitySamplesHighRes(int timestamp_from, int timestamp_to) {
return null;
}
@Override
public boolean hasHighResData() {
return false;
}
@Override @Override
public List<AbstractActivitySample> getActivitySamples(int timestamp_from, int timestamp_to) { public List<AbstractActivitySample> getActivitySamples(int timestamp_from, int timestamp_to) {
return null; return null;

View File

@ -305,12 +305,24 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
return processedSamples; 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) { private HuaweiActivitySample createDummySample(int timestamp) {
HuaweiActivitySample activitySample = new HuaweiActivitySample( HuaweiActivitySample activitySample = new HuaweiActivitySample(
timestamp, timestamp,
-1, -1,
-1, -1,
0, timestamp + 60, // Make sure the duration is 60
(byte) 0x00, (byte) 0x00,
ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED,
0, 0,
@ -438,7 +450,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
List<HuaweiWorkoutDataSample> workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to); List<HuaweiWorkoutDataSample> workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to);
for (int i = 0; i < workoutSamples.size(); i++) { 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(); boolean inWorkout = i != 0 && workoutSamples.get(i).getWorkoutId() == workoutSamples.get(i - 1).getWorkoutId();
// Skip the processed sample that are before this workout sample // 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); 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;
}
} }

View File

@ -66,7 +66,8 @@ public class HuaweiSpo2SampleProvider extends AbstractTimeSampleProvider<HuaweiS
@NonNull @NonNull
@Override @Override
public List<HuaweiSpo2Sample> getAllSamples(long timestampFrom, long timestampTo) { 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()); List<HuaweiSpo2Sample> spo2Samples = new ArrayList<>(activitySamples.size());
for (HuaweiActivitySample sample : activitySamples) { for (HuaweiActivitySample sample : activitySamples) {
if (sample.getSpo() == -1) if (sample.getSpo() == -1)