Compare commits

...

10 Commits

Author SHA1 Message Date
timklge
1ee564c7b2
Add screenshots to manifest (#52)
Some checks failed
Build / build (push) Failing after 1m45s
2025-05-02 00:22:42 +02:00
timklge
70d902ed2c
Update installation instructions 2025-05-02 00:15:58 +02:00
timklge
810ffad69d
Reset preferences if file is corrupted (#50)
* Reset preferences if file is corrupted

* Update developer name
2025-04-18 17:44:19 +02:00
timklge
04820cf120
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
2025-04-05 16:19:24 +02:00
timklge
24cada2cad
Only listen on data type changes tha have active reminders (#47) 2025-04-04 17:31:13 +02:00
dcde90981d Move funding link 2025-03-25 18:07:28 +01:00
timklge
f6085b09fd
Only fire interval reminders in riding state (#45)
* Only fire interval reminders in riding state

* Update release notes
2025-03-20 17:18:14 +01:00
timklge
c8d77009fb
Limit text length in reminder list and indicate overflow as ellipsis (#44) 2025-03-20 17:03:13 +01:00
timklge
a085d97ead
Support imperial units for temperature, tire pressures (#40)
* WIP add core temp, tire pressure trigger types

* Add gradient, core temperature, tire pressure, ambient temperature trigger types

* Update screenshots

* Update changelog

* Support imperial units for temperature, tire pressures

* Fix karoo system connection is not closed by activity
2025-03-13 20:01:24 +01:00
timklge
349cabca05
Add gradient, core temperature, tire pressure, ambient temperature trigger types (#36)
* WIP add core temp, tire pressure trigger types

* Add gradient, core temperature, tire pressure, ambient temperature trigger types

* Update screenshots

* Update changelog

* Use matching data field for core temp, tire pressure

* Allow negative values (for gradient)
2025-03-12 23:32:41 +01:00
13 changed files with 408 additions and 204 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
buy_me_a_coffee: timklge

View File

@ -53,6 +53,6 @@ jobs:
name: ${{ github.ref_name }}
prerelease: false
generateReleaseNotes: true
artifacts: app/build/outputs/apk/release/app-release.apk, app/manifest.json, app/karoo-reminder.png
artifacts: app/build/outputs/apk/release/app-release.apk, app/manifest.json, app/karoo-reminder.png, reminder.png, detail.png, list.png
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -8,24 +8,13 @@ Karoo extension that displays in-ride alerts based on custom triggers. Reminders
Compatible with Karoo 2 and Karoo 3 devices.
<a href="https://www.buymeacoffee.com/timklge" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
![Reminder List](list.png)
![Reminder Detail](detail.png)
![Reminder in ride](reminder.png)
## Installation
If you are using a Karoo 3, you can use [Hammerhead's sideloading procedure](https://support.hammerhead.io/hc/en-us/articles/31576497036827-Companion-App-Sideloading) to install the app:
1. Using the browser on your phone, long-press [this download link](https://github.com/timklge/karoo-reminder/releases/latest/download/app-release.apk) and share it with the Hammerhead Companion app.
2. Your karoo should show an info screen about the app now. Press "Install".
If you are using a Karoo 2, you can use manual sideloading:
1. Download the apk from the [releases page](https://github.com/timklge/karoo-reminder/releases) (or build it from source)
2. Set up your Karoo for sideloading. DC Rainmaker has a great [step-by-step guide](https://www.dcrainmaker.com/2021/02/how-to-sideload-android-apps-on-your-hammerhead-karoo-1-karoo-2.html).
3. Install the app by running `adb install app-release.apk`.
This extension is available from the extension library on your Karoo device. Find more information on extensions in the [Hammerhead FAQ](https://support.hammerhead.io/hc/en-us/articles/34676015530907-Karoo-OS-Extensions-Library).
## Credits

View File

@ -63,16 +63,20 @@ tasks.register("generateManifest") {
doLast {
val manifestFile = file("$projectDir/manifest.json")
val manifest = mapOf(
"label" to "karoo-reminder",
"label" to "Reminder",
"packageName" to "de.timklge.karooreminder",
"iconUrl" to "https://github.com/timklge/karoo-reminder/releases/latest/download/karoo-reminder.png",
"latestApkUrl" to "https://github.com/timklge/karoo-reminder/releases/latest/download/app-release.apk",
"latestVersion" to android.defaultConfig.versionName,
"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 "* Add additional beep patterns for Karoo 3\n" +
"* Add touchable back button",
"developer" to "github.com/timklge",
"description" to "Open-source extension that shows in-ride alerts after a given time interval has passed, distance has been traveled or HR / power / speed / cadence range is exceeded",
"releaseNotes" to "* Add rolling average setting for power triggers",
"screenshotUrls" to listOf(
"https://github.com/timklge/karoo-reminder/releases/latest/download/reminder.png",
"https://github.com/timklge/karoo-reminder/releases/latest/download/list.png",
"https://github.com/timklge/karoo-reminder/releases/latest/download/detail.png",
)
)
val gson = groovy.json.JsonBuilder(manifest).toPrettyString()

View File

@ -1,5 +1,11 @@
package de.timklge.karooreminder
import android.content.Context
import android.util.Log
import de.timklge.karooreminder.KarooReminderExtension.Companion.TAG
import de.timklge.karooreminder.screens.Reminder
import de.timklge.karooreminder.screens.defaultReminders
import de.timklge.karooreminder.screens.preferencesKey
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.OnStreamState
import io.hammerhead.karooext.models.RideState
@ -9,6 +15,8 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json
fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
return callbackFlow {
@ -42,3 +50,16 @@ fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
}
}
}
fun Context.streamPreferences(): Flow<MutableList<Reminder>> {
return applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
}
}
}

View File

@ -5,14 +5,13 @@ 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
import io.hammerhead.karooext.models.HardwareType
import io.hammerhead.karooext.models.InRideAlert
import io.hammerhead.karooext.models.PlayBeepPattern
import io.hammerhead.karooext.models.RideState
import io.hammerhead.karooext.models.StreamState
import io.hammerhead.karooext.models.TurnScreenOn
import io.hammerhead.karooext.models.UserProfile
@ -33,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"),
@ -47,14 +57,27 @@ enum class ReminderTrigger(val id: String, val label: String) {
SPEED_LIMIT_MAXIMUM_EXCEEDED("speed_limit_max", "Speed above value"),
SPEED_LIMIT_MINIMUM_EXCEEDED("speed_limit_min", "Speed below value"),
CADENCE_LIMIT_MAXIMUM_EXCEEDED("cadence_limit_max", "Cadence above value"),
CADENCE_LIMIT_MINIMUM_EXCEEDED("cadence_limit_min", "Cadence below value");
CADENCE_LIMIT_MINIMUM_EXCEEDED("cadence_limit_min", "Cadence below value"),
AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED("ambient_temperature_limit_max", "Ambient temperature above value"),
AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED("ambient_temperature_limit_min", "Ambient temperature below value"),
GRADIENT_LIMIT_MAXIMUM_EXCEEDED("gradient_limit_max", "Gradient above value"),
GRADIENT_LIMIT_MINIMUM_EXCEEDED("gradient_limit_min", "Gradient below value"),
CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED("core_temperature_limit_max", "Core temperature above value"),
CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED("core_temperature_limit_min", "Core temperature below value"),
FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED("front_tire_pressure_limit_max", "Front tire pressure above value"),
FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED("front_tire_pressure_limit_min", "Front tire pressure below value"),
REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED("rear_tire_pressure_limit_max", "Rear tire pressure above value"),
REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED("rear_tire_pressure_limit_min", "Rear tire pressure below value");
fun getPrefix(): String {
return when (this) {
HR_LIMIT_MINIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "<"
HR_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MAXIMUM_EXCEEDED -> ">"
ELAPSED_TIME, DISTANCE -> ""
ENERGY_OUTPUT -> ""
HR_LIMIT_MINIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED, CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, GRADIENT_LIMIT_MINIMUM_EXCEEDED -> "<"
HR_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MAXIMUM_EXCEEDED, CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, GRADIENT_LIMIT_MAXIMUM_EXCEEDED -> ">"
ELAPSED_TIME, DISTANCE, ENERGY_OUTPUT -> ""
}
}
@ -66,7 +89,55 @@ enum class ReminderTrigger(val id: String, val label: String) {
POWER_LIMIT_MAXIMUM_EXCEEDED, POWER_LIMIT_MINIMUM_EXCEEDED -> "W"
SPEED_LIMIT_MAXIMUM_EXCEEDED, SPEED_LIMIT_MINIMUM_EXCEEDED -> if(imperial) "mph" else "km/h"
CADENCE_LIMIT_MAXIMUM_EXCEEDED, CADENCE_LIMIT_MINIMUM_EXCEEDED -> "rpm"
CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> if(imperial) "°F" else "°C"
FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> if(imperial) "psi" else "bar"
ENERGY_OUTPUT -> "kJ"
GRADIENT_LIMIT_MAXIMUM_EXCEEDED, GRADIENT_LIMIT_MINIMUM_EXCEEDED -> "%"
}
}
fun isDecimalValue() = this == CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED || this == CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED ||
this == FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED || this == FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED ||
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")
}
}
}
@ -96,8 +167,6 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
private lateinit var karooSystem: KarooSystemService
private var jobs: MutableSet<Job> = mutableSetOf()
data class DisplayedReminder(val beepPattern: ReminderBeepPattern, val trigger: ReminderTrigger, val alert: InRideAlert)
private var reminderChannel = Channel<DisplayedReminder>(2, BufferOverflow.DROP_OLDEST)
@ -140,6 +209,9 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
}
private lateinit var receiveJob: Job
private lateinit var triggerStreamJob: Job
override fun onCreate() {
super.onCreate()
@ -147,10 +219,9 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
karooSystem = KarooSystemService(applicationContext)
val receiveJob = CoroutineScope(Dispatchers.IO).launch {
receiveJob = CoroutineScope(Dispatchers.IO).launch {
receiverWorker()
}
jobs.add(receiveJob)
karooSystem.connect { connected ->
if (connected) {
@ -158,89 +229,105 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
}
val distanceJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
val triggerJobs = mutableSetOf<Job>()
triggerStreamJob = CoroutineScope(Dispatchers.IO).launch {
streamPreferences().collect { reminders ->
triggerJobs.forEach { it.cancel() }
triggerJobs.clear()
if (reminders.any { it.trigger == ReminderTrigger.DISTANCE }){
val distanceJob = startIntervalJob(ReminderTrigger.DISTANCE) {
karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile }
.map { (distance, profile) ->
if (profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL){
(distance / 1609.344).toInt()
} else {
(distance / 1000.0).toInt()
}
}
.distinctUntilChanged()
.filterNot { it == 0 }
}
triggerJobs.add(distanceJob)
}
if (reminders.any { it.trigger == ReminderTrigger.ELAPSED_TIME }){
val elapsedTimeJob = startIntervalJob(ReminderTrigger.ELAPSED_TIME) {
karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { (it / 1000 / 60).toInt() }
.distinctUntilChanged()
.filterNot { it == 0 }
}
triggerJobs.add(elapsedTimeJob)
}
if (reminders.any { it.trigger == ReminderTrigger.ENERGY_OUTPUT }){
val energyOutputJob = startIntervalJob(ReminderTrigger.ENERGY_OUTPUT) {
karooSystem.streamDataFlow(DataType.Type.ENERGY_OUTPUT)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { it.toInt() }
.distinctUntilChanged()
.filterNot { it == 0 }
.allIntermediateInts()
}
triggerJobs.add(energyOutputJob)
}
val intervalTriggers = setOf(
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED
)
intervalTriggers.forEach { trigger ->
SmoothSetting.entries.forEach { smoothSetting ->
if (reminders.any { it.trigger == trigger && it.smoothSetting == smoothSetting }){
val job = startRangeExceededJob(trigger, smoothSetting)
triggerJobs.add(job)
}
}
}
}
karooSystem.streamDataFlow(DataType.Type.DISTANCE)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.combine(karooSystem.streamUserProfile()) { distance, profile -> distance to profile }
.map { (distance, profile) ->
if (profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL){
(distance / 1609.344).toInt()
} else {
(distance / 1000.0).toInt()
}
}
.distinctUntilChanged()
.filterNot { it == 0 }
.combine(preferences) { distance, reminders -> distance to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (distance, reminders) ->
val rs = reminders
.filter { reminder -> reminder.trigger == ReminderTrigger.DISTANCE && reminder.isActive && distance % reminder.interval == 0 }
for (reminder in rs){
Log.i(TAG, "Distance reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.DISTANCE, InRideAlert(
id = "reminder-${reminder.id}-${distance}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.timer,
textColor = reminder.displayForegroundColor?.getTextColor() ?: R.color.black,
backgroundColor = reminder.displayForegroundColor?.colorRes ?: R.color.hRed
)))
}
}
}
jobs.add(distanceJob)
}
jobs.addAll(listOf(
startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED),
startRangeExceededJob(ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED)
))
private fun startIntervalJob(trigger: ReminderTrigger, flow: () -> Flow<Int>): Job {
return CoroutineScope(Dispatchers.IO).launch {
val preferences = streamPreferences()
val elapsedTimeJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
}
}
karooSystem.streamDataFlow(DataType.Type.ELAPSED_TIME)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { (it / 1000 / 60).toInt() }
.distinctUntilChanged()
flow()
.filterNot { it == 0 }
.combine(preferences) { elapsedMinutes, reminders -> elapsedMinutes to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (elapsedMinutes, reminders) ->
val rs = reminders
.filter { reminder -> reminder.trigger == ReminderTrigger.ELAPSED_TIME && reminder.isActive && elapsedMinutes % reminder.interval == 0 }
.filter { reminder ->
val interval = reminder.interval
reminder.trigger == trigger && reminder.isActive && interval != null && elapsedMinutes % interval == 0
}
for (reminder in rs){
Log.i(TAG, "Elapsed time reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.ELAPSED_TIME, InRideAlert(
Log.i(TAG, "$trigger reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, trigger, InRideAlert(
id = "reminder-${reminder.id}-${elapsedMinutes}",
detail = reminder.text,
title = reminder.name,
@ -252,96 +339,97 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
}
}
jobs.add(elapsedTimeJob)
val energyOutputJob = CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch(e: Throwable){
Log.e(TAG,"Failed to read preferences", e)
mutableListOf()
}
}
karooSystem.streamDataFlow(DataType.Type.ENERGY_OUTPUT)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.map { it.toInt() }
.distinctUntilChanged()
.filterNot { it == 0 }
.allIntermediateInts()
.combine(preferences) { energyOutput, reminders -> energyOutput to reminders}
.distinctUntilChanged { old, new -> old.first == new.first }
.collectLatest { (energyOutput, reminders) ->
val rs = reminders
.filter { reminder -> reminder.trigger == ReminderTrigger.ENERGY_OUTPUT && reminder.isActive && energyOutput % reminder.interval == 0 }
for (reminder in rs){
Log.i(TAG, "Energy output reminder: ${reminder.name}")
reminderChannel.send(DisplayedReminder(reminder.tone, ReminderTrigger.ENERGY_OUTPUT, InRideAlert(
id = "reminder-${reminder.id}-${energyOutput}",
detail = reminder.text,
title = reminder.name,
autoDismissMs = if(reminder.isAutoDismiss) reminder.autoDismissSeconds * 1000L else null,
icon = R.drawable.timer,
textColor = reminder.displayForegroundColor?.getTextColor() ?: R.color.black,
backgroundColor = reminder.displayForegroundColor?.colorRes ?: R.color.hRed
)))
}
}
}
jobs.add(energyOutputJob)
}
data class StreamData(val value: Double, val reminders: MutableList<Reminder>? = null, val imperial: Boolean = false)
private fun startRangeExceededJob(triggerType: ReminderTrigger): Job {
private fun startRangeExceededJob(triggerType: ReminderTrigger, smoothSetting: SmoothSetting): Job {
return CoroutineScope(Dispatchers.IO).launch {
val preferences = applicationContext.dataStore.data.map { remindersJson ->
try {
Json.decodeFromString<MutableList<Reminder>>(
remindersJson[preferencesKey] ?: defaultReminders
)
} catch (e: Throwable) {
Log.e(TAG, "Failed to read preferences", e)
mutableListOf()
val preferences = streamPreferences()
Log.i(TAG, "Starting range exceeded job for trigger $triggerType with smooth setting $smoothSetting")
val valueStream = karooSystem.streamDataFlow(triggerType.getSmoothedDataType(smoothSetting))
.mapNotNull {
val dataPoint = (it as? StreamState.Streaming)?.dataPoint
@Suppress("KotlinConstantConditions")
when (triggerType) {
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> dataPoint?.values?.get(DataType.Field.CORE_TEMP)
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> dataPoint?.values?.get(DataType.Field.TIRE_PRESSURE)
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT,
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> dataPoint?.singleValue
}
}
}
.filter { it != 0.0 }
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.DISTANCE, ReminderTrigger.ELAPSED_TIME, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
data class StreamData(val value: Double, val reminders: MutableList<Reminder>, val distanceImperial: Boolean, val temperatureImperial: Boolean, val rideState: RideState)
karooSystem.streamDataFlow(dataType)
.mapNotNull { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.filter { it > 0.0 }
.combine(preferences) { value, reminders -> StreamData(value, reminders) }
.combine(karooSystem.streamUserProfile()) { streamData, profile -> streamData.copy(imperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL) }
.onlyIfNValuesReceivedWithinTimeframe(5, 1000 * 10) // At least 5 values have been received over the last 10 seconds
.map { (value, reminders, imperial) ->
val triggered = reminders?.filter { reminder ->
val isSpeedTrigger = triggerType == ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED || triggerType == ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED
val reminderValue = if (isSpeedTrigger){
// Convert m/s speed to km/h or mph
if (imperial) reminder.interval * 0.44704 else reminder.interval * 0.277778
} else {
reminder.interval.toDouble()
combine(valueStream, preferences, karooSystem.streamUserProfile(), karooSystem.streamRideState()) { value, reminders, profile, rideState ->
StreamData(distanceImperial = profile.preferredUnit.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, temperatureImperial = profile.preferredUnit.temperature == UserProfile.PreferredUnit.UnitType.IMPERIAL,
value = value, reminders = reminders, rideState = rideState)
}.filter {
it.rideState is RideState.Recording
}.let {
@Suppress("KotlinConstantConditions")
when (triggerType){
// Tire pressure, gradient and temperature triggers do not require ongoing measurements, as measurement rate is unknown
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> it
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> it.onlyIfNValuesReceivedWithinTimeframe(5, 1000 * 10) // At least 5 values have been received over the last 10 seconds
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
}
.map { (actualValue, reminders, distanceImperial, temperatureImperial) ->
val triggered = reminders.filter { reminder ->
val triggerThreshold = when(triggerType) {
ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> {
if (distanceImperial) reminder.interval?.times(0.44704) else reminder.interval?.times(0.277778)
}
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> {
if (temperatureImperial) (reminder.intervalFloat?.minus(32))?.div(1.8) else reminder.intervalFloat
}
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED -> {
if(distanceImperial) reminder.intervalFloat?.times(68.9476) /* psi to mbar */ else reminder.intervalFloat?.times(1000.0) /* bar to mbar */
}
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE,
ReminderTrigger.ENERGY_OUTPUT, ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> reminder.interval?.toDouble()
}
val triggerIsMet = when (triggerType){
ReminderTrigger.HR_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED -> value > reminderValue
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED -> triggerThreshold != null && actualValue > triggerThreshold
ReminderTrigger.HR_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.POWER_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED -> value < reminderValue
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.SPEED_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED, ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED,
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> triggerThreshold != null && actualValue < triggerThreshold
else -> error("Unsupported trigger type: $triggerType")
ReminderTrigger.ELAPSED_TIME, ReminderTrigger.DISTANCE, ReminderTrigger.ENERGY_OUTPUT -> error("Unsupported trigger type: $triggerType")
}
reminder.isActive && reminder.trigger == triggerType && triggerIsMet
@ -352,10 +440,12 @@ 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 ->
Log.i(TAG, "Triggered range reminder: ${reminders.size} reminders")
reminders.forEach { reminder ->
Log.d(TAG, "Dispatching reminder: ${reminder.name}")
reminderChannel.send(
DisplayedReminder(
reminder.tone, triggerType, InRideAlert(
@ -375,8 +465,8 @@ class KarooReminderExtension : KarooExtension("karoo-reminder", BuildConfig.VERS
}
override fun onDestroy() {
jobs.forEach { job -> job.cancel() }
jobs.clear()
receiveJob.cancel()
triggerStreamJob.cancel()
karooSystem.disconnect()
super.onDestroy()

View File

@ -2,15 +2,21 @@ package de.timklge.karooreminder
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import de.timklge.karooreminder.screens.ReminderAppNavHost
import de.timklge.karooreminder.theme.AppTheme
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings", corruptionHandler = ReplaceFileCorruptionHandler {
Log.w(KarooReminderExtension.TAG, "Error reading settings, using default values")
emptyPreferences()
})
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -36,6 +36,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -61,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
@ -75,28 +77,47 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
LaunchedEffect(Unit) {
karooSystem.connect{}
}
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
var title by remember { mutableStateOf(reminder.name) }
var text by remember { mutableStateOf(reminder.text) }
var selectedColor by remember { mutableStateOf(reminder.displayForegroundColor) }
val colorDialogState by remember { mutableStateOf(UseCaseState()) }
var duration by remember { mutableStateOf(reminder.interval.toString()) }
var duration by remember {
mutableStateOf(if (reminder.intervalFloat != null){
java.text.DecimalFormat("#.##").format(reminder.intervalFloat)
} else {
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) }
val profile by karooSystem.streamUserProfile().collectAsStateWithLifecycle(null)
fun getUpdatedReminder(): Reminder = Reminder(reminder.id, title, duration.toIntOrNull() ?: 1,
text = text,
displayForegroundColor = selectedColor,
isActive = isActive,
trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
fun getUpdatedReminder(): Reminder {
val durationString = duration.replace(",", ".")
return Reminder(reminder.id, title, interval = durationString.toDoubleOrNull()?.toInt() ?: 1,
intervalFloat = if (selectedTrigger.isDecimalValue()) durationString.toDoubleOrNull() else null,
text = text,
displayForegroundColor = selectedColor,
isActive = isActive,
smoothSetting = smoothSetting,
trigger = selectedTrigger,
isAutoDismiss = autoDismiss, tone = selectedTone, autoDismissSeconds = autoDismissSeconds.toIntOrNull() ?: 15)
}
Column(modifier = Modifier
.fillMaxSize()
@ -136,15 +157,37 @@ fun DetailScreen(isCreating: Boolean, reminder: Reminder, onSubmit: (updatedRemi
ReminderTrigger.CADENCE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum cadence")
ReminderTrigger.CADENCE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum cadence")
ReminderTrigger.ENERGY_OUTPUT -> Text("Energy Output")
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum core temp")
ReminderTrigger.CORE_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum core temp")
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Max front tire pressure")
ReminderTrigger.FRONT_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> Text("Min front tire pressure")
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Max rear tire pressure")
ReminderTrigger.REAR_TIRE_PRESSURE_LIMIT_MINIMUM_EXCEEDED -> Text("Min rear tire pressure")
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum temp")
ReminderTrigger.AMBIENT_TEMPERATURE_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum temp")
ReminderTrigger.GRADIENT_LIMIT_MAXIMUM_EXCEEDED -> Text("Maximum gradient")
ReminderTrigger.GRADIENT_LIMIT_MINIMUM_EXCEEDED -> Text("Minimum gradient")
}
},
suffix = {
Text(selectedTrigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL))
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
keyboardOptions = KeyboardOptions(keyboardType = if (selectedTrigger.isDecimalValue()) KeyboardType.Decimal else KeyboardType.Number),
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(
@ -311,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

@ -33,6 +33,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
@ -47,6 +48,7 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.datastore.preferences.core.edit
@ -134,7 +136,7 @@ fun ReminderAppNavHost(modifier: Modifier = Modifier, navController: NavHostCont
}
composable(route = "create") {
val nextReminderId = reminders.maxOfOrNull { it.id + 1 } ?: 0
val newReminder = Reminder(nextReminderId, "", 30, "")
val newReminder = Reminder(nextReminderId, "", 30, text = "")
DetailScreen(true, newReminder, { updatedReminder ->
updatedReminder?.let { r ->
@ -169,6 +171,18 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
showWarnings = true
}
LaunchedEffect(Unit) {
karooSystem.connect { connected ->
karooConnected = connected
}
}
DisposableEffect(Unit) {
onDispose {
karooSystem.disconnect()
}
}
Scaffold(
topBar = { TopAppBar(title = {Text("Reminder")}) },
content = {
@ -199,21 +213,14 @@ fun MainScreen(reminders: MutableList<Reminder>, onNavigateToReminder: (r: Remin
Spacer(modifier = Modifier.width(10.dp))
Text(reminder.name)
Text(modifier = Modifier.weight(1.0f), text = reminder.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
Spacer(Modifier.weight(1.0f))
Text("${reminder.trigger.getPrefix()}${reminder.interval}${reminder.trigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)}")
val value = if (reminder.trigger.isDecimalValue()) java.text.DecimalFormat("#.##").format(reminder.intervalFloat) else reminder.interval
Text("${reminder.trigger.getPrefix()}${value}${reminder.trigger.getSuffix(profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL)}")
}
}
}
LaunchedEffect(Unit) {
karooSystem.connect { connected ->
karooConnected = connected
}
}
if (showWarnings){
if (reminders.isEmpty()) Text(modifier = Modifier.padding(5.dp), text = "No reminders added.")

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
@ -126,7 +127,14 @@ enum class ReminderColor(@ColorRes val colorRes: Int, val whiteFont: Boolean, va
}
@Serializable
class Reminder(val id: Int, var name: String, var interval: Int, var text: String,
class Reminder(val id: Int, var name: String,
/** Trigger value used by all triggers except temperature, gradient, tire pressure */
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")
var foregroundColor: Int = android.graphics.Color.parseColor("#FF6060"),
@ -135,4 +143,4 @@ class Reminder(val id: Int, var name: String, var interval: Int, var text: Strin
var trigger: ReminderTrigger = ReminderTrigger.ELAPSED_TIME,
val autoDismissSeconds: Int = 15)
val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, "Take a sip!")))
val defaultReminders = Json.encodeToString(listOf(Reminder(0, "Drink", 30, text = "Take a sip!")))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
list.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 26 KiB