From a662cc67579ed9cb89437c787ab1852043a9554c Mon Sep 17 00:00:00 2001 From: Tim Kluge Date: Fri, 3 Jan 2025 00:18:42 +0100 Subject: [PATCH] fix #21: Save last known gps position --- .../de/timklge/karooheadwind/Extensions.kt | 79 ++++++++++++++++--- .../karooheadwind/KarooHeadwindExtension.kt | 6 ++ .../de/timklge/karooheadwind/Throttle.kt | 16 ++++ .../karooheadwind/datatypes/GpsCoordinates.kt | 2 +- .../datatypes/WeatherDataType.kt | 7 +- .../datatypes/WeatherForecastDataType.kt | 2 +- 6 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 app/src/main/kotlin/de/timklge/karooheadwind/Throttle.kt diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt b/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt index a3d5f80..0153f5d 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/Extensions.kt @@ -48,9 +48,8 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.timeout @@ -68,6 +67,7 @@ val settingsKey = stringPreferencesKey("settings") val widgetSettingsKey = stringPreferencesKey("widgetSettings") val currentDataKey = stringPreferencesKey("current") val statsKey = stringPreferencesKey("stats") +val lastKnownPositionKey = stringPreferencesKey("lastKnownPosition") suspend fun saveSettings(context: Context, settings: HeadwindSettings) { context.dataStore.edit { t -> @@ -93,6 +93,18 @@ suspend fun saveCurrentData(context: Context, forecast: OpenMeteoCurrentWeatherR } } +suspend fun saveLastKnownPosition(context: Context, gpsCoordinates: GpsCoordinates) { + Log.i(KarooHeadwindExtension.TAG, "Saving last known position: $gpsCoordinates") + + try { + context.dataStore.edit { t -> + t[lastKnownPositionKey] = Json.encodeToString(gpsCoordinates) + } + } catch(e: Throwable){ + Log.e(KarooHeadwindExtension.TAG, "Failed to save last known position", e) + } +} + fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow { return callbackFlow { val listenerId = addConsumer(OnStreamState.StartStreaming(dataTypeId)) { event: OnStreamState -> @@ -194,6 +206,22 @@ fun Context.streamStats(): Flow { }.distinctUntilChanged() } +suspend fun Context.getLastKnownPosition(): GpsCoordinates? { + val settingsJson = dataStore.data.first() + + try { + val lastKnownPositionString = settingsJson[lastKnownPositionKey] ?: return null + val lastKnownPosition = jsonWithUnknownKeys.decodeFromString( + lastKnownPositionString + ) + + return lastKnownPosition + } catch(e: Throwable){ + Log.e(KarooHeadwindExtension.TAG, "Failed to read last known position", e) + return null + } +} + fun KarooSystemService.streamUserProfile(): Flow { return callbackFlow { val listenerId = addConsumer { userProfile: UserProfile -> @@ -268,7 +296,7 @@ sealed class HeadingResponse { fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow { val currentWeatherData = context.streamCurrentWeatherData() - return getHeadingFlow() + return getHeadingFlow(context) .combine(currentWeatherData) { bearing, data -> bearing to data } .map { (bearing, data) -> when { @@ -287,13 +315,12 @@ fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow { +fun KarooSystemService.getHeadingFlow(context: Context): Flow { // return flowOf(HeadingResponse.Value(20.0)) - return streamDataFlow(DataType.Type.LOCATION) - .map { (it as? StreamState.Streaming)?.dataPoint?.values } - .map { values -> - val heading = values?.get(DataType.Field.LOC_BEARING) + return getGpsCoordinateFlow(context) + .map { coords -> + val heading = coords?.bearing Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading") val headingValue = heading?.let { HeadingResponse.Value(it) } @@ -318,24 +345,56 @@ fun KarooSystemService.getHeadingFlow(): Flow { } } +fun concatenate(vararg flows: Flow) = flow { + var hadNullValue = false + + for (flow in flows) { + flow.collect { value -> + if (!hadNullValue) { + emit(value) + if (value == null) hadNullValue = true + } else { + if (value != null) emit(value) + } + } + } +} + +@OptIn(FlowPreview::class) +suspend fun KarooSystemService.updateLastKnownGps(context: Context) { + getGpsCoordinateFlow(context) + .filterNotNull() + .throttle(60 * 1_000) // Only update last known gps position once every minute + .collect { gps -> + saveLastKnownPosition(context, gps) + } +} + @OptIn(FlowPreview::class) fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow { // return flowOf(GpsCoordinates(52.5164069,13.3784)) - return streamDataFlow(DataType.Type.LOCATION) + val initialFlow = flow { context.getLastKnownPosition() } + + val gpsFlow = streamDataFlow(DataType.Type.LOCATION) .map { (it as? StreamState.Streaming)?.dataPoint?.values } .map { values -> val lat = values?.get(DataType.Field.LOC_LATITUDE) val lon = values?.get(DataType.Field.LOC_LONGITUDE) + val bearing = values?.get(DataType.Field.LOC_BEARING) if (lat != null && lon != null){ Log.d(KarooHeadwindExtension.TAG, "Updated gps coordinates: $lat $lon") - GpsCoordinates(lat, lon) + GpsCoordinates(lat, lon, bearing) } else { Log.w(KarooHeadwindExtension.TAG, "Gps unavailable: $values") null } } + + val concatenatedFlow = concatenate(initialFlow, gpsFlow) + + return concatenatedFlow .combine(context.streamSettings(this)) { gps, settings -> gps to settings } .map { (gps, settings) -> val rounded = gps?.round(settings.roundLocationTo.km.toDouble()) diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt index dc029cd..95af250 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt @@ -40,8 +40,10 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.3") { const val TAG = "karoo-headwind" } + lateinit var karooSystem: KarooSystemService + private var updateLastKnownGpsJob: Job? = null private var serviceJob: Job? = null override val types by lazy { @@ -71,6 +73,10 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.3") { karooSystem = KarooSystemService(applicationContext) + updateLastKnownGpsJob = CoroutineScope(Dispatchers.IO).launch { + karooSystem.updateLastKnownGps(this@KarooHeadwindExtension) + } + serviceJob = CoroutineScope(Dispatchers.IO).launch { karooSystem.connect { connected -> if (connected) { diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/Throttle.kt b/app/src/main/kotlin/de/timklge/karooheadwind/Throttle.kt new file mode 100644 index 0000000..1fd88bc --- /dev/null +++ b/app/src/main/kotlin/de/timklge/karooheadwind/Throttle.kt @@ -0,0 +1,16 @@ +package de.timklge.karooheadwind + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +fun Flow.throttle(timeout: Long): Flow = flow { + var lastEmissionTime = 0L + + collect { value -> + val currentTime = System.currentTimeMillis() + if (currentTime - lastEmissionTime >= timeout) { + emit(value) + lastEmissionTime = currentTime + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/GpsCoordinates.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/GpsCoordinates.kt index bcf2bc4..eb1a5c2 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/GpsCoordinates.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/GpsCoordinates.kt @@ -9,7 +9,7 @@ import kotlin.math.sin import kotlin.math.sqrt @Serializable -data class GpsCoordinates(val lat: Double, val lon: Double){ +data class GpsCoordinates(val lat: Double, val lon: Double, val bearing: Double? = 0.0){ companion object { private fun roundDegrees(degrees: Double, km: Double): Double { val nkm = degrees * 111 diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt index 5d735be..1d33083 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt @@ -66,10 +66,9 @@ class WeatherDataType( val currentWeatherData = applicationContext.streamCurrentWeatherData() currentWeatherData - .filterNotNull() .collect { data -> - Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.current.weatherCode}") - emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to data.current.weatherCode.toDouble())))) + Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.current?.weatherCode}") + emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.current?.weatherCode?.toDouble() ?: 0.0))))) } } emitter.setCancellable { @@ -96,7 +95,7 @@ class WeatherDataType( context.streamCurrentWeatherData() .combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) } .combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) } - .combine(karooSystem.getHeadingFlow()) { data, heading -> data.copy(headingResponse = heading) } + .combine(karooSystem.getHeadingFlow(context)) { data, heading -> data.copy(headingResponse = heading) } .collect { (data, settings, userProfile, headingResponse) -> Log.d(KarooHeadwindExtension.TAG, "Updating weather view") diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt index e14668a..cd1e582 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt @@ -126,7 +126,7 @@ class WeatherForecastDataType( .combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) } .combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) } .combine(context.streamWidgetSettings()) { data, widgetSettings -> data.copy(widgetSettings = widgetSettings) } - .combine(karooSystem.getHeadingFlow()) { data, headingResponse -> data.copy(headingResponse = headingResponse) } + .combine(karooSystem.getHeadingFlow(context)) { data, headingResponse -> data.copy(headingResponse = headingResponse) } .collect { (data, settings, widgetSettings, userProfile, headingResponse) -> Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")