Add rolling average / smoothing setting to reminder ui for power trigger (#48)

* Add smooth setting enum

# Conflicts:
#	app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt

* Add rolling average setting to reminder ui

* Use smoothing for power trigger

* Update change log
This commit is contained in:
timklge 2025-04-05 16:19:24 +02:00 committed by GitHub
parent 24cada2cad
commit 04820cf120
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 118 additions and 31 deletions

View File

@ -71,9 +71,7 @@ tasks.register("generateManifest") {
"latestVersionCode" to android.defaultConfig.versionCode,
"developer" to "timklge",
"description" to "Shows in-ride alerts after a given time interval, distance or HR / power / speed / cadence out of range",
"releaseNotes" to "* Only show reminders while riding\n" +
"* Limit reminder title length in list\n" +
"* Use imperial units for temperature and tire pressure if selected\n"
"releaseNotes" to "* Add rolling average setting for power triggers"
)
val gson = groovy.json.JsonBuilder(manifest).toPrettyString()

View File

@ -5,8 +5,6 @@ import android.media.MediaPlayer
import android.util.Log
import de.timklge.karooreminder.screens.Reminder
import de.timklge.karooreminder.screens.ReminderBeepPattern
import de.timklge.karooreminder.screens.defaultReminders
import de.timklge.karooreminder.screens.preferencesKey
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.extension.KarooExtension
import io.hammerhead.karooext.models.DataType
@ -34,8 +32,19 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
enum class SmoothSetting(val label: String) {
NONE("None"),
SMOOTH_3S("3 seconds"),
SMOOTH_10S("10 seconds"),
SMOOTH_30S("30 seconds"),
SMOOTH_20M("20 minutes"),
SMOOTH_60M("60 minutes"),
SMOOTH_LAP("Lap"),
SMOOTH_RIDE("Ride");
}
enum class ReminderTrigger(val id: String, val label: String) {
ELAPSED_TIME("elapsed_time", "Elapsed Time"),
@ -92,6 +101,45 @@ enum class ReminderTrigger(val id: String, val label: String) {
this == REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED || this == REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED ||
this == AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED || this == AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED ||
this == GRADIENT_LIMIT_MAXIMUM_EXCEEDED || this == GRADIENT_LIMIT_MINIMUM_EXCEEDED
fun getSmoothedDataType(smoothSetting: SmoothSetting): String {
return when(this) {
POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> {
when (smoothSetting) {
SmoothSetting.NONE -> DataType.Type.POWER
SmoothSetting.SMOOTH_3S -> DataType.Type.SMOOTHED_3S_AVERAGE_POWER
SmoothSetting.SMOOTH_10S -> DataType.Type.SMOOTHED_10S_AVERAGE_POWER
SmoothSetting.SMOOTH_30S -> DataType.Type.SMOOTHED_30S_AVERAGE_POWER
SmoothSetting.SMOOTH_20M -> DataType.Type.SMOOTHED_20M_AVERAGE_POWER
SmoothSetting.SMOOTH_60M -> DataType.Type.SMOOTHED_1HR_AVERAGE_POWER
SmoothSetting.SMOOTH_LAP -> DataType.Type.POWER_LAP
SmoothSetting.SMOOTH_RIDE -> DataType.Type.AVERAGE_POWER
}
}
else -> getDataType()
}
}
fun hasSmoothedDataTypes(): Boolean {
return this == POWER_LIMIT_MAXIMUM_EXCEEDED || this == POWER_LIMIT_MINIMUM_EXCEEDED
}
fun getDataType(): String {
return when (this) {
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.HEART_RATE
POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_POWER
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_SPEED
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.CORE_TEMP
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TIRE_PRESSURE_FRONT
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TIRE_PRESSURE_REAR
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.ELEVATION_GRADE
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TEMPERATURE
ReminderTrigger.DISTANCE, ReminderTrigger.ELAPSED_TIME, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $this")
}
}
}
fun Flow<Int>.allIntermediateInts(): Flow<Int> = flow {
@ -251,9 +299,11 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
)
intervalTriggers.forEach { trigger ->
if (reminders.any { it.trigger == trigger }){
val job = startRangeExceededJob(trigger)
triggerJobs.add(job)
SmoothSetting.entries.forEach { smoothSetting ->
if (reminders.any { it.trigger == trigger && it.smoothSetting == smoothSetting }){
val job = startRangeExceededJob(trigger, smoothSetting)
triggerJobs.add(job)
}
}
}
}
@ -291,25 +341,13 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
}
private fun startRangeExceededJob(triggerType: ReminderTrigger): Job {
private fun startRangeExceededJob(triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job {
return CoroutineScope(Dispatchers.IO).launch {
val preferences = streamPreferences()
val dataType = when (triggerType) {
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.HEART_RATE
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_POWER
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_SPEED
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.CORE_TEMP
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TIRE_PRESSURE_FRONT
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TIRE_PRESSURE_REAR
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.ELEVATION_GRADE
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> DataType.Type.TEMPERATURE
Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting")
ReminderTrigger.DISTANCE, ReminderTrigger.ELAPSED_TIME, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
val valueStream = karooSystem.streamDataFlow(dataType)
val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(smoothSetting))
.mapNotNull {
val dataPoint = (it as? StreamState.Streaming)?.dataPoint
@ -394,13 +432,7 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
val result = reminder.isActive && reminder.trigger == triggerType && triggerIsMet
/* if (result){
Log.i(TAG, "Triggered range reminder: ${reminder.name} (${triggerType}): actual value $actualValue, threshold $triggerThreshold")
} else if(reminder.trigger == triggerType && reminder.isActive) {
Log.i(TAG, "Not triggered range reminder: ${reminder.name} (${triggerType}): actual value $actualValue, threshold $triggerThreshold")
} */
result
reminder.isActive && reminder.trigger == triggerType && triggerIsMet
}
triggered
@ -408,6 +440,9 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
.filterNotNull()
.filter { it.isNotEmpty() }
.throttle(1_000 * 60) // At most once every minute
.onCompletion {
Log.i(TAG, "Range exceeded job for trigger $triggerType with smooth setting $smoothSetting completed")
}
.collectLatest { reminders ->
reminders.forEach { reminder ->
Log.d(TAG, "Dispatching reminder: ${reminder.name}")

View File

@ -62,6 +62,7 @@ import com.maxkeppeler.sheets.color.models.MultipleColors
import com.maxkeppeler.sheets.color.models.SingleColor
import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting
import de.timklge.karooreminder.streamUserProfile
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.HardwareType
@ -92,11 +93,13 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
reminder.interval.toString()
})
}
var smoothSetting by remember { mutableStateOf(reminder.smoothSetting) }
var isActive by remember { mutableStateOf(reminder.isActive) }
var autoDismiss by remember { mutableStateOf(reminder.isAutoDismiss) }
var deleteDialogVisible by remember { mutableStateOf(false) }
var toneDialogVisible by remember { mutableStateOf(false) }
var triggerDialogVisible by remember { mutableStateOf(false) }
var smoothSettingDialogVisible by remember { mutableStateOf(false) }
var selectedTone by remember { mutableStateOf(reminder.tone) }
var autoDismissSeconds by remember { mutableStateOf(reminder.autoDismissSeconds.toString()) }
var selectedTrigger by remember { mutableStateOf(reminder.trigger) }
@ -111,6 +114,7 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
text = text,
displayForegroundColor = selectedColor,
isActive = isActive,
smoothSetting = smoothSetting,
trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
}
@ -172,6 +176,18 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
singleLine = true
)
if (selectedTrigger.hasSmoothedDataTypes()){
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(60.dp), onClick = {
smoothSettingDialogVisible = true
}) {
Icon(Icons.Default.Build, contentDescription = "Change Smooth Setting", modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(5.dp))
Text("Average: ${smoothSetting.label}")
}
}
ColorDialog(
state = colorDialogState,
selection = ColorSelection(
@ -338,6 +354,41 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
}
}
if (smoothSettingDialogVisible) {
Dialog(onDismissRequest = { smoothSettingDialogVisible = false }) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
shape = RoundedCornerShape(10.dp),
) {
Column(modifier = Modifier
.padding(5.dp)
.verticalScroll(rememberScrollState())
.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) {
SmoothSetting.entries.forEach { setting ->
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
smoothSetting = setting
smoothSettingDialogVisible = false
}, verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = smoothSetting == setting, onClick = {
smoothSetting = setting
smoothSettingDialogVisible = false
})
Text(
text = setting.label,
modifier = Modifier.padding(start = 10.dp)
)
}
}
}
}
}
}
if (toneDialogVisible){
Dialog(onDismissRequest = { toneDialogVisible = false }) {
Card(

View File

@ -5,6 +5,7 @@ import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import de.timklge.karooreminder.R
import de.timklge.karooreminder.ReminderTrigger
import de.timklge.karooreminder.SmoothSetting
import io.hammerhead.karooext.models.PlayBeepPattern
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
@ -131,6 +132,8 @@ class Reminder(val id: Int, var name: String,
var interval: Int? = null,
/** Trigger value used by temperature, gradient, tire pressure triggers */
var intervalFloat: Double? = null,
/** Smooth interval used by power, speed triggers */
var smoothSetting: SmoothSetting = SmoothSetting.SMOOTH_3S,
var text: String,
var displayForegroundColor: ReminderColor? = null,
@Deprecated("Use displayForegroundColor instead")