Files
boWave/plan.md
denshooter 946c0a5377 Initial implementation: Bordanlage boat onboard system
Complete multiroom audio + navigation dashboard:
- Docker stack: SignalK, Snapcast (4 zones), librespot, shairport-sync, Mopidy, Jellyfin, Portainer
- React 18 + Vite dashboard with nautical dark theme
- Full mock system (SignalK NMEA simulation, Snapcast zones, Mopidy player)
- Real API clients for all services with reconnect logic
- SVG instruments: Compass, WindRose, Gauge, DepthSounder, SpeedLog
- Pages: Overview, Navigation, Audio (zones/radio/library), Systems
- Dev mode runs fully without hardware (make dev)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 11:47:33 +01:00

16 KiB
Raw Blame History

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 <zonename>
  • 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 <name> --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

# 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

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: 47 Knoten, sanfte Schwankungen
  • COG/Heading: 200230°, leichte Drift
  • Tiefe: 820m, langsame Änderung
  • Windgeschwindigkeit: 1018 Knoten, böig
  • Windwinkel: 3060° (am Wind), variabel
  • Motorendrehzahl: 16002000 RPM
  • Batterien: Starter 12.412.8V, Bord 24.825.4V
  • Wassertemperatur: 1719°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:

{
  sog, cog, heading, depth, windSpeed, windAngle, windDirection,
  lat, lon, rpm, battery1, battery2, waterTemp, airTemp, rudder, fuel
}

useZones(): Verbindet sich mit Snapcast. Gibt zurück:

{
  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:

{
  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

[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

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 devhttp://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.