diff options
Diffstat (limited to 'android-app/app/src/main/kotlin/org/terst')
5 files changed, 255 insertions, 3 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/BarometerData.kt b/android-app/app/src/main/kotlin/org/terst/nav/BarometerData.kt new file mode 100644 index 0000000..5a8ccce --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/BarometerData.kt @@ -0,0 +1,42 @@ +package org.terst.nav + +import java.util.Locale + +data class BarometerReading( + val pressureHpa: Float, + val timestamp: Long = System.currentTimeMillis() +) + +enum class PressureTrend { + RISING_FAST, + RISING, + STEADY, + FALLING, + FALLING_FAST; + + override fun toString(): String { + return when (this) { + RISING_FAST -> "Rising Fast" + RISING -> "Rising" + STEADY -> "Steady" + FALLING -> "Falling" + FALLING_FAST -> "Falling Fast" + } + } +} + +data class BarometerStatus( + val currentPressureHpa: Float = 1013.25f, + val trend: PressureTrend = PressureTrend.STEADY, + val pressureChange3h: Float = 0f, + val history: List<BarometerReading> = emptyList() +) { + fun formatPressure(): String { + return String.format(Locale.getDefault(), "%.1f hPa", currentPressureHpa) + } + + fun formatTrend(): String { + val sign = if (pressureChange3h >= 0) "+" else "" + return String.format(Locale.getDefault(), "%s (%s%.1f hPa/3h)", trend.toString(), sign, pressureChange3h) + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/BarometerSensorManager.kt b/android-app/app/src/main/kotlin/org/terst/nav/BarometerSensorManager.kt new file mode 100644 index 0000000..cdd7f76 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/BarometerSensorManager.kt @@ -0,0 +1,99 @@ +package org.terst.nav + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.util.concurrent.TimeUnit +import android.util.Log + +class BarometerSensorManager(context: Context) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val pressureSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE) + + private val _barometerStatus = MutableStateFlow(BarometerStatus()) + val barometerStatus: StateFlow<BarometerStatus> = _barometerStatus.asStateFlow() + + private val historyMaxDurationMs = TimeUnit.HOURS.toMillis(24) // Keep 24h history + private val historySampleIntervalMs = TimeUnit.MINUTES.toMillis(15) // Sample every 15 min for history + private var lastHistorySampleTime = 0L + + fun start() { + if (pressureSensor != null) { + sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_NORMAL) + Log.d("BarometerManager", "Pressure sensor registered") + } else { + Log.w("BarometerManager", "No pressure sensor found on this device") + } + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("BarometerManager", "Pressure sensor unregistered") + } + + override fun onSensorChanged(event: SensorEvent) { + if (event.sensor.type == Sensor.TYPE_PRESSURE) { + val pressure = event.values[0] + updateCurrentPressure(pressure) + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used + } + + private fun updateCurrentPressure(pressure: Float) { + val now = System.currentTimeMillis() + + _barometerStatus.update { currentStatus -> + val isFirstSample = currentStatus.history.isEmpty() + val newHistory = if (isFirstSample || now - lastHistorySampleTime >= historySampleIntervalMs) { + lastHistorySampleTime = now + val updatedHistory = currentStatus.history + BarometerReading(pressure, now) + // Trim history to 24h + updatedHistory.filter { now - it.timestamp <= historyMaxDurationMs } + } else { + currentStatus.history + } + + val change3h = calculatePressureChange(newHistory, now, TimeUnit.HOURS.toMillis(3)) + val trend = determineTrend(change3h) + + currentStatus.copy( + currentPressureHpa = pressure, + trend = trend, + pressureChange3h = change3h, + history = newHistory + ) + } + } + + private fun calculatePressureChange(history: List<BarometerReading>, now: Long, durationMs: Long): Float { + if (history.isEmpty()) return 0f + + val targetTime = now - durationMs + val oldReading = history.find { it.timestamp >= targetTime } ?: history.first() + val currentReading = history.last() + + // If we don't have enough history, we might not be able to calculate a meaningful 3h change + // but we'll return the difference between the oldest available and current. + return currentReading.pressureHpa - oldReading.pressureHpa + } + + private fun determineTrend(change3h: Float): PressureTrend { + return when { + change3h >= 2.0f -> PressureTrend.RISING_FAST + change3h >= 0.5f -> PressureTrend.RISING + change3h <= -2.0f -> PressureTrend.FALLING_FAST + change3h <= -0.5f -> PressureTrend.FALLING + else -> PressureTrend.STEADY + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/BarometerTrendView.kt b/android-app/app/src/main/kotlin/org/terst/nav/BarometerTrendView.kt new file mode 100644 index 0000000..944d198 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/BarometerTrendView.kt @@ -0,0 +1,72 @@ +package org.terst.nav + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat + +class BarometerTrendView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var history: List<BarometerReading> = emptyList() + + private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = ContextCompat.getColor(context, R.color.instrument_text_normal) + strokeWidth = 4f + style = Paint.Style.STROKE + strokeCap = Paint.Cap.ROUND + strokeJoin = Paint.Join.ROUND + } + + private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = ContextCompat.getColor(context, R.color.instrument_text_secondary) + strokeWidth = 1f + style = Paint.Style.STROKE + } + + fun setHistory(newHistory: List<BarometerReading>) { + history = newHistory + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (history.size < 2) return + + val padding = 20f + val w = width.toFloat() - 2 * padding + val h = height.toFloat() - 2 * padding + + val minP = history.minOf { it.pressureHpa } + val maxP = history.maxOf { it.pressureHpa } + val rangeP = (maxP - minP).coerceAtLeast(1.0f) // Show at least 1 hPa range + + val minT = history.first().timestamp + val maxT = history.last().timestamp + val rangeT = (maxT - minT).coerceAtLeast(1L) + + // Draw simple grid + canvas.drawLine(padding, padding, padding, h + padding, gridPaint) + canvas.drawLine(padding, h + padding, w + padding, h + padding, gridPaint) + + val path = Path() + history.forEachIndexed { index, reading -> + val x = padding + (reading.timestamp - minT).toFloat() / rangeT * w + val y = padding + h - (reading.pressureHpa - minP) / rangeP * h + + if (index == 0) { + path.moveTo(x, y) + } else { + path.lineTo(x, y) + } + } + + canvas.drawPath(path, linePaint) + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt index 22290a5..24eb498 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt @@ -47,6 +47,7 @@ class LocationService : Service() { private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var locationCallback: LocationCallback private lateinit var anchorAlarmManager: AnchorAlarmManager + private lateinit var barometerSensorManager: BarometerSensorManager private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val NOTIFICATION_CHANNEL_ID = "location_service_channel" @@ -59,8 +60,16 @@ class LocationService : Service() { Log.d("LocationService", "Service created") fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) anchorAlarmManager = AnchorAlarmManager(this) // Initialize with service context + barometerSensorManager = BarometerSensorManager(this) createNotificationChannel() + // Observe barometer status and update our public state + serviceScope.launch { + barometerSensorManager.barometerStatus.collect { status -> + _barometerStatus.value = status + } + } + // Mock tidal current data generator serviceScope.launch { while (true) { @@ -121,10 +130,12 @@ class LocationService : Service() { Log.d("LocationService", "Starting foreground service") startForeground(NOTIFICATION_ID, createNotification()) startLocationUpdatesInternal() + barometerSensorManager.start() } ACTION_STOP_FOREGROUND_SERVICE -> { Log.d("LocationService", "Stopping foreground service") stopLocationUpdatesInternal() + barometerSensorManager.stop() stopSelf() } ACTION_START_ANCHOR_WATCH -> { @@ -158,6 +169,7 @@ class LocationService : Service() { Log.d("LocationService", "Service destroyed") stopLocationUpdatesInternal() anchorAlarmManager.stopAlarm() + barometerSensorManager.stop() _anchorWatchState.value = AnchorWatchState(isActive = false) isAlarmTriggered = false // Reset alarm trigger state serviceScope.cancel() // Cancel the coroutine scope @@ -284,9 +296,12 @@ class LocationService : Service() { get() = _anchorWatchState val tidalCurrentState: StateFlow<TidalCurrentState> get() = _tidalCurrentState + val barometerStatus: StateFlow<BarometerStatus> + get() = _barometerStatus private val _locationFlow = MutableSharedFlow<GpsData>(replay = 1) private val _anchorWatchState = MutableStateFlow(AnchorWatchState()) private val _tidalCurrentState = MutableStateFlow(TidalCurrentState()) + private val _barometerStatus = MutableStateFlow(BarometerStatus()) } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index b638136..e208892 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -104,6 +104,9 @@ class MainActivity : AppCompatActivity() { private lateinit var valueVmg: TextView private lateinit var valueDepth: TextView private lateinit var valuePolarPct: TextView + private lateinit var valueBaro: TextView + private lateinit var labelTrend: TextView + private lateinit var barometerTrendView: BarometerTrendView private lateinit var polarDiagramView: PolarDiagramView // Reference to the custom view // Anchor Watch UI elements @@ -134,6 +137,7 @@ class MainActivity : AppCompatActivity() { startLocationService() observeLocationUpdates() // Start observing location updates observeAnchorWatchState() // Start observing anchor watch state + observeBarometerStatus() // Start observing barometer status } else { // Permissions denied, handle the case (e.g., show a message to the user) Toast.makeText(this, "Location permissions denied", Toast.LENGTH_LONG).show() @@ -167,6 +171,7 @@ class MainActivity : AppCompatActivity() { startLocationService() observeLocationUpdates() // Start observing location updates observeAnchorWatchState() // Start observing anchor watch state + observeBarometerStatus() // Start observing barometer status } mapView = findViewById<MapView>(R.id.mapView) @@ -201,6 +206,9 @@ class MainActivity : AppCompatActivity() { valueVmg = findViewById(R.id.value_vmg) valueDepth = findViewById(R.id.value_depth) valuePolarPct = findViewById(R.id.value_polar_pct) + valueBaro = findViewById(R.id.value_baro) + labelTrend = findViewById(R.id.label_trend) + barometerTrendView = findViewById(R.id.barometer_trend_view) // Initialize PolarDiagramView polarDiagramView = findViewById(R.id.polar_diagram_view) @@ -226,7 +234,8 @@ class MainActivity : AppCompatActivity() { sog = "%.1f".format(Locale.getDefault(), simulatedBsp * 0.95), // SOG usually slightly less than BSP vmg = "%.1f".format(Locale.getDefault(), mockPolarTable.curves.firstOrNull { it.twS == simulatedTws }?.calculateVmg(simulatedTwa, simulatedBsp) ?: 0.0), depth = getString(R.string.placeholder_depth_value), - polarPct = "%.0f%%".format(Locale.getDefault(), mockPolarTable.calculatePolarPercentage(simulatedTws, simulatedTwa, simulatedBsp)) + polarPct = "%.0f%%".format(Locale.getDefault(), mockPolarTable.calculatePolarPercentage(simulatedTws, simulatedTwa, simulatedBsp)), + baro = getString(R.string.placeholder_baro_value) ) polarDiagramView.setCurrentPerformance(simulatedTws, simulatedTwa, simulatedBsp) @@ -259,7 +268,8 @@ class MainActivity : AppCompatActivity() { sog = getString(R.string.placeholder_sog_value), vmg = getString(R.string.placeholder_vmg_value), depth = getString(R.string.placeholder_depth_value), - polarPct = getString(R.string.placeholder_polar_value) + polarPct = getString(R.string.placeholder_polar_value), + baro = getString(R.string.placeholder_baro_value) ) fabToggleInstruments.setOnClickListener { @@ -535,6 +545,18 @@ class MainActivity : AppCompatActivity() { } } + private fun observeBarometerStatus() { + lifecycleScope.launch { + LocationService.barometerStatus.collect { status -> + withContext(Dispatchers.Main) { + valueBaro.text = String.format(Locale.getDefault(), "%.1f", status.currentPressureHpa) + labelTrend.text = String.format(Locale.getDefault(), "TREND: %s", status.formatTrend()) + barometerTrendView.setHistory(status.history) + } + } + } + } + private fun observeTidalCurrentState() { lifecycleScope.launch { LocationService.tidalCurrentState.collect { state -> @@ -708,7 +730,8 @@ class MainActivity : AppCompatActivity() { sog: String, vmg: String, depth: String, - polarPct: String + polarPct: String, + baro: String ) { valueAws.text = aws valueTws.text = tws @@ -719,6 +742,7 @@ class MainActivity : AppCompatActivity() { valueVmg.text = vmg valueDepth.text = depth valuePolarPct.text = polarPct + valueBaro.text = baro } override fun onStart() { |
