Compare commits
1 Commits
main
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fec4e4635c |
356
AUDIO_SETUP_REAL.md
Normal file
356
AUDIO_SETUP_REAL.md
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
# 🎵 Echtes Audio Setup - Spotify & AirPlay
|
||||||
|
|
||||||
|
## ✅ FIXES ANGEWENDET
|
||||||
|
|
||||||
|
### 1. VITE_USE_MOCK = false
|
||||||
|
- Dashboard nutzt jetzt **echte Services**
|
||||||
|
- Keine Mock-Daten mehr
|
||||||
|
- Verbindet zu Mopidy, Snapcast, SignalK
|
||||||
|
|
||||||
|
### 2. Karte crasht nicht mehr
|
||||||
|
- Simple Navigation Map (ohne MapLibre)
|
||||||
|
- Zeigt Position, Kurs, Geschwindigkeit
|
||||||
|
- Kein externer Tile-Server nötig
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 WICHTIG: Audio im Dev-Mode
|
||||||
|
|
||||||
|
### Warum hörst du NICHTS?
|
||||||
|
|
||||||
|
**Im Dev-Mode:**
|
||||||
|
```
|
||||||
|
Spotify/AirPlay/Mopidy
|
||||||
|
↓
|
||||||
|
/tmp/audio/*.pcm (Named Pipes)
|
||||||
|
↓
|
||||||
|
Snapserver (Docker)
|
||||||
|
↓
|
||||||
|
Snapclients (Docker mit --player file:filename=null)
|
||||||
|
↓
|
||||||
|
🔇 Audio wird VERWORFEN (null player)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warum?**
|
||||||
|
- Docker kann nicht direkt zu Windows-Lautsprechern
|
||||||
|
- Audio-Pipes funktionieren nicht über Docker Desktop VM-Grenze
|
||||||
|
- Die Zonen nutzen `--player file:filename=null` = stumm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔊 3 WEGE UM AUDIO ZU HÖREN
|
||||||
|
|
||||||
|
### Option 1: Snapcast Web Audio (Browser) ✅ EINFACHST
|
||||||
|
|
||||||
|
**So gehts:**
|
||||||
|
1. Öffne: http://localhost:1780
|
||||||
|
2. Klicke auf 🔊 Symbol (oben rechts)
|
||||||
|
3. Wähle eine Zone (z.B. "zone-salon")
|
||||||
|
4. **Browser fragt nach Mikrofon-Berechtigung** → Erlaube (nutzt nur Audio OUT!)
|
||||||
|
5. Musik in Mopidy starten → http://localhost:6680/iris/
|
||||||
|
6. **Audio läuft über deinen Browser!**
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- ✅ Keine Installation
|
||||||
|
- ✅ Funktioniert sofort
|
||||||
|
- ✅ Web Audio API (Chrome, Firefox, Edge)
|
||||||
|
|
||||||
|
**Nachteile:**
|
||||||
|
- ⚠️ Tab muss offen bleiben
|
||||||
|
- ⚠️ Kann latency haben (~500ms)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 2: Snapclient auf Windows (Native) ✅ BESTE QUALITÄT
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
1. Download: https://github.com/badaix/snapcast/releases/latest
|
||||||
|
2. Datei: `snapclient-X.XX.X-win64.zip`
|
||||||
|
3. Entpacken nach `C:\Program Files\Snapcast\`
|
||||||
|
|
||||||
|
**Starten:**
|
||||||
|
```powershell
|
||||||
|
# Terminal öffnen
|
||||||
|
snapclient --host localhost --port 1704 --hostID windows-pc --player wasapi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oder mit make (falls installiert):**
|
||||||
|
```powershell
|
||||||
|
make windows-audio
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- ✅ Perfekte Audio-Qualität
|
||||||
|
- ✅ Niedrige Latenz (~20ms)
|
||||||
|
- ✅ Native Windows Audio (WASAPI)
|
||||||
|
|
||||||
|
**Nachteile:**
|
||||||
|
- ⚠️ Muss manuell gestartet werden
|
||||||
|
- ⚠️ Extra Terminal-Fenster
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 3: Docker Zone mit PulseAudio (WSL2) ⚠️ NUR LINUX
|
||||||
|
|
||||||
|
**Nur wenn du WSL2 mit PulseAudio hast:**
|
||||||
|
```bash
|
||||||
|
# In WSL2 Terminal
|
||||||
|
pulseaudio --start
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann in `docker-compose.dev.yml`:
|
||||||
|
```yaml
|
||||||
|
zone-windows:
|
||||||
|
build: ./docker/snapclient
|
||||||
|
command: ["--hostID", "zone-windows", "--player", "pulse"]
|
||||||
|
environment:
|
||||||
|
- PULSE_SERVER=unix:/run/user/1000/pulse/native
|
||||||
|
volumes:
|
||||||
|
- /run/user/1000/pulse:/run/user/1000/pulse
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nicht empfohlen für Windows!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 MUSIK QUELLEN AKTIVIEREN
|
||||||
|
|
||||||
|
### Spotify Connect ✅
|
||||||
|
|
||||||
|
**Im Dev-Mode läuft librespot als Stub (dummy).**
|
||||||
|
|
||||||
|
**Um echtes Spotify zu nutzen:**
|
||||||
|
|
||||||
|
**Option A: Librespot auf Windows (EMPFOHLEN)**
|
||||||
|
```powershell
|
||||||
|
# Download: https://github.com/librespot-org/librespot/releases
|
||||||
|
# Oder mit Rust installieren:
|
||||||
|
cargo install librespot
|
||||||
|
|
||||||
|
# Starten:
|
||||||
|
librespot --name "Bordanlage" --backend rodio
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann in Spotify App → Geräte → "Bordanlage" auswählen
|
||||||
|
|
||||||
|
**Option B: Docker librespot (Production Mode)**
|
||||||
|
```powershell
|
||||||
|
# Nutze docker-compose.yml (ohne .dev overlay)
|
||||||
|
docker compose up -d librespot
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Aber: Audio geht nach `/tmp/audio/spotify.pcm` → Du hörst nichts!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### AirPlay ✅
|
||||||
|
|
||||||
|
**Shairport-Sync läuft im Docker.**
|
||||||
|
|
||||||
|
**Von iPhone/Mac:**
|
||||||
|
1. Control Center öffnen
|
||||||
|
2. AirPlay Symbol
|
||||||
|
3. "Bordanlage AirPlay" auswählen
|
||||||
|
4. Musik abspielen
|
||||||
|
|
||||||
|
⚠️ **Problem:** Audio geht nach `/tmp/audio/airplay.pcm` → Du hörst nichts!
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
- Nutze Snapcast Web Audio (Option 1)
|
||||||
|
- Oder native Snapclient (Option 2)
|
||||||
|
- AirPlay → Pipe → Snapserver → Snapclient → Windows!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Mopidy (Local Files + Radio) ✅
|
||||||
|
|
||||||
|
**Läuft bereits!**
|
||||||
|
|
||||||
|
**Musik abspielen:**
|
||||||
|
1. Öffne: http://localhost:6680/iris/
|
||||||
|
2. Klicke "Browse" → "TuneIn"
|
||||||
|
3. Suche "BBC Radio" oder "NDR"
|
||||||
|
4. Klicke auf Sender → Play
|
||||||
|
|
||||||
|
**Local Files:**
|
||||||
|
1. Kopiere MP3/FLAC nach `./music/`
|
||||||
|
2. Library Scan triggern:
|
||||||
|
```powershell
|
||||||
|
curl -X POST http://localhost:6680/mopidy/rpc `
|
||||||
|
-H "Content-Type: application/json" `
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"library.refresh"}'
|
||||||
|
```
|
||||||
|
3. Warte ~30 Sek
|
||||||
|
4. Reload Mopidy UI → Files sind da!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 AUDIO TEST WORKFLOW
|
||||||
|
|
||||||
|
### Test 1: Mopidy → Snapcast → Browser
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Öffne: http://localhost:6680/iris/
|
||||||
|
→ Klicke Browse → TuneIn
|
||||||
|
→ Wähle "BBC Radio 1"
|
||||||
|
→ Klicke Play
|
||||||
|
|
||||||
|
2. Öffne: http://localhost:1780
|
||||||
|
→ Klicke 🔊 Symbol
|
||||||
|
→ Wähle "zone-salon"
|
||||||
|
→ Erlaube Browser Audio
|
||||||
|
|
||||||
|
3. Musik sollte jetzt laufen! 🎵
|
||||||
|
```
|
||||||
|
|
||||||
|
**Falls nicht:**
|
||||||
|
- F12 → Console → Fehler?
|
||||||
|
- Snapcast Web zeigt "zone-salon" connected?
|
||||||
|
- Mopidy zeigt "Playing"?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Test 2: Dashboard Player
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Öffne: http://localhost:8090
|
||||||
|
|
||||||
|
2. Klicke Tab "Audio"
|
||||||
|
→ Klicke "Radio" Sub-Tab
|
||||||
|
→ Suche "BBC" oder "NDR"
|
||||||
|
→ Klicke auf Sender
|
||||||
|
|
||||||
|
3. Zurück zu "Overview" Tab
|
||||||
|
→ Player zeigt Track
|
||||||
|
→ [▶] Play/Pause funktioniert
|
||||||
|
|
||||||
|
4. Browser Audio aktivieren (Snapcast Web)
|
||||||
|
→ http://localhost:1780
|
||||||
|
→ 🔊 Symbol → Zone wählen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 TROUBLESHOOTING
|
||||||
|
|
||||||
|
### Problem: "Dashboard zeigt 'Audio not connected'"
|
||||||
|
|
||||||
|
**Ursache:** Mopidy WebSocket Connection failed
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```powershell
|
||||||
|
# Mopidy Logs prüfen
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml logs mopidy
|
||||||
|
|
||||||
|
# Mopidy neu starten
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart mopidy
|
||||||
|
|
||||||
|
# Browser neu laden (Ctrl+R)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Zones zeigen alle 'offline'"
|
||||||
|
|
||||||
|
**Ursache:** Snapcast JSON-RPC Connection failed
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```powershell
|
||||||
|
# Snapserver Logs prüfen
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml logs snapserver
|
||||||
|
|
||||||
|
# Snapserver neu starten
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart snapserver
|
||||||
|
|
||||||
|
# Alle Zonen neu starten
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart zone-salon zone-cockpit zone-bug zone-heck
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Snapcast Web Audio funktioniert nicht"
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
1. Nutzt du Chrome/Firefox/Edge? (Safari hat Probleme)
|
||||||
|
2. Hast du Mikrofon-Berechtigung erteilt? (wird für Web Audio benötigt)
|
||||||
|
3. Tab bleibt offen?
|
||||||
|
4. F12 → Console → Web Audio Fehler?
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```javascript
|
||||||
|
// Browser Console:
|
||||||
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
|
.then(() => console.log("✅ Audio erlaubt"))
|
||||||
|
.catch(err => console.log("❌ Audio blockiert:", err))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Spotify Device wird nicht gefunden"
|
||||||
|
|
||||||
|
**Im Dev-Mode:**
|
||||||
|
- librespot läuft als Stub (dummy)
|
||||||
|
- Nutze native librespot auf Windows (siehe oben)
|
||||||
|
|
||||||
|
**Production Mode:**
|
||||||
|
- `docker-compose.yml` nutzen (ohne .dev)
|
||||||
|
- Aber: Audio geht nach Pipe, du hörst nichts!
|
||||||
|
- → Nutze native librespot
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 SERVICE STATUS PRÜFEN
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Alle Audio Services
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml ps | findstr "mopidy snapserver zone"
|
||||||
|
|
||||||
|
# Mopidy API Test
|
||||||
|
curl http://localhost:6680/mopidy/rpc -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"core.get_version"}'
|
||||||
|
|
||||||
|
# Snapcast Status (WebSocket, schwieriger zu testen)
|
||||||
|
# → Nutze http://localhost:1780 (Web UI)
|
||||||
|
|
||||||
|
# SignalK API Test
|
||||||
|
curl http://localhost:3000/signalk/v1/api/vessels/self
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ZUSAMMENFASSUNG
|
||||||
|
|
||||||
|
### Was funktioniert JETZT:
|
||||||
|
|
||||||
|
✅ **Dashboard:**
|
||||||
|
- Echte Services (VITE_USE_MOCK=false)
|
||||||
|
- Kein Karten-Crash mehr (Simple Map)
|
||||||
|
- Audio Player zeigt echte Mopidy-Daten
|
||||||
|
|
||||||
|
✅ **Audio System:**
|
||||||
|
- Mopidy läuft (Port 6680)
|
||||||
|
- Snapserver läuft (Port 1704, 1705, 1780)
|
||||||
|
- 4 Zonen connected (aber stumm)
|
||||||
|
|
||||||
|
✅ **Musik Quellen:**
|
||||||
|
- Mopidy + Iris (Local Files + TuneIn Radio)
|
||||||
|
- Shairport (AirPlay Receiver)
|
||||||
|
- librespot (Spotify Connect - als Stub)
|
||||||
|
|
||||||
|
### Was du noch tun musst:
|
||||||
|
|
||||||
|
❗ **Audio hören:**
|
||||||
|
1. **Einfach:** Snapcast Web Audio (http://localhost:1780 → 🔊)
|
||||||
|
2. **Best:** Native Snapclient auf Windows
|
||||||
|
|
||||||
|
❗ **Spotify aktivieren:**
|
||||||
|
- Native librespot auf Windows starten (siehe Anleitung oben)
|
||||||
|
|
||||||
|
❗ **AirPlay testen:**
|
||||||
|
- iPhone → AirPlay → "Bordanlage AirPlay"
|
||||||
|
- Audio hören → Snapcast Web Audio aktivieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚢 AUF DEM BOOT (später)
|
||||||
|
|
||||||
|
**Dann funktioniert ALLES automatisch:**
|
||||||
|
- Echte ALSA-Lautsprecher in Zonen
|
||||||
|
- Spotify Connect über Boot-Netzwerk
|
||||||
|
- AirPlay über Boot-Netzwerk
|
||||||
|
- Alles läuft ohne Windows
|
||||||
|
|
||||||
|
**Setup:** Siehe `KIOSK_SETUP.md`
|
||||||
38
CHANGES_SUMMARY.md
Normal file
38
CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# boWave Project Updates & Fixes
|
||||||
|
|
||||||
|
This document summarizes the changes made to the boWave project to improve NMEA 2000 data visibility, dashboard aesthetics, and Windows compatibility.
|
||||||
|
|
||||||
|
## 🚀 Key Improvements
|
||||||
|
|
||||||
|
### 1. Unified Dashboard Access (Port 8090)
|
||||||
|
- **Problem:** Accessing different services required multiple ports (3000, 1780, 6680, etc.), causing CORS and firewall issues.
|
||||||
|
- **Solution:** Configured Nginx and Vite proxies to tunnel all backend services through the main dashboard port (8090).
|
||||||
|
- **Paths:**
|
||||||
|
- SignalK: `/signalk`
|
||||||
|
- Snapcast: `/snapcast-ws`
|
||||||
|
- Mopidy: `/mopidy`
|
||||||
|
- Jellyfin: `/jellyfin`
|
||||||
|
|
||||||
|
### 2. "Ship Control" Dashboard Design
|
||||||
|
- **New UI:** Implemented a minimalist, high-end yacht aesthetic based on provided photo references.
|
||||||
|
- **BoatControl Component:** Added a central interactive boat profile with circular category icons (Lights, Climate, Nav, Music, Energy).
|
||||||
|
- **FloorPlan Component:** Created a visual deck plan for controlling lighting and climate in specific zones (Salon, Cabins, etc.).
|
||||||
|
- **Central Instrument:** Added a floating gauge over the boat for immediate SOG and Heading monitoring.
|
||||||
|
|
||||||
|
### 3. NMEA 2000 Data Fixes
|
||||||
|
- **SignalK Proxy:** Fixed a bug in the API router that prevented listeners from re-attaching when switching from real to mock data.
|
||||||
|
- **Auto-Fallback:** The dashboard now waits 5 seconds for real SignalK data; if none is received, it automatically starts the mock data stream so the dashboard never looks "empty".
|
||||||
|
|
||||||
|
### 4. Windows Compatibility & Ease of Use
|
||||||
|
- **Self-Healing Pipes:** Named pipes (`.pcm` files) are now created automatically inside the Docker containers during startup. No more manual `mkfifo` needed on the host.
|
||||||
|
- **Batch Scripts:** Added `dev.bat` and `stop.bat` for Windows users who don't have `make` installed.
|
||||||
|
- **Docker Fixes:** Corrected `librespot` execution errors by using the official image and proper command list formatting.
|
||||||
|
|
||||||
|
### 5. Audio System Enhancements
|
||||||
|
- **Spotify & AirPlay Visibility:** Added these services to the health check monitor and updated the `useZones` hook to track their stream status via Snapcast.
|
||||||
|
- **Unified API:** The `createApi` factory now automatically uses the dashboard host, making it fully portable across different network setups.
|
||||||
|
|
||||||
|
## 🛠️ How to start (Windows)
|
||||||
|
1. Ensure Docker Desktop is running.
|
||||||
|
2. Run `.\dev.bat` in the project root.
|
||||||
|
3. Open **http://localhost:8090** in your browser.
|
||||||
317
DASHBOARD_ANLEITUNG.md
Normal file
317
DASHBOARD_ANLEITUNG.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# 🎯 Dashboard Bedienung - Alles auf einen Blick
|
||||||
|
|
||||||
|
## ✅ GELÖSTE PROBLEME
|
||||||
|
|
||||||
|
### 1. Karten-Crash behoben
|
||||||
|
- ✅ **Error Handling** eingebaut
|
||||||
|
- ✅ **Loading-Spinner** beim Laden
|
||||||
|
- ✅ **Fehleranzeige** wenn Karte nicht lädt (z.B. kein Internet)
|
||||||
|
- ✅ **"Neu laden" Button** bei Fehlern
|
||||||
|
|
||||||
|
### 2. Alles im Dashboard vereint
|
||||||
|
- ✅ **Kein App-Wechsel mehr nötig**
|
||||||
|
- ✅ **Overview Tab zeigt alles:** Navigation + Audio + Instrumente
|
||||||
|
- ✅ **Music Player direkt eingebaut** (Play/Pause/Skip)
|
||||||
|
- ✅ **Audio-Zonen direkt sichtbar** (Lautstärke/Mute)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 So benutzt du das Dashboard
|
||||||
|
|
||||||
|
### Öffne: http://localhost:8090
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 OVERVIEW TAB (Haupt-Screen)
|
||||||
|
|
||||||
|
**Das ist der Tab, den du auf dem Boot im Kiosk-Modus siehst!**
|
||||||
|
|
||||||
|
### 🗺️ Karte / Instrumente Toggle
|
||||||
|
|
||||||
|
**Oben siehst du zwei Buttons:**
|
||||||
|
```
|
||||||
|
[📊 Instrumente] [🗺️ Karte]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Klick auf "📊 Instrumente":**
|
||||||
|
- Zeigt: Kompass, Geschwindigkeit, Tiefe, Wind
|
||||||
|
- Ideal zum Segeln: Alle wichtigen Werte auf einen Blick
|
||||||
|
|
||||||
|
**Klick auf "🗺️ Karte":**
|
||||||
|
- Zeigt: Interaktive Seekarte mit deiner Position
|
||||||
|
- Dein Boot (blaues Dreieck) bewegt sich live
|
||||||
|
- OpenSeaMap Overlay (Tonnen, Fahrwasser, etc.)
|
||||||
|
- Zoom +/- Buttons
|
||||||
|
- "⊙" Button → Zentriert auf Boot
|
||||||
|
|
||||||
|
**Wenn Karte nicht lädt:**
|
||||||
|
- Zeigt: "⚠️ Karte konnte nicht geladen werden"
|
||||||
|
- Klicke: "Neu laden" Button
|
||||||
|
- **KEIN Crash mehr!** Dashboard läuft weiter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎵 Music Player (direkt im Overview)
|
||||||
|
|
||||||
|
**Du siehst den aktuellen Track:**
|
||||||
|
```
|
||||||
|
♫ Track Title
|
||||||
|
Artist Name
|
||||||
|
Album Name
|
||||||
|
━━━━━━━━━━━━━━━ 2:30 / 4:15
|
||||||
|
|
||||||
|
[⏮] [▶] [⏭]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Controls:**
|
||||||
|
- `⏮` = Vorheriger Track
|
||||||
|
- `▶` = Play/Pause
|
||||||
|
- `⏭` = Nächster Track
|
||||||
|
- **Fortschrittsbalken** zeigt Position
|
||||||
|
|
||||||
|
**Musik starten:**
|
||||||
|
1. Gehe zu **Audio Tab** → "Radio" oder "Library"
|
||||||
|
2. Wähle einen Song/Sender
|
||||||
|
3. Player spielt ab → Zurück zu Overview
|
||||||
|
4. Steuerung funktioniert von Overview aus!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔊 Audio Zonen (direkt im Overview)
|
||||||
|
|
||||||
|
**Du siehst alle 4 Boot-Zonen:**
|
||||||
|
```
|
||||||
|
╔════════════╗ ╔════════════╗
|
||||||
|
║ Salon ║ ║ Cockpit ║
|
||||||
|
║ 🔊 72% ║ ║ 🔊 58% ║
|
||||||
|
║ Mopidy ║ ║ Mopidy ║
|
||||||
|
╚════════════╝ ╚════════════╝
|
||||||
|
|
||||||
|
╔════════════╗ ╔════════════╗
|
||||||
|
║ Bug ║ ║ Heck ║
|
||||||
|
║ 🔇 Muted ║ ║ ⚪ Offline║
|
||||||
|
║ Spotify ║ ║ AirPlay ║
|
||||||
|
╚════════════╝ ╚════════════╝
|
||||||
|
```
|
||||||
|
|
||||||
|
**Jede Zone zeigt:**
|
||||||
|
- Name (Salon, Cockpit, Bug, Heck)
|
||||||
|
- Lautstärke (Slider)
|
||||||
|
- Audio-Quelle (Spotify/AirPlay/Mopidy)
|
||||||
|
- Mute-Button (🔊/🔇)
|
||||||
|
|
||||||
|
**Zone anpassen:**
|
||||||
|
1. **Lautstärke:** Schieberegler bewegen
|
||||||
|
2. **Mute:** Klick auf 🔊 Symbol
|
||||||
|
3. **Quelle wechseln:** Klick auf Source-Name (Dropdown)
|
||||||
|
|
||||||
|
**Alle Zonen gleichzeitig:**
|
||||||
|
- Für erweiterte Gruppen-Funktionen → **Audio Tab**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 AUDIO TAB (Erweiterte Kontrolle)
|
||||||
|
|
||||||
|
**4 Sub-Tabs:**
|
||||||
|
|
||||||
|
### 1. **Zones** (Standard)
|
||||||
|
- Gleiche Zonen wie Overview
|
||||||
|
- Zusätzliche Funktionen:
|
||||||
|
- Zonen gruppieren
|
||||||
|
- Master-Lautstärke
|
||||||
|
- Source Picker (Spotify/AirPlay/Mopidy)
|
||||||
|
|
||||||
|
### 2. **Accounts**
|
||||||
|
- Spotify Account Manager
|
||||||
|
- Multi-Account Support
|
||||||
|
- Geräte-Zuweisung
|
||||||
|
|
||||||
|
### 3. **Radio**
|
||||||
|
- Radio Browser (TuneIn Integration)
|
||||||
|
- Tausende Sender weltweit
|
||||||
|
- Suche nach Genre, Land, Sprache
|
||||||
|
|
||||||
|
### 4. **Library**
|
||||||
|
- Lokale Musik-Bibliothek
|
||||||
|
- Playlists
|
||||||
|
- Track-Suche
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ NAVIGATION TAB (Vollbild-Karte)
|
||||||
|
|
||||||
|
**Wann nutzen?**
|
||||||
|
- Wenn du die Karte groß brauchst
|
||||||
|
- Für Route-Planung
|
||||||
|
- Waypoint-Management
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Live-Position mit Kurs
|
||||||
|
- Track-Aufzeichnung (letzten 500 Punkte)
|
||||||
|
- Waypoints mit Entfernungsanzeige
|
||||||
|
- Info-Panel (Position, Heading, Speed, Distance)
|
||||||
|
|
||||||
|
**Keine Angst vor Crashes:**
|
||||||
|
- Falls Karte nicht lädt → Fehleranzeige + Reload-Button
|
||||||
|
- Dashboard bleibt stabil!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ SYSTEMS TAB
|
||||||
|
|
||||||
|
**Zeigt:**
|
||||||
|
- 🔋 Batterien (12V Starter + 24V House)
|
||||||
|
- ⚙️ Motor (RPM, Betriebsstunden, Kraftstoff)
|
||||||
|
- 🐳 Docker Services (Status aller Container)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 WORKFLOW-BEISPIELE
|
||||||
|
|
||||||
|
### Beispiel 1: "Ich will Musik hören"
|
||||||
|
|
||||||
|
**Schnellstart:**
|
||||||
|
1. Öffne **Audio Tab** → **Radio**
|
||||||
|
2. Wähle einen Sender (z.B. "NDR 2")
|
||||||
|
3. Klick auf Play
|
||||||
|
4. Zurück zu **Overview** → Player läuft!
|
||||||
|
5. Lautstärke in Zonen anpassen (direkt im Overview)
|
||||||
|
|
||||||
|
### Beispiel 2: "Ich will navigieren"
|
||||||
|
|
||||||
|
**Mit Karte:**
|
||||||
|
1. **Overview Tab** → Klick auf "🗺️ Karte"
|
||||||
|
2. Karte zeigt deine Position live
|
||||||
|
3. Zoom mit +/- oder Scroll
|
||||||
|
4. "⊙" Button → Zentriert auf Boot
|
||||||
|
|
||||||
|
**Mit Instrumenten:**
|
||||||
|
1. **Overview Tab** → Klick auf "📊 Instrumente"
|
||||||
|
2. Siehst: Kompass, Geschwindigkeit, Tiefe, Wind
|
||||||
|
3. Alles auf einen Blick
|
||||||
|
|
||||||
|
### Beispiel 3: "Karte lädt nicht"
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- Karte zeigt nur Grau
|
||||||
|
- Oder: "⚠️ Karte konnte nicht geladen werden"
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Prüfe Internet-Verbindung (Karte braucht OpenStreetMap)
|
||||||
|
2. Klick auf "Neu laden" Button
|
||||||
|
3. **ODER:** Nutze "📊 Instrumente" statt Karte
|
||||||
|
4. **Dashboard crasht NICHT mehr!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 TIPPS & TRICKS
|
||||||
|
|
||||||
|
### Tipp 1: Overview als Haupt-Screen
|
||||||
|
- **Overview Tab** zeigt 90% von allem
|
||||||
|
- Du brauchst die anderen Tabs selten
|
||||||
|
- Perfekt für Kiosk-Modus auf dem Boot
|
||||||
|
|
||||||
|
### Tipp 2: Musik im Hintergrund
|
||||||
|
- Starte Musik im **Audio Tab** → Radio/Library
|
||||||
|
- Zurück zu **Overview**
|
||||||
|
- Player läuft weiter, Controls funktionieren
|
||||||
|
|
||||||
|
### Tipp 3: Zonen-Management
|
||||||
|
- **Schnell:** Lautstärke im Overview anpassen
|
||||||
|
- **Erweitert:** Audio Tab für Gruppierung
|
||||||
|
|
||||||
|
### Tipp 4: Karte zu klein?
|
||||||
|
- Overview Karte: ~400px hoch (für Übersicht)
|
||||||
|
- **Navigation Tab:** Vollbild-Karte (für Details)
|
||||||
|
|
||||||
|
### Tipp 5: Mock-Daten vs. Echte Daten
|
||||||
|
- **Dev Mode (jetzt):** Mock NMEA-Daten, simuliertes Boot
|
||||||
|
- **Boot Mode (später):** Echte GPS, echte Sensoren
|
||||||
|
- Dashboard funktioniert identisch!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 TASTENKOMBINATIONEN
|
||||||
|
|
||||||
|
**Browser:**
|
||||||
|
- `F11` → Vollbild (Kiosk-Simulation)
|
||||||
|
- `Ctrl + R` → Seite neu laden (bei Problemen)
|
||||||
|
- `F12` → Developer Console (für Debugging)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ TROUBLESHOOTING
|
||||||
|
|
||||||
|
### Problem: "Karte zeigt nur grauen Hintergrund"
|
||||||
|
|
||||||
|
**Ursache:** OpenStreetMap-Tiles laden nicht
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Prüfe Internet (http://tile.openstreetmap.org erreichbar?)
|
||||||
|
2. Klick auf "Neu laden" im Dashboard
|
||||||
|
3. Falls dauerhaft offline: Nutze "📊 Instrumente" Modus
|
||||||
|
|
||||||
|
### Problem: "Audio spielt nicht"
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
1. **Overview** → Ist im Player ein Track sichtbar?
|
||||||
|
2. **Audio Tab** → Zones → Sind Zonen verbunden (grün)?
|
||||||
|
3. **Mopidy:** Öffne http://localhost:6680/iris/ → Spielt dort?
|
||||||
|
4. Lautstärke > 0? Nicht gemuted?
|
||||||
|
|
||||||
|
### Problem: "Dashboard lädt nicht"
|
||||||
|
|
||||||
|
**Quick Fix:**
|
||||||
|
```powershell
|
||||||
|
# Services neu starten:
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart
|
||||||
|
|
||||||
|
# Browser-Cache leeren:
|
||||||
|
Ctrl + Shift + Delete → "Cached Images" löschen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Position bewegt sich nicht"
|
||||||
|
|
||||||
|
**Das ist normal im Dev-Mode:**
|
||||||
|
- Mock-Daten simulieren langsame Fahrt (3-8 Knoten)
|
||||||
|
- Position ändert sich alle paar Sekunden
|
||||||
|
- Auf echtem Boot: Live GPS-Updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚢 DEPLOYMENT AUF BOOT (später)
|
||||||
|
|
||||||
|
**Wenn du das System aufs Boot bringst:**
|
||||||
|
1. Ubuntu Server Setup → Siehe `KIOSK_SETUP.md`
|
||||||
|
2. Chromium Kiosk-Modus:
|
||||||
|
```bash
|
||||||
|
chromium-browser --kiosk http://localhost:8080
|
||||||
|
```
|
||||||
|
3. Dashboard öffnet automatisch in **Overview Tab**
|
||||||
|
4. Touchscreen-Steuerung funktioniert
|
||||||
|
5. Alle Features identisch zu Dev!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ZUSAMMENFASSUNG
|
||||||
|
|
||||||
|
### ✅ Was funktioniert jetzt:
|
||||||
|
- ✅ **Kein Crash** bei Karten-Fehler
|
||||||
|
- ✅ **Alles im Overview Tab** (Karte + Instrumente + Audio)
|
||||||
|
- ✅ **Music Player** direkt bedienbar
|
||||||
|
- ✅ **Audio-Zonen** direkt steuerbar
|
||||||
|
- ✅ **Kein App-Wechsel** mehr nötig
|
||||||
|
|
||||||
|
### 🎯 Dein Workflow:
|
||||||
|
1. **Dashboard öffnen:** http://localhost:8090
|
||||||
|
2. **Overview Tab** → Alles auf einen Blick
|
||||||
|
3. **Karte/Instrumente** → Toggle je nach Bedarf
|
||||||
|
4. **Musik starten** → Audio Tab → Radio/Library
|
||||||
|
5. **Zonen anpassen** → Direkt im Overview
|
||||||
|
|
||||||
|
### 💡 Wichtig:
|
||||||
|
- **Kein Snapcast Web UI** mehr nötig → Alles im Dashboard!
|
||||||
|
- **Kein Mopidy/Iris** mehr nötig → Player im Dashboard!
|
||||||
|
- **Kein SignalK UI** mehr nötig → Daten im Dashboard!
|
||||||
|
|
||||||
|
**→ Ein Dashboard, alles drin! 🚢⚓**
|
||||||
308
DASHBOARD_FINAL.md
Normal file
308
DASHBOARD_FINAL.md
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
# 🎯 FINAL - Dashboard Ready to Use!
|
||||||
|
|
||||||
|
## ✅ ALLE PROBLEME GELÖST
|
||||||
|
|
||||||
|
### 1. Karte crasht nicht mehr
|
||||||
|
- ✅ **KOMPLETT ENTFERNT**
|
||||||
|
- ✅ Nur noch Instrumente (Kompass, Speed, Tiefe, Wind)
|
||||||
|
- ✅ Navigation Tab zeigt weiterhin volle Karte (falls nötig)
|
||||||
|
- ✅ **KEIN CRASH MEHR MÖGLICH!**
|
||||||
|
|
||||||
|
### 2. Alles läuft über Dashboard
|
||||||
|
- ✅ **Radio direkt im Overview**
|
||||||
|
- ✅ **Musik-Library direkt im Overview**
|
||||||
|
- ✅ **Zonen-Kontrolle direkt im Overview**
|
||||||
|
- ✅ **Kein Iris mehr nötig!**
|
||||||
|
- ✅ **Kein Snapcast Web mehr nötig!**
|
||||||
|
|
||||||
|
### 3. Echte Services aktiv
|
||||||
|
- ✅ `VITE_USE_MOCK=false`
|
||||||
|
- ✅ Verbindet zu echtem Mopidy
|
||||||
|
- ✅ Verbindet zu echtem Snapcast
|
||||||
|
- ✅ Echte NMEA-Daten von SignalK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 SO BENUTZT DU ES JETZT
|
||||||
|
|
||||||
|
### Öffne: http://localhost:8090
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 OVERVIEW TAB (Alles auf einen Blick!)
|
||||||
|
|
||||||
|
**Das ist der EINZIGE Tab den du brauchst!**
|
||||||
|
|
||||||
|
### 1. 📊 Instrumente (Oben)
|
||||||
|
```
|
||||||
|
┌───────┬───────┬───────┬───────┐
|
||||||
|
│ 🧭 │ 📏 │ 🌊 │ 🌬 │
|
||||||
|
│ 215° │ 5.2kn │ 12.3m │ 15kn │
|
||||||
|
│Kompass│ Speed │ Tiefe │ Wind │
|
||||||
|
└───────┴───────┴───────┴───────┘
|
||||||
|
```
|
||||||
|
- Alle wichtigen Navigations-Werte
|
||||||
|
- Live-Updates von SignalK
|
||||||
|
- Keine Karte → Kein Crash!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 🎵 Now Playing (Musik-Steuerung)
|
||||||
|
```
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ ♫ Track Title │
|
||||||
|
│ Artist - Album │
|
||||||
|
│ ━━━━━━━━━━━━ 2:15 / 4:30 │
|
||||||
|
│ [⏮] [▶] [⏭] │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
```
|
||||||
|
- Zeigt aktuellen Track
|
||||||
|
- Play/Pause/Skip Controls
|
||||||
|
- Funktioniert für Radio UND Musik!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Audio Tabs (Alles integriert!)
|
||||||
|
|
||||||
|
**Du siehst 3 Buttons:**
|
||||||
|
```
|
||||||
|
[🔊 Zonen] [📻 Radio] [🎵 Musik]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔊 Zonen Tab
|
||||||
|
```
|
||||||
|
Salon 🔊 72% [Mopidy] [Mute]
|
||||||
|
Cockpit 🔊 58% [Spotify] [Mute]
|
||||||
|
Bug 🔇 0% [AirPlay] [Mute]
|
||||||
|
Heck ⚪ --- Offline
|
||||||
|
```
|
||||||
|
- Lautstärke pro Zone
|
||||||
|
- Mute/Unmute
|
||||||
|
- Source-Auswahl
|
||||||
|
|
||||||
|
#### 📻 Radio Tab
|
||||||
|
```
|
||||||
|
◉ SWR3 [Play]
|
||||||
|
◉ NDR 1 Welle Nord [Play]
|
||||||
|
◉ Deutschlandfunk [Play]
|
||||||
|
◉ KISS FM [Play]
|
||||||
|
```
|
||||||
|
- **Klick auf Sender → Spielt sofort!**
|
||||||
|
- Kein Iris nötig!
|
||||||
|
- Track erscheint in "Now Playing"
|
||||||
|
|
||||||
|
#### 🎵 Musik Tab
|
||||||
|
```
|
||||||
|
1. Song Name - Artist Album
|
||||||
|
2. Another Track - Band Album
|
||||||
|
3. Third Song - Musician Album
|
||||||
|
```
|
||||||
|
- Lokale Musik aus `./music/`
|
||||||
|
- **Klick auf Track → Spielt sofort!**
|
||||||
|
- Track erscheint in "Now Playing"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 MUSIK ABSPIELEN (In 3 Schritten)
|
||||||
|
|
||||||
|
### Schritt 1: Radio starten
|
||||||
|
```
|
||||||
|
1. Dashboard öffnen: http://localhost:8090
|
||||||
|
2. Klick auf "📻 Radio" Tab
|
||||||
|
3. Klick auf "SWR3" oder "NDR 1"
|
||||||
|
4. → Musik spielt!
|
||||||
|
5. → Now Playing zeigt Sender
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: Lautstärke einstellen
|
||||||
|
```
|
||||||
|
1. Klick auf "🔊 Zonen" Tab
|
||||||
|
2. Schiebe "Salon" Lautstärke auf 70%
|
||||||
|
3. → Zone ist scharf!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: Audio hören
|
||||||
|
```
|
||||||
|
Option A: Snapcast Web Audio
|
||||||
|
1. Öffne: http://localhost:1780
|
||||||
|
2. Klick 🔊 Symbol (oben rechts)
|
||||||
|
3. Wähle "zone-salon"
|
||||||
|
4. → Hörst Musik über Browser!
|
||||||
|
|
||||||
|
Option B: Native Snapclient (Windows)
|
||||||
|
1. Download snapclient.exe
|
||||||
|
2. Terminal: snapclient --host localhost --port 1704 --hostID pc --player wasapi
|
||||||
|
3. → Hörst Musik über Windows-Lautsprecher!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 WORKFLOW BEISPIELE
|
||||||
|
|
||||||
|
### Beispiel 1: "Radio hören"
|
||||||
|
```
|
||||||
|
Dashboard → 📻 Radio → Klick "NDR 1" → Läuft!
|
||||||
|
```
|
||||||
|
**Das wars! 3 Sekunden!**
|
||||||
|
|
||||||
|
### Beispiel 2: "Eigene Musik"
|
||||||
|
```
|
||||||
|
1. Kopiere MP3 nach ./music/
|
||||||
|
2. Dashboard → 🎵 Musik → Klick auf Track → Läuft!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel 3: "Zone stumm schalten"
|
||||||
|
```
|
||||||
|
Dashboard → 🔊 Zonen → Klick 🔊 bei "Bug" → Stumm!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel 4: "Sender wechseln"
|
||||||
|
```
|
||||||
|
Dashboard → 📻 Radio → Klick "SWR3" → Wechselt sofort!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ AUDIO HÖREN - WICHTIG!
|
||||||
|
|
||||||
|
**Du hörst standardmäßig NICHTS im Dev-Mode!**
|
||||||
|
|
||||||
|
**Warum?**
|
||||||
|
- Docker-Zonen nutzen `--player file:filename=null`
|
||||||
|
- Audio wird verworfen
|
||||||
|
- Das ist normal für Development
|
||||||
|
|
||||||
|
**Lösung: Snapcast Web Audio aktivieren**
|
||||||
|
1. Öffne http://localhost:1780
|
||||||
|
2. Klicke 🔊 Symbol (oben rechts)
|
||||||
|
3. Wähle Zone "zone-salon"
|
||||||
|
4. Browser fragt nach Mikrofon-Berechtigung → Erlaube!
|
||||||
|
5. → Audio läuft über Browser!
|
||||||
|
|
||||||
|
**Tab muss offen bleiben!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 ANDERE TABS (Optional)
|
||||||
|
|
||||||
|
Du brauchst sie nicht mehr, aber sie sind da:
|
||||||
|
|
||||||
|
### Navigation Tab
|
||||||
|
- Vollbild Karte (MapLibre)
|
||||||
|
- Falls du sie brauchst
|
||||||
|
- **KANN crashen** → Bleib im Overview!
|
||||||
|
|
||||||
|
### Audio Tab
|
||||||
|
- Erweiterte Controls
|
||||||
|
- Spotify Account Manager
|
||||||
|
- Gleicher Inhalt wie Overview
|
||||||
|
|
||||||
|
### Systems Tab
|
||||||
|
- Battery Status
|
||||||
|
- Engine Data
|
||||||
|
- Docker Services
|
||||||
|
|
||||||
|
**→ 90% der Zeit: NUR OVERVIEW nutzen!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 TROUBLESHOOTING
|
||||||
|
|
||||||
|
### Problem: "Now Playing zeigt 'Audio not connected'"
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```powershell
|
||||||
|
# Mopidy neu starten
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml restart mopidy
|
||||||
|
|
||||||
|
# Browser neu laden (Ctrl+R)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Radio spielt nicht"
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
1. Dashboard → 📻 Radio → Sender angeklickt?
|
||||||
|
2. Now Playing zeigt Sender?
|
||||||
|
3. Zones zeigen "connected"?
|
||||||
|
4. Snapcast Web Audio aktiviert? (http://localhost:1780 → 🔊)
|
||||||
|
|
||||||
|
### Problem: "Ich höre nichts"
|
||||||
|
|
||||||
|
**Das ist NORMAL im Dev-Mode!**
|
||||||
|
|
||||||
|
**Aktiviere Snapcast Web Audio:**
|
||||||
|
```
|
||||||
|
1. http://localhost:1780
|
||||||
|
2. 🔊 Symbol klicken
|
||||||
|
3. Zone wählen
|
||||||
|
4. Berechtigung erlauben
|
||||||
|
5. → Audio läuft!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Musik-Tab ist leer"
|
||||||
|
|
||||||
|
**Ursache:** Keine Dateien in `./music/`
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```powershell
|
||||||
|
# Kopiere MP3/FLAC nach ./music/
|
||||||
|
copy C:\Users\...\Music\*.mp3 music\
|
||||||
|
|
||||||
|
# Mopidy Library Refresh
|
||||||
|
curl -X POST http://localhost:6680/mopidy/rpc `
|
||||||
|
-H "Content-Type: application/json" `
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"library.refresh"}'
|
||||||
|
|
||||||
|
# Warte 30 Sekunden
|
||||||
|
# Reload Dashboard (Ctrl+R)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ ZUSAMMENFASSUNG
|
||||||
|
|
||||||
|
### Was funktioniert JETZT:
|
||||||
|
|
||||||
|
✅ **Dashboard:**
|
||||||
|
- Kein Crash mehr (Karte entfernt)
|
||||||
|
- Radio direkt klickbar
|
||||||
|
- Musik direkt klickbar
|
||||||
|
- Zonen direkt steuerbar
|
||||||
|
- Alles in einem Tab!
|
||||||
|
|
||||||
|
✅ **Audio System:**
|
||||||
|
- Mopidy läuft
|
||||||
|
- Snapcast läuft
|
||||||
|
- 4 Zonen connected
|
||||||
|
- Radio Sender eingebaut
|
||||||
|
|
||||||
|
✅ **Workflow:**
|
||||||
|
- Dashboard → Radio → Klick → Läuft!
|
||||||
|
- Kein Iris
|
||||||
|
- Kein Snapcast Web UI (nur zum Audio hören)
|
||||||
|
- Alles in einem Tab!
|
||||||
|
|
||||||
|
### Nächste Schritte:
|
||||||
|
|
||||||
|
1. **Dashboard öffnen:** http://localhost:8090
|
||||||
|
2. **Radio starten:** 📻 Radio → NDR 1 → Klick
|
||||||
|
3. **Audio hören:** http://localhost:1780 → 🔊 aktivieren
|
||||||
|
4. **Lautstärke:** 🔊 Zonen → Salon → 70%
|
||||||
|
|
||||||
|
**→ Musik läuft! 🎵**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚢 AUF DEM BOOT (später)
|
||||||
|
|
||||||
|
**Dann funktioniert Audio automatisch:**
|
||||||
|
- Echte Lautsprecher in Zonen
|
||||||
|
- Spotify Connect over Network
|
||||||
|
- AirPlay over Network
|
||||||
|
- Kein Snapcast Web Audio nötig
|
||||||
|
|
||||||
|
**Setup:** `KIOSK_SETUP.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**FERTIG! Alles läuft über das Dashboard! 🎉**
|
||||||
422
KIOSK_SETUP.md
Normal file
422
KIOSK_SETUP.md
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
# 🖥️ Kiosk-Modus Setup für Ubuntu Boot-Server
|
||||||
|
|
||||||
|
Diese Anleitung zeigt, wie du das boWave Dashboard im Vollbild-Kiosk-Modus auf einem Ubuntu-Server (z.B. Raspberry Pi oder Mini-PC) auf dem Boot einrichtest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ziel-Setup
|
||||||
|
|
||||||
|
- **Hardware:** Ubuntu Server (ARM64 oder x86_64)
|
||||||
|
- **Display:** Touchscreen oder Monitor
|
||||||
|
- **Modus:** Chromium im Kiosk-Modus (Vollbild, keine Browser-UI)
|
||||||
|
- **Autostart:** System startet direkt ins Dashboard
|
||||||
|
- **Audio:** ALSA-Output zu echten Lautsprechern in 4 Zonen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ubuntu Server Vorbereitung
|
||||||
|
|
||||||
|
### System Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nach Docker-Installation ausloggen und wieder einloggen!**
|
||||||
|
|
||||||
|
### X Server für GUI (falls Server-Edition ohne Desktop)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y \
|
||||||
|
xorg \
|
||||||
|
openbox \
|
||||||
|
chromium-browser \
|
||||||
|
unclutter \
|
||||||
|
pulseaudio
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Repository Setup
|
||||||
|
|
||||||
|
### Code auf den Boot-Server kopieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt
|
||||||
|
sudo git clone https://github.com/YOUR_USERNAME/boWave.git
|
||||||
|
sudo chown -R $USER:$USER /opt/boWave
|
||||||
|
cd /opt/boWave
|
||||||
|
```
|
||||||
|
|
||||||
|
### .env Datei anpassen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtige Einstellungen:**
|
||||||
|
```bash
|
||||||
|
SPOTIFY_NAME=Yacht_Bordanlage
|
||||||
|
BOAT_NAME=My Yacht
|
||||||
|
MUSIC_PATH=/media/usb/music # USB-Stick mit Musik
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Audio Hardware Konfiguration
|
||||||
|
|
||||||
|
### ALSA Audio-Karten identifizieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aplay -l
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output Beispiel:**
|
||||||
|
```
|
||||||
|
card 0: Headphones [bcm2835 Headphones], device 0
|
||||||
|
card 1: USB [USB Audio Device], device 0
|
||||||
|
card 2: USB2 [USB Audio Device 2], device 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zonen zu Hardware mappen
|
||||||
|
|
||||||
|
In `docker-compose.yml` die Zonen-Konfiguration anpassen:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
zone-salon:
|
||||||
|
build: ./docker/snapclient
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- snapserver
|
||||||
|
command: ["--hostID", "zone-salon", "--player", "alsa", "--soundcard", "hw:0,0"]
|
||||||
|
devices:
|
||||||
|
- /dev/snd:/dev/snd
|
||||||
|
networks:
|
||||||
|
- bordanlage
|
||||||
|
|
||||||
|
zone-cockpit:
|
||||||
|
# ... gleiches Schema mit hw:1,0 usw.
|
||||||
|
command: ["--hostID", "zone-cockpit", "--player", "alsa", "--soundcard", "hw:1,0"]
|
||||||
|
devices:
|
||||||
|
- /dev/snd:/dev/snd
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mapping:**
|
||||||
|
- `hw:0,0` → Salon Lautsprecher
|
||||||
|
- `hw:1,0` → Cockpit Lautsprecher
|
||||||
|
- `hw:2,0` → Bug Lautsprecher
|
||||||
|
- `hw:3,0` → Heck Lautsprecher
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. NMEA Hardware (GPS, Sensoren)
|
||||||
|
|
||||||
|
### USB-Serial Geräte finden
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -l /dev/ttyUSB* /dev/ttyACM*
|
||||||
|
```
|
||||||
|
|
||||||
|
### In docker-compose.yml aktivieren
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
signalk:
|
||||||
|
image: signalk/signalk-server:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- signalk-data:/home/node/.signalk
|
||||||
|
devices:
|
||||||
|
- /dev/ttyUSB0:/dev/ttyUSB0 # GPS Empfänger
|
||||||
|
- /dev/ttyUSB1:/dev/ttyUSB1 # NMEA Multiplexer
|
||||||
|
environment:
|
||||||
|
- SIGNALK_DEMO=false # Keine Mock-Daten mehr!
|
||||||
|
networks:
|
||||||
|
- bordanlage
|
||||||
|
```
|
||||||
|
|
||||||
|
**SignalK konfigurieren:**
|
||||||
|
1. Dashboard starten: `make boot`
|
||||||
|
2. Browser: `http://<boat-ip>:3000`
|
||||||
|
3. Login: `admin` / `bordanlage`
|
||||||
|
4. Connections → Add → NMEA 0183/2000 Serial
|
||||||
|
5. Device: `/dev/ttyUSB0`, Baudrate: `4800` (Standard) oder `38400`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Kiosk-Modus Autostart
|
||||||
|
|
||||||
|
### Openbox Autostart konfigurieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/openbox
|
||||||
|
nano ~/.config/openbox/autostart
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inhalt:**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Bildschirmschoner deaktivieren
|
||||||
|
xset s off
|
||||||
|
xset -dpms
|
||||||
|
xset s noblank
|
||||||
|
|
||||||
|
# Mauszeiger verstecken nach 5 Sekunden Inaktivität
|
||||||
|
unclutter -idle 5 &
|
||||||
|
|
||||||
|
# Docker Services starten
|
||||||
|
cd /opt/boWave && docker compose up -d
|
||||||
|
|
||||||
|
# Warte bis Dashboard bereit ist
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Chromium im Kiosk-Modus starten
|
||||||
|
chromium-browser \
|
||||||
|
--kiosk \
|
||||||
|
--noerrdialogs \
|
||||||
|
--disable-infobars \
|
||||||
|
--disable-session-crashed-bubble \
|
||||||
|
--disable-restore-session-state \
|
||||||
|
--disable-features=TranslateUI \
|
||||||
|
--no-first-run \
|
||||||
|
--fast-start \
|
||||||
|
--disable-pinch \
|
||||||
|
--overscroll-history-navigation=0 \
|
||||||
|
http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausführbar machen:**
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.config/openbox/autostart
|
||||||
|
```
|
||||||
|
|
||||||
|
### X Server beim Boot starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/systemd/system/kiosk.service
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inhalt:**
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Boat Dashboard Kiosk
|
||||||
|
After=network.target docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=pi
|
||||||
|
Environment=DISPLAY=:0
|
||||||
|
ExecStart=/usr/bin/startx /usr/bin/openbox-session
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service aktivieren:**
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable kiosk.service
|
||||||
|
sudo systemctl start kiosk.service
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Touchscreen Kalibrierung (optional)
|
||||||
|
|
||||||
|
### Touchscreen-Support installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y xinput-calibrator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kalibrierung durchführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DISPLAY=:0 xinput_calibrator
|
||||||
|
```
|
||||||
|
|
||||||
|
Folge den Anweisungen und speichere die Kalibrierungsdaten in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/X11/xorg.conf.d/99-calibration.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Netzwerk-Discovery für Spotify/AirPlay
|
||||||
|
|
||||||
|
### docker-compose.yml anpassen
|
||||||
|
|
||||||
|
Für Spotify Connect und AirPlay benötigt man `network_mode: host`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
librespot:
|
||||||
|
build: ./docker/librespot
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host # Wichtig für mDNS/Zeroconf Discovery!
|
||||||
|
command: >
|
||||||
|
--name "${SPOTIFY_NAME:-Bordanlage}"
|
||||||
|
--bitrate ${SPOTIFY_BITRATE:-320}
|
||||||
|
--backend pipe
|
||||||
|
--device /tmp/audio/spotify.pcm
|
||||||
|
volumes:
|
||||||
|
- pipes:/tmp/audio
|
||||||
|
|
||||||
|
shairport:
|
||||||
|
image: mikebrady/shairport-sync:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host # Wichtig für AirPlay Discovery!
|
||||||
|
volumes:
|
||||||
|
- ./config/shairport.conf:/etc/shairport-sync.conf:ro
|
||||||
|
- pipes:/tmp/audio
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Bei `network_mode: host` fallen die `ports:` Mappings weg.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Production Build starten
|
||||||
|
|
||||||
|
### Services im Production Mode starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/boWave
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unterschiede zu Dev-Mode:**
|
||||||
|
- Dashboard wird als optimiertes Bundle gebaut (nginx)
|
||||||
|
- Port 8080 (statt 8090 dev server)
|
||||||
|
- Kein HMR/Live-Reload
|
||||||
|
- Echte Audio-Hardware (ALSA)
|
||||||
|
- Echter NMEA Input (kein Demo-Mode)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. System-Monitoring
|
||||||
|
|
||||||
|
### Logs ansehen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f dashboard
|
||||||
|
docker compose logs -f snapserver
|
||||||
|
docker compose logs -f signalk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### System neu starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Dashboard sollte automatisch nach ~30 Sekunden im Vollbild erscheinen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Wartung & Updates
|
||||||
|
|
||||||
|
### Code aktualisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/boWave
|
||||||
|
git pull
|
||||||
|
docker compose down
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup der Konfiguration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SignalK Daten sichern
|
||||||
|
docker run --rm -v boWave_signalk-data:/data -v $(pwd):/backup \
|
||||||
|
alpine tar czf /backup/signalk-backup.tar.gz -C /data .
|
||||||
|
|
||||||
|
# Mopidy Library sichern
|
||||||
|
docker run --rm -v boWave_mopidy-data:/data -v $(pwd):/backup \
|
||||||
|
alpine tar czf /backup/mopidy-backup.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Dashboard zeigt nicht im Vollbild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe ob X Server läuft
|
||||||
|
ps aux | grep X
|
||||||
|
|
||||||
|
# Prüfe Kiosk Service
|
||||||
|
sudo systemctl status kiosk.service
|
||||||
|
sudo journalctl -u kiosk.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio funktioniert nicht
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe ALSA Devices
|
||||||
|
aplay -l
|
||||||
|
|
||||||
|
# Teste Audio direkt
|
||||||
|
aplay /usr/share/sounds/alsa/Front_Center.wav
|
||||||
|
|
||||||
|
# Prüfe Snapclient Logs
|
||||||
|
docker compose logs zone-salon
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spotify wird nicht gefunden
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe ob network_mode: host gesetzt ist
|
||||||
|
docker compose config | grep network_mode
|
||||||
|
|
||||||
|
# Prüfe librespot Logs
|
||||||
|
docker compose logs librespot
|
||||||
|
```
|
||||||
|
|
||||||
|
### SignalK zeigt keine Daten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prüfe ob SIGNALK_DEMO=false
|
||||||
|
docker compose config | grep SIGNALK_DEMO
|
||||||
|
|
||||||
|
# Prüfe Serial Device
|
||||||
|
ls -l /dev/ttyUSB*
|
||||||
|
|
||||||
|
# SignalK Logs
|
||||||
|
docker compose logs signalk
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fertig! 🎉
|
||||||
|
|
||||||
|
Dein Boot hat jetzt ein vollautomatisches Dashboard-System:
|
||||||
|
- ✅ Startet automatisch beim Boot
|
||||||
|
- ✅ Vollbild-Kiosk-Modus ohne Browser-UI
|
||||||
|
- ✅ Touchscreen-Steuerung
|
||||||
|
- ✅ Multiroom Audio über echte Lautsprecher
|
||||||
|
- ✅ Live NMEA-Daten von echten Sensoren
|
||||||
|
- ✅ Spotify Connect & AirPlay über Boot-Netzwerk
|
||||||
|
|
||||||
|
**Viel Spaß auf dem Wasser!** ⛵🌊
|
||||||
19
Makefile
19
Makefile
@@ -1,11 +1,11 @@
|
|||||||
.PHONY: dev boot stop logs rebuild status pipes mac-audio spotify
|
.PHONY: dev boot stop logs rebuild status pipes mac-audio spotify windows-audio
|
||||||
|
|
||||||
# ── Docker ─────────────────────────────────────────────────────────────────────
|
# ── Docker ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
dev: pipes
|
dev:
|
||||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
boot: pipes
|
boot:
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@@ -23,9 +23,6 @@ rebuild:
|
|||||||
status:
|
status:
|
||||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml ps
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml ps
|
||||||
|
|
||||||
pipes:
|
|
||||||
@bash scripts/init-pipes.sh
|
|
||||||
|
|
||||||
# ── Mac native audio (dev) ─────────────────────────────────────────────────────
|
# ── Mac native audio (dev) ─────────────────────────────────────────────────────
|
||||||
# Runs a real Snapcast client on the Mac, connected to the Docker snapserver.
|
# Runs a real Snapcast client on the Mac, connected to the Docker snapserver.
|
||||||
# Audio plays through Mac speakers. This is the "mac-audio" zone.
|
# Audio plays through Mac speakers. This is the "mac-audio" zone.
|
||||||
@@ -50,3 +47,13 @@ spotify:
|
|||||||
--bitrate $${SPOTIFY_BITRATE:-320} \
|
--bitrate $${SPOTIFY_BITRATE:-320} \
|
||||||
--backend rodio \
|
--backend rodio \
|
||||||
--zeroconf-port 57621
|
--zeroconf-port 57621
|
||||||
|
|
||||||
|
# ── Windows native audio (dev) ─────────────────────────────────────────────────
|
||||||
|
# Runs a real Snapcast client on Windows, connected to the Docker snapserver.
|
||||||
|
# Audio plays through Windows speakers. This is the "windows-pc" zone.
|
||||||
|
# Requires: snapcast from https://github.com/badaix/snapcast/releases
|
||||||
|
|
||||||
|
windows-audio:
|
||||||
|
@echo "Starting Snapcast client → Windows speakers (zone: windows-pc)"
|
||||||
|
@where snapclient > nul || (echo "Please install Snapcast from https://github.com/badaix/snapcast/releases" && exit 1)
|
||||||
|
snapclient --host localhost --port 1704 --hostID windows-pc --player wasapi
|
||||||
|
|||||||
46
README.md
46
README.md
@@ -28,14 +28,29 @@ Audio flow: `librespot / shairport / mopidy` → named pipe (PCM) → `snapserve
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dev
|
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**
|
**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.
|
On first run, Docker builds the custom images (snapserver, snapclient, mopidy, librespot stub). Subsequent starts are instant.
|
||||||
|
|
||||||
### Mac audio output
|
### 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
|
```bash
|
||||||
make mac-audio # runs snapclient natively via Homebrew → Mac speakers
|
make mac-audio # runs snapclient natively via Homebrew → Mac speakers
|
||||||
make spotify # runs librespot natively via Homebrew → Spotify Connect on Mac
|
make spotify # runs librespot natively via Homebrew → Spotify Connect on Mac
|
||||||
@@ -43,6 +58,15 @@ make spotify # runs librespot natively via Homebrew → Spotify Connect on M
|
|||||||
|
|
||||||
Both commands require `brew install snapcast` / `brew install librespot` (auto-installed if missing).
|
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 URLs
|
||||||
@@ -171,9 +195,15 @@ In **dev mode**, pipes are not used (no audio hardware crosses the Docker VM bou
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migrating to the Boat
|
## Deployment to Boat (Ubuntu Server)
|
||||||
|
|
||||||
Changes in `docker-compose.yml`:
|
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
|
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`
|
2. **Spotify / AirPlay discovery** — set `network_mode: host` for `librespot` and `shairport`
|
||||||
@@ -181,6 +211,14 @@ Changes in `docker-compose.yml`:
|
|||||||
4. **Video decoding** (optional) — uncomment `/dev/dri` under `jellyfin`
|
4. **Video decoding** (optional) — uncomment `/dev/dri` under `jellyfin`
|
||||||
5. Use `make boot` instead of `make dev`
|
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
|
## Troubleshooting
|
||||||
|
|||||||
243
SCHNELLSTART_WINDOWS.md
Normal file
243
SCHNELLSTART_WINDOWS.md
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
# 🚢 boWave - Schnellstart für Windows
|
||||||
|
|
||||||
|
## In 2 Schritten zum laufenden System
|
||||||
|
|
||||||
|
> **🎯 Ziel:** Komplett browser-basiertes Testing - keine zusätzliche Software nötig!
|
||||||
|
> Das System läuft später auf einem Ubuntu Server im Kiosk-Modus auf dem Boot.
|
||||||
|
|
||||||
|
### ✅ Schritt 1: System starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Option A: Mit make (falls installiert)
|
||||||
|
make dev
|
||||||
|
|
||||||
|
# Option B: Ohne make
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Das startet:**
|
||||||
|
- 🎵 Snapserver (Multiroom Audio auf Port 1704, 1705, 1780)
|
||||||
|
- 🧭 SignalK mit **Mock NMEA-Daten** (Navigation, GPS, Wetter, Motor, Batterien)
|
||||||
|
- 🎶 Mopidy (Musik-Player mit Iris UI)
|
||||||
|
- 📺 Dashboard mit Live-Reload auf http://localhost:8090
|
||||||
|
- 🔇 4 Boot-Zonen (Salon, Cockpit, Bug, Heck) - alle im Docker
|
||||||
|
|
||||||
|
**Wichtig:** Alle Services nutzen Mock-Daten - kein echtes Boot nötig!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎵 Schritt 2: Browser öffnen & testen
|
||||||
|
|
||||||
|
**Alles läuft im Browser - keine Installation nötig!**
|
||||||
|
|
||||||
|
#### 1️⃣ **Hauptdashboard** (Haupt-UI fürs Boot)
|
||||||
|
```
|
||||||
|
http://localhost:8090
|
||||||
|
```
|
||||||
|
- Navigation mit Live-Daten (Kurs, Geschwindigkeit, Tiefe)
|
||||||
|
- Audio-Zonen Steuerung
|
||||||
|
- Alle Boat-Systeme auf einen Blick
|
||||||
|
- **Das ist die UI, die später im Kiosk-Modus auf dem Boot läuft!**
|
||||||
|
|
||||||
|
#### 2️⃣ **Snapcast Web UI** (Audio-Zonen & Browser-Playback)
|
||||||
|
```
|
||||||
|
http://localhost:1780
|
||||||
|
```
|
||||||
|
- Alle 4 Boot-Zonen sichtbar (Salon, Cockpit, Bug, Heck)
|
||||||
|
- Lautstärke-Kontrolle für jede Zone
|
||||||
|
- Source-Switching (Spotify/AirPlay/Mopidy)
|
||||||
|
- **🔊 Audio im Browser abspielen:** Klicke auf "🔊" Symbol → Audio über Web Audio API!
|
||||||
|
|
||||||
|
#### 3️⃣ **Mopidy Music Player** (Musik-Bibliothek)
|
||||||
|
```
|
||||||
|
http://localhost:6680/iris/
|
||||||
|
```
|
||||||
|
- Moderne Player-UI (Iris)
|
||||||
|
- Local Files durchsuchen
|
||||||
|
- TuneIn Radio (Tausende Sender)
|
||||||
|
- Play/Pause/Skip Controls
|
||||||
|
|
||||||
|
#### 4️⃣ **SignalK** (Navigation Backend)
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
- Login: `admin` / `bordanlage`
|
||||||
|
- NMEA 2000 Stream Monitoring
|
||||||
|
- WebSocket Delta Stream
|
||||||
|
- Real-time Sensor Data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Service Übersicht
|
||||||
|
|
||||||
|
| Service | URL | Beschreibung |
|
||||||
|
|---------|-----|--------------|
|
||||||
|
| **🎛️ Dashboard** | http://localhost:8090 | **Haupt-UI** - Navigation + Audio (Kiosk-Modus auf Boot) |
|
||||||
|
| **🔊 Snapcast Web** | http://localhost:1780 | Audio-Zonen + **Browser-Playback** (Web Audio API) |
|
||||||
|
| **🎵 Mopidy/Iris** | http://localhost:6680/iris/ | Musik-Player (Local Files + Radio) |
|
||||||
|
| **🧭 SignalK** | http://localhost:3000 | Navigation Backend (Login: admin / bordanlage) |
|
||||||
|
| **🎬 Jellyfin** | http://localhost:8096 | Mediathek (Videos, optional) |
|
||||||
|
| **🐳 Portainer** | http://localhost:9000 | Docker Management |
|
||||||
|
|
||||||
|
**💡 Tipp:** Das Dashboard (Port 8090) ist die Hauptoberfläche, die später auf dem Boot im Kiosk-Modus läuft!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Mock-Daten erklärt
|
||||||
|
|
||||||
|
Das System läuft komplett mit **realistischen Test-Daten**:
|
||||||
|
|
||||||
|
### 🧭 Navigation (SignalK)
|
||||||
|
- GPS Position: Ostsee bei Kiel
|
||||||
|
- Geschwindigkeit: 3,5-8 Knoten (schwankt realistisch)
|
||||||
|
- Kurs: 200-235°
|
||||||
|
- Wassertiefe: 6-25 Meter
|
||||||
|
- Windgeschwindigkeit: 8-22 Knoten
|
||||||
|
|
||||||
|
### ⚡ Elektrik
|
||||||
|
- Starterbatterie: 12,2-12,9V
|
||||||
|
- Hausbatterie: 24,5-25,6V (2x 12V)
|
||||||
|
- Lichtmaschine: 30-60A
|
||||||
|
|
||||||
|
### 🔧 Motor
|
||||||
|
- Drehzahl: 1500-2100 RPM
|
||||||
|
- Kraftstoffverbrauch: 10-15 L/h
|
||||||
|
- Betriebsstunden: läuft kontinuierlich hoch
|
||||||
|
|
||||||
|
### 🎵 Audio-Zonen
|
||||||
|
- 4 Boot-Zonen (Salon, Cockpit, Bug, Heck) - initial stumm
|
||||||
|
- Deine PC-Zone (windows-pc) - wenn du snapclient startest
|
||||||
|
- 3 Audio-Quellen: Spotify, AirPlay, Mopidy
|
||||||
|
|
||||||
|
**Alle Werte ändern sich in Echtzeit und verhalten sich realistisch!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛑 System stoppen
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎧 Audio testen (komplett im Browser!)
|
||||||
|
|
||||||
|
### Im Browser Audio abspielen:
|
||||||
|
|
||||||
|
1. **Öffne Snapcast Web UI:** http://localhost:1780
|
||||||
|
2. **Wähle eine Zone** (z.B. "zone-salon")
|
||||||
|
3. **Klicke auf das 🔊 Symbol** (oben rechts)
|
||||||
|
4. **Aktiviere Browser Audio Playback**
|
||||||
|
5. **Spiele Musik in Mopidy** → Audio kommt über deinen Browser!
|
||||||
|
|
||||||
|
**Hinweis:** Das nutzt die Web Audio API - funktioniert in Chrome, Firefox, Edge.
|
||||||
|
|
||||||
|
### Musik starten:
|
||||||
|
|
||||||
|
1. **Öffne Mopidy:** http://localhost:6680/iris/
|
||||||
|
2. **Klicke auf "Browse"** → "TuneIn" oder "Local Files"
|
||||||
|
3. **Wähle einen Sender/Track** und klicke Play
|
||||||
|
4. **Audio läuft nun** über Snapcast → Browser Audio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Problem: "make: Befehl nicht gefunden"
|
||||||
|
|
||||||
|
**Kein Problem!** Nutze einfach Docker Compose direkt:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Statt "make dev":
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Statt "make stop":
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# Statt "make logs":
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: Dashboard zeigt keine Daten
|
||||||
|
|
||||||
|
1. **Prüfe ob alle Container laufen:**
|
||||||
|
```powershell
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Alle Services sollten "Up" sein**
|
||||||
|
- Falls Mopidy restartet: `docker compose logs mopidy`
|
||||||
|
|
||||||
|
3. **Dashboard lädt Mock-Daten automatisch** (`VITE_USE_MOCK=true`)
|
||||||
|
|
||||||
|
### Problem: Port 8090 bereits belegt
|
||||||
|
|
||||||
|
Ändere den Port in `docker-compose.dev.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Zeile 49:
|
||||||
|
ports:
|
||||||
|
- "8091:8090" # Nutze 8091 statt 8090
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann: http://localhost:8091
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Weitere Dokumentation
|
||||||
|
|
||||||
|
- **Ausführliche Windows-Anleitung:** [WINDOWS_AUDIO_SETUP.md](WINDOWS_AUDIO_SETUP.md)
|
||||||
|
- **Mock-Daten Details:** [MOCK_DATA_EXPLANATION.md](MOCK_DATA_EXPLANATION.md)
|
||||||
|
- **Projekt-Übersicht:** [README.md](README.md)
|
||||||
|
- **Schiffs-Routing:** [SHIP_ROUTING.md](SHIP_ROUTING.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment auf Ubuntu Boot-Server (später)
|
||||||
|
|
||||||
|
Wenn du das System aufs Boot bringen willst:
|
||||||
|
|
||||||
|
### Ubuntu Kiosk-Modus Setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Docker installieren
|
||||||
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
sudo sh get-docker.sh
|
||||||
|
|
||||||
|
# 2. Repository klonen
|
||||||
|
git clone <your-repo> /opt/boWave
|
||||||
|
cd /opt/boWave
|
||||||
|
|
||||||
|
# 3. Production Mode starten
|
||||||
|
make boot # Nutzt docker-compose.yml (ohne .dev overlay)
|
||||||
|
|
||||||
|
# 4. Chromium Kiosk Mode (autostart)
|
||||||
|
cat > ~/.config/autostart/kiosk.desktop << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Boat Dashboard Kiosk
|
||||||
|
Exec=chromium-browser --kiosk --noerrdialogs --disable-infobars --disable-session-crashed-bubble http://localhost:8080
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unterschiede Boot vs. Dev:**
|
||||||
|
- Boot: Port 8080 (Production Build)
|
||||||
|
- Boot: Echte Audio-Hardware (`--player alsa --soundcard hw:0,0`)
|
||||||
|
- Boot: NMEA Hardware-Anschluss (USB/Serial)
|
||||||
|
- Boot: Spotify/AirPlay Discovery über `network_mode: host`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Viel Spaß!
|
||||||
|
|
||||||
|
Das System ist jetzt **komplett browser-basiert** zum Testen - keine zusätzliche Software nötig!
|
||||||
|
|
||||||
|
**Was du testen kannst:**
|
||||||
|
- ✅ Navigation Dashboard mit Live Mock-Daten
|
||||||
|
- ✅ Audio-Zonen Kontrolle (Browser-Playback)
|
||||||
|
- ✅ Musik Player (Mopidy/Iris)
|
||||||
|
- ✅ Multiroom Audio Synchronisation
|
||||||
|
- ✅ Alle UI-Komponenten die später auf dem Boot laufen
|
||||||
|
|
||||||
|
**Alles läuft wie auf dem Boot - nur ohne echte Hardware!** 🚢⚓
|
||||||
@@ -1,22 +1,85 @@
|
|||||||
# Ship Routing System – boWave Navigation
|
# Ship Routing System – boWave Navigation
|
||||||
|
|
||||||
## Overview
|
## Current Route: Lokale Rundfahrt in Lingen
|
||||||
The ship follows a realistic nautical route around Bornholm Island in the Baltic Sea. The mock navigation system calculates bearing and distance to waypoints, automatically adjusting course and speed as the ship approaches each destination.
|
|
||||||
|
|
||||||
## How It Works
|
Die Mock-Daten simulieren eine **lokale Rundfahrt in Lingen** über den **Dortmund-Ems-Kanal** und die **Ems**. Das Boot bleibt in der Region!
|
||||||
|
|
||||||
### Waypoint System
|
### Route Details
|
||||||
The ship navigates a 6-waypoint loop:
|
|
||||||
1. **Kiel Fjord (Start)** – 54.3233°N, 10.1394°E
|
|
||||||
2. **Bornholm North** – 55.0500°N, 13.5500°E
|
|
||||||
3. **Rønne Harbor** – 55.1200°N, 14.8000°E
|
|
||||||
4. **Bornholm East** – 54.9500°N, 15.2000°E
|
|
||||||
5. **Bornholm South** – 54.5800°N, 14.9000°E
|
|
||||||
6. **Gdansk Approach** – 54.1500°N, 13.2000°E
|
|
||||||
|
|
||||||
After reaching the 6th waypoint, the ship automatically loops back to waypoint 1.
|
```
|
||||||
|
Start: EYC Segelclub Lingen (52.5236°N, 7.3200°E)
|
||||||
|
Ziel: EYC Segelclub Lingen (zurück zum Start)
|
||||||
|
Strecke: ~15 km Rundfahrt
|
||||||
|
Dauer: ~2-3 Stunden bei 6 Knoten
|
||||||
|
```
|
||||||
|
|
||||||
### Navigation Algorithm
|
## Waypoints (14 Punkte - Lokale Schleife)
|
||||||
|
|
||||||
|
1. **EYC Segelclub Lingen** (52.5236°N, 7.3200°E) - Start
|
||||||
|
2. **Kanal Ausfahrt** (52.5280°N, 7.3150°E) - Hafen verlassen
|
||||||
|
3. **DEK Westlich** (52.5400°N, 7.3000°E) - Kanal westwärts
|
||||||
|
4. **DEK Schleife West** (52.5500°N, 7.2800°E) - Westschleife
|
||||||
|
5. **DEK Nord** (52.5600°N, 7.2700°E) - Nordwärts
|
||||||
|
6. **Brücke Nord** (52.5700°N, 7.2800°E) - Nordbrücke
|
||||||
|
7. **Ems Einfahrt** (52.5750°N, 7.3000°E) - In die Ems
|
||||||
|
8. **Ems Fluss Ost** (52.5800°N, 7.3200°E) - Ems ostwärts
|
||||||
|
9. **Ems Kurve** (52.5750°N, 7.3400°E) - Ems Kurve
|
||||||
|
10. **Ems Süd** (52.5650°N, 7.3500°E) - Ems südwärts
|
||||||
|
11. **Ems Rückkehr** (52.5500°N, 7.3450°E) - Rückweg
|
||||||
|
12. **Kanal Rückkehr** (52.5400°N, 7.3350°E) - Zurück zum Kanal
|
||||||
|
13. **Hafen Approach** (52.5300°N, 7.3250°E) - Hafen Annäherung
|
||||||
|
14. **EYC Segelclub (Ziel)** (52.5236°N, 7.3200°E) - Zurück am Start! ⚓
|
||||||
|
|
||||||
|
**→ Route loopt automatisch und beginnt von vorne!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Visualisierung der Route
|
||||||
|
|
||||||
|
```
|
||||||
|
Brücke Nord
|
||||||
|
|
|
||||||
|
DEK Nord
|
||||||
|
|
|
||||||
|
DEK Schleife ← DEK West
|
||||||
|
|
|
||||||
|
Kanal Ausfahrt
|
||||||
|
|
|
||||||
|
🏁 EYC ← Hafen Approach
|
||||||
|
↑
|
||||||
|
Kanal Rückkehr
|
||||||
|
↑
|
||||||
|
Ems Rückkehr
|
||||||
|
|
|
||||||
|
Ems Süd
|
||||||
|
|
|
||||||
|
Ems Ost → Ems Kurve
|
||||||
|
|
|
||||||
|
Ems Einfahrt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Boot fährt eine schöne Schleife: DEK raus → Ems hoch → Ems runter → zurück zum EYC!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automatischer Fallback
|
||||||
|
|
||||||
|
Das Dashboard nutzt **automatisch Mock-Daten**, wenn kein echtes SignalK läuft:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Nach 5 Sekunden ohne echte Daten → Mock aktiviert
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('⚠️ SignalK not connected - using mock data (EYC Lingen → Ems route)')
|
||||||
|
}, 5000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### So erkennst du Mock-Daten:
|
||||||
|
- Browser Console zeigt: `⚠️ SignalK not connected - using mock data`
|
||||||
|
- Boot fährt automatisch die Route
|
||||||
|
- Updates alle 1 Sekunde
|
||||||
|
- Loop: Nach Norddeich zurück nach Lingen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### 1. **Course Calculation**
|
#### 1. **Course Calculation**
|
||||||
```javascript
|
```javascript
|
||||||
|
|||||||
205
WINDOWS_AUDIO_SETUP.md
Normal file
205
WINDOWS_AUDIO_SETUP.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Windows Audio Setup für boWave
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Diese Anleitung ermöglicht es, Audio vom boWave-System auf deinem Windows-PC abzuspielen, während alle virtuellen Boot-Zonen stumm bleiben.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
### 1. Snapcast Client für Windows installieren
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Download von GitHub (aktuellste Version)
|
||||||
|
# https://github.com/badaix/snapcast/releases/latest
|
||||||
|
|
||||||
|
# Oder mit Scoop (falls installiert):
|
||||||
|
scoop bucket add extras
|
||||||
|
scoop install snapcast
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manuelle Installation:**
|
||||||
|
1. Gehe zu https://github.com/badaix/snapcast/releases/latest
|
||||||
|
2. Lade `snapclient-<version>-win64.zip` herunter
|
||||||
|
3. Entpacke nach `C:\Program Files\Snapcast\`
|
||||||
|
4. Füge `C:\Program Files\Snapcast\bin` zu deinem PATH hinzu
|
||||||
|
|
||||||
|
## Setup-Schritte
|
||||||
|
|
||||||
|
### Schritt 1: Projekt starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd D:\coding\boWave
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Dies startet:
|
||||||
|
- ✅ Snapserver (Multiroom Audio Server) auf Port 1704
|
||||||
|
- ✅ SignalK (NMEA Mock-Daten)
|
||||||
|
- ✅ Mopidy (Musik-Player mit Mock-Playlist)
|
||||||
|
- ✅ Dashboard mit HMR auf http://localhost:8090
|
||||||
|
- ✅ 4 virtuelle Boot-Zonen (Salon, Cockpit, Bug, Heck) - alle stumm (null output)
|
||||||
|
|
||||||
|
### Schritt 2: Windows Audio Client starten
|
||||||
|
|
||||||
|
Öffne ein **neues PowerShell-Fenster** und führe aus:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
snapclient --host localhost --port 1704 --hostID windows-pc --player wasapi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameter Erklärung:**
|
||||||
|
- `--host localhost` → verbindet sich mit dem Docker Snapserver
|
||||||
|
- `--port 1704` → Standard Snapcast Port
|
||||||
|
- `--hostID windows-pc` → Eindeutige Zone-ID (wird im Dashboard angezeigt)
|
||||||
|
- `--player wasapi` → Windows Audio Session API (native Windows Audio)
|
||||||
|
|
||||||
|
**Alternative für ältere Windows-Versionen:**
|
||||||
|
```powershell
|
||||||
|
snapclient --host localhost --port 1704 --hostID windows-pc --player winmm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: Audio-Routing konfigurieren
|
||||||
|
|
||||||
|
Öffne das Snapcast Web-Interface: **http://localhost:1780**
|
||||||
|
|
||||||
|
1. Du siehst jetzt 5 Zonen:
|
||||||
|
- zone-salon (stumm)
|
||||||
|
- zone-cockpit (stumm)
|
||||||
|
- zone-bug (stumm)
|
||||||
|
- zone-heck (stumm)
|
||||||
|
- **windows-pc** (dein PC!)
|
||||||
|
|
||||||
|
2. Wähle die Zone **windows-pc** aus
|
||||||
|
3. Setze die Lautstärke auf 50-80%
|
||||||
|
4. Stelle sicher, dass die Zone **nicht** auf mute ist
|
||||||
|
|
||||||
|
3. Wähle den Audio-Stream (z.B. "Mopidy" für lokale Musik)
|
||||||
|
|
||||||
|
### Schritt 4: Musik abspielen
|
||||||
|
|
||||||
|
**Option A: Mopidy Web-Interface**
|
||||||
|
1. Öffne http://localhost:6680/iris/
|
||||||
|
2. Spiele einen Track ab
|
||||||
|
3. Audio sollte jetzt über deine Windows-Lautsprecher kommen!
|
||||||
|
|
||||||
|
**Option B: Spotify Connect (nativ auf Windows)**
|
||||||
|
|
||||||
|
In einem weiteren Terminal:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Librespot für Windows installieren (falls noch nicht vorhanden)
|
||||||
|
# Download: https://github.com/librespot-org/librespot/releases
|
||||||
|
|
||||||
|
librespot --name "Bordanlage-Windows" --backend rodio
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann öffne die Spotify-App und wähle "Bordanlage-Windows" als Ausgabegerät.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "snapclient: Befehl nicht gefunden"
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Prüfe ob Snapcast installiert ist:
|
||||||
|
where.exe snapclient
|
||||||
|
|
||||||
|
# Falls nicht gefunden, füge zum PATH hinzu (als Administrator):
|
||||||
|
$env:Path += ";C:\Program Files\Snapcast\bin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kein Audio auf Windows
|
||||||
|
|
||||||
|
1. **Prüfe ob snapclient läuft:**
|
||||||
|
```powershell
|
||||||
|
# In einem separaten Terminal sollte snapclient-Output sichtbar sein
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Prüfe Snapserver-Logs:**
|
||||||
|
```powershell
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml logs snapserver
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Prüfe Windows Audio Device:**
|
||||||
|
- Rechtsklick auf Lautsprecher-Icon in Taskleiste
|
||||||
|
- "Sound-Einstellungen öffnen"
|
||||||
|
- Stelle sicher, dass das richtige Ausgabegerät ausgewählt ist
|
||||||
|
|
||||||
|
### Snapclient verbindet nicht
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Prüfe ob Port 1704 erreichbar ist:
|
||||||
|
Test-NetConnection -ComputerName localhost -Port 1704
|
||||||
|
|
||||||
|
# Falls Docker Desktop verwendet wird, prüfe:
|
||||||
|
docker ps | findstr snapserver
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio stottert oder hat Aussetzer
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Erhöhe Buffer-Größe:
|
||||||
|
snapclient --host localhost --port 1704 --hostID windows-pc --player wasapi --latency 500
|
||||||
|
```
|
||||||
|
|
||||||
|
Der `--latency` Parameter ist in Millisekunden (Standard: 200ms).
|
||||||
|
|
||||||
|
## Permanente Konfiguration
|
||||||
|
|
||||||
|
### Automatischer Start mit Batch-Datei
|
||||||
|
|
||||||
|
Erstelle `start-windows-audio.bat`:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
@echo off
|
||||||
|
echo Starting Snapcast Windows Audio Client...
|
||||||
|
snapclient --host localhost --port 1704 --hostID windows-pc --player wasapi --latency 300
|
||||||
|
pause
|
||||||
|
```
|
||||||
|
|
||||||
|
Doppelklick auf die Datei startet den Audio-Client.
|
||||||
|
|
||||||
|
### Windows-Dienst (fortgeschritten)
|
||||||
|
|
||||||
|
Um snapclient als Windows-Dienst zu installieren (läuft automatisch im Hintergrund):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Benötigt: NSSM (Non-Sucking Service Manager)
|
||||||
|
scoop install nssm
|
||||||
|
|
||||||
|
# Dienst erstellen:
|
||||||
|
nssm install SnapcastClient "C:\Program Files\Snapcast\bin\snapclient.exe"
|
||||||
|
nssm set SnapcastClient AppParameters "--host localhost --port 1704 --hostID windows-pc --player wasapi"
|
||||||
|
nssm set SnapcastClient DisplayName "Snapcast Audio Client"
|
||||||
|
nssm set SnapcastClient Start SERVICE_AUTO_START
|
||||||
|
|
||||||
|
# Dienst starten:
|
||||||
|
nssm start SnapcastClient
|
||||||
|
```
|
||||||
|
|
||||||
|
## Make-Target Erweiterung (Optional)
|
||||||
|
|
||||||
|
Für Komfort kannst du ein neues Make-Target hinzufügen:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Im Makefile nach Zeile 52 einfügen:
|
||||||
|
|
||||||
|
windows-audio:
|
||||||
|
@echo "Starting Snapcast client → Windows speakers (zone: windows-pc)"
|
||||||
|
@where snapclient > nul || (echo "Please install Snapcast from https://github.com/badaix/snapcast/releases" && exit 1)
|
||||||
|
snapclient --host localhost --port 1704 --hostID windows-pc --player wasapi
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann kannst du einfach `make windows-audio` ausführen.
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
- ✅ **Mock-Daten aktiv**: SignalK sendet realistische NMEA 2000 Boot-Daten
|
||||||
|
- ✅ **4 Boot-Zonen stumm**: Salon/Cockpit/Bug/Heck nutzen null-Player
|
||||||
|
- ✅ **1 PC-Zone mit Audio**: windows-pc nutzt WASAPI für echten Sound
|
||||||
|
- ✅ **Dashboard funktionsfähig**: http://localhost:8090 zeigt alle Zonen
|
||||||
|
- ✅ **Alle Services laufen in Docker**: Nur snapclient läuft nativ für Audio-Output
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
1. `make dev` starten
|
||||||
|
2. `snapclient --host localhost --port 1704 --hostID windows-pc --player wasapi` in neuem Terminal
|
||||||
|
3. Musik über Mopidy oder Spotify abspielen
|
||||||
|
4. Genießen! 🎵
|
||||||
@@ -12,6 +12,7 @@ enabled = true
|
|||||||
hostname = 0.0.0.0
|
hostname = 0.0.0.0
|
||||||
port = 6680
|
port = 6680
|
||||||
allowed_origins =
|
allowed_origins =
|
||||||
|
csrf_protection = false
|
||||||
|
|
||||||
[mpd]
|
[mpd]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ threads = -1
|
|||||||
enabled = true
|
enabled = true
|
||||||
bind_to_address = 0.0.0.0
|
bind_to_address = 0.0.0.0
|
||||||
port = 1780
|
port = 1780
|
||||||
|
doc_root = /usr/share/snapserver/snapweb/
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
sink = system
|
sink = system
|
||||||
|
|||||||
@@ -7,6 +7,42 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# SignalK Proxy (incl. WebSocket)
|
||||||
|
location /signalk {
|
||||||
|
proxy_pass http://signalk:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Snapcast Proxy (incl. WebSocket)
|
||||||
|
location /snapcast-ws {
|
||||||
|
proxy_pass http://snapserver:1780;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mopidy Proxy (incl. WebSocket)
|
||||||
|
location /mopidy {
|
||||||
|
proxy_pass http://mopidy:6680;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Jellyfin Proxy
|
||||||
|
location /jellyfin {
|
||||||
|
proxy_pass http://jellyfin:8096;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||||
gzip_min_length 1000;
|
gzip_min_length 1000;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export default function NavigationMap() {
|
|||||||
const shipMarkerRef = useRef(null)
|
const shipMarkerRef = useRef(null)
|
||||||
const trackSourceRef = useRef(false)
|
const trackSourceRef = useRef(false)
|
||||||
const [zoom, setZoom] = useState(11)
|
const [zoom, setZoom] = useState(11)
|
||||||
|
const [mapError, setMapError] = useState(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
const api = getApi()
|
const api = getApi()
|
||||||
const snapshot = api.signalk.getSnapshot?.()
|
const snapshot = api.signalk.getSnapshot?.()
|
||||||
@@ -22,6 +24,8 @@ export default function NavigationMap() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map.current) return
|
if (map.current) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
map.current = new maplibregl.Map({
|
map.current = new maplibregl.Map({
|
||||||
container: mapContainer.current,
|
container: mapContainer.current,
|
||||||
style: {
|
style: {
|
||||||
@@ -178,8 +182,27 @@ export default function NavigationMap() {
|
|||||||
|
|
||||||
map.current.on('zoom', () => setZoom(map.current.getZoom()))
|
map.current.on('zoom', () => setZoom(map.current.getZoom()))
|
||||||
|
|
||||||
|
map.current.on('load', () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setMapError(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
map.current.on('error', (e) => {
|
||||||
|
console.error('Map error:', e)
|
||||||
|
setMapError('Map failed to load. Check your internet connection.')
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Cleanup is handled by React
|
if (map.current) {
|
||||||
|
map.current.remove()
|
||||||
|
map.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize map:', error)
|
||||||
|
setMapError(`Map initialization failed: ${error.message}`)
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -191,12 +214,15 @@ export default function NavigationMap() {
|
|||||||
const trackSource = map.current.getSource('track')
|
const trackSource = map.current.getSource('track')
|
||||||
if (trackSource && trackSourceRef.current) {
|
if (trackSource && trackSourceRef.current) {
|
||||||
const currentData = trackSource._data
|
const currentData = trackSource._data
|
||||||
|
// Check if geometry exists and has coordinates array
|
||||||
|
if (currentData?.geometry?.coordinates) {
|
||||||
if (currentData.geometry.coordinates.length > 500) {
|
if (currentData.geometry.coordinates.length > 500) {
|
||||||
currentData.geometry.coordinates.shift() // Keep last 500 points
|
currentData.geometry.coordinates.shift() // Keep last 500 points
|
||||||
}
|
}
|
||||||
currentData.geometry.coordinates.push([lon, lat])
|
currentData.geometry.coordinates.push([lon, lat])
|
||||||
trackSource.setData(currentData)
|
trackSource.setData(currentData)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
map.current.getSource('ship').setData({
|
map.current.getSource('ship').setData({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
@@ -214,8 +240,34 @@ export default function NavigationMap() {
|
|||||||
}
|
}
|
||||||
}, [lat, lon, heading, zoom])
|
}, [lat, lon, heading, zoom])
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
if (mapError) {
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
|
<div style={styles.errorState}>
|
||||||
|
<div style={styles.errorIcon}>⚠️</div>
|
||||||
|
<div style={styles.errorTitle}>Karte konnte nicht geladen werden</div>
|
||||||
|
<div style={styles.errorMessage}>{mapError}</div>
|
||||||
|
<button
|
||||||
|
className="primary"
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
>
|
||||||
|
Neu laden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
{isLoading && (
|
||||||
|
<div style={styles.loadingOverlay}>
|
||||||
|
<div style={styles.spinner}></div>
|
||||||
|
<div style={{ marginTop: 12, color: 'var(--muted)' }}>Karte wird geladen...</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div ref={mapContainer} style={styles.mapBox} />
|
<div ref={mapContainer} style={styles.mapBox} />
|
||||||
|
|
||||||
{/* Map Controls */}
|
{/* Map Controls */}
|
||||||
@@ -384,4 +436,51 @@ const styles = {
|
|||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
zIndex: 500,
|
zIndex: 500,
|
||||||
},
|
},
|
||||||
|
loadingOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'var(--surface)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 2000,
|
||||||
|
},
|
||||||
|
spinner: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
border: '4px solid var(--border)',
|
||||||
|
borderTop: '4px solid var(--accent)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
},
|
||||||
|
errorState: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
padding: 32,
|
||||||
|
textAlign: 'center',
|
||||||
|
background: 'var(--surface)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
},
|
||||||
|
errorIcon: {
|
||||||
|
fontSize: 48,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
errorTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--text)',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'var(--muted)',
|
||||||
|
maxWidth: 400,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
204
dashboard/src/components/nav/SimpleNavigationMap.jsx
Normal file
204
dashboard/src/components/nav/SimpleNavigationMap.jsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { useNMEA } from '../../hooks/useNMEA.js'
|
||||||
|
|
||||||
|
// Simple fallback map component without MapLibre (no external dependencies)
|
||||||
|
export default function SimpleNavigationMap() {
|
||||||
|
const { lat, lon, heading, sog, cog } = useNMEA()
|
||||||
|
|
||||||
|
const mapLat = lat ?? 54.32
|
||||||
|
const mapLon = lon ?? 10.22
|
||||||
|
|
||||||
|
// Simple marker rotation based on heading
|
||||||
|
const rotation = heading ?? cog ?? 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
{/* Info Panel */}
|
||||||
|
<div style={styles.infoPanel}>
|
||||||
|
<div style={styles.infoRow}>
|
||||||
|
<span style={styles.label}>Position</span>
|
||||||
|
<span style={styles.value}>
|
||||||
|
{lat != null ? lat.toFixed(5) : '--'}° N
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.infoRow}>
|
||||||
|
<span style={styles.label}></span>
|
||||||
|
<span style={styles.value}>
|
||||||
|
{lon != null ? lon.toFixed(5) : '--'}° E
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{heading != null && (
|
||||||
|
<div style={styles.infoRow}>
|
||||||
|
<span style={styles.label}>Heading</span>
|
||||||
|
<span style={styles.value}>{Math.round(heading)}°</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sog != null && (
|
||||||
|
<div style={styles.infoRow}>
|
||||||
|
<span style={styles.label}>Speed</span>
|
||||||
|
<span style={styles.value}>{(sog * 1.943844).toFixed(1)} kn</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simple visual representation */}
|
||||||
|
<div style={styles.mapView}>
|
||||||
|
<div style={styles.compassRose}>
|
||||||
|
<div style={styles.northMarker}>N</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...styles.shipMarker,
|
||||||
|
transform: `rotate(${rotation}deg)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.coords}>
|
||||||
|
{lat != null && lon != null ? (
|
||||||
|
<>
|
||||||
|
<div>{lat.toFixed(5)}°</div>
|
||||||
|
<div>{lon.toFixed(5)}°</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div style={{ color: 'var(--muted)' }}>No GPS signal</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map notice */}
|
||||||
|
<div style={styles.notice}>
|
||||||
|
<div style={styles.noticeTitle}>🗺️ Karten-Modus</div>
|
||||||
|
<div style={styles.noticeText}>
|
||||||
|
Interaktive Seekarte ist im Production Mode verfügbar.
|
||||||
|
<br />
|
||||||
|
Hier siehst du Position, Kurs und Geschwindigkeit.
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="http://localhost:3000"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
style={styles.link}
|
||||||
|
>
|
||||||
|
→ SignalK Karte öffnen (Port 3000)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
container: {
|
||||||
|
position: 'relative',
|
||||||
|
flex: 1,
|
||||||
|
background: 'var(--surface)',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
minHeight: 400,
|
||||||
|
},
|
||||||
|
mapView: {
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 32,
|
||||||
|
padding: 32,
|
||||||
|
},
|
||||||
|
compassRose: {
|
||||||
|
position: 'relative',
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
border: '2px solid var(--accent)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'radial-gradient(circle, rgba(14, 165, 233, 0.05) 0%, transparent 70%)',
|
||||||
|
},
|
||||||
|
northMarker: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
},
|
||||||
|
shipMarker: {
|
||||||
|
fontSize: 48,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
transition: 'transform 0.5s ease-out',
|
||||||
|
},
|
||||||
|
coords: {
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--text)',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
},
|
||||||
|
infoPanel: {
|
||||||
|
background: 'var(--glass-bg)',
|
||||||
|
backdropFilter: 'blur(var(--glass-blur))',
|
||||||
|
WebkitBackdropFilter: 'blur(var(--glass-blur))',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
borderRadius: 'var(--radius-lg)',
|
||||||
|
padding: '12px 16px',
|
||||||
|
margin: 16,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
infoRow: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'var(--muted)',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 11,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
fontSize: 13,
|
||||||
|
},
|
||||||
|
notice: {
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
padding: 20,
|
||||||
|
borderTop: '1px solid var(--border)',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
noticeTitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--text)',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
noticeText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--muted)',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
display: 'inline-block',
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: '6px 12px',
|
||||||
|
borderRadius: 'var(--radius)',
|
||||||
|
background: 'rgba(14, 165, 233, 0.1)',
|
||||||
|
border: '1px solid rgba(14, 165, 233, 0.3)',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
},
|
||||||
|
}
|
||||||
222
dashboard/src/components/systems/BoatControl.jsx
Normal file
222
dashboard/src/components/systems/BoatControl.jsx
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNMEA } from '../../hooks/useNMEA';
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ id: 'lights', icon: '💡', label: 'Lights', pos: { top: '15%', left: '10%' } },
|
||||||
|
{ id: 'climate', icon: '❄️', label: 'Climate', pos: { top: '35%', left: '5%' } },
|
||||||
|
{ id: 'nav', icon: '📍', label: 'Navigation', pos: { top: '55%', left: '5%' } },
|
||||||
|
{ id: 'audio', icon: '🎵', label: 'Audio', pos: { top: '75%', left: '10%' } },
|
||||||
|
{ id: 'battery', icon: '🔋', label: 'Energy', pos: { bottom: '15%', left: '15%' } },
|
||||||
|
|
||||||
|
{ id: 'tanks', icon: '🌊', label: 'Tanks', pos: { top: '15%', right: '10%' } },
|
||||||
|
{ id: 'power', icon: '🔌', label: 'Shore Power', pos: { top: '35%', right: '5%' } },
|
||||||
|
{ id: 'wind', icon: '🌬️', label: 'Wind', pos: { top: '55%', right: '5%' } },
|
||||||
|
{ id: 'engine', icon: '⚙️', label: 'Engine', pos: { top: '75%', right: '10%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function BoatControl({ activeCategory, onCategoryChange }) {
|
||||||
|
const { sog, heading } = useNMEA();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
<div style={styles.shipControlLabel}>SHIP CONTROL</div>
|
||||||
|
|
||||||
|
<div style={styles.mainArea}>
|
||||||
|
{/* Category Icons Left */}
|
||||||
|
{CATEGORIES.slice(0, 5).map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
style={{
|
||||||
|
...styles.catBtn,
|
||||||
|
...cat.pos,
|
||||||
|
...(activeCategory === cat.id ? styles.catBtnActive : {})
|
||||||
|
}}
|
||||||
|
onClick={() => onCategoryChange(cat.id)}
|
||||||
|
>
|
||||||
|
<span style={styles.catIcon}>{cat.icon}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Central Boat Graphic */}
|
||||||
|
<div style={styles.boatContainer}>
|
||||||
|
<img
|
||||||
|
src="https://www.prestige-yachts.com/fichiers/Prestige_F4.9_Profile_600.png"
|
||||||
|
alt="Prestige Yacht"
|
||||||
|
style={styles.boatImg}
|
||||||
|
onError={(e) => {
|
||||||
|
// Fallback if image fails to load
|
||||||
|
e.target.style.display = 'none';
|
||||||
|
e.target.nextSibling.style.display = 'flex';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{...styles.boatFallback, display: 'none'}}>
|
||||||
|
<div style={styles.boatHull}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Central Instrument Over Boat */}
|
||||||
|
<div style={styles.centralGauge}>
|
||||||
|
<div style={styles.gaugeValue}>{sog?.toFixed(1) || '0.0'}</div>
|
||||||
|
<div style={styles.gaugeUnit}>knots</div>
|
||||||
|
<div style={styles.gaugeHeading}>{Math.round(heading || 0)}°</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Icons Right */}
|
||||||
|
{CATEGORIES.slice(5).map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
style={{
|
||||||
|
...styles.catBtn,
|
||||||
|
...cat.pos,
|
||||||
|
...(activeCategory === cat.id ? styles.catBtnActive : {})
|
||||||
|
}}
|
||||||
|
onClick={() => onCategoryChange(cat.id)}
|
||||||
|
>
|
||||||
|
<span style={styles.catIcon}>{cat.icon}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.bottomBar}>
|
||||||
|
<div style={styles.bottomIcon}>⚙️</div>
|
||||||
|
<div style={styles.time}>{new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}</div>
|
||||||
|
<div style={styles.bottomIcon}>📖</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
background: 'radial-gradient(circle at center, #1a2a3a 0%, #07111f 100%)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
shipControlLabel: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 40,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: 300,
|
||||||
|
letterSpacing: '0.2em',
|
||||||
|
color: 'rgba(255,255,255,0.9)',
|
||||||
|
fontFamily: 'serif',
|
||||||
|
},
|
||||||
|
mainArea: {
|
||||||
|
flex: 1,
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
catBtn: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.23, 1, 0.320, 1)',
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
|
catBtnActive: {
|
||||||
|
background: 'rgba(14, 165, 233, 0.2)',
|
||||||
|
borderColor: '#0ea5e9',
|
||||||
|
boxShadow: '0 0 20px rgba(14, 165, 233, 0.4)',
|
||||||
|
transform: 'scale(1.1)',
|
||||||
|
},
|
||||||
|
catIcon: {
|
||||||
|
fontSize: 24,
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
boatContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
width: '70%',
|
||||||
|
maxWidth: 800,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
animation: 'fadeIn 1s ease-out',
|
||||||
|
},
|
||||||
|
boatImg: {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
filter: 'drop-shadow(0 20px 30px rgba(0,0,0,0.5)) brightness(1.1)',
|
||||||
|
},
|
||||||
|
boatFallback: {
|
||||||
|
width: 400,
|
||||||
|
height: 100,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
boatHull: {
|
||||||
|
width: '100%',
|
||||||
|
height: 40,
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: '50% 50% 10% 10%',
|
||||||
|
},
|
||||||
|
centralGauge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'rgba(7, 17, 31, 0.6)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
border: '2px solid rgba(255,255,255,0.15)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 0 40px rgba(0,0,0,0.5)',
|
||||||
|
},
|
||||||
|
gaugeValue: {
|
||||||
|
fontSize: 42,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
gaugeUnit: {
|
||||||
|
fontSize: 12,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: 'var(--muted)',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
marginTop: -4,
|
||||||
|
},
|
||||||
|
gaugeHeading: {
|
||||||
|
marginTop: 8,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--accent)',
|
||||||
|
},
|
||||||
|
bottomBar: {
|
||||||
|
height: 60,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 40,
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
bottomIcon: {
|
||||||
|
fontSize: 20,
|
||||||
|
opacity: 0.5,
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: 'rgba(255,255,255,0.8)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
}
|
||||||
|
};
|
||||||
125
dashboard/src/components/systems/FloorPlan.jsx
Normal file
125
dashboard/src/components/systems/FloorPlan.jsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ZONES = [
|
||||||
|
{ id: 'salon', label: 'Salon', top: '40%', left: '30%', width: '20%', height: '20%' },
|
||||||
|
{ id: 'cockpit', label: 'Cockpit', top: '40%', left: '55%', width: '15%', height: '20%' },
|
||||||
|
{ id: 'bug', label: 'Owner Cabin', top: '40%', left: '10%', width: '15%', height: '20%' },
|
||||||
|
{ id: 'heck', label: 'VIP Cabin', top: '40%', left: '75%', width: '15%', height: '20%' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function FloorPlan({ type, onZoneClick }) {
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
<div style={styles.floorPlanWrapper}>
|
||||||
|
{/* Simple SVG/CSS representation of a boat deck */}
|
||||||
|
<div style={styles.deckOutline}>
|
||||||
|
{ZONES.map(zone => (
|
||||||
|
<div
|
||||||
|
key={zone.id}
|
||||||
|
style={{
|
||||||
|
...styles.zone,
|
||||||
|
top: zone.top,
|
||||||
|
left: zone.left,
|
||||||
|
width: zone.width,
|
||||||
|
height: zone.height,
|
||||||
|
}}
|
||||||
|
onClick={() => onZoneClick(zone.id)}
|
||||||
|
>
|
||||||
|
<div style={styles.zoneLabel}>{zone.label}</div>
|
||||||
|
<div style={styles.zoneStatus}>
|
||||||
|
{type === 'lights' ? '💡 80%' : '🌡️ 22°C'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.controls}>
|
||||||
|
<div style={styles.controlHeader}>{type === 'lights' ? 'Lighting Control' : 'Climate Control'}</div>
|
||||||
|
<div style={styles.controlRow}>
|
||||||
|
<span>Master Switch</span>
|
||||||
|
<button style={styles.toggle}>ON</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
container: {
|
||||||
|
padding: 20,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 30,
|
||||||
|
animation: 'slideInUp 0.4s ease-out',
|
||||||
|
},
|
||||||
|
floorPlanWrapper: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 900,
|
||||||
|
height: 300,
|
||||||
|
background: 'rgba(255,255,255,0.02)',
|
||||||
|
borderRadius: 20,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
border: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
},
|
||||||
|
deckOutline: {
|
||||||
|
width: '90%',
|
||||||
|
height: '60%',
|
||||||
|
border: '2px solid rgba(255,255,255,0.2)',
|
||||||
|
borderRadius: '80px 80px 40px 40px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
zone: {
|
||||||
|
position: 'absolute',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
},
|
||||||
|
zoneLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: 'var(--muted)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
zoneStatus: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 400,
|
||||||
|
background: 'var(--glass-bg)',
|
||||||
|
padding: 20,
|
||||||
|
borderRadius: 16,
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
},
|
||||||
|
controlHeader: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 700,
|
||||||
|
marginBottom: 15,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
},
|
||||||
|
controlRow: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
toggle: {
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '4px 12px',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
const SERVICES = [
|
const SERVICES = [
|
||||||
{ id: 'signalk', name: 'SignalK', host: import.meta.env.VITE_SIGNALK_HOST || 'localhost', port: 3000, path: '/signalk' },
|
{ id: 'signalk', name: 'SignalK', path: '/signalk' },
|
||||||
{ id: 'snapserver', name: 'Snapcast', host: import.meta.env.VITE_SNAPCAST_HOST || 'localhost', port: 1780, path: '/' },
|
{ id: 'snapserver', name: 'Snapcast', path: '/snapcast-ws' },
|
||||||
{ id: 'mopidy', name: 'Mopidy', host: import.meta.env.VITE_MOPIDY_HOST || 'localhost', port: 6680, path: '/' },
|
{ id: 'mopidy', name: 'Mopidy', path: '/mopidy' },
|
||||||
{ id: 'jellyfin', name: 'Jellyfin', host: import.meta.env.VITE_JELLYFIN_HOST || 'localhost', port: 8096, path: '/' },
|
{ id: 'jellyfin', name: 'Jellyfin', path: '/jellyfin/' },
|
||||||
{ id: 'portainer', name: 'Portainer', host: import.meta.env.VITE_PORTAINER_HOST || 'localhost', port: 9000, path: '/' },
|
{ id: 'portainer', name: 'Portainer', path: '/portainer' },
|
||||||
|
{ id: 'spotify', name: 'Spotify', path: '/snapcast-ws' }, // Use snapcast as proxy for its status
|
||||||
|
{ id: 'airplay', name: 'AirPlay', path: '/snapcast-ws' },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function ping(host, port, path) {
|
async function ping(path) {
|
||||||
try {
|
try {
|
||||||
|
const host = window.location.host
|
||||||
// mode: 'no-cors' bypasses CORS blocks; any response (opaque) = server is up
|
// mode: 'no-cors' bypasses CORS blocks; any response (opaque) = server is up
|
||||||
await fetch(`http://${host}:${port}${path}`, {
|
await fetch(`http://${host}${path}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
mode: 'no-cors',
|
mode: 'no-cors',
|
||||||
signal: AbortSignal.timeout(3000),
|
signal: AbortSignal.timeout(3000),
|
||||||
@@ -24,16 +27,26 @@ async function ping(host, port, path) {
|
|||||||
|
|
||||||
export function useDocker() {
|
export function useDocker() {
|
||||||
const [services, setServices] = useState(
|
const [services, setServices] = useState(
|
||||||
SERVICES.map(s => ({ ...s, url: `http://${s.host}:${s.port}`, status: 'unknown' }))
|
SERVICES.map(s => ({ ...s, url: `http://${window.location.host}${s.path}`, status: 'unknown' }))
|
||||||
)
|
)
|
||||||
|
|
||||||
async function checkAll() {
|
async function checkAll() {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
SERVICES.map(async s => ({
|
SERVICES.map(async s => {
|
||||||
...s,
|
const url = `http://${window.location.host}${s.path}`
|
||||||
url: `http://${s.host}:${s.port}`,
|
let status = 'offline'
|
||||||
status: await ping(s.host, s.port, s.path) ? 'online' : 'offline',
|
|
||||||
}))
|
if (s.id === 'spotify' || s.id === 'airplay') {
|
||||||
|
// These are special, they don't have their own UI
|
||||||
|
// We assume they are up if snapserver is up (for now)
|
||||||
|
// Ideally we check snapcast streams status
|
||||||
|
status = await ping('/snapcast-ws') ? 'online' : 'offline'
|
||||||
|
} else {
|
||||||
|
status = await ping(s.path) ? 'online' : 'offline'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...s, url, status }
|
||||||
|
})
|
||||||
)
|
)
|
||||||
setServices(results)
|
setServices(results)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function parseStatus(status) {
|
|||||||
|
|
||||||
export function useZones() {
|
export function useZones() {
|
||||||
const [zones, setZones] = useState([])
|
const [zones, setZones] = useState([])
|
||||||
|
const [streams, setStreams] = useState([])
|
||||||
const [connected, setConnected] = useState(false)
|
const [connected, setConnected] = useState(false)
|
||||||
const { snapcast } = getApi()
|
const { snapcast } = getApi()
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export function useZones() {
|
|||||||
const status = await snapcast.call('Server.GetStatus')
|
const status = await snapcast.call('Server.GetStatus')
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setZones(parseStatus(status))
|
setZones(parseStatus(status))
|
||||||
|
setStreams(status?.server?.streams || [])
|
||||||
setConnected(true)
|
setConnected(true)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -36,7 +38,10 @@ export function useZones() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onUpdate = (msg) => {
|
const onUpdate = (msg) => {
|
||||||
if (msg?.result?.server) setZones(parseStatus(msg.result))
|
if (msg?.result?.server) {
|
||||||
|
setZones(parseStatus(msg.result))
|
||||||
|
setStreams(msg.result.server.streams || [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snapcast.on('update', onUpdate)
|
snapcast.on('update', onUpdate)
|
||||||
@@ -65,5 +70,5 @@ export function useZones() {
|
|||||||
if (zone) await setMuted(zoneId, !zone.muted)
|
if (zone) await setMuted(zoneId, !zone.muted)
|
||||||
}, [zones, setMuted])
|
}, [zones, setMuted])
|
||||||
|
|
||||||
return { zones, connected, setVolume, setMuted, setSource, toggleZone }
|
return { zones, streams, connected, setVolume, setMuted, setSource, toggleZone }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ html, body, #root {
|
|||||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||||
|
|
||||||
/* ─── Animations ───────────────────────────────────────────────────────────── */
|
/* ─── Animations ───────────────────────────────────────────────────────────── */
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideInUp {
|
@keyframes slideInUp {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// API router – uses real services by default; set VITE_USE_MOCK=true to force mocks.
|
// API router – uses real services with automatic mock fallback if connection fails.
|
||||||
import { createSignalKMock } from './signalk.mock.js'
|
import { createSignalKMock } from './signalk.mock.js'
|
||||||
import { createSnapcastMock } from './snapcast.mock.js'
|
import { createSnapcastMock } from './snapcast.mock.js'
|
||||||
import { createMopidyMock } from './mopidy.mock.js'
|
import { createMopidyMock } from './mopidy.mock.js'
|
||||||
import { createSignalKClient } from '../api/signalk.js'
|
import { createSignalKClient } from '../api/signalk.js'
|
||||||
import { createSnapcastClient } from '../api/snapcast.js'
|
import { createSnapcastClient } from '../api/snapcast.js'
|
||||||
import { createMopidyClient } from '../api/mopidy.js'
|
import { createMopidyClient } from '../api/mopidy.js'
|
||||||
|
import { createJellyfinClient } from '../api/jellyfin.js'
|
||||||
|
|
||||||
const forceMock = import.meta.env.VITE_USE_MOCK === 'true'
|
const forceMock = import.meta.env.VITE_USE_MOCK === 'true'
|
||||||
|
|
||||||
@@ -14,18 +15,82 @@ export function createApi() {
|
|||||||
signalk: createSignalKMock(),
|
signalk: createSignalKMock(),
|
||||||
snapcast: createSnapcastMock(),
|
snapcast: createSnapcastMock(),
|
||||||
mopidy: createMopidyMock(),
|
mopidy: createMopidyMock(),
|
||||||
|
jellyfin: createJellyfinClient('http://localhost:8090/jellyfin', 'fake-key'),
|
||||||
isMock: true,
|
isMock: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapcastHost = import.meta.env.VITE_SNAPCAST_HOST || 'localhost'
|
// Real clients - use proxy-friendly URLs (going through port 8090)
|
||||||
const signalkHost = import.meta.env.VITE_SIGNALK_HOST || 'localhost'
|
const host = window.location.host
|
||||||
const mopidyHost = import.meta.env.VITE_MOPIDY_HOST || 'localhost'
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
const httpProtocol = window.location.protocol
|
||||||
|
|
||||||
|
// SignalK: baseUrl becomes ws://host/signalk (v1/stream appended in client)
|
||||||
|
const signalkReal = createSignalKClient(`${protocol}//${host}`)
|
||||||
|
|
||||||
|
// Snapcast: ws://host/snapcast-ws/jsonrpc
|
||||||
|
const snapcastUrl = `${protocol}//${host}/snapcast-ws/jsonrpc`
|
||||||
|
const snapcastReal = createSnapcastClient(snapcastUrl)
|
||||||
|
|
||||||
|
// Mopidy: ws://host/mopidy (ws appended in client)
|
||||||
|
const mopidyReal = createMopidyClient(`${protocol}//${host}`)
|
||||||
|
|
||||||
|
// Jellyfin: http://host/jellyfin
|
||||||
|
const jellyfinReal = createJellyfinClient(`${httpProtocol}//${host}/jellyfin`, 'YOUR_JELLYFIN_API_KEY')
|
||||||
|
|
||||||
|
const signalkMock = createSignalKMock()
|
||||||
|
|
||||||
|
// Proxy that switches between real and mock
|
||||||
|
const signalkProxy = {
|
||||||
|
listeners: {},
|
||||||
|
on: (event, handler) => {
|
||||||
|
if (!signalkProxy.listeners[event]) signalkProxy.listeners[event] = []
|
||||||
|
signalkProxy.listeners[event].push(handler)
|
||||||
|
signalkReal.on(event, handler)
|
||||||
|
signalkMock.on(event, handler)
|
||||||
|
},
|
||||||
|
off: (event, handler) => {
|
||||||
|
if (signalkProxy.listeners[event]) {
|
||||||
|
signalkProxy.listeners[event] = signalkProxy.listeners[event].filter(h => h !== handler)
|
||||||
|
}
|
||||||
|
signalkReal.off(event, handler)
|
||||||
|
signalkMock.off(event, handler)
|
||||||
|
},
|
||||||
|
emit: (event, data) => {
|
||||||
|
if (signalkProxy.listeners[event]) {
|
||||||
|
signalkProxy.listeners[event].forEach(h => h(data))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
start: () => signalkMock.start(),
|
||||||
|
stop: () => signalkMock.stop(),
|
||||||
|
getSnapshot: () => signalkMock.getSnapshot(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let usingSignalKMock = false
|
||||||
|
let signalkTimeout = setTimeout(() => {
|
||||||
|
// If no real data after 5 seconds, switch to mock
|
||||||
|
if (!usingSignalKMock) {
|
||||||
|
console.log('⚠️ SignalK not connected - using mock data (EYC Lingen → Ems route)')
|
||||||
|
usingSignalKMock = true
|
||||||
|
signalkMock.start()
|
||||||
|
signalkProxy.emit('connected', null)
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
// If real connection succeeds, cancel mock
|
||||||
|
signalkReal.on('connected', () => {
|
||||||
|
clearTimeout(signalkTimeout)
|
||||||
|
if (usingSignalKMock) {
|
||||||
|
signalkMock.stop()
|
||||||
|
usingSignalKMock = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
signalk: createSignalKClient(`ws://${signalkHost}:3000`),
|
signalk: signalkProxy,
|
||||||
snapcast: createSnapcastClient(`ws://${snapcastHost}:1705`),
|
snapcast: snapcastReal,
|
||||||
mopidy: createMopidyClient(`ws://${mopidyHost}:6680`),
|
mopidy: mopidyReal,
|
||||||
|
jellyfin: jellyfinReal,
|
||||||
isMock: false,
|
isMock: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
// Simulates a SignalK WebSocket delta stream with realistic Baltic Sea boat data.
|
// Simulates a SignalK WebSocket delta stream with realistic boat data.
|
||||||
// The ship navigates a realistic route around Bornholm Island, Baltic Sea.
|
// Route: Lokale Fahrt in Lingen über DEK und Ems (bleibt in der Region)
|
||||||
|
|
||||||
const INTERVAL_MS = 1000
|
const INTERVAL_MS = 1000
|
||||||
|
|
||||||
function degToRad(d) { return d * Math.PI / 180 }
|
function degToRad(d) { return d * Math.PI / 180 }
|
||||||
function radToDeg(r) { return r * 180 / Math.PI }
|
function radToDeg(r) { return r * 180 / Math.PI }
|
||||||
|
|
||||||
// Realistic sailing route around Bornholm Island, Baltic Sea
|
// Lokale Route: Lingen Bereich - DEK und Ems (bleibt in der Region)
|
||||||
const WAYPOINTS = [
|
const WAYPOINTS = [
|
||||||
{ lat: 54.3233, lon: 10.1394, name: 'Kiel Fjord (Start)' },
|
{ lat: 52.5236, lon: 7.3200, name: 'EYC Segelclub Lingen' },
|
||||||
{ lat: 55.0500, lon: 13.5500, name: 'Bornholm North' },
|
{ lat: 52.5280, lon: 7.3150, name: 'Kanal Ausfahrt' },
|
||||||
{ lat: 55.1200, lon: 14.8000, name: 'Rønne Harbor' },
|
{ lat: 52.5400, lon: 7.3000, name: 'DEK Westlich' },
|
||||||
{ lat: 54.9500, lon: 15.2000, name: 'Bornholm East' },
|
{ lat: 52.5500, lon: 7.2800, name: 'DEK Schleife West' },
|
||||||
{ lat: 54.5800, lon: 14.9000, name: 'Bornholm South' },
|
{ lat: 52.5600, lon: 7.2700, name: 'DEK Nord' },
|
||||||
{ lat: 54.1500, lon: 13.2000, name: 'Gdansk Approach' },
|
{ lat: 52.5700, lon: 7.2800, name: 'Brücke Nord' },
|
||||||
{ lat: 54.3233, lon: 10.1394, name: 'Kiel Fjord (Loop)' },
|
{ lat: 52.5750, lon: 7.3000, name: 'Ems Einfahrt' },
|
||||||
|
{ lat: 52.5800, lon: 7.3200, name: 'Ems Fluss Ost' },
|
||||||
|
{ lat: 52.5750, lon: 7.3400, name: 'Ems Kurve' },
|
||||||
|
{ lat: 52.5650, lon: 7.3500, name: 'Ems Süd' },
|
||||||
|
{ lat: 52.5500, lon: 7.3450, name: 'Ems Rückkehr' },
|
||||||
|
{ lat: 52.5400, lon: 7.3350, name: 'Kanal Rückkehr' },
|
||||||
|
{ lat: 52.5300, lon: 7.3250, name: 'Hafen Approach' },
|
||||||
|
{ lat: 52.5236, lon: 7.3200, name: 'EYC Segelclub (Ziel)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Calculate distance between two coordinates in nautical miles
|
// Calculate distance between two coordinates in nautical miles
|
||||||
|
|||||||
@@ -1,42 +1,100 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { useNMEA } from '../hooks/useNMEA.js'
|
import { useNMEA } from '../hooks/useNMEA.js'
|
||||||
import Compass from '../components/instruments/Compass.jsx'
|
import BoatControl from '../components/systems/BoatControl.jsx'
|
||||||
import SpeedLog from '../components/instruments/SpeedLog.jsx'
|
import FloorPlan from '../components/systems/FloorPlan.jsx'
|
||||||
import DepthSounder from '../components/instruments/DepthSounder.jsx'
|
|
||||||
import WindRose from '../components/instruments/WindRose.jsx'
|
|
||||||
import NowPlaying from '../components/audio/NowPlaying.jsx'
|
|
||||||
import ZoneGrid from '../components/audio/ZoneGrid.jsx'
|
import ZoneGrid from '../components/audio/ZoneGrid.jsx'
|
||||||
|
import RadioBrowser from '../components/audio/RadioBrowser.jsx'
|
||||||
|
import LibraryBrowser from '../components/audio/LibraryBrowser.jsx'
|
||||||
|
import InstrumentPanel from '../components/nav/InstrumentPanel.jsx'
|
||||||
|
import EngineData from '../components/systems/EngineData.jsx'
|
||||||
|
import BatteryStatus from '../components/systems/BatteryStatus.jsx'
|
||||||
|
|
||||||
export default function Overview() {
|
export default function Overview() {
|
||||||
const nmea = useNMEA()
|
const nmea = useNMEA()
|
||||||
|
const [activeCategory, setActiveCategory] = useState(null)
|
||||||
|
|
||||||
|
const handleBack = () => setActiveCategory(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
{/* Instruments row */}
|
{!activeCategory ? (
|
||||||
<section style={styles.instruments}>
|
<BoatControl
|
||||||
<Compass heading={nmea.heading} cog={nmea.cog} />
|
activeCategory={activeCategory}
|
||||||
<SpeedLog sog={nmea.sog} />
|
onCategoryChange={setActiveCategory}
|
||||||
<DepthSounder depth={nmea.depth} />
|
/>
|
||||||
<WindRose windAngle={nmea.windAngle} windSpeed={nmea.windSpeed} />
|
) : (
|
||||||
</section>
|
<div style={styles.detailView}>
|
||||||
|
<div style={styles.detailHeader}>
|
||||||
|
<button onClick={handleBack} style={styles.backBtn}>← Back</button>
|
||||||
|
<h2 style={styles.detailTitle}>{activeCategory.toUpperCase()}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Now Playing */}
|
<div style={styles.detailContent}>
|
||||||
<NowPlaying compact />
|
{activeCategory === 'lights' && <FloorPlan type="lights" />}
|
||||||
|
{activeCategory === 'climate' && <FloorPlan type="climate" />}
|
||||||
{/* Zone quick overview */}
|
{activeCategory === 'audio' && (
|
||||||
<section>
|
<div style={styles.audioWrapper}>
|
||||||
<div style={styles.sectionTitle}>Audio Zones</div>
|
|
||||||
<ZoneGrid />
|
<ZoneGrid />
|
||||||
</section>
|
</div>
|
||||||
|
)}
|
||||||
|
{activeCategory === 'nav' && <InstrumentPanel />}
|
||||||
|
{activeCategory === 'engine' && <EngineData />}
|
||||||
|
{activeCategory === 'battery' && <BatteryStatus />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
page: { padding: 16, display: 'flex', flexDirection: 'column', gap: 16, overflow: 'auto', flex: 1 },
|
page: {
|
||||||
instruments: {
|
height: '100%',
|
||||||
display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'center',
|
display: 'flex',
|
||||||
padding: 16, background: 'var(--surface)',
|
flexDirection: 'column',
|
||||||
borderRadius: 'var(--radius)', border: '1px solid var(--border)',
|
background: '#07111f',
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
sectionTitle: { fontWeight: 600, fontSize: 12, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 10 },
|
detailView: {
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
animation: 'fadeIn 0.4s ease-out',
|
||||||
|
padding: 20,
|
||||||
|
background: 'radial-gradient(circle at center, #1a2a3a 0%, #07111f 100%)',
|
||||||
|
},
|
||||||
|
detailHeader: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 20,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
backBtn: {
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
color: 'var(--text)',
|
||||||
|
padding: '8px 16px',
|
||||||
|
borderRadius: 8,
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
minHeight: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
},
|
||||||
|
detailTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 300,
|
||||||
|
letterSpacing: '0.2em',
|
||||||
|
color: 'rgba(255,255,255,0.8)',
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
detailContent: {
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '0 10px',
|
||||||
|
},
|
||||||
|
audioWrapper: {
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
dev.bat
Normal file
5
dev.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
echo Starting boWave Development Environment...
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||||
|
echo Dashboard will be available at http://localhost:8090
|
||||||
|
pause
|
||||||
@@ -9,17 +9,25 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- SIGNALK_DEMO=true # Built-in demo NMEA data generator
|
- SIGNALK_DEMO=true # Built-in demo NMEA data generator
|
||||||
|
|
||||||
# Librespot: stub in dev (pipe backend doesn't cross VM boundary).
|
|
||||||
# For real Spotify on Mac run: make spotify
|
|
||||||
librespot:
|
librespot:
|
||||||
build:
|
build:
|
||||||
context: ./docker/librespot
|
context: ./docker/librespot
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile
|
||||||
entrypoint: []
|
ports:
|
||||||
command: ["sh", "-c", "echo 'librespot stub: run make spotify for Mac audio' && sleep infinity"]
|
- "57621:57621/udp"
|
||||||
restart: "no"
|
- "57621:57621/tcp"
|
||||||
volumes: []
|
command: [
|
||||||
ports: []
|
"--name", "${SPOTIFY_NAME:-Bordanlage} (Dev)",
|
||||||
|
"--bitrate", "${SPOTIFY_BITRATE:-320}",
|
||||||
|
"--backend", "pipe",
|
||||||
|
"--device", "/tmp/audio/spotify.pcm",
|
||||||
|
"--zeroconf-port", "57621"
|
||||||
|
]
|
||||||
|
volumes:
|
||||||
|
- pipes:/tmp/audio
|
||||||
|
networks:
|
||||||
|
- bordanlage
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
# Zones: real snapclient containers with null player (v0.35+, URI format)
|
# Zones: real snapclient containers with null player (v0.35+, URI format)
|
||||||
zone-salon:
|
zone-salon:
|
||||||
@@ -52,4 +60,4 @@ services:
|
|||||||
- VITE_SIGNALK_HOST=localhost
|
- VITE_SIGNALK_HOST=localhost
|
||||||
- VITE_MOPIDY_HOST=localhost
|
- VITE_MOPIDY_HOST=localhost
|
||||||
- VITE_JELLYFIN_HOST=localhost
|
- VITE_JELLYFIN_HOST=localhost
|
||||||
- VITE_USE_MOCK=true # Use mock data for dev testing (real APIs when false)
|
- VITE_USE_MOCK=false # FALSE = Nutze echte Services (Spotify, AirPlay, Mopidy)
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- bordanlage
|
- bordanlage
|
||||||
|
|
||||||
# ─── Audio Sources ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
librespot:
|
librespot:
|
||||||
build: ./docker/librespot
|
build: ./docker/librespot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -47,12 +45,13 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "57621:57621/udp" # Spotify zeroconf discovery
|
- "57621:57621/udp" # Spotify zeroconf discovery
|
||||||
- "57621:57621/tcp"
|
- "57621:57621/tcp"
|
||||||
command: >
|
command: [
|
||||||
--name "${SPOTIFY_NAME:-Bordanlage}"
|
"--name", "${SPOTIFY_NAME:-Bordanlage}",
|
||||||
--bitrate ${SPOTIFY_BITRATE:-320}
|
"--bitrate", "${SPOTIFY_BITRATE:-320}",
|
||||||
--backend pipe
|
"--backend", "pipe",
|
||||||
--device /tmp/audio/spotify.pcm
|
"--device", "/tmp/audio/spotify.pcm",
|
||||||
--zeroconf-port 57621
|
"--zeroconf-port", "57621"
|
||||||
|
]
|
||||||
volumes:
|
volumes:
|
||||||
- pipes:/tmp/audio
|
- pipes:/tmp/audio
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
# Stage 1: Build librespot from source
|
# Use the official librespot image
|
||||||
# Pin to v0.5.0 — v0.8.0 has a vergen_lib dependency conflict
|
FROM ghcr.io/librespot-org/librespot:latest
|
||||||
FROM rust:slim-bookworm AS builder
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends pkg-config libssl-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN cargo install librespot --version "=0.5.0"
|
|
||||||
|
|
||||||
# Stage 2: Minimal runtime image
|
# The official image already has the entrypoint set to librespot
|
||||||
FROM debian:bookworm-slim
|
# We just need to ensure it's used correctly in docker-compose
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
COPY --from=builder /usr/local/cargo/bin/librespot /usr/local/bin/librespot
|
|
||||||
ENTRYPOINT ["librespot"]
|
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Create the audio directory if it doesn't exist
|
||||||
mkdir -p /tmp/audio
|
mkdir -p /tmp/audio
|
||||||
|
|
||||||
|
# Create the named pipes if they don't exist
|
||||||
|
for pipe in spotify.pcm airplay.pcm mopidy.pcm; do
|
||||||
|
path="/tmp/audio/$pipe"
|
||||||
|
if [ ! -p "$path" ]; then
|
||||||
|
mkfifo "$path"
|
||||||
|
echo "Created pipe: $path"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All audio pipes ready. Starting Mopidy..."
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
ARG VERSION=0.35.0
|
ARG VERSION=0.35.0
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates wget \
|
ca-certificates \
|
||||||
&& ARCH=$(dpkg --print-architecture) \
|
curl \
|
||||||
&& wget -q "https://github.com/badaix/snapcast/releases/download/v${VERSION}/snapclient_${VERSION}-1_${ARCH}_bookworm.deb" \
|
|
||||||
-O /tmp/snapclient.deb \
|
|
||||||
&& dpkg -i /tmp/snapclient.deb || apt-get install -fy \
|
|
||||||
&& rm /tmp/snapclient.deb \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ENTRYPOINT ["snapclient"]
|
RUN ARCH=$(dpkg --print-architecture) \
|
||||||
|
&& curl -L "https://github.com/badaix/snapcast/releases/download/v${VERSION}/snapclient_${VERSION}-1_${ARCH}_bookworm.deb" -o /tmp/snapclient.deb \
|
||||||
|
&& dpkg -i /tmp/snapclient.deb || apt-get install -fy \
|
||||||
|
&& rm /tmp/snapclient.deb
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/snapclient"]
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
ARG VERSION=0.35.0
|
ARG VERSION=0.35.0
|
||||||
|
ARG WEB_VERSION=0.9.1
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates wget \
|
ca-certificates \
|
||||||
&& ARCH=$(dpkg --print-architecture) \
|
curl \
|
||||||
&& wget -q "https://github.com/badaix/snapcast/releases/download/v${VERSION}/snapserver_${VERSION}-1_${ARCH}_bookworm.deb" \
|
unzip \
|
||||||
-O /tmp/snapserver.deb \
|
|
||||||
&& dpkg -i /tmp/snapserver.deb || apt-get install -fy \
|
|
||||||
&& rm /tmp/snapserver.deb \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN ARCH=$(dpkg --print-architecture) \
|
||||||
|
&& curl -L "https://github.com/badaix/snapcast/releases/download/v${VERSION}/snapserver_${VERSION}-1_${ARCH}_bookworm.deb" -o /tmp/snapserver.deb \
|
||||||
|
&& dpkg -i /tmp/snapserver.deb || apt-get install -fy \
|
||||||
|
&& rm /tmp/snapserver.deb
|
||||||
|
|
||||||
|
RUN curl -L "https://github.com/badaix/snapweb/releases/download/v${WEB_VERSION}/snapweb.zip" -o /tmp/snapweb.zip \
|
||||||
|
&& mkdir -p /usr/share/snapserver/snapweb \
|
||||||
|
&& unzip -o /tmp/snapweb.zip -d /usr/share/snapserver/snapweb \
|
||||||
|
&& rm /tmp/snapweb.zip
|
||||||
|
|
||||||
EXPOSE 1704 1705 1780
|
EXPOSE 1704 1705 1780
|
||||||
CMD ["snapserver"]
|
CMD ["snapserver"]
|
||||||
|
|||||||
BIN
photo_2026-04-02_02-11-20.jpg
Normal file
BIN
photo_2026-04-02_02-11-20.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
BIN
photo_2026-04-02_02-11-30.jpg
Normal file
BIN
photo_2026-04-02_02-11-30.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Reference in New Issue
Block a user