diff options
Diffstat (limited to 'android-app/app/src/main')
| -rw-r--r-- | android-app/app/src/main/kotlin/com/example/androidapp/gps/LocationService.kt | 87 |
1 files changed, 85 insertions, 2 deletions
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/gps/LocationService.kt b/android-app/app/src/main/kotlin/com/example/androidapp/gps/LocationService.kt index c6ff8b7..28dfc90 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/gps/LocationService.kt +++ b/android-app/app/src/main/kotlin/com/example/androidapp/gps/LocationService.kt @@ -8,17 +8,37 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +/** Source of the currently active GPS fix. */ +enum class GpsSource { NONE, NMEA, ANDROID } + /** * Aggregates real-time location and environmental sensor data for use throughout * the safety subsystem (Section 4.6 of COMPONENT_DESIGN.md). * - * Call [updateSensorData] whenever new NMEA or Signal K data arrives and + * ## GPS sensor fusion + * The service accepts fixes from two independent sources: + * - **NMEA GPS** — dedicated marine GPS received via [updateNmeaGps] (higher priority) + * - **Android GPS** — device built-in location via [updateAndroidGps] (fallback) + * + * Selection policy (evaluated on every new fix): + * 1. Prefer NMEA when its most recent fix is no older than [nmeaStalenessThresholdMs]. + * 2. Fall back to Android GPS when NMEA is stale. + * 3. Use stale NMEA only when Android GPS has never provided a fix. + * 4. [bestPosition] is null until at least one source has reported. + * + * Call [updateSensorData] whenever new NMEA or Signal K sensor data arrives and * [updateCurrentConditions] when a fresh marine-forecast response is received. * Use [snapshot] to capture a point-in-time reading at safety-critical moments * such as MOB activation. + * + * @param nmeaStalenessThresholdMs Maximum age (ms) of an NMEA fix before it is + * considered stale and Android GPS is preferred instead. Default: 5 000 ms. + * @param clockMs Injectable clock for unit-testable staleness checks. */ class LocationService( - private val windCalculator: TrueWindCalculator = TrueWindCalculator() + private val windCalculator: TrueWindCalculator = TrueWindCalculator(), + private val nmeaStalenessThresholdMs: Long = 5_000L, + private val clockMs: () -> Long = System::currentTimeMillis ) { private val _latestSensor = MutableStateFlow<SensorData?>(null) @@ -32,6 +52,23 @@ class LocationService( private val _currentSpeedKt = MutableStateFlow<Double?>(null) private val _currentDirectionDeg = MutableStateFlow<Double?>(null) + // ── GPS sensor fusion state ─────────────────────────────────────────────── + + private var lastNmeaPosition: GpsPosition? = null + private var lastAndroidPosition: GpsPosition? = null + + private val _bestPosition = MutableStateFlow<GpsPosition?>(null) + /** + * The best available GPS fix, selected from NMEA and Android sources according + * to the fusion policy described in the class KDoc. Null until at least one + * source reports a fix. + */ + val bestPosition: StateFlow<GpsPosition?> = _bestPosition.asStateFlow() + + private val _activeGpsSource = MutableStateFlow(GpsSource.NONE) + /** The source that produced [bestPosition]. [GpsSource.NONE] before any fix arrives. */ + val activeGpsSource: StateFlow<GpsSource> = _activeGpsSource.asStateFlow() + /** * Ingest a new sensor reading. If the reading carries apparent wind, boat speed, * and heading, true wind is resolved immediately via [TrueWindCalculator] and @@ -54,6 +91,52 @@ class LocationService( } } + // ── GPS source ingestion ────────────────────────────────────────────────── + + /** + * Ingest a new GPS fix from the NMEA source (e.g. a marine chartplotter or + * NMEA multiplexer). Triggers a fusion re-evaluation. + */ + fun updateNmeaGps(position: GpsPosition) { + lastNmeaPosition = position + recomputeBestPosition() + } + + /** + * Ingest a new GPS fix from the Android system location provider. + * Triggers a fusion re-evaluation. + */ + fun updateAndroidGps(position: GpsPosition) { + lastAndroidPosition = position + recomputeBestPosition() + } + + /** + * Selects the best GPS fix and updates [bestPosition] / [activeGpsSource]. + * + * Priority: + * 1. Fresh NMEA (age ≤ [nmeaStalenessThresholdMs]) + * 2. Android GPS (any age) + * 3. Stale NMEA (only if Android has never reported) + */ + private fun recomputeBestPosition() { + val now = clockMs() + val nmea = lastNmeaPosition + val android = lastAndroidPosition + + val nmeaFresh = nmea != null && (now - nmea.timestampMs) <= nmeaStalenessThresholdMs + + val (best, source) = when { + nmeaFresh -> nmea!! to GpsSource.NMEA + android != null -> android to GpsSource.ANDROID + nmea != null -> nmea to GpsSource.NMEA // stale, but only source + else -> null to GpsSource.NONE + } + + _bestPosition.value = best + _activeGpsSource.value = source + } + /** * Update the ocean current conditions from the latest marine-forecast response. * |
