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