Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include smoothing feature for HR reading in Wear watch #3320

Merged
merged 3 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
46 changes: 43 additions & 3 deletions wear/src/main/kotlin/app/aaps/wear/heartrate/HeartRateListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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<EventData.ActionHeartRate> = 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
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
}
}
}
}
}
14 changes: 14 additions & 0 deletions wear/src/main/res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,18 @@
<item>none</item>
</string-array>

<string-array name="heartrate_smoothing">
<item>@string/pref_1_min</item>
<item>@string/pref_5_min</item>
<item>@string/pref_10_min</item>
<item>@string/pref_15_min</item>
</string-array>

<string-array name="heartrate_smoothing_values">
<item>1</item>
<item>5</item>
<item>10</item>
<item>15</item>
</string-array>

</resources>
6 changes: 6 additions & 0 deletions wear/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
<string name="pref_3_hours">3 hours</string>
<string name="pref_4_hours">4 hours</string>
<string name="pref_5_hours">5 hours</string>
<string name="pref_1_min">disabled</string>
<string name="pref_5_min">5 min</string>
<string name="pref_10_min">10 min</string>
<string name="pref_15_min">15 min</string>
<string name="pref_input_design">Input Design</string>
<string name="pref_default">Default</string>
<string name="pref_quick_righty">Quick righty</string>
Expand Down Expand Up @@ -260,6 +264,8 @@
<string name="error">!err!</string>
<string name="key_heart_rate_sampling" translatable="false">heart_rate_sampling</string>
<string name="pref_heartRateSampling">Heart Rate</string>
<string name="key_heart_rate_smoothing" translatable="false">heart_rate_smoothing</string>
<string name="pref_heartRateSmoothing">HR Smoothing</string>
<string name="key_steps_sampling" translatable="false">key_steps_sampling</string>
<string name="pref_stepsSampling">Enable Steps Count</string>
<string name="tile_tempt_1">Temp Target 1</string>
Expand Down
8 changes: 8 additions & 0 deletions wear/src/main/res/xml/others_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
app:wear_iconOff="@drawable/settings_off"
app:wear_iconOn="@drawable/settings_on" />

<ListPreference
android:defaultValue="1"
android:entries="@array/heartrate_smoothing"
android:entryValues="@array/heartrate_smoothing_values"
android:key="@string/key_heart_rate_smoothing"
android:summary="Heart Rate smoothing"
android:title="@string/pref_heartRateSmoothing" />

<CheckBoxPreference
android:defaultValue="false"
android:key="@string/key_steps_sampling"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import android.hardware.Sensor
import android.hardware.SensorManager
import app.aaps.core.interfaces.rx.AapsSchedulers
import app.aaps.core.interfaces.rx.weardata.EventData.ActionHeartRate
import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.shared.tests.AAPSLoggerTest
import app.aaps.wear.R
import com.google.common.truth.Truth.assertThat
import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.disposables.Disposable
Expand All @@ -30,6 +32,7 @@ internal class HeartRateListenerTest {
override val newThread: Scheduler = mock(Scheduler::class.java)
}
private val schedule = mock(Disposable::class.java)
private val sp = mock(SP::class.java)
private val heartRates = mutableListOf<ActionHeartRate>()
private val device = "unknown unknown"

Expand All @@ -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)
)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -162,4 +170,7 @@ internal class HeartRateListenerTest {
assertThat(heartRates).containsExactly(ActionHeartRate(d3, start + d3, 95.0, device))
listener.dispose()
}



}