denshooter 19b2c30a0a Implement realistic ship routing with waypoint navigation
Major feature: Ship now follows a real nautical track around Bornholm Island

Waypoint System:
- 6-waypoint loop: Kiel → Bornholm North → Rønne → Bornholm East →
  Bornholm South → Gdansk → back to Kiel
- Great circle bearing calculation (haversine formula)
- Automatic waypoint progression when within 0.1 nm
- Route loops continuously

Navigation Algorithm:
- Calculates bearing to next waypoint using geodetic formulas
- Distance tracking in nautical miles
- Speed adjustment based on waypoint proximity:
  * 6 knots cruising (far)
  * 5-5.5 knots approaching
  * Gradual slowdown in final 0.5 nm
- Heading includes wind/current drift (±2-5°)
- Realistic position updates every 1 second
- Rudder angle reflects heading correction needed

UI Enhancements - Navigation Page:
- Canvas-based chart showing:
  * Ship position (triangle) with heading
  * Ship track (cyan line, 500-point history)
  * Waypoints (numbered circles)
  * Current waypoint highlighted
- Waypoint Info Box:
  * Current waypoint name
  * Distance to next waypoint
  * Visual route indicator (6 waypoint tags)
- Full NMEA data table with route fields

Code Changes:
- signalk.mock.js: Complete rewrite with:
  * WAYPOINTS constant (6 locations)
  * Bearing/distance calculation functions
  * Waypoint navigation logic
  * Dynamic speed adjustment
  * Heading drift simulation

- ChartPlaceholder.jsx: New canvas-based map:
  * Ship position and track rendering
  * Waypoint visualization
  * Real-time position updates
  * Legend and coordinates display

- InstrumentPanel.jsx: Enhanced with:
  * Waypoint routing box
  * Current waypoint display
  * Distance to waypoint (highlighted)
  * Visual route progression

- useNMEA.js: Extended to include:
  * distanceToWaypoint state
  * Snapshot integration for waypoint data

Documentation:
- Added SHIP_ROUTING.md with complete guide:
  * How waypoint navigation works
  * Customization instructions
  * Example scenarios (Baltic, Mediterranean, coastal)
  * Testing procedures

Performance:
- 1 Hz update rate (1 second intervals)
- Canvas renders at 60 FPS
- Track history: 500 points (~8 minutes)
- Memory: <100KB for routing system

Compatibility:
- Works with mock mode (VITE_USE_MOCK=true)
- Ready for real SignalK server integration
- NMEA2000 compliant data format

The ship now runs a realistic, continuous nautical route with proper
bearing calculations and waypoint navigation!

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 14:35:38 +01:00

Bordanlage Boat Onboard System

Multiroom audio + navigation dashboard for boats. Full Docker stack — works on any machine, no hardware required in dev mode.


Stack

Service Image / Build Purpose
SignalK signalk/signalk-server NMEA 0183/2000 gateway, WebSocket delta stream
Snapserver custom build (v0.35.0) Multiroom audio server, JSON-RPC on :1705
zone-salon/cockpit/bug/heck custom build (v0.35.0) Audio zones (snapclient)
Mopidy + Iris custom Dockerfile Local music, TuneIn radio, HTTP control on :6680
librespot custom build (v0.5.0) Spotify Connect receiver, pipe backend
shairport-sync mikebrady/shairport-sync AirPlay 2 receiver, pipe backend
Jellyfin jellyfin/jellyfin Media library, :8096
SignalK signalk/signalk-server Navigation data, demo NMEA in dev mode
Portainer portainer/portainer-ce Docker management UI, :9000
Dashboard React 18 + Vite Touch UI, built with Vite (dev: HMR on :8090)

Audio flow: librespot / shairport / mopidy → named pipe (PCM) → snapserversnapclient zones


Quick Start (Dev)

make dev

Dashboard with hot-reload: http://localhost:8090

On first run, Docker builds the custom images (snapserver, snapclient, mopidy, librespot stub). Subsequent starts are instant.

Mac audio output

make mac-audio   # runs snapclient natively via Homebrew → Mac speakers
make spotify     # runs librespot natively via Homebrew → Spotify Connect on Mac

Both commands require brew install snapcast / brew install librespot (auto-installed if missing).


Service URLs

Service URL Notes
Dashboard (dev) http://localhost:8090 Vite HMR, live reload
Dashboard (prod) http://localhost:8080 nginx-served built bundle
SignalK http://localhost:3000 Admin: admin / bordanlage
Mopidy/Iris http://localhost:6680/iris/ Music player UI
Snapcast Web http://localhost:1780 Zone control (Snapweb)
Jellyfin http://localhost:8096 Media library
Portainer http://localhost:9000 Docker management

Make Targets

make dev        # start dev stack (Vite HMR, null audio, SignalK demo data)
make boot       # start production stack (full audio pipeline)
make stop       # stop dev stack
make stop-boot  # stop production stack
make logs       # tail logs (dev stack)
make rebuild    # force rebuild all images (dev stack)
make status     # show container status
make pipes      # create audio named pipes in /tmp/audio (for boot mode)
make mac-audio  # run snapclient natively on Mac (Homebrew)
make spotify    # run librespot natively on Mac (Homebrew, Spotify Connect)

Spotify Connect

Dev mode (Mac): run make spotify — shows as "Bordanlage" in Spotify app.

Boot mode (boat): librespot runs inside Docker, pipe backend, writes PCM to /tmp/audio/spotify.pcm.

On Linux (boat), set network_mode: host for librespot and shairport in docker-compose.yml for reliable mDNS/Bonjour discovery.


AirPlay

The shairport container runs shairport-sync with pipe output.

  1. On iPhone/Mac: Control Center → AirPlay → select "Bordanlage AirPlay"
  2. Audio goes through the snapserver multiroom pipeline

Config: config/shairport.conf


Adding Music (Mopidy)

Drop files into ./music/. Trigger a library scan:

curl -s http://localhost:6680/mopidy/rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"local.scan"}'

Zones

Four zones are pre-configured:

Zone hostID Production use
Salon zone-salon Below-deck salon speakers
Cockpit zone-cockpit Cockpit/helm area
Bug zone-bug Forward cabin (Bug = bow in German)
Heck zone-heck Stern (Heck = stern in German)

Each zone runs snapclient connecting to snapserver on the Docker internal network. In dev mode the zones use --player file:filename=null (audio discarded, no hardware needed). In production, replace with --player alsa --soundcard hw:N,0 and expose /dev/snd.


Environment Variables

Copy .env.example to .env and adjust:

SPOTIFY_NAME=Bordanlage
SPOTIFY_BITRATE=320
MUSIC_PATH=./music
VITE_USE_MOCK=false     # true = force mock data in dashboard, false = real APIs

Dashboard API Configuration

The dashboard reads hosts from environment at build time:

Variable Default Description
VITE_SNAPCAST_HOST localhost Snapserver hostname
VITE_SIGNALK_HOST localhost SignalK hostname
VITE_MOPIDY_HOST localhost Mopidy hostname
VITE_JELLYFIN_HOST localhost Jellyfin hostname
VITE_USE_MOCK false Force mock data (no real APIs)

Architecture: Named Pipes

In boot mode, audio flows through named pipes on a tmpfs volume:

spotify.pcm   ← librespot  (Spotify Connect)
airplay.pcm   ← shairport  (AirPlay 2)
mopidy.pcm    ← mopidy     (local files, radio)

Snapserver reads all three as streams and routes them to zones.

Run make pipes once before make boot to create the pipes (or they are created automatically).

In dev mode, pipes are not used (no audio hardware crosses the Docker VM boundary on Mac).


Migrating to the Boat

Changes in docker-compose.yml:

  1. Zone audio output — replace --player file:filename=null with --player alsa --soundcard hw:N,0, uncomment /dev/snd device
  2. Spotify / AirPlay discovery — set network_mode: host for librespot and shairport
  3. NMEA hardware — uncomment the devices block under signalk, configure the connection at http://[boat-ip]:3000
  4. Video decoding (optional) — uncomment /dev/dri under jellyfin
  5. Use make boot instead of make dev

Troubleshooting

Zones stuck restarting:

  • Check docker compose logs zone-salon. With snapclient v0.35+, the server URI must be tcp://snapserver (not --host snapserver).
  • If "No audio player support": snapclient needs --player file:filename=null in dev (null player not compiled in the bookworm .deb).

Dashboard crashes with exit 127 / npm not found:

  • The local node:20-alpine image was overwritten by the production dashboard build. Fix: docker rmi node:20-alpine && make dev.
  • Root cause prevented by dashboard/Dockerfile.dev — dev uses its own build context.

Shairport config syntax error:

  • Boolean values in config/shairport.conf must be quoted: "yes" not yes.

Spotify device not visible:

  • On Mac, Docker Desktop bridges UDP poorly. Use make spotify (native librespot on Mac) for dev.
  • On boat: use network_mode: host in docker-compose.yml.

Librespot production build fails:

  • cargo install librespot (latest = v0.8.0) has a vergen_lib dependency conflict.
  • Fixed by pinning to --version "=0.5.0" in docker/librespot/Dockerfile.

Snapcast arm64 .deb not found:

  • v0.27.0 has no arm64 release. Use v0.35.0 which ships arm64_bookworm.deb.

Port conflicts (e.g. 8080 taken by Supabase):

  • Dev dashboard is on 8090, production on 8080. Change port mappings in docker-compose.dev.yml as needed.

Project Layout

.
├── Makefile
├── docker-compose.yml          # production stack
├── docker-compose.dev.yml      # dev overrides (Vite HMR, null zones, SignalK demo)
├── config/
│   ├── snapserver.conf         # pipe sources, HTTP API config
│   ├── mopidy.conf             # music backends, HTTP server
│   └── shairport.conf          # AirPlay name, pipe output
├── docker/
│   ├── snapserver/Dockerfile   # snapcast v0.35.0 from GitHub releases
│   ├── snapclient/Dockerfile   # snapcast v0.35.0 from GitHub releases
│   ├── mopidy/Dockerfile       # mopidy + iris + local + tunein
│   └── librespot/
│       ├── Dockerfile          # production: cargo install librespot v0.5.0
│       └── Dockerfile.dev      # dev stub: alpine + sleep
├── dashboard/
│   ├── Dockerfile              # production: Vite build → nginx
│   ├── Dockerfile.dev          # dev: node:20-alpine base (avoid image overwrite)
│   ├── nginx.conf
│   └── src/
│       ├── api/                # real API clients (snapcast, signalk, mopidy, jellyfin)
│       ├── mock/               # mock data (VITE_USE_MOCK=true)
│       ├── hooks/              # useNMEA, useZones, usePlayer, useDocker
│       └── components/
│           ├── layout/         # TopBar, TabNav
│           ├── audio/          # ZoneCard, ZoneGrid, NowPlaying, SourcePicker
│           └── nav/            # InstrumentPanel, ChartPlaceholder
├── scripts/
│   ├── init-pipes.sh           # create named pipes in /tmp/audio
│   ├── setup-dev.sh
│   └── setup-boot.sh
└── music/                      # drop audio files here (Mopidy + Jellyfin)
Description
No description provided
Readme 550 KiB
Languages
JavaScript 89.8%
CSS 4.8%
Dockerfile 2.1%
Makefile 1.7%
Shell 1.1%
Other 0.5%