1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
|
# Code Map & Context Tips
**Purpose:** Bootstrap context for any new agent session. Read this before touching code.
**Last updated:** 2026-06-03
---
## 1. Session Start Checklist
Run these four commands before anything else:
```bash
git branch --show-current # must be main (or a feature branch off main)
git checkout main # if not already there
git config core.hooksPath scripts/git-hooks # activate pre-commit hook
git pull origin main # sync latest
```
Then read `.agent/worklog.md` for current focus and recently completed work.
**Merge policy (non-negotiable):** merge to `main` after every complete unit of work, not at session end.
---
## 2. Module Topology
```
nav/
├── android-app/ Android application (API 36, Kotlin)
│ └── app/src/main/kotlin/org/terst/nav/ ← all app source
├── test-runner/ JVM-only test module (no Android SDK)
│ └── src/main/kotlin/org/terst/nav/ ← shared portable code
├── .agent/ Agent config, worklog, design, standards
├── scripts/
│ ├── git-hooks/pre-commit blocks pushes to master
│ └── .claude/ change templates (ct-*.yaml)
└── .github/workflows/android.yml CI; fires on main pushes only
```
**Build note:** `./gradlew assembleDebug` requires network access to Google plugin repo — unavailable in the cloud sandbox. Use `test-runner` for logic tests:
```bash
cd test-runner && ./gradlew test
```
---
## 3. Source Package Map
All source lives under `org.terst.nav`.
### Root-level (android-app only)
| File | Role |
|------|------|
| `MainActivity.kt` | Single-activity host; nav graph, SAF picker launcher |
| `NavApplication.kt` | Application class; DI/singleton initialization |
| `MainViewModel.kt` → `ui/MainViewModel.kt` | Top-level state (GPS, NMEA, mode) |
| `LocationService.kt` | Foreground service; provides GPS to background |
| `AnchorAlarmManager.kt` | Anchor drag detection; survives screen lock |
| `PerformanceViewModel.kt` | Real-time sailing metrics (VMG, polar %, wind triangle) |
| `PerformanceViewModelFactory.kt` | Factory for PerformanceViewModel |
| `UnitPrefs.kt` | Unit system prefs (knots/m/s, m/ft, °C/°F) |
| `NmeaConnectionPrefs.kt` | TCP/UDP NMEA connection settings |
| `PolarData.kt` | Polar table data class + interpolation |
| `PolarDiagramView.kt` | Custom View: polar diagram canvas renderer |
| `BarometerSensorManager.kt` | Device barometer reader |
| `BarometerTrendView.kt` | Pressure trend sparkline view |
| `BarometerData.kt` | Barometer data class |
| `MobData.kt` | MOB event data (position, timestamp) |
| `TidalCurrentData.kt` | Tidal current data model |
| `MockTidalCurrentGenerator.kt` | Test-only tidal current stub |
| `PowerMode.kt` | Enum: FULL / ECONOMY / ANCHOR_WATCH |
| `FeatureFlags.kt` → `settings/FeatureFlags.kt` | Runtime feature toggles |
### `ais/`
| File | Role |
|------|------|
| `AisRepository.kt` | In-memory AIS target store (MMSI → AisVessel) |
| `AisVessel.kt` | AIS target data class |
| `AisVdmParser.kt` → `nmea/` | Parses !AIVDM sentences |
| `CpaCalculator.kt` | CPA / TCPA computation |
| `CpaAlert.kt` | Alert data class for CPA threshold breach |
| `AisHubApiService.kt` | AISHub internet feed Retrofit service |
| `AisHubSource.kt` | Polls AISHub; feeds AisRepository |
### `data/`
| Sub-package | Files | Role |
|-------------|-------|------|
| `api/` | `ApiClient.kt`, `WeatherApiService.kt`, `MarineApiService.kt` | Retrofit/OkHttp setup; weather + marine endpoints |
| `model/` | `BoatPolars`, `GribFile`, `GribRegion`, `GribParameter`, `WeatherResponse`, `MarineResponse`, `MarineConditions`, `WindArrow`, `WindForecast`, `ForecastItem`, `TideStation`, `TideConstituent`, `TidePrediction`, `LogbookEntry`, `SensorData`, `SatelliteDownloadRequest` | Pure data classes, no Android deps |
| `repository/` | `WeatherRepository.kt` | Weather fetch + cache coordination |
| `storage/` | `GribFileManager.kt` | GRIB2 file lifecycle (download, store, delete) |
| `weather/` | `GribStalenessChecker.kt`, `SatelliteGribDownloader.kt` | GRIB freshness logic; satellite GRIB fetch |
### `db/`
| File | Role |
|------|------|
| `NavDatabase.kt` | Room database; holds `TrackDao` |
### `gps/`
| File | Role |
|------|------|
| `GpsProvider.kt` | Interface; abstracts device GPS vs NMEA GPS |
| `GpsPosition.kt` | Data class: lat, lon, sog, cog, accuracy, timestamp |
| `DeviceGpsProvider.kt` | FusedLocationProvider implementation |
### `logbook/`
| File | Role |
|------|------|
| `LogbookRepository.kt` | Interface |
| `InMemoryLogbookRepository.kt` | Thread-safe impl (`@Synchronized` on save/getAll/reload) |
| `LogbookStorage.kt` | JSON persistence to SAF directory |
| `LogEntry.kt` | Log entry data class (type enum, text, photo, position) |
| `LogbookFormatter.kt` | Formats log for display |
| `LogbookPdfExporter.kt` | PDF export |
| `VoiceLogViewModel.kt` | ViewModel for logbook + trip report tab |
| `VoiceLogState.kt` | Sealed state for voice log UI |
### `nmea/`
| File | Role |
|------|------|
| `NmeaParser.kt` | Parses all NMEA 0183 sentences (GGA, RMC, MWV, HDG, DBT, VHW, XTE…) |
| `AisVdmParser.kt` | Parses !AIVDM / !AIVDO AIS sentences |
| `NmeaStreamManager.kt` | TCP/UDP connection manager; streams sentences to NmeaParser |
### `routing/`
| File | Role |
|------|------|
| `IsochroneRouter.kt` | Isochrone weather routing engine |
| `IsochroneResult.kt` | Result data class with optimal route + isochrones |
| `RoutePoint.kt` | Waypoint data class for routing |
### `safety/`
| File | Role |
|------|------|
| `AnchorWatchState.kt` | State sealed class for anchor alarm UI |
### `sensors/`
Data-only classes (no logic): `WindData`, `HeadingData`, `BoatSpeedData`, `DepthData`, `AttitudeData`, `NmeaBaroData`
### `settings/`
| File | Role |
|------|------|
| `FeatureFlags.kt` | Runtime boolean flags (e.g. night mode, race mode) |
### `thermal/`
| File | Role |
|------|------|
| `ThermalMonitor.kt` | Monitors battery temp via `PowerManager`; fires alert if overheating |
### `tide/`
| File | Role |
|------|------|
| `HarmonicTideCalculator.kt` | Offline tidal harmonic prediction; no network required |
### `track/`
| File | Role |
|------|------|
| `TrackRepository.kt` | Coordinates GPX + Room storage; source of truth = GPX/SAF |
| `TrackStorage.kt` | SAF primary + MediaStore fallback for GPX files |
| `TrackEntity.kt` | Room entity; `toShallowSavedTrack()` for fast list load |
| `TrackDao.kt` | Room DAO |
| `TackDetector.kt` | Time-based tack/jibe detection (CRUISING + RACE modes) |
| `TackEvent.kt` | Detected tack data class |
| `GpxParser.kt` | GPX file → list of TrackPoints |
| `GpxSerializer.kt` | TrackPoints → GPX file |
| `SavedTrack.kt` | In-memory track with points + metadata |
| `TrackSummary.kt` | Lightweight summary (no points) for list display |
| `TrackStats.kt` | Computed stats: distance, max speed, duration, tacks |
| `TrackColors.kt` | Speed-to-color mapping for track rendering |
| `SavedTracksFragment.kt` | List of saved tracks; SAF setup button |
| `TrackDetailSheet.kt` | Bottom sheet: track stats, add note/photo |
| `TrackSummarySheet.kt` | Mini summary bottom sheet |
### `tripreport/`
| File | Role |
|------|------|
| `TripReportFragment.kt` | Full trip report view (map + stats + log entries) |
| `TripReportGenerator.kt` | Generates report from track + log data |
| `TripReportViewModel.kt` | ViewModel for trip report |
| `PreTripReportFragment.kt` | Pre-departure checklist / briefing |
| `PreTripReportGenerator.kt` | Generates pre-trip summary (weather, similar trips, watch items) |
| `PreTripReportViewModel.kt` | ViewModel for pre-trip report |
| `PreTripModels.kt` | Data classes for pre-trip report |
| `BoatProfileRepository.kt` | Stores boat profile (name, dimensions, polars) |
### `vessel/`
| File | Role |
|------|------|
| `VesselRepository.kt` | Crew + vessel data; in-memory + persistence |
### `ui/` (top level)
| File | Role |
|------|------|
| `MainViewModel.kt` | Top-level app state |
| `MobHandler.kt` | MOB activation logic; always-on-top button handler |
| `InstrumentHandler.kt` | Routes NMEA/GPS data to instrument display widgets |
| `MapHandler.kt` | MapLibre map setup, overlay rendering, track display |
| `MapLayerManager.kt` | Layer visibility management |
| `LayerPickerSheet.kt` | Bottom sheet: layer toggle UI |
| `NavLogger.kt` | In-app debug log (primary debugging tool — prefer over ADB) |
| `LocationPermissionHandler.kt` | Runtime location permission request flow |
| `DevLogSheet.kt` | Bottom sheet: shows NavLogger output |
| `DirectionArrowView.kt` | Custom View: compass/bearing arrow |
| `InclinometerView.kt` | Custom View: heel angle indicator |
| `WaveView.kt` | Custom View: wave height visualization |
| `WeatherActivity.kt` | Standalone weather details screen |
### `ui/map/`
| File | Role |
|------|------|
| `MapFragment.kt` | Main chart fragment; hosts MapLibre GL map |
| `ParticleWindView.kt` | Animated wind particle overlay on chart |
### `ui/safety/`
| File | Role |
|------|------|
| `SafetyFragment.kt` | Safety tab: MOB, anchor alarm (depth + rode inputs, radius display) |
### `ui/voicelog/`
| File | Role |
|------|------|
| `VoiceLogFragment.kt` | Log tab: logbook entries + trip report; passes both `onSelect` and `onReport` to `SavedTrackAdapter` |
### `ui/forecast/`
| File | Role |
|------|------|
| `ForecastFragment.kt` | Weather forecast tab |
| `ForecastAdapter.kt` | RecyclerView adapter for forecast items |
### `ui/vessel/`
| File | Role |
|------|------|
| `VesselRegistryFragment.kt` | Crew registry UI |
### `ui/doc/`
| File | Role |
|------|------|
| `DocFragment.kt` | In-app documentation viewer |
### `ui/learn/`
| File | Role |
|------|------|
| `LearnFragment.kt` | Sailing knowledge / learning content |
---
## 4. test-runner Module
JVM-only; mirrors a subset of android-app source with no Android SDK dependencies. Run with `cd test-runner && ./gradlew test`.
**Shared sources** (canonical copy in android-app, symlinked or mirrored in test-runner):
| Package | Files |
|---------|-------|
| `ais/` | `AisVessel`, `CpaCalculator`, `CpaAlert` |
| `data/model/` | `GribFile`, `GribRegion` |
| `data/storage/` | `GribFileManager` |
| `gps/` | `GpsPosition`, `GpsProvider` |
| `nmea/` | `NmeaParser`, `AisVdmParser` |
| `settings/` | `HardwareSource` |
| `track/` | `TackDetector` ← **symlink** to android-app canonical source |
| `vessel/` | `Vessel`, `VesselRepository`, `CrewMember` |
**Important:** `TackDetector.kt` in test-runner is a filesystem symlink (`../../../../../../../../android-app/.../TackDetector.kt`). Edit the android-app copy only — test-runner picks it up automatically.
---
## 5. Key Architectural Invariants
1. **Track storage source-of-truth = GPX files in `Documents/Nav/` via SAF.** Room (`NavDatabase`) is a read-cache for fast list loading. GPX/SAF is always authoritative.
2. **Offline-first.** Every core feature (navigation, tides, safety) works without connectivity. Never gate core functionality on network.
3. **`getExternalFilesDir()` is never acceptable** for user data. User data must survive uninstall. Use SAF (`Documents/Nav/`) or MediaStore.
4. **`NavLogger` is the primary debug tool.** Log to `NavLogger`, not just Android `Log`, so in-app debug sheet shows it.
5. **MOB button is always visible and non-dismissible.** Never place anything above it in the Z-order.
6. **Logbook `InMemoryLogbookRepository`** has `@Synchronized` on `save()`, `getAll()`, `reload()`. Don't remove synchronization.
7. **`VoiceLogFragment`** must pass both `onSelect` and `onReport` to `SavedTrackAdapter` — omitting either causes a CI failure.
8. **MapLibre Android 11.x**: `Expression.get("color")` silently fails for `lineColor` on `LineLayer`. Use `Expression.step(Expression.get("speed"), ...)` to map speed → color literal.
9. **Speed/temp/depth in UI** must use `UnitPrefs` conversions — never raw SI values.
10. **NMEA sentences from instruments are magnetic.** Convert to true using WMM variation from `HarmonicTideCalculator`'s companion magnetic model — not a hardcoded offset.
---
## 6. Testing Guide
```bash
# JVM unit tests (no Android SDK required, works in cloud sandbox)
cd test-runner && ./gradlew test
# Android unit tests (requires local dev env with Android SDK)
cd android-app && ./gradlew test
# Android instrumented tests (requires device/emulator)
cd android-app && ./gradlew connectedAndroidTest
```
Key test files by domain:
| Domain | Test file |
|--------|-----------|
| Tack detection | `test-runner/.../track/TackDetectorTest.kt` (16 tests CRUISING + 2 RACE) |
| Pre-trip report | `android-app/.../tripreport/PreTripReportGeneratorTest.kt` (18 JVM tests) |
| Tide calculator | `android-app/.../tide/HarmonicTideCalculatorTest.kt` |
| AIS / CPA | `test-runner/.../ais/CpaCalculatorTest.kt` |
| NMEA parsing | `test-runner/.../nmea/NmeaParserTest.kt` |
| GRIB storage | `test-runner/.../data/storage/GribFileManagerTest.kt` |
---
## 7. CI / Merge Workflow
```bash
# 1. Work on feature branch
git checkout -b feature/my-thing
# 2. Commit incrementally
git add <files> && git commit -m "..."
# 3. Push branch
git push -u origin feature/my-thing
# 4. Merge to main IMMEDIATELY when done (not at session end)
git checkout main
git merge --no-ff feature/my-thing
git push -u origin main
# 5. Delete feature branch (optional cleanup)
git branch -d feature/my-thing
```
CI (`android.yml`) triggers only on pushes to `main`. Firebase App Distribution fires on `main` pushes only.
---
## 8. Context Tips & Gotchas
### Navigation / GPS
- `DeviceGpsProvider` uses `FusedLocationProviderClient`. External GPS comes via NMEA TCP/UDP through `NmeaStreamManager` → `NmeaParser` → `GpsProvider` interface.
- GPS fix is stale after 10 s — `MainViewModel` shows red "No GPS" banner.
### NMEA
- `NmeaStreamManager` handles connection lifecycle (reconnect on failure). Don't manage socket lifecycle elsewhere.
- All wind data from NMEA is apparent (`AWS`, `AWA`). True wind is computed in `PerformanceViewModel` using the vector triangle.
### Tack Detection
- `TackDetector` uses time-based windows (not point-count). `T_SETTLE=30s` before/after apex.
- `circularMAD` stability check handles anchor-swing noise without an SOG gate.
- RACE mode: `T_SETTLE=12s`, `MIN_GAP=20s`, `STAB_MAX=30°`.
### Anchor Alarm
- Alarm must survive: app background, screen lock, low battery, OS memory pressure.
- `AnchorAlarmManager` runs in `LocationService` (foreground service), not in an Activity.
- UI inputs (depth, rode) embedded in `card_anchor` in `fragment_safety.xml` — there is no separate anchor config screen.
### Trip Report
- Report auto-generates on tab open; RETRY button appears only on error.
- Track map uses a full-bleed 260dp MapLibre instance embedded in the report fragment.
- Pirate mode Easter egg: long-press title in `TripReportFragment`.
### GRIB / Weather
- GRIB files stored via `GribFileManager`; staleness checked by `GribStalenessChecker`.
- `addGpsPoint()` matches current UTC hour prefix against `timeIso` for forecast slot lookup.
- Particle wind animation: `ParticleWindView` is a GL overlay on the map.
### Room / Track Storage
- Room is a read-cache only. On cold start, if Room is empty, GPX files are backfilled.
- `TrackEntity.toShallowSavedTrack()` serves the list view without loading track points.
- `TrackStorage` has SAF primary path with MediaStore fallback; SAF state is a `Flow` observed by `SavedTracksFragment`.
### Logbook
- `onSafDirectorySelected` must call `logbookRepository.reload()` so entries from the newly-granted directory appear immediately.
### Units
- `UnitPrefs` is the single source of truth for display units. Always convert through it.
- Distance: nm / km. Speed: knots / m/s. Depth: m / ft. Temperature: °C / °F.
|