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 (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