#!/usr/bin/env bash # =========================================================================== # FOSFENO :: instalador para Raspberry Pi OS Bookworm (Raspberry Pi 4 y 5) # # Uso: bash install.sh # Raspberry Pi: instala todo # bash install.sh --laptop # portatil Linux (cualquier distro) # bash install.sh --no-projectm # omite la compilacion de projectM # bash install.sh --check # solo comprueba el sistema, no instala # =========================================================================== set -uo pipefail DIR="$(cd "$(dirname "$0")" && pwd)" source "$DIR/scripts/lib.sh" MODE="install" SKIP_PM="no" LAPTOP="no" for arg in "$@"; do case "$arg" in --no-projectm) SKIP_PM="yes" ;; --check) MODE="check" ;; --laptop) LAPTOP="yes" ;; *) echo "Opcion desconocida: $arg"; exit 1 ;; esac done # Versiones minimas requeridas MIN_PYTHON="3.9" MIN_NODE="16" MIN_NPM="8" MIN_CMAKE="3.21" printf '%s\n' "$C_BLD===================================================" printf '%s\n' " FOSFENO :: instalador ($DIR)" printf '%s\n' "===================================================$C_RST" # --------------------------------------------------------------------------- # 1. Comprobacion del hardware y del sistema operativo # --------------------------------------------------------------------------- log_step "[1/9] Comprobando el sistema" if [ -r /etc/os-release ]; then . /etc/os-release log_info "Sistema: ${PRETTY_NAME:-desconocido}" fi if [ "$LAPTOP" = "yes" ]; then log_ok "Modo portatil (Linux de escritorio)" else MODELO="$(pi_model)" log_info "Modelo detectado: $MODELO" case "$MODELO" in *"Raspberry Pi 5"*) log_ok "Raspberry Pi 5 (soportada)" ;; *"Raspberry Pi 4"*) log_ok "Raspberry Pi 4 (soportada)" ;; *"Raspberry Pi"*) warn "Raspberry Pi distinta de 4/5: puede funcionar pero sin garantias" ;; *) warn "No parece una Raspberry Pi. Si es un portatil, usa: bash install.sh --laptop" ;; esac if [ "${VERSION_CODENAME:-}" != "bookworm" ]; then warn "Se recomienda Raspberry Pi OS Bookworm (detectado: ${VERSION_CODENAME:-?})" fi fi # --------------------------------------------------------------------------- # Modo --check: solo verifica lo que ya esta instalado y termina # --------------------------------------------------------------------------- if [ "$MODE" = "check" ]; then log_step "Comprobando herramientas ya instaladas" require_version "Python" "python3 --version" "$MIN_PYTHON" || true require_version "Node.js" "node --version" "$MIN_NODE" || true require_version "npm" "npm --version" "$MIN_NPM" || true require_version "CMake" "cmake --version" "$MIN_CMAKE" || true if need_cmd chromium-browser || need_cmd chromium; then log_ok "Chromium presente" else log_fail "Chromium no encontrado" fi need_cmd git && log_ok "git presente" || log_fail "git no encontrado" need_cmd projectMSDL && log_ok "projectM (projectMSDL) presente" \ || log_warn "projectM no instalado (opcional)" printf '\n' [ "$FOSFENO_WARNINGS" -eq 0 ] && log_ok "Comprobacion terminada sin avisos" \ || log_warn "Comprobacion terminada con $FOSFENO_WARNINGS aviso(s)" exit 0 fi # --------------------------------------------------------------------------- # 2. Paquetes del sistema # --------------------------------------------------------------------------- # Instala las dependencias en un portatil. Soporta apt, dnf, pacman y zypper, # de modo que FOSFENO corre en Debian/Ubuntu/Mint, Fedora, Arch/Manjaro y # openSUSE. En portatil solo hace falta un nucleo pequeno (Python, Node, git # y Chromium): los motores de visuales son web y no necesitan nada mas. install_laptop_deps() { local mgr="" if need_cmd apt-get; then mgr="apt" elif need_cmd dnf; then mgr="dnf" elif need_cmd pacman; then mgr="pacman" elif need_cmd zypper; then mgr="zypper" else log_fail "No reconozco el gestor de paquetes de tu distribucion." log_fail "Instala a mano y reintenta: python3 (con venv), nodejs, npm," log_fail "git y chromium." return 1 fi log_info "Gestor de paquetes detectado: $mgr" # --- Python y git --- case "$mgr" in apt) sudo apt-get update sudo apt-get install -y python3 python3-venv python3-pip git ;; dnf) sudo dnf install -y python3 python3-pip git ;; pacman) sudo pacman -Sy --needed --noconfirm python python-pip git ;; zypper) sudo zypper --non-interactive install python3 python3-pip git ;; esac || { log_fail "no se pudieron instalar Python y git"; return 1; } log_ok "Python y git instalados" # --- Node.js y npm: solo si faltan (respeta NodeSource, nvm, etc.) --- if need_cmd node && need_cmd npm; then log_ok "Node.js y npm ya estaban ($(node --version 2>/dev/null))" else case "$mgr" in apt) sudo apt-get install -y nodejs npm ;; dnf) sudo dnf install -y nodejs npm ;; pacman) sudo pacman -S --needed --noconfirm nodejs npm ;; zypper) sudo zypper --non-interactive install nodejs npm ;; esac need_cmd node && need_cmd npm \ && log_ok "Node.js y npm instalados" \ || warn "no se pudo instalar Node.js; instalalo a mano y reintenta" fi # --- Chromium --- if need_cmd chromium || need_cmd chromium-browser || need_cmd google-chrome; then log_ok "Chromium ya estaba instalado" else case "$mgr" in apt) sudo apt-get install -y chromium 2>/dev/null \ || sudo apt-get install -y chromium-browser ;; dnf) sudo dnf install -y chromium ;; pacman) sudo pacman -S --needed --noconfirm chromium ;; zypper) sudo zypper --non-interactive install chromium ;; esac need_cmd chromium || need_cmd chromium-browser \ && log_ok "Chromium instalado" \ || warn "no se pudo instalar Chromium; instalalo a mano (chromium o Chrome)" fi # --- avahi: opcional, permite llegar al panel por nombre .local --- case "$mgr" in apt) sudo apt-get install -y avahi-daemon 2>/dev/null || true ;; dnf) sudo dnf install -y avahi 2>/dev/null || true ;; pacman) sudo pacman -S --needed --noconfirm avahi 2>/dev/null || true ;; zypper) sudo zypper --non-interactive install avahi 2>/dev/null || true ;; esac return 0 } log_step "[2/9] Instalando dependencias del sistema" if [ "$LAPTOP" = "yes" ]; then install_laptop_deps || { log_fail "Faltan dependencias; aborto."; exit 1; } else sudo apt-get update if sudo apt-get install -y \ python3 python3-venv python3-pip \ git cmake build-essential pkg-config \ libsdl2-dev libgles2-mesa-dev mesa-common-dev libglm-dev libpoco-dev \ pulseaudio-utils \ v4l-utils \ avahi-daemon \ xdotool unclutter; then log_ok "Paquetes apt instalados" else log_fail "Fallo al instalar paquetes apt" exit 1 fi # Node.js y npm: si ya estan (p.ej. NodeSource) no se tocan, porque el # paquete 'npm' de Debian choca con el 'nodejs' de NodeSource. if need_cmd node && need_cmd npm; then log_ok "Node.js y npm ya estaban instalados ($(node --version 2>/dev/null))" elif need_cmd node; then warn "Node.js esta, pero falta npm; instalalo con el mismo metodo que node" elif sudo apt-get install -y nodejs npm 2>/dev/null; then log_ok "Node.js y npm instalados" else warn "no se pudo instalar Node.js; instalalo a mano y reintenta" fi # Chromium: 'chromium' o 'chromium-browser' segun la version del sistema. if need_cmd chromium || need_cmd chromium-browser; then log_ok "Chromium ya estaba instalado" elif sudo apt-get install -y chromium 2>/dev/null; then log_ok "Chromium instalado (paquete 'chromium')" elif sudo apt-get install -y chromium-browser 2>/dev/null; then log_ok "Chromium instalado (paquete 'chromium-browser')" else warn "no se pudo instalar Chromium automaticamente; instalalo a mano" fi fi # --------------------------------------------------------------------------- # 3. Verificacion de versiones de las herramientas # --------------------------------------------------------------------------- log_step "[3/9] Verificando versiones de las herramientas" require_version "Python" "python3 --version" "$MIN_PYTHON" \ || { log_fail "Python demasiado antiguo; aborto"; exit 1; } require_version "Node.js" "node --version" "$MIN_NODE" \ || warn "Node.js antiguo: 'npm install' podria fallar" require_version "npm" "npm --version" "$MIN_NPM" || true if [ "$SKIP_PM" = "no" ] && [ "$LAPTOP" = "no" ]; then require_version "CMake" "cmake --version" "$MIN_CMAKE" \ || { warn "CMake antiguo: projectM no se compilara"; SKIP_PM="yes"; } fi # --------------------------------------------------------------------------- # 4. Entorno Python # --------------------------------------------------------------------------- log_step "[4/9] Creando el entorno virtual de Python" python3 -m venv "$DIR/.venv" "$DIR/.venv/bin/pip" install --quiet --upgrade pip "$DIR/.venv/bin/pip" install --quiet -r "$DIR/backend/requirements.txt" if "$DIR/.venv/bin/python3" -c "import flask, flask_socketio" 2>/dev/null; then log_ok "Entorno Python listo (flask, flask-socketio)" else log_fail "Las dependencias de Python no se importan correctamente" exit 1 fi # --------------------------------------------------------------------------- # 5. Librerias web (Butterchurn, Hydra, Socket.IO, CodeMirror) # --------------------------------------------------------------------------- log_step "[5/9] Descargando librerias de visuales (npm)" cd "$DIR/web" if npm install --no-audit --no-fund --loglevel=error; then log_ok "Paquetes npm descargados" else log_fail "Fallo en 'npm install'" exit 1 fi mkdir -p "$DIR/web/lib/codemirror" # Copia un fichero y comprueba que existe y no esta vacio copy_lib() { # copy_lib if cp "$1" "$2" 2>/dev/null && [ -s "$2" ]; then log_ok "$3" else warn "no se pudo copiar: $3 ($1)" fi } # Busca un fichero por nombre dentro de un paquete y lo copia. Es robusto a # que el paquete cambie de carpeta (dist/, lib/, build/) entre versiones. find_copy() { # find_copy local f f="$(find "node_modules/$1" -name "$2" -type f 2>/dev/null | head -n1)" if [ -n "$f" ] && cp "$f" "$3" 2>/dev/null && [ -s "$3" ]; then log_ok "$4" else warn "no se encontro $4 ($2 dentro de node_modules/$1)" fi } find_copy butterchurn butterchurn.min.js lib/butterchurn.min.js "Butterchurn" find_copy butterchurn-presets butterchurnPresets.min.js lib/butterchurn-presets.min.js "Presets de Butterchurn" copy_lib node_modules/socket.io-client/dist/socket.io.min.js \ lib/socket.io.min.js "Socket.IO" copy_lib node_modules/qrcode-generator/qrcode.js \ lib/qrcode.js "Generador de codigo QR" copy_lib node_modules/codemirror/lib/codemirror.js \ lib/codemirror/codemirror.js "CodeMirror (editor)" copy_lib node_modules/codemirror/lib/codemirror.css \ lib/codemirror/codemirror.css "CodeMirror (estilos)" copy_lib node_modules/codemirror/mode/javascript/javascript.js \ lib/codemirror/javascript.js "CodeMirror (modo JavaScript)" copy_lib node_modules/codemirror/mode/clike/clike.js \ lib/codemirror/clike.js "CodeMirror (modo GLSL)" copy_lib node_modules/codemirror/theme/material-darker.css \ lib/codemirror/material-darker.css "CodeMirror (tema)" # Hydra no siempre ubica el build en el mismo sitio: probamos varias rutas if cp node_modules/hydra-synth/dist/hydra-synth.js lib/hydra-synth.js 2>/dev/null && [ -s lib/hydra-synth.js ]; then log_ok "Hydra" elif cp node_modules/hydra-synth/build/hydra-synth.js lib/hydra-synth.js 2>/dev/null && [ -s lib/hydra-synth.js ]; then log_ok "Hydra (build/)" else warn "no se encontro el build de hydra-synth en node_modules; revisa la ruta" fi cd "$DIR" # --------------------------------------------------------------------------- # 6. projectM nativo (compilado desde fuente) # --------------------------------------------------------------------------- log_step "[6/9] projectM (visualizador nativo)" if command -v projectMSDL >/dev/null 2>&1; then log_ok "projectM ya estaba instalado ($(command -v projectMSDL))" elif [ "$LAPTOP" = "yes" ]; then log_info "projectM nativo no instalado (es opcional)." log_info "Los motores Butterchurn, Hydra, Shaders y Mezclador funcionan sin el." log_info "Si lo quieres, instala el paquete 'projectm' de tu distribucion." elif [ "$SKIP_PM" = "yes" ]; then log_info "projectM omitido" else log_info "Compilando projectM desde fuente (puede tardar 10-20 min en una Pi)..." if bash "$DIR/scripts/build-projectm.sh"; then command -v projectMSDL >/dev/null 2>&1 \ && log_ok "projectM compilado e instalado" \ || warn "projectM compilo pero 'projectMSDL' no esta en el PATH" else warn "projectM no se pudo compilar; FOSFENO seguira con Butterchurn, Hydra y Shaders" fi fi # --------------------------------------------------------------------------- # 7. Presets de projectM y carpeta de videos # --------------------------------------------------------------------------- log_step "[7/9] Recursos (presets de projectM, carpeta de videos)" mkdir -p "$DIR/data/presets-projectm" "$DIR/data/videos" if [ -z "$(ls -A "$DIR/data/presets-projectm" 2>/dev/null)" ]; then if git clone --depth 1 \ https://github.com/projectM-visualizer/presets-cream-of-the-crop.git \ "$DIR/data/presets-projectm" 2>/dev/null; then N=$(find "$DIR/data/presets-projectm" -name '*.milk' | wc -l) log_ok "Presets de projectM descargados ($N presets)" else warn "no se pudieron descargar los presets de projectM" fi else log_ok "Presets de projectM ya presentes" fi log_info "Copia tus clips .mp4 en: $DIR/data/videos/" # --------------------------------------------------------------------------- # 8. Arranque automatico y permisos # --------------------------------------------------------------------------- log_step "[8/9] Arranque automatico y permisos" chmod +x "$DIR/scripts/"*.sh "$DIR/install.sh" "$DIR/uninstall.sh" 2>/dev/null [ -f "$DIR/fosfeno" ] && chmod +x "$DIR/fosfeno" HOSTNAME_WANT="$(hostname)" if [ "$LAPTOP" = "yes" ]; then log_info "Modo portatil: sin arranque automatico ni cambios en el sistema." log_info "FOSFENO se lanza a mano con ./fosfeno cuando quieras usarlo." else mkdir -p "$HOME/.config/autostart" if sed "s#__DIR__#$DIR#g" "$DIR/scripts/fosfeno-autostart.desktop" \ > "$HOME/.config/autostart/fosfeno.desktop"; then log_ok "Arranque automatico configurado (~/.config/autostart/fosfeno.desktop)" fi # Nombre de red fijo: deja el panel accesible en http://.local/ HOSTNAME_WANT="$(python3 -c "import json;print(json.load(open('$DIR/config.json')).get('network',{}).get('hostname','fosfeno'))" 2>/dev/null || echo fosfeno)" if [ "$(hostname)" != "$HOSTNAME_WANT" ]; then sudo hostnamectl set-hostname "$HOSTNAME_WANT" 2>/dev/null || true if grep -q "^127.0.1.1" /etc/hosts; then sudo sed -i "s/^127.0.1.1.*/127.0.1.1\t$HOSTNAME_WANT/" /etc/hosts else echo -e "127.0.1.1\t$HOSTNAME_WANT" | sudo tee -a /etc/hosts >/dev/null fi log_ok "Nombre de red puesto a '$HOSTNAME_WANT' (panel en http://$HOSTNAME_WANT.local/)" else log_ok "Nombre de red: $HOSTNAME_WANT" fi if echo "$USER ALL=(ALL) NOPASSWD: /sbin/reboot, /sbin/poweroff" \ | sudo tee /etc/sudoers.d/fosfeno >/dev/null \ && sudo chmod 440 /etc/sudoers.d/fosfeno; then log_ok "Permisos de reinicio/apagado configurados" fi fi # --------------------------------------------------------------------------- # 9. Acceso de red # --------------------------------------------------------------------------- log_step "[9/9] Acceso de red" if [ "$LAPTOP" = "yes" ]; then log_info "Modo portatil: el panel usara el puerto 8080, no hace falta nada mas." else PYBIN="$(readlink -f "$DIR/.venv/bin/python3")" if sudo setcap 'cap_net_bind_service=+ep' "$PYBIN" 2>/dev/null; then log_ok "Puerto 80 habilitado" else warn "no se pudo habilitar el puerto 80; cambia 'server.port' a 8080 en config.json" fi fi # --------------------------------------------------------------------------- # Resumen final # --------------------------------------------------------------------------- printf '\n%s\n' "$C_BLD===================================================" if [ "$FOSFENO_WARNINGS" -eq 0 ]; then log_ok "FOSFENO instalado correctamente, sin avisos." else log_warn "FOSFENO instalado con $FOSFENO_WARNINGS aviso(s) (revisa arriba)." fi printf '%s\n' "===================================================$C_RST" if [ "$LAPTOP" = "yes" ]; then cat < System Options -> Boot / Auto Login -> Desktop Autologin 2. Reinicia: sudo reboot Como entrar en el panel de control: - Al arrancar, el proyector muestra un codigo QR y la direccion. Escanea el QR con el movil y se abre el panel. Mas facil imposible. - Si prefieres escribirla: http://$HOSTNAME_WANT.local/ - El movil debe estar en la misma red (WiFi o cable) que la Raspberry. Para volver a comprobar el sistema sin reinstalar: bash install.sh --check EOF fi