From b57a9d0c0c96b72da6e68425c5e7631be55b8642 Mon Sep 17 00:00:00 2001 From: Philoul Date: Mon, 6 May 2024 08:59:22 +0200 Subject: [PATCH 1/2] Smooth HR values --- .../aaps/wear/heartrate/HeartRateListener.kt | 46 +++++++++++++++++-- .../wear/watchfaces/utils/BaseWatchFace.kt | 2 +- wear/src/main/res/values/arrays.xml | 14 ++++++ wear/src/main/res/values/strings.xml | 6 +++ wear/src/main/res/xml/others_preferences.xml | 8 ++++ .../wear/heartrate/HeartRateListenerTest.kt | 13 +++++- 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/heartrate/HeartRateListener.kt b/wear/src/main/kotlin/app/aaps/wear/heartrate/HeartRateListener.kt index 4679813a06a..0ad4d996a02 100644 --- a/wear/src/main/kotlin/app/aaps/wear/heartrate/HeartRateListener.kt +++ b/wear/src/main/kotlin/app/aaps/wear/heartrate/HeartRateListener.kt @@ -12,6 +12,8 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.weardata.EventData +import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.wear.R import app.aaps.wear.comm.IntentWearToMobile import io.reactivex.rxjava3.disposables.Disposable import java.util.concurrent.TimeUnit @@ -42,13 +44,14 @@ import kotlin.math.roundToInt class HeartRateListener( private val ctx: Context, private val aapsLogger: AAPSLogger, + sp: SP, aapsSchedulers: AapsSchedulers, now: Long = System.currentTimeMillis(), ) : SensorEventListener, Disposable { /** How often we send values to the phone. */ private val samplingIntervalMillis = 60_000L - private val sampler = Sampler(now) + private val sampler = Sampler(now, sp) private var schedule: Disposable? = null /** We only use values with these accuracies and ignore NO_CONTACT and UNRELIABLE. */ @@ -133,7 +136,12 @@ class HeartRateListener( sampler.setHeartRate(timestampMillis, heartRate) } - private class Sampler(timestampMillis: Long) { + private class Sampler(timestampMillis: Long, val sp: SP) { + + private val actionHeartRatehistory: MutableList = ArrayList() + private val averageHistory + get() = sp.getInt(R.string.key_heart_rate_smoothing, 1) + private val maxAverage = 15 private var startMillis: Long = timestampMillis private var lastEventMillis: Long = timestampMillis @@ -166,7 +174,9 @@ class HeartRateListener( fix(timestampMillis) return if (10 * activeMillis > lastEventMillis - startMillis) { val bpm = beats / activeMillis.toMinute() - EventData.ActionHeartRate(timestampMillis - startMillis, timestampMillis, bpm, device) + actionHeartRatehistory.add(EventData.ActionHeartRate(timestampMillis - startMillis, timestampMillis, bpm, device)) + averageHeartrate(timestampMillis - startMillis, timestampMillis, device) + //EventData.ActionHeartRate(timestampMillis - startMillis, timestampMillis, bpm, device) } else { null }.also { @@ -185,5 +195,35 @@ class HeartRateListener( currentBpm = heartRate } } + + fun averageHeartrate(duration: Long, timestamp: Long, device: String): EventData.ActionHeartRate? { + lock.withLock { + cleanActionHeartRatehistory(timestamp) // clean oldest values from memory + var bpm = 0.0 + var avgNb = 0 + var allDuration = 0L + actionHeartRatehistory.forEach { hr -> + if (hr.timestamp >= timestamp - (averageHistory - 1) * 62000L) { // If smoothing disabled, only last BPM is sent + bpm += hr.beatsPerMinute + avgNb++ + allDuration += hr.duration + } + } + return if (avgNb > averageHistory / 4 || allDuration.toMinute() > averageHistory.toDouble() / 2.0) { // When average is enabled, send value only if average is done on a number of values that is above half the selected duration + EventData.ActionHeartRate(duration, timestamp, bpm / avgNb, device) + } else + null + } + } + + fun cleanActionHeartRatehistory (timestamp: Long) { + val iterator = actionHeartRatehistory.iterator() + while(iterator.hasNext()){ + val hr = iterator.next() + if(hr.timestamp < timestamp - (maxAverage - 1) * 62000L){ // keep in memory the max duration + 2s marging for each min + iterator.remove() + } + } + } } } diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt index 82de720af22..fd6a7e7068b 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/utils/BaseWatchFace.kt @@ -166,7 +166,7 @@ abstract class BaseWatchFace : WatchFace() { if (sp.getBoolean(R.string.key_heart_rate_sampling, false)) { if (heartRateListener == null) { heartRateListener = HeartRateListener( - this, aapsLogger, aapsSchedulers + this, aapsLogger, sp, aapsSchedulers ).also { hrl -> disposable += hrl } } } else { diff --git a/wear/src/main/res/values/arrays.xml b/wear/src/main/res/values/arrays.xml index 1a9f62bb15a..028ae05f81c 100644 --- a/wear/src/main/res/values/arrays.xml +++ b/wear/src/main/res/values/arrays.xml @@ -117,4 +117,18 @@ none + + @string/pref_1_min + @string/pref_5_min + @string/pref_10_min + @string/pref_15_min + + + + 1 + 5 + 10 + 15 + + diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index 0c2220f87ed..f125a0aba81 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -53,6 +53,10 @@ 3 hours 4 hours 5 hours + disabled + 5 min + 10 min + 15 min Input Design Default Quick righty @@ -260,6 +264,8 @@ !err! heart_rate_sampling Heart Rate + heart_rate_smoothing + HR Smoothing key_steps_sampling Enable Steps Count Temp Target 1 diff --git a/wear/src/main/res/xml/others_preferences.xml b/wear/src/main/res/xml/others_preferences.xml index fad9f43da50..227a4984967 100644 --- a/wear/src/main/res/xml/others_preferences.xml +++ b/wear/src/main/res/xml/others_preferences.xml @@ -12,6 +12,14 @@ app:wear_iconOff="@drawable/settings_off" app:wear_iconOn="@drawable/settings_on" /> + + () private val device = "unknown unknown" @@ -40,7 +43,7 @@ internal class HeartRateListenerTest { any(), eq(60_000L), eq(60_000L), eq(TimeUnit.MILLISECONDS) ) ).thenReturn(schedule) - val listener = HeartRateListener(ctx, aapsLogger, aapsSchedulers, timestampMillis) + val listener = HeartRateListener(ctx, aapsLogger, sp, aapsSchedulers, timestampMillis) verify(aapsSchedulers.io).schedulePeriodicallyDirect( any(), eq(60_000L), eq(60_000L), eq(TimeUnit.MILLISECONDS) ) @@ -74,6 +77,7 @@ internal class HeartRateListenerTest { @Test fun onSensorChanged() { + `when`(sp.getInt(R.string.key_heart_rate_smoothing, 1)).thenReturn(1) val start = System.currentTimeMillis() val d1 = 10_000L val d2 = 20_000L @@ -91,6 +95,7 @@ internal class HeartRateListenerTest { @Test fun onSensorChanged2() { + `when`(sp.getInt(R.string.key_heart_rate_smoothing, 1)).thenReturn(1) val start = System.currentTimeMillis() val d1 = 10_000L val d2 = 40_000L @@ -111,6 +116,7 @@ internal class HeartRateListenerTest { @Test fun onSensorChangedMultiple() { + `when`(sp.getInt(R.string.key_heart_rate_smoothing, 1)).thenReturn(1) val start = System.currentTimeMillis() val d1 = 10_000L val d2 = 40_000L @@ -132,6 +138,7 @@ internal class HeartRateListenerTest { @Test fun onSensorChangedNoContact() { + `when`(sp.getInt(R.string.key_heart_rate_smoothing, 1)).thenReturn(1) val start = System.currentTimeMillis() val d1 = 10_000L val d2 = 40_000L @@ -148,6 +155,7 @@ internal class HeartRateListenerTest { @Test fun onAccuracyChanged() { + `when`(sp.getInt(R.string.key_heart_rate_smoothing, 1)).thenReturn(1) val start = System.currentTimeMillis() val d1 = 10_000L val d2 = 40_000L @@ -162,4 +170,7 @@ internal class HeartRateListenerTest { assertThat(heartRates).containsExactly(ActionHeartRate(d3, start + d3, 95.0, device)) listener.dispose() } + + + } From a8f9446a3ca24bb9cbde27059357ad12d8465df3 Mon Sep 17 00:00:00 2001 From: Philoul Date: Sun, 4 Aug 2024 22:35:23 +0200 Subject: [PATCH 2/2] Fix for HR smoothing --- .../kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt index 8713aacc7a3..9c061293706 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt @@ -195,7 +195,7 @@ class DataLayerListenerServiceWear : WearableListenerService() { if (sp.getBoolean(R.string.key_heart_rate_sampling, false)) { if (heartRateListener == null) { heartRateListener = HeartRateListener( - this, aapsLogger, aapsSchedulers + this, aapsLogger, sp, aapsSchedulers ).also { hrl -> disposable += hrl } } } else {