fix #21: Save last known gps position

This commit is contained in:
Tim Kluge 2025-01-03 00:18:42 +01:00
parent 407755e94b
commit a662cc6757
6 changed files with 96 additions and 16 deletions

View File

@ -48,9 +48,8 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.timeout import kotlinx.coroutines.flow.timeout
@ -68,6 +67,7 @@ val settingsKey = stringPreferencesKey("settings")
val widgetSettingsKey = stringPreferencesKey("widgetSettings") val widgetSettingsKey = stringPreferencesKey("widgetSettings")
val currentDataKey = stringPreferencesKey("current") val currentDataKey = stringPreferencesKey("current")
val statsKey = stringPreferencesKey("stats") val statsKey = stringPreferencesKey("stats")
val lastKnownPositionKey = stringPreferencesKey("lastKnownPosition")
suspend fun saveSettings(context: Context, settings: HeadwindSettings) { suspend fun saveSettings(context: Context, settings: HeadwindSettings) {
context.dataStore.edit { t -> 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<StreamState> { fun KarooSystemService.streamDataFlow(dataTypeId: String): Flow<StreamState> {
return callbackFlow { return callbackFlow {
val listenerId = addConsumer(OnStreamState.StartStreaming(dataTypeId)) { event: OnStreamState -> val listenerId = addConsumer(OnStreamState.StartStreaming(dataTypeId)) { event: OnStreamState ->
@ -194,6 +206,22 @@ fun Context.streamStats(): Flow<HeadwindStats> {
}.distinctUntilChanged() }.distinctUntilChanged()
} }
suspend fun Context.getLastKnownPosition(): GpsCoordinates? {
val settingsJson = dataStore.data.first()
try {
val lastKnownPositionString = settingsJson[lastKnownPositionKey] ?: return null
val lastKnownPosition = jsonWithUnknownKeys.decodeFromString<GpsCoordinates>(
lastKnownPositionString
)
return lastKnownPosition
} catch(e: Throwable){
Log.e(KarooHeadwindExtension.TAG, "Failed to read last known position", e)
return null
}
}
fun KarooSystemService.streamUserProfile(): Flow<UserProfile> { fun KarooSystemService.streamUserProfile(): Flow<UserProfile> {
return callbackFlow { return callbackFlow {
val listenerId = addConsumer { userProfile: UserProfile -> val listenerId = addConsumer { userProfile: UserProfile ->
@ -268,7 +296,7 @@ sealed class HeadingResponse {
fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingResponse> { fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingResponse> {
val currentWeatherData = context.streamCurrentWeatherData() val currentWeatherData = context.streamCurrentWeatherData()
return getHeadingFlow() return getHeadingFlow(context)
.combine(currentWeatherData) { bearing, data -> bearing to data } .combine(currentWeatherData) { bearing, data -> bearing to data }
.map { (bearing, data) -> .map { (bearing, data) ->
when { when {
@ -287,13 +315,12 @@ fun KarooSystemService.getRelativeHeadingFlow(context: Context): Flow<HeadingRes
} }
} }
fun KarooSystemService.getHeadingFlow(): Flow<HeadingResponse> { fun KarooSystemService.getHeadingFlow(context: Context): Flow<HeadingResponse> {
// return flowOf(HeadingResponse.Value(20.0)) // return flowOf(HeadingResponse.Value(20.0))
return streamDataFlow(DataType.Type.LOCATION) return getGpsCoordinateFlow(context)
.map { (it as? StreamState.Streaming)?.dataPoint?.values } .map { coords ->
.map { values -> val heading = coords?.bearing
val heading = values?.get(DataType.Field.LOC_BEARING)
Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading") Log.d(KarooHeadwindExtension.TAG, "Updated gps bearing: $heading")
val headingValue = heading?.let { HeadingResponse.Value(it) } val headingValue = heading?.let { HeadingResponse.Value(it) }
@ -318,24 +345,56 @@ fun KarooSystemService.getHeadingFlow(): Flow<HeadingResponse> {
} }
} }
fun <T> concatenate(vararg flows: Flow<T>) = 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) @OptIn(FlowPreview::class)
fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> { fun KarooSystemService.getGpsCoordinateFlow(context: Context): Flow<GpsCoordinates?> {
// return flowOf(GpsCoordinates(52.5164069,13.3784)) // return flowOf(GpsCoordinates(52.5164069,13.3784))
return streamDataFlow(DataType.Type.LOCATION) val initialFlow = flow<GpsCoordinates> { context.getLastKnownPosition() }
val gpsFlow = streamDataFlow(DataType.Type.LOCATION)
.map { (it as? StreamState.Streaming)?.dataPoint?.values } .map { (it as? StreamState.Streaming)?.dataPoint?.values }
.map { values -> .map { values ->
val lat = values?.get(DataType.Field.LOC_LATITUDE) val lat = values?.get(DataType.Field.LOC_LATITUDE)
val lon = values?.get(DataType.Field.LOC_LONGITUDE) val lon = values?.get(DataType.Field.LOC_LONGITUDE)
val bearing = values?.get(DataType.Field.LOC_BEARING)
if (lat != null && lon != null){ if (lat != null && lon != null){
Log.d(KarooHeadwindExtension.TAG, "Updated gps coordinates: $lat $lon") Log.d(KarooHeadwindExtension.TAG, "Updated gps coordinates: $lat $lon")
GpsCoordinates(lat, lon) GpsCoordinates(lat, lon, bearing)
} else { } else {
Log.w(KarooHeadwindExtension.TAG, "Gps unavailable: $values") Log.w(KarooHeadwindExtension.TAG, "Gps unavailable: $values")
null null
} }
} }
val concatenatedFlow = concatenate(initialFlow, gpsFlow)
return concatenatedFlow
.combine(context.streamSettings(this)) { gps, settings -> gps to settings } .combine(context.streamSettings(this)) { gps, settings -> gps to settings }
.map { (gps, settings) -> .map { (gps, settings) ->
val rounded = gps?.round(settings.roundLocationTo.km.toDouble()) val rounded = gps?.round(settings.roundLocationTo.km.toDouble())

View File

@ -40,8 +40,10 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.3") {
const val TAG = "karoo-headwind" const val TAG = "karoo-headwind"
} }
lateinit var karooSystem: KarooSystemService lateinit var karooSystem: KarooSystemService
private var updateLastKnownGpsJob: Job? = null
private var serviceJob: Job? = null private var serviceJob: Job? = null
override val types by lazy { override val types by lazy {
@ -71,6 +73,10 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.1.3") {
karooSystem = KarooSystemService(applicationContext) karooSystem = KarooSystemService(applicationContext)
updateLastKnownGpsJob = CoroutineScope(Dispatchers.IO).launch {
karooSystem.updateLastKnownGps(this@KarooHeadwindExtension)
}
serviceJob = CoroutineScope(Dispatchers.IO).launch { serviceJob = CoroutineScope(Dispatchers.IO).launch {
karooSystem.connect { connected -> karooSystem.connect { connected ->
if (connected) { if (connected) {

View File

@ -0,0 +1,16 @@
package de.timklge.karooheadwind
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
fun<T> Flow<T>.throttle(timeout: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { value ->
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime >= timeout) {
emit(value)
lastEmissionTime = currentTime
}
}
}

View File

@ -9,7 +9,7 @@ import kotlin.math.sin
import kotlin.math.sqrt import kotlin.math.sqrt
@Serializable @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 { companion object {
private fun roundDegrees(degrees: Double, km: Double): Double { private fun roundDegrees(degrees: Double, km: Double): Double {
val nkm = degrees * 111 val nkm = degrees * 111

View File

@ -66,10 +66,9 @@ class WeatherDataType(
val currentWeatherData = applicationContext.streamCurrentWeatherData() val currentWeatherData = applicationContext.streamCurrentWeatherData()
currentWeatherData currentWeatherData
.filterNotNull()
.collect { data -> .collect { data ->
Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data.current.weatherCode}") Log.d(KarooHeadwindExtension.TAG, "Wind code: ${data?.current?.weatherCode}")
emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to data.current.weatherCode.toDouble())))) emitter.onNext(StreamState.Streaming(DataPoint(dataTypeId, mapOf(DataType.Field.SINGLE to (data?.current?.weatherCode?.toDouble() ?: 0.0)))))
} }
} }
emitter.setCancellable { emitter.setCancellable {
@ -96,7 +95,7 @@ class WeatherDataType(
context.streamCurrentWeatherData() context.streamCurrentWeatherData()
.combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) } .combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) }
.combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) } .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) -> .collect { (data, settings, userProfile, headingResponse) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather view") Log.d(KarooHeadwindExtension.TAG, "Updating weather view")

View File

@ -126,7 +126,7 @@ class WeatherForecastDataType(
.combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) } .combine(context.streamSettings(karooSystem)) { data, settings -> StreamData(data, settings) }
.combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) } .combine(karooSystem.streamUserProfile()) { data, profile -> data.copy(profile = profile) }
.combine(context.streamWidgetSettings()) { data, widgetSettings -> data.copy(widgetSettings = widgetSettings) } .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) -> .collect { (data, settings, widgetSettings, userProfile, headingResponse) ->
Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view") Log.d(KarooHeadwindExtension.TAG, "Updating weather forecast view")