diff options
| author | Claude <noreply@anthropic.com> | 2026-06-04 00:11:20 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-06-04 00:11:20 +0000 |
| commit | 7a51bb3ca3b8929a52165be0b729cc77dd87912d (patch) | |
| tree | 0d65c91ccb3cd0a902c7a132c819751cdd8f9b04 | |
| parent | bfeab101083970a3fd5a67aa62d09a7506027da4 (diff) | |
| parent | 40d78f5f08d89b7c5d7f1870d4a0268cfcdd6437 (diff) | |
Merge claude/wind-particles-movement-eNrTx: fix wind particles not moving
https://claude.ai/code/session_017TwkSjhzhmTcHKmVG5MWUa
| -rw-r--r-- | .agent/code-map.md | 399 | ||||
| -rw-r--r-- | .agent/config.md | 1 | ||||
| -rw-r--r-- | .agent/worklog.md | 4 | ||||
| -rw-r--r-- | android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt | 10 | ||||
| -rw-r--r-- | docs/pmim-1-assembly.md | 276 |
5 files changed, 686 insertions, 4 deletions
diff --git a/.agent/code-map.md b/.agent/code-map.md new file mode 100644 index 0000000..eb2f564 --- /dev/null +++ b/.agent/code-map.md @@ -0,0 +1,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. diff --git a/.agent/config.md b/.agent/config.md index 52d135d..56d9df5 100644 --- a/.agent/config.md +++ b/.agent/config.md @@ -7,6 +7,7 @@ This is the primary source of truth for all AI agents working on **Nav**. These | File | Purpose | |------|---------| | `config.md` | **Main Entry Point** — Rules, workflows, and core mandates. | +| `code-map.md` | **Code Map** — Package topology, key classes, gotchas, testing guide. Read this to bootstrap context fast. | | `worklog.md` | **Session State** — Current focus, recently completed, and next steps. | | `design.md` | **Architecture** — Component design, UI mockups, and roadmap. | | `coding_standards.md` | **Technical Standards** — Android/Kotlin best practices, offline-first. | diff --git a/.agent/worklog.md b/.agent/worklog.md index c023a13..e55bf9f 100644 --- a/.agent/worklog.md +++ b/.agent/worklog.md @@ -8,6 +8,10 @@ ## Recently Completed +### Code Map & Context Tips — 2026-06-03 + +Created `.agent/code-map.md`: full package topology, key class table per domain, architectural invariants, testing guide, CI/merge workflow, and a "gotchas" section covering known tricky spots (MapLibre lineColor expression, TackDetector symlink, Room as read-cache, VoiceLogFragment adapter signature, etc.). Updated `config.md` directory table to reference the new file. + ### Tech Debt + Trip Report Redesign — 2026-06-03 Six tech debt items closed and trip report rebuilt: diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt index 26d9ea2..746efb5 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt @@ -21,7 +21,7 @@ import kotlin.random.Random * tail segment, then advance the position by the wind vector. * * Speed is scaled to the visible viewport so the animation looks consistent at any - * zoom level: a particle at reference wind (10 kt) crosses ~30% of the screen in + * zoom level: a particle at reference wind (10 kt) crosses ~100% of the screen in * MAX_AGE seconds. * * Longitude arithmetic is done in a west-relative [0, lonSpan] coordinate space @@ -103,8 +103,8 @@ class ParticleWindView @JvmOverloads constructor( // Span wraps around antimeridian when lonEast < lonWest (e.g. Pacific viewport) val lonSpan = if (lonEast >= lonWest) lonEast - lonWest else lonEast - lonWest + 360f - // Speed scale: at 10kt a particle crosses 30% of viewport in MAX_AGE seconds. - val speedScale = latRange * 0.03f / MAX_AGE + // Speed scale: at 10kt a particle crosses 100% of viewport in MAX_AGE seconds. + val speedScale = latRange * 0.1f / MAX_AGE // Geographic travel direction: opposite of the FROM direction. val travelRad = Math.toRadians((windDirFromDeg + 180.0) % 360.0) @@ -139,7 +139,9 @@ class ParticleWindView @JvmOverloads constructor( var newLon = lonWest + Random.nextFloat() * lonSpan if (newLon > 180f) newLon -= 360f particleLon[i] = newLon - particleAge[i] = Random.nextFloat() * MAX_AGE + // Start fresh so each particle gets a full lifetime to drift across the viewport. + // A tiny stagger (0–2 s) desynchronises bursts of simultaneous respawns. + particleAge[i] = Random.nextFloat() * 2f continue } diff --git a/docs/pmim-1-assembly.md b/docs/pmim-1-assembly.md new file mode 100644 index 0000000..95449ee --- /dev/null +++ b/docs/pmim-1-assembly.md @@ -0,0 +1,276 @@ +# PMIM-1 Assembly Guide +## Portable Marine Instrument Multiplexer + +The PMIM-1 is a self-contained sensor box that attaches to a stanchion or companionway and streams NMEA 0183 sentences to the Nav app over Wi-Fi UDP broadcast. It provides GPS, AIS, heading, attitude (heel/pitch), and barometric pressure/temperature. + +--- + +## Parts list + +| Part | Notes | Est. Cost | +|---|---|---| +| ESP32 DevKit V1 (30-pin) | Must be 30-pin, not 38-pin | $9 | +| dAISy Mini (UART variant) | TTL UART version, not USB | $85 | +| BN-880 module | Circular PCB ~28mm, integrated ceramic patch antenna | $24 | +| GY-521 (MPU-6050) | Small blue board ~21×16mm | $8 | +| BMP280 breakout | Verify BMP280, not BMP180 | $3 | +| IP67 enclosure, white body / clear lid | ~120×80×40mm interior | $15 | +| Anker PowerCore 10000mAh | No auto-shutoff at light load | $20 | +| IP67 SMA female bulkhead | Panel-mount style with O-ring | $5 | +| BNC-F to PL-259 adapter | For VHF antenna connection | $7 | +| IPEX (U.FL) to SMA pigtail, ~100mm | Connects dAISy to bulkhead | incl. | +| GoPro adhesive base + handlebar clamp | Standard Hero mount ecosystem | $15 | +| 28 AWG silicone wire | ~1m each: red, black, yellow, green, blue, white, orange | $5 | +| M2×6 standoffs + screws | 8× for ESP32 and dAISy | $2 | +| Perfboard, ~50×70mm | Single-sided, wiring substrate | $1 | +| PG7 cable gland | Sized for USB-C cable OD (~4–5mm) | $2 | +| 4.7kΩ resistors | 2× for I2C pull-ups if BMP280 board lacks them | — | +| 47Ω 0.5W resistor | Power bank keepalive load (optional, see notes) | — | +| Gore-Tex speaker mesh tape | Breathable waterproof vent cover | $3 | +| Silicone O-ring grease | Thin smear on lid O-ring at close-up | — | +| **Total** | | **~$204** | + +--- + +## Tools + +- Soldering iron, fine tip, 320–350°C +- Solder, 0.5mm rosin-core, and flux pen +- Multimeter (continuity + DC voltage) +- Step drill or 15.5mm bit (SMA bulkhead hole) +- 4mm drill bit (BMP280 vent) +- PG7 cable gland hole size per gland datasheet (~12mm) +- Rotary tool or hand drill +- Hot glue gun (strain relief) +- Tweezers and helping hands + +--- + +## Enclosure orientation + +Orient the enclosure so the **clear lid faces up toward the sky**. This gives the BN-880's ceramic patch antenna a direct sky view. The GoPro mount adheres to the solid bottom face. + +``` + Clear lid (faces sky) + ┌─────────────────────────┐ + │ GPS antenna view │ + └─────────────────────────┘ + Front face (toward bow preferred) + ┌──────────┐ + │ [SMA] │ ← AIS antenna exits here + └──────────┘ + Back face + ┌──────────────────────────┐ + │ [4mm vent, Gore-Tex] │ ← BMP280 pressure equalization + └──────────────────────────┘ + Bottom face (GoPro mount) + ┌──────────────────────────┐ + │ [PG7 cable gland] │ ← USB-C power from power bank + └──────────────────────────┘ +``` + +--- + +## Step 1 — Drill the enclosure + +1. **SMA bulkhead hole:** Center on the front face, ~15mm from one edge. Step-drill to 15.5mm. Install the SMA bulkhead connector with O-ring and nut — finger-tight plus ¼ turn. + +2. **BMP280 vent:** 4mm hole on the back face, as high as possible to avoid splash. Leave uncovered until components are mounted (Step 5). + +3. **Power cable gland:** Bottom face. Drill to PG7 spec (~12mm). Thread the USB-C cable through before soldering its end — you can't pull a USB-C plug through a gland. + +--- + +## Step 2 — Perfboard layout + +Cut perfboard to fit the enclosure floor with ~5mm clearance each side. Lay out components before soldering anything: + +``` +┌────────────────────────────────────────────┐ +│ │ +│ [BN-880] [ESP32 DevKit V1] │ +│ compass far corner ┌────────────────┐ │ +│ ≥35mm from ESP32 │ │ │ +│ ○ (28mm circle) │ 30-pin │ │ +│ │ DevKit │ │ +│ [BMP280] │ │ │ +│ near back vent └────────────────┘ │ +│ │ +│ [GY-521] [dAISy Mini] │ +│ center right side, near SMA wall │ +│ │ +└────────────────────────────────────────────┘ +``` + +> **Critical:** Keep BN-880 (HMC5883L compass) at least 35mm from the ESP32. The 2.4 GHz Wi-Fi radio causes magnetic interference at close range. + +Mount ESP32 and dAISy on M2×6 standoffs. Mount BN-880, GY-521, and BMP280 with double-sided foam tape, BN-880 ceramic antenna facing up toward the clear lid. + +--- + +## Step 3 — Power wiring + +``` +Power bank → USB-C cable → PG7 cable gland + → ESP32 VIN (5V) [red wire] + → ESP32 GND [black wire] + +ESP32 3.3V pin → 3.3V bus rail along perfboard edge +ESP32 GND → GND bus rail + +3.3V rail → dAISy VCC, BN-880 VCC, GY-521 VCC, BMP280 VCC +GND rail → all module GNDs +``` + +> **Before powering:** Measure 3.3V rail to GND resistance cold (power off). Should be several kΩ. A short here will kill the ESP32's onboard regulator. + +**Power bank keepalive:** If the power bank cuts out at light load, solder a 47Ω 0.5W resistor between the 5V and GND rails on the perfboard. This draws ~100mA continuously and keeps the bank alive. Try without it first — Anker PowerCore units usually don't need it. + +--- + +## Step 4 — UART wiring + +| Signal | ESP32 pin | Module pin | Wire color | +|---|---|---|---| +| UART1 RX | GPIO16 | dAISy TX | Yellow | +| UART1 TX | GPIO17 | dAISy RX | Orange | +| UART2 RX | GPIO4 | BN-880 TX | Green | +| UART2 TX | GPIO2 | BN-880 RX | White | + +> dAISy RX and BN-880 RX are only needed for sending config commands. Leave them unconnected for basic operation if you want to keep it simple. + +**Baud rates:** +- dAISy Mini UART: 38400 8N1 +- BN-880 GPS: 9600 8N1 (default) + +--- + +## Step 5 — I2C bus wiring + +Single shared bus to all three sensors: + +| Signal | ESP32 pin | Wire color | +|---|---|---| +| SDA | GPIO21 | Blue | +| SCL | GPIO22 | Purple | + +Daisy-chain both SDA and SCL to each sensor: GY-521 → BN-880 → BMP280. + +**Pull-up resistors:** GY-521 and BN-880 breakouts have 4.7kΩ pull-ups onboard. Check your BMP280 board — if it lacks them, add 4.7kΩ from SDA→3.3V and SCL→3.3V on the perfboard. + +**I2C addresses:** + +| Device | Address | How to set | +|---|---|---| +| MPU-6050 (GY-521) | 0x68 | Leave AD0 floating or tie to GND | +| HMC5883L (BN-880) | 0x1E | Fixed | +| BMP280 | 0x76 | SDO pulled LOW (0x77 if HIGH) | + +--- + +## Step 6 — RF chain + +``` +dAISy Mini IPEX/U.FL port + → IPEX-to-SMA pigtail (route flat inside enclosure) + → SMA bulkhead (in enclosure wall, O-ring sealed) + → [outside] BNC-F to PL-259 adapter + → VHF antenna coax +``` + +Keep the IPEX pigtail as short as possible and route it away from I2C and UART wires. Seat the IPEX connector firmly — it should click. + +--- + +## Step 7 — Calibration button + +Add a normally-open tactile button for IMU zero-offset calibration: + +- One terminal → GPIO0 +- Other terminal → GND +- 10kΩ pull-up from GPIO0 → 3.3V (or use ESP32 internal pull-up in firmware) + +For field access without opening the box, thread an IP67 panel-mount push-button through the enclosure wall. Otherwise, accept that calibration requires opening the lid — acceptable for initial dock setup. + +--- + +## Step 8 — BMP280 pressure vent + +Cut a 15mm circle of Gore-Tex speaker mesh tape. Apply it over the 4mm vent hole from the **inside** of the enclosure, pressing firmly around all edges. This lets atmospheric pressure equalize for accurate barometer readings while blocking water. + +Position the BMP280 within 20mm of this hole with its sensor port facing toward it. + +--- + +## Step 9 — Pre-close checklist + +Run through this before sealing the enclosure: + +- [ ] Continuity: all GND connections ring out to common ground +- [ ] No shorts: 3.3V rail to GND ≥ 5kΩ cold +- [ ] IPEX connector fully seated (it clicks) +- [ ] SMA bulkhead O-ring seated, nut snug +- [ ] PG7 cable gland compression ring tight around USB-C cable +- [ ] BN-880 ceramic antenna facing up toward clear lid +- [ ] All wires lie flat — none above the tallest component +- [ ] I2C scan (with firmware running) shows 0x1E, 0x68, 0x76 +- [ ] NMEA sentences confirmed arriving on phone before sealing + +--- + +## Step 10 — Seal and mount + +1. Confirm lid O-ring is clean and seated in its groove. Apply a thin smear of silicone grease. +2. Close lid. Tighten all screws evenly in a cross-pattern. +3. **IP test:** Hold under running tap water for 10 seconds. Dry, open, check for moisture. Do this before attaching the GoPro mount. +4. Attach GoPro adhesive base to the bottom face (or use the handlebar clamp on a stanchion). + +**Companionway:** Peel adhesive, press to clean dry surface, weight for 24h cure. +**Stanchion:** Handlebar clamp around 25mm tube. Use a rubber shim if your stanchion diameter differs. Orient clear lid skyward and inboard for best GPS view. + +--- + +## First power-on + +1. Leave box open for initial test. +2. Connect power bank. ESP32 LED should illuminate. +3. On your phone, join the PMIM-1 Wi-Fi AP (SSID set in firmware). +4. Open Nav app → enable NMEA Instruments in hardware settings. +5. Confirm data: GPS fix within ~60s cold start; AIS targets if vessels nearby; attitude immediately; baro after a few seconds. +6. Tilt the box ~15° and confirm the inclinometer in the app responds. +7. Close box, re-test over Wi-Fi. + +--- + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---|---|---| +| Power bank cuts out after ~30s | Auto-shutoff at light load | Add 47Ω keepalive resistor | +| Compass heading drifts or spins | BN-880 too close to ESP32 | Increase separation, add mu-metal shim | +| No AIS targets (even near traffic) | IPEX not seated / bad antenna connection | Re-seat IPEX, check SMA continuity | +| dAISy takes ~30s to start | Normal warm-up | Wait — not a fault | +| GPS cold start >5 min | Poor sky view | Orient clear lid more skyward | +| 0x76 missing from I2C scan | BMP280 SDO high (→ 0x77) or bad connection | Check SDO pin, rescan at 0x77 | +| Baro reads but lags weather changes | Vent hole blocked or Gore-Tex not breathable | Check mesh tape seal | + +--- + +## Pin summary + +``` +ESP32 DevKit V1 — complete pin assignment + +GPIO1 → UART0 TX (debug/flash — do not use for peripherals) +GPIO3 → UART0 RX (debug/flash — do not use for peripherals) +GPIO2 → UART2 TX → BN-880 RX +GPIO4 → UART2 RX → BN-880 TX +GPIO16 → UART1 RX → dAISy TX +GPIO17 → UART1 TX → dAISy RX +GPIO21 → I2C SDA → MPU-6050, HMC5883L, BMP280 +GPIO22 → I2C SCL → MPU-6050, HMC5883L, BMP280 +GPIO0 → Calibration button (pull-up, active LOW) +VIN → 5V from power bank +3.3V → 3.3V supply rail +GND → Common ground +``` |
