# 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) ```bash make dev # or without make: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d ``` **Dashboard with hot-reload:** http://localhost:8090 **Everything runs in the browser - no additional software needed!** On first run, Docker builds the custom images (snapserver, snapclient, mopidy, librespot stub). Subsequent starts are instant. ### Browser-Only Testing (Windows/Mac) All services are accessible via browser: - **Dashboard (Main UI):** http://localhost:8090 - Full boat control interface - **Snapcast Web:** http://localhost:1780 - Audio zones + browser playback via Web Audio API - **Mopidy/Iris:** http://localhost:6680/iris/ - Music player - **SignalK:** http://localhost:3000 - Navigation backend (admin/bordanlage) **Audio in browser:** Open Snapcast Web UI → Click 🔊 icon → Enable Web Audio playback! ### Native audio output **Mac:** ```bash 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). **Windows:** ```powershell make windows-audio # runs snapclient natively → Windows speakers (WASAPI) ``` Requires manual install: Download from https://github.com/badaix/snapcast/releases See [WINDOWS_AUDIO_SETUP.md](WINDOWS_AUDIO_SETUP.md) for detailed Windows setup instructions. --- ## 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: ```bash 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). --- ## Deployment to Boat (Ubuntu Server) This system is designed to run on a boat with an Ubuntu server in kiosk mode. **See detailed guide:** [KIOSK_SETUP.md](KIOSK_SETUP.md) ### Quick Summary Changes in `docker-compose.yml` for production: 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` ### Kiosk Mode (Chromium Fullscreen) ```bash chromium-browser --kiosk http://localhost:8080 ``` See [KIOSK_SETUP.md](KIOSK_SETUP.md) for complete Ubuntu server setup with autostart. --- ## 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) ```