summaryrefslogtreecommitdiff
path: root/scripts/.claude/ct-wind-wiring.txt
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/.claude/ct-wind-wiring.txt')
-rw-r--r--scripts/.claude/ct-wind-wiring.txt156
1 files changed, 156 insertions, 0 deletions
diff --git a/scripts/.claude/ct-wind-wiring.txt b/scripts/.claude/ct-wind-wiring.txt
new file mode 100644
index 0000000..09768ab
--- /dev/null
+++ b/scripts/.claude/ct-wind-wiring.txt
@@ -0,0 +1,156 @@
+Context
+-------
+MainViewModel.addGpsPoint() (android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt,
+line 67) builds TrackPoint with hardcoded wind zeroes:
+
+ windSpeedKnots = 0.0, windAngleDeg = 0.0, isTrueWind = false
+
+LocationService already exposes live NMEA wind data:
+ LocationService.nmeaWindDataFlow: SharedFlow<WindData> (companion object, line 362)
+
+WindData (android-app/app/src/main/kotlin/org/terst/nav/sensors/WindData.kt):
+ windAngle: Double // degrees, relative or true
+ windSpeed: Double // knots
+ isTrueWind: Boolean
+ timestampMs: Long
+
+TrackPoint (android-app/app/src/main/kotlin/org/terst/nav/track/TrackPoint.kt) already has
+windSpeedKnots, windAngleDeg, isTrueWind fields — they just aren't populated.
+
+Goal
+----
+Cache the latest WindData in MainViewModel and use it when building TrackPoints.
+Default to zero wind if no NMEA wind sentence has arrived yet.
+
+Step 1 — Red: write failing tests
+----------------------------------
+Create android-app/app/src/test/kotlin/org/terst/nav/ui/MainViewModelWindTest.kt
+
+Setup boilerplate needed (Dispatchers.setMain / resetMain with UnconfinedTestDispatcher):
+
+ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+ import kotlinx.coroutines.Dispatchers
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
+ import kotlinx.coroutines.test.UnconfinedTestDispatcher
+ import kotlinx.coroutines.test.resetMain
+ import kotlinx.coroutines.test.runTest
+ import kotlinx.coroutines.test.setMain
+ import org.junit.After
+ import org.junit.Assert.assertEquals
+ import org.junit.Before
+ import org.junit.Rule
+ import org.junit.Test
+ import org.terst.nav.sensors.WindData
+ import org.terst.nav.ui.MainViewModel
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ class MainViewModelWindTest {
+
+ @get:Rule val instantTask = InstantTaskExecutorRule()
+ private val dispatcher = UnconfinedTestDispatcher()
+
+ @Before fun setUp() { Dispatchers.setMain(dispatcher) }
+ @After fun tearDown() { Dispatchers.resetMain() }
+
+ @Test fun `addGpsPoint uses zero wind when no wind update received`() = runTest {
+ val vm = MainViewModel()
+ vm.startTrack()
+ vm.addGpsPoint(37.0, -122.0, 5.0, 90.0)
+ val pt = vm.trackPoints.value.first()
+ assertEquals(0.0, pt.windSpeedKnots, 0.001)
+ assertEquals(0.0, pt.windAngleDeg, 0.001)
+ assertEquals(false, pt.isTrueWind)
+ }
+
+ @Test fun `addGpsPoint uses latest wind data after updateWind`() = runTest {
+ val vm = MainViewModel()
+ vm.updateWind(WindData(windAngle = 45.0, windSpeed = 15.5, isTrueWind = true, timestampMs = 1000L))
+ vm.startTrack()
+ vm.addGpsPoint(37.0, -122.0, 5.0, 90.0)
+ val pt = vm.trackPoints.value.first()
+ assertEquals(15.5, pt.windSpeedKnots, 0.001)
+ assertEquals(45.0, pt.windAngleDeg, 0.001)
+ assertEquals(true, pt.isTrueWind)
+ }
+
+ @Test fun `addGpsPoint reflects wind update between fixes`() = runTest {
+ val vm = MainViewModel()
+ vm.startTrack()
+ vm.addGpsPoint(37.0, -122.0, 5.0, 90.0)
+ vm.updateWind(WindData(windAngle = 180.0, windSpeed = 8.0, isTrueWind = false, timestampMs = 2000L))
+ vm.addGpsPoint(37.1, -122.0, 5.0, 90.0)
+ val pts = vm.trackPoints.value
+ assertEquals(0.0, pts[0].windSpeedKnots, 0.001)
+ assertEquals(8.0, pts[1].windSpeedKnots, 0.001)
+ }
+ }
+
+Run and confirm RED:
+ cd android-app && ./gradlew :app:testDebugUnitTest --tests "*MainViewModelWind*"
+
+Step 2 — Green: implement
+--------------------------
+In android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt:
+
+1. Add import:
+ import org.terst.nav.sensors.WindData
+
+2. Add field after trackRepository:
+ private var latestWind: WindData? = null
+
+3. Add method:
+ fun updateWind(wind: WindData) {
+ latestWind = wind
+ }
+
+4. Update addGpsPoint() to use latestWind:
+ fun addGpsPoint(lat: Double, lon: Double, sogKnots: Double, cogDeg: Double) {
+ val wind = latestWind
+ val point = TrackPoint(
+ lat = lat, lon = lon,
+ sogKnots = sogKnots, cogDeg = cogDeg,
+ windSpeedKnots = wind?.windSpeed ?: 0.0,
+ windAngleDeg = wind?.windAngle ?: 0.0,
+ isTrueWind = wind?.isTrueWind ?: false,
+ timestampMs = System.currentTimeMillis()
+ )
+ if (trackRepository.addPoint(point)) {
+ _trackPoints.value = trackRepository.getPoints()
+ }
+ }
+
+Run and confirm GREEN:
+ cd android-app && ./gradlew :app:testDebugUnitTest --tests "*MainViewModelWind*"
+
+Step 3 — Wire: feed wind updates from LocationService
+------------------------------------------------------
+In android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt,
+inside observeDataSources(), add after the anchorWatchState collector:
+
+ lifecycleScope.launch {
+ LocationService.nmeaWindDataFlow.collect { wind ->
+ viewModel.updateWind(wind)
+ }
+ }
+
+No additional imports needed (LocationService is already imported).
+
+Step 4 — Verify all tests pass
+-------------------------------
+cd android-app && ./gradlew :app:testDebugUnitTest
+
+Confirm all existing tests still pass alongside the 3 new wind tests.
+
+Step 5 — Commit and push
+-------------------------
+git add -A && git commit -m "feat(track): wire NMEA wind data into GPS track points
+
+MainViewModel caches the latest WindData from LocationService.nmeaWindDataFlow
+via updateWind(). addGpsPoint() populates TrackPoint wind fields from the cache,
+defaulting to zero if no NMEA wind sentence has arrived yet.
+
+MainActivity.observeDataSources() feeds LocationService.nmeaWindDataFlow
+into viewModel.updateWind() alongside the existing GPS and anchor observers.
+
+3 new unit tests in MainViewModelWindTest verify zero-default, wind capture,
+and mid-track wind update behaviour." && git push origin main