summaryrefslogtreecommitdiff
path: root/android-app
diff options
context:
space:
mode:
Diffstat (limited to 'android-app')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt8
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt68
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt58
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt42
-rw-r--r--android-app/app/src/main/res/layout/sheet_dev_log.xml74
5 files changed, 228 insertions, 22 deletions
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 7a00748..35a0e91 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
@@ -146,10 +146,16 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
hudDepth.text = "—"
applyUnitLabels()
+ NavLogger.i("app", "started")
setupBottomSheet()
setupBottomNavigation()
setupHandlers()
setupMap()
+ // Hidden dev log — long-press the drag handle on the instrument sheet
+ findViewById<View>(R.id.drag_handle).setOnLongClickListener {
+ DevLogSheet().show(supportFragmentManager, "dev_log")
+ true
+ }
fabRecordTrack.setOnClickListener {
if (!viewModel.isRecording.value) viewModel.startTrack()
@@ -482,6 +488,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
hudCog.text = "%.0f°".format(Locale.getDefault(), gpsData.cog)
if (!conditionsLoaded) {
conditionsLoaded = true
+ NavLogger.i("gps", "first fix lat=%.4f lon=%.4f sog=%.1fkt cog=%.0f°"
+ .format(gpsData.latitude, gpsData.longitude, gpsData.sog, gpsData.cog))
viewModel.loadConditions(gpsData.latitude, gpsData.longitude)
}
if (!weatherLoaded) {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt
index 26c5da7..a716051 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/data/repository/WeatherRepository.kt
@@ -7,6 +7,7 @@ import org.terst.nav.data.api.WeatherApiService
import org.terst.nav.data.model.ForecastItem
import org.terst.nav.data.model.MarineConditions
import org.terst.nav.data.model.WindArrow
+import org.terst.nav.ui.NavLogger
class WeatherRepository(
private val weatherApi: WeatherApiService,
@@ -16,8 +17,9 @@ class WeatherRepository(
/**
* Fetch 7-day hourly forecast items for the given position.
*/
- suspend fun fetchForecastItems(lat: Double, lon: Double): Result<List<ForecastItem>> =
- runCatching {
+ suspend fun fetchForecastItems(lat: Double, lon: Double): Result<List<ForecastItem>> {
+ NavLogger.i("forecast", "lat=%.4f lon=%.4f".format(lat, lon))
+ return runCatching {
val weather = weatherApi.getWeatherForecast(lat, lon)
val h = weather.hourly
h.time.indices.map { i ->
@@ -30,7 +32,15 @@ class WeatherRepository(
weatherCode = h.weathercode[i]
)
}
+ }.also { result ->
+ result.onSuccess { items ->
+ val first = items.firstOrNull()
+ NavLogger.i("forecast", "OK ${items.size} items wind[0]=${first?.windKt?.let { "%.1f".format(it) } ?: "—"}kt@${first?.windDirDeg?.toInt() ?: "—"}°")
+ }.onFailure { e ->
+ NavLogger.e("forecast", "${e.javaClass.simpleName}: ${e.message}")
+ }
}
+ }
/**
* Fetch a single WindArrow representing the current conditions at [lat]/[lon].
@@ -52,29 +62,43 @@ class WeatherRepository(
* Fetches current marine conditions (first forecast hour) for [lat]/[lon].
* Ocean current velocity is converted from m/s to knots.
*/
- suspend fun fetchCurrentConditions(lat: Double, lon: Double): Result<MarineConditions> =
- runCatching {
+ suspend fun fetchCurrentConditions(lat: Double, lon: Double): Result<MarineConditions> {
+ NavLogger.i("conditions", "lat=%.4f lon=%.4f".format(lat, lon))
+ return runCatching {
coroutineScope {
- val weatherDeferred = async { weatherApi.getWeatherForecast(lat, lon, forecastDays = 1) }
- val marineDeferred = async { marineApi.getMarineForecast(lat, lon) }
- val weather = weatherDeferred.await()
- val marine = marineDeferred.await()
- val w = weather.hourly
- val m = marine.hourly
- MarineConditions(
- windSpeedKt = w.windspeed10m.firstOrNull(),
- windDirDeg = w.winddirection10m.firstOrNull(),
- tempC = w.temperature2m.firstOrNull(),
- waveHeightM = m.waveHeight.firstOrNull(),
- waveDirDeg = m.waveDirection.firstOrNull(),
- swellHeightM = m.swellWaveHeight.firstOrNull(),
- swellDirDeg = m.swellWaveDirection.firstOrNull(),
- swellPeriodS = m.swellWavePeriod.firstOrNull(),
- currentSpeedKt = m.oceanCurrentVelocity.firstOrNull()?.let { it * 1.94384 },
- currentDirDeg = m.oceanCurrentDirection.firstOrNull()
- )
+ val weatherDeferred = async { weatherApi.getWeatherForecast(lat, lon, forecastDays = 1) }
+ val marineDeferred = async { marineApi.getMarineForecast(lat, lon) }
+ val weather = weatherDeferred.await()
+ val marine = marineDeferred.await()
+ val w = weather.hourly
+ val m = marine.hourly
+ MarineConditions(
+ windSpeedKt = w.windspeed10m.firstOrNull(),
+ windDirDeg = w.winddirection10m.firstOrNull(),
+ tempC = w.temperature2m.firstOrNull(),
+ waveHeightM = m.waveHeight.firstOrNull(),
+ waveDirDeg = m.waveDirection.firstOrNull(),
+ swellHeightM = m.swellWaveHeight.firstOrNull(),
+ swellDirDeg = m.swellWaveDirection.firstOrNull(),
+ swellPeriodS = m.swellWavePeriod.firstOrNull(),
+ currentSpeedKt = m.oceanCurrentVelocity.firstOrNull()?.let { it * 1.94384 },
+ currentDirDeg = m.oceanCurrentDirection.firstOrNull()
+ )
+ }
+ }.also { result ->
+ result.onSuccess { c ->
+ NavLogger.i("conditions", "OK" +
+ " wind=${c.windSpeedKt?.let { "%.1f".format(it) } ?: "—"}kt@${c.windDirDeg?.toInt() ?: "—"}°" +
+ " temp=${c.tempC?.let { "%.1f".format(it) } ?: "—"}°C" +
+ " waves=${c.waveHeightM?.let { "%.2f".format(it) } ?: "—"}m" +
+ " swell=${c.swellHeightM?.let { "%.2f".format(it) } ?: "—"}m@${c.swellDirDeg?.toInt() ?: "—"}°/${c.swellPeriodS?.toInt() ?: "—"}s" +
+ " curr=${c.currentSpeedKt?.let { "%.2f".format(it) } ?: "—"}kt@${c.currentDirDeg?.toInt() ?: "—"}°"
+ )
+ }.onFailure { e ->
+ NavLogger.e("conditions", "${e.javaClass.simpleName}: ${e.message}")
}
}
+ }
/**
* Fetch a current wind arrow for each point in [points] using Open-Meteo's batch endpoint.
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt
new file mode 100644
index 0000000..99e6d5f
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/DevLogSheet.kt
@@ -0,0 +1,58 @@
+package org.terst.nav.ui
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ScrollView
+import android.widget.TextView
+import android.widget.Toast
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.button.MaterialButton
+import org.terst.nav.R
+
+class DevLogSheet : BottomSheetDialogFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = inflater.inflate(R.layout.sheet_dev_log, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ // Expand to full height
+ (dialog as? BottomSheetDialog)?.behavior?.apply {
+ state = BottomSheetBehavior.STATE_EXPANDED
+ skipCollapsed = true
+ }
+
+ val logText = view.findViewById<TextView>(R.id.log_text)
+ val scrollView = view.findViewById<ScrollView>(R.id.scroll_view)
+
+ fun refresh() {
+ val text = NavLogger.getLog()
+ logText.text = text.ifEmpty { "(no log entries yet)" }
+ scrollView.post { scrollView.fullScroll(View.FOCUS_DOWN) }
+ }
+
+ refresh()
+
+ view.findViewById<MaterialButton>(R.id.btn_copy).setOnClickListener {
+ val cm = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ cm.setPrimaryClip(ClipData.newPlainText("nav_dev_log", NavLogger.getLog()))
+ Toast.makeText(requireContext(), "Copied to clipboard", Toast.LENGTH_SHORT).show()
+ }
+
+ view.findViewById<MaterialButton>(R.id.btn_clear).setOnClickListener {
+ NavLogger.clear()
+ refresh()
+ }
+ }
+}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt
new file mode 100644
index 0000000..c638113
--- /dev/null
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/NavLogger.kt
@@ -0,0 +1,42 @@
+package org.terst.nav.ui
+
+import java.util.Calendar
+
+/**
+ * Lightweight in-memory log ring-buffer.
+ *
+ * Format: `HH:mm:ss.SSS LEVEL tag: message`
+ *
+ * Designed so both humans and Claude can read the output at a glance.
+ * Entries are captured from API calls, GPS fixes, and key state changes.
+ * Retrieve with [getLog] and copy to clipboard via DevLogSheet (long-press
+ * the drag handle on the instrument sheet).
+ */
+object NavLogger {
+
+ private const val MAX_ENTRIES = 200
+ private val entries = ArrayDeque<String>()
+
+ fun i(tag: String, msg: String) = append("INFO ", tag, msg)
+ fun w(tag: String, msg: String) = append("WARN ", tag, msg)
+ fun e(tag: String, msg: String) = append("ERROR", tag, msg)
+
+ private fun append(level: String, tag: String, msg: String) {
+ val cal = Calendar.getInstance()
+ val time = "%02d:%02d:%02d.%03d".format(
+ cal.get(Calendar.HOUR_OF_DAY),
+ cal.get(Calendar.MINUTE),
+ cal.get(Calendar.SECOND),
+ cal.get(Calendar.MILLISECOND)
+ )
+ val entry = "$time $level $tag: $msg"
+ synchronized(entries) {
+ entries.addLast(entry)
+ while (entries.size > MAX_ENTRIES) entries.removeFirst()
+ }
+ }
+
+ fun getLog(): String = synchronized(entries) { entries.joinToString("\n") }
+
+ fun clear() = synchronized(entries) { entries.clear() }
+}
diff --git a/android-app/app/src/main/res/layout/sheet_dev_log.xml b/android-app/app/src/main/res/layout/sheet_dev_log.xml
new file mode 100644
index 0000000..5464242
--- /dev/null
+++ b/android-app/app/src/main/res/layout/sheet_dev_log.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="?attr/colorSurface">
+
+ <!-- Header: title + action buttons -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingStart="16dp"
+ android:paddingEnd="8dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="8dp"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="Dev Log"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ android:textColor="?attr/colorOnSurface" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/btn_clear"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Clear"
+ android:textColor="?attr/colorOnSurfaceVariant" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/btn_copy"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Copy" />
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?attr/colorOutline"
+ android:alpha="0.3" />
+
+ <ScrollView
+ android:id="@+id/scroll_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="10dp"
+ android:paddingBottom="24dp"
+ android:clipToPadding="false">
+
+ <TextView
+ android:id="@+id/log_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fontFamily="monospace"
+ android:textSize="11sp"
+ android:textColor="?attr/colorOnSurface"
+ android:lineSpacingMultiplier="1.35"
+ android:textIsSelectable="true" />
+
+ </ScrollView>
+
+</LinearLayout>