diff options
Diffstat (limited to 'SESSION_STATE.md')
| -rw-r--r-- | SESSION_STATE.md | 282 |
1 files changed, 75 insertions, 207 deletions
diff --git a/SESSION_STATE.md b/SESSION_STATE.md index d4cd865..a5ccf86 100644 --- a/SESSION_STATE.md +++ b/SESSION_STATE.md @@ -1,216 +1,84 @@ -# SESSION STATE +# SESSION_STATE.md -**Current Task Goal:** Add wind/current map overlay and weather forecast display that loads on application startup. +## Current Task Goal +GPS navigation implementation: position model, SOG/COG, NMEA RMC parser — COMPLETE -**Status:** [REVIEW_READY] — Implementation complete. All source files created, tests written. - ---- - -## Project Context - -- **Platform:** Android (Kotlin), API 24+, compileSdk 34 -- **Build system:** Gradle (Groovy DSL) -- **Location:** `android-app/` -- **Current state:** Empty project skeleton (no source code, only build files) -- **Design doc:** `docs/COMPONENT_DESIGN.md` - ---- +## Verified (2026-03-15) +- All 22 GPS/NMEA tests GREEN via test-runner (BUILD SUCCESSFUL) +- NmeaParser extended with MWV (wind), DBT (depth), HDG/HDM (heading) parsers +- Sensor data classes added: WindData, DepthData, HeadingData +- NmeaStreamManager added for TCP stream management ## Completed Items -- [x] Explored project structure -- [x] Read COMPONENT_DESIGN.md — full architecture documented -- [x] Identified UI framework: MapLibre GL Native (Android) -- [x] Identified weather data sources (see below) -- [x] Written implementation plan (this document) -- [x] `android-app/app/build.gradle` — added all deps (MapLibre, Retrofit, Moshi, Coroutines, Location, MockK, Turbine, MockWebServer) -- [x] `AndroidManifest.xml` — INTERNET + LOCATION permissions, MainActivity launcher -- [x] `proguard-rules.pro` — Moshi/Retrofit keep rules -- [x] `data/model/WindArrow.kt` — with isCalm(), normalisedDirection(), beaufortScale() -- [x] `data/model/ForecastItem.kt` — with weatherDescription(), isRainy() -- [x] `data/model/WeatherResponse.kt`, `MarineResponse.kt` — Moshi-annotated API response classes -- [x] `data/api/WeatherApiService.kt`, `MarineApiService.kt` — Retrofit interfaces -- [x] `data/api/ApiClient.kt` — OkHttp + Retrofit singletons for both base URLs -- [x] `data/repository/WeatherRepository.kt` — parallel fetch, Result<T> error propagation -- [x] `ui/MainViewModel.kt` — StateFlow UiState (Loading/Success/Error), windArrow, forecast -- [x] Layouts: activity_main.xml, fragment_map.xml, fragment_forecast.xml, item_forecast.xml -- [x] Resources: colors.xml, strings.xml, themes.xml, bottom_nav_menu.xml, ic_wind_arrow.xml -- [x] `ui/MainActivity.kt` — location permission, GPS fetch, fragment back-stack -- [x] `ui/map/MapFragment.kt` — MapLibre init, wind-arrow SymbolLayer with icon-rotate -- [x] `ui/forecast/ForecastFragment.kt` + `ForecastAdapter.kt` — RecyclerView with ListAdapter -- [x] Tests: WindArrowTest, ForecastItemTest, WeatherApiServiceTest, WeatherRepositoryTest, MainViewModelTest - ---- - -## Data Sources - -| Data | Source | Auth | -|------|--------|------| -| Wind forecast (10m, kt) | Open-Meteo API (`api.open-meteo.com`) | None (free) | -| Weather forecast (temp, precip, code) | Open-Meteo API | None (free) | -| Marine / ocean currents | Open-Meteo Marine API (`marine-api.open-meteo.com`) | None (free) | -| Base map tiles | MapLibre GL + OpenStreetMap tiles (free CDN) | None | - -Open-Meteo is chosen because it requires no API key, covers global marine/weather data, and returns JSON that's easy to parse. The design doc also lists NOAA and Windy as future sources; those can be swapped in later via the repository abstraction. - -### Key API endpoints - -**Weather forecast:** -``` -GET https://api.open-meteo.com/v1/forecast - ?latitude={lat}&longitude={lon} - &hourly=windspeed_10m,winddirection_10m,temperature_2m, - precipitation_probability,weathercode - &forecast_days=7&wind_speed_unit=kn -``` - -**Marine/current:** -``` -GET https://marine-api.open-meteo.com/v1/marine - ?latitude={lat}&longitude={lon} - &hourly=wave_height,wave_direction,ocean_current_velocity, - ocean_current_direction - &forecast_days=7 -``` - ---- - -## Architecture - -``` -Presentation - MainActivity (single-activity host) - ├── MapFragment — MapLibre map + wind-arrow overlay - └── ForecastFragment — 7-day forecast list - -ViewModel (lifecycle-aware state) - MainViewModel - ├── uiState: StateFlow<UiState> (Loading | Success | Error) - ├── windArrows: StateFlow<List<WindArrow>> - └── forecast: StateFlow<List<ForecastItem>> - -Repository - WeatherRepository - ├── fetchWeather(lat, lon): WeatherData - └── fetchMarine(lat, lon): MarineData - -Data / API - WeatherApiService (Retrofit — Open-Meteo weather endpoint) - MarineApiService (Retrofit — Open-Meteo marine endpoint) - ApiClient (OkHttp singleton, base URL config) - -Models - WeatherData, MarineData (API response data classes) - WindArrow (lat, lon, speed_kt, direction_deg) - ForecastItem (time, wind_kt, wind_dir, temp_c, precip_pct, icon) -``` - -**Startup flow:** -1. MainActivity.onCreate → MainViewModel.init -2. ViewModel launches coroutine → FusedLocationProviderClient.lastLocation -3. Location → WeatherRepository.fetchWeather(lat, lon) and .fetchMarine(lat, lon) in parallel -4. Results → StateFlow updates → UI observes: - - MapFragment draws wind arrows at sampled grid points (current hour) - - ForecastFragment renders RecyclerView with 7-day items - ---- - -## Implementation Plan (TDD) - -### Step 1 — Project dependencies (`android-app/app/build.gradle`) -Add: -- `org.maplibre.gl:android-sdk:10.0.2` -- `com.squareup.retrofit2:retrofit:2.9.0` -- `com.squareup.retrofit2:converter-moshi:2.9.0` -- `com.squareup.moshi:moshi-kotlin:1.15.0` -- `com.squareup.okhttp3:okhttp:4.12.0` -- `org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3` -- `androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0` -- `androidx.lifecycle:lifecycle-runtime-ktx:2.7.0` -- `com.google.android.gms:play-services-location:21.2.0` -- `androidx.fragment:fragment-ktx:1.6.2` -- `androidx.recyclerview:recyclerview:1.3.2` -- Test deps: `mockk:1.13.9`, `kotlinx-coroutines-test:1.7.3`, `turbine:1.1.0` - -### Step 2 — AndroidManifest.xml -- INTERNET, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION permissions -- Declare MainActivity as launcher activity - -### Step 3 — Models (`data/model/`) -**Files:** -- `WindArrow.kt` — `data class WindArrow(val lat: Double, val lon: Double, val speedKt: Double, val directionDeg: Double)` -- `ForecastItem.kt` — hourly weather item -- `WeatherResponse.kt`, `MarineResponse.kt` — Moshi-annotated API response classes - -**Tests (red first):** -- `WindArrowTest.kt` — parse correctly, edge cases (0 kt, 360°) - -### Step 4 — API services (`data/api/`) -**Files:** -- `WeatherApiService.kt` — Retrofit interface with `@GET("v1/forecast")` -- `MarineApiService.kt` — Retrofit interface with `@GET("v1/marine")` -- `ApiClient.kt` — Retrofit singleton factory - -**Tests:** -- `WeatherApiServiceTest.kt` — mock HTTP server (MockWebServer), verify request URL + parse response - -### Step 5 — Repository (`data/repository/WeatherRepository.kt`) -- Coordinates parallel API calls via `async { }` + `awaitAll()` -- Maps raw API responses → domain models (WindArrow, ForecastItem) -- Returns `Result<T>` to propagate errors cleanly - -**Tests:** -- `WeatherRepositoryTest.kt` — mock services, verify data mapping and error handling - -### Step 6 — ViewModel (`ui/MainViewModel.kt`) -- Exposes `uiState: StateFlow<UiState>`, `windArrows`, `forecast` -- On init: request location permission → fetch data - -**Tests:** -- `MainViewModelTest.kt` — mock repository, test Loading/Success/Error states with Turbine - -### Step 7 — Layouts -- `res/layout/activity_main.xml` — BottomNavigationView + FragmentContainerView -- `res/layout/fragment_map.xml` — MapLibre MapView (fullscreen) -- `res/layout/fragment_forecast.xml` — RecyclerView for forecast list -- `res/layout/item_forecast.xml` — single forecast row - -### Step 8 — MainActivity + Fragments -- `MainActivity.kt` — sets up bottom nav, fragment back-stack -- `MapFragment.kt` — initialises MapLibre, adds wind-arrow symbol layer on map ready -- `ForecastFragment.kt` — observes ViewModel.forecast, drives RecyclerView adapter - -### Step 9 — Wind Overlay on Map -- Load wind data for current hour from `windArrows` -- Sample to a sparse grid (~50 points visible on screen) -- Add MapLibre `SymbolLayer` with rotated arrow icons -- Arrow rotation = wind direction; size/opacity proportional to speed - ---- - -## Next 3 Specific Steps (after approval) - -1. **`android-app/app/build.gradle`** — add all dependencies listed in Step 1; enable `buildFeatures { viewBinding true }` -2. **`android-app/app/src/main/AndroidManifest.xml`** — create with INTERNET + LOCATION permissions -3. **Write failing tests for `WindArrow` model** — `src/test/kotlin/.../data/model/WindArrowTest.kt` - ---- - -## Constraints & Notes - -- No API keys needed (Open-Meteo is free/open). -- MapLibre GL requires a style URL; use MapLibre's free demo style or OSM raster tiles. -- Wind arrows will use a bundled SVG/PNG asset (arrow icon) for the symbol layer. -- Location: request at runtime (Android 6+ permission model). -- Min SDK 24 means no Jetpack Compose (needs API 21+, but project uses XML views). -- All implementation follows TDD: write failing test → implement → green. - ---- +### [APPROVED] GpsPosition data class +- File: `app/src/main/kotlin/org/terst/nav/gps/GpsPosition.kt` +- Package: `org.terst.nav.gps` +- Fields: latitude, longitude, sog (knots), cog (degrees true), timestampMs + +### [APPROVED] GpsProvider / GpsListener interfaces +- File: `app/src/main/kotlin/org/terst/nav/gps/GpsProvider.kt` +- `GpsProvider`: start/stop, position property, addListener/removeListener +- `GpsListener`: onPositionUpdate(GpsPosition), onFixLost() + +### [APPROVED] DeviceGpsProvider +- File: `app/src/main/kotlin/org/terst/nav/gps/DeviceGpsProvider.kt` +- Wraps `LocationManager` with `GPS_PROVIDER` +- Default interval: 1000ms (1 Hz); configurable via constructor +- SOG: Location.speed (m/s) × 1.94384 → knots +- COG: Location.bearing (degrees true, no conversion) +- Fix-lost timer: fires `onFixLost()` after 10s with no update +- Thread-safe listener list (synchronized) +- Android dependency: Context, LocationManager, Handler — device only + +### [APPROVED] FakeGpsProvider + GpsProviderTest (9 tests — all GREEN) +- File: `app/src/test/kotlin/org/terst/nav/gps/GpsProviderTest.kt` +- No Android dependencies — pure JVM +- Tests: + - `start sets started to true` + - `stop sets started to false` + - `listener receives position update` + - `listener notified of fix lost` + - `multiple listeners all receive position update` + - `multiple listeners all notified of fix lost` + - `removing listener stops notifications` + - `position property reflects last simulated position` + - `SOG conversion sanity check - 1 mps is approximately 1_94384 knots` + +## Build Notes +- `app/build` and `app/.kotlin/sessions` are root-owned from a prior run. + Tests were verified via direct `kotlinc` + `JUnitCore` invocation (all 9 pass). + Full Gradle build requires fixing directory permissions: `chown -R www-data:www-data app/build app/.kotlin` +- Pre-existing XML layout error in `activity_main.xml:300` (unrelated to GPS work) + +### [APPROVED] NmeaParser — RMC parser +- File: `app/src/main/kotlin/org/terst/nav/nmea/NmeaParser.kt` +- Parses any `*RMC` sentence (GP, GN, etc.) +- Returns `null` for void status (V), malformed input, wrong sentence type +- SOG/COG default to 0.0 when fields are empty +- Latitude: positive = North, negative = South +- Longitude: positive = East, negative = West +- Timestamp: HHMMSS + DDMMYY → Unix epoch millis UTC (YY < 70 → 2000+YY) +- No Android dependencies — pure JVM + +### [APPROVED] GpsPositionTest + NmeaParserTest (22 tests — all GREEN) +- `app/src/test/kotlin/org/terst/nav/gps/GpsPositionTest.kt` (2 tests) +- `app/src/test/kotlin/org/terst/nav/nmea/NmeaParserTest.kt` (11 tests) +- `app/src/test/kotlin/org/terst/nav/gps/GpsProviderTest.kt` (9 tests, pre-existing) +- All verified via direct `kotlinc` (1.9.22) + `JUnitCore` invocation + +## Next 3 Specific Steps +1. **UI instrument display** — SOG/COG readout widget in `MainActivity`; bind to `GpsProvider` + listener; update TextView/custom view on each `onPositionUpdate` +2. **NmeaGpsProvider** — `GpsProvider` implementation parsing NMEA RMC sentences over TCP/UDP + socket using existing `NmeaParser`; automatic reconnect on disconnect +3. **Fix build permissions** — `chown -R www-data:www-data /workspace/nav/android-app/app/build` + to enable full Gradle unit test runs ## Scripts Added - -_(none yet)_ +- `test-runner/` — standalone Kotlin/JVM Gradle project; runs all 22 GPS/NMEA tests without Android SDK + - Command: `cd /workspace/nav/test-runner && GRADLE_USER_HOME=/tmp/gradle-home ./gradlew test` ## Process Improvements - -_(none yet)_ +- Gradle builds blocked by Android SDK requirement; added `test-runner/` JVM-only subproject as reliable test runner +- All 22 tests verified GREEN via `test-runner/` JVM project (2026-03-14) |
