From 4bc5fa83f13cae764ff320d3813423fbbce4f74e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 09:47:21 +0100 Subject: [PATCH 01/63] Initial commit: installer + pinout + docs --- CHANGELOG.md | 31 ++++ Docs/Pinout.md | 24 ++- Docs/Roadmap.md | 4 +- README.md | 2 +- hardware/buttons.py | 107 ------------ input/input_daemon.py | 39 ----- install.sh | 175 +++++++++++++++++++ oled/config.json | 33 ---- oled/graphics/anim/frame01.pbm | 66 ------- oled/graphics/anim/frame02.pbm | 18 -- oled/graphics/anim/frame03.pbm | 18 -- oled/graphics/bits.pbm | 0 oled/graphics/khz.pbm | 0 oled/graphics/logo.pbm | 18 -- oled/graphics/pause.pbm | 0 oled/graphics/play.pbm | 18 -- oled/graphics/radio.pbm | 0 oled/graphics/time.pbm | 0 oled/graphics/volume.pbm | 18 -- oled/oled_daemon.py | 199 --------------------- scripts/clone_repo.sh | 12 ++ scripts/configure_audio.sh | 10 ++ scripts/configure_bt.sh | 7 + scripts/configure_camilladsp.sh | 17 ++ scripts/configure_mpd.sh | 14 ++ scripts/install_packages.sh | 12 ++ scripts/install_python.sh | 6 + start_install.sh | 296 -------------------------------- systemd/oled.service | 11 -- 29 files changed, 306 insertions(+), 849 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 hardware/buttons.py delete mode 100644 input/input_daemon.py create mode 100755 install.sh delete mode 100644 oled/config.json delete mode 100644 oled/graphics/anim/frame01.pbm delete mode 100644 oled/graphics/anim/frame02.pbm delete mode 100644 oled/graphics/anim/frame03.pbm delete mode 100644 oled/graphics/bits.pbm delete mode 100644 oled/graphics/khz.pbm delete mode 100644 oled/graphics/logo.pbm delete mode 100644 oled/graphics/pause.pbm delete mode 100644 oled/graphics/play.pbm delete mode 100644 oled/graphics/radio.pbm delete mode 100644 oled/graphics/time.pbm delete mode 100644 oled/graphics/volume.pbm delete mode 100644 oled/oled_daemon.py create mode 100644 scripts/clone_repo.sh create mode 100644 scripts/configure_audio.sh create mode 100644 scripts/configure_bt.sh create mode 100644 scripts/configure_camilladsp.sh create mode 100644 scripts/configure_mpd.sh create mode 100644 scripts/install_packages.sh create mode 100644 scripts/install_python.sh delete mode 100755 start_install.sh delete mode 100644 systemd/oled.service diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a025832 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog — Raspberry Pi Audio Streamer + +Format zgodny z [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Wersjonowanie zgodne z `0.xxx` (pre‑alpha). + +--- + +## [0.010a] — 2026‑02‑10 +### Dodano +- Utworzono pełną strukturę projektu (`audio/`, `ui/`, `hardware/`, `utils/`, `scripts/`). +- Dodano instalator `install.sh` z wyborem trybu (instalacja/aktualizacja). +- Dodano logger z przełącznikiem `ENABLE_LOGGER`. +- Dodano inteligentne wykrywanie sprzętu (DAC, OLED, BT, Wi‑Fi). +- Dodano bezpieczne restartowanie usług (tylko jeśli istnieją). +- Dodano komplet skryptów instalacyjnych w `scripts/`. + +### Zmieniono +- kompletnie przebudowano projekt od nowa +- Uproszczono logikę instalatora — jedno pytanie na start. +- Ujednolicono komunikaty instalatora (OK / WARN / ERROR). + +### Znane problemy +- Brak pełnej konfiguracji CamillaDSP (placeholder). +- Brak usługi OLED (zostanie dodana po implementacji UI). + +--- + +## [0.000] — 2026‑02‑01 +### Start projektu +- Utworzenie repozytorium. +- Wstępne założenia funkcjonalne. diff --git a/Docs/Pinout.md b/Docs/Pinout.md index 906ab34..b1d7e40 100644 --- a/Docs/Pinout.md +++ b/Docs/Pinout.md @@ -1,4 +1,4 @@ -# Pinout – Streamer v0.07a1 (wersja minimalna) +# Pinout – Streamer v0.010a1 (wersja minimalna) ## Raspberry Pi – sygnały krytyczne @@ -8,19 +8,33 @@ | I2C SCL (OLED) | 3 | 5 | I2C | jw. | | GND | — | 6 | Masa | Wspólna masa | | 1-Wire (DS18B20) | 4 | 7 | 1-Wire | Czujniki temperatury | -| Enkoder A | 17 | 11 | GPIO | Enkoder 1 – sygnał A | -| Enkoder B | 27 | 13 | GPIO | Enkoder 1 – sygnał B | -| Enkoder SW | 22 | 15 | GPIO | Klik enkodera | +| Enkoder A | 24 | 18 | GPIO | Enkoder 1 – sygnał A | +| Enkoder B | 23 | 16 | GPIO | Enkoder 1 – sygnał B | +| Enkoder SW | 13 | 33 | GPIO | Klik enkodera | | IR odbiornik | 25 | 22 | GPIO | TSOP / VS1838B | | GND | — | 9/14/20/25 | Masa | Użyj kilku dla stabilności | +Pin GPIO Funkcja +29 GPIO5 BTN_POWER +31 GPIO6 BTN_STOP +32 GPIO12 BTN_PLAY/PAUSE +35 GPIO19 BTN_NEXT +37 GPIO26 BTN_PREV + + +Pin GPIO Kolor +36 GPIO16 LED_R +38 GPIO20 LED_G +40 GPIO21 LED_B +Każdy kolor przez rezystor 150–330 Ω. + ## OLED - Magistrala I2C (SDA/SCL) - Adres: 0x3C ## MPD - Standardowa konfiguracja `/etc/mpd.conf` -- Upewnić się, że `/etc/default/mpd` zawiera: +- Upewnić się, że `/etc/default/mpd` zawiera: `MPDCONF=/etc/mpd.conf` ## Rezerwa diff --git a/Docs/Roadmap.md b/Docs/Roadmap.md index 4ca19c9..8a79e29 100644 --- a/Docs/Roadmap.md +++ b/Docs/Roadmap.md @@ -2,7 +2,7 @@ Ten dokument opisuje plan rozwoju projektu w kolejnych etapach wersjonowania. Każda główna gałąź wersji ma jasno określony cel, zakres oraz oczekiwany poziom stabilności. -v0.07a1 – aktualny etap +v0.010a1 – aktualny etap Pierwsza faza implementacji funkcjonalności wejściowych. W tej wersji pojawia się obsługa enkodera (A/B/SW), integracja z MPD oraz stabilizacja OLED. @@ -164,4 +164,4 @@ finalized enclosure, complete documentation. -This is the first public release of the project. \ No newline at end of file +This is the first public release of the project. diff --git a/README.md b/README.md index 44372bb..7760d75 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Projekt jest rozwijany z naciskiem na: ### Instalacja przez `curl` ```bash -curl -s https://gitlab.com/aloisy/streamer/-/raw/master/start_install.sh -o install.sh +curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/install.sh | bash | tee install.log chmod +x install.sh ./install.sh diff --git a/hardware/buttons.py b/hardware/buttons.py deleted file mode 100644 index 8195690..0000000 --- a/hardware/buttons.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -import RPi.GPIO as GPIO -import os -import json - -BASE_DIR = os.path.join(os.path.expanduser("~"), "streamer") -GPIO_MAP_PATH = os.path.join(BASE_DIR, "config", "gpio.json") - - -class Buttons: - """ - Moduł obsługi wejść: - - enkoder (A/B/SW) - - przyciski GPIO (w przyszłości) - """ - - def __init__(self, on_rotate=None, on_click=None): - self.on_rotate = on_rotate - self.on_click = on_click - - # Wczytaj mapę GPIO - with open(GPIO_MAP_PATH, "r") as f: - gpio = json.load(f) - - enc = gpio.get("encoder", {}) - self.enabled = enc.get("enabled", False) - - self.pin_a = enc.get("pin_a") - self.pin_b = enc.get("pin_b") - self.pin_sw = enc.get("pin_sw") - - GPIO.setmode(GPIO.BCM) - GPIO.setwarnings(False) - - if self.enabled: - self._init_encoder() - - # ----------------------------------------- - # ENCODER - # ----------------------------------------- - - def _init_encoder(self): - """Inicjalizacja enkodera z bezpiecznym fallbackiem.""" - - # Jeśli któryś pin nie jest ustawiony → wyłącz enkoder - if None in (self.pin_a, self.pin_b, self.pin_sw): - print("[buttons] Brak pinów enkodera w gpio.json — wyłączam.") - self.enabled = False - return - - GPIO.setup(self.pin_a, GPIO.IN, pull_up_down=GPIO.PUD_UP) - GPIO.setup(self.pin_b, GPIO.IN, pull_up_down=GPIO.PUD_UP) - GPIO.setup(self.pin_sw, GPIO.IN, pull_up_down=GPIO.PUD_UP) - - self.last_state = GPIO.input(self.pin_a) - - # --- bezpieczne dodawanie przerwań dla A --- - try: - GPIO.add_event_detect( - self.pin_a, - GPIO.BOTH, - callback=self._rotary_callback, - bouncetime=2 - ) - except RuntimeError as e: - print("[buttons] Nie można dodać event_detect dla pin_a:", e) - print("[buttons] Wyłączam obsługę enkodera.") - self.enabled = False - return - - # --- bezpieczne dodawanie przerwań dla SW --- - try: - GPIO.add_event_detect( - self.pin_sw, - GPIO.FALLING, - callback=self._button_callback, - bouncetime=200 - ) - except RuntimeError as e: - print("[buttons] Nie można dodać event_detect dla pin_sw:", e) - print("[buttons] Wyłączam obsługę przycisku.") - - def _rotary_callback(self, channel): - """Obsługa obrotu enkodera.""" - if not self.enabled: - return - - a = GPIO.input(self.pin_a) - b = GPIO.input(self.pin_b) - - if a != self.last_state: - direction = +1 if b != a else -1 - if self.on_rotate: - self.on_rotate(direction) - self.last_state = a - - def _button_callback(self, channel): - """Obsługa kliknięcia.""" - if self.on_click: - self.on_click() - - # ----------------------------------------- - # CLEANUP - # ----------------------------------------- - - def cleanup(self): - GPIO.cleanup() diff --git a/input/input_daemon.py b/input/input_daemon.py deleted file mode 100644 index 45ec14c..0000000 --- a/input/input_daemon.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -import time -import os -import sys - -BASE_DIR = os.path.join(os.path.expanduser("~"), "streamer") -sys.path.append(os.path.join(BASE_DIR, "hardware")) - -from buttons import Buttons - - -def main(): - print("[input_daemon] start") - - # Callbacki — na razie tylko logi - def on_rotate(direction): - print(f"[input_daemon] rotate: {direction}") - - def on_click(): - print("[input_daemon] click") - - # Inicjalizacja hardware - btn = Buttons( - on_rotate=on_rotate, - on_click=on_click - ) - - try: - while True: - time.sleep(0.1) - except KeyboardInterrupt: - pass - finally: - btn.cleanup() - print("[input_daemon] stop") - - -if __name__ == "__main__": - main() diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..591aa91 --- /dev/null +++ b/install.sh @@ -0,0 +1,175 @@ +#!/bin/bash +set -e + +############################################### +# Raspberry Pi Audio Streamer Installer +############################################### + +# --- Logger --- +ENABLE_LOGGER=1 # 1 = logger aktywny, 0 = logger wyłączony +LOGFILE="/home/$USER/streamer_install.log" + +log() { + if [ "$ENABLE_LOGGER" -eq 1 ]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') | $1" | tee -a "$LOGFILE" + else + echo "$1" + fi +} + +log "=== Uruchomiono instalator ===" + +############################################### +# Wybór trybu +############################################### + +echo "==============================================" +echo " Raspberry Pi Audio Streamer Installer" +echo "==============================================" +echo "Wybierz tryb:" +echo "1) Instalacja (pełna konfiguracja)" +echo "2) Aktualizacja (git pull + restart usług)" +read -p "Wybór [1/2]: " MODE + +log "Wybrany tryb: $MODE" + +############################################### +# Funkcje pomocnicze +############################################### + +service_exists() { + systemctl list-unit-files | grep -q "$1" +} + +restart_service_if_exists() { + if service_exists "$1"; then + log "[OK] Restart: $1" + sudo systemctl restart "$1" || log "[!] Błąd restartu: $1" + else + log "[-] Usługa $1 nie istnieje — pomijam." + fi +} + +detect_dac() { + if aplay -l 2>/dev/null | grep -q "sndrpihifiberry"; then + log "[OK] Wykryto DAC PCM5122." + return 0 + else + log "[!] BŁĄD: Nie wykryto DAC PCM5122!" + log " - Sprawdź połączenia I2S" + log " - Sprawdź overlay w /boot/config.txt" + return 1 + fi +} + +detect_oled() { + if i2cdetect -y 1 | grep -q "3c"; then + log "[OK] Wykryto OLED (0x3C)." + return 0 + else + log "[-] OLED nie wykryty — pomijam konfigurację ekranu." + return 1 + fi +} + +detect_bt() { + if lsusb | grep -qi "Bluetooth"; then + log "[OK] Wykryto adapter Bluetooth USB." + return 0 + else + log "[-] Brak adaptera Bluetooth — pomijam BlueALSA." + return 1 + fi +} + +detect_wifi() { + if iwconfig 2>/dev/null | grep -q "wlan0"; then + log "[OK] Wykryto Wi-Fi." + return 0 + else + log "[-] Brak Wi-Fi — radio internetowe może nie działać." + return 1 + fi +} + +############################################### +# TRYB AKTUALIZACJI +############################################### + +if [ "$MODE" = "2" ]; then + log "=== TRYB AKTUALIZACJI ===" + + if [ ! -d "/home/$USER/streamer" ]; then + log "[!] Błąd: katalog /home/$USER/streamer nie istnieje!" + exit 1 + fi + + cd /home/$USER/streamer + log "Pobieram zmiany z GitHub..." + git pull + + log "Wykrywanie sprzętu..." + detect_dac + detect_oled + detect_bt + detect_wifi + + log "Restart usług..." + restart_service_if_exists "mpd.service" + restart_service_if_exists "bluealsa.service" + restart_service_if_exists "camilladsp.service" + restart_service_if_exists "oled.service" + + log "=== Aktualizacja zakończona ===" + exit 0 +fi + +############################################### +# TRYB INSTALACJI +############################################### + +log "=== TRYB INSTALACJI ===" + +read -p "Czy zaktualizować system? [y/N]: " UPD +if [[ "$UPD" =~ ^[Yy]$ ]]; then + log "Aktualizacja systemu..." + sudo apt update + sudo apt upgrade -y +fi + +log "Instalacja pakietów..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_packages.sh) + +log "Konfiguracja audio..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_audio.sh) + +log "Konfiguracja MPD..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_mpd.sh) + +log "Konfiguracja Bluetooth..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_bt.sh) + +log "Konfiguracja CamillaDSP..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_camilladsp.sh) + +log "Instalacja Python..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_python.sh) + +log "Pobieranie projektu..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/clone_repo.sh) + +log "Wykrywanie sprzętu..." +detect_dac +detect_oled +detect_bt +detect_wifi + +log "Restart usług..." +restart_service_if_exists "mpd.service" +restart_service_if_exists "bluealsa.service" +restart_service_if_exists "camilladsp.service" +restart_service_if_exists "oled.service" + +log "==============================================" +log " Instalacja zakończona — uruchom ponownie RPi." +log "==============================================" diff --git a/oled/config.json b/oled/config.json deleted file mode 100644 index db0cbae..0000000 --- a/oled/config.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "oled": { - "enabled": true, - "i2c_bus": 1, - "address": "0x3C", - "width": 128, - "height": 64, - "rotation": 0 - }, - - "encoder": { - "enabled": true, - "pin_a": 17, - "pin_b": 27, - "pin_sw": 22, - "direction": "normal", - "debounce_ms": 2 - }, - - "mpd": { - "host": "localhost", - "port": 6600, - "timeout": 2 - }, - - "features": { - "ir": false, - "temperature": false, - "rgb": false, - "relays": false, - "mcp23017": false - } -} diff --git a/oled/graphics/anim/frame01.pbm b/oled/graphics/anim/frame01.pbm deleted file mode 100644 index c74aa85..0000000 --- a/oled/graphics/anim/frame01.pbm +++ /dev/null @@ -1,66 +0,0 @@ -P1 -128 64 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000111100000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000111111110000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000001111111111100000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000011111000011111000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000111100000000111100000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000001111000000000001111000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000001110000000000000111000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000100000000000000001000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/oled/graphics/anim/frame02.pbm b/oled/graphics/anim/frame02.pbm deleted file mode 100644 index 3bc997c..0000000 --- a/oled/graphics/anim/frame02.pbm +++ /dev/null @@ -1,18 +0,0 @@ -P1 -128 64 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 -0 1 0 1 1 0 1 1 1 1 0 1 1 0 1 0 -0 1 0 1 0 0 1 0 0 1 0 0 1 0 1 0 -0 1 0 1 1 0 1 1 1 1 0 1 1 0 1 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 -0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/oled/graphics/anim/frame03.pbm b/oled/graphics/anim/frame03.pbm deleted file mode 100644 index 497e297..0000000 --- a/oled/graphics/anim/frame03.pbm +++ /dev/null @@ -1,18 +0,0 @@ -P1 -128 64 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 -0 1 0 1 1 0 1 1 1 1 0 1 1 0 1 0 -0 1 0 1 0 0 1 0 0 1 0 0 1 0 1 0 -0 1 0 1 1 0 1 1 1 1 0 1 1 0 1 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 -0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/oled/graphics/bits.pbm b/oled/graphics/bits.pbm deleted file mode 100644 index e69de29..0000000 diff --git a/oled/graphics/khz.pbm b/oled/graphics/khz.pbm deleted file mode 100644 index e69de29..0000000 diff --git a/oled/graphics/logo.pbm b/oled/graphics/logo.pbm deleted file mode 100644 index a7d07b7..0000000 --- a/oled/graphics/logo.pbm +++ /dev/null @@ -1,18 +0,0 @@ -P1 -16 16 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 -0 1 0 1 1 0 1 1 1 1 0 1 1 0 1 0 -0 1 0 1 0 0 1 0 0 1 0 0 1 0 1 0 -0 1 0 1 1 0 1 1 1 1 0 1 1 0 1 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 -0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/oled/graphics/pause.pbm b/oled/graphics/pause.pbm deleted file mode 100644 index e69de29..0000000 diff --git a/oled/graphics/play.pbm b/oled/graphics/play.pbm deleted file mode 100644 index 6a31d75..0000000 --- a/oled/graphics/play.pbm +++ /dev/null @@ -1,18 +0,0 @@ -P1 -16 16 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/oled/graphics/radio.pbm b/oled/graphics/radio.pbm deleted file mode 100644 index e69de29..0000000 diff --git a/oled/graphics/time.pbm b/oled/graphics/time.pbm deleted file mode 100644 index e69de29..0000000 diff --git a/oled/graphics/volume.pbm b/oled/graphics/volume.pbm deleted file mode 100644 index 79eceae..0000000 --- a/oled/graphics/volume.pbm +++ /dev/null @@ -1,18 +0,0 @@ -P1 -16 16 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 -0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 -0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 -0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 -0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 -0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 -0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 -0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 -0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 -0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 -0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 -0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 diff --git a/oled/oled_daemon.py b/oled/oled_daemon.py deleted file mode 100644 index 2c4769c..0000000 --- a/oled/oled_daemon.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python3 -import os -import time -import json -import board, busio -from mpd import MPDClient -from adafruit_ssd1306 import SSD1306_I2C -from PIL import Image, ImageDraw, ImageFont - -BASE_DIR = os.path.join(os.path.expanduser("~"), "streamer") -CONFIG_PATH = os.path.join(BASE_DIR, "oled", "config.json") -GPIO_MAP_PATH = os.path.join(BASE_DIR, "config", "gpio.json") -GRAPHICS_DIR = os.path.join(BASE_DIR, "oled", "graphics") - -# ------------------------------- -# CONFIG LOADERS -# ------------------------------- - -def load_runtime_config(): - defaults = { - "oled": { - "brightness_normal": 80, - "brightness_dim": 1, - "dim_timeout": 30, - "off_timeout": 120 - }, - "mpd": { - "host": "localhost", - "port": 6600, - "timeout": 2 - } - } - try: - with open(CONFIG_PATH, "r") as f: - data = json.load(f) - return {**defaults, **data} - except Exception: - return defaults - - -def load_gpio_map(): - with open(GPIO_MAP_PATH, "r") as f: - return json.load(f) - -# ------------------------------- -# OLED -# ------------------------------- - -def init_display(oled_cfg): - i2c = busio.I2C(board.SCL, board.SDA) - disp = SSD1306_I2C( - oled_cfg["width"], - oled_cfg["height"], - i2c, - addr=int(oled_cfg["address"], 16) - ) - disp.fill(0) - disp.show() - return disp - - -def load_pbm(name): - path = os.path.join(GRAPHICS_DIR, name) - return Image.open(path).convert("1") - - -def show_logo(display): - try: - frames = [] - anim_dir = os.path.join(GRAPHICS_DIR, "anim") - - if os.path.isdir(anim_dir): - for fname in sorted(os.listdir(anim_dir)): - if fname.lower().endswith(".pbm"): - frames.append(load_pbm(os.path.join("anim", fname))) - - if not frames: - frames = [load_pbm("logo.pbm")] - - for _ in range(2): - for frame in frames: - display.image(frame) - display.show() - time.sleep(0.1) - - display.fill(0) - display.show() - time.sleep(1) - - except Exception as e: - print("Logo/anim error:", e) - -# ------------------------------- -# MPD -# ------------------------------- - -def connect_mpd(host, port): - client = MPDClient() - client.timeout = 2 - client.idletimeout = None - try: - client.connect(host, port) - return client - except Exception: - return None - - -def get_mpd_status(client): - try: - status = client.status() - song = client.currentsong() - - station = ( - song.get("name") - or song.get("title") - or song.get("file") - or "Brak danych" - ) - - return { - "station": station, - "volume": status.get("volume", "0"), - "state": status.get("state", "stop"), - "bitrate": status.get("bitrate", ""), - "samplerate": song.get("samplerate", ""), - "bits": song.get("bitdepth", "") - } - except Exception: - return { - "station": "Brak połączenia", - "volume": "0", - "state": "stop", - "bitrate": "", - "samplerate": "", - "bits": "" - } - -# ------------------------------- -# OLED STATUS SCREEN -# ------------------------------- - -def draw_status(display, info): - image = Image.new("1", (128, 64)) - draw = ImageDraw.Draw(image) - font = ImageFont.load_default() - - line1 = info["station"][:20] - line2 = f"Vol:{info['volume']}% {'▶' if info['state']=='play' else '❚❚' if info['state']=='pause' else '■'}" - - draw.text((0, 0), line1, font=font, fill=255) - draw.text((0, 16), line2, font=font, fill=255) - - display.image(image) - display.show() - -# ------------------------------- -# MAIN LOOP -# ------------------------------- - -def main(): - cfg = load_runtime_config() - gpio = load_gpio_map() - - oled_cfg = gpio["oled"] - mpd_cfg = cfg["mpd"] - oled_rt = cfg["oled"] - - display = init_display(oled_cfg) - show_logo(display) - - client = connect_mpd(mpd_cfg["host"], mpd_cfg["port"]) - last_activity = time.time() - - try: - while True: - if client is None: - client = connect_mpd(mpd_cfg["host"], mpd_cfg["port"]) - - info = get_mpd_status(client) - draw_status(display, info) - - idle = time.time() - last_activity - - if idle > oled_rt["off_timeout"]: - display.fill(0) - display.show() - elif idle > oled_rt["dim_timeout"]: - display.contrast(oled_rt["brightness_dim"]) - else: - display.contrast(oled_rt["brightness_normal"]) - - time.sleep(0.5) - - finally: - pass - - -if __name__ == "__main__": - main() diff --git a/scripts/clone_repo.sh b/scripts/clone_repo.sh new file mode 100644 index 0000000..4cbd7f7 --- /dev/null +++ b/scripts/clone_repo.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +echo "[clone_repo] Pobieram projekt z GitHub..." + +cd /home/$USER + +if [ -d "streamer" ]; then + echo "[clone_repo] Katalog streamer już istnieje — pomijam klonowanie." +else + git clone https://github.com//streamer.git +fi diff --git a/scripts/configure_audio.sh b/scripts/configure_audio.sh new file mode 100644 index 0000000..2a972f7 --- /dev/null +++ b/scripts/configure_audio.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +echo "[configure_audio] Konfiguruję I2S + PCM5122..." + +sudo sed -i '/dtparam=i2s=on/d' /boot/config.txt +sudo sed -i '/dtoverlay=hifiberry-dacplus/d' /boot/config.txt + +echo "dtparam=i2s=on" | sudo tee -a /boot/config.txt +echo "dtoverlay=hifiberry-dacplus" | sudo tee -a /boot/config.txt diff --git a/scripts/configure_bt.sh b/scripts/configure_bt.sh new file mode 100644 index 0000000..bee3b76 --- /dev/null +++ b/scripts/configure_bt.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +echo "[configure_bt] Konfiguruję Bluetooth (BlueALSA)..." + +sudo systemctl enable bluetooth +sudo systemctl enable bluealsa diff --git a/scripts/configure_camilladsp.sh b/scripts/configure_camilladsp.sh new file mode 100644 index 0000000..90ccc12 --- /dev/null +++ b/scripts/configure_camilladsp.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +echo "[configure_camilladsp] Konfiguruję CamillaDSP..." + +sudo mkdir -p /etc/camilladsp + +# Domyślny config (placeholder) +cat </dev/null; do - for i in $(seq 0 3); do - printf "\r${BLUE}Przetwarzanie...${RESET} ${spin:$i:1}" - sleep $delay - done - done - printf "\r${GREEN}Zakończono.${RESET}\n" -} - -ensure_line() { - local line="$1" - local file="$2" - if ! grep -Fxq "$line" "$file"; then - echo "$line" | sudo tee -a "$file" >/dev/null - log "Dodano do $(basename "$file"): $line" - return 0 - fi - return 1 -} - -echo -e "${BLUE}==============================================" -echo -e " STREAMER AUDIO – Instalator v$SOFT_VERSION" -echo -e "==============================================${RESET}" -pause_step - -mkdir -p "$LOG_DIR" "$CONFIG_DIR" "$INSTALLER_DIR" "$CHANGELOG_DIR" "$MEDIA_DIR" \ - "$OLED_DIR/graphics/anim" -touch "$LOGFILE" -log "Struktura katalogów gotowa." - -echo -e "${BLUE}Krok 1: Aktualizacja systemu${RESET}" -(sudo apt update && sudo apt upgrade -y) & -spinner $! -log "System zaktualizowany." -pause_step - -echo -e "${BLUE}Krok 2: Instalacja pakietów${RESET}" -(sudo apt install -y git python3 python3-pip python3-venv \ - mpd mpc alsa-utils i2c-tools jq curl wget unzip sox) & -spinner $! -log "Pakiety zainstalowane." -pause_step - -echo -e "${BLUE}Krok 3: Instalacja bibliotek Python${RESET}" -(pip3 install --break-system-packages \ - python-mpd2 RPi.GPIO pillow adafruit-circuitpython-ssd1306 requests) & -spinner $! -log "Biblioteki Python zainstalowane." -pause_step - -echo -e "${BLUE}Krok 4: Synchronizacja config.txt${RESET}" - -CHANGES=0 -ensure_line "dtoverlay=hifiberry-dac" "$CONFIG_TXT" && CHANGES=1 -ensure_line "dtparam=i2c_arm=on" "$CONFIG_TXT" && CHANGES=1 -ensure_line "dtparam=i2s=on" "$CONFIG_TXT" && CHANGES=1 - -if grep -q "^dtparam=audio=on" "$CONFIG_TXT"; then - sudo sed -i 's/^dtparam=audio=on/#dtparam=audio=on/' "$CONFIG_TXT" - log "Wyłączono wbudowane audio." - CHANGES=1 -fi - -log "Synchronizacja config.txt zakończona." -pause_step - -echo -e "${BLUE}Krok 5: Restart MPD${RESET}" -sudo systemctl restart mpd -mpc stop >/dev/null 2>&1 -log "MPD uruchomiony i zatrzymany." -pause_step - -echo -e "${BLUE}Krok 6: Autodetekcja DAC${RESET}" -if aplay -l | grep -qi "sndrpihifiberry"; then - log "Wykryto DAC: PCM5102A" -else - log "Nie wykryto DAC!" -fi -pause_step - -echo -e "${BLUE}Krok 7: Autodetekcja OLED${RESET}" -if sudo i2cdetect -y 1 | grep -q "3c"; then - log "OLED wykryty." - OLED_PRESENT=1 -else - log "OLED nie wykryty." - OLED_PRESENT=0 -fi -pause_step - -echo -e "${BLUE}Krok 8: Dodanie stacji radiowej${RESET}" - -RADIO_URL="http://stream.rcs.revma.com/ye5kghkgcm0uv" - -if mpc playlist | grep -q "$RADIO_URL"; then - RADIO_NEW=0 - log "Stacja radiowa już istnieje." -else - mpc clear - mpc add "$RADIO_URL" - - if ! mpc lsplaylists | grep -q "^radio$"; then - mpc save radio - fi - - mpc volume 30 - mpc play - RADIO_NEW=1 - log "Dodano i uruchomiono Radio 357." -fi - -pause_step - -echo -e "${BLUE}Krok 9: Test DAC${RESET}" - -TEST_WAV="$MEDIA_DIR/test.wav" - -log "Generuję test.wav (400 Hz, stereo, 0.5 s)." -sox -n -r 48000 -b 16 -c 2 "$TEST_WAV" synth 0.5 sine 400 - -mpc stop >/dev/null 2>&1 -sudo systemctl stop mpd - -aplay "$TEST_WAV" -D plughw:0,0 -log "Test DAC zakończony." -pause_step - -echo -e "${BLUE}Krok 10: Test OLED${RESET}" - -if [ "$OLED_PRESENT" -eq 1 ]; then -python3 << 'EOF' -import time -import board, busio -from adafruit_ssd1306 import SSD1306_I2C -from PIL import Image, ImageDraw, ImageFont - -try: - i2c = busio.I2C(board.SCL, board.SDA) - display = SSD1306_I2C(128, 64, i2c, addr=0x3C) -except Exception as e: - print("OLED not detected:", e) - exit(0) - -display.contrast(1) - -image = Image.new("1", (128, 64)) -draw = ImageDraw.Draw(image) -font = ImageFont.load_default() - -text = "STREAMER" -bbox = draw.textbbox((0, 0), text, font=font) -w = bbox[2] - bbox[0] -h = bbox[3] - bbox[1] - -draw.text(((128 - w) // 2, (64 - h) // 2), text, font=font, fill=255) - -display.image(image) -display.show() - -time.sleep(2) - -display.fill(0) -display.show() -EOF - log "OLED test zakończony (niska jasność + wygaszenie)." -else - log "OLED pominięty – brak urządzenia." -fi - -pause_step - -echo -e "${BLUE}Krok 11: Pobieranie i aktualizacja projektu STREAMER${RESET}" - -TMP_DIR=$(mktemp -d) -git clone --depth=1 "$REPO_GIT" "$TMP_DIR" >/dev/null 2>&1 - -if [ $? -ne 0 ]; then - log "Błąd: nie udało się pobrać repozytorium!" - exit 1 -fi - -rsync -av \ - --exclude=installer \ - --exclude=logs \ - --exclude=.git \ - --exclude=.gitignore \ - "$TMP_DIR/" "$STREAMER_DIR/" - -log "Repozytorium zsynchronizowane." -pause_step - -echo -e "${BLUE}Krok 12: Aktualizacja changelog${RESET}" - -if [ -f "$STREAMER_DIR/changelog/change_log" ]; then - cp "$STREAMER_DIR/changelog/change_log" "$CHANGELOG_DIR/latest.txt" - log "Changelog zaktualizowany z repozytorium." -else - log "Brak pliku change_log w repozytorium." -fi - -pause_step - -echo -e "${BLUE}Krok 13: Instalacja usług systemd${RESET}" - -CURRENT_USER="$(whoami)" - -if [ -f "$STREAMER_DIR/systemd/oled.service" ]; then - sudo cp "$STREAMER_DIR/systemd/oled.service" /etc/systemd/system/oled.service - sudo sed -i "s/%i/$CURRENT_USER/g" /etc/systemd/system/oled.service - sudo systemctl enable oled.service - sudo systemctl restart oled.service - log "Usługa OLED zainstalowana i uruchomiona." -else - log "Brak pliku systemd/oled.service w repozytorium." -fi - -if [ -f "$STREAMER_DIR/systemd/input.service" ]; then - sudo cp "$STREAMER_DIR/systemd/input.service" /etc/systemd/system/input.service - sudo sed -i "s/%i/$CURRENT_USER/g" /etc/systemd/system/input.service - sudo systemctl enable input.service - sudo systemctl restart input.service - log "Usługa INPUT zainstalowana i uruchomiona." -else - log "Brak pliku systemd/input.service w repozytorium (pomijam)." -fi - -sudo systemctl daemon-reload -pause_step - -echo -e "${BLUE}Krok 14: Przenoszenie instalatora${RESET}" - -SCRIPT_NAME="$(basename "$0")" -TARGET_PATH="$INSTALLER_DIR/$SCRIPT_NAME" - -if [ "$(realpath "$0")" != "$TARGET_PATH" ]; then - cp "$0" "$TARGET_PATH" - rm "$0" - log "Instalator przeniesiony do $TARGET_PATH" -fi - -pause_step - -mpc stop >/dev/null 2>&1 - -echo -e "${GREEN}==============================================" -echo -e " INSTALACJA ZAKOŃCZONA SUKCESEM" -echo -e " Log: $LOGFILE" -echo -e "${GREEN}==============================================${RESET}" - -log "=== Instalacja zakończona pomyślnie ===" diff --git a/systemd/oled.service b/systemd/oled.service deleted file mode 100644 index af7faa7..0000000 --- a/systemd/oled.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Streamer OLED Display Daemon -After=network.target mpd.service - -[Service] -ExecStart=/usr/bin/python3 /home/%i/streamer/oled/oled_daemon.py -User=%i -Restart=always - -[Install] -WantedBy=multi-user.target From 6cdca907c3df2e5b59d473e60b99a4aa0ec002ac Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 10:03:34 +0100 Subject: [PATCH 02/63] update --- audio/player.py | 14 ++++++++++++++ audio/volume.py | 9 +++++++++ config.py | 16 ++++++++++++++++ config/gpio.json | 33 --------------------------------- main.py | 32 ++++++++++++++++++++++++++++++++ ui/display.py | 12 ++++++++++++ ui/encoder.py | 26 ++++++++++++++++++++++++++ ui/menu.py | 15 +++++++++++++++ utils/logger.py | 12 ++++++++++++ 9 files changed, 136 insertions(+), 33 deletions(-) create mode 100644 audio/player.py create mode 100644 audio/volume.py create mode 100644 config.py delete mode 100644 config/gpio.json create mode 100644 main.py create mode 100644 ui/display.py create mode 100644 ui/encoder.py create mode 100644 ui/menu.py create mode 100644 utils/logger.py diff --git a/audio/player.py b/audio/player.py new file mode 100644 index 0000000..0b3e537 --- /dev/null +++ b/audio/player.py @@ -0,0 +1,14 @@ +from mpd import MPDClient + +class Player: + def __init__(self): + self.client = MPDClient() + self.client.connect("localhost", 6600) + + def play_radio(self, url): + self.client.clear() + self.client.add(url) + self.client.play() + + def stop(self): + self.client.stop() diff --git a/audio/volume.py b/audio/volume.py new file mode 100644 index 0000000..ce53e5d --- /dev/null +++ b/audio/volume.py @@ -0,0 +1,9 @@ +from mpd import MPDClient + +class Volume: + def __init__(self): + self.client = MPDClient() + self.client.connect("localhost", 6600) + + def set(self, value): + self.client.setvol(value) diff --git a/config.py b/config.py new file mode 100644 index 0000000..7e8ee8d --- /dev/null +++ b/config.py @@ -0,0 +1,16 @@ +RADIO_STREAM = "http://stream.rcs.revma.com/ypqt40u0x1zuv" +OLED_I2C_ADDR = 0x3C + +GPIO_ENCODER_A = 24 +GPIO_ENCODER_B = 23 +GPIO_ENCODER_SW = 13 + +GPIO_BTN_POWER = 5 +GPIO_BTN_STOP = 6 +GPIO_BTN_PLAY = 12 +GPIO_BTN_NEXT = 19 +GPIO_BTN_PREV = 26 + +GPIO_LED_R = 16 +GPIO_LED_G = 20 +GPIO_LED_B = 21 diff --git a/config/gpio.json b/config/gpio.json deleted file mode 100644 index 7636e37..0000000 --- a/config/gpio.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Version": "0.7a3", - - "oled": { - "type": "ssd1306", - "width": 128, - "height": 64, - "address": "0x3C", - "rotation": "0" - }, - - "i2s": { - "enabled": true, - "dac_overlay": "hifiberry-dac", - "bck": 18, - "lrck": 19, - "din": 21, - "mclk": 5, - "mute": 6 - }, - - "encoder": { - "enabled": true, - "pin_a": 23, - "pin_b": 24, - "pin_sw": 13 - } - - "buttons": { - "enabled": false, - "pins": [] - } -} diff --git a/main.py b/main.py new file mode 100644 index 0000000..e7ca06c --- /dev/null +++ b/main.py @@ -0,0 +1,32 @@ +from utils.logger import setup_logger +from ui.display import Display +from ui.encoder import Encoder +from ui.menu import Menu +from audio.player import Player +from audio.volume import Volume +import config + +def main(): + log = setup_logger() + log.info("Streamer start") + + display = Display() + player = Player() + volume = Volume() + menu = Menu(display, player, volume) + + Encoder( + config.GPIO_ENCODER_A, + config.GPIO_ENCODER_B, + config.GPIO_ENCODER_SW, + callback_rotate=menu.rotate, + callback_press=menu.press + ) + + display.text("Ready") + + while True: + pass + +if __name__ == "__main__": + main() diff --git a/ui/display.py b/ui/display.py new file mode 100644 index 0000000..a6cbc00 --- /dev/null +++ b/ui/display.py @@ -0,0 +1,12 @@ +from luma.core.interface.serial import i2c +from luma.oled.device import ssd1306 + +class Display: + def __init__(self): + serial = i2c(port=1, address=0x3C) + self.device = ssd1306(serial) + + def text(self, msg, line=0): + from luma.core.render import canvas + with canvas(self.device) as draw: + draw.text((0, line * 12), msg, fill=255) diff --git a/ui/encoder.py b/ui/encoder.py new file mode 100644 index 0000000..5c8ae9e --- /dev/null +++ b/ui/encoder.py @@ -0,0 +1,26 @@ +import RPi.GPIO as GPIO + +class Encoder: + def __init__(self, pin_a, pin_b, pin_sw, callback_rotate, callback_press): + GPIO.setmode(GPIO.BCM) + GPIO.setup(pin_a, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(pin_b, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(pin_sw, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + self.pin_a = pin_a + self.pin_b = pin_b + self.pin_sw = pin_sw + self.callback_rotate = callback_rotate + self.callback_press = callback_press + + GPIO.add_event_detect(pin_a, GPIO.FALLING, callback=self._rotary) + GPIO.add_event_detect(pin_sw, GPIO.FALLING, callback=self._press, bouncetime=300) + + def _rotary(self, channel): + if GPIO.input(self.pin_b) == 0: + self.callback_rotate(+1) + else: + self.callback_rotate(-1) + + def _press(self, channel): + self.callback_press() diff --git a/ui/menu.py b/ui/menu.py new file mode 100644 index 0000000..3d87fdc --- /dev/null +++ b/ui/menu.py @@ -0,0 +1,15 @@ +class Menu: + def __init__(self, display, player, volume): + self.display = display + self.player = player + self.volume = volume + self.volume_level = 50 + + def rotate(self, direction): + self.volume_level = max(0, min(100, self.volume_level + direction)) + self.volume.set(self.volume_level) + self.display.text(f"Volume: {self.volume_level}") + + def press(self): + self.player.play_radio("http://stream.rcs.revma.com/ypqt40u0x1zuv") + self.display.text("Playing radio") diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..2c374b6 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,12 @@ +import logging + +def setup_logger(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler("/var/log/streamer.log"), + logging.StreamHandler() + ] + ) + return logging.getLogger("streamer") From a29efb8fc1c9ce4084ee05c453a25028368c172f Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:05:32 +0100 Subject: [PATCH 03/63] Update README.md --- README.md | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7760d75..ee5d3be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD # Streamer Audio – Raspberry Pi I2S DAC + OLED Streamer Audio to otwarto‑źródłowy projekt odtwarzacza audio opartego na Raspberry Pi, z obsługą: @@ -67,17 +66,28 @@ chmod +x install.sh ./install.sh -streamer/ - ├── config/ - │ └── gpio.json - ├── logs/ - │ └── install.log - ├── media/ - │ └── test.wav - ├── installer/ - │ └── start_install.sh - ├── change_log - └── README.md +/streamer +│ +├── main.py # główny loop +├── config.py # ustawienia +│ +├── audio/ +│ ├── player.py # MPD/Spotify/BT/Radio +│ ├── dsp.py # EQ, loudness, filtry (CamillaDSP/ALSA) +│ └── volume.py # głośność (PCM5122 + soft) +│ +├── ui/ +│ ├── display.py # OLED +│ ├── menu.py # logika menu +│ └── encoder.py # enkoder + przyciski +│ +├── hardware/ +│ ├── relays.py # przekaźniki/tyrystory +│ ├── rtc.py # DS3231 (później) +│ └── power.py # standby, mute, itp. +│ +└── utils/ +└── logger.py # logi Projekt składa się z trzech warstw licencyjnych: @@ -119,7 +129,7 @@ Already a pro? Just edit this README.md and make it your own. Want to make it ea ``` cd existing_repo -git remote add origin https://gitlab.com/aloisy/streamer.git +git remote add origin https://raw.githubusercontent.com/xtreamx2/streamer/Second/install.sh git branch -M main git push -uf origin main ``` From 2f4b7a8adaa31079c16385c936cd5bd3b900e301 Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:41:22 +0100 Subject: [PATCH 04/63] Update configure_camilladsp.sh --- scripts/configure_camilladsp.sh | 42 ++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/scripts/configure_camilladsp.sh b/scripts/configure_camilladsp.sh index 90ccc12..507ee1c 100644 --- a/scripts/configure_camilladsp.sh +++ b/scripts/configure_camilladsp.sh @@ -1,17 +1,53 @@ #!/bin/bash set -e -echo "[configure_camilladsp] Konfiguruję CamillaDSP..." +echo "[configure_camilladsp] Instaluję i konfiguruję CamillaDSP..." +# ================================ +# 1. Pobranie CamillaDSP +# ================================ +LATEST_URL=$(curl -s https://api.github.com/repos/HEnquist/camilladsp/releases/latest \ + | grep browser_download_url \ + | grep arm64.deb \ + | cut -d '"' -f 4) + +echo "[configure_camilladsp] Pobieram: $LATEST_URL" +wget -q "$LATEST_URL" -O /tmp/camilladsp.deb + +sudo apt install -y /tmp/camilladsp.deb +rm /tmp/camilladsp.deb + +# ================================ +# 2. Katalog konfiguracyjny +# ================================ sudo mkdir -p /etc/camilladsp -# Domyślny config (placeholder) cat < Date: Mon, 9 Feb 2026 10:57:45 +0100 Subject: [PATCH 05/63] Update install_packages.sh --- scripts/install_packages.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install_packages.sh b/scripts/install_packages.sh index ed56a95..d938847 100644 --- a/scripts/install_packages.sh +++ b/scripts/install_packages.sh @@ -5,8 +5,8 @@ echo "[install_packages] Instaluję pakiety systemowe..." sudo apt install -y \ mpd mpc \ - bluez bluealsa bluealsa-aplay \ - camilladsp \ +# bluez bluealsa bluealsa-aplay \ +# camilladsp \ python3 python3-pip python3-venv \ git i2c-tools alsa-utils \ curl wget unzip From 9c26695de3c2c6442611ead091eac1d27f2fce5e Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:17:35 +0100 Subject: [PATCH 06/63] Update install_python.sh --- scripts/install_python.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/install_python.sh b/scripts/install_python.sh index 7c9d4cd..036ef89 100644 --- a/scripts/install_python.sh +++ b/scripts/install_python.sh @@ -3,4 +3,8 @@ set -e echo "[install_python] Instaluję biblioteki Python..." -pip3 install RPi.GPIO smbus2 pillow mpd2 +python3 -m pip install --break-system-packages \ + RPi.GPIO \ + smbus2 \ + pillow \ + mpd2 From 62e16529d697724d2aa098dde041f611cf2ba1a6 Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:37:05 +0100 Subject: [PATCH 07/63] Update install.sh --- install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 591aa91..aa098b0 100755 --- a/install.sh +++ b/install.sh @@ -111,12 +111,12 @@ if [ "$MODE" = "2" ]; then log "Wykrywanie sprzętu..." detect_dac detect_oled - detect_bt - detect_wifi +# detect_bt +# detect_wifi log "Restart usług..." restart_service_if_exists "mpd.service" - restart_service_if_exists "bluealsa.service" +# restart_service_if_exists "bluealsa.service" restart_service_if_exists "camilladsp.service" restart_service_if_exists "oled.service" From 500413a13ba568c0c76866e235a1ad5ea0afd0e7 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 11:39:45 +0100 Subject: [PATCH 08/63] update --- scripts/install_packages.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/install_packages.sh b/scripts/install_packages.sh index d938847..3715195 100644 --- a/scripts/install_packages.sh +++ b/scripts/install_packages.sh @@ -5,8 +5,10 @@ echo "[install_packages] Instaluję pakiety systemowe..." sudo apt install -y \ mpd mpc \ -# bluez bluealsa bluealsa-aplay \ -# camilladsp \ python3 python3-pip python3-venv \ git i2c-tools alsa-utils \ curl wget unzip + +# pakiety usunięte +# bluez bluealsa bluealsa-aplay +# camilladsp From 50aef48379677508bb91a0c03decb1d7a662caac Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 11:47:19 +0100 Subject: [PATCH 09/63] update --- scripts/configure_bt.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/configure_bt.sh b/scripts/configure_bt.sh index bee3b76..3039a90 100644 --- a/scripts/configure_bt.sh +++ b/scripts/configure_bt.sh @@ -3,5 +3,5 @@ set -e echo "[configure_bt] Konfiguruję Bluetooth (BlueALSA)..." -sudo systemctl enable bluetooth -sudo systemctl enable bluealsa +#sudo systemctl enable bluetooth +#sudo systemctl enable bluealsa From 5fc09d76e5b119bfc475949d39e2d03c1d6cceb3 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 11:53:07 +0100 Subject: [PATCH 10/63] update --- scripts/configure_camilladsp.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/configure_camilladsp.sh b/scripts/configure_camilladsp.sh index 507ee1c..ef06b52 100644 --- a/scripts/configure_camilladsp.sh +++ b/scripts/configure_camilladsp.sh @@ -8,8 +8,13 @@ echo "[configure_camilladsp] Instaluję i konfiguruję CamillaDSP..." # ================================ LATEST_URL=$(curl -s https://api.github.com/repos/HEnquist/camilladsp/releases/latest \ | grep browser_download_url \ - | grep arm64.deb \ - | cut -d '"' -f 4) + | grep -E 'arm64.*\.deb' \ + | cut -d '"' -f 4 | head -n 1) + +if [ -z "$LATEST_URL" ]; then + echo "[configure_camilladsp] Błąd: nie znaleziono pakietu ARM64 w najnowszym release!" + exit 1 +fi echo "[configure_camilladsp] Pobieram: $LATEST_URL" wget -q "$LATEST_URL" -O /tmp/camilladsp.deb @@ -40,7 +45,7 @@ After=sound.target [Service] ExecStart=/usr/bin/camilladsp -p /etc/camilladsp/config.yml Restart=always -User=pi +User=$USER [Install] WantedBy=multi-user.target From 104afc67fe3f4c9241e0af31009e4a628d8b4efc Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 12:58:07 +0100 Subject: [PATCH 11/63] update --- scripts/configure_camilladsp.sh | 56 +++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/scripts/configure_camilladsp.sh b/scripts/configure_camilladsp.sh index ef06b52..9d4de00 100644 --- a/scripts/configure_camilladsp.sh +++ b/scripts/configure_camilladsp.sh @@ -4,26 +4,50 @@ set -e echo "[configure_camilladsp] Instaluję i konfiguruję CamillaDSP..." # ================================ -# 1. Pobranie CamillaDSP +# 0. Sprawdzenie czy już jest # ================================ -LATEST_URL=$(curl -s https://api.github.com/repos/HEnquist/camilladsp/releases/latest \ - | grep browser_download_url \ - | grep -E 'arm64.*\.deb' \ - | cut -d '"' -f 4 | head -n 1) - -if [ -z "$LATEST_URL" ]; then - echo "[configure_camilladsp] Błąd: nie znaleziono pakietu ARM64 w najnowszym release!" - exit 1 -fi +if command -v camilladsp >/dev/null 2>&1; then + echo "[configure_camilladsp] CamillaDSP już jest zainstalowany — pomijam instalację." +else + echo "[configure_camilladsp] CamillaDSP nie znaleziony — instaluję najnowszą wersję." + + # ================================ + # 1. Instalacja zależności + # ================================ + sudo apt install -y build-essential cargo libasound2-dev libssl-dev pkg-config + + # ================================ + # 2. Pobranie źródeł (tylko raz) + # ================================ + if [ ! -d "/home/$USER/camilladsp" ]; then + echo "[configure_camilladsp] Pobieram repozytorium..." + git clone https://github.com/HEnquist/camilladsp /home/$USER/camilladsp + else + echo "[configure_camilladsp] Repozytorium już istnieje — aktualizuję." + cd /home/$USER/camilladsp + git pull + fi -echo "[configure_camilladsp] Pobieram: $LATEST_URL" -wget -q "$LATEST_URL" -O /tmp/camilladsp.deb + # ================================ + # 3. Kompilacja (tylko jeśli brak binarki) + # ================================ + cd /home/$USER/camilladsp -sudo apt install -y /tmp/camilladsp.deb -rm /tmp/camilladsp.deb + if [ ! -f "target/release/camilladsp" ]; then + echo "[configure_camilladsp] Kompiluję CamillaDSP..." + cargo build --release + else + echo "[configure_camilladsp] Binarka już istnieje — pomijam kompilację." + fi + + # ================================ + # 4. Instalacja binarki + # ================================ + sudo cp target/release/camilladsp /usr/bin/ +fi # ================================ -# 2. Katalog konfiguracyjny +# 5. Katalog konfiguracyjny # ================================ sudo mkdir -p /etc/camilladsp @@ -35,7 +59,7 @@ pipeline: EOF # ================================ -# 3. Usługa systemd +# 6. Usługa systemd # ================================ cat < Date: Mon, 9 Feb 2026 13:21:51 +0100 Subject: [PATCH 12/63] update --- scripts/configure_camilladsp.sh | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scripts/configure_camilladsp.sh b/scripts/configure_camilladsp.sh index 9d4de00..53854ba 100644 --- a/scripts/configure_camilladsp.sh +++ b/scripts/configure_camilladsp.sh @@ -12,9 +12,18 @@ else echo "[configure_camilladsp] CamillaDSP nie znaleziony — instaluję najnowszą wersję." # ================================ - # 1. Instalacja zależności + # 1. Instalacja rustup (Rust 1.82+) # ================================ - sudo apt install -y build-essential cargo libasound2-dev libssl-dev pkg-config + if ! command -v rustup >/dev/null 2>&1; then + echo "[configure_camilladsp] Instaluję rustup (Rust 1.82+)..." + curl https://sh.rustup.rs -sSf | sh -s -- -y + source $HOME/.cargo/env + else + echo "[configure_camilladsp] rustup już zainstalowany." + source $HOME/.cargo/env + fi + + rustup update stable # ================================ # 2. Pobranie źródeł (tylko raz) @@ -29,16 +38,11 @@ else fi # ================================ - # 3. Kompilacja (tylko jeśli brak binarki) + # 3. Kompilacja # ================================ cd /home/$USER/camilladsp - - if [ ! -f "target/release/camilladsp" ]; then - echo "[configure_camilladsp] Kompiluję CamillaDSP..." - cargo build --release - else - echo "[configure_camilladsp] Binarka już istnieje — pomijam kompilację." - fi + echo "[configure_camilladsp] Kompiluję CamillaDSP..." + cargo build --release # ================================ # 4. Instalacja binarki From 421e80e48f40b417b5ff392b90aba17196073225 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 13:47:50 +0100 Subject: [PATCH 13/63] update --- scripts/install_python.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_python.sh b/scripts/install_python.sh index 036ef89..12e86b9 100644 --- a/scripts/install_python.sh +++ b/scripts/install_python.sh @@ -7,4 +7,4 @@ python3 -m pip install --break-system-packages \ RPi.GPIO \ smbus2 \ pillow \ - mpd2 + python-mpd2 From 96e00d03b9f0c51f40de275990edea70a37bf32f Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 13:52:20 +0100 Subject: [PATCH 14/63] update --- scripts/clone_repo.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/clone_repo.sh b/scripts/clone_repo.sh index 4cbd7f7..623c3f0 100644 --- a/scripts/clone_repo.sh +++ b/scripts/clone_repo.sh @@ -3,10 +3,18 @@ set -e echo "[clone_repo] Pobieram projekt z GitHub..." -cd /home/$USER +REPO_URL="https://github.com/xtreamx2/streamer.git" +TARGET_DIR="/home/$USER/streamer" +BRANCH="Second" -if [ -d "streamer" ]; then - echo "[clone_repo] Katalog streamer już istnieje — pomijam klonowanie." +# Jeśli repo istnieje → aktualizacja +if [ -d "$TARGET_DIR/.git" ]; then + echo "[clone_repo] Repozytorium już istnieje — aktualizuję." + cd "$TARGET_DIR" + git fetch + git checkout "$BRANCH" + git pull else - git clone https://github.com//streamer.git + echo "[clone_repo] Klonuję repozytorium (gałąź: $BRANCH)..." + git clone -b "$BRANCH" "$REPO_URL" "$TARGET_DIR" fi From fdb7cfab705171d92054fe9f733f6409b0149bf0 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 14:11:20 +0100 Subject: [PATCH 15/63] update --- scripts/configure_audio.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/configure_audio.sh b/scripts/configure_audio.sh index 2a972f7..d64a006 100644 --- a/scripts/configure_audio.sh +++ b/scripts/configure_audio.sh @@ -1,10 +1,20 @@ #!/bin/bash set -e -echo "[configure_audio] Konfiguruję I2S + PCM5122..." +echo "[configure_audio] Konfiguruję I2S + rpi-dac..." -sudo sed -i '/dtparam=i2s=on/d' /boot/config.txt -sudo sed -i '/dtoverlay=hifiberry-dacplus/d' /boot/config.txt +CONFIG="/boot/config.txt" -echo "dtparam=i2s=on" | sudo tee -a /boot/config.txt -echo "dtoverlay=hifiberry-dacplus" | sudo tee -a /boot/config.txt +# Usuń stare overlaye DAC +sudo sed -i '/dtoverlay=hifiberry-dacplus/d' "$CONFIG" +sudo sed -i '/dtoverlay=rpi-dac/d' "$CONFIG" +sudo sed -i '/dtparam=i2s=on/d' "$CONFIG" + +# Dodaj nowe wpisy +echo "dtoverlay=rpi-dac" | sudo tee -a "$CONFIG" >/dev/null +echo "dtparam=i2s=on" | sudo tee -a "$CONFIG" >/dev/null + +echo "[configure_audio] Ustawiono:" +echo " dtoverlay=rpi-dac" +echo " dtparam=i2s=on" +echo "[configure_audio] Restart wymagany, aby DAC został wykryty." From 87935d4abf41ba0e2c1f60c8c760a14704ff2d5c Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 14:20:35 +0100 Subject: [PATCH 16/63] update --- scripts/configure_audio.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/configure_audio.sh b/scripts/configure_audio.sh index d64a006..fef3f52 100644 --- a/scripts/configure_audio.sh +++ b/scripts/configure_audio.sh @@ -5,16 +5,21 @@ echo "[configure_audio] Konfiguruję I2S + rpi-dac..." CONFIG="/boot/config.txt" -# Usuń stare overlaye DAC +# ================================ +# 1. Usuń stare wpisy DAC +# ================================ sudo sed -i '/dtoverlay=hifiberry-dacplus/d' "$CONFIG" sudo sed -i '/dtoverlay=rpi-dac/d' "$CONFIG" sudo sed -i '/dtparam=i2s=on/d' "$CONFIG" -# Dodaj nowe wpisy +# ================================ +# 2. Dodaj właściwy overlay +# ================================ echo "dtoverlay=rpi-dac" | sudo tee -a "$CONFIG" >/dev/null echo "dtparam=i2s=on" | sudo tee -a "$CONFIG" >/dev/null echo "[configure_audio] Ustawiono:" echo " dtoverlay=rpi-dac" echo " dtparam=i2s=on" + echo "[configure_audio] Restart wymagany, aby DAC został wykryty." From 4aa097784bc962545fdf0633ff9bd1fc4e5441e7 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 14:23:35 +0100 Subject: [PATCH 17/63] update --- scripts/configure_audio.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/configure_audio.sh b/scripts/configure_audio.sh index fef3f52..2b4a262 100644 --- a/scripts/configure_audio.sh +++ b/scripts/configure_audio.sh @@ -3,7 +3,8 @@ set -e echo "[configure_audio] Konfiguruję I2S + rpi-dac..." -CONFIG="/boot/config.txt" +# Bookworm używa nowej ścieżki: +CONFIG="/boot/firmware/config.txt" # ================================ # 1. Usuń stare wpisy DAC From a195a6702c8d0f44f6112d408b0c34d21d06d9dc Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 14:26:35 +0100 Subject: [PATCH 18/63] update --- install.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index aa098b0..3930212 100755 --- a/install.sh +++ b/install.sh @@ -51,15 +51,22 @@ restart_service_if_exists() { } detect_dac() { - if aplay -l 2>/dev/null | grep -q "sndrpihifiberry"; then - log "[OK] Wykryto DAC PCM5122." + log "Wykrywanie sprzętu..." + + if aplay -l 2>/dev/null | grep -qi "rpi-dac"; then + log "[OK] Wykryto DAC rpi-dac (I2S)." return 0 - else - log "[!] BŁĄD: Nie wykryto DAC PCM5122!" - log " - Sprawdź połączenia I2S" - log " - Sprawdź overlay w /boot/config.txt" - return 1 fi + + if aplay -l 2>/dev/null | grep -qi "hifiberry"; then + log "[OK] Wykryto DAC Hifiberry / PCM5122." + return 0 + fi + + log "[!] BŁĄD: Nie wykryto żadnego DAC I2S!" + log " - Sprawdź połączenia I2S" + log " - Sprawdź overlay w /boot/firmware/config.txt" + return 1 } detect_oled() { From 9861af8d83b3eeed5134ce88067cacd2609657de Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 14:39:29 +0100 Subject: [PATCH 19/63] update --- install.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3930212..30d6dbb 100755 --- a/install.sh +++ b/install.sh @@ -53,11 +53,13 @@ restart_service_if_exists() { detect_dac() { log "Wykrywanie sprzętu..." + # Wykrywanie prostych DAC-ów I2S (rpi-dac) if aplay -l 2>/dev/null | grep -qi "rpi-dac"; then - log "[OK] Wykryto DAC rpi-dac (I2S)." + log "[OK] Wykryto DAC I2S (rpi-dac)." return 0 fi + # Wykrywanie DAC-ów z EEPROM (Hifiberry / PCM5122) if aplay -l 2>/dev/null | grep -qi "hifiberry"; then log "[OK] Wykryto DAC Hifiberry / PCM5122." return 0 From 433e4f78738c1da2e7ee1a3cd06f700803b39000 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 14:50:22 +0100 Subject: [PATCH 20/63] update --- install.sh | 3 +++ scripts/configure_oled.sh | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 scripts/configure_oled.sh diff --git a/install.sh b/install.sh index 30d6dbb..7a00a07 100755 --- a/install.sh +++ b/install.sh @@ -152,6 +152,9 @@ bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/script log "Konfiguracja audio..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_audio.sh) +log "Konfiguracja I2C..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_oled.sh) + log "Konfiguracja MPD..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_mpd.sh) diff --git a/scripts/configure_oled.sh b/scripts/configure_oled.sh new file mode 100644 index 0000000..539186d --- /dev/null +++ b/scripts/configure_oled.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -e + +echo "[configure_oled] Konfiguruję OLED (I2C)..." + +CONFIG="/boot/firmware/config.txt" + +# ================================ +# 1. Włącz I2C +# ================================ +sudo sed -i '/dtparam=i2c_arm=on/d' "$CONFIG" +echo "dtparam=i2c_arm=on" | sudo tee -a "$CONFIG" >/dev/null + +# ================================ +# 2. Instalacja zależności +# ================================ +sudo apt install -y i2c-tools python3-smbus python3-pil + +# ================================ +# 3. Sprawdzenie magistrali +# ================================ +if [ ! -e /dev/i2c-1 ]; then + echo "[configure_oled] I2C nieaktywne — wymagany restart." + exit 0 +fi + +# ================================ +# 4. Wykrywanie OLED +# ================================ +ADDR=$(i2cdetect -y 1 | grep -oE '3c|3d' | head -n 1) + +if [ -z "$ADDR" ]; then + echo "[configure_oled] [-] OLED nie wykryty — pomijam konfigurację." + exit 0 +fi + +echo "[configure_oled] [OK] Wykryto OLED na adresie 0x$ADDR." + +# ================================ +# 5. Zapis konfiguracji OLED +# ================================ +mkdir -p /etc/streamer +echo "oled_address=0x$ADDR" | sudo tee /etc/streamer/oled.conf >/dev/null + +echo "[configure_oled] OLED gotowy." From 2e78817d27dc205358c9b507eb40cbd0dbac6795 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 14:54:20 +0100 Subject: [PATCH 21/63] update --- install.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/install.sh b/install.sh index 7a00a07..a6e76e2 100755 --- a/install.sh +++ b/install.sh @@ -6,7 +6,7 @@ set -e ############################################### # --- Logger --- -ENABLE_LOGGER=1 # 1 = logger aktywny, 0 = logger wyłączony +ENABLE_LOGGER=1 LOGFILE="/home/$USER/streamer_install.log" log() { @@ -53,13 +53,11 @@ restart_service_if_exists() { detect_dac() { log "Wykrywanie sprzętu..." - # Wykrywanie prostych DAC-ów I2S (rpi-dac) if aplay -l 2>/dev/null | grep -qi "rpi-dac"; then log "[OK] Wykryto DAC I2S (rpi-dac)." return 0 fi - # Wykrywanie DAC-ów z EEPROM (Hifiberry / PCM5122) if aplay -l 2>/dev/null | grep -qi "hifiberry"; then log "[OK] Wykryto DAC Hifiberry / PCM5122." return 0 @@ -72,6 +70,11 @@ detect_dac() { } detect_oled() { + if [ ! -e /dev/i2c-1 ]; then + log "[-] I2C nieaktywne — OLED nie może być wykryty." + return 1 + fi + if i2cdetect -y 1 | grep -q "3c"; then log "[OK] Wykryto OLED (0x3C)." return 0 @@ -120,12 +123,9 @@ if [ "$MODE" = "2" ]; then log "Wykrywanie sprzętu..." detect_dac detect_oled -# detect_bt -# detect_wifi log "Restart usług..." restart_service_if_exists "mpd.service" -# restart_service_if_exists "bluealsa.service" restart_service_if_exists "camilladsp.service" restart_service_if_exists "oled.service" @@ -149,10 +149,10 @@ fi log "Instalacja pakietów..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_packages.sh) -log "Konfiguracja audio..." +log "Konfiguracja audio (I2S)..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_audio.sh) -log "Konfiguracja I2C..." +log "Konfiguracja I2C + OLED..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_oled.sh) log "Konfiguracja MPD..." From 47123f3ae46fcfeac558155dc463551c5e32c2d6 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 15:08:59 +0100 Subject: [PATCH 22/63] update --- install.sh | 5 ++++- scripts/configure_audio.sh | 12 +++--------- scripts/configure_i2c.sh | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 scripts/configure_i2c.sh diff --git a/install.sh b/install.sh index a6e76e2..bb2856a 100755 --- a/install.sh +++ b/install.sh @@ -152,7 +152,10 @@ bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/script log "Konfiguracja audio (I2S)..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_audio.sh) -log "Konfiguracja I2C + OLED..." +log "Konfiguracja I2C..." +bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_i2c.sh) + +log "Konfiguracja OLED..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_oled.sh) log "Konfiguracja MPD..." diff --git a/scripts/configure_audio.sh b/scripts/configure_audio.sh index 2b4a262..bd68f86 100644 --- a/scripts/configure_audio.sh +++ b/scripts/configure_audio.sh @@ -3,24 +3,18 @@ set -e echo "[configure_audio] Konfiguruję I2S + rpi-dac..." -# Bookworm używa nowej ścieżki: CONFIG="/boot/firmware/config.txt" -# ================================ -# 1. Usuń stare wpisy DAC -# ================================ +# Czyścimy stare wpisy DAC sudo sed -i '/dtoverlay=hifiberry-dacplus/d' "$CONFIG" sudo sed -i '/dtoverlay=rpi-dac/d' "$CONFIG" sudo sed -i '/dtparam=i2s=on/d' "$CONFIG" -# ================================ -# 2. Dodaj właściwy overlay -# ================================ +# Dodajemy DAC PO I2C (I2C ustawia configure_i2c.sh) echo "dtoverlay=rpi-dac" | sudo tee -a "$CONFIG" >/dev/null -echo "dtparam=i2s=on" | sudo tee -a "$CONFIG" >/dev/null +echo "dtparam=i2s=on" | sudo tee -a "$CONFIG" >/dev/null echo "[configure_audio] Ustawiono:" echo " dtoverlay=rpi-dac" echo " dtparam=i2s=on" - echo "[configure_audio] Restart wymagany, aby DAC został wykryty." diff --git a/scripts/configure_i2c.sh b/scripts/configure_i2c.sh new file mode 100644 index 0000000..2beeeec --- /dev/null +++ b/scripts/configure_i2c.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +echo "[configure_i2c] Włączam I2C..." + +CONFIG="/boot/firmware/config.txt" + +# Usuń stare wpisy +sudo sed -i '/dtparam=i2c_arm=on/d' "$CONFIG" +sudo sed -i '/dtoverlay=i2c1/d' "$CONFIG" + +# Dodaj poprawną konfigurację I2C1 na pinach 2/3 +echo "dtparam=i2c_arm=on" | sudo tee -a "$CONFIG" >/dev/null +echo "dtoverlay=i2c1,pins_2_3" | sudo tee -a "$CONFIG" >/dev/null + +echo "[configure_i2c] Ustawiono:" +echo " dtparam=i2c_arm=on" +echo " dtoverlay=i2c1,pins_2_3" +echo "[configure_i2c] Restart wymagany, aby I2C zostało aktywowane." From 3f79bd4cbe1bc1e96bf726f413595590638daf56 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 15:32:30 +0100 Subject: [PATCH 23/63] update --- scripts/configure_i2c.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/configure_i2c.sh b/scripts/configure_i2c.sh index 2beeeec..2cc034e 100644 --- a/scripts/configure_i2c.sh +++ b/scripts/configure_i2c.sh @@ -17,3 +17,11 @@ echo "[configure_i2c] Ustawiono:" echo " dtparam=i2c_arm=on" echo " dtoverlay=i2c1,pins_2_3" echo "[configure_i2c] Restart wymagany, aby I2C zostało aktywowane." + +# Załaduj moduły I2C teraz +sudo modprobe i2c-bcm2835 || true +sudo modprobe i2c-dev || true + +# Upewnij się, że ładują się przy starcie +echo "i2c-bcm2835" | sudo tee /etc/modules-load.d/i2c.conf >/dev/null +echo "i2c-dev" | sudo tee -a /etc/modules-load.d/i2c.conf >/dev/null From 494748eec821c998863d8c3b91bf0bd0611f42d8 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 15:35:24 +0100 Subject: [PATCH 24/63] update --- scripts/configure_oled.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/configure_oled.sh b/scripts/configure_oled.sh index 539186d..ebe8359 100644 --- a/scripts/configure_oled.sh +++ b/scripts/configure_oled.sh @@ -39,7 +39,7 @@ echo "[configure_oled] [OK] Wykryto OLED na adresie 0x$ADDR." # ================================ # 5. Zapis konfiguracji OLED # ================================ -mkdir -p /etc/streamer +sudo mkdir -p /etc/streamer echo "oled_address=0x$ADDR" | sudo tee /etc/streamer/oled.conf >/dev/null echo "[configure_oled] OLED gotowy." From f5b92bdc1e84823968d714e435f54e78d2e655c4 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 9 Feb 2026 15:49:56 +0100 Subject: [PATCH 25/63] update --- install.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index bb2856a..a5131a8 100755 --- a/install.sh +++ b/install.sh @@ -174,10 +174,10 @@ log "Pobieranie projektu..." bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/clone_repo.sh) log "Wykrywanie sprzętu..." -detect_dac -detect_oled -detect_bt -detect_wifi +detect_dac || true +detect_oled || true +detect_bt || true +detect_wifi || true log "Restart usług..." restart_service_if_exists "mpd.service" From b09c8b8ea5d6fe9a676e4d0abdf7434f88d45b10 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 08:12:45 +0100 Subject: [PATCH 26/63] update --- install.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/install.sh b/install.sh index a5131a8..aaa3925 100755 --- a/install.sh +++ b/install.sh @@ -179,6 +179,33 @@ detect_oled || true detect_bt || true detect_wifi || true +log "Instalacja usługi OLED..." + +# Tworzenie pliku usługi +sudo tee /etc/systemd/system/oled.service >/dev/null << 'EOF' +[Unit] +Description=OLED Display Service +After=network.target syslog.target dev-i2c-1.device + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /home/'"$USER"'/streamer/oled/oled.py +Restart=always +User='"$USER"' + +[Install] +WantedBy=multi-user.target +EOF + +# Przeładowanie systemd +sudo systemctl daemon-reload + +# Włączenie usługi +sudo systemctl enable oled.service + +log "[OK] Usługa OLED zainstalowana i uruchomiona." + + log "Restart usług..." restart_service_if_exists "mpd.service" restart_service_if_exists "bluealsa.service" From d5616927ee7609c885cb6d7012eacda00ea43c09 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 08:21:56 +0100 Subject: [PATCH 27/63] update --- install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index aaa3925..e29c279 100755 --- a/install.sh +++ b/install.sh @@ -181,6 +181,8 @@ detect_wifi || true log "Instalacja usługi OLED..." +USER_NAME=$(whoami) + # Tworzenie pliku usługi sudo tee /etc/systemd/system/oled.service >/dev/null << 'EOF' [Unit] @@ -189,9 +191,9 @@ After=network.target syslog.target dev-i2c-1.device [Service] Type=simple -ExecStart=/usr/bin/python3 /home/'"$USER"'/streamer/oled/oled.py +ExecStart=/usr/bin/python3 /home/'"$USER_NAME"'/streamer/oled/oled.py Restart=always -User='"$USER"' +User='"$USER_NAME"' [Install] WantedBy=multi-user.target From 317925a33c4ebfa4c05fa1107cef33c6e560e7a1 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 08:25:35 +0100 Subject: [PATCH 28/63] update --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index e29c279..36665e4 100755 --- a/install.sh +++ b/install.sh @@ -191,9 +191,9 @@ After=network.target syslog.target dev-i2c-1.device [Service] Type=simple -ExecStart=/usr/bin/python3 /home/'"$USER_NAME"'/streamer/oled/oled.py +ExecStart=/usr/bin/python3 /home/"$USER_NAME"/streamer/oled/oled.py Restart=always -User='"$USER_NAME"' +User="$USER_NAME" [Install] WantedBy=multi-user.target From 42aa61b87111a2505504476a66ccc80b3a973bc8 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 08:26:47 +0100 Subject: [PATCH 29/63] update --- install.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 36665e4..5a9f2c5 100755 --- a/install.sh +++ b/install.sh @@ -183,17 +183,16 @@ log "Instalacja usługi OLED..." USER_NAME=$(whoami) -# Tworzenie pliku usługi -sudo tee /etc/systemd/system/oled.service >/dev/null << 'EOF' +sudo tee /etc/systemd/system/oled.service >/dev/null << EOF [Unit] Description=OLED Display Service After=network.target syslog.target dev-i2c-1.device [Service] Type=simple -ExecStart=/usr/bin/python3 /home/"$USER_NAME"/streamer/oled/oled.py +ExecStart=/usr/bin/python3 /home/$USER_NAME/streamer/oled/oled.py Restart=always -User="$USER_NAME" +User=$USER_NAME [Install] WantedBy=multi-user.target From ab66b60952686dcfcbb551a78e8697dab975ae93 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 08:38:01 +0100 Subject: [PATCH 30/63] update --- install.sh | 25 +++++++++++++++++++++++++ oled/oled.py | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100755 oled/oled.py diff --git a/install.sh b/install.sh index 5a9f2c5..0c1549d 100755 --- a/install.sh +++ b/install.sh @@ -206,6 +206,31 @@ sudo systemctl enable oled.service log "[OK] Usługa OLED zainstalowana i uruchomiona." +OLED_SCRIPT="/home/$USER_NAME/streamer/oled/oled.py" + +if [ -f "$OLED_SCRIPT" ]; then + log "Instalacja usługi OLED..." + sudo tee /etc/systemd/system/oled.service >/dev/null << EOF +[Unit] +Description=OLED Display Service +After=network.target syslog.target dev-i2c-1.device + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /home/$USER_NAME/streamer/oled/oled.py +Restart=always +User=$USER_NAME + +[Install] +WantedBy=multi-user.target +EOF + + sudo systemctl daemon-reload + sudo systemctl enable --now oled.service + log "[OK] Usługa OLED zainstalowana i uruchomiona." +else + log "[-] Brak skryptu OLED ($OLED_SCRIPT) — pomijam instalację usługi OLED." +fi log "Restart usług..." restart_service_if_exists "mpd.service" diff --git a/oled/oled.py b/oled/oled.py new file mode 100755 index 0000000..c2b1f1d --- /dev/null +++ b/oled/oled.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# Minimalny testowy skrypt OLED — wyświetla "OK" +import time +try: + from luma.core.interface.serial import i2c + from luma.oled.device import ssd1306 + from PIL import Image, ImageDraw, ImageFont + serial = i2c(port=1, address=0x3c) + device = ssd1306(serial) + img = Image.new("1", device.size) + draw = ImageDraw.Draw(img) + font = ImageFont.load_default() + draw.text((0, 0), "OLED OK", font=font, fill=255) + device.display(img) + time.sleep(5) +except Exception as e: + # Jeśli biblioteki nie są dostępne, wypisz błąd i zakończ + print("OLED test failed:", e) + raise From 8e33bcf318d142e543c3e14f370f20f1c5831f5f Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:40:37 +0100 Subject: [PATCH 31/63] Update install.sh --- install.sh | 306 ++++++++++++++++++++++------------------------------- 1 file changed, 126 insertions(+), 180 deletions(-) diff --git a/install.sh b/install.sh index 0c1549d..b02fe8f 100755 --- a/install.sh +++ b/install.sh @@ -1,243 +1,189 @@ -#!/bin/bash -set -e - -############################################### -# Raspberry Pi Audio Streamer Installer -############################################### - -# --- Logger --- -ENABLE_LOGGER=1 -LOGFILE="/home/$USER/streamer_install.log" - +#!/usr/bin/env bash +# Robust installer for streamer (branch: Second) +# - single daemon-reload and grouped restarts +# - tolerant to missing hardware +# - auto-fix corrupt git by recloning +# - create oled.service only if oled.py exists +# - safe logging and non-fatal checks + +# ----------------------- +# Helpers +# ----------------------- log() { - if [ "$ENABLE_LOGGER" -eq 1 ]; then - echo "$(date '+%Y-%m-%d %H:%M:%S') | $1" | tee -a "$LOGFILE" - else - echo "$1" - fi + echo "$(date '+%Y-%m-%d %H:%M:%S') | $*" } -log "=== Uruchomiono instalator ===" - -############################################### -# Wybór trybu -############################################### - -echo "==============================================" -echo " Raspberry Pi Audio Streamer Installer" -echo "==============================================" -echo "Wybierz tryb:" -echo "1) Instalacja (pełna konfiguracja)" -echo "2) Aktualizacja (git pull + restart usług)" -read -p "Wybór [1/2]: " MODE - -log "Wybrany tryb: $MODE" - -############################################### -# Funkcje pomocnicze -############################################### - -service_exists() { - systemctl list-unit-files | grep -q "$1" +# Run a command but don't exit script on failure; log result +run_safe() { + if ! bash -c "$*"; then + log "(!) Command failed: $*" + return 1 + fi + return 0 } +# Restart or enable service only if unit file exists restart_service_if_exists() { - if service_exists "$1"; then - log "[OK] Restart: $1" - sudo systemctl restart "$1" || log "[!] Błąd restartu: $1" - else - log "[-] Usługa $1 nie istnieje — pomijam." - fi + local svc="$1" + if ! systemctl list-unit-files --type=service | awk '{print $1}' | grep -qx "$svc"; then + log "[-] $svc: brak jednostki, pomijam." + return 0 + fi + + # If active -> restart, else enable+start + if systemctl is-active --quiet "$svc"; then + log "[..] Restartuję $svc" + sudo systemctl restart "$svc" || log "(!) Nie udało się zrestartować $svc" + else + log "[..] Włączam i uruchamiam $svc" + sudo systemctl enable --now "$svc" || log "(!) Nie udało się włączyć/uruchomić $svc" + fi } -detect_dac() { - log "Wykrywanie sprzętu..." - - if aplay -l 2>/dev/null | grep -qi "rpi-dac"; then - log "[OK] Wykryto DAC I2S (rpi-dac)." - return 0 - fi - - if aplay -l 2>/dev/null | grep -qi "hifiberry"; then - log "[OK] Wykryto DAC Hifiberry / PCM5122." - return 0 - fi - - log "[!] BŁĄD: Nie wykryto żadnego DAC I2S!" - log " - Sprawdź połączenia I2S" - log " - Sprawdź overlay w /boot/firmware/config.txt" +# Detect functions should never abort installation +detect_safe() { + local name="$1"; shift + if ! "$@"; then + log "[-] Detekcja $name: brak lub błąd — pomijam." return 1 + else + log "[OK] Detekcja $name: OK." + return 0 + fi } -detect_oled() { - if [ ! -e /dev/i2c-1 ]; then - log "[-] I2C nieaktywne — OLED nie może być wykryty." - return 1 - fi - - if i2cdetect -y 1 | grep -q "3c"; then - log "[OK] Wykryto OLED (0x3C)." - return 0 - else - log "[-] OLED nie wykryty — pomijam konfigurację ekranu." - return 1 - fi -} - -detect_bt() { - if lsusb | grep -qi "Bluetooth"; then - log "[OK] Wykryto adapter Bluetooth USB." - return 0 +# Ensure git clone is healthy; if corrupt -> remove and reclone +ensure_git_clone() { + local repo_url="$1" + local branch="$2" + local dest="$3" + + if [ -d "$dest/.git" ]; then + log "Sprawdzam repozytorium w $dest..." + if ! (cd "$dest" && git fsck --full >/dev/null 2>&1); then + log "(!) Repozytorium uszkodzone. Usuwam i klonuję ponownie." + rm -rf "$dest" else - log "[-] Brak adaptera Bluetooth — pomijam BlueALSA." - return 1 + log "[OK] Repozytorium wygląda dobrze. Aktualizuję..." + if ! (cd "$dest" && git fetch --all --prune && git reset --hard "origin/$branch"); then + log "(!) Aktualizacja nie powiodła się. Usuwam i klonuję ponownie." + rm -rf "$dest" + fi fi -} + fi -detect_wifi() { - if iwconfig 2>/dev/null | grep -q "wlan0"; then - log "[OK] Wykryto Wi-Fi." - return 0 - else - log "[-] Brak Wi-Fi — radio internetowe może nie działać." - return 1 + if [ ! -d "$dest" ]; then + log "Klonuję repozytorium $repo_url (branch: $branch) do $dest..." + if ! git clone --depth 1 --branch "$branch" "$repo_url" "$dest"; then + log "(!) Klonowanie nie powiodło się." + return 1 fi + fi + return 0 } -############################################### -# TRYB AKTUALIZACJI -############################################### - -if [ "$MODE" = "2" ]; then - log "=== TRYB AKTUALIZACJI ===" - - if [ ! -d "/home/$USER/streamer" ]; then - log "[!] Błąd: katalog /home/$USER/streamer nie istnieje!" - exit 1 - fi - - cd /home/$USER/streamer - log "Pobieram zmiany z GitHub..." - git pull - - log "Wykrywanie sprzętu..." - detect_dac - detect_oled - - log "Restart usług..." - restart_service_if_exists "mpd.service" - restart_service_if_exists "camilladsp.service" - restart_service_if_exists "oled.service" - - log "=== Aktualizacja zakończona ===" - exit 0 -fi - -############################################### -# TRYB INSTALACJI -############################################### +# ----------------------- +# Start instalacji +# ----------------------- +log "=== Uruchomiono instalator ===" -log "=== TRYB INSTALACJI ===" +# Użytkownik wykonujący instalację (używamy literalnej nazwy w plikach systemd) +USER_NAME="$(whoami)" +HOME_DIR="$(eval echo ~"$USER_NAME")" +REPO_URL="https://github.com/xtreamx2/streamer" +BRANCH="Second" +DEST_DIR="$HOME_DIR/streamer" +# Opcjonalna aktualizacja systemu read -p "Czy zaktualizować system? [y/N]: " UPD if [[ "$UPD" =~ ^[Yy]$ ]]; then - log "Aktualizacja systemu..." - sudo apt update - sudo apt upgrade -y + log "Aktualizacja systemu..." + sudo apt update && sudo apt upgrade -y || log "(!) Aktualizacja systemu nie powiodła się." fi +# Instalacja pakietów (podskrypt) log "Instalacja pakietów..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_packages.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_packages.sh)" +# Konfiguracje (uruchamiamy podskrypty, ale nie pozwalamy im restartować globalnie) log "Konfiguracja audio (I2S)..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_audio.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_audio.sh)" log "Konfiguracja I2C..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_i2c.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_i2c.sh)" log "Konfiguracja OLED..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_oled.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_oled.sh)" log "Konfiguracja MPD..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_mpd.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_mpd.sh)" log "Konfiguracja Bluetooth..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_bt.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_bt.sh)" log "Konfiguracja CamillaDSP..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_camilladsp.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_camilladsp.sh)" log "Instalacja Python..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_python.sh) +run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_python.sh)" +# Pobierz/aktualizuj repo (bez przerywania instalacji) log "Pobieranie projektu..." -bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/clone_repo.sh) +if ! ensure_git_clone "$REPO_URL" "$BRANCH" "$DEST_DIR"; then + log "(!) Nie udało się pobrać repozytorium. Kontynuuję, ale niektóre funkcje mogą być niedostępne." +fi +# Wykrywanie sprzętu (detekcje nie przerywają instalacji) log "Wykrywanie sprzętu..." -detect_dac || true -detect_oled || true -detect_bt || true -detect_wifi || true - -log "Instalacja usługi OLED..." - -USER_NAME=$(whoami) - -sudo tee /etc/systemd/system/oled.service >/dev/null << EOF -[Unit] -Description=OLED Display Service -After=network.target syslog.target dev-i2c-1.device - -[Service] -Type=simple -ExecStart=/usr/bin/python3 /home/$USER_NAME/streamer/oled/oled.py -Restart=always -User=$USER_NAME - -[Install] -WantedBy=multi-user.target -EOF - -# Przeładowanie systemd -sudo systemctl daemon-reload - -# Włączenie usługi -sudo systemctl enable oled.service - -log "[OK] Usługa OLED zainstalowana i uruchomiona." - -OLED_SCRIPT="/home/$USER_NAME/streamer/oled/oled.py" - +detect_safe "DAC" bash -c 'lsmod | grep -qi snd_soc || true' +detect_safe "OLED" bash -c 'i2cdetect -y 1 >/dev/null 2>&1 || true' +detect_safe "Bluetooth" bash -c 'lsusb | grep -qi Bluetooth || true' +detect_safe "WiFi" bash -c 'ip link show wlan0 >/dev/null 2>&1 || true' + +# ----------------------- +# Instalacja usługi OLED (tylko jeśli skrypt istnieje w repo) +# ----------------------- +OLED_SCRIPT="$DEST_DIR/oled/oled.py" if [ -f "$OLED_SCRIPT" ]; then log "Instalacja usługi OLED..." - sudo tee /etc/systemd/system/oled.service >/dev/null << EOF + sudo tee /etc/systemd/system/oled.service >/dev/null < Date: Tue, 10 Feb 2026 08:46:29 +0100 Subject: [PATCH 32/63] OLED fix --- install.sh | 16 ++++++++++++++++ scripts/install_python.sh | 15 +++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index b02fe8f..cce0ffd 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,22 @@ run_safe() { return 0 } +echo "Wybierz tryb:" +echo "1) Instalacja (fresh)" +echo "2) Aktualizacja (update only)" +read -p "Wybierz [1/2]: " MODE + +if [[ "$MODE" == "1" ]]; then + # pełna instalacja: pakiety, konfiguracje, klon repo, usługi +elif [[ "$MODE" == "2" ]]; then + # tylko aktualizacja repo i restart usług (bez ponownej konfiguracji systemu) + ensure_git_clone "$REPO_URL" "$BRANCH" "$DEST_DIR" + sudo systemctl daemon-reload + for s in "${SERVICES_TO_CHECK[@]}"; do restart_service_if_exists "$s"; done + exit 0 +fi + + # Restart or enable service only if unit file exists restart_service_if_exists() { local svc="$1" diff --git a/scripts/install_python.sh b/scripts/install_python.sh index 12e86b9..a5ffc47 100644 --- a/scripts/install_python.sh +++ b/scripts/install_python.sh @@ -3,8 +3,19 @@ set -e echo "[install_python] Instaluję biblioteki Python..." -python3 -m pip install --break-system-packages \ +# Zależności systemowe potrzebne do Pillow i kompilacji niektórych pakietów +sudo apt install -y python3-pip python3-venv build-essential libjpeg-dev zlib1g-dev + +# Uaktualnij pip/setuptools wheel (globalnie) +sudo -H python3 -m pip install --upgrade pip setuptools wheel + +# Instalacja bibliotek wymaganych przez projekt +# --break-system-packages pozostawiamy jeśli system wymaga (Debian/Ubuntu pip policy) +sudo -H python3 -m pip install --break-system-packages \ RPi.GPIO \ smbus2 \ pillow \ - python-mpd2 + python-mpd2 \ + luma.oled + +echo "[install_python] Gotowe." From 3b00088c9dd1b07b71c854b94895fc7c5f85635f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 08:54:30 +0100 Subject: [PATCH 33/63] OLED fix --- install.sh | 263 +++++++++++++++++++++++++++-------------------------- 1 file changed, 136 insertions(+), 127 deletions(-) diff --git a/install.sh b/install.sh index cce0ffd..49c03d7 100755 --- a/install.sh +++ b/install.sh @@ -1,52 +1,35 @@ #!/usr/bin/env bash -# Robust installer for streamer (branch: Second) -# - single daemon-reload and grouped restarts -# - tolerant to missing hardware -# - auto-fix corrupt git by recloning -# - create oled.service only if oled.py exists -# - safe logging and non-fatal checks - -# ----------------------- -# Helpers -# ----------------------- + +# ============================================ +# streamer - installer / updater (branch Second) +# tryb 1: pełna instalacja +# tryb 2: aktualizacja (update only) +# ============================================ + +set -euo pipefail + +# ---------- helpers ---------- + log() { echo "$(date '+%Y-%m-%d %H:%M:%S') | $*" } -# Run a command but don't exit script on failure; log result run_safe() { if ! bash -c "$*"; then - log "(!) Command failed: $*" + log "(!) Command failed (kontynuuję): $*" return 1 fi return 0 } -echo "Wybierz tryb:" -echo "1) Instalacja (fresh)" -echo "2) Aktualizacja (update only)" -read -p "Wybierz [1/2]: " MODE - -if [[ "$MODE" == "1" ]]; then - # pełna instalacja: pakiety, konfiguracje, klon repo, usługi -elif [[ "$MODE" == "2" ]]; then - # tylko aktualizacja repo i restart usług (bez ponownej konfiguracji systemu) - ensure_git_clone "$REPO_URL" "$BRANCH" "$DEST_DIR" - sudo systemctl daemon-reload - for s in "${SERVICES_TO_CHECK[@]}"; do restart_service_if_exists "$s"; done - exit 0 -fi - - -# Restart or enable service only if unit file exists restart_service_if_exists() { local svc="$1" + if ! systemctl list-unit-files --type=service | awk '{print $1}' | grep -qx "$svc"; then log "[-] $svc: brak jednostki, pomijam." return 0 fi - # If active -> restart, else enable+start if systemctl is-active --quiet "$svc"; then log "[..] Restartuję $svc" sudo systemctl restart "$svc" || log "(!) Nie udało się zrestartować $svc" @@ -56,19 +39,6 @@ restart_service_if_exists() { fi } -# Detect functions should never abort installation -detect_safe() { - local name="$1"; shift - if ! "$@"; then - log "[-] Detekcja $name: brak lub błąd — pomijam." - return 1 - else - log "[OK] Detekcja $name: OK." - return 0 - fi -} - -# Ensure git clone is healthy; if corrupt -> remove and reclone ensure_git_clone() { local repo_url="$1" local branch="$2" @@ -90,78 +60,41 @@ ensure_git_clone() { if [ ! -d "$dest" ]; then log "Klonuję repozytorium $repo_url (branch: $branch) do $dest..." - if ! git clone --depth 1 --branch "$branch" "$repo_url" "$dest"; then - log "(!) Klonowanie nie powiodło się." - return 1 - fi + git clone --depth 1 --branch "$branch" "$repo_url" "$dest" fi - return 0 } -# ----------------------- -# Start instalacji -# ----------------------- -log "=== Uruchomiono instalator ===" - -# Użytkownik wykonujący instalację (używamy literalnej nazwy w plikach systemd) -USER_NAME="$(whoami)" -HOME_DIR="$(eval echo ~"$USER_NAME")" -REPO_URL="https://github.com/xtreamx2/streamer" -BRANCH="Second" -DEST_DIR="$HOME_DIR/streamer" - -# Opcjonalna aktualizacja systemu -read -p "Czy zaktualizować system? [y/N]: " UPD -if [[ "$UPD" =~ ^[Yy]$ ]]; then - log "Aktualizacja systemu..." - sudo apt update && sudo apt upgrade -y || log "(!) Aktualizacja systemu nie powiodła się." -fi +install_python_deps() { + log "[PY] Instaluję zależności Python..." -# Instalacja pakietów (podskrypt) -log "Instalacja pakietów..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_packages.sh)" + sudo apt update + sudo apt install -y python3-pip python3-venv build-essential libjpeg-dev zlib1g-dev -# Konfiguracje (uruchamiamy podskrypty, ale nie pozwalamy im restartować globalnie) -log "Konfiguracja audio (I2S)..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_audio.sh)" + sudo -H python3 -m pip install --upgrade pip setuptools wheel -log "Konfiguracja I2C..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_i2c.sh)" + sudo -H python3 -m pip install --break-system-packages \ + RPi.GPIO \ + smbus2 \ + pillow \ + python-mpd2 \ + luma.oled -log "Konfiguracja OLED..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_oled.sh)" - -log "Konfiguracja MPD..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_mpd.sh)" + log "[PY] Zależności Python gotowe." +} -log "Konfiguracja Bluetooth..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_bt.sh)" +install_oled_service() { + local user_name="$1" + local repo_dir="$2" -log "Konfiguracja CamillaDSP..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/configure_camilladsp.sh)" + local oled_script="$repo_dir/oled/oled.py" -log "Instalacja Python..." -run_safe "bash <(curl -s https://raw.githubusercontent.com/xtreamx2/streamer/Second/scripts/install_python.sh)" + if [ ! -f "$oled_script" ]; then + log "[-] Brak skryptu OLED ($oled_script) — pomijam instalację usługi OLED." + return 0 + fi -# Pobierz/aktualizuj repo (bez przerywania instalacji) -log "Pobieranie projektu..." -if ! ensure_git_clone "$REPO_URL" "$BRANCH" "$DEST_DIR"; then - log "(!) Nie udało się pobrać repozytorium. Kontynuuję, ale niektóre funkcje mogą być niedostępne." -fi + log "[OLED] Tworzę usługę systemd..." -# Wykrywanie sprzętu (detekcje nie przerywają instalacji) -log "Wykrywanie sprzętu..." -detect_safe "DAC" bash -c 'lsmod | grep -qi snd_soc || true' -detect_safe "OLED" bash -c 'i2cdetect -y 1 >/dev/null 2>&1 || true' -detect_safe "Bluetooth" bash -c 'lsusb | grep -qi Bluetooth || true' -detect_safe "WiFi" bash -c 'ip link show wlan0 >/dev/null 2>&1 || true' - -# ----------------------- -# Instalacja usługi OLED (tylko jeśli skrypt istnieje w repo) -# ----------------------- -OLED_SCRIPT="$DEST_DIR/oled/oled.py" -if [ -f "$OLED_SCRIPT" ]; then - log "Instalacja usługi OLED..." sudo tee /etc/systemd/system/oled.service >/dev/null < Date: Tue, 10 Feb 2026 08:59:38 +0100 Subject: [PATCH 34/63] Installer fix --- install.sh | 256 ++++++++++++++++++++------------------ scripts/install_python.sh | 9 +- 2 files changed, 137 insertions(+), 128 deletions(-) diff --git a/install.sh b/install.sh index 49c03d7..244fa53 100755 --- a/install.sh +++ b/install.sh @@ -1,101 +1,112 @@ #!/usr/bin/env bash -# ============================================ -# streamer - installer / updater (branch Second) -# tryb 1: pełna instalacja -# tryb 2: aktualizacja (update only) -# ============================================ +# streamer installer / updater (branch Second) set -euo pipefail -# ---------- helpers ---------- +LOG_FILE="/var/log/streamer_install.log" log() { - echo "$(date '+%Y-%m-%d %H:%M:%S') | $*" + local msg="$*" + echo "$(date '+%Y-%m-%d %H:%M:%S') | $msg" + echo "$(date '+%Y-%m-%d %H:%M:%S') | $msg" >> "$LOG_FILE" } run_safe() { - if ! bash -c "$*"; then - log "(!) Command failed (kontynuuję): $*" - return 1 - fi - return 0 + local cmd="$*" + log "[..] $cmd" + if ! bash -c "$cmd"; then + log "[!] Błąd: $cmd" + return 1 + fi + return 0 } restart_service_if_exists() { - local svc="$1" + local svc="$1" - if ! systemctl list-unit-files --type=service | awk '{print $1}' | grep -qx "$svc"; then - log "[-] $svc: brak jednostki, pomijam." - return 0 - fi - - if systemctl is-active --quiet "$svc"; then - log "[..] Restartuję $svc" - sudo systemctl restart "$svc" || log "(!) Nie udało się zrestartować $svc" - else - log "[..] Włączam i uruchamiam $svc" - sudo systemctl enable --now "$svc" || log "(!) Nie udało się włączyć/uruchomić $svc" - fi + if ! systemctl list-unit-files --type=service | awk '{print $1}' | grep -qx "$svc"; then + log "[-] $svc: brak jednostki, pomijam." + return 0 + fi + + if systemctl is-active --quiet "$svc"; then + log "[..] Restartuję $svc" + if ! sudo systemctl restart "$svc"; then + log "[!] Nie udało się zrestartować $svc" + else + log "[OK] Zrestartowano $svc" + fi + else + log "[..] Włączam i uruchamiam $svc" + if ! sudo systemctl enable --now "$svc"; then + log "[!] Nie udało się włączyć/uruchomić $svc" + else + log "[OK] $svc włączona i uruchomiona" + fi + fi } ensure_git_clone() { - local repo_url="$1" - local branch="$2" - local dest="$3" - - if [ -d "$dest/.git" ]; then - log "Sprawdzam repozytorium w $dest..." - if ! (cd "$dest" && git fsck --full >/dev/null 2>&1); then - log "(!) Repozytorium uszkodzone. Usuwam i klonuję ponownie." - rm -rf "$dest" - else - log "[OK] Repozytorium wygląda dobrze. Aktualizuję..." - if ! (cd "$dest" && git fetch --all --prune && git reset --hard "origin/$branch"); then - log "(!) Aktualizacja nie powiodła się. Usuwam i klonuję ponownie." - rm -rf "$dest" - fi + local repo_url="$1" + local branch="$2" + local dest="$3" + + if [ -d "$dest/.git" ]; then + log "[clone_repo] Sprawdzam repozytorium w $dest..." + if ! (cd "$dest" && git fsck --full >/dev/null 2>&1); then + log "[clone_repo] (!) Repozytorium uszkodzone. Usuwam i klonuję ponownie." + rm -rf "$dest" + else + log "[clone_repo] Repozytorium OK. Aktualizuję..." + if ! (cd "$dest" && git fetch --all --prune && git reset --hard "origin/$branch"); then + log "[clone_repo] (!) Aktualizacja nie powiodła się. Usuwam i klonuję ponownie." + rm -rf "$dest" + fi + fi fi - fi - if [ ! -d "$dest" ]; then - log "Klonuję repozytorium $repo_url (branch: $branch) do $dest..." - git clone --depth 1 --branch "$branch" "$repo_url" "$dest" - fi + if [ ! -d "$dest" ]; then + log "[clone_repo] Klonuję $repo_url (branch: $branch) do $dest..." + git clone --depth 1 --branch "$branch" "$repo_url" "$dest" + log "[clone_repo] [OK] Repozytorium pobrane." + else + log "[clone_repo] [OK] Repozytorium już istnieje i zostało zaktualizowane." + fi } install_python_deps() { - log "[PY] Instaluję zależności Python..." + log "[install_python] Instaluję biblioteki Python..." - sudo apt update - sudo apt install -y python3-pip python3-venv build-essential libjpeg-dev zlib1g-dev + sudo apt update + sudo apt install -y python3-pip python3-venv build-essential libjpeg-dev zlib1g-dev - sudo -H python3 -m pip install --upgrade pip setuptools wheel + sudo -H python3 -m pip install --break-system-packages --upgrade pip setuptools wheel - sudo -H python3 -m pip install --break-system-packages \ - RPi.GPIO \ - smbus2 \ - pillow \ - python-mpd2 \ - luma.oled + sudo -H python3 -m pip install --break-system-packages \ + RPi.GPIO \ + smbus2 \ + pillow \ + python-mpd2 \ + luma.oled - log "[PY] Zależności Python gotowe." + log "[install_python] [OK] Biblioteki Python zainstalowane." } install_oled_service() { - local user_name="$1" - local repo_dir="$2" + local user_name="$1" + local repo_dir="$2" - local oled_script="$repo_dir/oled/oled.py" + local oled_script="$repo_dir/oled/oled.py" - if [ ! -f "$oled_script" ]; then - log "[-] Brak skryptu OLED ($oled_script) — pomijam instalację usługi OLED." - return 0 - fi + if [ ! -f "$oled_script" ]; then + log "[oled] [-] Brak skryptu OLED ($oled_script) — pomijam instalację usługi." + return 0 + fi - log "[OLED] Tworzę usługę systemd..." + log "[oled] Tworzę usługę systemd..." - sudo tee /etc/systemd/system/oled.service >/dev/null </dev/null < Date: Tue, 10 Feb 2026 09:05:13 +0100 Subject: [PATCH 35/63] Installer fix --- install.sh | 126 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/install.sh b/install.sh index 244fa53..0bb26f7 100755 --- a/install.sh +++ b/install.sh @@ -4,19 +4,50 @@ set -euo pipefail -LOG_FILE="/var/log/streamer_install.log" +# ---------- konfiguracja logów ---------- + +DEBUG_LOG=1 # 1 = zapisuj debug do install.log, 0 = tylko streamer_install.log + +LOG_FILE="$HOME/streamer_install.log" +DEBUG_FILE="$HOME/install.log" + +touch "$LOG_FILE" +if [[ "$DEBUG_LOG" == "1" ]]; then + touch "$DEBUG_FILE" +fi + +# ---------- kolory ---------- + +GREEN="\e[32m" +RED="\e[31m" +YELLOW="\e[33m" +BLUE="\e[34m" +RESET="\e[0m" log() { - local msg="$*" - echo "$(date '+%Y-%m-%d %H:%M:%S') | $msg" - echo "$(date '+%Y-%m-%d %H:%M:%S') | $msg" >> "$LOG_FILE" + local msg="$1" + local type="${2:-INFO}" + + case "$type" in + OK) prefix="${GREEN}[OK]${RESET}" ;; + ERR) prefix="${RED}[ERROR]${RESET}" ;; + WARN) prefix="${YELLOW}[-]${RESET}" ;; + *) prefix="${BLUE}[..]${RESET}" ;; + esac + + echo -e "$prefix $msg" + echo "$(date '+%Y-%m-%d %H:%M:%S') | $prefix $msg" >> "$LOG_FILE" + + if [[ "$DEBUG_LOG" == "1" ]]; then + echo "$(date '+%Y-%m-%d %H:%M:%S') | $msg" >> "$DEBUG_FILE" + fi } run_safe() { local cmd="$*" - log "[..] $cmd" + log "$cmd" "INFO" if ! bash -c "$cmd"; then - log "[!] Błąd: $cmd" + log "Błąd: $cmd" "ERR" return 1 fi return 0 @@ -26,23 +57,23 @@ restart_service_if_exists() { local svc="$1" if ! systemctl list-unit-files --type=service | awk '{print $1}' | grep -qx "$svc"; then - log "[-] $svc: brak jednostki, pomijam." + log "[$svc] brak jednostki, pomijam." "WARN" return 0 fi if systemctl is-active --quiet "$svc"; then - log "[..] Restartuję $svc" + log "[$svc] restartuję..." "INFO" if ! sudo systemctl restart "$svc"; then - log "[!] Nie udało się zrestartować $svc" + log "[$svc] nie udało się zrestartować" "ERR" else - log "[OK] Zrestartowano $svc" + log "[$svc] zrestartowano" "OK" fi else - log "[..] Włączam i uruchamiam $svc" + log "[$svc] włączam i uruchamiam..." "INFO" if ! sudo systemctl enable --now "$svc"; then - log "[!] Nie udało się włączyć/uruchomić $svc" + log "[$svc] nie udało się włączyć/uruchomić" "ERR" else - log "[OK] $svc włączona i uruchomiona" + log "[$svc] włączona i uruchomiona" "OK" fi fi } @@ -53,30 +84,30 @@ ensure_git_clone() { local dest="$3" if [ -d "$dest/.git" ]; then - log "[clone_repo] Sprawdzam repozytorium w $dest..." + log "[clone_repo] sprawdzam repozytorium w $dest..." "INFO" if ! (cd "$dest" && git fsck --full >/dev/null 2>&1); then - log "[clone_repo] (!) Repozytorium uszkodzone. Usuwam i klonuję ponownie." + log "[clone_repo] repozytorium uszkodzone, usuwam i klonuję ponownie" "WARN" rm -rf "$dest" else - log "[clone_repo] Repozytorium OK. Aktualizuję..." + log "[clone_repo] repozytorium OK, aktualizuję..." "INFO" if ! (cd "$dest" && git fetch --all --prune && git reset --hard "origin/$branch"); then - log "[clone_repo] (!) Aktualizacja nie powiodła się. Usuwam i klonuję ponownie." + log "[clone_repo] aktualizacja nie powiodła się, usuwam i klonuję ponownie" "WARN" rm -rf "$dest" fi fi fi if [ ! -d "$dest" ]; then - log "[clone_repo] Klonuję $repo_url (branch: $branch) do $dest..." + log "[clone_repo] klonuję $repo_url (branch: $branch) do $dest..." "INFO" git clone --depth 1 --branch "$branch" "$repo_url" "$dest" - log "[clone_repo] [OK] Repozytorium pobrane." + log "[clone_repo] repozytorium pobrane" "OK" else - log "[clone_repo] [OK] Repozytorium już istnieje i zostało zaktualizowane." + log "[clone_repo] repozytorium istnieje i zostało zaktualizowane" "OK" fi } install_python_deps() { - log "[install_python] Instaluję biblioteki Python..." + log "[install_python] instaluję biblioteki Python..." "INFO" sudo apt update sudo apt install -y python3-pip python3-venv build-essential libjpeg-dev zlib1g-dev @@ -90,7 +121,7 @@ install_python_deps() { python-mpd2 \ luma.oled - log "[install_python] [OK] Biblioteki Python zainstalowane." + log "[install_python] biblioteki Python zainstalowane" "OK" } install_oled_service() { @@ -100,11 +131,11 @@ install_oled_service() { local oled_script="$repo_dir/oled/oled.py" if [ ! -f "$oled_script" ]; then - log "[oled] [-] Brak skryptu OLED ($oled_script) — pomijam instalację usługi." + log "[oled] brak skryptu ($oled_script) — pomijam usługę" "WARN" return 0 fi - log "[oled] Tworzę usługę systemd..." + log "[oled] tworzę usługę systemd..." "INFO" sudo tee /etc/systemd/system/oled.service >/dev/null < Date: Tue, 10 Feb 2026 09:46:34 +0100 Subject: [PATCH 36/63] Installer fix --- config/config-dsp.json | 5 + config/config-oled.json | 7 + config/config-radio.json | 16 ++ config/config-system.json | 5 + oled/encoder.py | 122 ++++++++++++ oled/oled.py | 409 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 547 insertions(+), 17 deletions(-) create mode 100644 config/config-dsp.json create mode 100644 config/config-oled.json create mode 100644 config/config-radio.json create mode 100644 config/config-system.json create mode 100644 oled/encoder.py diff --git a/config/config-dsp.json b/config/config-dsp.json new file mode 100644 index 0000000..b4bf52f --- /dev/null +++ b/config/config-dsp.json @@ -0,0 +1,5 @@ +{ + "eq_5band": [0, 0, 0, 0, 0], + "eq_2band": [0, 0], + "active_eq": "5band" +} diff --git a/config/config-oled.json b/config/config-oled.json new file mode 100644 index 0000000..e7c3165 --- /dev/null +++ b/config/config-oled.json @@ -0,0 +1,7 @@ +{ + "eq_mode": "5band", + "screensaver_dim_after": 20, + "screensaver_dim_level": 10, + "screensaver_off_after": 60, + "brightness_default": 255 +} diff --git a/config/config-radio.json b/config/config-radio.json new file mode 100644 index 0000000..9a8d8ba --- /dev/null +++ b/config/config-radio.json @@ -0,0 +1,16 @@ +{ + "stations": [ + { + "name": "Radio Paradise (FLAC)", + "url": "http://stream.radioparadise.com/flac", + "favorite": true, + "tags": ["hires", "flac"] + }, + { + "name": "Example 320k MP3", + "url": "http://example.com/stream.mp3", + "favorite": false, + "tags": ["mp3", "320k"] + } + ] +} diff --git a/config/config-system.json b/config/config-system.json new file mode 100644 index 0000000..fd6e877 --- /dev/null +++ b/config/config-system.json @@ -0,0 +1,5 @@ +{ + "hostname": "streamer v0.010a", + "i2s_card": "hifiberry-dac", + "bluetooth_enabled": false +} diff --git a/oled/encoder.py b/oled/encoder.py new file mode 100644 index 0000000..e27b7ef --- /dev/null +++ b/oled/encoder.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +import time +import threading +import RPi.GPIO as GPIO + + +# ========================== +# KONFIGURACJA PINÓW +# ========================== + +PIN_A = 17 # CLK +PIN_B = 27 # DT +PIN_SW = 22 # SW (przycisk) + +DEBOUNCE_ROTATE = 0.002 # 2 ms +DEBOUNCE_CLICK = 0.05 # 50 ms +HOLD_TIME = 1.2 # przytrzymanie 1.2 sekundy + + +# ========================== +# KLASA ENKODERA +# ========================== + +class Encoder: + def __init__(self, on_rotate=None, on_click=None, on_hold=None): + """ + on_rotate(direction) → direction = +1 / -1 + on_click() → klik + on_hold() → przytrzymanie + """ + + self.on_rotate = on_rotate + self.on_click = on_click + self.on_hold = on_hold + + self.last_state_A = 1 + self.last_button_state = 1 + self.button_down_time = None + self.hold_fired = False + + self.running = True + + GPIO.setmode(GPIO.BCM) + GPIO.setup(PIN_A, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(PIN_B, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(PIN_SW, GPIO.IN, pull_up_down=GPIO.PUD_UP) + + # wątek do obsługi enkodera + self.thread = threading.Thread(target=self._loop, daemon=True) + self.thread.start() + + # ========================== + # PĘTLA GŁÓWNA ENKODERA + # ========================== + + def _loop(self): + while self.running: + self._check_rotation() + self._check_button() + time.sleep(0.001) # 1 ms + + # ========================== + # ROTACJA + # ========================== + + def _check_rotation(self): + state_A = GPIO.input(PIN_A) + if state_A != self.last_state_A: + # zmiana stanu = impuls + time.sleep(DEBOUNCE_ROTATE) + + state_B = GPIO.input(PIN_B) + + if state_A == 0: # impuls w dół + if state_B == 1: + direction = +1 + else: + direction = -1 + + if self.on_rotate: + self.on_rotate(direction) + + self.last_state_A = state_A + + # ========================== + # PRZYCISK + # ========================== + + def _check_button(self): + state = GPIO.input(PIN_SW) + + # przycisk wciśnięty + if state == 0 and self.last_button_state == 1: + self.button_down_time = time.time() + self.hold_fired = False + time.sleep(DEBOUNCE_CLICK) + + # przycisk trzymany + if state == 0 and self.button_down_time: + if not self.hold_fired and (time.time() - self.button_down_time) >= HOLD_TIME: + self.hold_fired = True + if self.on_hold: + self.on_hold() + + # przycisk puszczony + if state == 1 and self.last_button_state == 0: + if not self.hold_fired: + if self.on_click: + self.on_click() + self.button_down_time = None + time.sleep(DEBOUNCE_CLICK) + + self.last_button_state = state + + # ========================== + # ZATRZYMANIE + # ========================== + + def stop(self): + self.running = False + time.sleep(0.05) + GPIO.cleanup() diff --git a/oled/oled.py b/oled/oled.py index c2b1f1d..d26149c 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -1,19 +1,394 @@ #!/usr/bin/env python3 -# Minimalny testowy skrypt OLED — wyświetla "OK" import time -try: - from luma.core.interface.serial import i2c - from luma.oled.device import ssd1306 - from PIL import Image, ImageDraw, ImageFont - serial = i2c(port=1, address=0x3c) - device = ssd1306(serial) - img = Image.new("1", device.size) - draw = ImageDraw.Draw(img) - font = ImageFont.load_default() - draw.text((0, 0), "OLED OK", font=font, fill=255) - device.display(img) - time.sleep(5) -except Exception as e: - # Jeśli biblioteki nie są dostępne, wypisz błąd i zakończ - print("OLED test failed:", e) - raise +import json +import signal +import sys +from dataclasses import dataclass, field +from pathlib import Path + +from luma.core.interface.serial import i2c +from luma.core.render import canvas +from luma.oled.device import ssd1306 + +from encoder import Encoder # integracja z enkoderem + + +# ================== ŚCIEŻKI ================== + +BASE_DIR = Path(__file__).resolve().parent +CONFIG_DIR = BASE_DIR / "config" +CONFIG_DIR.mkdir(exist_ok=True) + +CONFIG_OLED = CONFIG_DIR / "config-oled.json" +CONFIG_RADIO = CONFIG_DIR / "config-radio.json" + + +# ================== KONFIG DOMYŚLNY ================== + +DEFAULT_OLED_CONFIG = { + "eq_mode": "5band", + "screensaver_dim_after": 20, + "screensaver_dim_level": 10, + "screensaver_off_after": 60, + "brightness_default": 255 +} + +DEFAULT_RADIO_CONFIG = { + "stations": [ + { + "name": "Radio Paradise (FLAC)", + "url": "http://stream.radioparadise.com/flac", + "favorite": True, + "tags": ["hires", "flac"] + } + ] +} + + +# ================== DANE / STANY ================== + +@dataclass +class NowPlaying: + source: str = "radio" # "radio", "file", "bt" + artist: str = "Artist" + title: str = "Title" + bitrate_kbps: int = 320 + bit_depth: int = 24 + sample_rate: int = 96000 + volume: int = 42 + playing: bool = True + + +@dataclass +class ScreenState: + mode: str = "main" + menu_path: list = field(default_factory=list) + last_input_time: float = field(default_factory=time.time) + scroll_offset_line1: int = 0 + scroll_offset_line2: int = 0 + last_scroll_time: float = field(default_factory=time.time) + + selected_index: int = 0 + scroll_offset: int = 0 + + +@dataclass +class Settings: + eq_mode: str = "5band" + screensaver_dim_after: int = 20 + screensaver_dim_level: int = 10 + screensaver_off_after: int = 60 + brightness_default: int = 255 + + +# ================== PARAMETRY OLED ================== + +I2C_PORT = 1 +I2C_ADDRESS = 0x3C + +FPS = 20 +SCROLL_SPEED = 0.3 +MENU_TIMEOUT = 10 + + +# ================== KONFIG / RADIO ================== + +def load_json(path: Path, default: dict) -> dict: + if not path.exists(): + path.write_text(json.dumps(default, indent=2), encoding="utf-8") + return default.copy() + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception: + return default.copy() + + +def save_json(path: Path, data: dict): + path.write_text(json.dumps(data, indent=2), encoding="utf-8") + + +def load_oled_config() -> Settings: + cfg = load_json(CONFIG_OLED, DEFAULT_OLED_CONFIG) + return Settings( + eq_mode=cfg.get("eq_mode", "5band"), + screensaver_dim_after=int(cfg.get("screensaver_dim_after", 20)), + screensaver_dim_level=int(cfg.get("screensaver_dim_level", 10)), + screensaver_off_after=int(cfg.get("screensaver_off_after", 60)), + brightness_default=int(cfg.get("brightness_default", 255)), + ) + + +def load_radio_stations(): + data = load_json(CONFIG_RADIO, DEFAULT_RADIO_CONFIG) + return data.get("stations", []) + + +# ================== OLED INIT ================== + +def init_device(): + serial = i2c(port=I2C_PORT, address=I2C_ADDRESS) + device = ssd1306(serial, rotate=0) + return device + + +# ================== RYSOWANIE ================== + +def draw_startup_animation(device): + w, h = device.width, device.height + steps = 10 + + logo_lines = [ + " _____ _ ", + " / ____| | ", + "| (___ | |_ _ __ ___ _ __ ___ _ _ ", + " \\___ \\| __| '__/ _ \\| '_ ` _ \\| | | |", + " ____) | |_| | | (_) | | | | | | |_| |", + "|_____/ \\__|_| \\___/|_| |_| |_|\\__,_|", + " STREAMER ", + ] + + for i in range(steps): + with canvas(device) as draw: + draw.rectangle((0, 0, w, h), outline=0, fill=0) + max_line = int(len(logo_lines) * (i + 1) / steps) + y = 0 + for line in logo_lines[:max_line]: + draw.text((0, y), line[:21], fill=255) + y += 8 + time.sleep(0.1) + + +def get_source_icon(np: NowPlaying) -> str: + if np.source in ("radio", "file", "bt"): + return "▶" if np.playing else "⏸" + return "▶" if np.playing else "⏸" + + +def is_hires(np: NowPlaying) -> bool: + return np.bit_depth >= 24 or np.sample_rate > 48000 + + +def format_bitrate(np: NowPlaying) -> str: + return f"{np.bitrate_kbps}k" + + +def format_bitdepth(np: NowPlaying) -> str: + sr_khz = int(np.sample_rate / 1000) + return f"{np.bit_depth}/{sr_khz}" + + +def draw_volume_bar(draw, x, y, width, height, volume): + segments = 12 + seg_width = width // segments + filled = int(segments * volume / 100) + + for i in range(segments): + x0 = x + i * seg_width + x1 = x0 + seg_width - 1 + if i < filled: + draw.rectangle((x0, y, x1, y + height), outline=255, fill=255) + else: + draw.rectangle((x0, y, x1, y + height), outline=255, fill=0) + + +def scroll_text(text: str, width_chars: int, offset: int) -> str: + if len(text) <= width_chars: + return text.ljust(width_chars) + padded = text + " " + start = offset % len(padded) + view = (padded + padded)[start:start + width_chars] + return view + + +def draw_main_screen(device, np: NowPlaying, state: ScreenState): + w, h = device.width, device.height + chars_per_line = 16 + + icon = get_source_icon(np) + hires = is_hires(np) + + if np.source == "radio" and np.artist: + line1_text = np.artist + line2_text = np.title + else: + line1_text = np.title + line2_text = f"{format_bitrate(np)} {format_bitdepth(np)}" + + now = time.time() + if now - state.last_scroll_time > SCROLL_SPEED: + state.scroll_offset_line1 += 1 + state.scroll_offset_line2 += 1 + state.last_scroll_time = now + + line1_scrolled = scroll_text(line1_text, chars_per_line - 2, state.scroll_offset_line1) + line2_scrolled = scroll_text(line2_text, chars_per_line, state.scroll_offset_line2) + + with canvas(device) as draw: + draw.text((0, 0), icon, fill=255) + draw.text((12, 0), line1_scrolled, fill=255) + + draw.text((0, 10), line2_scrolled, fill=255) + if hires: + draw.text((w - 18, 10), "HR", fill=255) + + draw_volume_bar(draw, 0, h - 8, w, 6, np.volume) + + +# ================== MENU (SZKIELET) ================== + +MENU_STRUCTURE = { + "root": ["Ustawienia", "Ulubione stacje"], + "Ustawienia": ["Filtry EQ", "Wygaszacz", "Ekran", "ESC"], + "Filtry EQ": ["EQ 5-pasmowy", "EQ 2-pasmowy", "ESC"], + "Wygaszacz": ["Czas do przyciemnienia", "Jasność po przyciemnieniu", "Czas do wygaszenia", "ESC"], + "Ekran": ["Jasność domyślna", "ESC"], + "Ulubione stacje": ["(lista z config-radio)", "ESC"], +} + + +def current_menu_items(state: ScreenState): + if not state.menu_path: + return MENU_STRUCTURE["root"] + key = state.menu_path[-1] + return MENU_STRUCTURE.get(key, ["ESC"]) + + +def draw_menu(device, state: ScreenState): + items = current_menu_items(state) + title = state.menu_path[-1] if state.menu_path else "MENU" + + visible_lines = 2 # ile pozycji pokazujemy naraz + + # koryguj scroll + if state.selected_index < state.scroll_offset: + state.scroll_offset = state.selected_index + elif state.selected_index >= state.scroll_offset + visible_lines: + state.scroll_offset = state.selected_index - visible_lines + 1 + + with canvas(device) as draw: + draw.text((0, 0), title[:16], fill=255) + + for i in range(visible_lines): + item_index = state.scroll_offset + i + if item_index >= len(items): + break + + prefix = "> " if item_index == state.selected_index else " " + draw.text((0, 10 + i * 10), prefix + items[item_index][:14], fill=255) + +# ================== ENKODER – CALLBACKI ================== + +def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState): + state.last_input_time = time.time() + + if state.mode == "main": + np.volume = max(0, min(100, np.volume + direction)) + elif state.mode == "menu": + items = current_menu_items(state) + state.selected_index = max(0, min(len(items) - 1, state.selected_index + direction)) + + +def on_encoder_click(np: NowPlaying, state: ScreenState): + state.last_input_time = time.time() + + if state.mode == "main": + np.playing = not np.playing + elif state.mode == "menu": + items = current_menu_items(state) + choice = items[state.selected_index] + + if choice == "ESC": + if state.menu_path: + state.menu_path.pop() + else: + state.mode = "main" + state.selected_index = 0 + state.scroll_offset = 0 + return + + # jeśli to podmenu + if choice in MENU_STRUCTURE: + state.menu_path.append(choice) + state.selected_index = 0 + state.scroll_offset = 0 + return + + # jeśli to opcja końcowa (np. EQ 5-pasmowy) + # tu będzie logika ustawień + print("Wybrano:", choice) + + +def on_encoder_hold(np: NowPlaying, state: ScreenState): + state.last_input_time = time.time() + + if state.mode == "main": + state.mode = "menu" + state.menu_path = [] + elif state.mode == "menu": + if state.menu_path: + state.menu_path.pop() + else: + state.mode = "main" + + +# ================== PĘTLA GŁÓWNA ================== + +running = True + + +def signal_handler(sig, frame): + global running + running = False + + +def main(): + global running + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + device = init_device() + settings = load_oled_config() + stations = load_radio_stations() + + np = NowPlaying() + state = ScreenState() + + draw_startup_animation(device) + + def rotate_cb(direction): + on_encoder_rotate(direction, np, state) + + def click_cb(): + on_encoder_click(np, state) + + def hold_cb(): + on_encoder_hold(np, state) + + enc = Encoder( + on_rotate=rotate_cb, + on_click=click_cb, + on_hold=hold_cb + ) + + while running: + now = time.time() + + if state.mode == "menu" and (now - state.last_input_time) > MENU_TIMEOUT: + state.mode = "main" + + # TODO: aktualizacja np z MPD / playera / stacji + + if state.mode == "main": + draw_main_screen(device, np, state) + elif state.mode == "menu": + draw_menu(device, state) + + time.sleep(1.0 / FPS) + + enc.stop() + sys.exit(0) + + +if __name__ == "__main__": + main() From c22a271b07200466b373135f53e29a27d5c226b7 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 10:11:35 +0100 Subject: [PATCH 37/63] encoder fix --- oled/encoder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oled/encoder.py b/oled/encoder.py index e27b7ef..5e8d13a 100644 --- a/oled/encoder.py +++ b/oled/encoder.py @@ -8,9 +8,9 @@ # KONFIGURACJA PINÓW # ========================== -PIN_A = 17 # CLK -PIN_B = 27 # DT -PIN_SW = 22 # SW (przycisk) +PIN_A = 24 # CLK +PIN_B = 23 # DT +PIN_SW = 13 # SW (przycisk) DEBOUNCE_ROTATE = 0.002 # 2 ms DEBOUNCE_CLICK = 0.05 # 50 ms From d53ac921061dfcf231700d06ad9265d3249685e6 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 10:20:07 +0100 Subject: [PATCH 38/63] encoder fix --- oled/oled.py | 137 ++++++++++++++++++++++++++------------------------- 1 file changed, 70 insertions(+), 67 deletions(-) diff --git a/oled/oled.py b/oled/oled.py index d26149c..1b133db 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -10,7 +10,7 @@ from luma.core.render import canvas from luma.oled.device import ssd1306 -from encoder import Encoder # integracja z enkoderem +from encoder import Encoder # ================== ŚCIEŻKI ================== @@ -49,7 +49,7 @@ @dataclass class NowPlaying: - source: str = "radio" # "radio", "file", "bt" + source: str = "radio" artist: str = "Artist" title: str = "Title" bitrate_kbps: int = 320 @@ -64,6 +64,7 @@ class ScreenState: mode: str = "main" menu_path: list = field(default_factory=list) last_input_time: float = field(default_factory=time.time) + scroll_offset_line1: int = 0 scroll_offset_line2: int = 0 last_scroll_time: float = field(default_factory=time.time) @@ -103,10 +104,6 @@ def load_json(path: Path, default: dict) -> dict: return default.copy() -def save_json(path: Path, data: dict): - path.write_text(json.dumps(data, indent=2), encoding="utf-8") - - def load_oled_config() -> Settings: cfg = load_json(CONFIG_OLED, DEFAULT_OLED_CONFIG) return Settings( @@ -123,6 +120,11 @@ def load_radio_stations(): return data.get("stations", []) +def get_favorite_stations(): + stations = load_radio_stations() + return [s["name"] for s in stations if s.get("favorite")] + + # ================== OLED INIT ================== def init_device(): @@ -159,8 +161,6 @@ def draw_startup_animation(device): def get_source_icon(np: NowPlaying) -> str: - if np.source in ("radio", "file", "bt"): - return "▶" if np.playing else "⏸" return "▶" if np.playing else "⏸" @@ -196,8 +196,7 @@ def scroll_text(text: str, width_chars: int, offset: int) -> str: return text.ljust(width_chars) padded = text + " " start = offset % len(padded) - view = (padded + padded)[start:start + width_chars] - return view + return (padded + padded)[start:start + width_chars] def draw_main_screen(device, np: NowPlaying, state: ScreenState): @@ -220,69 +219,69 @@ def draw_main_screen(device, np: NowPlaying, state: ScreenState): state.scroll_offset_line2 += 1 state.last_scroll_time = now - line1_scrolled = scroll_text(line1_text, chars_per_line - 2, state.scroll_offset_line1) - line2_scrolled = scroll_text(line2_text, chars_per_line, state.scroll_offset_line2) + line1 = scroll_text(line1_text, chars_per_line - 2, state.scroll_offset_line1) + line2 = scroll_text(line2_text, chars_per_line, state.scroll_offset_line2) with canvas(device) as draw: draw.text((0, 0), icon, fill=255) - draw.text((12, 0), line1_scrolled, fill=255) + draw.text((12, 0), line1, fill=255) - draw.text((0, 10), line2_scrolled, fill=255) + draw.text((0, 10), line2, fill=255) if hires: draw.text((w - 18, 10), "HR", fill=255) - draw_volume_bar(draw, 0, h - 8, w, 6, np.volume) + draw_volume_bar(draw, 0, h - 8, w - 30, 6, np.volume) + draw.text((w - 28, h - 10), f"{np.volume}%", fill=255) -# ================== MENU (SZKIELET) ================== +# ================== MENU ================== MENU_STRUCTURE = { - "root": ["Ustawienia", "Ulubione stacje"], + "root": ["Ustawienia", "Ulubione stacje", "ESC"], "Ustawienia": ["Filtry EQ", "Wygaszacz", "Ekran", "ESC"], "Filtry EQ": ["EQ 5-pasmowy", "EQ 2-pasmowy", "ESC"], "Wygaszacz": ["Czas do przyciemnienia", "Jasność po przyciemnieniu", "Czas do wygaszenia", "ESC"], "Ekran": ["Jasność domyślna", "ESC"], - "Ulubione stacje": ["(lista z config-radio)", "ESC"], + "Ulubione stacje": get_favorite_stations() + ["ESC"], } def current_menu_items(state: ScreenState): if not state.menu_path: return MENU_STRUCTURE["root"] - key = state.menu_path[-1] - return MENU_STRUCTURE.get(key, ["ESC"]) + return MENU_STRUCTURE.get(state.menu_path[-1], ["ESC"]) def draw_menu(device, state: ScreenState): items = current_menu_items(state) title = state.menu_path[-1] if state.menu_path else "MENU" - visible_lines = 2 # ile pozycji pokazujemy naraz + visible = 2 - # koryguj scroll if state.selected_index < state.scroll_offset: state.scroll_offset = state.selected_index - elif state.selected_index >= state.scroll_offset + visible_lines: - state.scroll_offset = state.selected_index - visible_lines + 1 + elif state.selected_index >= state.scroll_offset + visible: + state.scroll_offset = state.selected_index - visible + 1 with canvas(device) as draw: draw.text((0, 0), title[:16], fill=255) - for i in range(visible_lines): - item_index = state.scroll_offset + i - if item_index >= len(items): + for i in range(visible): + idx = state.scroll_offset + i + if idx >= len(items): break + prefix = "> " if idx == state.selected_index else " " + draw.text((0, 10 + i * 10), prefix + items[idx][:14], fill=255) - prefix = "> " if item_index == state.selected_index else " " - draw.text((0, 10 + i * 10), prefix + items[item_index][:14], fill=255) -# ================== ENKODER – CALLBACKI ================== +# ================== ENKODER ================== def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState): state.last_input_time = time.time() if state.mode == "main": np.volume = max(0, min(100, np.volume + direction)) + elif state.mode == "menu": items = current_menu_items(state) state.selected_index = max(0, min(len(items) - 1, state.selected_index + direction)) @@ -293,29 +292,27 @@ def on_encoder_click(np: NowPlaying, state: ScreenState): if state.mode == "main": np.playing = not np.playing - elif state.mode == "menu": - items = current_menu_items(state) - choice = items[state.selected_index] + return - if choice == "ESC": - if state.menu_path: - state.menu_path.pop() - else: - state.mode = "main" - state.selected_index = 0 - state.scroll_offset = 0 - return + items = current_menu_items(state) + choice = items[state.selected_index] - # jeśli to podmenu - if choice in MENU_STRUCTURE: - state.menu_path.append(choice) - state.selected_index = 0 - state.scroll_offset = 0 - return + if choice == "ESC": + if state.menu_path: + state.menu_path.pop() + else: + state.mode = "main" + state.selected_index = 0 + state.scroll_offset = 0 + return + + if choice in MENU_STRUCTURE: + state.menu_path.append(choice) + state.selected_index = 0 + state.scroll_offset = 0 + return - # jeśli to opcja końcowa (np. EQ 5-pasmowy) - # tu będzie logika ustawień - print("Wybrano:", choice) + print("Wybrano:", choice) def on_encoder_hold(np: NowPlaying, state: ScreenState): @@ -324,11 +321,16 @@ def on_encoder_hold(np: NowPlaying, state: ScreenState): if state.mode == "main": state.mode = "menu" state.menu_path = [] + state.selected_index = 0 + state.scroll_offset = 0 + elif state.mode == "menu": if state.menu_path: state.menu_path.pop() else: state.mode = "main" + state.selected_index = 0 + state.scroll_offset = 0 # ================== PĘTLA GŁÓWNA ================== @@ -349,39 +351,40 @@ def main(): device = init_device() settings = load_oled_config() - stations = load_radio_stations() np = NowPlaying() state = ScreenState() draw_startup_animation(device) - def rotate_cb(direction): - on_encoder_rotate(direction, np, state) - - def click_cb(): - on_encoder_click(np, state) - - def hold_cb(): - on_encoder_hold(np, state) - enc = Encoder( - on_rotate=rotate_cb, - on_click=click_cb, - on_hold=hold_cb + on_rotate=lambda d: on_encoder_rotate(d, np, state), + on_click=lambda: on_encoder_click(np, state), + on_hold=lambda: on_encoder_hold(np, state) ) while running: now = time.time() + inactive = now - state.last_input_time + + # wygaszacz + if inactive > settings.screensaver_off_after: + with canvas(device) as draw: + draw.rectangle((0, 0, device.width, device.height), outline=0, fill=0) + time.sleep(0.1) + continue + elif inactive > settings.screensaver_dim_after: + device.contrast(settings.screensaver_dim_level) + else: + device.contrast(settings.brightness_default) - if state.mode == "menu" and (now - state.last_input_time) > MENU_TIMEOUT: + # timeout menu + if state.mode == "menu" and inactive > MENU_TIMEOUT: state.mode = "main" - # TODO: aktualizacja np z MPD / playera / stacji - if state.mode == "main": draw_main_screen(device, np, state) - elif state.mode == "menu": + else: draw_menu(device, state) time.sleep(1.0 / FPS) From c86865fcb0e95db91df653e4559fbcc9ba3e31f7 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 10:34:26 +0100 Subject: [PATCH 39/63] add MPD --- oled/encoder.py | 4 +- oled/oled.py | 220 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 174 insertions(+), 50 deletions(-) diff --git a/oled/encoder.py b/oled/encoder.py index 5e8d13a..d5ca25a 100644 --- a/oled/encoder.py +++ b/oled/encoder.py @@ -8,8 +8,8 @@ # KONFIGURACJA PINÓW # ========================== -PIN_A = 24 # CLK -PIN_B = 23 # DT +PIN_A = 23 # CLK +PIN_B = 24 # DT PIN_SW = 13 # SW (przycisk) DEBOUNCE_ROTATE = 0.002 # 2 ms diff --git a/oled/oled.py b/oled/oled.py index 1b133db..cd96805 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -10,6 +10,9 @@ from luma.core.render import canvas from luma.oled.device import ssd1306 +from PIL import ImageFont +from mpd import MPDClient + from encoder import Encoder @@ -22,6 +25,10 @@ CONFIG_OLED = CONFIG_DIR / "config-oled.json" CONFIG_RADIO = CONFIG_DIR / "config-radio.json" +FONT_PATH = BASE_DIR / "fonts" / "DejaVuSansMono.ttf" +FONT_SMALL = ImageFont.truetype(str(FONT_PATH), 10) +FONT_NORMAL = ImageFont.truetype(str(FONT_PATH), 12) + # ================== KONFIG DOMYŚLNY ================== @@ -49,14 +56,14 @@ @dataclass class NowPlaying: - source: str = "radio" - artist: str = "Artist" - title: str = "Title" - bitrate_kbps: int = 320 - bit_depth: int = 24 - sample_rate: int = 96000 + source: str = "radio" # "radio", "file", "bt" + artist: str = "" + title: str = "" + bitrate_kbps: int = 0 + bit_depth: int = 16 + sample_rate: int = 44100 volume: int = 42 - playing: bool = True + playing: bool = False @dataclass @@ -104,6 +111,10 @@ def load_json(path: Path, default: dict) -> dict: return default.copy() +def save_json(path: Path, data: dict): + path.write_text(json.dumps(data, indent=2), encoding="utf-8") + + def load_oled_config() -> Settings: cfg = load_json(CONFIG_OLED, DEFAULT_OLED_CONFIG) return Settings( @@ -133,6 +144,40 @@ def init_device(): return device +# ================== TEKST / FORMAT ================== + +def normalize(text: str) -> str: + # przy TTF nie musimy transliterować, ale zostawiamy na wszelki wypadek + return text or "" + + +def get_source_icon(np: NowPlaying) -> str: + return "▶" if np.playing else "⏸" + + +def format_bitrate(np: NowPlaying) -> str: + return f"{np.bitrate_kbps}k" if np.bitrate_kbps else "" + + +def format_bitdepth(np: NowPlaying) -> str: + sr_khz = int(np.sample_rate / 1000) + return f"{np.bit_depth}/{sr_khz}" + + +def is_hq(np: NowPlaying) -> bool: + if not np.playing: + return False + # HQ od 16/44.1 + return np.bit_depth >= 16 and np.sample_rate >= 44100 + + +def is_hires(np: NowPlaying) -> bool: + if not np.playing: + return False + # HiRes powyżej 16/44.1 + return np.bit_depth >= 24 or np.sample_rate > 48000 + + # ================== RYSOWANIE ================== def draw_startup_animation(device): @@ -155,28 +200,11 @@ def draw_startup_animation(device): max_line = int(len(logo_lines) * (i + 1) / steps) y = 0 for line in logo_lines[:max_line]: - draw.text((0, y), line[:21], fill=255) + draw.text((0, y), line[:21], font=FONT_SMALL, fill=255) y += 8 time.sleep(0.1) -def get_source_icon(np: NowPlaying) -> str: - return "▶" if np.playing else "⏸" - - -def is_hires(np: NowPlaying) -> bool: - return np.bit_depth >= 24 or np.sample_rate > 48000 - - -def format_bitrate(np: NowPlaying) -> str: - return f"{np.bitrate_kbps}k" - - -def format_bitdepth(np: NowPlaying) -> str: - sr_khz = int(np.sample_rate / 1000) - return f"{np.bit_depth}/{sr_khz}" - - def draw_volume_bar(draw, x, y, width, height, volume): segments = 12 seg_width = width // segments @@ -192,6 +220,7 @@ def draw_volume_bar(draw, x, y, width, height, volume): def scroll_text(text: str, width_chars: int, offset: int) -> str: + text = normalize(text) if len(text) <= width_chars: return text.ljust(width_chars) padded = text + " " @@ -204,14 +233,15 @@ def draw_main_screen(device, np: NowPlaying, state: ScreenState): chars_per_line = 16 icon = get_source_icon(np) + hq = is_hq(np) hires = is_hires(np) if np.source == "radio" and np.artist: line1_text = np.artist - line2_text = np.title + line2_text = np.title or "" else: - line1_text = np.title - line2_text = f"{format_bitrate(np)} {format_bitdepth(np)}" + line1_text = np.title or "" + line2_text = f"{format_bitrate(np)} {format_bitdepth(np)}".strip() now = time.time() if now - state.last_scroll_time > SCROLL_SPEED: @@ -223,27 +253,38 @@ def draw_main_screen(device, np: NowPlaying, state: ScreenState): line2 = scroll_text(line2_text, chars_per_line, state.scroll_offset_line2) with canvas(device) as draw: - draw.text((0, 0), icon, fill=255) - draw.text((12, 0), line1, fill=255) + # linia 1: ikona + tekst + draw.text((0, 0), icon, font=FONT_NORMAL, fill=255) + draw.text((14, 0), line1, font=FONT_NORMAL, fill=255) - draw.text((0, 10), line2, fill=255) + # linia 2: tekst + HQ/HiRes + draw.text((0, 14), line2, font=FONT_NORMAL, fill=255) if hires: - draw.text((w - 18, 10), "HR", fill=255) + draw.text((w - 26, 14), "HiRes", font=FONT_SMALL, fill=255) + elif hq: + draw.text((w - 20, 14), "HQ", font=FONT_SMALL, fill=255) - draw_volume_bar(draw, 0, h - 8, w - 30, 6, np.volume) - draw.text((w - 28, h - 10), f"{np.volume}%", fill=255) + # pasek głośności + % + bar_width = w - 32 + draw_volume_bar(draw, 0, h - 10, bar_width, 6, np.volume) + draw.text((w - 30, h - 12), f"{np.volume}%", font=FONT_SMALL, fill=255) # ================== MENU ================== -MENU_STRUCTURE = { - "root": ["Ustawienia", "Ulubione stacje", "ESC"], - "Ustawienia": ["Filtry EQ", "Wygaszacz", "Ekran", "ESC"], - "Filtry EQ": ["EQ 5-pasmowy", "EQ 2-pasmowy", "ESC"], - "Wygaszacz": ["Czas do przyciemnienia", "Jasność po przyciemnieniu", "Czas do wygaszenia", "ESC"], - "Ekran": ["Jasność domyślna", "ESC"], - "Ulubione stacje": get_favorite_stations() + ["ESC"], -} +def build_menu_structure(): + return { + "root": ["Ustawienia", "Ulubione stacje", "Źródło", "ESC"], + "Ustawienia": ["Filtry EQ", "Wygaszacz", "Ekran", "ESC"], + "Filtry EQ": ["EQ 5-pasmowy", "EQ 2-pasmowy", "ESC"], + "Wygaszacz": ["Czas do przyciemnienia", "Jasność po przyciemnieniu", "Czas do wygaszenia", "ESC"], + "Ekran": ["Jasność domyślna", "ESC"], + "Ulubione stacje": get_favorite_stations() + ["ESC"], + "Źródło": ["Radio", "Pliki", "Bluetooth (niedostępne)", "ESC"], + } + + +MENU_STRUCTURE = build_menu_structure() def current_menu_items(state: ScreenState): @@ -264,14 +305,75 @@ def draw_menu(device, state: ScreenState): state.scroll_offset = state.selected_index - visible + 1 with canvas(device) as draw: - draw.text((0, 0), title[:16], fill=255) + draw.text((0, 0), title[:16], font=FONT_NORMAL, fill=255) for i in range(visible): idx = state.scroll_offset + i if idx >= len(items): break prefix = "> " if idx == state.selected_index else " " - draw.text((0, 10 + i * 10), prefix + items[idx][:14], fill=255) + draw.text((0, 14 + i * 12), prefix + items[idx][:14], font=FONT_NORMAL, fill=255) + + +# ================== MPD ================== + +def init_mpd(): + client = MPDClient() + client.timeout = 2 + client.idletimeout = None + try: + client.connect("localhost", 6600) + except Exception: + client = None + return client + + +def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): + if client is None: + return + + try: + status = client.status() + song = client.currentsong() + + np.playing = (status.get("state") == "play") + + if "volume" in status: + try: + np.volume = int(status["volume"]) + except ValueError: + pass + + if "audio" in status: + # "44100:16:2" + parts = status["audio"].split(":") + if len(parts) >= 2: + try: + np.sample_rate = int(parts[0]) + np.bit_depth = int(parts[1]) + except ValueError: + pass + + if "bitrate" in status: + try: + np.bitrate_kbps = int(status["bitrate"]) + except ValueError: + pass + + file_path = song.get("file", "") + + if file_path.startswith("http"): + np.source = "radio" + elif file_path.startswith("bluetooth:"): + np.source = "bt" + else: + np.source = "file" + + np.title = song.get("title", file_path) + np.artist = song.get("artist", "") + + except Exception: + pass # ================== ENKODER ================== @@ -287,7 +389,24 @@ def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState): state.selected_index = max(0, min(len(items) - 1, state.selected_index + direction)) -def on_encoder_click(np: NowPlaying, state: ScreenState): +def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings: Settings): + if choice == "Radio": + np.source = "radio" + elif choice == "Pliki": + np.source = "file" + elif choice == "Bluetooth (niedostępne)": + # tylko placeholder + pass + elif choice == "EQ 5-pasmowy": + settings.eq_mode = "5band" + save_json(CONFIG_OLED, settings.__dict__) + elif choice == "EQ 2-pasmowy": + settings.eq_mode = "2band" + save_json(CONFIG_OLED, settings.__dict__) + # tu można dalej rozwijać ustawienia (wygaszacz, jasność, itp.) + + +def on_encoder_click(np: NowPlaying, state: ScreenState, settings: Settings): state.last_input_time = time.time() if state.mode == "main": @@ -295,6 +414,9 @@ def on_encoder_click(np: NowPlaying, state: ScreenState): return items = current_menu_items(state) + if not items: + return + choice = items[state.selected_index] if choice == "ESC": @@ -312,7 +434,7 @@ def on_encoder_click(np: NowPlaying, state: ScreenState): state.scroll_offset = 0 return - print("Wybrano:", choice) + handle_menu_action(choice, np, state, settings) def on_encoder_hold(np: NowPlaying, state: ScreenState): @@ -351,15 +473,15 @@ def main(): device = init_device() settings = load_oled_config() - np = NowPlaying() state = ScreenState() + client = init_mpd() draw_startup_animation(device) enc = Encoder( on_rotate=lambda d: on_encoder_rotate(d, np, state), - on_click=lambda: on_encoder_click(np, state), + on_click=lambda: on_encoder_click(np, state, settings), on_hold=lambda: on_encoder_hold(np, state) ) @@ -367,6 +489,8 @@ def main(): now = time.time() inactive = now - state.last_input_time + update_now_playing_from_mpd(client, np) + # wygaszacz if inactive > settings.screensaver_off_after: with canvas(device) as draw: From c841d4c4470578c36fb6cf0cbd1ab67653cf99cd Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 11:05:18 +0100 Subject: [PATCH 40/63] add menu OLED --- config/config-oled.json | 6 +-- config/config-radio.json | 66 +++++++++++++++++++++-- oled/oled.py | 114 +++++++++++++++++++++++++++------------ 3 files changed, 145 insertions(+), 41 deletions(-) diff --git a/config/config-oled.json b/config/config-oled.json index e7c3165..7a88c3c 100644 --- a/config/config-oled.json +++ b/config/config-oled.json @@ -1,7 +1,7 @@ { "eq_mode": "5band", - "screensaver_dim_after": 20, + "screensaver_dim_after": 10, "screensaver_dim_level": 10, - "screensaver_off_after": 60, - "brightness_default": 255 + "screensaver_off_after": 30, + "brightness_default": 100 } diff --git a/config/config-radio.json b/config/config-radio.json index 9a8d8ba..69622dc 100644 --- a/config/config-radio.json +++ b/config/config-radio.json @@ -4,13 +4,73 @@ "name": "Radio Paradise (FLAC)", "url": "http://stream.radioparadise.com/flac", "favorite": true, + "tags": ["hires", "flac", "electronic"] + }, + { + "name": "Radio Paradise Mellow (FLAC)", + "url": "http://stream.radioparadise.com/mellow-flac", + "favorite": true, + "tags": ["hires", "flac", "chill"] + }, + { + "name": "Radio Paradise Rock (FLAC)", + "url": "http://stream.radioparadise.com/rock-flac", + "favorite": false, + "tags": ["hires", "flac"] + }, + { + "name": "Radio Paradise World (FLAC)", + "url": "http://stream.radioparadise.com/world-etc-flac", + "favorite": false, "tags": ["hires", "flac"] }, { - "name": "Example 320k MP3", - "url": "http://example.com/stream.mp3", + "name": "FIP Électro", + "url": "https://icecast.radiofrance.fr/fipelectro-midfi.mp3", + "favorite": true, + "tags": ["electronic", "aac"] + }, + { + "name": "FIP Groove", + "url": "https://icecast.radiofrance.fr/fipgroove-midfi.mp3", + "favorite": false, + "tags": ["groove", "aac"] + }, + { + "name": "SomaFM Groove Salad", + "url": "https://ice2.somafm.com/groovesalad-256-aac", + "favorite": true, + "tags": ["chill", "ambient"] + }, + { + "name": "SomaFM Deep Space One", + "url": "https://ice2.somafm.com/deepspaceone-256-aac", + "favorite": false, + "tags": ["ambient", "space"] + }, + { + "name": "SomaFM Drone Zone", + "url": "https://ice2.somafm.com/dronezone-256-aac", + "favorite": false, + "tags": ["ambient", "drone"] + }, + { + "name": "DI.FM Trance", + "url": "http://prem2.di.fm/trance", + "favorite": true, + "tags": ["trance", "electronic"] + }, + { + "name": "DI.FM Vocal Trance", + "url": "http://prem2.di.fm/vocaltrance", + "favorite": false, + "tags": ["trance", "vocal"] + }, + { + "name": "DI.FM Progressive", + "url": "http://prem2.di.fm/progressive", "favorite": false, - "tags": ["mp3", "320k"] + "tags": ["progressive", "electronic"] } ] } diff --git a/oled/oled.py b/oled/oled.py index cd96805..b33c2b9 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -19,13 +19,14 @@ # ================== ŚCIEŻKI ================== BASE_DIR = Path(__file__).resolve().parent -CONFIG_DIR = BASE_DIR / "config" +# config i fonts są poziom wyżej: streamer/config, streamer/fonts +CONFIG_DIR = BASE_DIR.parent / "config" CONFIG_DIR.mkdir(exist_ok=True) CONFIG_OLED = CONFIG_DIR / "config-oled.json" CONFIG_RADIO = CONFIG_DIR / "config-radio.json" -FONT_PATH = BASE_DIR / "fonts" / "DejaVuSansMono.ttf" +FONT_PATH = BASE_DIR.parent / "fonts" / "DejaVuSansMono.ttf" FONT_SMALL = ImageFont.truetype(str(FONT_PATH), 10) FONT_NORMAL = ImageFont.truetype(str(FONT_PATH), 12) @@ -34,10 +35,10 @@ DEFAULT_OLED_CONFIG = { "eq_mode": "5band", - "screensaver_dim_after": 20, - "screensaver_dim_level": 10, - "screensaver_off_after": 60, - "brightness_default": 255 + "screensaver_dim_after": 10, # po ilu sekundach ściemnia do ~10% + "screensaver_dim_level": 10, # procent jasności przy przyciemnieniu (soft) + "screensaver_off_after": 60, # po ilu sekundach całkowicie wygasić + "brightness_default": 50 # domyślnie 50% (soft) } DEFAULT_RADIO_CONFIG = { @@ -83,10 +84,10 @@ class ScreenState: @dataclass class Settings: eq_mode: str = "5band" - screensaver_dim_after: int = 20 + screensaver_dim_after: int = 10 screensaver_dim_level: int = 10 screensaver_off_after: int = 60 - brightness_default: int = 255 + brightness_default: int = 50 # ================== PARAMETRY OLED ================== @@ -119,10 +120,10 @@ def load_oled_config() -> Settings: cfg = load_json(CONFIG_OLED, DEFAULT_OLED_CONFIG) return Settings( eq_mode=cfg.get("eq_mode", "5band"), - screensaver_dim_after=int(cfg.get("screensaver_dim_after", 20)), + screensaver_dim_after=int(cfg.get("screensaver_dim_after", 10)), screensaver_dim_level=int(cfg.get("screensaver_dim_level", 10)), screensaver_off_after=int(cfg.get("screensaver_off_after", 60)), - brightness_default=int(cfg.get("brightness_default", 255)), + brightness_default=int(cfg.get("brightness_default", 50)), ) @@ -133,7 +134,7 @@ def load_radio_stations(): def get_favorite_stations(): stations = load_radio_stations() - return [s["name"] for s in stations if s.get("favorite")] + return [s for s in stations if s.get("favorite")] # ================== OLED INIT ================== @@ -205,18 +206,33 @@ def draw_startup_animation(device): time.sleep(0.1) -def draw_volume_bar(draw, x, y, width, height, volume): - segments = 12 - seg_width = width // segments - filled = int(segments * volume / 100) +def draw_volume_triangle(draw, x, y, width, height, volume): + """ + Trójkątny wskaźnik głośności: + - obrys: pełny trójkąt + - wypełnienie: proporcjonalne do volume (0–100) + """ + x0, y0 = x, y + height + x1, y1 = x + width, y + x2, y2 = x + width, y + height - for i in range(segments): - x0 = x + i * seg_width - x1 = x0 + seg_width - 1 - if i < filled: - draw.rectangle((x0, y, x1, y + height), outline=255, fill=255) - else: - draw.rectangle((x0, y, x1, y + height), outline=255, fill=0) + # obrys + draw.line((x0, y0, x1, y1), fill=255) + draw.line((x1, y1, x2, y2), fill=255) + draw.line((x2, y2, x0, y0), fill=255) + + if volume <= 0: + return + + fill_width = int(width * (volume / 100.0)) + if fill_width <= 0: + return + + fx1 = x + fill_width + fx2 = x + fill_width + fy1 = y + int((1 - (volume / 100.0)) * height) + + draw.polygon([(x0, y0), (fx1, fy1), (fx2, y2)], outline=255, fill=255) def scroll_text(text: str, width_chars: int, offset: int) -> str: @@ -264,22 +280,28 @@ def draw_main_screen(device, np: NowPlaying, state: ScreenState): elif hq: draw.text((w - 20, 14), "HQ", font=FONT_SMALL, fill=255) - # pasek głośności + % - bar_width = w - 32 - draw_volume_bar(draw, 0, h - 10, bar_width, 6, np.volume) - draw.text((w - 30, h - 12), f"{np.volume}%", font=FONT_SMALL, fill=255) + # trójkątny wskaźnik głośności + % + tri_width = 24 + tri_height = 12 + tri_x = 0 + tri_y = h - tri_height - 1 + draw_volume_triangle(draw, tri_x, tri_y, tri_width, tri_height, np.volume) + draw.text((tri_x + tri_width + 4, h - 12), f"{np.volume}%", font=FONT_SMALL, fill=255) # ================== MENU ================== def build_menu_structure(): + fav_names = [s["name"] for s in get_favorite_stations()] + if not fav_names: + fav_names = ["(brak ulubionych)"] return { "root": ["Ustawienia", "Ulubione stacje", "Źródło", "ESC"], "Ustawienia": ["Filtry EQ", "Wygaszacz", "Ekran", "ESC"], "Filtry EQ": ["EQ 5-pasmowy", "EQ 2-pasmowy", "ESC"], "Wygaszacz": ["Czas do przyciemnienia", "Jasność po przyciemnieniu", "Czas do wygaszenia", "ESC"], "Ekran": ["Jasność domyślna", "ESC"], - "Ulubione stacje": get_favorite_stations() + ["ESC"], + "Ulubione stacje": fav_names + ["ESC"], "Źródło": ["Radio", "Pliki", "Bluetooth (niedostępne)", "ESC"], } @@ -376,6 +398,21 @@ def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): pass +def play_station_by_name(client: MPDClient | None, name: str): + if client is None: + return + stations = load_radio_stations() + for s in stations: + if s["name"] == name: + try: + client.clear() + client.add(s["url"]) + client.play() + except Exception: + pass + break + + # ================== ENKODER ================== def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState): @@ -389,7 +426,7 @@ def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState): state.selected_index = max(0, min(len(items) - 1, state.selected_index + direction)) -def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings: Settings): +def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings: Settings, client: MPDClient | None): if choice == "Radio": np.source = "radio" elif choice == "Pliki": @@ -403,10 +440,11 @@ def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings elif choice == "EQ 2-pasmowy": settings.eq_mode = "2band" save_json(CONFIG_OLED, settings.__dict__) - # tu można dalej rozwijać ustawienia (wygaszacz, jasność, itp.) + elif choice in [s["name"] for s in get_favorite_stations()]: + play_station_by_name(client, choice) -def on_encoder_click(np: NowPlaying, state: ScreenState, settings: Settings): +def on_encoder_click(np: NowPlaying, state: ScreenState, settings: Settings, client: MPDClient | None): state.last_input_time = time.time() if state.mode == "main": @@ -434,7 +472,7 @@ def on_encoder_click(np: NowPlaying, state: ScreenState, settings: Settings): state.scroll_offset = 0 return - handle_menu_action(choice, np, state, settings) + handle_menu_action(choice, np, state, settings, client) def on_encoder_hold(np: NowPlaying, state: ScreenState): @@ -481,7 +519,7 @@ def main(): enc = Encoder( on_rotate=lambda d: on_encoder_rotate(d, np, state), - on_click=lambda: on_encoder_click(np, state, settings), + on_click=lambda: on_encoder_click(np, state, settings, client), on_hold=lambda: on_encoder_hold(np, state) ) @@ -491,16 +529,22 @@ def main(): update_now_playing_from_mpd(client, np) - # wygaszacz + # soft-dimming: 50% → 10% → OFF if inactive > settings.screensaver_off_after: with canvas(device) as draw: draw.rectangle((0, 0, device.width, device.height), outline=0, fill=0) time.sleep(0.1) continue elif inactive > settings.screensaver_dim_after: - device.contrast(settings.screensaver_dim_level) + # soft – nie ruszamy hardware contrast, tylko rysujemy mniej często / zostawiamy jak jest + # tu możesz później dodać np. ciemniejszy motyw + pass else: - device.contrast(settings.brightness_default) + # jasność domyślna – jeśli Twój sterownik wspiera contrast() + try: + device.contrast(int(255 * (settings.brightness_default / 100.0))) + except Exception: + pass # timeout menu if state.mode == "menu" and inactive > MENU_TIMEOUT: From 926a4b805f314671af0d52cba2772c436f12fa10 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 11:33:16 +0100 Subject: [PATCH 41/63] add menu OLED --- oled/oled.py | 94 +++++++++++++--------------------------------------- 1 file changed, 23 insertions(+), 71 deletions(-) diff --git a/oled/oled.py b/oled/oled.py index b33c2b9..6c7d7c5 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -19,7 +19,6 @@ # ================== ŚCIEŻKI ================== BASE_DIR = Path(__file__).resolve().parent -# config i fonts są poziom wyżej: streamer/config, streamer/fonts CONFIG_DIR = BASE_DIR.parent / "config" CONFIG_DIR.mkdir(exist_ok=True) @@ -35,10 +34,10 @@ DEFAULT_OLED_CONFIG = { "eq_mode": "5band", - "screensaver_dim_after": 10, # po ilu sekundach ściemnia do ~10% - "screensaver_dim_level": 10, # procent jasności przy przyciemnieniu (soft) - "screensaver_off_after": 60, # po ilu sekundach całkowicie wygasić - "brightness_default": 50 # domyślnie 50% (soft) + "screensaver_dim_after": 10, + "screensaver_dim_level": 10, + "screensaver_off_after": 60, + "brightness_default": 50 } DEFAULT_RADIO_CONFIG = { @@ -57,7 +56,7 @@ @dataclass class NowPlaying: - source: str = "radio" # "radio", "file", "bt" + source: str = "radio" artist: str = "" title: str = "" bitrate_kbps: int = 0 @@ -148,7 +147,6 @@ def init_device(): # ================== TEKST / FORMAT ================== def normalize(text: str) -> str: - # przy TTF nie musimy transliterować, ale zostawiamy na wszelki wypadek return text or "" @@ -168,14 +166,12 @@ def format_bitdepth(np: NowPlaying) -> str: def is_hq(np: NowPlaying) -> bool: if not np.playing: return False - # HQ od 16/44.1 return np.bit_depth >= 16 and np.sample_rate >= 44100 def is_hires(np: NowPlaying) -> bool: if not np.playing: return False - # HiRes powyżej 16/44.1 return np.bit_depth >= 24 or np.sample_rate > 48000 @@ -206,33 +202,24 @@ def draw_startup_animation(device): time.sleep(0.1) -def draw_volume_triangle(draw, x, y, width, height, volume): +def draw_volume_icon(draw, x, y, height, width, volume): """ - Trójkątny wskaźnik głośności: - - obrys: pełny trójkąt - - wypełnienie: proporcjonalne do volume (0–100) + Ikona głośności: + |-------------------- + |████████------------ + |████████████████---- """ - x0, y0 = x, y + height - x1, y1 = x + width, y - x2, y2 = x + width, y + height + # pionowa kreska + draw.line((x, y, x, y + height), fill=255) - # obrys - draw.line((x0, y0, x1, y1), fill=255) - draw.line((x1, y1, x2, y2), fill=255) - draw.line((x2, y2, x0, y0), fill=255) - - if volume <= 0: - return + # obrys poziomej linii + mid = y + height // 2 + draw.line((x, mid, x + width, mid), fill=255) + # wypełnienie fill_width = int(width * (volume / 100.0)) - if fill_width <= 0: - return - - fx1 = x + fill_width - fx2 = x + fill_width - fy1 = y + int((1 - (volume / 100.0)) * height) - - draw.polygon([(x0, y0), (fx1, fy1), (fx2, y2)], outline=255, fill=255) + if fill_width > 0: + draw.line((x, mid, x + fill_width, mid), fill=255) def scroll_text(text: str, width_chars: int, offset: int) -> str: @@ -269,24 +256,18 @@ def draw_main_screen(device, np: NowPlaying, state: ScreenState): line2 = scroll_text(line2_text, chars_per_line, state.scroll_offset_line2) with canvas(device) as draw: - # linia 1: ikona + tekst draw.text((0, 0), icon, font=FONT_NORMAL, fill=255) draw.text((14, 0), line1, font=FONT_NORMAL, fill=255) - # linia 2: tekst + HQ/HiRes draw.text((0, 14), line2, font=FONT_NORMAL, fill=255) if hires: draw.text((w - 26, 14), "HiRes", font=FONT_SMALL, fill=255) elif hq: draw.text((w - 20, 14), "HQ", font=FONT_SMALL, fill=255) - # trójkątny wskaźnik głośności + % - tri_width = 24 - tri_height = 12 - tri_x = 0 - tri_y = h - tri_height - 1 - draw_volume_triangle(draw, tri_x, tri_y, tri_width, tri_height, np.volume) - draw.text((tri_x + tri_width + 4, h - 12), f"{np.volume}%", font=FONT_SMALL, fill=255) + # ikona głośności + draw_volume_icon(draw, 0, h - 12, 10, w - 40, np.volume) + draw.text((w - 30, h - 12), f"{np.volume}%", font=FONT_SMALL, fill=255) # ================== MENU ================== @@ -367,7 +348,6 @@ def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): pass if "audio" in status: - # "44100:16:2" parts = status["audio"].split(":") if len(parts) >= 2: try: @@ -432,7 +412,6 @@ def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings elif choice == "Pliki": np.source = "file" elif choice == "Bluetooth (niedostępne)": - # tylko placeholder pass elif choice == "EQ 5-pasmowy": settings.eq_mode = "5band" @@ -529,37 +508,10 @@ def main(): update_now_playing_from_mpd(client, np) - # soft-dimming: 50% → 10% → OFF + # soft-dimming if inactive > settings.screensaver_off_after: with canvas(device) as draw: draw.rectangle((0, 0, device.width, device.height), outline=0, fill=0) time.sleep(0.1) continue - elif inactive > settings.screensaver_dim_after: - # soft – nie ruszamy hardware contrast, tylko rysujemy mniej często / zostawiamy jak jest - # tu możesz później dodać np. ciemniejszy motyw - pass - else: - # jasność domyślna – jeśli Twój sterownik wspiera contrast() - try: - device.contrast(int(255 * (settings.brightness_default / 100.0))) - except Exception: - pass - - # timeout menu - if state.mode == "menu" and inactive > MENU_TIMEOUT: - state.mode = "main" - - if state.mode == "main": - draw_main_screen(device, np, state) - else: - draw_menu(device, state) - - time.sleep(1.0 / FPS) - - enc.stop() - sys.exit(0) - - -if __name__ == "__main__": - main() + elif inactive From 74e086b0a667c38f0d9e37504dd1a40360a4beea Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 11:40:21 +0100 Subject: [PATCH 42/63] fix font --- oled/oled.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oled/oled.py b/oled/oled.py index 6c7d7c5..5385ede 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -25,7 +25,7 @@ CONFIG_OLED = CONFIG_DIR / "config-oled.json" CONFIG_RADIO = CONFIG_DIR / "config-radio.json" -FONT_PATH = BASE_DIR.parent / "fonts" / "DejaVuSansMono.ttf" +FONT_PATH = Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf") FONT_SMALL = ImageFont.truetype(str(FONT_PATH), 10) FONT_NORMAL = ImageFont.truetype(str(FONT_PATH), 12) From f3badc5c420b4d5d88007a27ba3d60f058ebcd32 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 11:46:20 +0100 Subject: [PATCH 43/63] fix font --- oled/oled.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/oled/oled.py b/oled/oled.py index 5385ede..a40f12f 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -514,4 +514,34 @@ def main(): draw.rectangle((0, 0, device.width, device.height), outline=0, fill=0) time.sleep(0.1) continue - elif inactive + elif inactive > settings.screensaver_dim_after: + # soft dimming (10%) + try: + device.contrast(int(255 * (settings.screensaver_dim_level / 100.0))) + except Exception: + pass + else: + # jasność domyślna + try: + device.contrast(int(255 * (settings.brightness_default / 100.0))) + except Exception: + pass + + # timeout menu + if state.mode == "menu" and inactive > MENU_TIMEOUT: + state.mode = "main" + + # rysowanie ekranu + if state.mode == "main": + draw_main_screen(device, np, state) + else: + draw_menu(device, state) + + time.sleep(1.0 / FPS) + + enc.stop() + sys.exit(0) + + +if __name__ == "__main__": + main() From 60814ab2c9ef685fe3ea9900edbbd4def57e9965 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 12:00:43 +0100 Subject: [PATCH 44/63] fix encoder and oled --- oled/oled.py | 67 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/oled/oled.py b/oled/oled.py index a40f12f..f215b1b 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -203,25 +203,14 @@ def draw_startup_animation(device): def draw_volume_icon(draw, x, y, height, width, volume): - """ - Ikona głośności: - |-------------------- - |████████------------ - |████████████████---- - """ - # pionowa kreska draw.line((x, y, x, y + height), fill=255) - - # obrys poziomej linii mid = y + height // 2 draw.line((x, mid, x + width, mid), fill=255) - # wypełnienie fill_width = int(width * (volume / 100.0)) if fill_width > 0: draw.line((x, mid, x + fill_width, mid), fill=255) - def scroll_text(text: str, width_chars: int, offset: int) -> str: text = normalize(text) if len(text) <= width_chars: @@ -239,13 +228,19 @@ def draw_main_screen(device, np: NowPlaying, state: ScreenState): hq = is_hq(np) hires = is_hires(np) - if np.source == "radio" and np.artist: - line1_text = np.artist - line2_text = np.title or "" + # RADIO + if np.source == "radio": + if np.artist: + line1_text = np.artist + line2_text = np.title + else: + line1_text = np.title + line2_text = "" else: line1_text = np.title or "" line2_text = f"{format_bitrate(np)} {format_bitdepth(np)}".strip() + # przewijanie now = time.time() if now - state.last_scroll_time > SCROLL_SPEED: state.scroll_offset_line1 += 1 @@ -255,11 +250,17 @@ def draw_main_screen(device, np: NowPlaying, state: ScreenState): line1 = scroll_text(line1_text, chars_per_line - 2, state.scroll_offset_line1) line2 = scroll_text(line2_text, chars_per_line, state.scroll_offset_line2) + bitinfo = f"{np.bit_depth}bit / {np.sample_rate//1000}kHz" + with canvas(device) as draw: draw.text((0, 0), icon, font=FONT_NORMAL, fill=255) draw.text((14, 0), line1, font=FONT_NORMAL, fill=255) draw.text((0, 14), line2, font=FONT_NORMAL, fill=255) + + draw.text((0, 26), bitinfo, font=FONT_SMALL, fill=255) + + # HQ / HiRes if hires: draw.text((w - 26, 14), "HiRes", font=FONT_SMALL, fill=255) elif hq: @@ -341,12 +342,14 @@ def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): np.playing = (status.get("state") == "play") + # głośność z MPD if "volume" in status: try: np.volume = int(status["volume"]) except ValueError: pass + # audio format if "audio" in status: parts = status["audio"].split(":") if len(parts) >= 2: @@ -356,6 +359,7 @@ def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): except ValueError: pass + # bitrate if "bitrate" in status: try: np.bitrate_kbps = int(status["bitrate"]) @@ -364,6 +368,7 @@ def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): file_path = song.get("file", "") + # źródło if file_path.startswith("http"): np.source = "radio" elif file_path.startswith("bluetooth:"): @@ -371,6 +376,27 @@ def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): else: np.source = "file" + # RADIO — NAZWA STACJI + if np.source == "radio": + stations = load_radio_stations() + for s in stations: + if s["url"] in file_path: + np.title = s["name"] + break + else: + np.title = song.get("title", file_path) + + # METADANE STREAMU: "Artist - Title" + stream_title = song.get("title", "") + if " - " in stream_title: + artist, title = stream_title.split(" - ", 1) + np.artist = artist + np.title = title + else: + np.artist = "" + return + + # PLIKI np.title = song.get("title", file_path) np.artist = song.get("artist", "") @@ -395,17 +421,22 @@ def play_station_by_name(client: MPDClient | None, name: str): # ================== ENKODER ================== -def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState): +def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState, client: MPDClient | None): state.last_input_time = time.time() if state.mode == "main": - np.volume = max(0, min(100, np.volume + direction)) + new_vol = max(0, min(100, np.volume + direction)) + if client: + try: + client.setvol(new_vol) + except: + pass + np.volume = new_vol elif state.mode == "menu": items = current_menu_items(state) state.selected_index = max(0, min(len(items) - 1, state.selected_index + direction)) - def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings: Settings, client: MPDClient | None): if choice == "Radio": np.source = "radio" @@ -497,7 +528,7 @@ def main(): draw_startup_animation(device) enc = Encoder( - on_rotate=lambda d: on_encoder_rotate(d, np, state), + on_rotate=lambda d: on_encoder_rotate(d, np, state, client), on_click=lambda: on_encoder_click(np, state, settings, client), on_hold=lambda: on_encoder_hold(np, state) ) From 738dd0b6929ac8a04f37fb9e3d65e5444c37bcff Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 12:27:47 +0100 Subject: [PATCH 45/63] fix encoder and oled --- oled/oled.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/oled/oled.py b/oled/oled.py index f215b1b..9bc8f3a 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -278,11 +278,12 @@ def build_menu_structure(): if not fav_names: fav_names = ["(brak ulubionych)"] return { - "root": ["Ustawienia", "Ulubione stacje", "Źródło", "ESC"], + "root": ["Ustawienia", "Ulubione stacje", "Stacje radiowe", "Źródło", "ESC"] "Ustawienia": ["Filtry EQ", "Wygaszacz", "Ekran", "ESC"], "Filtry EQ": ["EQ 5-pasmowy", "EQ 2-pasmowy", "ESC"], "Wygaszacz": ["Czas do przyciemnienia", "Jasność po przyciemnieniu", "Czas do wygaszenia", "ESC"], "Ekran": ["Jasność domyślna", "ESC"], + "Stacje radiowe": [s["name"] for s in load_radio_stations()] + ["ESC"], "Ulubione stacje": fav_names + ["ESC"], "Źródło": ["Radio", "Pliki", "Bluetooth (niedostępne)", "ESC"], } @@ -432,7 +433,6 @@ def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState, client except: pass np.volume = new_vol - elif state.mode == "menu": items = current_menu_items(state) state.selected_index = max(0, min(len(items) - 1, state.selected_index + direction)) @@ -458,8 +458,13 @@ def on_encoder_click(np: NowPlaying, state: ScreenState, settings: Settings, cli state.last_input_time = time.time() if state.mode == "main": - np.playing = not np.playing - return + if client: + if np.playing: + client.pause(1) + else: + client.pause(0) + np.playing = not np.playing + return items = current_menu_items(state) if not items: From 456a3d98d2803527b9dab17ab616a8b763b8b42b Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 13:05:27 +0100 Subject: [PATCH 46/63] fix encoder and oled --- oled/oled.py | 58 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/oled/oled.py b/oled/oled.py index 9bc8f3a..40e00d8 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -201,6 +201,11 @@ def draw_startup_animation(device): y += 8 time.sleep(0.1) +def draw_edit_screen(device, state: ScreenState): + with canvas(device) as draw: + draw.text((0, 0), f"Ustaw: {state.edit_key}", font=FONT_NORMAL, fill=255) + draw.text((0, 20), f"{state.edit_value}", font=FONT_NORMAL, fill=255) + draw.text((0, 40), "Klik = Zapis", font=FONT_SMALL, fill=255) def draw_volume_icon(draw, x, y, height, width, volume): draw.line((x, y, x, y + height), fill=255) @@ -278,7 +283,7 @@ def build_menu_structure(): if not fav_names: fav_names = ["(brak ulubionych)"] return { - "root": ["Ustawienia", "Ulubione stacje", "Stacje radiowe", "Źródło", "ESC"] + "root": ["Ustawienia", "Ulubione stacje", "Stacje radiowe", "Źródło", "ESC"], "Ustawienia": ["Filtry EQ", "Wygaszacz", "Ekran", "ESC"], "Filtry EQ": ["EQ 5-pasmowy", "EQ 2-pasmowy", "ESC"], "Wygaszacz": ["Czas do przyciemnienia", "Jasność po przyciemnieniu", "Czas do wygaszenia", "ESC"], @@ -425,6 +430,9 @@ def play_station_by_name(client: MPDClient | None, name: str): def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState, client: MPDClient | None): state.last_input_time = time.time() + if state.mode == "edit": + state.edit_value = max(0, min(255, state.edit_value + direction)) + return if state.mode == "main": new_vol = max(0, min(100, np.volume + direction)) if client: @@ -452,19 +460,48 @@ def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings save_json(CONFIG_OLED, settings.__dict__) elif choice in [s["name"] for s in get_favorite_stations()]: play_station_by_name(client, choice) + elif choice in [s["name"] for s in load_radio_stations()]: + play_station_by_name(client, choice) + elif choice == "Czas do przyciemnienia": + state.mode = "edit" + state.edit_key = "screensaver_dim_after" + state.edit_value = settings.screensaver_dim_after + return + elif choice == "Jasność po przyciemnieniu": + state.mode = "edit" + state.edit_key = "screensaver_dim_level" + state.edit_value = settings.screensaver_dim_level + return + + elif choice == "Czas do wygaszenia": + state.mode = "edit" + state.edit_key = "screensaver_off_after" + state.edit_value = settings.screensaver_off_after + return + + elif choice == "Jasność domyślna": + state.mode = "edit" + state.edit_key = "brightness_default" + state.edit_value = settings.brightness_default + return def on_encoder_click(np: NowPlaying, state: ScreenState, settings: Settings, client: MPDClient | None): state.last_input_time = time.time() + if state.mode == "edit": + setattr(settings, state.edit_key, state.edit_value) + save_json(CONFIG_OLED, settings.__dict__) + state.mode = "menu" + return if state.mode == "main": - if client: - if np.playing: - client.pause(1) - else: - client.pause(0) - np.playing = not np.playing - return + if client: + if np.playing: + client.pause(1) + else: + client.pause(0) + np.playing = not np.playing + return items = current_menu_items(state) if not items: @@ -570,8 +607,10 @@ def main(): # rysowanie ekranu if state.mode == "main": draw_main_screen(device, np, state) - else: + elif state.mode == "menu": draw_menu(device, state) + elif state.mode == "edit": + draw_edit_screen(device, state) time.sleep(1.0 / FPS) @@ -580,4 +619,3 @@ def main(): if __name__ == "__main__": - main() From fe6d6c110db6abb024749e62a583d4fc701c463c Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 13:12:32 +0100 Subject: [PATCH 47/63] fix encoder and oled --- oled/oled.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oled/oled.py b/oled/oled.py index 40e00d8..c92cad1 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -619,3 +619,4 @@ def main(): if __name__ == "__main__": +main() From bf99ff7688382815179f7a0053fdf93d36e426f7 Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:42:02 +0100 Subject: [PATCH 48/63] Update config-radio.json --- config/config-radio.json | 48 +++++----------------------------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/config/config-radio.json b/config/config-radio.json index 69622dc..a30e3c4 100644 --- a/config/config-radio.json +++ b/config/config-radio.json @@ -1,29 +1,5 @@ { "stations": [ - { - "name": "Radio Paradise (FLAC)", - "url": "http://stream.radioparadise.com/flac", - "favorite": true, - "tags": ["hires", "flac", "electronic"] - }, - { - "name": "Radio Paradise Mellow (FLAC)", - "url": "http://stream.radioparadise.com/mellow-flac", - "favorite": true, - "tags": ["hires", "flac", "chill"] - }, - { - "name": "Radio Paradise Rock (FLAC)", - "url": "http://stream.radioparadise.com/rock-flac", - "favorite": false, - "tags": ["hires", "flac"] - }, - { - "name": "Radio Paradise World (FLAC)", - "url": "http://stream.radioparadise.com/world-etc-flac", - "favorite": false, - "tags": ["hires", "flac"] - }, { "name": "FIP Électro", "url": "https://icecast.radiofrance.fr/fipelectro-midfi.mp3", @@ -36,30 +12,18 @@ "favorite": false, "tags": ["groove", "aac"] }, - { - "name": "SomaFM Groove Salad", - "url": "https://ice2.somafm.com/groovesalad-256-aac", - "favorite": true, - "tags": ["chill", "ambient"] - }, - { - "name": "SomaFM Deep Space One", - "url": "https://ice2.somafm.com/deepspaceone-256-aac", - "favorite": false, - "tags": ["ambient", "space"] - }, - { - "name": "SomaFM Drone Zone", - "url": "https://ice2.somafm.com/dronezone-256-aac", - "favorite": false, - "tags": ["ambient", "drone"] - }, { "name": "DI.FM Trance", "url": "http://prem2.di.fm/trance", "favorite": true, "tags": ["trance", "electronic"] }, + { + "name": "Dance Classics Radio", + "url": "http://vriezenet.nl:11051/dcflac", + "favorite": false, + "tags": ["Classic Dance"] + }, { "name": "DI.FM Vocal Trance", "url": "http://prem2.di.fm/vocaltrance", From 5896995d18a821ef3d0a57d1ee19803563ecbe9c Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Feb 2026 13:49:39 +0100 Subject: [PATCH 49/63] add web server --- oled/oled.py | 2 +- web/app.py | 104 +++++++++++++++++++++++++++++++++++++++ web/templates/edit.html | 35 +++++++++++++ web/templates/index.html | 37 ++++++++++++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 web/app.py create mode 100644 web/templates/edit.html create mode 100644 web/templates/index.html diff --git a/oled/oled.py b/oled/oled.py index c92cad1..f196e06 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -619,4 +619,4 @@ def main(): if __name__ == "__main__": -main() + main() diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..52d9ce6 --- /dev/null +++ b/web/app.py @@ -0,0 +1,104 @@ +from flask import Flask, render_template, request, redirect +from mpd import MPDClient +import json +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent +CONFIG_DIR = BASE_DIR.parent / "config" +CONFIG_RADIO = CONFIG_DIR / "config-radio.json" + +app = Flask(__name__) + +def load_stations(): + if not CONFIG_RADIO.exists(): + return {"stations": []} + return json.loads(CONFIG_RADIO.read_text()) + +def save_stations(data): + CONFIG_RADIO.write_text(json.dumps(data, indent=2)) + +def mpd_client(): + c = MPDClient() + try: + c.connect("localhost", 6600) + return c + except: + return None + +@app.route("/") +def index(): + data = load_stations() + client = mpd_client() + status = {} + if client: + try: + status = client.status() + song = client.currentsong() + status["title"] = song.get("title", "") + except: + pass + return render_template("index.html", stations=data["stations"], status=status) + +@app.route("/play/") +def play(name): + data = load_stations() + client = mpd_client() + if client: + for s in data["stations"]: + if s["name"] == name: + try: + client.clear() + client.add(s["url"]) + client.play() + except: + pass + break + return redirect("/") + +@app.route("/stop") +def stop(): + client = mpd_client() + if client: + try: + client.stop() + except: + pass + return redirect("/") + +@app.route("/edit/", methods=["GET", "POST"]) +def edit(name): + data = load_stations() + station = next((s for s in data["stations"] if s["name"] == name), None) + + if request.method == "POST": + station["name"] = request.form["name"] + station["url"] = request.form["url"] + station["favorite"] = ("favorite" in request.form) + save_stations(data) + return redirect("/") + + return render_template("edit.html", station=station) + +@app.route("/delete/") +def delete(name): + data = load_stations() + data["stations"] = [s for s in data["stations"] if s["name"] != name] + save_stations(data) + return redirect("/") + +@app.route("/add", methods=["GET", "POST"]) +def add(): + if request.method == "POST": + data = load_stations() + data["stations"].append({ + "name": request.form["name"], + "url": request.form["url"], + "favorite": ("favorite" in request.form), + "tags": [] + }) + save_stations(data) + return redirect("/") + return render_template("edit.html", station=None) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080) diff --git a/web/templates/edit.html b/web/templates/edit.html new file mode 100644 index 0000000..82b9983 --- /dev/null +++ b/web/templates/edit.html @@ -0,0 +1,35 @@ + + + + + Edytuj stację + + + + +

{{ station and "Edytuj stację" or "Dodaj stację" }}

+ +
+

Nazwa:
+

+ +

URL:
+

+ +

+ +

+ + +
+ +

← Powrót

+ + + diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..69acfee --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,37 @@ + + + + + Streamer – Stacje radiowe + + + + +

Stacje radiowe

+ +{% for s in stations %} +
+ {{ s.name }}
+ ▶ Play | + ✎ Edytuj | + 🗑 Usuń + {% if s.favorite %} ⭐{% endif %} +
+{% endfor %} + +

➕ Dodaj stację

+ +

Odtwarzacz

+ +

Status: {{ status.state }}

+

Aktualnie: {{ status.title }}

+

Głośność: {{ status.volume }}%

+ +⏹ Stop + + + From 481ad0645621726c1b09d6c7a6cbad7dd8092133 Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:06:40 +0100 Subject: [PATCH 50/63] Update install_python.sh --- scripts/install_python.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/install_python.sh b/scripts/install_python.sh index 500ef8d..360c343 100644 --- a/scripts/install_python.sh +++ b/scripts/install_python.sh @@ -3,7 +3,6 @@ set -e echo "[install_python] Instaluję biblioteki Python..." -sudo apt update sudo apt install -y python3-pip python3-venv build-essential libjpeg-dev zlib1g-dev sudo -H python3 -m pip install --break-system-packages --upgrade pip setuptools wheel @@ -14,3 +13,5 @@ sudo -H python3 -m pip install --break-system-packages \ pillow \ python-mpd2 \ luma.oled + +sudo apt install -y python3-flask From f42323c33b4889cb59d265e08df083c6f0ac2437 Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:07:58 +0100 Subject: [PATCH 51/63] Update app.py --- web/app.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/web/app.py b/web/app.py index 52d9ce6..4a01720 100644 --- a/web/app.py +++ b/web/app.py @@ -1,6 +1,7 @@ from flask import Flask, render_template, request, redirect from mpd import MPDClient import json +import requests from pathlib import Path BASE_DIR = Path(__file__).resolve().parent @@ -9,6 +10,10 @@ app = Flask(__name__) +# ------------------------------ +# Helpers +# ------------------------------ + def load_stations(): if not CONFIG_RADIO.exists(): return {"stations": []} @@ -25,11 +30,33 @@ def mpd_client(): except: return None +# ------------------------------ +# M3U resolver +# ------------------------------ + +def resolve_m3u(url): + if not url.lower().endswith(".m3u"): + return url + try: + r = requests.get(url, timeout=5) + for line in r.text.splitlines(): + line = line.strip() + if line and not line.startswith("#"): + return line + except: + pass + return url + +# ------------------------------ +# Routes +# ------------------------------ + @app.route("/") def index(): data = load_stations() client = mpd_client() status = {} + if client: try: status = client.status() @@ -37,12 +64,14 @@ def index(): status["title"] = song.get("title", "") except: pass + return render_template("index.html", stations=data["stations"], status=status) @app.route("/play/") def play(name): data = load_stations() client = mpd_client() + if client: for s in data["stations"]: if s["name"] == name: @@ -53,6 +82,7 @@ def play(name): except: pass break + return redirect("/") @app.route("/stop") @@ -72,7 +102,7 @@ def edit(name): if request.method == "POST": station["name"] = request.form["name"] - station["url"] = request.form["url"] + station["url"] = resolve_m3u(request.form["url"]) station["favorite"] = ("favorite" in request.form) save_stations(data) return redirect("/") @@ -92,7 +122,7 @@ def add(): data = load_stations() data["stations"].append({ "name": request.form["name"], - "url": request.form["url"], + "url": resolve_m3u(request.form["url"]), "favorite": ("favorite" in request.form), "tags": [] }) @@ -100,5 +130,9 @@ def add(): return redirect("/") return render_template("edit.html", station=None) +# ------------------------------ +# Run +# ------------------------------ + if __name__ == "__main__": app.run(host="0.0.0.0", port=8080) From 8fcb34d9da5ae92a68051a166758fdffb80e935b Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:15:28 +0100 Subject: [PATCH 52/63] Update install.sh --- install.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/install.sh b/install.sh index 0bb26f7..5c46440 100755 --- a/install.sh +++ b/install.sh @@ -156,6 +156,24 @@ EOF log "[oled] plik usługi utworzony" "OK" } + sudo tee /etc/systemd/system/streamer-web.service >/dev/null < Date: Tue, 10 Feb 2026 14:17:30 +0100 Subject: [PATCH 53/63] Update install.sh --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 5c46440..14645e1 100755 --- a/install.sh +++ b/install.sh @@ -154,8 +154,8 @@ WantedBy=multi-user.target EOF log "[oled] plik usługi utworzony" "OK" -} - +}, +{ sudo tee /etc/systemd/system/streamer-web.service >/dev/null < Date: Tue, 10 Feb 2026 14:18:03 +0100 Subject: [PATCH 54/63] Update install.sh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 14645e1..a4a4ca9 100755 --- a/install.sh +++ b/install.sh @@ -172,7 +172,7 @@ WantedBy=multi-user.target EOF log "[Web Panel] plik usługi utworzony" "OK" -} + run_config_scripts() { log "[configure_audio] konfiguracja audio (I2S)..." "INFO" From a60d118637d8e89f67453aef19dc9262ecdccd83 Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:19:44 +0100 Subject: [PATCH 55/63] Update install.sh --- install.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index a4a4ca9..2de601f 100755 --- a/install.sh +++ b/install.sh @@ -154,25 +154,29 @@ WantedBy=multi-user.target EOF log "[oled] plik usługi utworzony" "OK" -}, -{ +} +install_web_service() { + local user_name="$1" + + log "[web] tworzę usługę systemd..." "INFO" + sudo tee /etc/systemd/system/streamer-web.service >/dev/null < Date: Tue, 10 Feb 2026 14:22:29 +0100 Subject: [PATCH 56/63] Update install.sh --- install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.sh b/install.sh index 2de601f..0fc54a3 100755 --- a/install.sh +++ b/install.sh @@ -250,6 +250,7 @@ if [[ "$MODE" == "1" ]]; then ensure_git_clone "$REPO_URL" "$BRANCH" "$DEST_DIR" install_oled_service "$USER_NAME" "$DEST_DIR" + install_web_service "$USER_NAME" restart_all_services @@ -264,6 +265,7 @@ elif [[ "$MODE" == "2" ]]; then ensure_git_clone "$REPO_URL" "$BRANCH" "$DEST_DIR" install_oled_service "$USER_NAME" "$DEST_DIR" + install_web_service "$USER_NAME" restart_all_services From 3f3e9f13907d3187dce841f3777b24410892e0d6 Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:47:35 +0100 Subject: [PATCH 57/63] Update oled.py --- oled/oled.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/oled/oled.py b/oled/oled.py index f196e06..1c31723 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -68,6 +68,7 @@ class NowPlaying: @dataclass class ScreenState: + screen_off: bool = False mode: str = "main" menu_path: list = field(default_factory=list) last_input_time: float = field(default_factory=time.time) @@ -487,6 +488,13 @@ def handle_menu_action(choice: str, np: NowPlaying, state: ScreenState, settings return def on_encoder_click(np: NowPlaying, state: ScreenState, settings: Settings, client: MPDClient | None): + + # jeśli ekran jest wygaszony → klik tylko wybudza + if state.screen_off: + state.last_input_time = time.time() + state.screen_off = False + return + state.last_input_time = time.time() if state.mode == "edit": @@ -581,13 +589,18 @@ def main(): update_now_playing_from_mpd(client, np) + # soft-dimming if inactive > settings.screensaver_off_after: + state.screen_off = True with canvas(device) as draw: draw.rectangle((0, 0, device.width, device.height), outline=0, fill=0) time.sleep(0.1) continue - elif inactive > settings.screensaver_dim_after: + else: + state.screen_off = False + + if inactive > settings.screensaver_dim_after: # soft dimming (10%) try: device.contrast(int(255 * (settings.screensaver_dim_level / 100.0))) From e3632959215bdebd65a7eff8667eef1ac554e33c Mon Sep 17 00:00:00 2001 From: xtreamx2 <103985527+xtreamx2@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:01:55 +0100 Subject: [PATCH 58/63] Update config-radio.json --- config/config-radio.json | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/config/config-radio.json b/config/config-radio.json index a30e3c4..19dad37 100644 --- a/config/config-radio.json +++ b/config/config-radio.json @@ -1,40 +1,36 @@ { "stations": [ { - "name": "FIP Électro", + "name": "FIP \u00c9lectro", "url": "https://icecast.radiofrance.fr/fipelectro-midfi.mp3", "favorite": true, - "tags": ["electronic", "aac"] + "tags": [ + "electronic", + "aac" + ] }, { "name": "FIP Groove", "url": "https://icecast.radiofrance.fr/fipgroove-midfi.mp3", "favorite": false, - "tags": ["groove", "aac"] - }, - { - "name": "DI.FM Trance", - "url": "http://prem2.di.fm/trance", - "favorite": true, - "tags": ["trance", "electronic"] + "tags": [ + "groove", + "aac" + ] }, { "name": "Dance Classics Radio", "url": "http://vriezenet.nl:11051/dcflac", "favorite": false, - "tags": ["Classic Dance"] - }, - { - "name": "DI.FM Vocal Trance", - "url": "http://prem2.di.fm/vocaltrance", - "favorite": false, - "tags": ["trance", "vocal"] + "tags": [ + "Classic Dance" + ] }, { - "name": "DI.FM Progressive", - "url": "http://prem2.di.fm/progressive", + "name": "TNM Radio", + "url": "http://stream.tnm-radio.eu/ultra", "favorite": false, - "tags": ["progressive", "electronic"] + "tags": [] } ] } From 6970416f36f7393d31bd259d35bb7c6127f177c6 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Feb 2026 13:01:10 +0100 Subject: [PATCH 59/63] fix bug --- oled/oled.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oled/oled.py b/oled/oled.py index 1c31723..7a9d63e 100755 --- a/oled/oled.py +++ b/oled/oled.py @@ -410,7 +410,6 @@ def update_now_playing_from_mpd(client: MPDClient | None, np: NowPlaying): except Exception: pass - def play_station_by_name(client: MPDClient | None, name: str): if client is None: return @@ -418,6 +417,7 @@ def play_station_by_name(client: MPDClient | None, name: str): for s in stations: if s["name"] == name: try: + client.stop() client.clear() client.add(s["url"]) client.play() @@ -425,7 +425,6 @@ def play_station_by_name(client: MPDClient | None, name: str): pass break - # ================== ENKODER ================== def on_encoder_rotate(direction: int, np: NowPlaying, state: ScreenState, client: MPDClient | None): From d5f2781f886c7218a0a8fb021387798ca7673097 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Feb 2026 14:01:37 +0100 Subject: [PATCH 60/63] Add EQ --- audio/dsp.py | 90 +++++++++++++++++++++++++++ config/config-eq.json | 29 +++++++++ install.sh | 27 +++++++++ ui/eq.py | 137 ++++++++++++++++++++++++++++++++++++++++++ ui/menu.py | 112 +++++++++++++++++++++++++++++++++- 5 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 audio/dsp.py create mode 100644 config/config-eq.json create mode 100644 ui/eq.py diff --git a/audio/dsp.py b/audio/dsp.py new file mode 100644 index 0000000..4169fd7 --- /dev/null +++ b/audio/dsp.py @@ -0,0 +1,90 @@ +# streamer/audio/dsp.py + +import json +import time +import subprocess +from pathlib import Path + +BASE = Path(__file__).resolve().parents[1] +EQ_CONFIG = BASE / "config" / "config-eq.json" +CAMILLA_CONFIG = Path("/etc/camilladsp/streamer.yml") + + +def load_cfg(): + return json.loads(EQ_CONFIG.read_text()) + + +def write_camilla_yaml(text): + CAMILLA_CONFIG.write_text(text) + + +def reload_camilla(): + subprocess.run(["systemctl", "reload", "camilladsp"], check=False) + + +def render_yaml(cfg): + mode = cfg["mode"] + presets = cfg["presets"] + c2 = cfg["custom2_profiles"] + c5 = cfg["custom5_profiles"] + loud = cfg["loudness"] + + if mode == "preset": + gains = presets[cfg["selected_preset"]] + + elif mode.startswith("custom2"): + key = mode[-1] + gains = { + "60": c2[key]["bass"], + "230": c2[key]["bass"], + "910": 0, + "3600": c2[key]["treble"], + "14000": c2[key]["treble"] + } + + elif mode.startswith("custom5"): + key = mode[-1] + gains = c5[key] + + else: + gains = presets["FLAT"] + + # YAML – uproszczony, pipeline dopasujesz później + out = ["filters:"] + for f, g in gains.items(): + out.append(f" peq_{f}:") + out.append(" type: Peq") + out.append(f" freq: {float(f)}") + out.append(" q: 1.0") + out.append(f" gain: {g}") + + if loud["enabled"]: + out.append(" loud_low:") + out.append(" type: Lowshelf") + out.append(" freq: 80") + out.append(" q: 0.7") + out.append(f" gain: {loud['strength'] / 10}") + + out.append(" loud_high:") + out.append(" type: Highshelf") + out.append(" freq: 8000") + out.append(" q: 0.7") + out.append(f" gain: {loud['strength'] / 15}") + + return "\n".join(out) + "\n" + + +def main(): + last = None + while True: + cfg = load_cfg() + if cfg != last: + yaml = render_yaml(cfg) + write_camilla_yaml(yaml) + reload_camilla() + last = cfg + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/config/config-eq.json b/config/config-eq.json new file mode 100644 index 0000000..cef3e33 --- /dev/null +++ b/config/config-eq.json @@ -0,0 +1,29 @@ +{ + "mode": "preset", // preset / custom2 / custom5 + "selected_preset": "ROCK", + + "custom2_profiles": { + "A": { "bass": 0, "treble": 0 }, + "B": { "bass": 3, "treble": -2 } + }, + + "custom5_profiles": { + "A": { "60": 0, "230": 0, "910": 0, "3600": 0, "14000": 0 }, + "B": { "60": 4, "230": 2, "910": -1, "3600": 3, "14000": 5 } + }, + + "presets": { + "ROCK": { "60": 4, "230": 2, "910": -1, "3600": 3, "14000": 5 }, + "JAZZ": { "60": -2, "230": 1, "910": 3, "3600": 2, "14000": 1 }, + "DANCE": { "60": 6, "230": 4, "910": 0, "3600": 2, "14000": 3 }, + "POP": { "60": 3, "230": 1, "910": 0, "3600": 2, "14000": 4 }, + "VOCAL": { "60": -1, "230": 0, "910": 2, "3600": 4, "14000": 3 }, + "CLASSIC":{ "60": -3, "230": -1, "910": 1, "3600": 2, "14000": 1 }, + "FLAT": { "60": 0, "230": 0, "910": 0, "3600": 0, "14000": 0 } + }, + + "loudness": { + "enabled": false, + "strength": 50 + } +} diff --git a/install.sh b/install.sh index 0fc54a3..3150720 100755 --- a/install.sh +++ b/install.sh @@ -177,6 +177,29 @@ EOF log "[web] plik usługi utworzony" "OK" } +install_eq_service() { + local user_name="$1" + + log "[eq] tworzę usługę systemd..." "INFO" + + sudo tee /etc/systemd/system/streamer-eq.service >/dev/null < Date: Wed, 11 Feb 2026 14:05:34 +0100 Subject: [PATCH 61/63] Add EQ --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 3150720..293ca00 100755 --- a/install.sh +++ b/install.sh @@ -276,7 +276,7 @@ if [[ "$MODE" == "1" ]]; then install_oled_service "$USER_NAME" "$DEST_DIR" install_web_service "$USER_NAME" - install_eq_service "$user_name" + install_eq_service "$USER_NAME" restart_all_services @@ -292,7 +292,7 @@ elif [[ "$MODE" == "2" ]]; then install_oled_service "$USER_NAME" "$DEST_DIR" install_web_service "$USER_NAME" - install_eq_service "$user_name" + install_eq_service "$USER_NAME" restart_all_services From 7c9c0b01e00d7edbae1c8fec9581465df203c946 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Feb 2026 14:17:04 +0100 Subject: [PATCH 62/63] Add EQ --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 293ca00..4f4c0c5 100755 --- a/install.sh +++ b/install.sh @@ -231,7 +231,7 @@ restart_all_services() { "camilladsp.service" "oled.service" "streamer-web.service" - "eq.service" + "streamer-eq.service" ) for s in "${services[@]}"; do From 1d2a9acb2bbfc8565401e0d9a796e454a4f19dc8 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Feb 2026 15:37:48 +0100 Subject: [PATCH 63/63] Add EQ --- CHANGELOG.md | 8 ++++++++ config/config-eq.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a025832..70c4c21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Wersjonowanie zgodne z `0.xxx` (pre‑alpha). --- +## [0.011a] - 2026-02-11 +### Dodano +- EQ +- Web server na porcie 8080 + +### +- nadal problemy z Enkoderem i EQ + ## [0.010a] — 2026‑02‑10 ### Dodano - Utworzono pełną strukturę projektu (`audio/`, `ui/`, `hardware/`, `utils/`, `scripts/`). diff --git a/config/config-eq.json b/config/config-eq.json index cef3e33..e4447c9 100644 --- a/config/config-eq.json +++ b/config/config-eq.json @@ -1,5 +1,5 @@ { - "mode": "preset", // preset / custom2 / custom5 + "mode": "preset", "selected_preset": "ROCK", "custom2_profiles": {