diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 507b415..c0d641d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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() diff --git a/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt b/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt index c385978..7371dbc 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/KarooReminderExtension.kt @@ -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.allIntermediateInts(): Flow = 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}") diff --git a/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt b/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt index 3cc8a67..efb2476 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/DetailScreen.kt @@ -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( diff --git a/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt b/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt index 5db417b..53bc565 100644 --- a/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt +++ b/app/src/main/kotlin/de/timklge/karooreminder/screens/Reminder.kt @@ -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")