# Bordanlage – Vollständiges Multiroom Audio + Bootsdaten Dashboard ## Projektbeschreibung Baue ein vollständiges, produktionsreifes System namens `bordanlage` für ein Boot. Es besteht aus drei Teilen: 1. **Docker-Stack** – alle Backend-Dienste (Audio, Navigation, Management) 2. **React-Dashboard** – ein modernes, touch-optimiertes Web-UI für Touchscreen-Display 3. **Dev-Modus** – vollständig ohne Hardware lauffähig (Mac & Windows), mit simulierten Daten --- ## Technologie-Stack ### Backend (Docker) - **Snapcast** (snapserver + snapclient) → synchrones Multiroom-Audio, 4 Zonen - **librespot** → Spotify Connect (echter Endpunkt, erscheint in Spotify App) - **shairport-sync** → AirPlay 2 Empfänger (erscheint auf iPhone/Mac) - **Mopidy** + Plugins → Web Radio (HTTP-Streams) + lokale Musikbibliothek - **Jellyfin** → Mediathek (Musik, Hörbücher, Videos von Festplatte/USB) - **SignalK** → NMEA 2000 / NMEA 0183 Gateway (Bootsdaten) - **Portainer** → Docker Management UI - **Nginx** → Reverse Proxy, served das Dashboard ### Frontend (React + Vite) - React 18 + Vite - Keine UI-Bibliothek – eigenes Design-System - WebSocket-Verbindung zu SignalK für Live-Bootsdaten - Snapcast JSON-RPC API für Zonen-Steuerung - Mopidy JSON-RPC API für Musik-Steuerung - Jellyfin REST API für Mediathek --- ## Projektstruktur Erstelle folgende Verzeichnisstruktur: ``` bordanlage/ ├── docker-compose.yml # Produktion (Boot) ├── docker-compose.dev.yml # Development Override (Mac/Windows) ├── docker-compose.override.yml # Symlink → dev wenn DEV=true ├── .env.example ├── .env ├── Makefile # make dev / make boot / make stop / make logs ├── README.md │ ├── config/ │ ├── snapserver.conf │ ├── mopidy.conf │ ├── shairport.conf │ └── nginx/ │ └── default.conf │ ├── dashboard/ # React App │ ├── package.json │ ├── vite.config.js │ ├── index.html │ └── src/ │ ├── main.jsx │ ├── App.jsx │ ├── api/ │ │ ├── snapcast.js # Snapcast JSON-RPC Client │ │ ├── mopidy.js # Mopidy JSON-RPC Client │ │ ├── signalk.js # SignalK WebSocket Client │ │ └── jellyfin.js # Jellyfin REST Client │ ├── mock/ │ │ ├── index.js # Mock-Router: echte API wenn prod, fake wenn dev │ │ ├── signalk.mock.js # Simulierte NMEA-Daten mit realistischen Werten │ │ ├── snapcast.mock.js # Simulierte Zonen, Lautstärke, Quellen │ │ └── mopidy.mock.js # Simulierte Tracks, Radio, Wiedergabe │ ├── hooks/ │ │ ├── useNMEA.js # Hook: NMEA-Daten (echt oder mock) │ │ ├── useZones.js # Hook: Snapcast Zonen │ │ ├── usePlayer.js # Hook: Wiedergabe-Steuerung │ │ └── useDocker.js # Hook: Container-Status via Portainer API │ ├── components/ │ │ ├── layout/ │ │ │ ├── TopBar.jsx │ │ │ └── TabNav.jsx │ │ ├── instruments/ │ │ │ ├── Gauge.jsx # Analoges Rundinstrument (SVG) │ │ │ ├── Compass.jsx # Kompassrose (SVG, animiert) │ │ │ ├── WindRose.jsx # Windrose (SVG, animiert) │ │ │ ├── DepthSounder.jsx # Tiefenmesser mit Warngrenze │ │ │ └── SpeedLog.jsx # Fahrtmesser │ │ ├── audio/ │ │ │ ├── NowPlaying.jsx # Track-Info, Cover, Playback-Controls │ │ │ ├── ZoneCard.jsx # Eine Snapcast-Zone │ │ │ ├── ZoneGrid.jsx # Alle Zonen │ │ │ ├── SourcePicker.jsx # Quelle wählen (Spotify/AirPlay/Radio/Jellyfin) │ │ │ ├── RadioBrowser.jsx # Senderliste mit Suche │ │ │ └── LibraryBrowser.jsx # Jellyfin Mediathek │ │ ├── nav/ │ │ │ ├── ChartPlaceholder.jsx # Seekarten-Iframe (OpenCPN/SignalK) │ │ │ └── InstrumentPanel.jsx │ │ └── systems/ │ │ ├── BatteryStatus.jsx │ │ ├── EngineData.jsx │ │ └── ServiceHealth.jsx # Docker-Container Status │ └── pages/ │ ├── Overview.jsx # Tab 1: Instrumente + Now Playing + Zonen │ ├── Navigation.jsx # Tab 2: Seekarte + alle Navigationsdaten │ ├── Audio.jsx # Tab 3: Vollständige Audio-Steuerung │ └── Systems.jsx # Tab 4: Batterien, Motor, Docker-Status │ └── scripts/ ├── init-pipes.sh # Named Pipes anlegen ├── setup-dev.sh # Erstkonfiguration Entwicklung └── setup-boot.sh # Erstkonfiguration echtes Boot ``` --- ## Detaillierte Anforderungen ### 1. Docker Compose – Produktion (`docker-compose.yml`) Definiere folgende Services: **signalk:** - Image: `signalk/signalk-server:latest` - Port: 3000 - Volume: signalk-data - Kommentar für NMEA-Adapter: `/dev/ttyUSB0` (auskommentiert) **librespot (Spotify Connect):** - Image: `ghcr.io/librespot-org/librespot:latest` - Umgebungsvariablen aus `.env`: `SPOTIFY_NAME`, `SPOTIFY_BITRATE` (default: 320) - Backend: `pipe`, Output: `/tmp/audio/spotify.pcm` - Volume: pipes - WICHTIG: Der Service muss wirklich in der Spotify App als Gerät erscheinen. Stelle sicher, dass `network_mode: host` auf Linux gesetzt ist (Boot). Auf Mac/Windows alternative Port-Mappings verwenden. **shairport-sync (AirPlay):** - Image: `mikebrady/shairport-sync:latest` - Config: `./config/shairport.conf` (Output auf `/tmp/audio/airplay.pcm`) - Avahi/mDNS: Auf Linux `network_mode: host`. Auf Mac/Windows eigenen avahi-daemon Container (`hauscontribs/avahi`) bereitstellen damit AirPlay discoverable ist. - Volume: pipes **mopidy:** - Image: `ghcr.io/mopidy/mopidy:latest` - Port: 6680 - Plugins installieren via Custom-Dockerfile in `./docker/mopidy/`: ``` mopidy-iris mopidy-local mopidy-stream mopidy-tunein (Web Radio Suche) mopidy-podcast ``` - Audio-Output via GStreamer-Pipeline auf `/tmp/audio/mopidy.pcm` - Volumes: pipes, mopidy-data, music **jellyfin:** - Image: `jellyfin/jellyfin:latest` - Port: 8096 - Volumes: jellyfin-config, jellyfin-cache, music (read-only) - Hardware-Decoding: `/dev/dri` auskommentiert für Boot **snapserver:** - Image: `ghcr.io/badaix/snapcast:latest` - Ports: 1704 (Protokoll), 1705 (Control API), 1780 (Snapweb) - Config: `./config/snapserver.conf` - 3 Streams: Spotify, AirPlay, Mopidy - depends_on: librespot, mopidy **zone-salon, zone-cockpit, zone-bug, zone-heck:** - Je ein `ghcr.io/badaix/snapcast:latest` Container als snapclient - `--host snapserver --hostID ` - Auf dem Boot: `--soundcard hw:N,0` und `/dev/snd` device - Im Dev-Modus: Ohne Soundkarte (Audio wird simuliert, kein Fehler) **portainer:** - Image: `portainer/portainer-ce:latest` - Port: 9000 - Volume: docker.sock + portainer-data **dashboard:** - Custom Dockerfile in `./dashboard/` - Nginx serviert den gebauten React-Build - Port: 8080 - Env-Variablen: alle API-URLs ### 2. Docker Compose Dev Override (`docker-compose.dev.yml`) Überschreibt für lokale Entwicklung ohne Hardware: - **signalk**: Zusätzliche Umgebungsvariable `SIGNALK_DEMO=true` → SignalK generiert dann selbst Demo-NMEA-Daten (eingebautes Feature!) - **snapclients**: Alle 4 Zonen erhalten `command: snapclient --host snapserver --hostID --player null` (Null-Player: kein Audio-Output, kein Fehler) - **dashboard**: Statt gebautem Nginx → Vite Dev-Server mit Hot Reload (`node:20-alpine`, `npm run dev`, Port 8080) - **librespot**: `LIBRESPOT_DISABLE_DISCOVERY=false` → erscheint trotzdem in Spotify (funktioniert über TCP auch ohne host network, solange Port 57621 gemappt) - **shairport**: avahi-container für mDNS-Discovery auf Mac/Windows ### 3. `.env` Konfiguration ```env # Allgemein COMPOSE_PROJECT_NAME=bordanlage DEV=true # Spotify Connect SPOTIFY_NAME=Bordanlage SPOTIFY_BITRATE=320 SPOTIFY_CACHE_SIZE=1024 # Boot-Info BOAT_NAME=Meine Yacht BOAT_MMSI=123456789 # Pfade (Musik, Logs) MUSIC_PATH=./music ``` ### 4. Makefile ```makefile dev: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d boot: docker compose up -d stop: docker compose down logs: docker compose logs -f rebuild: docker compose build --no-cache status: docker compose ps ``` ### 5. Dashboard – Mock-System Das Mock-System ist KRITISCH. Es muss folgendes können: **`src/mock/index.js`:** Exportiert einen `createApi()`-Factory, der prüft ob `import.meta.env.DEV === true`. - Im Dev-Modus: gibt Mock-Implementierungen zurück - Im Prod-Modus: gibt echte API-Clients zurück **`src/mock/signalk.mock.js`:** Simuliert einen SignalK WebSocket mit realistischen Bootsdaten. Werte ändern sich kontinuierlich und realistisch: - SOG: 4–7 Knoten, sanfte Schwankungen - COG/Heading: 200–230°, leichte Drift - Tiefe: 8–20m, langsame Änderung - Windgeschwindigkeit: 10–18 Knoten, böig - Windwinkel: 30–60° (am Wind), variabel - Motorendrehzahl: 1600–2000 RPM - Batterien: Starter 12.4–12.8V, Bord 24.8–25.4V - Wassertemperatur: 17–19°C - GPS: bewegt sich entlang einer realistischen Route (Ostsee-Koordinaten) Gibt ein EventEmitter-ähnliches Objekt zurück, das `on('delta', callback)` unterstützt. Format: echtes SignalK Delta-Format (`{"updates": [{"values": [...]}]}`) **`src/mock/snapcast.mock.js`:** Simuliert die Snapcast JSON-RPC API: - 4 Zonen (Salon, Cockpit, Bug, Heck) - Salon + Cockpit initial aktiv - Implementiert: `Server.GetStatus`, `Client.SetVolume`, `Client.SetMuted`, `Group.SetStream`, `Server.GetRPCVersion` **`src/mock/mopidy.mock.js`:** Simuliert die Mopidy WebSocket JSON-RPC API: - Tracks mit realistischen Metadaten (Titel, Künstler, Album, Dauer) - Wiedergabestatus: playing/paused/stopped - Position läuft mit - Implementiert: `playback.get_current_track`, `playback.get_state`, `playback.get_time_position`, `playback.play`, `playback.pause`, `playback.next`, `playback.previous`, `tracklist.get_tracks`, `library.browse`, `library.search` ### 6. Dashboard – API-Clients **`src/api/snapcast.js`:** Echter Snapcast JSON-RPC Client über WebSocket (`ws://host:1705`). Implementiert alle Methoden des Mock-Clients. Reconnect-Logik mit exponential backoff. **`src/api/signalk.js`:** Echter SignalK WebSocket Client (`ws://host:3000/signalk/v1/stream`). Abonniert alle relevanten Pfade: - `navigation.speedOverGround` - `navigation.courseOverGroundTrue` - `navigation.headingTrue` - `environment.depth.belowKeel` - `environment.wind.speedApparent` - `environment.wind.angleApparent` - `propulsion.*.revolutions` - `electrical.batteries.*.voltage` **`src/api/mopidy.js`:** Echter Mopidy JSON-RPC Client über WebSocket (`ws://host:6680/mopidy/ws`). **`src/api/jellyfin.js`:** Jellyfin REST API Client. Authentifizierung via API-Key aus `.env`. Implementiert: Musik browsen, Alben, Künstler, Suche, Stream-URL. ### 7. Dashboard – Hooks **`useNMEA()`:** Verbindet sich mit SignalK (echt oder mock). Gibt strukturiertes Objekt zurück: ```js { sog, cog, heading, depth, windSpeed, windAngle, windDirection, lat, lon, rpm, battery1, battery2, waterTemp, airTemp, rudder, fuel } ``` **`useZones()`:** Verbindet sich mit Snapcast. Gibt zurück: ```js { zones, // Array aller Zonen mit {id, name, active, volume, source, muted} setVolume, // (zoneId, volume) => void setMuted, // (zoneId, muted) => void setSource, // (zoneId, streamId) => void toggleZone, // (zoneId) => void } ``` **`usePlayer()`:** Verbindet sich mit Mopidy und Spotify-Status-API. Gibt zurück: ```js { currentTrack, // {title, artist, album, duration, coverUrl} state, // 'playing' | 'paused' | 'stopped' position, // Sekunden activeSource, // 'spotify' | 'airplay' | 'mopidy' | 'jellyfin' play, pause, next, previous, seek } ``` ### 8. Dashboard – Design **Farbschema:** Nautisch-dunkel. Hauptfarben: - Background: `#07111f` (tiefes Marineblau) - Surface: `#0a1928` - Border: `#1e2a3a` - Text primary: `#e2eaf2` - Text muted: `#4a6080` - Accent: `#38bdf8` (Cyan/Himmelblau) - Success: `#34d399` - Warning: `#f59e0b` - Danger: `#ef4444` - Spotify: `#1DB954` - AirPlay: `#60a5fa` **Schriften:** `DM Mono` für Instrumentenwerte, `DM Sans` für UI-Text. **Instrumente (SVG-basiert, animiert):** - Rundinstrumente mit Zeiger (Gauge.jsx): SOG, Tiefe, RPM, Wassertemp - Kompassrose (animiert, dreht sich): Heading - Windrose: Windrichtung + Geschwindigkeit - Alle Instrumente reagieren flüssig auf Daten-Updates (CSS transitions) **Touch-Optimierung:** - Alle Buttons mindestens 44×44px - Volume Slider touch-fähig (pointer events) - Keine Hover-only Interaktionen **Tabs:** - Übersicht: Alle Instrumente + Now Playing + Zonen-Schnellübersicht - Navigation: Seekarte (SignalK iframe / Placeholder) + Detaildaten - Audio: Vollständige Zonen-Steuerung + Quelle + Radio + Bibliothek - Systeme: Batterien, Motor, Kraftstoff, Container-Status **DEV-Indikator:** Wenn `import.meta.env.DEV === true`: kleines Badge "DEV · MOCK DATA" in der TopBar. ### 9. Snapserver Konfiguration ```ini [stream] source = pipe:///tmp/audio/spotify.pcm?name=Spotify&codec=pcm&sampleformat=44100:16:2&chunk_ms=20 source = pipe:///tmp/audio/airplay.pcm?name=AirPlay&codec=pcm&sampleformat=44100:16:2&chunk_ms=20 source = pipe:///tmp/audio/mopidy.pcm?name=Mopidy&codec=pcm&sampleformat=44100:16:2&chunk_ms=20 [server] threads = -1 [http] enabled = true bind_to_address = 0.0.0.0 port = 1780 [logging] sink = system filter = *:info ``` ### 10. Shairport-sync Konfiguration ```conf general = { name = "Bordanlage AirPlay"; port = 5000; interpolation = "auto"; output_backend = "pipe"; }; pipe = { name = "/tmp/audio/airplay.pcm"; }; ``` ### 11. README.md Erstelle eine vollständige README mit: - Voraussetzungen (Docker Desktop, make) - Schnellstart (`make dev` → http://localhost:8080) - Alle Service-URLs - Anleitung: Wie erscheint der Spotify Connect Endpunkt? - Anleitung: Wie erscheint der AirPlay Endpunkt? - Anleitung: Wie füge ich Musik hinzu? (./music Ordner) - Anleitung: Wie verbinde ich echte NMEA-Hardware? - Migration Boot: Was muss geändert werden? (network_mode, soundcards, dri) - Troubleshooting (häufige Fehler auf Mac/Windows) --- ## Implementierungsreihenfolge 1. Projektstruktur und alle Konfigurationsdateien anlegen 2. `docker-compose.yml` + `docker-compose.dev.yml` + `.env` + `Makefile` 3. Mopidy Custom-Dockerfile mit Plugins 4. Dashboard Grundgerüst (Vite + React Setup) 5. Mock-System vollständig implementieren 6. Echte API-Clients implementieren 7. Hooks implementieren (useNMEA, useZones, usePlayer) 8. UI-Komponenten (Instrumente, Audio, Navigation) 9. Pages (Overview, Navigation, Audio, Systems) 10. Dashboard Dockerfile + Nginx-Config 11. README.md --- ## Wichtige Hinweise für die Implementierung - **Spotify Connect** funktioniert über mDNS. Auf Mac/Windows muss Port 57621 (UDP) gemappt sein und `--zeroconf-port 57621` an librespot übergeben werden. Alternativ: Spotify-Nutzer kann den Endpunkt auch manuell über "Gerät verbinden" mit der IP des Computers finden. - **AirPlay** braucht Bonjour/mDNS. Auf Mac läuft Bonjour nativ, daher kann shairport-sync mit `network_mode: host` auf Mac funktionieren wenn `--host` Netzwerk erlaubt ist. Auf Windows WSL2 muss avahi im Container laufen. - **Named Pipes** müssen VOR dem Start existieren. Das `init-pipes.sh` Skript legt sie an. In Docker: via `entrypoint` sicherstellen dass die Pipes existieren bevor der eigentliche Prozess startet. - **Multiroom** bedeutet: alle aktiven Snapclients spielen synchron denselben Stream. Jede Zone kann aber einen anderen Stream (Quelle) zugewiesen bekommen. - **Fehlertoleranz**: Wenn eine API nicht erreichbar ist (z.B. SignalK offline), soll das Dashboard nicht abstürzen – stattdessen graceful degradation mit "Nicht verbunden" Anzeige. Starte jetzt mit der Implementierung. Beginne mit der Verzeichnisstruktur und arbeite dich dann systematisch durch alle Komponenten.