summaryrefslogtreecommitdiff
path: root/android-app/app/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main/kotlin')
-rw-r--r--android-app/app/src/main/kotlin/com/example/androidapp/gps/LocationService.kt87
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.
*