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>
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) → snapserver → snapclient 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: hostforlibrespotandshairportindocker-compose.ymlfor reliable mDNS/Bonjour discovery.
AirPlay
The shairport container runs shairport-sync with pipe output.
- On iPhone/Mac: Control Center → AirPlay → select "Bordanlage AirPlay"
- 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:
- Zone audio output — replace
--player file:filename=nullwith--player alsa --soundcard hw:N,0, uncomment/dev/snddevice - Spotify / AirPlay discovery — set
network_mode: hostforlibrespotandshairport - NMEA hardware — uncomment the
devicesblock undersignalk, configure the connection at http://[boat-ip]:3000 - Video decoding (optional) — uncomment
/dev/driunderjellyfin - Use
make bootinstead ofmake dev
Troubleshooting
Zones stuck restarting:
- Check
docker compose logs zone-salon. With snapclient v0.35+, the server URI must betcp://snapserver(not--host snapserver). - If "No audio player support": snapclient needs
--player file:filename=nullin dev (null player not compiled in the bookworm .deb).
Dashboard crashes with exit 127 / npm not found:
- The local
node:20-alpineimage 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.confmust be quoted:"yes"notyes.
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: hostindocker-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"indocker/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.ymlas 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)