From c202f20320aecb5299db46e927d096544ed97c7e Mon Sep 17 00:00:00 2001 From: timklge <2026103+timklge@users.noreply.github.com> Date: Sat, 12 Jul 2025 22:05:43 +0200 Subject: [PATCH] fix #42, fix #43: Add grade data source (#45) --- .../de/timklge/karoopowerbar/Settings.kt | 8 ++- .../kotlin/de/timklge/karoopowerbar/Window.kt | 55 ++++++++++++++++++- .../karoopowerbar/screens/MainScreen.kt | 49 ++++++++++++++--- app/src/main/res/values/colors.xml | 12 ++++ 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Settings.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Settings.kt index 4acb37e..a6da74a 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Settings.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Settings.kt @@ -29,7 +29,11 @@ data class PowerbarSettings( val minSpeed: Float = defaultMinSpeedMs, val maxSpeed: Float = defaultMaxSpeedMs, // 50 km/h in m/s val minPower: Int? = null, val maxPower: Int? = null, val minHr: Int? = null, val maxHr: Int? = null, - val useCustomHrRange: Boolean = false, val useCustomPowerRange: Boolean = false + val minGradient: Int? = defaultMinGradient, val maxGradient: Int? = defaultMaxGradient, + + val useCustomGradientRange: Boolean = false, + val useCustomHrRange: Boolean = false, + val useCustomPowerRange: Boolean = false ){ companion object { val defaultSettings = Json.encodeToString(PowerbarSettings()) @@ -37,6 +41,8 @@ data class PowerbarSettings( const val defaultMaxSpeedMs = 13.89f const val defaultMinCadence = 50 const val defaultMaxCadence = 120 + const val defaultMinGradient = 0 + const val defaultMaxGradient = 20 } } diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt index 1df4410..bcc360c 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.Locale import kotlin.math.roundToInt fun remap(value: Double?, fromMin: Double, fromMax: Double, toMin: Double, toMax: Double): Double? { @@ -153,7 +154,8 @@ class Window( SelectedSource.CADENCE_3S -> streamCadence(true) SelectedSource.ROUTE_PROGRESS -> streamRouteProgress(::getRouteProgress) SelectedSource.REMAINING_ROUTE -> streamRouteProgress(::getRemainingRouteProgress) - else -> {} + SelectedSource.GRADE -> streamGrade() + SelectedSource.NONE -> {} } } @@ -288,6 +290,57 @@ class Window( } } + private suspend fun streamGrade() { + @ColorRes + fun getInclineIndicatorColor(percent: Float): Int? { + return when(percent) { + in -Float.MAX_VALUE..<-7.5f -> R.color.eleDarkBlue // Dark blue + in -7.5f..<-4.6f -> R.color.eleLightBlue // Light blue + in -4.6f..<-2f -> R.color.eleWhite // White + in 2f..<4.6f -> R.color.eleDarkGreen // Dark green + in 4.6f..<7.5f -> R.color.eleLightGreen // Light green + in 7.5f..<12.5f -> R.color.eleYellow // Yellow + in 12.5f..<15.5f -> R.color.eleLightOrange // Light Orange + in 15.5f..<19.5f -> R.color.eleDarkOrange // Dark Orange + in 19.5f..<23.5f -> R.color.eleRed // Red + in 23.5f..Float.MAX_VALUE -> R.color.elePurple // Purple + else -> null + } + } + + val gradeFlow = karooSystem.streamDataFlow(DataType.Type.ELEVATION_GRADE) + .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } + .distinctUntilChanged() + + data class StreamData(val userProfile: UserProfile, val value: Double?, val settings: PowerbarSettings? = null) + + val settingsFlow = context.streamSettings() + + combine(karooSystem.streamUserProfile(), gradeFlow, settingsFlow) { userProfile, grade, settings -> + StreamData(userProfile, grade, settings) + }.distinctUntilChanged().throttle(1_000).collect { streamData -> + val value = streamData.value + + if (value != null) { + val minGradient = streamData.settings?.minGradient ?: PowerbarSettings.defaultMinGradient + val maxGradient = streamData.settings?.maxGradient ?: PowerbarSettings.defaultMaxGradient + + powerbar.progressColor = getInclineIndicatorColor(value.toFloat()) ?: context.getColor(R.color.zone0) + powerbar.progress = remap(value.toDouble(), minGradient.toDouble(), maxGradient.toDouble(), 0.0, 1.0) + powerbar.label = "${String.format(Locale.getDefault(), "%.1f", value)}%" + + Log.d(TAG, "Grade: $value") + } else { + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = null + powerbar.label = "?" + + Log.d(TAG, "Grade: Unavailable") + } + powerbar.invalidate() + } + } + private suspend fun streamCadence(smoothed: Boolean) { val cadenceFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE else DataType.Type.CADENCE) .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt index caf1008..4876d4d 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt @@ -87,6 +87,7 @@ enum class SelectedSource(val id: String, val label: String) { SPEED_3S("speed_3s", "Speed (3 sec avg"), CADENCE("cadence", "Cadence"), CADENCE_3S("cadence_3s", "Cadence (3 sec avg)"), + GRADE("grade", "Grade"), ROUTE_PROGRESS("route_progress", "Route Progress"), REMAINING_ROUTE("route_progress_remaining", "Route Remaining"); @@ -160,6 +161,8 @@ fun MainScreen(onFinish: () -> Unit) { var customMaxPower by remember { mutableStateOf("") } var customMinHr by remember { mutableStateOf("") } var customMaxHr by remember { mutableStateOf("") } + var minGrade by remember { mutableStateOf("0") } + var maxGrade by remember { mutableStateOf("0") } var useCustomPowerRange by remember { mutableStateOf(false) } var useCustomHrRange by remember { mutableStateOf(false) } @@ -188,6 +191,8 @@ fun MainScreen(onFinish: () -> Unit) { maxPower = customMaxPower.toIntOrNull(), minHr = customMinHr.toIntOrNull(), maxHr = customMaxHr.toIntOrNull(), + minGradient = minGrade.toIntOrNull() ?: PowerbarSettings.defaultMinGradient, + maxGradient = maxGrade.toIntOrNull() ?: PowerbarSettings.defaultMaxGradient, barBarSize = barBarSize, barFontSize = barFontSize, useCustomPowerRange = useCustomPowerRange, @@ -243,6 +248,8 @@ fun MainScreen(onFinish: () -> Unit) { customMaxPower = settings.maxPower?.toString() ?: "" customMinHr = settings.minHr?.toString() ?: "" customMaxHr = settings.maxHr?.toString() ?: "" + minGrade = settings.minGradient?.toString() ?: "" + maxGrade = settings.maxGradient?.toString() ?: "" useCustomPowerRange = settings.useCustomPowerRange useCustomHrRange = settings.useCustomHrRange } @@ -344,7 +351,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(right = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { minSpeed = it }, + onValueChange = { minSpeed = it.filter { c -> c.isDigit() } }, label = { Text("Min Speed") }, suffix = { Text(if (isImperial) "mph" else "kph") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -355,7 +362,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(left = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { maxSpeed = it }, + onValueChange = { maxSpeed = it.filter { c -> c.isDigit() } }, label = { Text("Max Speed") }, suffix = { Text(if (isImperial) "mph" else "kph") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -380,7 +387,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(right = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { customMinPower = it }, + onValueChange = { customMinPower = it.filter { c -> c.isDigit() } }, label = { Text("Min Power", fontSize = 12.sp) }, suffix = { Text("W") }, placeholder = { Text("$profileMinPower") }, @@ -392,7 +399,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(left = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { customMaxPower = it }, + onValueChange = { customMaxPower = it.filter { c -> c.isDigit() } }, label = { Text("Max Power", fontSize = 12.sp) }, suffix = { Text("W") }, placeholder = { Text("$profileMaxPower") }, @@ -419,7 +426,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(right = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { customMinHr = it }, + onValueChange = { customMinHr = it.filter { c -> c.isDigit() } }, label = { Text("Min Hr") }, suffix = { Text("bpm") }, placeholder = { if(profileRestHr > 0) Text("$profileRestHr") else Unit }, @@ -431,7 +438,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(left = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { customMaxHr = it }, + onValueChange = { customMaxHr = it.filter { c -> c.isDigit() } }, label = { Text("Max Hr") }, suffix = { Text("bpm") }, placeholder = { if(profileMaxHr > 0) Text("$profileMaxHr") else Unit }, @@ -450,7 +457,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(right = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { minCadence = it }, + onValueChange = { minCadence = it.filter { c -> c.isDigit() } }, label = { Text("Min Cadence") }, suffix = { Text("rpm") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -461,7 +468,7 @@ fun MainScreen(onFinish: () -> Unit) { .weight(1f) .absolutePadding(left = 2.dp) .onFocusEvent(::updateFocus), - onValueChange = { maxCadence = it }, + onValueChange = { maxCadence = it.filter { c -> c.isDigit() } }, label = { Text("Min Cadence") }, suffix = { Text("rpm") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -470,6 +477,32 @@ fun MainScreen(onFinish: () -> Unit) { } } + if (topSelectedSource == SelectedSource.GRADE || bottomSelectedSource == SelectedSource.GRADE){ + Row(verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField(value = minGrade, modifier = Modifier + .weight(1f) + .absolutePadding(right = 2.dp) + .onFocusEvent(::updateFocus), + onValueChange = { minGrade = it.filterIndexed { index, c -> c.isDigit() || (c == '-' && index == 0) } }, + label = { Text("Min Grade") }, + suffix = { Text("%") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + + OutlinedTextField(value = maxGrade, modifier = Modifier + .weight(1f) + .absolutePadding(left = 2.dp) + .onFocusEvent(::updateFocus), + onValueChange = { maxGrade = it.filterIndexed { index, c -> c.isDigit() || (c == '-' && index == 0) } }, + label = { Text("Max Grade") }, + suffix = { Text("%") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + } + } + Row(verticalAlignment = Alignment.CenterVertically) { Switch(checked = colorBasedOnZones, onCheckedChange = { colorBasedOnZones = it diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index be7bb44..709b7c5 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,4 +12,16 @@ #FE581F #D60404 #B700A2 + + #079d78 + #58c597 + #e7e021 + #e59174 + #e7693a + #c82425 + #f05a28 + #b222a3 + #ffffff + #4fc3f7 + #2D58AF \ No newline at end of file