From 209f81aa3d7d295d833a085da0cfe1818a09521d Mon Sep 17 00:00:00 2001 From: jbperrin Date: Thu, 16 Jul 2026 00:55:29 +0200 Subject: [PATCH] first commit --- .gitea/workflows/deploy.yml | 112 +++ .gitignore | 42 + Opencode/config/gpio-notes.conf | 53 ++ Opencode/config/nodered-flows-backup.json | 5 + Opencode/config/photobooth-config-backup.json | 690 ++++++++++++++ .../config/photobooth-config-optimized.json | 548 +++++++++++ Opencode/config/zoraxy-notes.conf | 80 ++ Opencode/docs/ANALYSE-CONFIG.md | 87 ++ Opencode/docs/ARCHITECTURE.md | 222 +++++ Opencode/docs/GUIDE-COMPLET.md | 777 ++++++++++++++++ Opencode/docs/GUIDE-INSTALLATION.md | 872 ++++++++++++++++++ Opencode/scripts/display_actions.py | 73 ++ Opencode/scripts/health-check.sh | 242 +++++ Opencode/scripts/maintenance.sh | 143 +++ Opencode/scripts/optimize-desktop.sh | 252 +++++ Opencode/scripts/optimize-system.sh | 374 ++++++++ Opencode/scripts/photobooth-config.sh | 761 +++++++++++++++ Opencode/scripts/ram-watchdog.sh | 68 ++ Opencode/scripts/script_print.sh | 138 +++ Opencode/scripts/setup-photobooth.sh | 276 ++++++ Opencode/scripts/setup-printer.sh | 321 +++++++ Opencode/scripts/switch-quality.sh | 187 ++++ Opencode/systemd/photobooth-app.service | 63 ++ .../systemd/photobooth-maintenance.service | 14 + Opencode/systemd/photobooth-maintenance.timer | 16 + Opencode/systemd/photobooth-watchdog.service | 25 + Opencode/systemd/photobooth-watchdog.timer | 17 + README.md | 93 ++ backend/__init__.py | 0 backend/api/__init__.py | 0 backend/api/actions_api.py | 121 +++ backend/api/admin_api.py | 130 +++ backend/api/admin_gallery_api.py | 122 +++ backend/api/gallery.py | 78 ++ backend/api/leds_api.py | 51 + backend/api/print_api.py | 114 +++ backend/api/system_api.py | 52 ++ backend/api/webhooks.py | 92 ++ backend/models/__init__.py | 0 backend/services/__init__.py | 0 backend/services/button_service.py | 219 +++++ backend/services/config_service.py | 183 ++++ backend/services/led_service.py | 308 +++++++ backend/services/photobooth_service.py | 129 +++ backend/services/printer_service.py | 242 +++++ backend/services/system_service.py | 117 +++ backend/utils/__init__.py | 0 backend/utils/auth.py | 19 + backend/utils/ws_manager.py | 45 + config/settings.yaml | 96 ++ data/.gitkeep | 0 docs/CI-CD-SETUP.md | 121 +++ docs/JH-Photomaton-Guide-Installation.docx | Bin 0 -> 32963 bytes docs/MIGRATION-NODE-RED.md | 105 +++ frontend/static/css/main.css | 491 ++++++++++ frontend/templates/admin/actions.html | 225 +++++ frontend/templates/admin/dashboard.html | 281 ++++++ frontend/templates/admin/gallery.html | 151 +++ frontend/templates/admin/login.html | 45 + frontend/templates/admin/print.html | 190 ++++ frontend/templates/base.html | 94 ++ frontend/templates/public/gallery.html | 119 +++ main.py | 190 ++++ photobooth-app/config/config.json | 548 +++++++++++ .../config/config.json_backup-20260626-114238 | 480 ++++++++++ .../config/config.json_backup-20260709-174236 | 480 ++++++++++ .../config/config.json_backup-20260709-174328 | 514 +++++++++++ .../config/config.json_backup-20260709-174548 | 514 +++++++++++ .../config/config.json_backup-20260709-174642 | 514 +++++++++++ .../config/config.json_backup-20260709-174655 | 548 +++++++++++ .../config/config.json_backup-20260709-175626 | 548 +++++++++++ .../config/config.json_backup-20260709-181113 | 548 +++++++++++ .../config/config.json_backup-20260709-183641 | 548 +++++++++++ .../config/config.json_backup-20260709-183701 | 548 +++++++++++ photobooth-app/config/plugin_commander.json | 88 ++ ...ugin_commander.json_backup-20260427-120604 | 59 ++ ...ugin_commander.json_backup-20260427-120614 | 59 ++ ...ugin_commander.json_backup-20260709-175723 | 59 ++ ...ugin_commander.json_backup-20260709-181059 | 59 ++ ...ugin_commander.json_backup-20260709-181516 | 59 ++ ...ugin_commander.json_backup-20260709-181521 | 59 ++ ...ugin_commander.json_backup-20260709-182004 | 59 ++ ...ugin_commander.json_backup-20260709-182052 | 55 ++ ...ugin_commander.json_backup-20260709-182115 | 88 ++ ...ugin_commander.json_backup-20260709-182616 | 88 ++ .../config/plugin_commander_jh.json | 67 ++ .../config/plugin_filter_pilgram2.json | 45 + ...ilter_pilgram2.json_backup-20260427-120540 | 45 + photobooth-app/config/plugin_gpiolights.json | 15 + .../config/plugin_synchronizer.json | 27 + ...n_synchronizer.json_backup-20260427-120639 | 27 + .../config/plugin_synchronizer_rclone.json | 33 + photobooth-app/config/plugin_wled.json | 4 + .../plugin_wled.json_backup-20260412-154117 | 4 + requirements.txt | 17 + scripts/install.sh | 105 +++ scripts/start.sh | 7 + scripts/sudoers-jh-photomaton | 17 + scripts/update.sh | 38 + systemd/jh-photomaton.service | 28 + 100 files changed, 17682 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 Opencode/config/gpio-notes.conf create mode 100644 Opencode/config/nodered-flows-backup.json create mode 100644 Opencode/config/photobooth-config-backup.json create mode 100644 Opencode/config/photobooth-config-optimized.json create mode 100644 Opencode/config/zoraxy-notes.conf create mode 100644 Opencode/docs/ANALYSE-CONFIG.md create mode 100644 Opencode/docs/ARCHITECTURE.md create mode 100644 Opencode/docs/GUIDE-COMPLET.md create mode 100644 Opencode/docs/GUIDE-INSTALLATION.md create mode 100644 Opencode/scripts/display_actions.py create mode 100644 Opencode/scripts/health-check.sh create mode 100644 Opencode/scripts/maintenance.sh create mode 100644 Opencode/scripts/optimize-desktop.sh create mode 100644 Opencode/scripts/optimize-system.sh create mode 100644 Opencode/scripts/photobooth-config.sh create mode 100644 Opencode/scripts/ram-watchdog.sh create mode 100644 Opencode/scripts/script_print.sh create mode 100644 Opencode/scripts/setup-photobooth.sh create mode 100644 Opencode/scripts/setup-printer.sh create mode 100644 Opencode/scripts/switch-quality.sh create mode 100644 Opencode/systemd/photobooth-app.service create mode 100644 Opencode/systemd/photobooth-maintenance.service create mode 100644 Opencode/systemd/photobooth-maintenance.timer create mode 100644 Opencode/systemd/photobooth-watchdog.service create mode 100644 Opencode/systemd/photobooth-watchdog.timer create mode 100644 README.md create mode 100644 backend/__init__.py create mode 100644 backend/api/__init__.py create mode 100644 backend/api/actions_api.py create mode 100644 backend/api/admin_api.py create mode 100644 backend/api/admin_gallery_api.py create mode 100644 backend/api/gallery.py create mode 100644 backend/api/leds_api.py create mode 100644 backend/api/print_api.py create mode 100644 backend/api/system_api.py create mode 100644 backend/api/webhooks.py create mode 100644 backend/models/__init__.py create mode 100644 backend/services/__init__.py create mode 100644 backend/services/button_service.py create mode 100644 backend/services/config_service.py create mode 100644 backend/services/led_service.py create mode 100644 backend/services/photobooth_service.py create mode 100644 backend/services/printer_service.py create mode 100644 backend/services/system_service.py create mode 100644 backend/utils/__init__.py create mode 100644 backend/utils/auth.py create mode 100644 backend/utils/ws_manager.py create mode 100644 config/settings.yaml create mode 100644 data/.gitkeep create mode 100644 docs/CI-CD-SETUP.md create mode 100644 docs/JH-Photomaton-Guide-Installation.docx create mode 100644 docs/MIGRATION-NODE-RED.md create mode 100644 frontend/static/css/main.css create mode 100644 frontend/templates/admin/actions.html create mode 100644 frontend/templates/admin/dashboard.html create mode 100644 frontend/templates/admin/gallery.html create mode 100644 frontend/templates/admin/login.html create mode 100644 frontend/templates/admin/print.html create mode 100644 frontend/templates/base.html create mode 100644 frontend/templates/public/gallery.html create mode 100644 main.py create mode 100644 photobooth-app/config/config.json create mode 100644 photobooth-app/config/config.json_backup-20260626-114238 create mode 100644 photobooth-app/config/config.json_backup-20260709-174236 create mode 100644 photobooth-app/config/config.json_backup-20260709-174328 create mode 100644 photobooth-app/config/config.json_backup-20260709-174548 create mode 100644 photobooth-app/config/config.json_backup-20260709-174642 create mode 100644 photobooth-app/config/config.json_backup-20260709-174655 create mode 100644 photobooth-app/config/config.json_backup-20260709-175626 create mode 100644 photobooth-app/config/config.json_backup-20260709-181113 create mode 100644 photobooth-app/config/config.json_backup-20260709-183641 create mode 100644 photobooth-app/config/config.json_backup-20260709-183701 create mode 100644 photobooth-app/config/plugin_commander.json create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260427-120604 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260427-120614 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-175723 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-181059 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-181516 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-181521 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-182004 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-182052 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-182115 create mode 100644 photobooth-app/config/plugin_commander.json_backup-20260709-182616 create mode 100644 photobooth-app/config/plugin_commander_jh.json create mode 100644 photobooth-app/config/plugin_filter_pilgram2.json create mode 100644 photobooth-app/config/plugin_filter_pilgram2.json_backup-20260427-120540 create mode 100644 photobooth-app/config/plugin_gpiolights.json create mode 100644 photobooth-app/config/plugin_synchronizer.json create mode 100644 photobooth-app/config/plugin_synchronizer.json_backup-20260427-120639 create mode 100644 photobooth-app/config/plugin_synchronizer_rclone.json create mode 100644 photobooth-app/config/plugin_wled.json create mode 100644 photobooth-app/config/plugin_wled.json_backup-20260412-154117 create mode 100644 requirements.txt create mode 100644 scripts/install.sh create mode 100644 scripts/start.sh create mode 100644 scripts/sudoers-jh-photomaton create mode 100644 scripts/update.sh create mode 100644 systemd/jh-photomaton.service diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..b6975ee --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,112 @@ +# ══════════════════════════════════════════════════════════════════════════════ +# JH Photomaton — CI/CD Gitea Actions +# Déploiement automatique sur le Raspberry Pi via SSH +# +# SECRETS À CONFIGURER dans Gitea (Settings → Secrets) : +# PI_SSH_HOST → IP ou hostname du Pi (ex: 192.168.1.42 ou PiPhotobooth.local) +# PI_SSH_USER → pi +# PI_SSH_KEY → Clé privée SSH (voir docs/CI-CD-SETUP.md) +# PI_SSH_PORT → 22 (optionnel, 22 par défaut) +# +# PRÉREQUIS sur le Pi : +# - Le dépôt est cloné dans /home/pi/jh-photomaton +# - La règle sudoers est en place (scripts/sudoers-jh-photomaton) +# - Un runner Gitea Actions est configuré (voir docs/CI-CD-SETUP.md) +# ══════════════════════════════════════════════════════════════════════════════ + +name: 🚀 Deploy — JH Photomaton + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + force_install: + description: 'Forcer la réinstallation complète (install.sh)' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' + +jobs: + # ──────────────────────────────────────────────────────────────────────────── + # Job 1 : Lint / vérification rapide avant deploy + # ──────────────────────────────────────────────────────────────────────────── + lint: + name: 🔍 Vérification + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Installer les dépendances (sans paquets Pi-spécifiques) + run: | + pip install fastapi uvicorn jinja2 python-multipart itsdangerous httpx \ + psutil aiosqlite pyyaml aiofiles pillow qrcode --quiet + + - name: Vérification syntaxe Python + run: | + python -m py_compile main.py + python -m py_compile backend/services/config_service.py + python -m py_compile backend/services/led_service.py + python -m py_compile backend/services/button_service.py + python -m py_compile backend/services/photobooth_service.py + python -m py_compile backend/services/printer_service.py + python -m py_compile backend/services/system_service.py + echo "✅ Syntaxe Python OK" + + # ──────────────────────────────────────────────────────────────────────────── + # Job 2 : Déploiement SSH sur le Raspberry Pi + # ──────────────────────────────────────────────────────────────────────────── + deploy: + name: 🍓 Deploy sur le Pi + runs-on: ubuntu-latest + needs: lint + + steps: + - name: 🔑 Déploiement via SSH (mise à jour standard) + if: github.event.inputs.force_install != 'true' + uses: https://gitea.com/actions/appleboy-ssh-action@master + with: + host: ${{ secrets.PI_SSH_HOST }} + username: ${{ secrets.PI_SSH_USER }} + key: ${{ secrets.PI_SSH_KEY }} + port: ${{ secrets.PI_SSH_PORT || '22' }} + script_stop: true + script: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " JH Photomaton — Mise à jour" + echo " $(date '+%Y-%m-%d %H:%M:%S')" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + bash /home/pi/jh-photomaton/scripts/update.sh + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "✅ Déploiement terminé !" + + - name: 🔧 Installation complète (force_install=true) + if: github.event.inputs.force_install == 'true' + uses: https://gitea.com/actions/appleboy-ssh-action@master + with: + host: ${{ secrets.PI_SSH_HOST }} + username: ${{ secrets.PI_SSH_USER }} + key: ${{ secrets.PI_SSH_KEY }} + port: ${{ secrets.PI_SSH_PORT || '22' }} + script_stop: true + script: | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " JH Photomaton — Installation complète" + echo " $(date '+%Y-%m-%d %H:%M:%S')" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + cd /home/pi/jh-photomaton + git stash + git pull origin main + git stash pop || true + sudo bash /home/pi/jh-photomaton/scripts/install.sh + echo "✅ Installation complète terminée !" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..792839e --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +.venv/ +venv/ +*.egg-info/ + +# Base de données locale +data/*.db +data/*.db-shm +data/*.db-wal + +# Logs +*.log +logs/ + +# Data (ne pas versionner les photos) +data/photos/ +data/thumbnails/ +data/sessions/ +data/logs/ + +# Config locale (contient éventuellement des secrets) +config/settings.local.yaml + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp + +# Node (script de génération doc) +node_modules/ +package-lock.json +package.json + +# Sauvegardes config photobooth-app générées automatiquement +photobooth-app/config/*_backup_*.json diff --git a/Opencode/config/gpio-notes.conf b/Opencode/config/gpio-notes.conf new file mode 100644 index 0000000..2412eea --- /dev/null +++ b/Opencode/config/gpio-notes.conf @@ -0,0 +1,53 @@ +# ============================================================================= +# Notes GPIO - Simplification du declenchement bouton +# ============================================================================= +# +# SITUATION ACTUELLE: +# Bouton physique -> Relay 12V -> GPIO -> Node-RED -> API photobooth-app +# +# PROBLEME: +# - Node-RED consomme ~150-300 Mo de RAM (Node.js) +# - Un composant intermediaire de plus = un point de panne de plus +# - Latence supplementaire +# +# SOLUTION RECOMMANDEE: +# photobooth-app supporte NATIVEMENT le GPIO Raspberry Pi via gpiozero. +# Tu peux brancher le bouton directement sur un GPIO sans relay ni Node-RED. +# +# OPTION A: GPIO DIRECT (recommande - sans relay) +# - Brancher le bouton entre GND et GPIO 27 (pin physique 13) +# - photobooth-app active les resistances pull-up internes +# - Bouton normalement ouvert (NO) +# - Schema: GND ---[BOUTON]--- GPIO27 +# +# OPTION B: GARDER LE RELAY 12V (si le bouton necessite du 12V) +# - Le relay sort sur un GPIO du Pi +# - Configurer ce GPIO dans photobooth-app directement +# - PAS BESOIN de Node-RED pour ca +# +# CONFIGURATION DANS PHOTOBOOTH-APP: +# 1. Ouvrir l'admin: http://localhost:8083/admin +# 2. Aller dans: CONFIGURATION -> hardwareinput +# 3. Activer "GPIO" +# 4. Dans Actions, configurer: +# - Picture trigger: GPIO 27 (ou le GPIO connecte a ton relay) +# - Collage trigger: GPIO 22 (optionnel) +# - Print trigger: GPIO 23 (optionnel) +# - Shutdown: GPIO 17 (hold 2 secondes) +# +# GPIO PAR DEFAUT DE PHOTOBOOTH-APP: +# | Fonction | GPIO | Pin physique | Type | +# |-------------|------|-------------|------------| +# | Photo | 27 | 13 | Appui | +# | Collage | 22 | 15 | Appui | +# | Animation | 24 | 18 | Appui | +# | Video | 25 | 22 | Appui | +# | Impression | 23 | 16 | Appui | +# | Shutdown | 17 | 11 | Maintien 2s| +# | Reboot | 18 | 12 | Maintien 2s| +# +# APRES MIGRATION: +# - Desactiver Node-RED: sudo systemctl disable nodered.service +# - Gain: ~150-300 Mo de RAM liberee ! +# +# ============================================================================= diff --git a/Opencode/config/nodered-flows-backup.json b/Opencode/config/nodered-flows-backup.json new file mode 100644 index 0000000..f098bcf --- /dev/null +++ b/Opencode/config/nodered-flows-backup.json @@ -0,0 +1,5 @@ +[ + { "_comment": "Backup du flow Node-RED complet - voir le fichier original pour le contenu" }, + { "_comment": "Sauvegarde faite le 2026-06-24" }, + { "_comment": "Ce fichier est trop volumineux pour etre inclus ici - copier directement depuis le Pi" } +] diff --git a/Opencode/config/photobooth-config-backup.json b/Opencode/config/photobooth-config-backup.json new file mode 100644 index 0000000..396a7c2 --- /dev/null +++ b/Opencode/config/photobooth-config-backup.json @@ -0,0 +1,690 @@ +{ + "common": { + "admin_password": "xxxxxxxxxxx", + "logging_level": "DEBUG", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "background stars", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "white", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": false, + "img_frame_file": "userdata/demoassets/frames/frame_image_photobooth-app.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Star back", + "icon": "photo_camera", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Normal image", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#2d68c4", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": false, + "img_frame_file": "userdata/demoassets/frames/frame_image_photobooth-app.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Normal", + "icon": "photo_camera", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "LULU", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/LSDW/lulu-versaire2.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "LULU", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "LULU - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/LSDW/lulu-versaire2.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "LULU - Etoile", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile - copy", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - nuitée étoilée new", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Printing copies", + "handles_images_only": true, + "processing": { + "share_command": "echo {filename} media_type={media_type} action_config_name={action_config_name} copies={copies}", + "ask_user_for_parameter_input": true, + "parameters_dialog_caption": "How many copies?", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "3" + } + ], + "share_blocked_time": 3, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Print Copies", + "icon": "print", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Mailing action", + "handles_images_only": false, + "processing": { + "share_command": "echo {filename} media_type={media_type} action_config_name={action_config_name} to mail {mail}", + "ask_user_for_parameter_input": true, + "parameters_dialog_caption": "E-Mail your image...", + "parameters_dialog_action_icon": "mail", + "parameters_dialog_action_label": "Send", + "parameters": [ + { + "key": "mail", + "label": "E-Mail address", + "ui_type": "input", + "default": "me@mgineer85.de", + "valid_min": "5", + "valid_max": "128" + } + ], + "share_blocked_time": 3, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Send Mail", + "icon": "mail", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Pouet", + "handles_images_only": true, + "processing": { + "share_command": "echo OK && exit 0", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 0, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "pouet", + "icon": "", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": false, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/hopnbloc/frames/hnb cadre final.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
\tSi vous utilisez le photomaton, vous nous autorisez à utiliser les photos.
\tCe photomaton est proposé en libre-service par 'Les Sapins Du Web'.
\tLes photos peuvent être imprimées sur demande et sur dons libre. (2 Photos maximum par personne)
\tMerci de votre compréhension. \t
Compét Hop'N Bloc !!!!!
\"LSDW
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "ThisIsTheDefaultSecret", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} diff --git a/Opencode/config/photobooth-config-optimized.json b/Opencode/config/photobooth-config-optimized.json new file mode 100644 index 0000000..46dcea6 --- /dev/null +++ b/Opencode/config/photobooth-config-optimized.json @@ -0,0 +1,548 @@ +{ + "_comment": "Config optimisee pour RPi4 2Go - Photomaton Les Sapins Du Web", + "_comment2": "Changements vs config originale marques avec OPTI:", + + "common": { + "admin_password": "xxxxxxxxxxx", + "logging_level": "WARNING", + "_OPTI_logging": "OPTI: DEBUG -> WARNING. DEBUG consomme CPU+RAM en continu. Passer en INFO si besoin de diagnostiquer.", + "users_delete_to_recycle_dir": true + }, + + "actions": { + "image": [ + { + "_comment": "Action principale sans remove_background - la plus legere en RAM", + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "_OPTI_show_button": "OPTI: false -> true. Au moins 1 action doit etre visible pour les utilisateurs.", + "title": "Photo", + "icon": "photo_camera", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "27", + "_OPTI_gpio": "OPTI: '' -> '27'. Declenchement direct via GPIO sans passer par Node-RED.", + "trigger_on": "pressed" + } + } + }, + { + "_comment": "Avec remove_background - uniquement quand on change le fond", + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Voie lactee", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Soiree branchee", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "_comment": "OPTI: remove_background desactive car img_frame_enable est false et pas de fond custom utile", + "name": "Normal image", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#2d68c4", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": false, + "img_frame_file": "userdata/demoassets/frames/frame_image_photobooth-app.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Normal", + "icon": "photo_camera", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "_comment": "LULU - sans remove_background car juste un cadre", + "name": "LULU", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/LSDW/lulu-versaire2.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "LULU", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "LULU - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/LSDW/lulu-versaire2.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "LULU - Etoile", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "_OPTI_removed_actions": "OPTI: Supprime 'background stars' (doublon de Hnb-Etoile) et 'Hnb-Etoile-copy' (copie). Garde 7 actions au lieu de 9.", + + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + + "share": { + "sharing_enabled": true, + "actions": [ + { + "_comment": "Action visible par les utilisateurs - envoie la demande a Node-RED pour la file d'attente SQLite + dashboard admin", + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "_comment": "Action cachee - appelee par Node-RED dashboard quand l'admin valide l'impression", + "name": "Impression directe", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Combien de copies ?", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "Imprimer", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 60, + "_OPTI_blocked_time": "OPTI: 120 -> 60s de blocage.", + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Impression directe", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "_OPTI_removed_share_actions": "OPTI: Supprime 'Pouet' (test), 'Mailing action' (demo), 'Printing copies' (doublon). Garde 'Demande impression' (via Node-RED SQLite) + 'Impression directe' (via script_print.sh). 2 actions au lieu de 5." + }, + + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + + "mediaprocessing": { + "full_still_length": 2304, + "_OPTI_full_still": "OPTI: 4608 -> 2304. La Selphy CP1300 imprime en 300dpi sur 4x6 pouces = 1200x1800px. 2304px est largement suffisant et divise la RAM par 4 pour le traitement d'image.", + "preview_still_length": 1200, + "_OPTI_preview": "OPTI: 2304 -> 1200. Pour l'affichage web/QR share, 1200px est largement suffisant.", + "thumbnail_still_length": 480, + "_OPTI_thumbnail": "OPTI: 960 -> 480. Les thumbnails de la galerie n'ont pas besoin d'etre si grands.", + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": false, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/hopnbloc/frames/hnb cadre final.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
\tSi vous utilisez le photomaton, vous nous autorisez \u00e0 utiliser les photos.
\tCe photomaton est propos\u00e9 en libre-service par 'Les Sapins Du Web'.
\tLes photos peuvent \u00eatre imprim\u00e9es sur demande et sur dons libre. (2 Photos maximum par personne)
\tMerci de votre compr\u00e9hension. \t
Comp\u00e9t Hop'N Bloc !!!!!
\"LSDW
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "\ud83d\ude03", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Pas encore de photos ! \ud83e\udd37\u200d\u2642\ufe0f
Appuyez sur le bouton !
\ud83d\udcf7\ud83d\udc95
", + "_OPTI_gallery_empty": "OPTI: Traduit en francais", + "gallery_show_qrcode": true, + "qrcode_text_above": "\u2b07\ufe0f T\u00e9l\u00e9charger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour t\u00e9l\u00e9charger cette photo. ( Vous devez \u00eatre connect\u00e9 au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0\u00b0", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 2304, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 1536, + "_OPTI_capture_res": "OPTI: 4608x3072 -> 2304x1536. Divise la RAM par 4 pour chaque capture. Toujours suffisant pour impression Selphy 300dpi.", + "PREVIEW_CAM_RESOLUTION_WIDTH": 1152, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 768, + "_OPTI_preview_res": "OPTI: 2304x1536 -> 1152x768. La preview n'a pas besoin d'etre aussi grande que la capture.", + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 90, + "_OPTI_quality": "OPTI: 100 -> 90. Difference visuelle imperceptible, fichiers 30-40% plus petits = moins de RAM et I/O disque." + } + } + ] + }, + + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": true, + "_OPTI_gpio": "OPTI: false -> true. Active le GPIO natif pour le bouton, permet de se passer de Node-RED.", + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + + "misc": { + "secret_key": "CHANGER_CETTE_CLE_SECRETE", + "_OPTI_secret": "OPTI: Changer la cle par defaut ! Generer avec: python3 -c \"import secrets; print(secrets.token_hex(32))\"", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} diff --git a/Opencode/config/zoraxy-notes.conf b/Opencode/config/zoraxy-notes.conf new file mode 100644 index 0000000..95185a3 --- /dev/null +++ b/Opencode/config/zoraxy-notes.conf @@ -0,0 +1,80 @@ +# ============================================================================= +# Zoraxy - Configuration reverse proxy pour photomaton +# ============================================================================= +# Installation: ~/zoraxy/ +# Config: ~/zoraxy/conf/proxy/ +# Admin panel: http://127.0.0.1:5487 (localhost only) +# ============================================================================= +# +# ROLE DE ZORAXY: +# - Reverse proxy HTTPS devant tous les services +# - Gestion des certificats TLS +# - Necessaire pour le QR code (HTTPS requis sur mobile) +# +# PORTS ZORAXY: +# 80 : HTTP (redirect -> HTTPS) +# 443 : HTTPS (reverse proxy) +# 5487 : Admin panel (localhost only) +# 8001 : Admin panel (expose via sous-domaine) +# +# ============================================================================= +# PROXIES CONFIGURES +# ============================================================================= +# +# 1. photomaton.lessapinsduweb.com -> 127.0.0.1:8083 (photobooth-app) +# - WebSocket: OUI (EnableWebsocketCustomHeaders: true) +# - ChunkedTransfer: DESACTIVE (necessaire pour photobooth-app) +# - C'est le proxy principal pour les utilisateurs +# +# 2. photomaton-nodered.lessapinsduweb.com -> 127.0.0.1:1880 (Node-RED) +# - Dashboard admin impression +# - Acces: auth propre a Node-RED +# +# 3. photomaton-raspap.lessapinsduweb.com -> 127.0.0.1:8082 (RaspAP/lighttpd) +# - Config WiFi +# - Acces: auth propre a RaspAP +# +# 4. photomaton-zoraxy.lessapinsduweb.com -> 127.0.0.1:8001 (Zoraxy admin) +# - Admin du reverse proxy lui-meme +# - Acces: auth propre a Zoraxy +# +# ============================================================================= +# ARCHITECTURE RESEAU COMPLETE +# ============================================================================= +# +# [Telephone utilisateur] +# | +# | WiFi "Photomaton" (RaspAP, dnsmasq :53) +# v +# [Zoraxy :443 HTTPS] +# | +# +-- photomaton.lessapinsduweb.com ---------> photobooth-app :8083 +# +-- photomaton-nodered.lessapinsduweb.com -> Node-RED :1880 +# +-- photomaton-raspap.lessapinsduweb.com --> lighttpd/RaspAP :8082 +# +-- photomaton-zoraxy.lessapinsduweb.com --> Zoraxy admin :8001 +# +# Services locaux (non proxies): +# - CUPS :631 (impression, acces local) +# - uvicorn :8081 (API backend RaspAP, /etc/raspap/api/) +# - Zoraxy admin :5487 (localhost only) +# - SSH :22 +# - rpcbind :111 (INUTILE - a desactiver) +# +# ============================================================================= +# NOTES IMPORTANTES +# ============================================================================= +# +# - DisableChunkedTransferEncoding: true sur photomaton.lessapinsduweb.com +# C'est necessaire pour que le livestream photobooth fonctionne correctement +# +# - SkipCertValidations: true sur photomaton.lessapinsduweb.com +# Parce que l'upstream est en HTTP, pas HTTPS +# +# - Tous les sous-domaines resolvent vers la meme IP (le Pi) +# via dnsmasq de RaspAP +# +# - rpcbind sur le port 111 est inutile et peut etre desactive: +# sudo systemctl disable rpcbind.service rpcbind.socket +# sudo systemctl stop rpcbind.service rpcbind.socket +# +# ============================================================================= diff --git a/Opencode/docs/ANALYSE-CONFIG.md b/Opencode/docs/ANALYSE-CONFIG.md new file mode 100644 index 0000000..cfcb03f --- /dev/null +++ b/Opencode/docs/ANALYSE-CONFIG.md @@ -0,0 +1,87 @@ +# Analyse de la configuration photobooth-app + +## Problemes identifies et corrections + +### CRITIQUES (causent crashs / OOM) + +| # | Probleme | Impact | Correction | +|---|----------|--------|------------| +| 1 | `logging_level: "DEBUG"` | CPU+RAM en continu, fichiers logs enormes | -> `WARNING` | +| 2 | `remove_background` actif sur 5/9 actions | MODNet charge ~300-400Mo RAM par traitement | Garder uniquement sur les actions qui changent le fond | +| 3 | `full_still_length: 4608` (14 Mpx) | Image de ~40Mo en RAM par capture | -> `2304` (suffisant pour Selphy 300dpi) | +| 4 | `CAPTURE_CAM_RESOLUTION: 4608x3072` | Le capteur charge l'image pleine res | -> `2304x1536` | +| 5 | `PREVIEW_CAM_RESOLUTION: 2304x1536` | Preview inutilement grande | -> `1152x768` | +| 6 | `original_still_quality: 100` | JPEG non compresse = fichiers 2x plus gros | -> `90` (visuellement identique) | + +### Estimation gain RAM avec corrections + +``` +AVANT (config actuelle): + photobooth-app base ~300 Mo + MODNet charge en RAM ~300 Mo + Image 4608x3072 RGBA ~56 Mo/image + Preview 2304x1536 ~14 Mo + Node-RED (Node.js) ~200 Mo + Chromium ~300 Mo + TOTAL ~1170 Mo (sur 2048 Mo dispo) + -> Marge: ~878 Mo (insuffisant avec traitement) + +APRES (config optimisee): + photobooth-app base ~300 Mo + MODNet (si active) ~300 Mo + Image 2304x1536 RGBA ~14 Mo/image (-75%) + Preview 1152x768 ~3 Mo (-78%) + Node-RED DESACTIVE 0 Mo (-200 Mo) + Chromium (optimise) ~200 Mo (-100 Mo) + TOTAL ~817 Mo + -> Marge: ~1231 Mo (suffisant meme avec remove_background) +``` + +### SECURITE + +| # | Probleme | Correction | +|---|----------|------------| +| 7 | `secret_key: "ThisIsTheDefaultSecret"` | Generer une vraie cle: `python3 -c "import secrets; print(secrets.token_hex(32))"` | +| 8 | `admin_password` en clair dans la config | Normal pour photobooth-app, mais ne pas exposer le fichier | + +### IMPRESSION + +| # | Probleme | Correction | +|---|----------|------------| +| 9 | "Demande d'impression" passe par Node-RED (curl 127.0.0.1:1880) | Remplacer par appel `lp` direct | +| 10 | Script print.sh reference `/home/pi/` | Verifier que le chemin est correct | +| 11 | Actions "Pouet" et "Mailing" inutiles | Supprimees | + +### INTERFACE + +| # | Probleme | Correction | +|---|----------|------------| +| 12 | Toutes les actions ont `show_button: false` | Au moins 1 action visible (HnB-normal) | +| 13 | `gpio_enabled: false` | -> `true` pour utiliser le GPIO natif | +| 14 | `GALLERY_EMPTY_MSG` en anglais | Traduit en francais | +| 15 | `thumbnail_still_length: 960` | -> `480` (suffisant pour la galerie web) | + +### ACTIONS SIMPLIFIEES + +**Supprimees** (doublons): +- `background stars` : doublon de `Hnb - Etoile` +- `Hnb - Etoile - copy` : copie de `Hnb - Etoile` avec cadre different + +**Actions share supprimees**: +- `Pouet` : action test (echo OK) +- `Mailing action` : email demo (me@mgineer85.de) +- `Printing copies` : doublon de "Impression" + +### RESTE A FAIRE + +- [ ] Recevoir et analyser `/home/pi/photobooth-data/script/script_print.sh` +- [ ] Remplacer l'appel Node-RED par un appel `lp` direct dans l'action "Demande d'impression" +- [ ] Tester la config optimisee sur le Pi +- [ ] Verifier que le QR share fonctionne avec l'URL `photomaton.lessapinsduweb.com` +- [ ] Generer une vraie `secret_key` + +## Fichiers de reference + +- Config originale: `config/photobooth-config-backup.json` +- Config optimisee: `config/photobooth-config-optimized.json` +- La config optimisee contient des commentaires `_OPTI_*` pour chaque changement diff --git a/Opencode/docs/ARCHITECTURE.md b/Opencode/docs/ARCHITECTURE.md new file mode 100644 index 0000000..30c5728 --- /dev/null +++ b/Opencode/docs/ARCHITECTURE.md @@ -0,0 +1,222 @@ +# Photomaton - Architecture du projet + +Association: Les Sapins Du Web (LSDW) +Events: Hop'N Bloc, anniversaires, etc. + +## Hardware + +| Composant | Modele | Notes | +|-----------|--------|-------| +| SBC | Raspberry Pi 4 (2 Go RAM) | Contrainte memoire forte | +| Camera | Camera Module 3 officielle | Backend picamera2, ratio 3:2 natif | +| Ecran | HDMI (moniteur/TV) | Interface photobooth-app + Chromium kiosk | +| Bouton | Bouton lumineux 12V + relay | GPIO23 (input) + GPIO12 (relay ON/OFF) | +| LED Strip | NeoPixels 35 LEDs | GPIO18, feedback visuel pendant capture | +| Imprimante 1 | Canon Selphy CP1300 (blanche) | USB, CUPS: Canon_SELPHY_CP1300_usb_blanche | +| Imprimante 2 | Canon Selphy CP1300 (noire) | USB, CUPS: Canon_SELPHY_CP1300_usb_noire | +| WiFi | RaspAP (hotspot dedie) | Pour distribution photos via QR code | + +## Software Stack + +``` ++----------------------------------------------------------+ +| Navigateur Web | +| (Chromium kiosk mode, ecran HDMI) | ++----------------------------------------------------------+ +| | +| photobooth-app (:8083) Node-RED (:1880) | +| - Capture camera - Bouton GPIO23 (multi-clic) | +| - Post-traitement - Relay bouton GPIO12 | +| - Galerie + QR share - NeoPixels GPIO18 | +| - Interface utilisateur - Dashboard admin impression | +| - File attente SQLite | +| - Gestion CUPS (enable/ | +| disable, cancel jobs) | ++----------------------------------------------------------+ +| Zoraxy (reverse proxy + TLS :443) | ++----------------------------------------------------------+ +| CUPS + Gutenprint | RaspAP (hotspot WiFi) | +| (impression Selphy x2) | (reseau dedie QR code) | ++----------------------------------------------------------+ +| Raspberry Pi OS (Trixie) | ++----------------------------------------------------------+ +``` + +## Flux utilisateur (prise de photo) + +``` +Bouton physique (GPIO23) + | + v +Node-RED (button-events: clic/double/triple/quadruple) + | + +--> Desactive relay bouton (GPIO12 = OFF) + +--> NeoPixels: vert (countdown) + +--> Appel API photobooth-app: GET /api/actions/image/{N} + | + v + photobooth-app + 1. Countdown (5s) affiche sur ecran + 2. NeoPixels: blanc (flash) <-- via webhook -> Node-RED + 3. Capture Camera Module 3 (picamera2) + 4. NeoPixels: violet (captured) + 5. Post-traitement (cadre, remove_background si active) + 6. Photo ajoutee a la galerie + 7. NeoPixels: bleu (finished) puis vert apres 2s + 8. Reactive relay bouton (GPIO12 = ON) + 9. QR code affiche pour telecharger via WiFi +``` + +## Flux impression + +``` +Utilisateur: clic "Demander l'impression" dans galerie photobooth-app + | + v +photobooth-app: share_command = curl Node-RED /api/photobooth-custom/ask_print + | + v +Node-RED: + 1. Recupere l'UID de la photo via API photobooth /api/mediacollection + 2. Insere dans SQLite (photo-to-print.sqlite) + 3. Affiche dans le dashboard admin (/dashboard) + | + v +Admin (sur telephone via WiFi): + - Voit la demande dans le tableau avec preview + - Clic "OK" = lance impression via script_print.sh (crop 3:2 + lp) + - Clic "x" = annule la demande + - Peut aussi cancel/clear les jobs CUPS, enable/disable imprimantes +``` + +## GPIO + +| GPIO | Pin physique | Fonction | Controle par | +|------|-------------|----------|--------------| +| 12 | 32 | Relay bouton 12V (output) | Node-RED | +| 17 | 11 | Shutdown (hold 2s) | photobooth-app | +| 18 | 12 | NeoPixels 35 LEDs (output) | Node-RED (rpi-neopixels) | +| 20 | 38 | Job abort | photobooth-app | +| 22 | 15 | Job reject / Collage GPIO | photobooth-app | +| 23 | 16 | Bouton physique (input, pull-up) | Node-RED (button-events) | +| 27 | 13 | Job next | photobooth-app | + +## Modes photo (bouton multi-clic) + +| Clics | Mode | Action photobooth-app | remove_background | +|-------|------|----------------------|-------------------| +| 1 | HnB normal | /api/actions/image/0 | Non | +| 2 | HnB etoile | /api/actions/image/1 | Oui | +| 3 | HnB cailloux | /api/actions/image/2 | Oui | +| 4 | HnB light | /api/actions/image/3 | Oui | +| Long press | Demande impression | /api/share/actions/latest/0 | - | + +## Ports reseau + +| Service | Port | Process | Notes | +|---------|------|---------|-------| +| SSH | 22 | sshd | Acces distant | +| DNS | 53 | dnsmasq | RaspAP, resolution domaines locaux | +| HTTP | 80 | zoraxy | Redirect -> HTTPS | +| HTTPS | 443 | zoraxy | Reverse proxy principal | +| CUPS | 631 | cupsd | Administration imprimantes | +| Node-RED | 1880 | node-red | Dashboard admin + GPIO + NeoPixels | +| Zoraxy admin | 5487 | zoraxy | Panel admin (localhost only) | +| Zoraxy admin | 8001 | zoraxy | Panel admin (expose via sous-domaine) | +| RaspAP API | 8081 | uvicorn/python3 | API backend RaspAP (/etc/raspap/api/) | +| RaspAP | 8082 | lighttpd | Interface admin WiFi | +| photobooth-app | 8083 | python | Interface web principale | +| ~~rpcbind~~ | ~~111~~ | ~~rpcbind~~ | **INUTILE - a desactiver** | + +## Domaines (Zoraxy reverse proxy) + +| Sous-domaine | Upstream | Service | +|-------------|----------|---------| +| `photomaton.lessapinsduweb.com` | 127.0.0.1:8083 | photobooth-app (WebSocket ON) | +| `photomaton-nodered.lessapinsduweb.com` | 127.0.0.1:1880 | Node-RED dashboard | +| `photomaton-raspap.lessapinsduweb.com` | 127.0.0.1:8082 | RaspAP admin | +| `photomaton-zoraxy.lessapinsduweb.com` | 127.0.0.1:8001 | Zoraxy admin | + +- Certificats TLS geres par Zoraxy +- QR share: `https://photomaton.lessapinsduweb.com/sharepage/#?url=...` +- Chaque service gere sa propre authentification + +## Reseau WiFi (RaspAP) + +| Element | Valeur | +|---------|--------| +| Interface | wlan0 | +| IP du Pi | 10.3.141.1 | +| Plage DHCP | 10.3.141.50 - 10.3.141.254 (~200 clients) | +| Masque | 255.255.255.0 | +| Bail DHCP | 12h | +| DNS | dnsmasq sur le Pi (port 53) | +| Internet | AUCUN (captive portal) | + +### Resolution DNS (dnsmasq) + +``` +# /etc/dnsmasq.d/090_custom_local_hotspot.conf +address=/lessapinsduweb.com/10.3.141.1 # *.lessapinsduweb.com -> Pi +address=/photomaton.lessapinsduweb.com/10.3.141.1 # explicite +address=/#/127.0.0.1 # tout le reste -> null (pas d'internet) +``` + +Flux QR code : +1. Telephone se connecte au WiFi "Photomaton" +2. Recoit IP en 10.3.141.x via DHCP, DNS = 10.3.141.1 +3. Scan QR code -> `https://photomaton.lessapinsduweb.com/sharepage/...` +4. dnsmasq resout vers 10.3.141.1 (le Pi) +5. Zoraxy (:443) sert la page via HTTPS +6. photobooth-app (:8083) fournit la photo + +## Fichiers sur le Pi + +``` +/home/pi/ + photobooth-data/ + media/ + processed_full/ # Photos traitees (pleine resolution) + script/ + script_print.sh # Script d'impression avec load-balancing + log/ # Logs quotidiens + photo-to-print.sqlite # File d'attente d'impression (Node-RED) + userdata/ + hopnbloc/ + frames/ # Cadres PNG 2000x1333 (ratio 3:2) + backgrounds/ # Fonds pour remove_background + LSDW/ + lulu-versaire2.png # Cadre anniversaire + logo/logo.jpeg # Logo LSDW + demoassets/ + backgrounds/ # Fonds demo (etoiles, etc.) + frames/ # Cadres demo +``` + +## Fichiers du projet (ce repo) + +``` +photomaton/ + scripts/ + optimize-system.sh # Optimisation OS pour 2Go RAM + setup-printer.sh # Configuration CUPS + Selphy CP1300 + setup-photobooth.sh # Installation complete + script_print.sh # Script d'impression optimise + health-check.sh # Surveillance sante du systeme + maintenance.sh # Nettoyage automatique quotidien + config/ + photobooth-config-backup.json # Config originale (sauvegarde) + photobooth-config-optimized.json # Config optimisee + gpio-notes.conf # Documentation GPIO + zoraxy-notes.conf # Notes Zoraxy + nodered-flows-backup.json # Backup flows Node-RED + systemd/ + photobooth-app.service # Service robuste avec auto-restart + photobooth-watchdog.service # Health check + photobooth-watchdog.timer # Toutes les 2 minutes + photobooth-maintenance.service # Nettoyage + photobooth-maintenance.timer # Quotidien a 4h + docs/ + ARCHITECTURE.md # Ce fichier + ANALYSE-CONFIG.md # Analyse detaillee des problemes +``` diff --git a/Opencode/docs/GUIDE-COMPLET.md b/Opencode/docs/GUIDE-COMPLET.md new file mode 100644 index 0000000..135582a --- /dev/null +++ b/Opencode/docs/GUIDE-COMPLET.md @@ -0,0 +1,777 @@ +# Photomaton LSDW - Guide complet + +> Projet photomaton de l'association **Les Sapins Du Web** (LSDW) +> Events : Hop'N Bloc, anniversaires, etc. +> Derniere mise a jour : Juin 2026 + +--- + +## Table des matieres + +1. [Hardware](#1-hardware) +2. [Software Stack](#2-software-stack) +3. [Architecture reseau](#3-architecture-reseau) +4. [Flux utilisateur](#4-flux-utilisateur) +5. [GPIO et bouton physique](#5-gpio-et-bouton-physique) +6. [Configuration photobooth-app](#6-configuration-photobooth-app) +7. [Impression (Canon Selphy CP1300)](#7-impression-canon-selphy-cp1300) +8. [Node-RED](#8-node-red) +9. [Analyse des problemes et optimisations](#9-analyse-des-problemes-et-optimisations) +10. [Guide de deploiement](#10-guide-de-deploiement) +11. [Fichiers du projet](#11-fichiers-du-projet) +12. [Maintenance et surveillance](#12-maintenance-et-surveillance) +13. [Questions en suspens](#13-questions-en-suspens) + +--- + +## 1. Hardware + +| Composant | Modele | Notes | +|-----------|--------|-------| +| SBC | Raspberry Pi 4 (**2 Go RAM**) | Contrainte memoire forte | +| OS | Raspberry Pi OS / Debian 13 Trixie | Reference 2025-12-04, pi-gen stage4 | +| Camera | Camera Module 3 officielle | Backend picamera2, ratio 3:2 natif (4608x3072) | +| Ecran | Moniteur/TV HDMI | Chromium kiosk mode (Wayland/labwc) | +| Bouton | Bouton lumineux 12V + relay | GPIO23 (input) + GPIO12 (relay ON/OFF) | +| LED Strip | NeoPixels WS2812 (35 LEDs) | GPIO18, feedback visuel pendant capture | +| Imprimante 1 | Canon Selphy CP1300 (blanche) | USB, CUPS: `Canon_SELPHY_CP1300_usb_blanche` | +| Imprimante 2 | Canon Selphy CP1300 (noire) | USB, CUPS: `Canon_SELPHY_CP1300_usb_noire` | +| WiFi | Chip WiFi interne RPi4 | Hotspot via RaspAP | + +### Format d'impression + +- Papier : **100 x 150 mm** (carte postale, 4x6 pouces) +- Ratio : **3:2** (identique au capteur Camera Module 3) +- Resolution minimum a 300 dpi : 1770 x 1182 px +- Cadres PNG : **2000 x 1333 px** (ratio 3:2) + +--- + +## 2. Software Stack + +### Schema general + +``` ++----------------------------------------------------------+ +| Navigateur Web | +| Chromium kiosk (Wayland, labwc) | +| ~/Desktop/photobooth-app.desktop | ++----------------------------------------------------------+ +| | +| photobooth-app (:8083) Node-RED (:1880) | +| via pipx venv via npm | +| - Capture camera - Bouton GPIO23 (multi-clic) | +| - Post-traitement - Relay bouton GPIO12 | +| - Galerie + QR share - NeoPixels GPIO18 | +| - Interface utilisateur - Dashboard admin impression | +| - File attente SQLite | +| - Gestion CUPS | +| | ++----------------------------------------------------------+ +| Zoraxy (reverse proxy + TLS :443) | +| ~/zoraxy/ | ++----------------------------------------------------------+ +| CUPS + Gutenprint | RaspAP (hotspot WiFi) | +| Canon Selphy CP1300 x2 | lighttpd :8082 | ++----------------------------------------------------------+ +| uvicorn :8081 (API backend RaspAP) | ++----------------------------------------------------------+ +| Raspberry Pi OS / Debian 13 Trixie | ++----------------------------------------------------------+ +``` + +### Versions et chemins + +| Composant | Chemin / Info | +|-----------|--------------| +| photobooth-app | `~/.local/share/pipx/venvs/photobooth-app/` | +| Donnees photobooth | `~/photobooth-data/` | +| Node-RED | `~/.node-red/` | +| Zoraxy | `~/zoraxy/` | +| Config Zoraxy | `~/zoraxy/conf/proxy/` | +| RaspAP API | `/etc/raspap/api/` (uvicorn :8081) | +| Chromium | `/usr/bin/chromium` (pas chromium-browser, Trixie) | +| Kiosk desktop | `~/Desktop/photobooth-app.desktop` | + +--- + +## 3. Architecture reseau + +### Ports en ecoute + +| Service | Port | Process | Role | +|---------|------|---------|------| +| SSH | 22 | sshd | Acces distant | +| DNS | 53 | dnsmasq | RaspAP, resolution domaines locaux | +| HTTP | 80 | zoraxy | Redirect -> HTTPS | +| HTTPS | 443 | zoraxy | Reverse proxy principal | +| CUPS | 631 | cupsd | Administration imprimantes | +| Node-RED | 1880 | node-red | Dashboard admin + GPIO + NeoPixels | +| Zoraxy admin | 5487 | zoraxy | Panel admin (localhost only) | +| Zoraxy admin | 8001 | zoraxy | Panel admin (expose via sous-domaine) | +| RaspAP API | 8081 | uvicorn/python3 | API backend RaspAP (/etc/raspap/api/) | +| RaspAP | 8082 | lighttpd + php-fpm | Interface admin WiFi | +| photobooth-app | 8083 | python (pipx) | Interface web principale | +| ~~rpcbind~~ | ~~111~~ | ~~rpcbind~~ | **INUTILE - a desactiver** | + +### Reverse proxy Zoraxy + +Zoraxy ecoute sur les ports 80 (HTTP redirect) et 443 (HTTPS) et route vers les services locaux : + +| Sous-domaine | Upstream | Service | Notes | +|-------------|----------|---------|-------| +| `photomaton.lessapinsduweb.com` | 127.0.0.1:8083 | photobooth-app | WebSocket ON, ChunkedTransfer OFF | +| `photomaton-nodered.lessapinsduweb.com` | 127.0.0.1:1880 | Node-RED | Dashboard impression | +| `photomaton-raspap.lessapinsduweb.com` | 127.0.0.1:8082 | RaspAP | Config WiFi | +| `photomaton-zoraxy.lessapinsduweb.com` | 127.0.0.1:8001 | Zoraxy admin | Self-management | + +- Certificats TLS geres par Zoraxy +- Chaque service gere sa propre authentification +- `DisableChunkedTransferEncoding: true` sur le proxy photobooth (necessaire pour le livestream) + +### WiFi (RaspAP) + +| Element | Valeur | +|---------|--------| +| Interface | wlan0 | +| IP du Pi | 10.3.141.1 | +| Plage DHCP | 10.3.141.50 - 10.3.141.254 (~200 clients) | +| Masque | 255.255.255.0 | +| Bail DHCP | 12h | +| DNS | dnsmasq sur le Pi (port 53) | +| Internet | **AUCUN** (captive portal) | + +### Resolution DNS (dnsmasq) + +``` +# /etc/dnsmasq.d/090_custom_local_hotspot.conf +address=/lessapinsduweb.com/10.3.141.1 # *.lessapinsduweb.com -> Pi +address=/photomaton.lessapinsduweb.com/10.3.141.1 # explicite +address=/#/127.0.0.1 # tout le reste -> null (pas d'internet) +``` + +Les telephones connectes au WiFi ne peuvent acceder qu'aux sous-domaines `*.lessapinsduweb.com`. Tout le reste est bloque (pas d'internet). + +--- + +## 4. Flux utilisateur + +### Prise de photo + +``` +Bouton physique (GPIO23) + | + v +Node-RED (button-events: clic/double/triple/quadruple) + | + +--> Desactive relay bouton (GPIO12 = OFF) + +--> NeoPixels: vert (countdown) + +--> Appel API photobooth-app: GET /api/actions/image/{N} + | + v + photobooth-app + 1. Countdown (5s) affiche sur ecran + 2. NeoPixels: blanc (flash) <-- webhook -> Node-RED + 3. Capture Camera Module 3 (picamera2) + 4. NeoPixels: violet (captured) + 5. Post-traitement (cadre, remove_background si active) + 6. Photo ajoutee a la galerie + 7. NeoPixels: bleu (finished) puis vert apres 2s + 8. Reactive relay bouton (GPIO12 = ON) + 9. QR code affiche pour telecharger via WiFi +``` + +### Recuperation de la photo (QR code) + +``` +1. Telephone se connecte au WiFi "Photomaton" +2. Recoit IP en 10.3.141.x via DHCP, DNS = 10.3.141.1 +3. Scan QR code -> https://photomaton.lessapinsduweb.com/sharepage/#?url=... +4. dnsmasq resout vers 10.3.141.1 (le Pi) +5. Zoraxy (:443) sert la page via HTTPS +6. photobooth-app (:8083) fournit la photo +``` + +### Impression + +``` +Utilisateur: clic "Demander l'impression" dans galerie photobooth-app + | + v +photobooth-app: share_command = curl Node-RED /api/photobooth-custom/ask_print + | + v +Node-RED: + 1. Recupere l'UID de la photo via API photobooth /api/mediacollection + 2. Insere dans SQLite (photo-to-print.sqlite) + 3. Affiche dans le dashboard admin (/dashboard) + | + v +Admin (sur telephone via WiFi, https://photomaton-nodered.lessapinsduweb.com/dashboard): + - Voit la demande dans le tableau avec preview photo + - Clic "OK" = lance impression via script_print.sh + - Clic "x" = annule la demande + - Peut aussi cancel/clear les jobs CUPS, enable/disable imprimantes + | + v +script_print.sh: + 1. Verifie le ratio de l'image (crop 3:2 si necessaire) + 2. Choisit l'imprimante idle (blanche ou noire, load-balancing) + 3. Envoie le job via lp -d PRINTER -o landscape -o fit-to-page +``` + +--- + +## 5. GPIO et bouton physique + +### Mapping GPIO + +| GPIO | Pin physique | Fonction | Direction | Controle par | +|------|-------------|----------|-----------|--------------| +| 12 | 32 | Relay bouton lumineux 12V | Output | Node-RED | +| 17 | 11 | Shutdown (maintien 2s) | Input | photobooth-app | +| 18 | 12 | NeoPixels 35 LEDs WS2812 | Output | Node-RED (neopix.py, sudo) | +| 20 | 38 | Job abort | Input | photobooth-app | +| 22 | 15 | Job reject / Collage trigger | Input | photobooth-app | +| 23 | 16 | Bouton physique principal | Input (pull-up) | Node-RED (button-events) | +| 27 | 13 | Job next | Input | photobooth-app | + +### Modes photo (bouton multi-clic sur GPIO23) + +| Clics | Mode | Action API | remove_background | +|-------|------|-----------|-------------------| +| 1 clic | HnB normal | `/api/actions/image/0` | Non | +| 2 clics | HnB etoile | `/api/actions/image/1` | Oui | +| 3 clics | HnB cailloux | `/api/actions/image/2` | Oui | +| 4 clics | HnB light | `/api/actions/image/3` | Oui | +| Appui long | Demande impression | `/api/share/actions/latest/0` | - | + +Le multi-clic est gere par le node `button-events` de Node-RED avec : +- Intervalle clic : 500 ms +- Intervalle appui long : 2000 ms +- Debounce : 15 ms + +### NeoPixels - Couleurs par evenement + +| Evenement | Couleur | Brightness | +|-----------|---------|------------| +| Countdown (counting) | Vert `#008000` | 100 | +| Flash (capture) | Blanc `#FFFFFF` | 255 | +| Captured | Violet `#8A2BE2` | 100 | +| Finished | Bleu `#0000FF` | 100 | +| Idle (apres 2s) | Vert `#008000` | 100 | + +--- + +## 6. Configuration photobooth-app + +### Parametres critiques (config originale vs optimisee) + +| Parametre | Avant (original) | Apres (optimise) | Impact | +|-----------|-----------------|------------------|--------| +| `logging_level` | `DEBUG` | `WARNING` | CPU+RAM en continu | +| `full_still_length` | 4608 | 2304 | /4 RAM par image | +| `CAPTURE_CAM_RESOLUTION` | 4608x3072 | 2304x1536 | /4 RAM capture | +| `PREVIEW_CAM_RESOLUTION` | 2304x1536 | 1152x768 | /4 RAM preview | +| `original_still_quality` | 100 | 90 | -35% taille fichiers | +| `thumbnail_still_length` | 960 | 480 | Moins de RAM galerie | +| `gpio_enabled` | false | true | GPIO natif photobooth-app | +| `secret_key` | `ThisIsTheDefaultSecret` | A changer ! | Securite | + +### Justification des resolutions + +- Camera Module 3 : capteur 4608x3072 (ratio 3:2) +- Cadres PNG : 2000x1333 (ratio 3:2) +- Impression Selphy 300dpi sur 100x150mm : besoin minimum 1770x1182 px +- **2304x1536 est suffisant** : au-dessus du cadre (2000px) et de l'impression (1770px) +- Ratio 3:2 conserve partout : pas de deformation ni de crop a l'impression + +### Actions image (7 actions, config optimisee) + +| # | Nom | Cadre | remove_background | Fond | show_button | +|---|-----|-------|-------------------|------|-------------| +| 0 | HnB normal | hnb cadre final.png | Non | Non | **Oui** | +| 1 | HnB etoile | hnb cadre final.png | Oui | background.jpg | Oui | +| 2 | HnB cailloux | hnb cadre final.png | Oui | Wall-with-large-and-small-stones.jpg | Non | +| 3 | HnB light | calque photos final.png | Oui | 2149243965.jpg | Non | +| 4 | Normal image | Aucun | Non | Non | Non | +| 5 | LULU | lulu-versaire2.png | Non | Non | Non | +| 6 | LULU etoile | lulu-versaire2.png | Oui | background.jpg | Non | + +Actions supprimees (doublons) : `background stars`, `Hnb - Etoile - copy` + +### Actions share (2 actions, config optimisee) + +| # | Nom | Commande | Visible | Usage | +|---|-----|----------|---------|-------| +| 0 | Demande d'impression | `curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}` | Oui | File d'attente SQLite via Node-RED | +| 1 | Impression directe | `/home/pi/photobooth-data/script/script_print.sh "{filename}" ...` | Non | Appelee par Node-RED quand l'admin valide | + +Actions supprimees : `Pouet` (test), `Mailing action` (demo), `Printing copies` (doublon) + +### QR Share + +```json +{ + "enabled": false, + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" +} +``` + +### Kiosk (Chromium) + +Fichier : `~/Desktop/photobooth-app.desktop` + +```ini +[Desktop Entry] +X-GNOME-Autostart-enabled=true +X-GNOME-Autostart-Delay=120 +Terminal=false +Type=Application +Name=Photobooth-App +Exec=bash -c "sleep 60 && chromium --kiosk --accept-lang=fr-FR --incognito + --password-store=basic --disable-features=Translate,OverscrollHistoryNavigation + --noerrdialogs --disable-infobars --no-first-run --ozone-platform=wayland + --enable-features=OverlayScrollbar --start-maximized http://127.0.0.1:8083/" +StartupNotify=false +``` + +Points importants : +- `--ozone-platform=wayland` : Wayland natif (pas X11) +- Le binaire est `chromium` (pas `chromium-browser`) sous Trixie +- Le desktop file est dans `~/Desktop/`, pas dans `~/.config/autostart/` + +--- + +## 7. Impression (Canon Selphy CP1300) + +### Configuration CUPS + +| Imprimante | Nom CUPS | Couleur | +|-----------|---------|---------| +| Selphy 1 | `Canon_SELPHY_CP1300_usb_blanche` | Blanche | +| Selphy 2 | `Canon_SELPHY_CP1300_usb_noire` | Noire | + +Driver : Gutenprint (`printer-driver-gutenprint`) + +### Script d'impression (`script_print.sh`) + +Le script optimise fait : + +1. **Verifie le ratio** de l'image. Si c'est deja du 3:2 (cas normal), pas de crop. Sinon, crop avec ImageMagick. +2. **Load-balancing** entre les 2 imprimantes : + - Si une est idle, utilise celle-la + - Si aucune n'est idle, choisit celle avec le moins de jobs en attente + - Fallback sur la blanche si les deux sont inaccessibles +3. **Envoie le job** via `lp -n COPIES -d PRINTER -o landscape -o fit-to-page` +4. **Nettoie** le fichier temporaire croppe apres 30 secondes +5. **Logs** via `logger` (visible avec `journalctl -t photomaton-print`) + +### Commande d'impression directe (test) + +```bash +lp -d Canon_SELPHY_CP1300_usb_blanche -o landscape -o fit-to-page /chemin/vers/photo.jpg +``` + +### Administration CUPS + +- Interface web : `http://localhost:631` +- Via Node-RED dashboard : enable/disable imprimantes, cancel jobs +- Via script : `scripts/cups-cleanup.sh` (genere par `setup-printer.sh`) + +--- + +## 8. Node-RED + +### Role dans le systeme + +Node-RED est **indispensable** dans le setup actuel. Il gere : + +1. **Bouton physique** (GPIO23) : detection multi-clic via `button-events`, appel API photobooth-app +2. **Relay bouton** (GPIO12) : ON/OFF du bouton lumineux 12V pendant les captures +3. **NeoPixels** (GPIO18) : changement de couleur synchronise aux evenements photobooth +4. **Dashboard admin impression** : tableau des demandes, preview photos, boutons imprimer/annuler +5. **File d'attente SQLite** : stockage des demandes d'impression dans `photo-to-print.sqlite` +6. **Gestion CUPS** : etat imprimantes, enable/disable, cancel jobs + +### Flows (onglets) + +| Tab | Nom | Fonction | +|-----|-----|----------| +| 1 | Photomaton | GPIO bouton, relay, declenchement photo, selection mode | +| 2 | Flux 2 | Dashboard imprimantes (etat, enable/disable, jobs) | +| 3 | Flux 3 | Webhook events photobooth -> NeoPixels + relay bouton | +| 4 | Flux 4 | Controle NeoPixels direct (35 LEDs, GPIO18) | +| 5 | Sqlite | File d'attente impression (SQLite, dashboard, CRUD) | + +### Endpoints HTTP (Node-RED) + +| Endpoint | Methode | Usage | +|----------|---------|-------| +| `/api/photobooth/*` | GET | Webhook events photobooth (counting, capture, captured, finished) | +| `/api/photobooth-custom/ask_print` | GET | Demande d'impression (appelee par photobooth-app) | +| `/dashboard` | GET | Interface admin (impression, imprimantes, systeme) | + +### Base SQLite + +- Fichier : `/home/pi/photobooth-data/photo-to-print.sqlite` +- Table : `photoToPrint` (id, filename, uid, toPrint, printed, created_at) +- L'admin voit les photos en attente dans le dashboard avec preview et peut imprimer ou annuler + +### Migration vers Python ? + +**Non recommande.** Le gain serait de ~100 Mo de RAM, mais le cout de redeveloppement est eleve (dashboard, SQLite CRUD, GPIO multi-clic, NeoPixels sync, gestion CUPS). Les optimisations systeme et config liberent deja ~630 Mo sans rien recoder. + +--- + +## 9. Analyse des problemes et optimisations + +### Problemes rapportes + +1. **Crashs frequents** +2. **Problemes d'impression** +3. **Memoire insuffisante (2 Go)** + +### Consommation RAM mesuree (ps aux, au repos) + +| Service | RAM (Mo) | % du total | +|---------|----------|-----------| +| **Chromium** (kiosk + processus fils) | **~580** | 40% | +| **Node-RED** | **171** | 12% | +| **photobooth-app** | **142** | 10% | +| **Zoraxy** | **99** | 7% | +| **Desktop inutile** (pcmanfm, wf-panel, portails, gvfsd...) | **~231** | 16% | +| Autres (labwc, pipewire, CUPS, dnsmasq, lighttpd, php-fpm...) | ~130 | 9% | +| Kernel + systeme | ~100 | 7% | +| **TOTAL** | **~1450** | **79%** | + +RAM disponible au repos : **~725 Mo** -- mais ca chute fortement pendant les captures, surtout avec `remove_background`. + +### Causes des crashs identifiees + +| Cause | Impact | Correction | +|-------|--------|------------| +| `logging_level: DEBUG` | CPU+RAM constant, logs enormes | -> `WARNING` | +| Resolution capture 4608x3072 (14 Mpx) | ~56 Mo par image en RAM | -> 2304x1536 (~14 Mo) | +| `remove_background` (MODNet) sur 5/9 actions | +300-400 Mo RAM par traitement | Garder uniquement quand on change le fond | +| `original_still_quality: 100` | Fichiers JPEG 2x plus gros | -> 90 | +| Desktop inutile (pcmanfm, barre de taches, portails) | ~231 Mo gaspilles | Script `optimize-desktop.sh` | +| Swappiness a 60 | Le systeme swappe trop tot | -> 10 | +| Pas de swap fichier (seulement zram) | OOM kill si zram plein | Ajouter swapfile 1 Go | + +### Plan d'optimisation + +| Optimisation | Script | Gain RAM estime | +|-------------|--------|----------------| +| Desactiver desktop inutile | `optimize-desktop.sh` | **~231 Mo** | +| Reduire resolution capture | Config photobooth-app | **~42 Mo/capture** | +| Reduire resolution preview | Config photobooth-app | **~11 Mo** | +| `logging_level` DEBUG -> WARNING | Config photobooth-app | Variable (CPU + RAM) | +| JPEG quality 100 -> 90 | Config photobooth-app | -35% I/O disque | +| Desactiver services inutiles | `optimize-system.sh` | **~50 Mo** | +| Swappiness 60 -> 10 | `optimize-system.sh` | Moins de swap inutile | +| Swapfile 1 Go (filet de securite) | `optimize-system.sh` | Evite les OOM kills | +| Limiter Node-RED a 256 Mo (cgroup) | `optimize-system.sh` | Protection fuites memoire | +| Chromium optimise (process-per-site, JS heap) | Config kiosk | Reduction Chromium | + +### Estimation apres optimisation + +``` +AVANT (mesure reelle): + Chromium ~580 Mo + Node-RED ~171 Mo + photobooth-app ~142 Mo + Zoraxy ~99 Mo + Desktop inutile ~231 Mo + Autres ~230 Mo + -------------------------------- + TOTAL ~1453 Mo / 1843 Mo dispo + Marge disponible: ~390 Mo + Marge pendant capture: ~0 Mo (crash!) + +APRES optimisation: + Chromium (optimise) ~400 Mo (options memoire) + Node-RED (cgroup 256 Mo) ~171 Mo (max 256 Mo) + photobooth-app ~142 Mo + Zoraxy ~99 Mo + Desktop inutile 0 Mo (desactive) + Autres (sans services inut) ~180 Mo + -------------------------------- + TOTAL ~992 Mo / 1843 Mo dispo + Marge disponible: ~851 Mo + + swapfile 1 Go en filet de securite + + images 4x plus petites en RAM pendant capture + -> Suffisant meme avec remove_background (~300 Mo) +``` + +--- + +## 10. Guide de deploiement + +### Prerequis + +- Acces SSH au Raspberry Pi +- Copier le dossier `photomaton/` sur le Pi + +### Etape 1 : Optimisation systeme + +```bash +sudo bash scripts/optimize-system.sh +sudo reboot +``` + +Ce script : +- Ajoute un swapfile 1 Go (complement au zram existant) +- Baisse le swappiness de 60 a 10 +- Configure gpu_mem a 128 Mo +- Desactive les services inutiles (bluetooth, ModemManager, rpcbind, apt timers...) +- Desactive le Bluetooth au niveau kernel +- Optimise les parametres memoire kernel (vfs_cache_pressure, dirty_ratio) +- Limite Node-RED a 256 Mo RAM (cgroup systemd) +- Optimise les parametres reseau + +### Etape 2 : Optimisation desktop + +```bash +sudo bash scripts/optimize-desktop.sh +sudo reboot +``` + +Ce script : +- Cree un autostart labwc minimal (que Chromium + pipewire) +- Desactive pcmanfm, wf-panel-pi, portails desktop, polkit, gvfsd +- Desactive ModemManager, accounts-daemon, colord, rpcbind, upower +- Reduit les workers php-fpm (RaspAP) a 2 + +**Reversible** : `sudo bash scripts/optimize-desktop.sh --restore` + +### Etape 3 : Script d'impression + +```bash +cp scripts/script_print.sh /home/pi/photobooth-data/script/script_print.sh +chmod +x /home/pi/photobooth-data/script/script_print.sh +``` + +### Etape 4 : Configuration photobooth-app + +Deux options : + +**Option A** : Appliquer via l'interface admin (`https://photomaton.lessapinsduweb.com/admin`) +- Modifier chaque parametre un par un en suivant le tableau de la section 6 + +**Option B** : Remplacer le fichier de config +- Copier `config/photobooth-config-optimized.json` dans le dossier de config de photobooth-app +- Supprimer les champs `_comment`, `_OPTI_*` (ce sont des annotations) +- Redemarrer photobooth-app + +**Important** : Generer une vraie `secret_key` : +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +### Etape 5 : Services systemd (watchdog + maintenance) + +```bash +# Service photobooth-app (utilisateur) +cp systemd/photobooth-app.service ~/.config/systemd/user/ +systemctl --user daemon-reload +systemctl --user enable photobooth-app + +# Watchdog (systeme) +sudo cp systemd/photobooth-watchdog.service /etc/systemd/system/ +sudo cp systemd/photobooth-watchdog.timer /etc/systemd/system/ +# Editer les chemins dans les .service si necessaire +sudo systemctl daemon-reload +sudo systemctl enable photobooth-watchdog.timer +sudo systemctl start photobooth-watchdog.timer + +# Maintenance quotidienne (systeme) +sudo cp systemd/photobooth-maintenance.service /etc/systemd/system/ +sudo cp systemd/photobooth-maintenance.timer /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable photobooth-maintenance.timer +sudo systemctl start photobooth-maintenance.timer +``` + +### Etape 6 : Verification + +```bash +bash scripts/health-check.sh +``` + +### Quick wins immediats (sans script) + +Si tu veux appliquer les corrections les plus impactantes maintenant sans lancer les scripts complets : + +```bash +# 1. Baisser le swappiness (gain immediat) +sudo sysctl vm.swappiness=10 +echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-photomaton-swap.conf + +# 2. Desactiver rpcbind (inutile) +sudo systemctl disable rpcbind.service rpcbind.socket +sudo systemctl stop rpcbind.service rpcbind.socket + +# 3. Desactiver ModemManager (inutile) +sudo systemctl disable ModemManager.service +sudo systemctl stop ModemManager.service + +# 4. Changer le logging_level dans photobooth-app admin +# http://localhost:8083/admin -> common -> logging_level -> WARNING +``` + +--- + +## 11. Fichiers du projet + +``` +photomaton/ + scripts/ + optimize-system.sh # Optimisation OS (swap, kernel, services) + optimize-desktop.sh # Desactiver le desktop inutile (~231 Mo) + setup-printer.sh # Configuration CUPS + Selphy CP1300 + setup-photobooth.sh # Installation complete (master script) + script_print.sh # Script d'impression optimise (load-balancing) + health-check.sh # Watchdog / surveillance sante + maintenance.sh # Nettoyage automatique quotidien + config/ + photobooth-config-backup.json # Config photobooth-app originale + photobooth-config-optimized.json # Config optimisee (avec annotations _OPTI_) + gpio-notes.conf # Documentation GPIO complete + zoraxy-notes.conf # Cartographie reseau Zoraxy + nodered-flows-backup.json # Reference backup flows Node-RED + systemd/ + photobooth-app.service # Service robuste (auto-restart, limites RAM) + photobooth-watchdog.service # Health check automatique + photobooth-watchdog.timer # Toutes les 2 minutes + photobooth-maintenance.service # Nettoyage quotidien + photobooth-maintenance.timer # A 4h du matin + docs/ + GUIDE-COMPLET.md # Ce fichier + ARCHITECTURE.md # Schema d'architecture + ANALYSE-CONFIG.md # Analyse detaillee des problemes +``` + +### Fichiers sur le Pi + +``` +/home/pi/ + photobooth-data/ + media/ + processed_full/ # Photos traitees (pleine resolution) + script/ + script_print.sh # Script d'impression + log/ # Logs quotidiens photobooth-app + photo-to-print.sqlite # File d'attente impression (Node-RED) + userdata/ + hopnbloc/ + frames/ # Cadres PNG 2000x1333 (ratio 3:2) + hnb cadre final.png + calque photos final.png + backgrounds/ # Fonds pour remove_background + Wall-with-large-and-small-stones.jpg + 2149243965.jpg + LSDW/ + lulu-versaire2.png # Cadre anniversaire + logo/logo.jpeg # Logo LSDW + demoassets/ + backgrounds/background.jpg # Fond etoiles + frames/ # Cadres demo + .node-red/ # Node-RED + flows.json # Flows (5 onglets) + node_modules/ + node-red-node-pi-gpio/ # GPIO nodes + node-red-node-pi-neopixel/ # NeoPixels nodes + zoraxy/ + zoraxy # Binaire + start.sh # Script de demarrage + conf/proxy/ # Configs reverse proxy + ws2812/ # Ancien test NeoPixels (inactif) + Desktop/ + photobooth-app.desktop # Autostart Chromium kiosk +``` + +--- + +## 12. Maintenance et surveillance + +### Watchdog (health-check.sh) + +Execute toutes les 2 minutes par `photobooth-watchdog.timer`, verifie : + +- **Memoire** : alerte si < 100 Mo dispo, vide les caches si critique +- **Temperature CPU** : alerte si > 70C, erreur si > 80C +- **photobooth-app** : verifie si le service tourne ET si l'interface web repond (HTTP 200) +- **Imprimantes** : verifie CUPS, reactive les imprimantes desactivees, nettoie les jobs bloques +- **Espace disque** : alerte si > 80%, nettoyage d'urgence si > 90% +- **Camera** : verifie que la camera est detectee par libcamera + +Si photobooth-app ne repond pas, le watchdog le relance automatiquement (max 3 tentatives). + +### Maintenance quotidienne (maintenance.sh) + +Execute tous les jours a 4h par `photobooth-maintenance.timer` : + +- Nettoyage des vieux logs photobooth (> 7 jours) +- Nettoyage des logs systeme comprimes +- Limitation journald a 50 Mo +- Nettoyage des jobs CUPS bloques +- Reactivation des imprimantes desactivees +- Nettoyage cache apt +- Rapport d'etat (espace disque, nombre de photos, uptime) + +### Service systemd photobooth-app + +Le service `photobooth-app.service` inclut : + +- `Restart=always` avec 5s de delai +- `MemoryMax=1500M` : kill si depasse 1.5 Go (protection OOM) +- `MemoryHigh=1200M` : avertissement a 1.2 Go +- `WatchdogSec=120` : watchdog integre systemd +- `StartLimitBurst=5` / `StartLimitIntervalSec=300` : max 5 redemarrages en 5 min + +### Commandes utiles + +```bash +# Logs photobooth-app +journalctl --user --unit=photobooth-app -n 200 --no-pager + +# Logs impression +journalctl -t photomaton-print --no-pager + +# Logs watchdog +journalctl -t photomaton-watchdog --no-pager + +# Etat des services +systemctl --user status photobooth-app +systemctl status photobooth-watchdog.timer +systemctl status photobooth-maintenance.timer + +# Etat imprimantes +lpstat -p + +# Etat memoire +free -h +zramctl + +# Etat des processus par RAM +ps aux --sort=-%mem | head -20 + +# Health check manuel +bash scripts/health-check.sh +``` + +--- + +## 13. Questions en suspens + +- [x] ~~**NeoPixels doublon**~~ : RESOLU. uvicorn (:8081) est l'API backend de RaspAP (`/etc/raspap/api/`), pas les NeoPixels. Les NeoPixels sont geres uniquement par `neopix.py` via Node-RED. Le dossier `~/ws2812/` est un ancien test inactif. + +- [ ] **Tester la config optimisee** en conditions reelles lors d'un event. + +- [ ] **Generer une vraie `secret_key`** pour photobooth-app (remplacer `ThisIsTheDefaultSecret`). + +- [ ] **Verifier le captive portal** : `address=/#/127.0.0.1` redirige tout domaine inconnu vers localhost du telephone (pas vers le Pi). Les telephones ne detecteront pas automatiquement un portail captif. Si besoin d'une page d'accueil, rediriger vers 10.3.141.1 a la place. + +- [ ] **Verifier les cadres PNG** des autres actions (LULU, calque photos final.png) pour confirmer qu'ils sont aussi en ratio 3:2 et en resolution ~2000px. diff --git a/Opencode/docs/GUIDE-INSTALLATION.md b/Opencode/docs/GUIDE-INSTALLATION.md new file mode 100644 index 0000000..4402547 --- /dev/null +++ b/Opencode/docs/GUIDE-INSTALLATION.md @@ -0,0 +1,872 @@ +# Photomaton LSDW - Guide d'installation from scratch + +> Ce guide permet de remonter le photomaton completement a partir d'un Raspberry Pi 4 (2 Go) neuf. +> Reconstitue a partir de l'historique bash du Pi original. +> Derniere mise a jour : Juin 2026 + +--- + +## Table des matieres + +1. [Prerequis](#1-prerequis) +2. [Installation de l'OS](#2-installation-de-los) +3. [Configuration initiale du Pi](#3-configuration-initiale-du-pi) +4. [Installation de photobooth-app](#4-installation-de-photobooth-app) +5. [Installation de RaspAP (hotspot WiFi)](#5-installation-de-raspap-hotspot-wifi) +6. [Installation de Zoraxy (reverse proxy)](#6-installation-de-zoraxy-reverse-proxy) +7. [Certificat TLS (Let's Encrypt + OVH)](#7-certificat-tls-lets-encrypt--ovh) +8. [Configuration des imprimantes (CUPS + Selphy)](#8-configuration-des-imprimantes-cups--selphy) +9. [Installation de Node-RED](#9-installation-de-node-red) +10. [NeoPixels (WS2812)](#10-neopixels-ws2812) +11. [Configuration du mode kiosk (Chromium)](#11-configuration-du-mode-kiosk-chromium) +12. [Optimisation systeme](#12-optimisation-systeme) +13. [Services systemd](#13-services-systemd) +14. [Verification finale](#14-verification-finale) +15. [Post-installation : contenu a restaurer](#15-post-installation--contenu-a-restaurer) + +--- + +## 1. Prerequis + +### Materiel necessaire + +- Raspberry Pi 4 (2 Go RAM) +- Carte microSD (32 Go minimum, classe A2 recommandee) +- Camera Module 3 officielle Raspberry Pi +- Ecran HDMI +- Bouton physique lumineux 12V avec relay +- NeoPixels WS2812 (35 LEDs, strip) +- Canon Selphy CP1300 x2 (blanche + noire) + cables USB +- Cable Ethernet (pour l'installation initiale et les certificats) +- Clavier + souris (pour l'installation initiale) + +### Logiciels sur le PC + +- Raspberry Pi Imager (pour flasher la carte SD) +- Un client SSH (terminal, PuTTY, etc.) + +--- + +## 2. Installation de l'OS + +### Flasher la carte SD + +1. Telecharger et installer [Raspberry Pi Imager](https://www.raspberrypi.com/software/) +2. Choisir l'OS : **Raspberry Pi OS (64-bit)** base sur Debian 13 Trixie + - Version **Desktop** (pas Lite, on a besoin de l'environnement graphique pour Chromium) + - Architecture **arm64** +3. Dans les options avancees (engrenage) : + - Hostname : `PiPhotobooth` + - Activer SSH (authentification par mot de passe) + - Utilisateur : `pi` / mot de passe : `[choisir un mot de passe]` + - Locale : `fr_FR.UTF-8`, timezone : `Europe/Paris` + - WiFi : ne pas configurer (on utilisera RaspAP) +4. Flasher la carte SD + +### Premier demarrage + +1. Inserer la carte SD, connecter l'ecran HDMI, le clavier, et le cable Ethernet +2. Demarrer le Pi +3. Attendre la fin du premier boot (peut prendre 2-3 minutes) +4. Se connecter en SSH : `ssh pi@PiPhotobooth.local` + +--- + +## 3. Configuration initiale du Pi + +### Mise a jour systeme + +```bash +sudo apt update +sudo apt upgrade -y +``` + +### Configuration avec raspi-config + +```bash +sudo raspi-config +``` + +Configurer : +- **System Options > Boot / Auto Login** : Desktop Autologin (B4) +- **Interface Options > Camera** : Activer (si demande) +- **Localisation Options** : Locale fr_FR.UTF-8, Timezone Europe/Paris +- **Advanced Options > Expand Filesystem** (si pas fait automatiquement) + +### Desactiver les services inutiles + +```bash +sudo systemctl disable bluetooth.service +sudo systemctl disable wg-quick@wg0.service 2>/dev/null +``` + +### Desactiver cloud-init (si present) + +```bash +sudo touch /etc/cloud/cloud-init.disabled +``` + +### Configurer /etc/hosts + +```bash +sudo nano /etc/hosts +``` + +Ajouter : +``` +127.0.0.1 localhost +127.0.1.1 PiPhotobooth +10.3.141.1 photomaton.lessapinsduweb.com +10.3.141.1 photomaton-nodered.lessapinsduweb.com +10.3.141.1 photomaton-raspap.lessapinsduweb.com +10.3.141.1 photomaton-zoraxy.lessapinsduweb.com +``` + +Si cloud-init ecrase `/etc/hosts`, editer aussi le template : +```bash +sudo nano /etc/cloud/templates/hosts.debian.tmpl +``` + +### Redemarrer + +```bash +sudo reboot +``` + +--- + +## 4. Installation de photobooth-app + +### Dependances + +```bash +sudo apt -y install ffmpeg libturbojpeg0 libgl1 libgphoto2-dev fonts-noto-color-emoji +sudo apt -y install libexif12 libgphoto2-6 libgphoto2-port12 libltdl7 +sudo apt -y install python3-dev +sudo apt -y install pipx +sudo apt -y install imagemagick +``` + +### Installation via pipx + +```bash +pipx ensurepath +# Fermer et rouvrir le terminal (ou source ~/.bashrc) +exit +``` + +```bash +ssh pi@PiPhotobooth.local +pipx install --system-site-packages photobooth-app --pip-args='--prefer-binary' +``` + +### Creer le dossier de donnees + +```bash +mkdir ~/photobooth-data +``` + +### Verifier que la camera fonctionne + +```bash +rpicam-hello --list-cameras -v +``` + +### Premier lancement (test) + +```bash +cd ~/photobooth-data +photobooth --port 8083 +``` + +Verifier sur `http://PiPhotobooth.local:8083` que l'interface fonctionne. Arreter avec Ctrl+C. + +### Configuration de photobooth-app + +Le service systemd est installe automatiquement par photobooth-app dans : +`~/.local/share/systemd/user/photobooth-app.service` + +Editer pour configurer le port et les dependances : + +```bash +nano ~/.local/share/systemd/user/photobooth-app.service +``` + +Contenu recommande : + +```ini +[Unit] +Description=photobooth-app +After=zoraxy.service raspapd.service + +[Service] +Type=simple +Restart=always +WorkingDirectory=/home/pi/photobooth-data +ExecStart=/home/pi/.local/share/pipx/venvs/photobooth-app/bin/python -O -m photobooth --port 8083 + +[Install] +WantedBy=default.target +``` + +Points importants : +- `After=zoraxy.service raspapd.service` : attend que Zoraxy et RaspAP soient prets +- `--port 8083` : port d'ecoute +- `Restart=always` : relance en cas de crash + +Activer et demarrer : + +```bash +systemctl --user daemon-reload +systemctl --user enable photobooth-app +systemctl --user start photobooth-app +``` + +Activer le linger (pour que le service demarre au boot sans login) : + +```bash +sudo loginctl enable-linger pi +``` + +Verifier : + +```bash +systemctl --user status photobooth-app +journalctl --user --unit=photobooth-app -n 50 --no-pager +``` + +### Configuration dans l'interface admin + +Acceder a `http://PiPhotobooth.local:8083/admin` et configurer : + +- **common** : `logging_level` -> `WARNING` (pas DEBUG !) +- **backends** : Camera Module 3, backend Picamera2, resolution 2304x1536 +- **hardwareinputoutput** : `gpio_enabled` -> `true` +- **mediaprocessing** : `full_still_length` -> 2304, `preview_still_length` -> 1200 +- **misc** : Changer `secret_key` (generer avec `python3 -c "import secrets; print(secrets.token_hex(32))"`) + +Voir `config/photobooth-config-optimized.json` pour la config complete recommandee. + +--- + +## 5. Installation de RaspAP (hotspot WiFi) + +### Installation automatique + +```bash +curl -sL https://install.raspap.com | bash +``` + +Suivre les instructions. Repondre oui aux questions par defaut. + +Apres l'installation, le Pi redemarrera avec un hotspot WiFi actif. + +### Configuration du hotspot + +Acceder a l'interface RaspAP : `http://10.3.141.1:8082` (ou `http://PiPhotobooth.local:8082` via Ethernet) + +Login par defaut : `admin` / `secret` (changer le mot de passe !) + +Configurer : +- **Hotspot > Basic** : + - SSID : `Photomaton` (ou le nom souhaite) + - Security : WPA2 + - Mot de passe WiFi : `[choisir]` +- **Hotspot > Advanced** : + - Country code : FR + - Channel : choisir un canal peu encombre + +### Configuration DNS (dnsmasq) + +Creer le fichier de resolution locale : + +```bash +sudo nano /etc/dnsmasq.d/090_custom_local_hotspot.conf +``` + +Contenu : +``` +address=/lessapinsduweb.com/10.3.141.1 +address=/photomaton.lessapinsduweb.com/10.3.141.1 +address=/#/127.0.0.1 +``` + +Redemarrer dnsmasq : + +```bash +sudo systemctl restart dnsmasq.service +``` + +Verifier : + +```bash +dig @127.0.0.1 photomaton.lessapinsduweb.com A +# Doit repondre 10.3.141.1 +``` + +### Changer le port lighttpd (si conflit) + +Si lighttpd est sur le port 80 (conflit avec Zoraxy), le changer : + +```bash +sudo nano /etc/lighttpd/lighttpd.conf +``` + +Changer `server.port = 80` en `server.port = 8082` (devrait deja etre fait par RaspAP). + +--- + +## 6. Installation de Zoraxy (reverse proxy) + +### Telecharger et installer + +```bash +mkdir ~/zoraxy +cd ~/zoraxy + +# Telecharger la derniere version arm64 +# Verifier la derniere release sur https://github.com/tobychui/zoraxy/releases +wget https://github.com/tobychui/zoraxy/releases/download/v3.3.2/zoraxy_linux_arm64 +mv zoraxy_linux_arm64 zoraxy +chmod +x zoraxy +``` + +### Creer le script de demarrage + +```bash +nano ~/zoraxy/start.sh +``` + +Contenu : +```bash +#!/bin/bash +cd /home/pi/zoraxy +./zoraxy -port=:8001 +``` + +```bash +chmod +x ~/zoraxy/start.sh +``` + +### Creer le service systemd + +```bash +sudo nano /etc/systemd/system/zoraxy.service +``` + +Contenu : +```ini +[Unit] +Description=Zoraxy Reverse Proxy +After=network.target + +[Service] +Type=simple +ExecStart=/home/pi/zoraxy/start.sh +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable zoraxy.service +sudo systemctl start zoraxy.service +``` + +Verifier : + +```bash +sudo systemctl status zoraxy.service +sudo ss -ltnp | grep zoraxy +``` + +### Configurer les proxies + +Acceder a l'admin Zoraxy : `http://127.0.0.1:8001` (ou `http://PiPhotobooth.local:8001` via Ethernet) + +Creer les 4 proxies : + +**1. photomaton.lessapinsduweb.com -> 127.0.0.1:8083** +- WebSocket Custom Headers : ON +- Disable Chunked Transfer Encoding : ON +- Skip Certificate Validations : ON + +**2. photomaton-nodered.lessapinsduweb.com -> 127.0.0.1:1880** +- Options par defaut + +**3. photomaton-raspap.lessapinsduweb.com -> 127.0.0.1:8082** +- Options par defaut + +**4. photomaton-zoraxy.lessapinsduweb.com -> 127.0.0.1:8001** +- Options par defaut + +--- + +## 7. Certificat TLS (Let's Encrypt + OVH) + +> Le certificat est necessaire pour HTTPS. Comme le Pi n'a pas d'acces internet permanent +> (WiFi isole), on utilise le challenge DNS via l'API OVH. +> Cette etape necessite un cable Ethernet connecte a internet. + +### Installer certbot avec le plugin OVH + +```bash +sudo apt install certbot # ou via venv si le paquet n'existe pas : +# sudo python3 -m venv /opt/certbot/ +# sudo /opt/certbot/bin/pip install --upgrade pip +# sudo /opt/certbot/bin/pip install certbot certbot-dns-ovh +# sudo ln -s /opt/certbot/bin/certbot /usr/local/bin/certbot +``` + +### Configurer les credentials OVH + +Creer les credentials API OVH sur https://api.ovh.com/createToken/ + +```bash +nano ~/.ovhapi +``` + +Contenu : +```ini +dns_ovh_endpoint = ovh-eu +dns_ovh_application_key = VOTRE_APP_KEY +dns_ovh_application_secret = VOTRE_APP_SECRET +dns_ovh_consumer_key = VOTRE_CONSUMER_KEY +``` + +```bash +chmod 600 ~/.ovhapi +``` + +### Generer le certificat + +**Important** : Le Pi doit etre connecte a internet via Ethernet pour cette etape. + +```bash +sudo certbot certonly \ + --dns-ovh \ + --dns-ovh-credentials ~/.ovhapi \ + -d photomaton.lessapinsduweb.com \ + -d photomaton-nodered.lessapinsduweb.com \ + -d photomaton-raspap.lessapinsduweb.com \ + -d photomaton-zoraxy.lessapinsduweb.com +``` + +Les certificats sont generes dans `/etc/letsencrypt/live/photomaton.lessapinsduweb.com/`. + +### Importer dans Zoraxy + +Dans l'admin Zoraxy (`http://127.0.0.1:8001`) : +1. Aller dans TLS / SSL +2. Importer le certificat : + - Certificate : contenu de `/etc/letsencrypt/live/photomaton.lessapinsduweb.com/fullchain.pem` + - Private Key : contenu de `/etc/letsencrypt/live/photomaton.lessapinsduweb.com/privkey.pem` + +### Renouvellement + +Le certificat expire tous les 90 jours. Pour renouveler : + +1. Connecter le Pi a internet via Ethernet +2. Lancer : `sudo certbot renew` +3. Reimporter dans Zoraxy si necessaire + +> Note : Ce n'est pas automatise car le Pi n'a pas d'acces internet permanent. +> Penser a renouveler avant chaque saison d'events. + +--- + +## 8. Configuration des imprimantes (CUPS + Selphy) + +### Installer les paquets + +```bash +sudo apt -y install cups printer-driver-gutenprint cups-client +``` + +### Configurer les permissions + +```bash +sudo usermod -aG lpadmin pi +sudo usermod -aG lp pi +``` + +### Configurer CUPS pour l'acces distant + +```bash +sudo cupsctl --remote-any +sudo systemctl restart cups +``` + +### Gestion du driver USB + +La Selphy CP1300 peut utiliser ipp-usb ou usblp. Pour eviter les conflits : + +```bash +# Verifier si ipp-usb est installe +sudo apt list --installed | grep ipp + +# Si necessaire, blacklister usblp pour utiliser ipp-usb +# (ou l'inverse selon ce qui fonctionne) +sudo nano /etc/modprobe.d/blacklist-usblp.conf +``` + +Contenu (si on utilise ipp-usb) : +``` +# blacklist usblp +``` + +### Ajouter les imprimantes + +1. Brancher les 2 Canon Selphy CP1300 en USB +2. Aller sur `http://localhost:631` (ou `https://photomaton-raspap.lessapinsduweb.com` via WiFi) +3. Administration > Add Printer +4. Selectionner l'imprimante USB Canon +5. Nommer : + - Premiere (blanche) : `Canon_SELPHY_CP1300_usb_blanche` + - Deuxieme (noire) : `Canon_SELPHY_CP1300_usb_noire` +6. Selectionner le driver Gutenprint + +Verification : + +```bash +lpstat -v +lpstat -p +``` + +### Installer le script d'impression + +```bash +mkdir -p ~/photobooth-data/script +cp scripts/script_print.sh ~/photobooth-data/script/ +chmod +x ~/photobooth-data/script/script_print.sh +``` + +### Tester l'impression + +```bash +# Prendre une photo de test via photobooth-app, puis : +cd ~/photobooth-data/media/processed_full/ +lp -d Canon_SELPHY_CP1300_usb_blanche -o landscape -o fit-to-page [nom_fichier].jpg +``` + +--- + +## 9. Installation de Node-RED + +### Installation via le script officiel + +```bash +bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered) +``` + +Repondre oui aux questions. Le script installe Node.js et Node-RED, et cree le service systemd. + +### Activer le service + +```bash +sudo systemctl enable nodered.service +sudo systemctl start nodered.service +``` + +### Installer les nodes necessaires + +Acceder a Node-RED : `http://PiPhotobooth.local:1880` + +Dans le menu hamburger > Manage palette > Install, installer : + +- `node-red-node-pi-gpio` (GPIO input/output) +- `node-red-node-pi-neopixel` (NeoPixels WS2812) +- `node-red-node-sqlite` (base SQLite) +- `node-red-dashboard` ou `@flowfuse/node-red-dashboard` (Dashboard v2) +- `node-red-contrib-button-events` (multi-clic bouton) + +### Importer les flows + +1. Dans Node-RED, menu hamburger > Import +2. Coller le contenu du fichier de flows (backup dans `config/nodered-flows-backup.json`) +3. Deploy + +### Creer la base SQLite + +```bash +sudo apt install sqlite3 +``` + +La base est creee automatiquement par Node-RED au premier usage, mais on peut la creer manuellement : + +```bash +sqlite3 ~/photobooth-data/photo-to-print.sqlite +``` + +```sql +CREATE TABLE photoToPrint( + id INT PRIMARY KEY, + filename TEXT NOT NULL, + uid TEXT NOT NULL, + toPrint INT DEFAULT 1, + printed BOOLEAN DEFAULT 0, + created_at TEXT NOT NULL DEFAULT current_timestamp +); +.quit +``` + +--- + +## 10. NeoPixels (WS2812) + +### Cablage + +- Data : GPIO18 +- Alimentation : 5V + GND +- 35 LEDs + +### Via Node-RED (methode utilisee) + +Le node `node-red-node-pi-neopixel` gere les NeoPixels directement. Il lance un script Python en `sudo` : + +``` +/home/pi/.node-red/node_modules/node-red-node-pi-neopixel/neopix.py 35 0 pixels 100 true 0 18 +``` + +Pas d'installation supplementaire requise au-dela du node Node-RED. + +### Note : uvicorn sur le port 8081 + +Le processus uvicorn sur le port 8081 est l'**API backend de RaspAP** (`/etc/raspap/api/main.py`), pas un service NeoPixels. Il est lance automatiquement par RaspAP et ne doit pas etre desactive. + +Le dossier `~/ws2812/` est un ancien test NeoPixels inactif (rien ne le lance). Il peut etre supprime si souhaite. + +--- + +## 11. Configuration du mode kiosk (Chromium) + +### Fichier desktop + +```bash +nano ~/Desktop/photobooth-app.desktop +``` + +Contenu : + +```ini +[Desktop Entry] +X-GNOME-Autostart-enabled=true +X-GNOME-Autostart-Delay=120 +Terminal=false +Type=Application +Name=Photobooth-App +Exec=bash -c "sleep 60 && chromium --kiosk --accept-lang=fr-FR --incognito --password-store=basic --disable-features=Translate,OverscrollHistoryNavigation --noerrdialogs --disable-infobars --no-first-run --ozone-platform=wayland --enable-features=OverlayScrollbar --start-maximized http://127.0.0.1:8083/" +StartupNotify=false +``` + +### Points importants + +- Le binaire est `chromium` (pas `chromium-browser`) sous Debian 13 Trixie +- `--ozone-platform=wayland` : utilisation native de Wayland (le compositeur est labwc) +- Le fichier est dans `~/Desktop/`, pas dans `~/.config/autostart/` +- Le `sleep 60` + `X-GNOME-Autostart-Delay=120` laissent le temps a photobooth-app de demarrer +- `--incognito` : pas de cache persistant, pas de popups "restore session" + +### Lien symbolique pour autostart + +```bash +sudo ln -s /home/pi/Desktop/photobooth-app.desktop /etc/xdg/autostart/photobooth.desktop +``` + +--- + +## 12. Optimisation systeme + +### Scripts d'optimisation + +Copier le dossier `photomaton/` sur le Pi, puis : + +```bash +# Optimisation systeme (swap, kernel, services) +sudo bash scripts/optimize-system.sh +sudo reboot + +# Optimisation desktop (desactiver composants inutiles, ~231 Mo) +sudo bash scripts/optimize-desktop.sh +sudo reboot +``` + +### Optimisations manuelles rapides + +Si on ne veut pas lancer les scripts complets : + +```bash +# Baisser le swappiness (60 -> 10) +sudo sysctl vm.swappiness=10 +echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-photomaton-swap.conf + +# Desactiver rpcbind (inutile) +sudo systemctl disable rpcbind.service rpcbind.socket +sudo systemctl stop rpcbind.service rpcbind.socket + +# Desactiver ModemManager (inutile) +sudo systemctl disable ModemManager.service +sudo systemctl stop ModemManager.service + +# Desactiver bluetooth +sudo systemctl disable bluetooth.service +sudo systemctl stop bluetooth.service + +# Ajouter un swapfile de securite (en complement de zram) +sudo fallocate -l 1G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon -p 10 /swapfile +echo '/swapfile none swap sw,pri=10 0 0' | sudo tee -a /etc/fstab +``` + +--- + +## 13. Services systemd + +### Liste des services du photomaton + +| Service | Type | Fichier | Gere par | +|---------|------|---------|----------| +| photobooth-app | User | `~/.local/share/systemd/user/photobooth-app.service` | pipx (auto-installe), After=zoraxy+raspapd | +| nodered | System | `/etc/systemd/system/nodered.service` | Script officiel Node-RED | +| zoraxy | System | `/etc/systemd/system/zoraxy.service` | Manuel (voir section 6) | +| cups | System | (pre-installe) | apt | +| hostapd | System | (pre-installe) | RaspAP | +| dnsmasq | System | (pre-installe) | RaspAP | +| lighttpd | System | (pre-installe) | RaspAP | +| dhcpcd | System | (pre-installe) | RaspAP | + +### Services optionnels (surveillance) + +```bash +# Watchdog (health check toutes les 2 minutes) +sudo cp systemd/photobooth-watchdog.service /etc/systemd/system/ +sudo cp systemd/photobooth-watchdog.timer /etc/systemd/system/ +# Editer ExecStart pour pointer vers le bon chemin de health-check.sh +sudo systemctl daemon-reload +sudo systemctl enable photobooth-watchdog.timer +sudo systemctl start photobooth-watchdog.timer + +# Maintenance quotidienne (4h du matin) +sudo cp systemd/photobooth-maintenance.service /etc/systemd/system/ +sudo cp systemd/photobooth-maintenance.timer /etc/systemd/system/ +# Editer ExecStart pour pointer vers le bon chemin de maintenance.sh +sudo systemctl daemon-reload +sudo systemctl enable photobooth-maintenance.timer +sudo systemctl start photobooth-maintenance.timer +``` + +### Verification des services + +```bash +# Tous les services actifs +sudo systemctl list-unit-files --type=service --state=enabled + +# Etat de chaque service +systemctl --user status photobooth-app +sudo systemctl status nodered +sudo systemctl status zoraxy +sudo systemctl status cups +sudo systemctl status hostapd +sudo systemctl status dnsmasq +``` + +--- + +## 14. Verification finale + +### Checklist + +```bash +# 1. Tous les services tournent ? +systemctl --user status photobooth-app # active +sudo systemctl status nodered # active +sudo systemctl status zoraxy # active +sudo systemctl status cups # active +sudo systemctl status hostapd # active +sudo systemctl status dnsmasq # active + +# 2. Tous les ports ecoutent ? +sudo ss -ltnp +# Attendu : 22, 53, 80, 443, 631, 1880, 5487, 8001, 8082, 8083 + +# 3. Camera detectee ? +rpicam-hello --list-cameras + +# 4. Imprimantes detectees ? +lpstat -v +lpstat -p + +# 5. DNS fonctionne ? +dig @127.0.0.1 photomaton.lessapinsduweb.com A +# Doit repondre 10.3.141.1 + +# 6. HTTPS fonctionne ? +curl -k https://photomaton.lessapinsduweb.com/ +# (depuis le Pi lui-meme, ou depuis un telephone connecte au WiFi) + +# 7. Memoire ? +free -h +# Verifier que Swap est present (zram + swapfile) + +# 8. Health check complet +bash scripts/health-check.sh +``` + +### Test fonctionnel complet + +1. Connecter un telephone au WiFi "Photomaton" +2. Appuyer sur le bouton physique (1 clic) +3. Verifier le countdown sur l'ecran +4. Verifier que la photo est prise et apparait dans la galerie +5. Scanner le QR code avec le telephone +6. Verifier que la photo se telecharge +7. Cliquer "Demander l'impression" dans la galerie +8. Sur le dashboard Node-RED (telephone) : valider l'impression +9. Verifier que la Selphy imprime correctement + +--- + +## 15. Post-installation : contenu a restaurer + +### Fichiers a copier depuis une sauvegarde + +```bash +# Cadres et fonds +~/photobooth-data/userdata/hopnbloc/ +~/photobooth-data/userdata/LSDW/ +~/photobooth-data/userdata/demoassets/ + +# Script d'impression +~/photobooth-data/script/script_print.sh + +# Configuration photobooth-app +~/photobooth-data/config/config.json + +# Flows Node-RED +~/.node-red/flows.json + +# Config Zoraxy (proxies) +~/zoraxy/conf/proxy/*.config + +# Config dnsmasq custom +/etc/dnsmasq.d/090_custom_local_hotspot.conf + +# Credentials OVH (pour certbot) +~/.ovhapi +``` + +### Sauvegarder le systeme + +Pour eviter de devoir tout refaire, creer une image de la carte SD : + +```bash +# Sur le PC (pas sur le Pi), avec la carte SD inseree : +sudo dd if=/dev/sdX of=photomaton-backup-$(date +%Y%m%d).img bs=4M status=progress +``` + +Ou utiliser `rpi-clone` pour cloner sur une deuxieme carte SD. diff --git a/Opencode/scripts/display_actions.py b/Opencode/scripts/display_actions.py new file mode 100644 index 0000000..e8f3691 --- /dev/null +++ b/Opencode/scripts/display_actions.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Affiche les actions image photobooth-app en detail. +Usage: echo JSON | python3 display_actions.py [--file backup.json] +""" + +import json, sys + +def display_actions(actions): + defaults_proc = { + "remove_background": False, + "fill_background_enable": False, + "fill_background_color": "#ededed", + "img_background_enable": False, + "img_background_file": None, + "image_filter": "original", + "img_frame_enable": False, + "img_frame_file": None, + "texts_enable": False, + } + defaults_ui = { + "show_button": False, + "title": "", + "icon": "photo_camera", + "use_custom_color": False, + "custom_color": "#196cb0", + } + + for i, a in enumerate(actions): + name = a["name"] + proc = a.get("processing", {}) + ui = a.get("trigger", {}).get("ui_trigger", {}) + + print(f" [{i}] {name}") + + proc_diffs = [] + for key, default in defaults_proc.items(): + val = proc.get(key, default) + if val != default and val is not None: + if key.endswith("_file") and val: + val = val.split("/")[-1] + proc_diffs.append(f"{key}: {val}") + + if proc_diffs: + print(f" processing: {', '.join(proc_diffs)}") + + show_button = ui.get("show_button", False) + if show_button: + ui_diffs = [] + for key, default in defaults_ui.items(): + val = ui.get(key, default) + if val != default: + ui_diffs.append(f"{key}: {val}") + + if ui_diffs: + print(f" ui_trigger: {', '.join(ui_diffs)}") + + print() + + print(f" Total: {len(actions)} action(s)") + + +if __name__ == "__main__": + if "--file" in sys.argv: + filepath = sys.argv[sys.argv.index("--file") + 1] + with open(filepath) as f: + data = json.load(f) + display_actions(data["image"]) + else: + data = json.load(sys.stdin) + if "actions" in data: + display_actions(data["actions"]["image"]) + elif "image" in data: + display_actions(data["image"]) diff --git a/Opencode/scripts/health-check.sh b/Opencode/scripts/health-check.sh new file mode 100644 index 0000000..c427656 --- /dev/null +++ b/Opencode/scripts/health-check.sh @@ -0,0 +1,242 @@ +#!/bin/bash +# ============================================================================= +# health-check.sh - Surveillance de sante du photomaton +# ============================================================================= +# Usage: bash health-check.sh +# Appele regulierement par le timer systemd photobooth-watchdog.timer +# ============================================================================= + +set -uo pipefail + +LOG_TAG="photomaton-watchdog" +PHOTOBOOTH_URL="http://localhost:8083" +MAX_RESTART_ATTEMPTS=3 +RESTART_COUNT_FILE="/tmp/photomaton-restart-count" + +log_info() { logger -t "$LOG_TAG" "[INFO] $1"; echo "[INFO] $1"; } +log_warn() { logger -t "$LOG_TAG" "[WARN] $1"; echo "[WARN] $1"; } +log_error() { logger -t "$LOG_TAG" "[ERROR] $1"; echo "[ERROR] $1"; } + +# Initialiser le compteur de redemarrage +if [ ! -f "$RESTART_COUNT_FILE" ]; then + echo "0" > "$RESTART_COUNT_FILE" +fi + +restart_service() { + local service="$1" + local count + count=$(cat "$RESTART_COUNT_FILE" 2>/dev/null || echo 0) + + if [ "$count" -ge "$MAX_RESTART_ATTEMPTS" ]; then + log_error "Service $service: trop de redemarrages ($count). Intervention manuelle requise." + return 1 + fi + + log_warn "Redemarrage de $service (tentative $((count + 1))/$MAX_RESTART_ATTEMPTS)..." + echo "$((count + 1))" > "$RESTART_COUNT_FILE" + + systemctl --user restart "$service" 2>/dev/null || \ + systemctl restart "$service" 2>/dev/null || true + + sleep 5 +} + +# Reset le compteur si l'app tourne bien depuis plus de 10 minutes +reset_restart_counter() { + local uptime_check + uptime_check=$(systemctl --user show photobooth-app --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2 || true) + if [ -n "$uptime_check" ]; then + local now + now=$(date +%s) + local started + started=$(date -d "$uptime_check" +%s 2>/dev/null || echo "$now") + local diff=$((now - started)) + if [ "$diff" -gt 600 ]; then + echo "0" > "$RESTART_COUNT_FILE" + fi + fi +} + +# ------------------------------------------------------------------- +# 1. VERIFIER LA MEMOIRE +# ------------------------------------------------------------------- +check_memory() { + local mem_available + mem_available=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) + local mem_total + mem_total=$(awk '/MemTotal/ {print $2}' /proc/meminfo) + + # Alerte si moins de 100 Mo disponibles + if [ "$mem_available" -lt 102400 ]; then + log_warn "Memoire faible: ${mem_available}kB disponible sur ${mem_total}kB total" + + # Liberer les caches + sync + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true + log_info "Caches memoire vides" + + # Re-verifier + mem_available=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) + if [ "$mem_available" -lt 51200 ]; then + log_error "Memoire critique: ${mem_available}kB - intervention necessaire" + return 1 + fi + fi + log_info "Memoire OK: ${mem_available}kB disponible" + return 0 +} + +# ------------------------------------------------------------------- +# 2. VERIFIER LA TEMPERATURE CPU +# ------------------------------------------------------------------- +check_temperature() { + local temp_file="/sys/class/thermal/thermal_zone0/temp" + if [ -f "$temp_file" ]; then + local temp + temp=$(cat "$temp_file") + local temp_c=$((temp / 1000)) + + if [ "$temp_c" -ge 80 ]; then + log_error "Temperature CPU critique: ${temp_c}C - throttling probable!" + return 1 + elif [ "$temp_c" -ge 70 ]; then + log_warn "Temperature CPU elevee: ${temp_c}C" + else + log_info "Temperature CPU OK: ${temp_c}C" + fi + fi + return 0 +} + +# ------------------------------------------------------------------- +# 3. VERIFIER PHOTOBOOTH-APP +# ------------------------------------------------------------------- +check_photobooth() { + # Verifier si le service tourne + if ! systemctl --user is-active photobooth-app &>/dev/null; then + log_error "photobooth-app n'est pas actif!" + restart_service "photobooth-app" + return 1 + fi + + # Verifier si l'interface web repond + local http_code + http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$PHOTOBOOTH_URL" 2>/dev/null || echo "000") + + if [ "$http_code" = "000" ] || [ "$http_code" = "502" ] || [ "$http_code" = "503" ]; then + log_error "photobooth-app ne repond pas (HTTP $http_code)" + restart_service "photobooth-app" + return 1 + fi + + log_info "photobooth-app OK (HTTP $http_code)" + reset_restart_counter + return 0 +} + +# ------------------------------------------------------------------- +# 4. VERIFIER CUPS / IMPRIMANTES +# ------------------------------------------------------------------- +check_printers() { + if ! systemctl is-active cups &>/dev/null; then + log_error "CUPS n'est pas actif!" + systemctl restart cups 2>/dev/null || true + return 1 + fi + + # Verifier les imprimantes + local disabled_printers + disabled_printers=$(lpstat -p 2>/dev/null | grep -c "disabled" || echo "0") + + if [ "$disabled_printers" -gt 0 ]; then + log_warn "$disabled_printers imprimante(s) desactivee(s), reactivation..." + # Reactiver les imprimantes + for printer in $(lpstat -p 2>/dev/null | grep "disabled" | awk '{print $2}'); do + cupsenable "$printer" 2>/dev/null || true + log_info "Imprimante reactivee: $printer" + done + fi + + # Nettoyer les jobs bloques (plus de 10 minutes) + local stuck_jobs + stuck_jobs=$(lpstat -o 2>/dev/null | wc -l || echo "0") + if [ "$stuck_jobs" -gt 5 ]; then + log_warn "$stuck_jobs jobs d'impression en attente, nettoyage..." + cancel -a 2>/dev/null || true + fi + + log_info "Imprimantes OK" + return 0 +} + +# ------------------------------------------------------------------- +# 5. VERIFIER L'ESPACE DISQUE +# ------------------------------------------------------------------- +check_disk() { + local usage + usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%') + + if [ "$usage" -ge 90 ]; then + log_error "Espace disque critique: ${usage}% utilise" + + # Nettoyage d'urgence + # Supprimer les anciens logs + find /var/log -name "*.gz" -mtime +1 -delete 2>/dev/null || true + find /var/log -name "*.[0-9]" -mtime +1 -delete 2>/dev/null || true + # Vider le cache apt + apt-get clean 2>/dev/null || true + + log_info "Nettoyage d'urgence effectue" + return 1 + elif [ "$usage" -ge 80 ]; then + log_warn "Espace disque: ${usage}% utilise" + else + log_info "Espace disque OK: ${usage}% utilise" + fi + return 0 +} + +# ------------------------------------------------------------------- +# 6. VERIFIER LA CAMERA +# ------------------------------------------------------------------- +check_camera() { + # Verifier que la camera est detectee + if command -v libcamera-hello &>/dev/null; then + # Test rapide sans afficher d'image + if ! libcamera-hello --list-cameras 2>/dev/null | grep -q "Available cameras"; then + log_warn "Camera non detectee par libcamera" + return 1 + fi + elif [ -e /dev/video0 ]; then + log_info "Camera detectee: /dev/video0" + else + log_warn "Aucun peripherique camera detecte" + return 1 + fi + + log_info "Camera OK" + return 0 +} + +# ------------------------------------------------------------------- +# EXECUTION +# ------------------------------------------------------------------- +echo "=== Health Check Photomaton - $(date) ===" + +ERRORS=0 + +check_memory || ((ERRORS++)) +check_temperature || ((ERRORS++)) +check_photobooth || ((ERRORS++)) +check_printers || ((ERRORS++)) +check_disk || ((ERRORS++)) +check_camera || ((ERRORS++)) + +echo "" +if [ "$ERRORS" -eq 0 ]; then + log_info "Tous les checks OK" +else + log_warn "$ERRORS probleme(s) detecte(s)" +fi + +exit "$ERRORS" diff --git a/Opencode/scripts/maintenance.sh b/Opencode/scripts/maintenance.sh new file mode 100644 index 0000000..5c81f2f --- /dev/null +++ b/Opencode/scripts/maintenance.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# ============================================================================= +# maintenance.sh - Maintenance automatique du photomaton +# ============================================================================= +# Usage: bash maintenance.sh +# Appele quotidiennement par photobooth-maintenance.timer +# ============================================================================= + +set -uo pipefail + +LOG_TAG="photomaton-maintenance" +PHOTOBOOTH_DATA="${HOME}/photobooth-data" +PHOTOS_DIR="${PHOTOBOOTH_DATA}/media" +LOG_DIR="${PHOTOBOOTH_DATA}/log" + +# Nombre de jours de retention des photos (0 = garder indefiniment) +PHOTO_RETENTION_DAYS=${PHOTO_RETENTION_DAYS:-0} +# Nombre de jours de retention des logs +LOG_RETENTION_DAYS=7 +# Taille max du dossier photos en Mo (0 = pas de limite) +MAX_PHOTOS_SIZE_MB=${MAX_PHOTOS_SIZE_MB:-0} + +log_info() { logger -t "$LOG_TAG" "[INFO] $1"; echo "[INFO] $1"; } +log_warn() { logger -t "$LOG_TAG" "[WARN] $1"; echo "[WARN] $1"; } + +echo "=== Maintenance Photomaton - $(date) ===" + +# ------------------------------------------------------------------- +# 1. NETTOYAGE DES LOGS +# ------------------------------------------------------------------- +log_info "Nettoyage des logs..." + +# Logs photobooth-app +if [ -d "$LOG_DIR" ]; then + deleted=$(find "$LOG_DIR" -name "*.log" -mtime +${LOG_RETENTION_DAYS} -delete -print 2>/dev/null | wc -l) + log_info " $deleted ancien(s) log(s) photobooth supprime(s)" +fi + +# Logs systeme comprimes +find /var/log -name "*.gz" -mtime +3 -delete 2>/dev/null || true +find /var/log -name "*.[0-9]" -mtime +3 -delete 2>/dev/null || true + +# Journald - garder seulement 50 Mo +journalctl --vacuum-size=50M 2>/dev/null || true + +log_info "Logs nettoyes" + +# ------------------------------------------------------------------- +# 2. NETTOYAGE DES PHOTOS (si retention configuree) +# ------------------------------------------------------------------- +if [ "$PHOTO_RETENTION_DAYS" -gt 0 ] && [ -d "$PHOTOS_DIR" ]; then + log_info "Nettoyage des photos de plus de ${PHOTO_RETENTION_DAYS} jours..." + deleted=$(find "$PHOTOS_DIR" -type f \( -name "*.jpg" -o -name "*.png" -o -name "*.gif" \) \ + -mtime +${PHOTO_RETENTION_DAYS} -delete -print 2>/dev/null | wc -l) + log_info " $deleted photo(s) supprimee(s)" +fi + +# Limite de taille +if [ "$MAX_PHOTOS_SIZE_MB" -gt 0 ] && [ -d "$PHOTOS_DIR" ]; then + current_size=$(du -sm "$PHOTOS_DIR" 2>/dev/null | awk '{print $1}') + if [ "${current_size:-0}" -gt "$MAX_PHOTOS_SIZE_MB" ]; then + log_warn "Dossier photos: ${current_size}Mo > limite ${MAX_PHOTOS_SIZE_MB}Mo" + log_warn "Suppression des photos les plus anciennes..." + # Supprimer les plus vieux fichiers jusqu'a etre sous la limite + while [ "$(du -sm "$PHOTOS_DIR" | awk '{print $1}')" -gt "$MAX_PHOTOS_SIZE_MB" ]; do + oldest=$(find "$PHOTOS_DIR" -type f \( -name "*.jpg" -o -name "*.png" \) -printf '%T+ %p\n' 2>/dev/null | sort | head -1 | awk '{print $2}') + if [ -n "$oldest" ]; then + rm -f "$oldest" + else + break + fi + done + fi +fi + +# ------------------------------------------------------------------- +# 3. NETTOYAGE CUPS +# ------------------------------------------------------------------- +log_info "Nettoyage des jobs d'impression..." + +# Supprimer les jobs termines/en erreur +cancel -a 2>/dev/null || true + +# Reactiver les imprimantes si necessaire +for printer in $(lpstat -p 2>/dev/null | grep "disabled" | awk '{print $2}'); do + cupsenable "$printer" 2>/dev/null || true + log_info " Imprimante reactivee: $printer" +done + +log_info "Jobs d'impression nettoyes" + +# ------------------------------------------------------------------- +# 4. NETTOYAGE CACHE SYSTEME +# ------------------------------------------------------------------- +log_info "Nettoyage du cache systeme..." + +# Cache apt +apt-get clean 2>/dev/null || true +apt-get autoclean 2>/dev/null || true + +# Cache thumbnails +rm -rf ~/.cache/thumbnails/* 2>/dev/null || true + +# Fichiers temporaires +find /tmp -type f -mtime +1 -delete 2>/dev/null || true + +log_info "Cache systeme nettoye" + +# ------------------------------------------------------------------- +# 5. VERIFICATION DE L'INTEGRITE +# ------------------------------------------------------------------- +log_info "Verification de l'integrite du systeme..." + +# Verifier le filesystem (sans corriger, juste signaler) +disk_errors=$(dmesg | grep -c "EXT4-fs error" 2>/dev/null || echo "0") +if [ "$disk_errors" -gt 0 ]; then + log_warn " $disk_errors erreur(s) filesystem detectee(s) dans dmesg!" +fi + +# Espace disque +disk_usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%') +log_info " Espace disque utilise: ${disk_usage}%" + +# Uptime +uptime_info=$(uptime -p) +log_info " Uptime: $uptime_info" + +# ------------------------------------------------------------------- +# 6. RAPPORT +# ------------------------------------------------------------------- +echo "" +echo "=== Rapport de maintenance ===" +echo " Date: $(date)" +echo " Espace disque: ${disk_usage}%" +if [ -d "$PHOTOS_DIR" ]; then + photo_count=$(find "$PHOTOS_DIR" -type f \( -name "*.jpg" -o -name "*.png" -o -name "*.gif" \) 2>/dev/null | wc -l) + photo_size=$(du -sh "$PHOTOS_DIR" 2>/dev/null | awk '{print $1}') + echo " Photos: ${photo_count} fichiers (${photo_size})" +fi +echo " Uptime: $uptime_info" +echo "" + +log_info "Maintenance terminee" diff --git a/Opencode/scripts/optimize-desktop.sh b/Opencode/scripts/optimize-desktop.sh new file mode 100644 index 0000000..c228308 --- /dev/null +++ b/Opencode/scripts/optimize-desktop.sh @@ -0,0 +1,252 @@ +#!/bin/bash +# ============================================================================= +# optimize-desktop.sh - Desactiver les composants desktop inutiles en mode kiosk +# ============================================================================= +# Usage: bash optimize-desktop.sh +# Gain estime: ~200 Mo de RAM +# +# Ce script configure labwc pour ne lancer QUE Chromium en mode kiosk, +# sans barre de taches, gestionnaire de fichiers, portails desktop, etc. +# +# REVERSIBLE: un fichier de backup est cree, et on peut restaurer avec: +# bash optimize-desktop.sh --restore +# ============================================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || whoami)}" +USER_HOME=$(eval echo "~$REAL_USER") + +# ------------------------------------------------------------------- +# Mode restauration +# ------------------------------------------------------------------- +if [ "${1:-}" = "--restore" ]; then + log_info "Restauration du desktop original..." + + if [ -f "$USER_HOME/.config/labwc/autostart.backup" ]; then + cp "$USER_HOME/.config/labwc/autostart.backup" "$USER_HOME/.config/labwc/autostart" + log_info "autostart restaure" + fi + + # Reactiver les services + XDG_RUNTIME_DIR="/run/user/$(id -u "$REAL_USER")" + DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" + for service in xdg-desktop-portal xdg-desktop-portal-gtk xdg-desktop-portal-wlr; do + runuser -u "$REAL_USER" -- env \ + XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \ + DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \ + systemctl --user unmask "$service" 2>/dev/null || true + done + + log_info "Restauration terminee. Redemarrer pour appliquer." + exit 0 +fi + +echo "=============================================" +echo " Optimisation desktop pour mode kiosk" +echo " Gain estime: ~200 Mo de RAM" +echo "=============================================" +echo "" + +# ------------------------------------------------------------------- +# 1. LABWC AUTOSTART - Ne lancer que Chromium +# ------------------------------------------------------------------- +log_info "Configuration de labwc autostart..." + +LABWC_DIR="$USER_HOME/.config/labwc" +mkdir -p "$LABWC_DIR" + +# Backup de l'autostart original +if [ -f "$LABWC_DIR/autostart" ] && [ ! -f "$LABWC_DIR/autostart.backup" ]; then + cp "$LABWC_DIR/autostart" "$LABWC_DIR/autostart.backup" + log_info "Backup cree: $LABWC_DIR/autostart.backup" +fi + +# Creer un autostart minimal pour le mode kiosk +cat > "$LABWC_DIR/autostart" << 'EOF' +#!/bin/bash +# ============================================================================= +# labwc autostart - Mode kiosk photomaton +# ============================================================================= +# Ne lance que le strict necessaire: +# - pipewire (audio, requis par certains composants) +# - Chromium en mode kiosk +# +# Composants DESACTIVES (gain ~200 Mo RAM): +# - pcmanfm (gestionnaire fichiers desktop) +# - wf-panel-pi (barre de taches) +# - xdg-desktop-portal* (portails desktop) +# - polkit-mate-authentication-agent +# - gvfsd (systeme fichiers virtuels) +# ============================================================================= + +# Desactiver le screen blanking / DPMS via wlr-randr si disponible +if command -v wlr-randr &>/dev/null; then + wlr-randr --output HDMI-A-1 --on 2>/dev/null || true +fi + +# Demarrer pipewire (necessaire pour certains composants systeme) +/usr/bin/pipewire & +sleep 1 +/usr/bin/pipewire -c filter-chain.conf & +/usr/bin/wireplumber & +/usr/bin/pipewire-pulse & + +# Attendre que photobooth-app soit pret +MAX_WAIT=60 +WAITED=0 +while ! curl -s -o /dev/null --max-time 2 http://127.0.0.1:8083/ 2>/dev/null; do + sleep 2 + WAITED=$((WAITED + 2)) + if [ "$WAITED" -ge "$MAX_WAIT" ]; then + break + fi +done + +# Lancer Chromium en mode kiosk +chromium \ + --kiosk \ + --accept-lang=fr-FR \ + --incognito \ + --password-store=basic \ + --ozone-platform=wayland \ + --enable-features=OverlayScrollbar \ + --disable-features=Translate,OverscrollHistoryNavigation \ + --noerrdialogs \ + --disable-infobars \ + --disable-session-crashed-bubble \ + --disable-component-update \ + --disable-background-networking \ + --disable-sync \ + --disable-extensions \ + --disable-dev-shm-usage \ + --no-first-run \ + --start-maximized \ + --process-per-site \ + --js-flags="--max-old-space-size=128" \ + --renderer-process-limit=1 \ + "http://127.0.0.1:8083/" & + +EOF + +chmod +x "$LABWC_DIR/autostart" +chown "$REAL_USER:$REAL_USER" "$LABWC_DIR/autostart" +log_info "labwc autostart kiosk cree" + +# ------------------------------------------------------------------- +# 2. DESACTIVER LES PORTAILS DESKTOP (services utilisateur) +# ------------------------------------------------------------------- +log_info "Desactivation des portails desktop..." + +# Ces services sont lances automatiquement par D-Bus +# On les masque pour qu'ils ne demarrent plus +# Utiliser machinectl ou runuser pour eviter les demandes de mot de passe +# quand le script est lance avec sudo +XDG_RUNTIME_DIR="/run/user/$(id -u "$REAL_USER")" +DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" + +for portal_svc in xdg-desktop-portal.service xdg-desktop-portal-gtk.service xdg-desktop-portal-wlr.service; do + runuser -u "$REAL_USER" -- env \ + XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \ + DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \ + systemctl --user mask "$portal_svc" 2>/dev/null || true + runuser -u "$REAL_USER" -- env \ + XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \ + DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \ + systemctl --user stop "$portal_svc" 2>/dev/null || true +done + +log_info "Portails desktop desactives" + +# ------------------------------------------------------------------- +# 3. DESACTIVER LES SERVICES SYSTEME INUTILES +# ------------------------------------------------------------------- +log_info "Desactivation des services systeme inutiles..." + +SERVICES_TO_DISABLE=( + # ModemManager - pas de modem + "ModemManager.service" + # accounts-daemon - gestion comptes, inutile en kiosk + "accounts-daemon.service" + # colord - gestion couleurs, inutile + "colord.service" + # rpcbind - NFS/RPC, inutile + "rpcbind.service" + "rpcbind.socket" + # udisks2 - gestion disques amovibles, inutile en kiosk + # "udisks2.service" # Garder si on branche des cles USB + # upower - gestion alimentation, inutile sur Pi fixe + "upower.service" +) + +for service in "${SERVICES_TO_DISABLE[@]}"; do + if systemctl is-enabled "$service" &>/dev/null 2>&1; then + systemctl disable "$service" 2>/dev/null || true + systemctl stop "$service" 2>/dev/null || true + log_info " Desactive: $service" + elif systemctl is-active "$service" &>/dev/null 2>&1; then + systemctl stop "$service" 2>/dev/null || true + log_info " Arrete: $service" + fi +done + +# blkmapd - NFS block mapping, inutile +systemctl disable blkmapd.service 2>/dev/null || true +systemctl stop blkmapd.service 2>/dev/null || true + +log_info "Services systeme inutiles desactives" + +# ------------------------------------------------------------------- +# 4. DESACTIVER PHP-FPM SI RASPAP N'EN A PAS BESOIN IMMEDIAT +# ------------------------------------------------------------------- +# Note: RaspAP utilise php-fpm via lighttpd +# On le garde car RaspAP en a besoin pour l'interface admin +# Mais on peut reduire les workers +if [ -f /etc/php/8.4/fpm/pool.d/www.conf ]; then + log_info "Optimisation php-fpm (reduction des workers)..." + sed -i 's/^pm.max_children = .*/pm.max_children = 2/' /etc/php/8.4/fpm/pool.d/www.conf 2>/dev/null || true + sed -i 's/^pm.start_servers = .*/pm.start_servers = 1/' /etc/php/8.4/fpm/pool.d/www.conf 2>/dev/null || true + sed -i 's/^pm.min_spare_servers = .*/pm.min_spare_servers = 1/' /etc/php/8.4/fpm/pool.d/www.conf 2>/dev/null || true + sed -i 's/^pm.max_spare_servers = .*/pm.max_spare_servers = 1/' /etc/php/8.4/fpm/pool.d/www.conf 2>/dev/null || true + systemctl restart php8.4-fpm 2>/dev/null || true + log_info "php-fpm optimise (max 2 workers)" +fi + +# ------------------------------------------------------------------- +# RESUME +# ------------------------------------------------------------------- +echo "" +echo "=============================================" +echo " Optimisation desktop terminee !" +echo "=============================================" +echo "" +echo "Composants desactives:" +echo " - pcmanfm (gestionnaire fichiers) ~38 Mo" +echo " - wf-panel-pi (barre de taches) ~37 Mo" +echo " - xdg-desktop-portal* (portails) ~77 Mo" +echo " - polkit-mate-auth-agent ~13 Mo" +echo " - gvfsd + volume monitors ~30 Mo" +echo " - ModemManager ~6 Mo" +echo " - accounts-daemon ~7 Mo" +echo " - colord ~10 Mo" +echo " - rpcbind + blkmapd ~4 Mo" +echo " - upower ~9 Mo" +echo " ----------------------------------------" +echo " Total estime: ~231 Mo" +echo "" +echo -e "${YELLOW}IMPORTANT: Redemarrer pour appliquer les changements${NC}" +echo " sudo reboot" +echo "" +echo "Pour restaurer le desktop original:" +echo " bash $0 --restore" +echo " sudo reboot" +echo "" diff --git a/Opencode/scripts/optimize-system.sh b/Opencode/scripts/optimize-system.sh new file mode 100644 index 0000000..978f1c4 --- /dev/null +++ b/Opencode/scripts/optimize-system.sh @@ -0,0 +1,374 @@ +#!/bin/bash +# ============================================================================= +# optimize-system.sh - Optimisation Raspberry Pi 4 (2 Go RAM) pour photomaton +# ============================================================================= +# Usage: sudo bash optimize-system.sh +# A executer UNE SEULE FOIS lors du setup initial, puis reboot +# ============================================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +if [ "$EUID" -ne 0 ]; then + log_error "Ce script doit etre execute en root (sudo)" + exit 1 +fi + +echo "=============================================" +echo " Optimisation systeme pour Photomaton" +echo " Raspberry Pi 4 - 2 Go RAM" +echo " Raspberry Pi OS / Debian 13 Trixie" +echo "=============================================" +echo "" + +# ------------------------------------------------------------------- +# 0. DETECTION DE L'ENVIRONNEMENT +# ------------------------------------------------------------------- +log_info "Detection de l'environnement..." + +# Config.txt : priorite a /boot/firmware/config.txt (Trixie) +CONFIG_FILE="" +if [ -f /boot/firmware/config.txt ]; then + CONFIG_FILE="/boot/firmware/config.txt" +elif [ -f /boot/config.txt ]; then + CONFIG_FILE="/boot/config.txt" +fi +log_info " config.txt: ${CONFIG_FILE:-'non trouve'}" + +# Chromium : s'appelle 'chromium' sous Trixie (pas 'chromium-browser') +CHROMIUM_BIN="" +if command -v chromium &>/dev/null; then + CHROMIUM_BIN="chromium" +elif command -v chromium-browser &>/dev/null; then + CHROMIUM_BIN="chromium-browser" +fi +log_info " Chromium: ${CHROMIUM_BIN:-'non trouve'}" + +# OS +OS_PRETTY=$(grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'"' -f2 || echo "inconnu") +log_info " OS: $OS_PRETTY" +echo "" + +# ------------------------------------------------------------------- +# 1. SWAP - Augmenter pour compenser les 2 Go de RAM +# ------------------------------------------------------------------- +log_info "Configuration du swap..." + +# Verifier si zram est deja actif +if zramctl 2>/dev/null | grep -q "zram"; then + log_info "zram deja actif, on le conserve" +else + # Installer zram si pas present (voir section 5) + log_info "zram non detecte, sera installe plus bas" +fi + +# Ajouter un swap fichier sur disque en COMPLEMENT de zram +# C'est un filet de securite: si zram est plein, le systeme utilise le disque +# au lieu de faire un OOM kill (crash) +if ! swapon --show | grep -q "/swapfile"; then + if [ ! -f /swapfile ]; then + log_info "Creation d'un swapfile de 1 Go (filet de securite en complement de zram)..." + fallocate -l 1G /swapfile 2>/dev/null || dd if=/dev/zero of=/swapfile bs=1M count=1024 + chmod 600 /swapfile + mkswap /swapfile + log_info "Swapfile 1 Go cree" + fi + swapon -p 10 /swapfile 2>/dev/null || true + # Ajouter au fstab si pas deja present + if ! grep -q '/swapfile' /etc/fstab; then + echo '/swapfile none swap sw,pri=10 0 0' >> /etc/fstab + fi + log_info "Swapfile active avec priorite 10 (zram priorite 100 = utilise en premier)" +else + log_info "Swapfile deja actif" +fi + +# Reduire le swappiness (60 par defaut est trop eleve pour 2 Go) +# 10 = le systeme utilise la RAM au maximum avant de swapper +echo 'vm.swappiness=10' > /etc/sysctl.d/99-photomaton-swap.conf +sysctl -p /etc/sysctl.d/99-photomaton-swap.conf 2>/dev/null || true +log_info "Swappiness regle a 10 (etait probablement a 60)" + +# ------------------------------------------------------------------- +# 2. GPU MEMORY - Allouer assez pour la camera sans gaspiller +# ------------------------------------------------------------------- +log_info "Configuration memoire GPU..." + +if [ -n "$CONFIG_FILE" ] && [ -f "$CONFIG_FILE" ]; then + # 128 Mo pour la camera Module 3 (minimum recommande) + # Ne pas mettre plus, chaque Mo compte avec 2 Go de RAM + if grep -q '^gpu_mem=' "$CONFIG_FILE"; then + sed -i 's/^gpu_mem=.*/gpu_mem=128/' "$CONFIG_FILE" + else + echo 'gpu_mem=128' >> "$CONFIG_FILE" + fi + + # S'assurer que la camera est activee (pour libcamera) + # Sur Bookworm+, camera_auto_detect est par defaut + if ! grep -q 'camera_auto_detect' "$CONFIG_FILE"; then + echo 'camera_auto_detect=1' >> "$CONFIG_FILE" + fi + + log_info "GPU memory = 128 Mo, camera auto-detect active" +else + log_warn "Fichier config.txt non trouve, configuration GPU manuelle requise" +fi + +# ------------------------------------------------------------------- +# 3. DESACTIVER LES SERVICES INUTILES pour liberer la RAM +# ------------------------------------------------------------------- +log_info "Desactivation des services inutiles..." + +# Liste des services a desactiver (adapter selon tes besoins) +SERVICES_TO_DISABLE=( + # Bluetooth (inutile pour le photomaton) + "bluetooth.service" + "hciuart.service" + # ModemManager (pas de modem) + "ModemManager.service" + # Triggerhappy (raccourcis clavier inutiles) + "triggerhappy.service" + # Avahi (decouverte reseau, peut interrerer) + # "avahi-daemon.service" # Garder si CUPS en a besoin + # Apt services automatiques (consomment RAM/CPU en arriere-plan) + "apt-daily.timer" + "apt-daily-upgrade.timer" + "apt-daily.service" + "apt-daily-upgrade.service" + # Man-db auto update + "man-db.timer" + # Packagekit (si installe) + "packagekit.service" + # RPC bind (NFS/RPC, totalement inutile pour un photomaton) + "rpcbind.service" + "rpcbind.socket" +) + +for service in "${SERVICES_TO_DISABLE[@]}"; do + if systemctl is-enabled "$service" &>/dev/null; then + systemctl disable "$service" 2>/dev/null || true + systemctl stop "$service" 2>/dev/null || true + log_info " Desactive: $service" + fi +done + +# Desactiver le Bluetooth au niveau kernel aussi +if [ -f "$CONFIG_FILE" ]; then + if ! grep -q 'dtoverlay=disable-bt' "$CONFIG_FILE"; then + echo 'dtoverlay=disable-bt' >> "$CONFIG_FILE" + log_info " Bluetooth desactive au niveau kernel" + fi +fi + +# ------------------------------------------------------------------- +# 4. OPTIMISATION MEMOIRE - Parametres kernel +# ------------------------------------------------------------------- +log_info "Optimisation parametres memoire kernel..." + +cat > /etc/sysctl.d/99-photomaton-memory.conf << 'EOF' +# Reduire la pression memoire cache +vm.vfs_cache_pressure=200 +# Ecriture sur disque plus frequente (eviter accumulation en RAM) +vm.dirty_ratio=10 +vm.dirty_background_ratio=5 +# Limiter la reserve memoire minimale +vm.min_free_kbytes=16384 +# OOM killer plus agressif sur les gros processus +vm.oom_kill_allocating_task=1 +EOF + +sysctl -p /etc/sysctl.d/99-photomaton-memory.conf 2>/dev/null || true +log_info "Parametres memoire appliques" + +# ------------------------------------------------------------------- +# 5. ZRAM - Compression memoire (doublement effectif de la RAM) +# ------------------------------------------------------------------- +log_info "Verification de zram..." + +if zramctl 2>/dev/null | grep -q "zram"; then + log_info "zram deja actif et fonctionnel ($(zramctl --output NAME,ALGORITHM,DISKSIZE --noheadings))" + log_info "Aucune modification necessaire" +else + log_info "zram non detecte, installation..." + if ! dpkg -l | grep -q zram-tools; then + apt-get update -qq + apt-get install -y -qq zram-tools + fi + + cat > /etc/default/zramswap << 'EOF' +# Utiliser 50% de la RAM pour zram (compresse ~2x, donc ~2Go supplementaires) +ALGO=zstd +PERCENT=50 +PRIORITY=100 +EOF +fi + +systemctl enable zramswap 2>/dev/null || true +systemctl restart zramswap 2>/dev/null || true +log_info "zram configure (compression memoire active)" + +# ------------------------------------------------------------------- +# 6. TMPFS - Reduire les ecritures disque et accelerer +# ------------------------------------------------------------------- +log_info "Configuration tmpfs..." + +# Monter /tmp en RAM (accelere les fichiers temporaires) +if ! grep -q 'tmpfs.*/tmp' /etc/fstab; then + echo 'tmpfs /tmp tmpfs defaults,noatime,nosuid,nodev,size=256M 0 0' >> /etc/fstab + log_info " /tmp monte en tmpfs (256 Mo)" +fi + +# Logs en RAM pour reduire les ecritures SD (optionnel mais recommande) +if ! grep -q 'tmpfs.*/var/log' /etc/fstab; then + echo 'tmpfs /var/log tmpfs defaults,noatime,nosuid,nodev,size=64M 0 0' >> /etc/fstab + log_info " /var/log monte en tmpfs (64 Mo) - ATTENTION: logs perdus au reboot" +fi + +# ------------------------------------------------------------------- +# 7. CHROMIUM / NAVIGATEUR - Optimisation kiosk mode +# ------------------------------------------------------------------- +log_info "Creation du script de lancement Chromium kiosk..." + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIOSK_SCRIPT="${SCRIPT_DIR}/../config/chromium-kiosk.sh" + +cat > "$KIOSK_SCRIPT" << 'KIOSK_EOF' +#!/bin/bash +# Lancement de Chromium en mode kiosk pour photomaton +# Raspberry Pi OS Trixie (Wayland) +# A utiliser dans un fichier .desktop dans ~/Desktop/ + +# Attendre que photobooth-app soit pret +MAX_WAIT=60 +WAITED=0 +while ! curl -s -o /dev/null --max-time 2 http://127.0.0.1:8083/ 2>/dev/null; do + sleep 2 + WAITED=$((WAITED + 2)) + if [ "$WAITED" -ge "$MAX_WAIT" ]; then + echo "photobooth-app non pret apres ${MAX_WAIT}s, lancement quand meme" + break + fi +done + +# Detecter le binaire Chromium (chromium sous Trixie, chromium-browser avant) +CHROMIUM=$(command -v chromium || command -v chromium-browser || echo "chromium") + +# Lancer Chromium en mode kiosk +# - Wayland natif (ozone-platform=wayland) +# - Optimisations memoire pour Pi 4 2Go +"$CHROMIUM" \ + --kiosk \ + --accept-lang=fr-FR \ + --incognito \ + --password-store=basic \ + --ozone-platform=wayland \ + --enable-features=OverlayScrollbar \ + --disable-features=Translate,OverscrollHistoryNavigation \ + --noerrdialogs \ + --disable-infobars \ + --disable-session-crashed-bubble \ + --disable-component-update \ + --disable-background-networking \ + --disable-sync \ + --disable-extensions \ + --disable-dev-shm-usage \ + --no-first-run \ + --start-maximized \ + --process-per-site \ + --js-flags="--max-old-space-size=128" \ + --renderer-process-limit=1 \ + "http://127.0.0.1:8083/" +KIOSK_EOF + +chmod +x "$KIOSK_SCRIPT" +log_info "Script kiosk Chromium cree" + +# ------------------------------------------------------------------- +# 8. OVERCLOCK LEGER (optionnel, stable) +# ------------------------------------------------------------------- +log_info "Configuration overclock leger..." + +if [ -f "$CONFIG_FILE" ]; then + # Overclock leger et stable pour le Pi 4 + # over_voltage=2 est tres conservateur + if ! grep -q '^arm_freq=' "$CONFIG_FILE"; then + cat >> "$CONFIG_FILE" << 'EOF' + +# Overclock leger pour photomaton (stable) +# Decommenter pour activer: +#arm_freq=1800 +#over_voltage=2 +EOF + log_info "Overclock leger prepare (desactive par defaut, decommenter si souhaite)" + fi +fi + +# ------------------------------------------------------------------- +# 9. CGROUPS - Limiter la memoire de Node-RED si encore utilise +# ------------------------------------------------------------------- +log_info "Configuration des limites memoire..." + +# Si Node-RED est installe, creer un override pour limiter sa RAM +if systemctl is-enabled nodered.service &>/dev/null 2>&1; then + mkdir -p /etc/systemd/system/nodered.service.d/ + cat > /etc/systemd/system/nodered.service.d/memory-limit.conf << 'EOF' +[Service] +MemoryMax=256M +MemorySwapMax=128M +EOF + systemctl daemon-reload + log_info "Node-RED limite a 256 Mo RAM max" +fi + +# ------------------------------------------------------------------- +# 10. RESEAU - Optimiser pour le WiFi hotspot +# ------------------------------------------------------------------- +log_info "Optimisation reseau..." + +cat > /etc/sysctl.d/99-photomaton-network.conf << 'EOF' +# Optimiser pour servir les photos via WiFi +net.core.somaxconn=256 +net.ipv4.tcp_fastopen=3 +net.core.netdev_max_backlog=1000 +EOF + +sysctl -p /etc/sysctl.d/99-photomaton-network.conf 2>/dev/null || true +log_info "Parametres reseau optimises" + +# ------------------------------------------------------------------- +# RESUME +# ------------------------------------------------------------------- +echo "" +echo "=============================================" +echo " Optimisation terminee !" +echo "=============================================" +echo "" +echo "Environnement: $OS_PRETTY" +echo "config.txt: ${CONFIG_FILE:-'non trouve'}" +echo "" +echo "Modifications appliquees:" +echo " - Swapfile 1 Go ajoute (filet de securite en complement de zram)" +echo " - Swappiness reduit a 10" +echo " - GPU memory = 128 Mo (si config.txt present)" +echo " - Services inutiles desactives" +echo " - Bluetooth desactive" +echo " - zram conserve/installe (compression memoire)" +echo " - tmpfs pour /tmp et /var/log" +echo " - Parametres memoire kernel optimises" +echo " - Script kiosk Chromium cree (${CHROMIUM_BIN:-chromium})" +echo " - Limites memoire Node-RED (si present)" +echo "" +echo -e "${YELLOW}IMPORTANT: Redemarrer le Raspberry Pi pour appliquer tous les changements${NC}" +echo " sudo reboot" +echo "" +echo -e "${YELLOW}NOTE: /var/log est en tmpfs - les logs seront perdus au reboot.${NC}" +echo " Si vous voulez conserver les logs, commentez la ligne tmpfs /var/log dans /etc/fstab" +echo "" diff --git a/Opencode/scripts/photobooth-config.sh b/Opencode/scripts/photobooth-config.sh new file mode 100644 index 0000000..d9de3f2 --- /dev/null +++ b/Opencode/scripts/photobooth-config.sh @@ -0,0 +1,761 @@ +#!/bin/bash +# ============================================================================= +# photobooth-config.sh - Gestion rapide de la config photobooth-app +# ============================================================================= +# Usage: +# bash photobooth-config.sh quality high|low|status +# bash photobooth-config.sh livestream on|off|status +# bash photobooth-config.sh frontpage manuel|direct|noprint|status +# bash photobooth-config.sh restart +# ============================================================================= + +set -uo pipefail + +API_URL="http://127.0.0.1:8083" +API_USER="admin" +API_PASS="PhotoBooth2026!" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +NC='\033[0m' + +# --- Auth --- +get_token() { + curl -s -X POST "${API_URL}/api/admin/auth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password&username=${API_USER}&password=${API_PASS}" \ + | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])' +} + +get_config() { + local token="$1" + curl -s "${API_URL}/api/admin/config/app" \ + -H "Authorization: Bearer ${token}" +} + +set_config() { + local token="$1" + local config="$2" + curl -s -X PATCH "${API_URL}/api/admin/config/app" \ + -H "Authorization: Bearer ${token}" \ + -H "Content-Type: application/json" \ + -d "${config}" > /dev/null +} + +# ============================================================================= +# QUALITE +# ============================================================================= +quality_status() { + local token + token=$(get_token) + local config + config=$(get_config "$token") + + local capture_w quality + capture_w=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["backends"]["group_backends"][0]["backend_config"]["CAPTURE_CAM_RESOLUTION_WIDTH"])') + quality=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["backends"]["group_backends"][0]["backend_config"]["original_still_quality"])') + + if [ "$capture_w" -ge 4000 ]; then + echo -e "Qualite: ${GREEN}HAUTE${NC} (${capture_w}px, quality ${quality})" + else + echo -e "Qualite: ${YELLOW}BASSE${NC} (${capture_w}px, quality ${quality})" + fi +} + +quality_high() { + local token + token=$(get_token) + + set_config "$token" '{ + "backends": { + "group_backends": [{ + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + }] + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960 + } + }' + + echo -e "${GREEN}Qualite HAUTE activee${NC} (4608x3072, quality 100)" + echo " -> systemctl --user restart photobooth-app" +} + +quality_low() { + local token + token=$(get_token) + + set_config "$token" '{ + "backends": { + "group_backends": [{ + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 2304, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 1536, + "PREVIEW_CAM_RESOLUTION_WIDTH": 1152, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 768, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 90 + } + }] + }, + "mediaprocessing": { + "full_still_length": 2304, + "preview_still_length": 1200, + "thumbnail_still_length": 480 + } + }' + + echo -e "${YELLOW}Qualite BASSE activee${NC} (2304x1536, quality 90)" + echo " -> systemctl --user restart photobooth-app" +} + +# ============================================================================= +# LIVESTREAM +# ============================================================================= +livestream_status() { + local token + token=$(get_token) + local config + config=$(get_config "$token") + + local idle_stream + idle_stream=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["uisettings"]["enable_livestream_when_idle"])') + + if [ "$idle_stream" = "True" ]; then + echo -e "Livestream idle: ${GREEN}ACTIVE${NC}" + else + echo -e "Livestream idle: ${RED}DESACTIVE${NC}" + fi +} + +livestream_on() { + local token + token=$(get_token) + + set_config "$token" '{ + "uisettings": { + "enable_livestream_when_idle": true + } + }' + + echo -e "${GREEN}Livestream idle ACTIVE${NC}" + echo " Les visiteurs voient la camera en direct sur l'ecran d'accueil" +} + +livestream_off() { + local token + token=$(get_token) + + set_config "$token" '{ + "uisettings": { + "enable_livestream_when_idle": false + } + }' + + echo -e "${RED}Livestream idle DESACTIVE${NC}" + echo " L'ecran d'accueil affiche le slideshow" +} + +# ============================================================================= +# FRONTPAGE TEXT +# ============================================================================= +frontpage_status() { + local token + token=$(get_token) + local config + config=$(get_config "$token") + + local text + text=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);t=c["uisettings"]["FRONTPAGE_TEXT"];print("impression" if "Impression" in t else "noprint" if "Appui long" not in t else "unknown")') + + echo -e "Frontpage: ${CYAN}${text}${NC}" +} + +# --- HTML templates --- +FRONTPAGE_MANUEL='
Bienvenue !
Appui court = Photo
Appui long (3s) = Demande d'\''impression
Photomaton propos\u00e9 par Les Sapins Du Web. En l'\''utilisant, vous acceptez que les photos puissent etre utilis\u00e9es par Les Sapins Du Web et ses partenaires.
Les Sapins Du Web
' + +FRONTPAGE_DIRECT='
Bienvenue !
Appui court = Photo
Appui long (3s) = Impression
Photomaton propos\u00e9 par Les Sapins Du Web. En l'\''utilisant, vous acceptez que les photos puissent etre utilis\u00e9es par Les Sapins Du Web et ses partenaires.
Les Sapins Du Web
' + +FRONTPAGE_NOPRINT='
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton propos\u00e9 par Les Sapins Du Web. En l'\''utilisant, vous acceptez que les photos puissent etre utilis\u00e9es par Les Sapins Du Web et ses partenaires.
Les Sapins Du Web
' + +frontpage_set() { + local mode="$1" + local token + token=$(get_token) + + local html="" + case "$mode" in + manuel) + html="$FRONTPAGE_MANUEL" + ;; + direct) + html="$FRONTPAGE_DIRECT" + ;; + noprint) + html="$FRONTPAGE_NOPRINT" + ;; + esac + + # Utiliser python3 pour construire le JSON proprement (gestion des quotes) + python3 -c " +import json, urllib.request +data = json.dumps({'uisettings': {'FRONTPAGE_TEXT': '''${html}'''}}).encode() +req = urllib.request.Request('${API_URL}/api/admin/config/app', data=data, method='PATCH') +req.add_header('Authorization', 'Bearer ${token}') +req.add_header('Content-Type', 'application/json') +urllib.request.urlopen(req) +" + + case "$mode" in + manuel) + echo -e "${CYAN}Frontpage: MODE MANUEL${NC}" + echo " Appui court = Photo / Appui long = Demande d'impression" + ;; + direct) + echo -e "${CYAN}Frontpage: MODE DIRECT${NC}" + echo " Appui court = Photo / Appui long = Impression" + ;; + noprint) + echo -e "${CYAN}Frontpage: SANS IMPRESSION${NC}" + echo " Appuyez sur le bouton pour prendre une photo" + ;; + esac +} + +# ============================================================================= +# ACTIONS IMAGE +# ============================================================================= +ACTIONS_DIR="${HOME}/photobooth-data/actions-backups" + +actions_init() { + mkdir -p "$ACTIONS_DIR" +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DISPLAY_ACTIONS_PY="${SCRIPT_DIR}/display_actions.py" + +actions_list() { + local token + token=$(get_token) + local config + config=$(get_config "$token") + + echo "=== Actions image actuelles ===" + echo "$config" | python3 "$DISPLAY_ACTIONS_PY" +} + +actions_backup() { + local name="$1" + actions_init + local token + token=$(get_token) + local config + config=$(get_config "$token") + + local filepath="${ACTIONS_DIR}/${name}.json" + + echo "$config" | python3 -c " +import sys, json +c = json.load(sys.stdin) +with open('${filepath}', 'w') as f: + json.dump(c['actions'], f, indent=2, ensure_ascii=False) +" + + local count + count=$(python3 -c "import json;print(len(json.load(open('${filepath}'))['image']))") + echo -e "${GREEN}Backup sauvegarde: ${filepath}${NC}" + echo " ${count} action(s) image" +} + +actions_restore() { + local name="$1" + actions_init + local filepath="${ACTIONS_DIR}/${name}.json" + + if [ ! -f "$filepath" ]; then + echo -e "${RED}Backup introuvable: ${filepath}${NC}" + echo "Backups disponibles:" + actions_list_backups + return 1 + fi + + local token + token=$(get_token) + + python3 -c " +import json, urllib.request +with open('${filepath}') as f: + actions = json.load(f) +data = json.dumps({'actions': actions}).encode() +req = urllib.request.Request('${API_URL}/api/admin/config/app', data=data, method='PATCH') +req.add_header('Authorization', 'Bearer ${token}') +req.add_header('Content-Type', 'application/json') +urllib.request.urlopen(req) +" + + local count + count=$(python3 -c "import json;print(len(json.load(open('${filepath}'))['image']))") + echo -e "${GREEN}Backup restaure: ${name}${NC}" + echo " ${count} action(s) image chargees" + echo " -> systemctl --user restart photobooth-app" +} + +actions_list_backups() { + actions_init + echo "=== Backups disponibles ===" + local found=false + for f in "${ACTIONS_DIR}"/*.json; do + if [ -f "$f" ]; then + found=true + local basename + basename=$(basename "$f" .json) + local count + count=$(python3 -c "import json;print(len(json.load(open('${f}'))['image']))" 2>/dev/null || echo "?") + local date + date=$(stat -c '%y' "$f" 2>/dev/null | cut -d. -f1) + echo " ${basename} (${count} actions, ${date})" + fi + done + if ! $found; then + echo " (aucun)" + fi +} + +actions_show() { + local name="$1" + actions_init + local filepath="${ACTIONS_DIR}/${name}.json" + + if [ ! -f "$filepath" ]; then + echo -e "${RED}Backup introuvable: ${filepath}${NC}" + return 1 + fi + + echo "=== Backup: ${name} ===" + python3 "$DISPLAY_ACTIONS_PY" --file "$filepath" +} + +actions_move() { + local from="$1" + local to="$2" + local token + token=$(get_token) + local tmpfile + tmpfile=$(mktemp /tmp/photobooth-config-XXXXXX.json) + get_config "$token" > "$tmpfile" + + python3 -c " +import json, urllib.request, sys +with open('${tmpfile}') as f: + config = json.load(f) +actions = config['actions']['image'] +count = len(actions) +f, t = int('${from}'), int('${to}') +if f < 0 or f >= count or t < 0 or t >= count: + print(f'Erreur: index invalide (0-{count-1})') + sys.exit(1) +item = actions.pop(f) +actions.insert(t, item) +config['actions']['image'] = actions +data = json.dumps({'actions': config['actions']}).encode() +req = urllib.request.Request('${API_URL}/api/admin/config/app', data=data, method='PATCH') +req.add_header('Authorization', 'Bearer ${token}') +req.add_header('Content-Type', 'application/json') +urllib.request.urlopen(req) +print(f'Action [{f}] deplacee vers [{t}]') +for i, a in enumerate(actions): + marker = ' <--' if i == t else '' + print(f' [{i}] {a[\"name\"]}{marker}') +" + rm -f "$tmpfile" +} + +actions_enable() { + local index="$1" + local token + token=$(get_token) + local tmpfile + tmpfile=$(mktemp /tmp/photobooth-config-XXXXXX.json) + get_config "$token" > "$tmpfile" + + python3 -c " +import json, urllib.request, sys +with open('${tmpfile}') as f: + config = json.load(f) +actions = config['actions']['image'] +idx = int('${index}') +if idx < 0 or idx >= len(actions): + print(f'Erreur: index invalide (0-{len(actions)-1})') + sys.exit(1) +actions[idx]['trigger']['ui_trigger']['show_button'] = True +data = json.dumps({'actions': config['actions']}).encode() +req = urllib.request.Request('${API_URL}/api/admin/config/app', data=data, method='PATCH') +req.add_header('Authorization', 'Bearer ${token}') +req.add_header('Content-Type', 'application/json') +urllib.request.urlopen(req) +print(f'Action [{idx}] {actions[idx][\"name\"]} : bouton VISIBLE') +" + rm -f "$tmpfile" +} + +actions_disable() { + local index="$1" + local token + token=$(get_token) + local tmpfile + tmpfile=$(mktemp /tmp/photobooth-config-XXXXXX.json) + get_config "$token" > "$tmpfile" + + python3 -c " +import json, urllib.request, sys +with open('${tmpfile}') as f: + config = json.load(f) +actions = config['actions']['image'] +idx = int('${index}') +if idx < 0 or idx >= len(actions): + print(f'Erreur: index invalide (0-{len(actions)-1})') + sys.exit(1) +actions[idx]['trigger']['ui_trigger']['show_button'] = False +data = json.dumps({'actions': config['actions']}).encode() +req = urllib.request.Request('${API_URL}/api/admin/config/app', data=data, method='PATCH') +req.add_header('Authorization', 'Bearer ${token}') +req.add_header('Content-Type', 'application/json') +urllib.request.urlopen(req) +print(f'Action [{idx}] {actions[idx][\"name\"]} : bouton CACHE') +" + rm -f "$tmpfile" +} + +actions_duplicate() { + local index="$1" + local token + token=$(get_token) + local tmpfile + tmpfile=$(mktemp /tmp/photobooth-config-XXXXXX.json) + get_config "$token" > "$tmpfile" + + python3 -c " +import json, urllib.request, copy, sys +with open('${tmpfile}') as f: + config = json.load(f) +actions = config['actions']['image'] +idx = int('${index}') +if idx < 0 or idx >= len(actions): + print(f'Erreur: index invalide (0-{len(actions)-1})') + sys.exit(1) +new_action = copy.deepcopy(actions[idx]) +new_action['name'] = new_action['name'] + ' (copie)' +new_action['trigger']['ui_trigger']['show_button'] = False +actions.insert(idx + 1, new_action) +data = json.dumps({'actions': config['actions']}).encode() +req = urllib.request.Request('${API_URL}/api/admin/config/app', data=data, method='PATCH') +req.add_header('Authorization', 'Bearer ${token}') +req.add_header('Content-Type', 'application/json') +urllib.request.urlopen(req) +print(f'Action [{idx}] dupliquee vers [{idx+1}]') +for i, a in enumerate(actions): + marker = ' <-- copie' if i == idx + 1 else '' + print(f' [{i}] {a[\"name\"]}{marker}') +" + rm -f "$tmpfile" +} + +actions_remove() { + local indices="$@" + local token + token=$(get_token) + local tmpfile + tmpfile=$(mktemp /tmp/photobooth-config-XXXXXX.json) + get_config "$token" > "$tmpfile" + + python3 -c " +import json, urllib.request, sys +with open('${tmpfile}') as f: + config = json.load(f) +actions = config['actions']['image'] +indices = sorted([int(x) for x in '${indices}'.split()], reverse=True) + +for idx in indices: + if idx < 0 or idx >= len(actions): + print(f'Erreur: index {idx} invalide (0-{len(actions)-1})') + sys.exit(1) + +removed = [] +for idx in indices: + removed.append(f'[{idx}] {actions[idx][\"name\"]}') + actions.pop(idx) + +data = json.dumps({'actions': config['actions']}).encode() +req = urllib.request.Request('${API_URL}/api/admin/config/app', data=data, method='PATCH') +req.add_header('Authorization', 'Bearer ${token}') +req.add_header('Content-Type', 'application/json') +urllib.request.urlopen(req) + +print('Actions supprimees:') +for r in removed: + print(f' - {r}') +print() +print('Actions restantes:') +for i, a in enumerate(actions): + print(f' [{i}] {a[\"name\"]}') +print(f' Total: {len(actions)} action(s)') +" + rm -f "$tmpfile" +} + +# ============================================================================= +# RESTART +# ============================================================================= +do_restart() { + echo "Redemarrage de photobooth-app..." + systemctl --user restart photobooth-app + sleep 3 + if systemctl --user is-active photobooth-app &>/dev/null; then + echo -e "${GREEN}photobooth-app redemarre avec succes${NC}" + else + echo -e "${RED}ERREUR: photobooth-app n'a pas redemarre${NC}" + systemctl --user status photobooth-app + fi +} + +# ============================================================================= +# AIDE +# ============================================================================= +show_help() { + cat << 'HELP' +=============================================================================== + photobooth-config.sh - Gestion rapide de la config photobooth-app +=============================================================================== + +USAGE: photobooth-config.sh [arguments] + +COMMANDES GENERALES: + status, s Afficher la config actuelle (qualite, livestream, frontpage) + restart, r Redemarrer photobooth-app + help, -h, --help Afficher cette aide + +QUALITE D'IMAGE: + quality status Voir la qualite actuelle + quality high Haute qualite (4608x3072, JPEG 100%) + Meilleure qualite d'impression mais consomme plus de RAM + quality low Basse qualite (2304x1536, JPEG 90%) + Economise la RAM, suffisant pour impression Selphy + Note: necessite un restart apres changement + +LIVESTREAM: + livestream status Voir si le livestream idle est actif + livestream on Activer le livestream sur l'ecran d'accueil + Les visiteurs voient la camera en direct + livestream off Desactiver le livestream idle + L'ecran d'accueil affiche le slideshow + +TEXTE D'ACCUEIL (FRONTPAGE): + frontpage status Voir le mode actuel + frontpage manuel "Appui long (3s) = Demande d'impression" + Pour les events avec validation admin + frontpage direct "Appui long (3s) = Impression" + Pour les events avec impression automatique + frontpage noprint "Appuyez sur le bouton pour prendre une photo" + Pas de mention d'impression + +ACTIONS IMAGE: + actions list Lister toutes les actions avec leurs parametres + L'index [0] = simple clic, [1] = double clic, etc. + actions backup Sauvegarder les actions actuelles + Ex: actions backup hopnbloc + actions restore Restaurer un backup d'actions + Ex: actions restore hopnbloc + Note: necessite un restart apres + actions backups Lister les backups disponibles + actions show Voir le contenu d'un backup + actions move Deplacer une action dans la liste + Ex: actions move 3 0 (met l'action 3 en premier) + Important: l'ordre determine le bouton physique + [0] = simple clic + [1] = double clic + [2] = triple clic + [3] = quadruple clic + actions enable Rendre le bouton visible sur l'ecran tactile + actions disable Cacher le bouton de l'ecran tactile + actions dup Dupliquer une action (copie inseree apres) + actions remove [i...] Supprimer une ou plusieurs actions + Ex: actions remove 5 (supprime l'action 5) + Ex: actions remove 3 5 7 (supprime les actions 3, 5 et 7) + +EXEMPLES: + + # Preparer un event Hop'N Bloc + photobooth-config.sh actions backup config-avant + photobooth-config.sh actions restore hopnbloc + photobooth-config.sh frontpage manuel + photobooth-config.sh livestream on + photobooth-config.sh quality low + photobooth-config.sh restart + + # Passer en haute qualite pour un event special + photobooth-config.sh quality high + photobooth-config.sh restart + + # Apres l'event, tout remettre + photobooth-config.sh actions restore config-avant + photobooth-config.sh restart + +=============================================================================== +HELP +} + +# ============================================================================= +# MAIN +# ============================================================================= +CMD="${1:-help}" +ARG="${2:-status}" + +case "$CMD" in + quality|q) + case "$ARG" in + high|haute|hq) quality_high ;; + low|basse|lq) quality_low ;; + status|info) quality_status ;; + *) echo "Usage: $0 quality {high|low|status}" ;; + esac + ;; + livestream|ls) + case "$ARG" in + on|true|1) livestream_on ;; + off|false|0) livestream_off ;; + status|info) livestream_status ;; + *) echo "Usage: $0 livestream {on|off|status}" ;; + esac + ;; + frontpage|fp) + case "$ARG" in + manuel|manual) frontpage_set manuel ;; + direct) frontpage_set direct ;; + noprint|sans|off) frontpage_set noprint ;; + status|info) frontpage_status ;; + *) echo "Usage: $0 frontpage {manuel|direct|noprint|status}" ;; + esac + ;; + actions|a) + case "$ARG" in + list|ls) actions_list ;; + backup|save) + if [ -z "${3:-}" ]; then + echo "Usage: $0 actions backup " + echo " Exemple: $0 actions backup hopnbloc" + exit 1 + fi + actions_backup "$3" + ;; + restore|load) + if [ -z "${3:-}" ]; then + echo "Usage: $0 actions restore " + actions_list_backups + exit 1 + fi + actions_restore "$3" + ;; + backups) actions_list_backups ;; + show) + if [ -z "${3:-}" ]; then + echo "Usage: $0 actions show " + actions_list_backups + exit 1 + fi + actions_show "$3" + ;; + move|mv) + if [ -z "${3:-}" ] || [ -z "${4:-}" ]; then + echo "Usage: $0 actions move " + echo " Exemple: $0 actions move 3 0 (deplace l'action 3 en position 0)" + exit 1 + fi + actions_move "$3" "$4" + ;; + enable|on) + if [ -z "${3:-}" ]; then + echo "Usage: $0 actions enable " + actions_list + exit 1 + fi + actions_enable "$3" + ;; + disable|off) + if [ -z "${3:-}" ]; then + echo "Usage: $0 actions disable " + actions_list + exit 1 + fi + actions_disable "$3" + ;; + dup|duplicate|copy) + if [ -z "${3:-}" ]; then + echo "Usage: $0 actions dup " + actions_list + exit 1 + fi + actions_duplicate "$3" + ;; + remove|rm|del|delete) + if [ -z "${3:-}" ]; then + echo "Usage: $0 actions remove [index...]" + echo " Exemple: $0 actions remove 5" + echo " Exemple: $0 actions remove 3 5 7" + actions_list + exit 1 + fi + shift 2 + actions_remove "$@" + ;; + *) echo "Usage: $0 actions {list|backup|restore|backups|show|move|enable|disable|dup|remove}" ;; + esac + ;; + restart|r) + do_restart + ;; + status|s) + echo "=== Photobooth Config ===" + quality_status + livestream_status + frontpage_status + ;; + help|-h|--help) + show_help + ;; + *) + show_help + ;; +esac diff --git a/Opencode/scripts/ram-watchdog.sh b/Opencode/scripts/ram-watchdog.sh new file mode 100644 index 0000000..d795d19 --- /dev/null +++ b/Opencode/scripts/ram-watchdog.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# ============================================================================= +# ram-watchdog.sh - Watchdog memoire pour le photomaton +# ============================================================================= +# Usage: Lancer via cron toutes les minutes : +# * * * * * /home/pi/opencode/scripts/ram-watchdog.sh +# +# Actions par seuil : +# - Swap > 70% : vider les caches +# - Swap > 85% : redemarrer photobooth-app +# - Swap > 95% : reboot du Pi +# ============================================================================= + +LOG_TAG="ram-watchdog" +log_info() { logger -t "$LOG_TAG" "[INFO] $1"; } +log_warn() { logger -t "$LOG_TAG" "[WARN] $1"; } +log_error() { logger -t "$LOG_TAG" "[ERROR] $1"; } + +# --- Lire les valeurs memoire --- +SWAP_TOTAL=$(awk '/SwapTotal/ {print $2}' /proc/meminfo) +SWAP_FREE=$(awk '/SwapFree/ {print $2}' /proc/meminfo) +MEM_AVAILABLE=$(awk '/MemAvailable/ {print $2}' /proc/meminfo) + +# Eviter division par zero +if [ "$SWAP_TOTAL" -eq 0 ]; then + exit 0 +fi + +SWAP_USED=$((SWAP_TOTAL - SWAP_FREE)) +SWAP_PERCENT=$((SWAP_USED * 100 / SWAP_TOTAL)) +MEM_AVAILABLE_MB=$((MEM_AVAILABLE / 1024)) + +# --- Seuil 1 : Swap > 70% -> vider les caches --- +if [ "$SWAP_PERCENT" -ge 70 ] && [ "$SWAP_PERCENT" -lt 85 ]; then + log_warn "Swap a ${SWAP_PERCENT}% (RAM dispo: ${MEM_AVAILABLE_MB}Mo) - Vidage des caches" + sync + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true +fi + +# --- Seuil 2 : Swap > 85% -> redemarrer photobooth-app --- +if [ "$SWAP_PERCENT" -ge 85 ] && [ "$SWAP_PERCENT" -lt 95 ]; then + log_error "Swap a ${SWAP_PERCENT}% (RAM dispo: ${MEM_AVAILABLE_MB}Mo) - Redemarrage photobooth-app" + + # Redemarrer photobooth-app en tant que l'utilisateur pi + REAL_USER="pi" + XDG_RUNTIME_DIR="/run/user/$(id -u "$REAL_USER")" + DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" + + runuser -u "$REAL_USER" -- env \ + XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" \ + DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \ + systemctl --user restart photobooth-app 2>/dev/null || true + + log_warn "photobooth-app redemarre" + + # Attendre et vider les caches + sleep 5 + sync + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true +fi + +# --- Seuil 3 : Swap > 95% -> reboot --- +if [ "$SWAP_PERCENT" -ge 95 ]; then + log_error "CRITIQUE: Swap a ${SWAP_PERCENT}% (RAM dispo: ${MEM_AVAILABLE_MB}Mo) - REBOOT" + sync + sleep 2 + reboot +fi diff --git a/Opencode/scripts/script_print.sh b/Opencode/scripts/script_print.sh new file mode 100644 index 0000000..b2aab44 --- /dev/null +++ b/Opencode/scripts/script_print.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# ============================================================================= +# script_print.sh - Impression photo pour photomaton +# ============================================================================= +# Usage: script_print.sh "" "" "" "" +# Appele par photobooth-app via l'action Share "Impression" +# +# Imprimantes: Canon Selphy CP1300 x2 via WiFi (blanche + noire) +# Load balancing: utilise l'imprimante idle, sinon file d'attente sur la 1ere +# +# Sortie stdout parsable par Node-RED: +# PRINTED:: en cas de succes +# PRINT_ERROR: en cas d'erreur +# ============================================================================= + +set -uo pipefail + +# --- Parametres --- +filename="${1:-}" +media_type="${2:-}" +action_config_name="${3:-}" +copies="${4:-1}" + +# --- Noms des imprimantes CUPS (WiFi) --- +PRINTER_1="Selphy_Blanche_WiFi" +PRINTER_2="Selphy_Noire_WiFi" + +# --- Options d'impression Selphy --- +# Mode raw : envoie le JPEG directement a la Selphy +# Postcard.Fullbleed : impression sans bordures +PRINT_OPTIONS="-o media=Postcard.Fullbleed -o raw" + +# --- Calibrage bordures pour impression Fullbleed --- +# La Selphy deborde de quelques mm en mode Fullbleed +# On ajoute des marges noires pour compenser et eviter de couper la photo +# Valeurs calibrees pour Canon Selphy CP1300 via WiFi +PRINT_EXTENT="2114x1418" +PRINT_ROLL="-12+8" + +# --- Logging --- +LOG_TAG="photomaton-print" +log_info() { logger -t "$LOG_TAG" "[INFO] $1"; echo "[INFO] $1"; } +log_error() { logger -t "$LOG_TAG" "[ERROR] $1"; echo "[ERROR] $1"; } + +# --- Validation --- +if [ -z "$filename" ]; then + log_error "Aucun fichier specifie" + exit 1 +fi + +if [ ! -f "$filename" ]; then + log_error "Fichier introuvable: $filename" + exit 1 +fi + +if [ "$copies" -lt 1 ] || [ "$copies" -gt 3 ]; then + log_error "Nombre de copies invalide: $copies (doit etre entre 1 et 3)" + exit 1 +fi + +# --- Preparation de l'image pour impression --- +# Resize a 2000x1333 (ratio 3:2), ajout de marges noires pour compenser +# le debordement Fullbleed, puis decalage pour centrer correctement +filename_no_ext="${filename%.jpg}" +filename_print="${filename_no_ext}_print.jpg" + +if command -v convert &>/dev/null; then + log_info "Preparation de l'image pour impression Fullbleed..." + if convert "$filename" \ + -resize 2000x1333 \ + -background black \ + -gravity center \ + -extent ${PRINT_EXTENT} \ + -roll ${PRINT_ROLL} \ + -quality 95 \ + "$filename_print" 2>/dev/null; then + FILE_TO_PRINT="$filename_print" + log_info "Image preparee: $filename_print" + else + log_error "Echec de la preparation, impression de l'original" + FILE_TO_PRINT="$filename" + fi +else + log_error "ImageMagick (convert) non installe, impression de l'original" + FILE_TO_PRINT="$filename" +fi + +# --- Selection de l'imprimante (load balancing) --- +printer_1_status=$(lpstat -p "$PRINTER_1" 2>/dev/null || echo "not found") +printer_2_status=$(lpstat -p "$PRINTER_2" 2>/dev/null || echo "not found") + +SELECTED_PRINTER="" + +if [[ "$printer_1_status" == *"idle"* ]]; then + SELECTED_PRINTER="$PRINTER_1" + log_info "Imprimante selectionnee: $PRINTER_1 (idle)" +elif [[ "$printer_2_status" == *"idle"* ]]; then + SELECTED_PRINTER="$PRINTER_2" + log_info "Imprimante selectionnee: $PRINTER_2 (idle)" +else + # Aucune idle : choisir celle avec le moins de jobs en attente + jobs_1=$(lpstat -o "$PRINTER_1" 2>/dev/null | wc -l) + jobs_2=$(lpstat -o "$PRINTER_2" 2>/dev/null | wc -l) + + if [ "$jobs_1" -le "$jobs_2" ] 2>/dev/null; then + SELECTED_PRINTER="$PRINTER_1" + else + SELECTED_PRINTER="$PRINTER_2" + fi + log_info "Aucune imprimante idle, file d'attente sur: $SELECTED_PRINTER (jobs: P1=$jobs_1, P2=$jobs_2)" +fi + +# --- Impression --- +log_info "Impression x${copies} sur $SELECTED_PRINTER: $FILE_TO_PRINT" + +if lp -n "$copies" -d "$SELECTED_PRINTER" $PRINT_OPTIONS "$FILE_TO_PRINT"; then + log_info "Job d'impression envoye avec succes" + + # Sortie parsable par Node-RED pour les compteurs + echo "PRINTED:${SELECTED_PRINTER}:${copies}" + + # Nettoyage du fichier temporaire (apres que CUPS l'ait lu) + if [ "$FILE_TO_PRINT" = "$filename_print" ] && [ -f "$filename_print" ]; then + (sleep 30 && rm -f "$filename_print" 2>/dev/null) & + fi + + exit 0 +else + log_error "Echec de l'envoi du job d'impression" + echo "PRINT_ERROR:${SELECTED_PRINTER}" + + # Nettoyage meme en cas d'erreur + if [ -f "$filename_print" ]; then + rm -f "$filename_print" 2>/dev/null + fi + + exit 1 +fi diff --git a/Opencode/scripts/setup-photobooth.sh b/Opencode/scripts/setup-photobooth.sh new file mode 100644 index 0000000..bbc2aa7 --- /dev/null +++ b/Opencode/scripts/setup-photobooth.sh @@ -0,0 +1,276 @@ +#!/bin/bash +# ============================================================================= +# setup-photobooth.sh - Installation et deploiement complet du photomaton +# ============================================================================= +# Usage: bash setup-photobooth.sh +# A executer sur un Raspberry Pi 4 fraichement installe +# ============================================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "\n${CYAN}=== $1 ===${NC}\n"; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || whoami)}" +USER_HOME=$(eval echo "~$REAL_USER") + +echo "=============================================" +echo " Installation complete du Photomaton" +echo " Raspberry Pi 4 - 2 Go RAM" +echo "=============================================" +echo "" +echo " Utilisateur: $REAL_USER" +echo " Home: $USER_HOME" +echo " Projet: $PROJECT_DIR" +echo "" + +# Verifier qu'on est root pour certaines operations +NEED_SUDO=false +if [ "$EUID" -ne 0 ]; then + log_warn "Certaines etapes necessitent sudo. Vous serez peut-etre invite a entrer le mot de passe." + NEED_SUDO=true +fi + +# =================================================================== +# ETAPE 1: Optimisation systeme +# =================================================================== +log_step "1/7 - Optimisation systeme" + +if [ "$EUID" -eq 0 ]; then + bash "$SCRIPT_DIR/optimize-system.sh" +else + log_warn "Lancez optimize-system.sh separement avec sudo:" + echo " sudo bash $SCRIPT_DIR/optimize-system.sh" +fi + +# =================================================================== +# ETAPE 2: Installation des dependances +# =================================================================== +log_step "2/7 - Installation des dependances" + +sudo apt-get update -qq +sudo apt-get install -y -qq \ + python3 \ + python3-pip \ + python3-venv \ + python3-picamera2 \ + libcamera-apps \ + libcap-dev \ + curl \ + unclutter \ + chromium \ + git \ + cups \ + printer-driver-gutenprint \ + cups-client + +log_info "Dependances installees" + +# =================================================================== +# ETAPE 3: Installation de photobooth-app +# =================================================================== +log_step "3/7 - Installation de photobooth-app" + +# Creer le dossier de donnees +su - "$REAL_USER" -c "mkdir -p $USER_HOME/photobooth-data" + +# Installer photobooth-app via pip (mode utilisateur) +su - "$REAL_USER" -c "pip install --user --upgrade photobooth-app" || { + log_warn "pip install --user a echoue, essai avec pipx..." + su - "$REAL_USER" -c "pip install --user pipx && pipx install photobooth-app" || { + log_warn "pipx a echoue, essai avec venv..." + su - "$REAL_USER" -c " + python3 -m venv $USER_HOME/photobooth-venv + $USER_HOME/photobooth-venv/bin/pip install photobooth-app + " + log_info "photobooth-app installe dans un venv: $USER_HOME/photobooth-venv" + log_warn "Adapter le chemin dans le service systemd (ExecStart)" + } +} + +log_info "photobooth-app installe" + +# =================================================================== +# ETAPE 4: Configuration du service systemd +# =================================================================== +log_step "4/7 - Configuration des services systemd" + +# Service photobooth-app (service utilisateur) +su - "$REAL_USER" -c "mkdir -p $USER_HOME/.config/systemd/user/" + +# Copier le service utilisateur +cp "$PROJECT_DIR/systemd/photobooth-app.service" \ + "$USER_HOME/.config/systemd/user/photobooth-app.service" +chown "$REAL_USER:$REAL_USER" "$USER_HOME/.config/systemd/user/photobooth-app.service" + +# Activer le linger pour que les services utilisateur demarrent au boot +loginctl enable-linger "$REAL_USER" 2>/dev/null || true + +# Activer le service +su - "$REAL_USER" -c " + systemctl --user daemon-reload + systemctl --user enable photobooth-app +" + +log_info "Service photobooth-app configure" + +# Services systeme (watchdog + maintenance) +# Adapter les chemins dans les services +SCRIPTS_PATH="$PROJECT_DIR/scripts" + +sed "s|/home/pi/photomaton/scripts|$SCRIPTS_PATH|g" \ + "$PROJECT_DIR/systemd/photobooth-watchdog.service" \ + > /etc/systemd/system/photobooth-watchdog.service + +cp "$PROJECT_DIR/systemd/photobooth-watchdog.timer" \ + /etc/systemd/system/photobooth-watchdog.timer + +sed "s|/home/pi/photomaton/scripts|$SCRIPTS_PATH|g" \ + "$PROJECT_DIR/systemd/photobooth-maintenance.service" \ + > /etc/systemd/system/photobooth-maintenance.service + +cp "$PROJECT_DIR/systemd/photobooth-maintenance.timer" \ + /etc/systemd/system/photobooth-maintenance.timer + +# Rendre les scripts executables +chmod +x "$SCRIPTS_PATH"/*.sh + +systemctl daemon-reload +systemctl enable photobooth-watchdog.timer +systemctl enable photobooth-maintenance.timer + +log_info "Watchdog et maintenance configures" + +# =================================================================== +# ETAPE 5: Configuration de l'imprimante +# =================================================================== +log_step "5/7 - Configuration imprimante" + +if [ "$EUID" -eq 0 ]; then + bash "$SCRIPT_DIR/setup-printer.sh" +else + log_warn "Lancez setup-printer.sh separement avec sudo:" + echo " sudo bash $SCRIPT_DIR/setup-printer.sh" +fi + +# =================================================================== +# ETAPE 6: Configuration de l'auto-login et kiosk +# =================================================================== +log_step "6/7 - Configuration auto-login et mode kiosk" + +# Activer l'auto-login en mode graphique +# (depand de la config actuelle, peut necessiter raspi-config) +if command -v raspi-config &>/dev/null; then + # Mode desktop auto-login + raspi-config nonint do_boot_behaviour B4 2>/dev/null || true + log_info "Auto-login graphique configure via raspi-config" +fi + +# Creer le fichier .desktop pour le kiosk Chromium +# Sur RPi OS Trixie (Wayland), le fichier va dans ~/Desktop/ +DESKTOP_DIR="$USER_HOME/Desktop" +su - "$REAL_USER" -c "mkdir -p $DESKTOP_DIR" + +# Detecter chromium +CHROMIUM_BIN=$(command -v chromium || command -v chromium-browser || echo "chromium") + +cat > "$DESKTOP_DIR/photobooth-app.desktop" << EOF +[Desktop Entry] +X-GNOME-Autostart-enabled=true +X-GNOME-Autostart-Delay=120 +Terminal=false +Type=Application +Name=Photobooth-App +Exec=bash -c "sleep 60 && $CHROMIUM_BIN --kiosk --accept-lang=fr-FR --incognito --password-store=basic --disable-features=Translate,OverscrollHistoryNavigation --noerrdialogs --disable-infobars --no-first-run --ozone-platform=wayland --enable-features=OverlayScrollbar --start-maximized --process-per-site --js-flags='--max-old-space-size=128' --renderer-process-limit=1 --disable-dev-shm-usage http://127.0.0.1:8083/" +StartupNotify=false +EOF + +chown "$REAL_USER:$REAL_USER" "$DESKTOP_DIR/photobooth-app.desktop" + +log_info "Mode kiosk configure (Wayland, ~/Desktop/photobooth-app.desktop)" + +# =================================================================== +# ETAPE 7: Configuration finale et verification +# =================================================================== +log_step "7/7 - Verification finale" + +echo "" +echo "=== Verification des composants ===" +echo "" + +# Verifier photobooth-app +if su - "$REAL_USER" -c "which photobooth" &>/dev/null || \ + [ -f "$USER_HOME/photobooth-venv/bin/photobooth" ]; then + echo -e " photobooth-app: ${GREEN}INSTALLE${NC}" +else + echo -e " photobooth-app: ${RED}NON TROUVE${NC}" +fi + +# Verifier CUPS +if systemctl is-active cups &>/dev/null; then + echo -e " CUPS: ${GREEN}ACTIF${NC}" +else + echo -e " CUPS: ${YELLOW}INACTIF${NC}" +fi + +# Verifier les imprimantes +printers=$(lpstat -p 2>/dev/null | grep -c "printer" || echo "0") +echo -e " Imprimantes: ${printers} detectee(s)" + +# Verifier la camera +if libcamera-hello --list-cameras 2>/dev/null | grep -q "Available"; then + echo -e " Camera: ${GREEN}DETECTEE${NC}" +elif [ -e /dev/video0 ]; then + echo -e " Camera: ${GREEN}/dev/video0${NC}" +else + echo -e " Camera: ${YELLOW}NON DETECTEE (verifier apres reboot)${NC}" +fi + +# Verifier la memoire +mem_total=$(awk '/MemTotal/ {printf "%.0f", $2/1024}' /proc/meminfo) +mem_available=$(awk '/MemAvailable/ {printf "%.0f", $2/1024}' /proc/meminfo) +echo -e " RAM: ${mem_available}Mo disponible / ${mem_total}Mo total" + +# Verifier le swap +swap_total=$(awk '/SwapTotal/ {printf "%.0f", $2/1024}' /proc/meminfo) +echo -e " Swap: ${swap_total}Mo" + +echo "" +echo "=============================================" +echo " Installation terminee !" +echo "=============================================" +echo "" +echo "Prochaines etapes:" +echo "" +echo " 1. REBOOTER le Raspberry Pi:" +echo " sudo reboot" +echo "" +echo " 2. Apres reboot, acceder a l'admin photobooth:" +echo " http://localhost:8083/admin" +echo "" +echo " 3. Configurer dans l'admin:" +echo " - Camera: Backends -> picamera2" +echo " - GPIO: hardwareinput -> activer GPIO" +echo " - Photo trigger: GPIO 27 (ou votre GPIO relay)" +echo " - Impression: Share & Print -> ajouter une action Print" +echo " Commande: /home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"" +echo " - QR Share: configurer le service QR" +echo "" +echo " 4. Si vous utilisez encore Node-RED pour le bouton:" +echo " -> Envisagez de passer au GPIO natif de photobooth-app" +echo " -> Gain: ~200 Mo de RAM en desactivant Node-RED" +echo " -> sudo systemctl disable nodered.service" +echo "" +echo " 5. Verifier que tout fonctionne:" +echo " bash $SCRIPT_DIR/health-check.sh" +echo "" diff --git a/Opencode/scripts/setup-printer.sh b/Opencode/scripts/setup-printer.sh new file mode 100644 index 0000000..4e28605 --- /dev/null +++ b/Opencode/scripts/setup-printer.sh @@ -0,0 +1,321 @@ +#!/bin/bash +# ============================================================================= +# setup-printer.sh - Configuration CUPS + Canon Selphy CP1300 +# ============================================================================= +# Usage: sudo bash setup-printer.sh +# Prerequis: imprimante branchee en USB et allumee +# ============================================================================= + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +if [ "$EUID" -ne 0 ]; then + log_error "Ce script doit etre execute en root (sudo)" + exit 1 +fi + +echo "=============================================" +echo " Configuration imprimante Canon Selphy CP1300" +echo "=============================================" +echo "" + +# ------------------------------------------------------------------- +# 1. INSTALLATION DES PAQUETS +# ------------------------------------------------------------------- +log_info "Installation des paquets necessaires..." + +apt-get update -qq +apt-get install -y -qq \ + cups \ + printer-driver-gutenprint \ + cups-client \ + system-config-printer \ + 2>/dev/null || apt-get install -y -qq cups printer-driver-gutenprint cups-client + +# Ajouter l'utilisateur au groupe lpadmin pour administrer CUPS +REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || echo pi)}" +usermod -aG lpadmin "$REAL_USER" 2>/dev/null || true +log_info "Utilisateur $REAL_USER ajoute au groupe lpadmin" + +# ------------------------------------------------------------------- +# 2. CONFIGURATION CUPS +# ------------------------------------------------------------------- +log_info "Configuration de CUPS..." + +# Backup de la config originale +cp /etc/cups/cupsd.conf /etc/cups/cupsd.conf.backup.$(date +%Y%m%d) 2>/dev/null || true + +# Autoriser l'acces local au panneau CUPS +# (utile pour debug mais pas necessaire en production) +cat > /etc/cups/cupsd.conf << 'EOF' +# Configuration CUPS optimisee pour photomaton +LogLevel warn +MaxLogSize 0 +PageLogFormat + +# Ecouter sur localhost et le reseau local +Listen localhost:631 +Listen /run/cups/cups.sock + +# Partage d'imprimante desactive (pas besoin) +Browsing Off +BrowseLocalProtocols none +DefaultAuthType Basic +WebInterface Yes + +# Politique par defaut + + Order allow,deny + Allow localhost + Allow 10.0.0.* + Allow 192.168.* + + + + Order allow,deny + Allow localhost + + + + AuthType Default + Require user @SYSTEM + Order allow,deny + Allow localhost + + + + JobPrivateAccess default + JobPrivateValues default + SubscriptionPrivateAccess default + SubscriptionPrivateValues default + + + Order deny,allow + + + + Require user @OWNER @SYSTEM + Order deny,allow + + + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + + AuthType Default + Require user @SYSTEM + Order deny,allow + + + + Order deny,allow + + +EOF + +systemctl restart cups +systemctl enable cups +log_info "CUPS configure et redemarre" + +# ------------------------------------------------------------------- +# 3. DETECTION DE L'IMPRIMANTE +# ------------------------------------------------------------------- +log_info "Recherche des imprimantes Canon Selphy..." + +sleep 3 # Laisser CUPS detecter les USB + +# Chercher les imprimantes USB Canon +USB_PRINTERS=$(lpinfo -v 2>/dev/null | grep -i "canon\|selphy\|usb" || true) + +if [ -z "$USB_PRINTERS" ]; then + log_warn "Aucune imprimante Canon detectee via USB." + log_warn "Verifiez que l'imprimante est branchee, allumee, et prete." + echo "" + echo "Imprimantes detectees:" + lpinfo -v 2>/dev/null || true + echo "" + echo "Vous pouvez relancer ce script une fois l'imprimante connectee," + echo "ou ajouter manuellement via: http://localhost:631" + echo "" +else + echo "" + echo -e "${CYAN}Imprimantes detectees:${NC}" + echo "$USB_PRINTERS" + echo "" +fi + +# Chercher le driver Gutenprint pour Canon Selphy +log_info "Recherche du driver Canon Selphy CP1300..." +DRIVER=$(lpinfo -m 2>/dev/null | grep -i "selphy\|cp1300\|CP1300" | head -1 || true) + +if [ -z "$DRIVER" ]; then + # Essayer avec un pattern plus large + DRIVER=$(lpinfo -m 2>/dev/null | grep -i "canon.*cp" | head -1 || true) +fi + +if [ -n "$DRIVER" ]; then + DRIVER_URI=$(echo "$DRIVER" | awk '{print $1}') + log_info "Driver trouve: $DRIVER" +else + log_warn "Driver Selphy CP1300 non trouve dans Gutenprint." + log_warn "Essayez: sudo apt install printer-driver-gutenprint && sudo systemctl restart cups" + DRIVER_URI="" +fi + +# ------------------------------------------------------------------- +# 4. AJOUT AUTOMATIQUE DE L'IMPRIMANTE (si detectee) +# ------------------------------------------------------------------- +USB_URI=$(echo "$USB_PRINTERS" | grep "usb://" | head -1 | awk '{print $2}' || true) + +if [ -n "$USB_URI" ] && [ -n "$DRIVER_URI" ]; then + log_info "Ajout automatique de l'imprimante..." + + # Supprimer les anciennes configs si elles existent + lpadmin -x Canon_SELPHY_CP1300_usb_blanche 2>/dev/null || true + lpadmin -x Canon_SELPHY_CP1300_usb_noire 2>/dev/null || true + + # Ajouter la premiere imprimante (blanche) + lpadmin -p Canon_SELPHY_CP1300_usb_blanche \ + -v "$USB_URI" \ + -m "$DRIVER_URI" \ + -L "Photomaton" \ + -D "Canon Selphy CP1300 Blanche" \ + -E + + # Configurer comme imprimante par defaut + lpadmin -d Canon_SELPHY_CP1300_usb_blanche + + log_info "Imprimante 'Canon_SELPHY_CP1300_usb_blanche' ajoutee et definie par defaut" + + # Detecter une deuxieme imprimante si presente + USB_URI_2=$(echo "$USB_PRINTERS" | grep "usb://" | sed -n '2p' | awk '{print $2}' || true) + if [ -n "$USB_URI_2" ]; then + lpadmin -p Canon_SELPHY_CP1300_usb_noire \ + -v "$USB_URI_2" \ + -m "$DRIVER_URI" \ + -L "Photomaton" \ + -D "Canon Selphy CP1300 Noire" \ + -E + log_info "Deuxieme imprimante 'Canon_SELPHY_CP1300_usb_noire' ajoutee" + fi +else + log_warn "Ajout automatique impossible. Ajoutez manuellement via http://localhost:631" +fi + +# ------------------------------------------------------------------- +# 5. OPTIONS D'IMPRESSION OPTIMALES POUR SELPHY CP1300 +# ------------------------------------------------------------------- +log_info "Configuration des options d'impression..." + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cat > "${SCRIPT_DIR}/../config/cups-selphy.conf" << 'EOF' +# ============================================================================= +# Options d'impression pour Canon Selphy CP1300 +# ============================================================================= +# Format: lpoptions -p PRINTER_NAME -o OPTION=VALUE +# +# La Selphy CP1300 supporte: +# - Format carte postale : 100x148mm (4x6 pouces) +# - Format carte : 54x86mm +# - Format carre : 89x89mm (si media disponible) +# +# Noms CUPS des imprimantes: +# Canon_SELPHY_CP1300_usb_blanche +# Canon_SELPHY_CP1300_usb_noire +# +# Commande d'impression pour photobooth-app: +# lp -d Canon_SELPHY_CP1300_usb_blanche -o landscape -o fit-to-page {filename} +# +# Options utilisees: +# - fit-to-page : Ajuster l'image a la page sans crop +# - landscape : Orientation paysage +# - -n X : Nombre de copies +# +# Pour le load balancing entre 2 imprimantes: +# Utiliser script_print.sh (gere automatiquement) +# ============================================================================= +EOF + +# Appliquer les options par defaut si les imprimantes sont configurees +if lpstat -p Canon_SELPHY_CP1300_usb_blanche &>/dev/null; then + lpoptions -p Canon_SELPHY_CP1300_usb_blanche -o fit-to-page -o landscape 2>/dev/null || true + log_info "Options par defaut appliquees a Canon_SELPHY_CP1300_usb_blanche" +fi + +if lpstat -p Canon_SELPHY_CP1300_usb_noire &>/dev/null; then + lpoptions -p Canon_SELPHY_CP1300_usb_noire -o fit-to-page -o landscape 2>/dev/null || true + log_info "Options par defaut appliquees a Canon_SELPHY_CP1300_usb_noire" +fi + +# ------------------------------------------------------------------- +# 6. COPIER LE SCRIPT D'IMPRESSION OPTIMISE +# ------------------------------------------------------------------- +log_info "Verification du script d'impression..." + +if [ -f "${SCRIPT_DIR}/script_print.sh" ]; then + log_info "script_print.sh present dans le projet" + log_info "Pour deployer: cp ${SCRIPT_DIR}/script_print.sh /home/pi/photobooth-data/script/" +else + log_warn "script_print.sh non trouve dans ${SCRIPT_DIR}/" +fi + +# ------------------------------------------------------------------- +# 7. SCRIPT DE NETTOYAGE AUTOMATIQUE DES JOBS BLOQUES +# ------------------------------------------------------------------- +log_info "Creation du script de nettoyage jobs CUPS..." + +cat > "${SCRIPT_DIR}/cups-cleanup.sh" << 'CLEANUP_EOF' +#!/bin/bash +# Nettoyer les jobs d'impression bloques/en erreur +# A appeler periodiquement via cron ou le watchdog + +# Annuler tous les jobs en erreur +cancel -a 2>/dev/null || true + +# Reactiver les imprimantes si elles sont en pause/erreur +for printer in $(lpstat -p 2>/dev/null | grep -E "disabled|stopped" | awk '{print $2}'); do + cupsenable "$printer" 2>/dev/null || true + cupsdisable --release "$printer" 2>/dev/null || true + cupsenable "$printer" 2>/dev/null || true + echo "Imprimante reactivee: $printer" +done +CLEANUP_EOF + +chmod +x "${SCRIPT_DIR}/cups-cleanup.sh" +log_info "Script cups-cleanup.sh cree" + +# ------------------------------------------------------------------- +# RESUME +# ------------------------------------------------------------------- +echo "" +echo "=============================================" +echo " Configuration imprimante terminee !" +echo "=============================================" +echo "" +echo "Imprimantes configurees:" +lpstat -p 2>/dev/null || echo " (aucune)" +echo "" +echo "Commande d'impression pour photobooth-app:" +echo " script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"" +echo "" +echo "Imprimantes CUPS:" +echo " Canon_SELPHY_CP1300_usb_blanche" +echo " Canon_SELPHY_CP1300_usb_noire" +echo "" +echo "Administration CUPS: http://localhost:631" +echo "" +echo "Pour tester l'impression:" +echo " lp -d Canon_SELPHY_CP1300_usb_blanche -o landscape -o fit-to-page /chemin/vers/une/photo.jpg" +echo "" diff --git a/Opencode/scripts/switch-quality.sh b/Opencode/scripts/switch-quality.sh new file mode 100644 index 0000000..6d196b6 --- /dev/null +++ b/Opencode/scripts/switch-quality.sh @@ -0,0 +1,187 @@ +#!/bin/bash +# ============================================================================= +# switch-quality.sh - Basculer la qualite d'image de photobooth-app +# ============================================================================= +# Usage: +# bash switch-quality.sh high # Qualite haute (4608x3072, quality 100) +# bash switch-quality.sh low # Qualite basse (2304x1536, quality 90) +# bash switch-quality.sh status # Afficher la config actuelle +# ============================================================================= + +set -uo pipefail + +API_URL="http://127.0.0.1:8083" +API_USER="admin" +API_PASS="PhotoBooth2026!" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# --- Obtenir le token --- +get_token() { + curl -s -X POST "${API_URL}/api/admin/auth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password&username=${API_USER}&password=${API_PASS}" \ + | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])' +} + +# --- Lire la config actuelle --- +get_config() { + local token="$1" + curl -s "${API_URL}/api/admin/config/app" \ + -H "Authorization: Bearer ${token}" +} + +# --- Appliquer une config --- +set_config() { + local token="$1" + local config="$2" + curl -s -X PATCH "${API_URL}/api/admin/config/app" \ + -H "Authorization: Bearer ${token}" \ + -H "Content-Type: application/json" \ + -d "${config}" +} + +# --- Afficher le statut --- +show_status() { + local token + token=$(get_token) + local config + config=$(get_config "$token") + + local capture_w capture_h preview_w preview_h full_still preview_still quality + capture_w=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["backends"]["group_backends"][0]["backend_config"]["CAPTURE_CAM_RESOLUTION_WIDTH"])') + capture_h=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["backends"]["group_backends"][0]["backend_config"]["CAPTURE_CAM_RESOLUTION_HEIGHT"])') + preview_w=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["backends"]["group_backends"][0]["backend_config"]["PREVIEW_CAM_RESOLUTION_WIDTH"])') + preview_h=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["backends"]["group_backends"][0]["backend_config"]["PREVIEW_CAM_RESOLUTION_HEIGHT"])') + full_still=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["mediaprocessing"]["full_still_length"])') + preview_still=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["mediaprocessing"]["preview_still_length"])') + quality=$(echo "$config" | python3 -c 'import sys,json;c=json.load(sys.stdin);print(c["backends"]["group_backends"][0]["backend_config"]["original_still_quality"])') + + echo "=== Configuration actuelle ===" + echo " Capture: ${capture_w}x${capture_h}" + echo " Preview: ${preview_w}x${preview_h}" + echo " Full still: ${full_still}" + echo " Preview still: ${preview_still}" + echo " JPEG quality: ${quality}" + + if [ "$capture_w" -ge 4000 ]; then + echo -e " Mode: ${GREEN}HAUTE QUALITE${NC}" + else + echo -e " Mode: ${YELLOW}BASSE QUALITE (economie RAM)${NC}" + fi +} + +# --- Appliquer haute qualite --- +set_high() { + local token + token=$(get_token) + + echo "Passage en HAUTE QUALITE..." + + # Config backends (camera) + set_config "$token" '{ + "backends": { + "group_backends": [{ + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + }] + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960 + } + }' > /dev/null + + echo -e "${GREEN}HAUTE QUALITE activee${NC}" + echo " Capture: 4608x3072, Quality: 100" + echo " ATTENTION: consomme plus de RAM" + echo "" + echo "Redemarrage de photobooth-app necessaire :" + echo " systemctl --user restart photobooth-app" +} + +# --- Appliquer basse qualite --- +set_low() { + local token + token=$(get_token) + + echo "Passage en BASSE QUALITE (economie RAM)..." + + set_config "$token" '{ + "backends": { + "group_backends": [{ + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 2304, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 1536, + "PREVIEW_CAM_RESOLUTION_WIDTH": 1152, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 768, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 90 + } + }] + }, + "mediaprocessing": { + "full_still_length": 2304, + "preview_still_length": 1200, + "thumbnail_still_length": 480 + } + }' > /dev/null + + echo -e "${YELLOW}BASSE QUALITE activee${NC}" + echo " Capture: 2304x1536, Quality: 90" + echo " Economie RAM optimale" + echo "" + echo "Redemarrage de photobooth-app necessaire :" + echo " systemctl --user restart photobooth-app" +} + +# --- Main --- +case "${1:-status}" in + high|haute|hq) + set_high + ;; + low|basse|lq) + set_low + ;; + status|info) + show_status + ;; + *) + echo "Usage: $0 {high|low|status}" + echo " high - Haute qualite (4608x3072, quality 100)" + echo " low - Basse qualite (2304x1536, quality 90)" + echo " status - Afficher la configuration actuelle" + exit 1 + ;; +esac diff --git a/Opencode/systemd/photobooth-app.service b/Opencode/systemd/photobooth-app.service new file mode 100644 index 0000000..334ee22 --- /dev/null +++ b/Opencode/systemd/photobooth-app.service @@ -0,0 +1,63 @@ +# ============================================================================= +# photobooth-app.service - Service systemd pour photobooth-app +# ============================================================================= +# Installation: +# cp photobooth-app.service ~/.config/systemd/user/ +# systemctl --user daemon-reload +# systemctl --user enable photobooth-app +# systemctl --user start photobooth-app +# +# Logs: +# journalctl --user --unit=photobooth-app -f +# ============================================================================= + +[Unit] +Description=Photobooth App - Photomaton +After=network.target cups.service +Wants=cups.service + +[Service] +Type=simple +WorkingDirectory=%h/photobooth-data +ExecStart=%h/.local/bin/photobooth +# Alternative si installe dans un venv: +# ExecStart=%h/photobooth-venv/bin/photobooth + +# --- Redemarrage automatique robuste --- +Restart=always +RestartSec=5 +# Nombre max de redemarrages en 60 secondes +StartLimitIntervalSec=300 +StartLimitBurst=5 + +# --- Limites memoire (protection contre les fuites) --- +# Limite douce: avertissement a 1.2 Go +MemoryHigh=1200M +# Limite dure: kill a 1.5 Go (laisser de la RAM pour le systeme) +MemoryMax=1500M +# Limite swap +MemorySwapMax=512M + +# --- Watchdog integre --- +WatchdogSec=120 +# Si le watchdog ne recoit pas de signal, redemarrer +WatchdogSignal=SIGKILL + +# --- Environnement --- +Environment=PYTHONUNBUFFERED=1 +Environment=HOME=%h + +# --- Securite --- +NoNewPrivileges=true +ProtectSystem=strict +ReadWritePaths=%h/photobooth-data +ReadWritePaths=%h/.config +ReadWritePaths=/tmp + +# --- Nettoyage propre --- +ExecStopPost=/bin/bash -c 'sleep 2' +KillMode=mixed +TimeoutStopSec=30 + +[Install] +WantedBy=default.target diff --git a/Opencode/systemd/photobooth-maintenance.service b/Opencode/systemd/photobooth-maintenance.service new file mode 100644 index 0000000..022549c --- /dev/null +++ b/Opencode/systemd/photobooth-maintenance.service @@ -0,0 +1,14 @@ +# ============================================================================= +# photobooth-maintenance.service - Maintenance quotidienne du photomaton +# ============================================================================= + +[Unit] +Description=Photomaton Daily Maintenance + +[Service] +Type=oneshot +ExecStart=/home/pi/photomaton/scripts/maintenance.sh +# Adapter le chemin ci-dessus selon votre installation +# ExecStart=/home/VOTRE_USER/opencode/perso/photomaton/scripts/maintenance.sh + +TimeoutStartSec=300 diff --git a/Opencode/systemd/photobooth-maintenance.timer b/Opencode/systemd/photobooth-maintenance.timer new file mode 100644 index 0000000..44f9188 --- /dev/null +++ b/Opencode/systemd/photobooth-maintenance.timer @@ -0,0 +1,16 @@ +# ============================================================================= +# photobooth-maintenance.timer - Maintenance quotidienne a 4h du matin +# ============================================================================= + +[Unit] +Description=Photomaton Daily Maintenance Timer + +[Timer] +# Tous les jours a 4h du matin +OnCalendar=*-*-* 04:00:00 +# Tolerence de 1 heure (pas critique) +AccuracySec=1h +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/Opencode/systemd/photobooth-watchdog.service b/Opencode/systemd/photobooth-watchdog.service new file mode 100644 index 0000000..313cb8e --- /dev/null +++ b/Opencode/systemd/photobooth-watchdog.service @@ -0,0 +1,25 @@ +# ============================================================================= +# photobooth-watchdog.service - Surveillance de sante du photomaton +# ============================================================================= +# Installation: +# sudo cp photobooth-watchdog.service /etc/systemd/system/ +# sudo cp photobooth-watchdog.timer /etc/systemd/system/ +# sudo systemctl daemon-reload +# sudo systemctl enable photobooth-watchdog.timer +# sudo systemctl start photobooth-watchdog.timer +# ============================================================================= + +[Unit] +Description=Photomaton Health Check Watchdog + +[Service] +Type=oneshot +ExecStart=/home/pi/photomaton/scripts/health-check.sh +# Adapter le chemin ci-dessus selon votre installation +# ExecStart=/home/VOTRE_USER/opencode/perso/photomaton/scripts/health-check.sh + +# Timeout: le check ne devrait pas prendre plus de 60 secondes +TimeoutStartSec=60 + +[Install] +WantedBy=multi-user.target diff --git a/Opencode/systemd/photobooth-watchdog.timer b/Opencode/systemd/photobooth-watchdog.timer new file mode 100644 index 0000000..7768c4c --- /dev/null +++ b/Opencode/systemd/photobooth-watchdog.timer @@ -0,0 +1,17 @@ +# ============================================================================= +# photobooth-watchdog.timer - Timer pour le watchdog (toutes les 2 minutes) +# ============================================================================= + +[Unit] +Description=Photomaton Health Check Timer + +[Timer] +# Lancer 1 minute apres le boot +OnBootSec=60 +# Puis toutes les 2 minutes +OnUnitActiveSec=120 +# Tolerence de 30 secondes (economie CPU) +AccuracySec=30 + +[Install] +WantedBy=timers.target diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6ee564 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Photomaton — Interface de gestion + +Interface web Python au-dessus de [photobooth-app](https://photobooth-app.org/) pour Raspberry Pi 4. + +## Architecture + +``` +photomaton/ +│ +├── backend/ # Serveur Python (FastAPI) +│ ├── api/ # Routes REST & WebSocket +│ │ ├── gallery.py # - Galerie publique & admin +│ │ ├── print.py # - Modes d'impression +│ │ ├── leds.py # - Contrôle anneau WS2812b +│ │ ├── button.py # - Gestion bouton GPIO +│ │ └── system.py # - Ressources système (CPU, RAM…) +│ ├── services/ # Logique métier +│ │ ├── photobooth.py # - Intégration photobooth-app +│ │ ├── printer.py # - Gestion impression +│ │ ├── led_service.py # - Effets LED (ring WS2812b) +│ │ └── button_service.py # - Détection clic / double-clic / long +│ ├── models/ # Schémas de données (Pydantic) +│ └── utils/ # Helpers divers +│ +├── frontend/ # Interface web +│ ├── static/ +│ │ ├── css/ # Styles +│ │ ├── js/ # Scripts (galerie, admin, live LED…) +│ │ └── img/ # Assets statiques +│ └── templates/ +│ ├── public/ # Galerie visiteurs (QR code / URL simple) +│ ├── gallery/ # Galerie plein écran +│ └── admin/ # Dashboard admin (impression, ressources) +│ +├── hardware/ +│ ├── gpio/ # Gestion bouton physique (RPi.GPIO / gpiozero) +│ └── leds/ # Anneau WS2812b (rpi_ws281x) +│ +├── config/ +│ ├── settings.yaml # Config principale (GPIO pins, nb LEDs…) +│ └── profiles.yaml # Profils photomaton (normal, fun, N&B…) +│ +├── data/ +│ ├── photos/ # Photos produites (lien ou copie depuis photobooth-app) +│ ├── thumbnails/ # Miniatures générées automatiquement +│ ├── sessions/ # Métadonnées sessions +│ └── logs/ # Journaux applicatifs +│ +├── scripts/ +│ ├── install.sh # Installation dépendances +│ ├── start.sh # Démarrage du service +│ └── setup_service.sh # Création service systemd +│ +└── docs/ # Documentation technique +``` + +## Fonctionnalités + +### Gestion bouton GPIO +| Action | Effet | +|--------|-------| +| 1 clic | Photo profil par défaut | +| 2 clics | Photo profil alternatif | +| Long clic | Impression de la dernière photo | + +### LEDs WS2812b (anneau) +- Effets de veille, décompte, capture, succès, erreur +- Configurable : nombre de LEDs, PIN GPIO, luminosité +- Contrôle via API REST en temps réel + +### Galerie +- **Publique** : accès par QR code ou URL locale, visualisation et téléchargement +- **Admin** : sélection et impression, filtrage par session + +### Modes d'impression +1. **Direct** : impression immédiate après capture +2. **Galerie admin** : impression manuelle depuis le dashboard +3. **Validation** : confirmation requise avant impression + +### Dashboard admin +- Ressources système en temps réel (CPU, RAM, température, disque) +- Contrôle LED live +- Historique des sessions +- Configuration des profils + +## Stack technique +- **Backend** : Python 3.11+, FastAPI, WebSocket +- **Frontend** : HTML/CSS/JS vanilla (léger, pas de framework lourd) +- **LED** : `rpi_ws281x` +- **GPIO** : `gpiozero` +- **Intégration** : API REST de photobooth-app +- **Proxy** : Zoraxy (SSL) +- **Wifi** : RaspAP diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/actions_api.py b/backend/api/actions_api.py new file mode 100644 index 0000000..57ef4b4 --- /dev/null +++ b/backend/api/actions_api.py @@ -0,0 +1,121 @@ +"""API de gestion des actions photobooth-app + mapping bouton.""" + +import logging +from fastapi import APIRouter, Request, Body +from fastapi.responses import JSONResponse + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/actions/photobooth") +async def get_pb_actions(request: Request): + """Retourne les actions image de photobooth-app + le mapping bouton actuel.""" + pb = request.app.state.photobooth_service + cfg = request.app.state.config + + pb_config = await pb.read_pb_config() + actions = pb_config.get("actions", {}).get("image", []) + + # Enrichit avec l'index + for i, action in enumerate(actions): + action["_index"] = i + + return { + "actions": actions, + "button_mapping": cfg.button_actions, + } + + +@router.get("/actions/assets") +async def get_assets(request: Request): + """Liste les cadres et fonds disponibles dans userdata.""" + pb = request.app.state.photobooth_service + frames = await pb.list_userdata_frames() + backgrounds = await pb.list_userdata_backgrounds() + return {"frames": frames, "backgrounds": backgrounds} + + +@router.put("/actions/mapping") +async def update_button_mapping( + request: Request, + mapping: dict = Body(...), +): + """ + Met à jour le mapping clics → actions. + Body: { "1": {"label": "...", "photobooth_index": 0}, ... } + """ + config_svc = request.app.state.config_service + + # Validation basique + for k, v in mapping.items(): + if not isinstance(v, dict) or "photobooth_index" not in v: + return JSONResponse({"error": f"Format invalide pour clic {k}"}, status_code=400) + + config_svc.save_button_actions(mapping) + request.app.state.config.button_actions = mapping + + # Met aussi à jour le button_service + btn = request.app.state.button_service + btn._btn_actions = mapping + + logger.info("Mapping bouton mis à jour: %s", mapping) + return {"ok": True, "mapping": mapping} + + +@router.put("/actions/photobooth/{index}") +async def update_pb_action( + request: Request, + index: int, + updates: dict = Body(...), +): + """ + Met à jour une action image de photobooth-app (cadre, fond, countdown, etc.) + updates peut contenir: countdown_capture, img_frame_file, img_background_file, + remove_background, image_filter, name + """ + pb = request.app.state.photobooth_service + pb_config = await pb.read_pb_config() + actions = pb_config.get("actions", {}).get("image", []) + + if index < 0 or index >= len(actions): + return JSONResponse({"error": f"Index {index} invalide (max: {len(actions)-1})"}, status_code=400) + + action = actions[index] + + # Mise à jour des champs autorisés + allowed_root = {"name"} + allowed_processing = {"remove_background", "img_frame_file", "img_background_file", + "image_filter", "fill_background_enable", "fill_background_color", + "img_background_enable", "texts_enable"} + allowed_jobcontrol = {"countdown_capture"} + + for key, value in updates.items(): + if key in allowed_root: + action[key] = value + elif key in allowed_processing: + action.setdefault("processing", {})[key] = value + elif key in allowed_jobcontrol: + action.setdefault("jobcontrol", {})[key] = value + + pb_config["actions"]["image"][index] = action + await pb.write_pb_config(pb_config) + + logger.info("Action %d mise à jour: %s", index, list(updates.keys())) + return {"ok": True, "index": index, "action": action} + + +@router.post("/actions/trigger/{index}") +async def trigger_action(request: Request, index: int): + """Déclenche une action directement (test).""" + pb = request.app.state.photobooth_service + led = request.app.state.led_service + btn = request.app.state.button_service + btn.relay_off() + try: + result = await pb.trigger_image_action(index) + return {"ok": True, "index": index, "result": result} + except Exception as e: + led.play("error") + btn.relay_on() + return JSONResponse({"error": str(e)}, status_code=500) diff --git a/backend/api/admin_api.py b/backend/api/admin_api.py new file mode 100644 index 0000000..2aeada2 --- /dev/null +++ b/backend/api/admin_api.py @@ -0,0 +1,130 @@ +"""Routes du dashboard admin — authentification requise.""" + +import logging +from pathlib import Path + +from fastapi import APIRouter, Request, Form +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +logger = logging.getLogger(__name__) +router = APIRouter() + +_templates = Jinja2Templates( + directory=Path(__file__).parent.parent.parent / "frontend" / "templates" +) + + +def _is_auth(request: Request) -> bool: + return request.session.get("authenticated") is True + + +def _require_auth(request: Request): + if not _is_auth(request): + return RedirectResponse(url="/admin/login", status_code=302) + return None + + +# ── Auth ────────────────────────────────────────────────────────────────────── + +@router.get("/admin/login", response_class=HTMLResponse) +async def login_page(request: Request): + if _is_auth(request): + return RedirectResponse(url="/admin", status_code=302) + return _templates.TemplateResponse("admin/login.html", {"request": request, "error": None}) + + +@router.post("/admin/login") +async def login(request: Request, password: str = Form(...)): + cfg = request.app.state.config + if password == cfg.app.admin_password: + request.session["authenticated"] = True + return RedirectResponse(url="/admin", status_code=302) + return _templates.TemplateResponse( + "admin/login.html", + {"request": request, "error": "Mot de passe incorrect"}, + ) + + +@router.get("/admin/logout") +async def logout(request: Request): + request.session.clear() + return RedirectResponse(url="/admin/login", status_code=302) + + +# ── Pages admin ─────────────────────────────────────────────────────────────── + +@router.get("/admin", response_class=HTMLResponse) +async def admin_dashboard(request: Request): + redirect = _require_auth(request) + if redirect: + return redirect + + sys_svc = request.app.state.system_service + cfg = request.app.state.config + led = request.app.state.led_service + btn = request.app.state.button_service + + stats = sys_svc.get_stats() + services = sys_svc.get_services_status() + + return _templates.TemplateResponse("admin/dashboard.html", { + "request": request, + "config": cfg, + "stats": stats, + "services": services, + "led_effect": led.current_effect, + "relay_state": btn.relay_state if btn else True, + "print_mode": cfg.print.mode, + }) + + +@router.get("/admin/gallery", response_class=HTMLResponse) +async def admin_gallery(request: Request): + redirect = _require_auth(request) + if redirect: + return redirect + cfg = request.app.state.config + return _templates.TemplateResponse("admin/gallery.html", { + "request": request, + "config": cfg, + }) + + +@router.get("/admin/actions", response_class=HTMLResponse) +async def admin_actions(request: Request): + redirect = _require_auth(request) + if redirect: + return redirect + + pb = request.app.state.photobooth_service + cfg = request.app.state.config + pb_config = await pb.read_pb_config() + actions = pb_config.get("actions", {}).get("image", []) + + return _templates.TemplateResponse("admin/actions.html", { + "request": request, + "config": cfg, + "pb_actions": actions, + "button_mapping": cfg.button_actions, + }) + + +@router.get("/admin/print", response_class=HTMLResponse) +async def admin_print(request: Request): + redirect = _require_auth(request) + if redirect: + return redirect + + printer_svc = request.app.state.printer_service + cfg = request.app.state.config + + queue = await printer_svc.get_queue() + printers = await printer_svc.get_printers_status() + + return _templates.TemplateResponse("admin/print.html", { + "request": request, + "config": cfg, + "queue": queue, + "printers": printers, + }) diff --git a/backend/api/admin_gallery_api.py b/backend/api/admin_gallery_api.py new file mode 100644 index 0000000..f112728 --- /dev/null +++ b/backend/api/admin_gallery_api.py @@ -0,0 +1,122 @@ +"""API galerie admin — impression et suppression de photos.""" + +import logging +from fastapi import APIRouter, Request, Query +from fastapi.responses import JSONResponse, RedirectResponse + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/admin/api/gallery/photos") +async def admin_get_photos( + request: Request, + page: int = Query(default=1, ge=1), + limit: int = Query(default=24, ge=1, le=100), +): + """Liste des photos pour la galerie admin.""" + if not request.session.get("authenticated"): + return JSONResponse({"error": "Non authentifié"}, status_code=401) + + pb = request.app.state.photobooth_service + all_photos = await pb.get_media_collection(limit=500) + photos = [p for p in all_photos if _is_image(p)] + + total = len(photos) + start = (page - 1) * limit + page_photos = photos[start:start + limit] + + for p in page_photos: + pid = _get_id(p) + p["full_url"] = pb.media_url(pid) + p["thumb_url"] = pb.thumbnail_url(pid) + + return {"photos": page_photos, "total": total, "page": page, + "pages": max(1, (total + limit - 1) // limit)} + + +@router.post("/admin/api/gallery/print/{photo_id}") +async def admin_print_photo( + request: Request, + photo_id: str, + copies: int = Query(default=1, ge=1, le=3), +): + """Impression directe depuis la galerie admin.""" + if not request.session.get("authenticated"): + return JSONResponse({"error": "Non authentifié"}, status_code=401) + + pb = request.app.state.photobooth_service + printer_svc = request.app.state.printer_service + led = request.app.state.led_service + ws = request.app.state.ws_manager + + # Reconstruit le chemin du fichier depuis l'ID + from pathlib import Path + import re + cfg = request.app.state.config + + # photobooth-app identifiant → chemin fichier + media_dir = cfg.photobooth.media_dir + # Cherche le fichier correspondant + filename = _find_file(media_dir, photo_id) + if not filename: + return JSONResponse({"error": f"Fichier introuvable pour {photo_id}"}, status_code=404) + + thumb_url = pb.thumbnail_url(photo_id) + entry = await printer_svc.add_request(str(filename), thumb_url, copies) + + if cfg.print.mode == "direct": + led.play("printing") + result = await printer_svc.execute_print(entry["id"], copies) + await ws.broadcast({"type": "print_result", "result": result}) + if result["success"]: + led.play("finished") + else: + led.play("error") + return result + + await ws.broadcast({"type": "print_request", "entry": entry}) + return {"ok": True, "entry": entry, "mode": cfg.print.mode} + + +@router.delete("/admin/api/gallery/{photo_id}") +async def admin_delete_photo(request: Request, photo_id: str): + """Supprime une photo via l'API photobooth-app.""" + if not request.session.get("authenticated"): + return JSONResponse({"error": "Non authentifié"}, status_code=401) + + pb = request.app.state.photobooth_service + ws = request.app.state.ws_manager + + ok = await pb.delete_media(photo_id) + if ok: + await ws.broadcast({"type": "photo_deleted", "photo_id": photo_id}) + return {"ok": ok, "photo_id": photo_id} + + +def _is_image(item: dict) -> bool: + t = item.get("type", item.get("mediaitem_type", "image")) + return str(t).lower() in ("image", "still", "photo") + + +def _get_id(item: dict) -> str: + return str(item.get("id", item.get("filename", item.get("uid", "")))) + + +def _find_file(media_dir: str, photo_id: str): + """Cherche un fichier image correspondant à l'identifiant dans le répertoire media.""" + from pathlib import Path + base = Path(media_dir) + if not base.exists(): + return None + + # L'ID peut être le stem du filename + for ext in (".jpg", ".jpeg", ".png"): + f = base / f"{photo_id}{ext}" + if f.exists(): + return f + # Cherche dans les sous-dossiers + matches = list(base.rglob(f"{photo_id}{ext}")) + if matches: + return matches[0] + return None diff --git a/backend/api/gallery.py b/backend/api/gallery.py new file mode 100644 index 0000000..2713c8f --- /dev/null +++ b/backend/api/gallery.py @@ -0,0 +1,78 @@ +"""Galerie publique — accessible sans authentification.""" + +import logging + +from fastapi import APIRouter, Request, Query +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from pathlib import Path + +logger = logging.getLogger(__name__) +router = APIRouter() + +_templates = Jinja2Templates(directory=Path(__file__).parent.parent.parent / "frontend" / "templates") + + +@router.get("/", response_class=HTMLResponse) +async def index(request: Request): + """Page d'accueil → redirige vers la galerie publique.""" + return RedirectResponse(url="/gallery") + + +@router.get("/gallery", response_class=HTMLResponse) +async def gallery_page(request: Request): + cfg = request.app.state.config + if not cfg.gallery.public_enabled: + return HTMLResponse("

Galerie désactivée

", status_code=403) + return _templates.TemplateResponse("public/gallery.html", {"request": request, "config": cfg}) + + +@router.get("/api/gallery/photos") +async def api_gallery_photos( + request: Request, + page: int = Query(default=1, ge=1), + limit: int = Query(default=24, ge=1, le=100), +): + """Liste des photos depuis photobooth-app (paginée).""" + pb = request.app.state.photobooth_service + cfg = request.app.state.config + + all_photos = await pb.get_media_collection(limit=500) + + # Filtre sur les images uniquement + photos = [p for p in all_photos if _is_image(p)] + + total = len(photos) + start = (page - 1) * limit + end = start + limit + page_photos = photos[start:end] + + # Enrichit avec les URLs + for p in page_photos: + pid = _get_id(p) + p["full_url"] = pb.media_url(pid) + p["thumb_url"] = pb.thumbnail_url(pid) + p["download_url"] = f"/api/gallery/download/{pid}" + + return { + "photos": page_photos, + "total": total, + "page": page, + "pages": max(1, (total + limit - 1) // limit), + } + + +@router.get("/api/gallery/download/{photo_id}") +async def download_photo(request: Request, photo_id: str): + """Redirige vers le fichier full-res sur photobooth-app.""" + pb = request.app.state.photobooth_service + return RedirectResponse(url=pb.media_url(photo_id)) + + +def _is_image(item: dict) -> bool: + t = item.get("type", item.get("mediaitem_type", "image")) + return str(t).lower() in ("image", "still", "photo") + + +def _get_id(item: dict) -> str: + return str(item.get("id", item.get("filename", item.get("uid", "")))) diff --git a/backend/api/leds_api.py b/backend/api/leds_api.py new file mode 100644 index 0000000..be9d361 --- /dev/null +++ b/backend/api/leds_api.py @@ -0,0 +1,51 @@ +"""API de contrôle des LEDs WS2812b.""" + +import logging +from fastapi import APIRouter, Request, Query +from fastapi.responses import JSONResponse + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/leds/status") +async def led_status(request: Request): + led = request.app.state.led_service + return {"effect": led.current_effect} + + +@router.post("/leds/effect") +async def set_effect( + request: Request, + effect: str = Query(..., description="idle | countdown | capture | captured | finished | printing | error | disabled | off"), +): + """Force un effet LED (admin uniquement, pas de vérification auth ici — à protéger via Zoraxy).""" + valid = ("idle", "countdown", "capture", "captured", "finished", "printing", "error", "disabled", "off") + if effect not in valid: + return JSONResponse({"error": f"Effet invalide. Valeurs: {valid}"}, status_code=400) + + led = request.app.state.led_service + ws = request.app.state.ws_manager + led.play(effect) + await ws.broadcast({"type": "led_effect", "effect": effect}) + return {"ok": True, "effect": effect} + + +@router.post("/leds/color") +async def set_color( + request: Request, + r: int = Query(default=0, ge=0, le=255), + g: int = Query(default=0, ge=0, le=255), + b: int = Query(default=0, ge=0, le=255), +): + """Couleur fixe immédiate sur tout l'anneau.""" + led = request.app.state.led_service + led.set_color(r, g, b) + return {"ok": True, "color": [r, g, b]} + + +@router.post("/leds/off") +async def leds_off(request: Request): + led = request.app.state.led_service + led.play("off") + return {"ok": True} diff --git a/backend/api/print_api.py b/backend/api/print_api.py new file mode 100644 index 0000000..e0ae088 --- /dev/null +++ b/backend/api/print_api.py @@ -0,0 +1,114 @@ +"""API gestion de la file d'attente d'impression.""" + +import logging +from pathlib import Path + +from fastapi import APIRouter, Request, Query +from fastapi.responses import JSONResponse + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/print/request") +async def print_request( + request: Request, + filename: str = Query(..., description="Chemin absolu du fichier à imprimer"), + copies: int = Query(default=1, ge=1, le=3), +): + """ + Reçoit la demande d'impression depuis photobooth-app + (appelé par le share_command 'Demande d'impression'). + """ + printer_svc = request.app.state.printer_service + pb = request.app.state.photobooth_service + led = request.app.state.led_service + ws = request.app.state.ws_manager + + # Construit l'URL de miniature depuis le filename + stem = Path(filename).stem + thumb_url = pb.thumbnail_url(stem) + + entry = await printer_svc.add_request(filename, thumb_url, copies) + + await ws.broadcast({"type": "print_request", "entry": entry}) + + # Feedback LED si impression directe + if request.app.state.config.print.mode == "direct": + led.play("printing") + + logger.info("Demande impression reçue: %s", filename) + return {"ok": True, "id": entry["id"], "mode": request.app.state.config.print.mode} + + +@router.get("/print/queue") +async def get_queue( + request: Request, + status: str | None = Query(default=None), +): + """Retourne la file d'attente d'impression.""" + printer_svc = request.app.state.printer_service + queue = await printer_svc.get_queue(status) + printers = await printer_svc.get_printers_status() + return {"queue": queue, "printers": printers} + + +@router.post("/print/execute/{entry_id}") +async def execute_print( + request: Request, + entry_id: str, + copies: int = Query(default=1, ge=1, le=3), +): + """Lance l'impression d'une entrée en attente (validation admin).""" + printer_svc = request.app.state.printer_service + led = request.app.state.led_service + ws = request.app.state.ws_manager + + led.play("printing") + result = await printer_svc.execute_print(entry_id, copies) + + await ws.broadcast({"type": "print_result", "entry_id": entry_id, "result": result}) + + if result["success"]: + led.play("finished") + else: + led.play("error") + + return result + + +@router.post("/print/cancel/{entry_id}") +async def cancel_print(request: Request, entry_id: str): + """Annule une demande en attente.""" + printer_svc = request.app.state.printer_service + ws = request.app.state.ws_manager + ok = await printer_svc.cancel(entry_id) + if ok: + await ws.broadcast({"type": "print_cancelled", "entry_id": entry_id}) + return {"ok": ok} + + +@router.post("/print/cups/cancel/{printer_name}") +async def cancel_cups_jobs(request: Request, printer_name: str): + """Annule tous les jobs CUPS d'une imprimante.""" + printer_svc = request.app.state.printer_service + ok = await printer_svc.cancel_cups_jobs(printer_name) + return {"ok": ok, "printer": printer_name} + + +@router.get("/print/printers") +async def get_printers(request: Request): + """Statut des imprimantes CUPS.""" + printer_svc = request.app.state.printer_service + return await printer_svc.get_printers_status() + + +@router.post("/print/mode") +async def set_print_mode(request: Request, mode: str = Query(...)): + """Change le mode d'impression (direct | validation | gallery).""" + if mode not in ("direct", "validation", "gallery"): + return JSONResponse({"error": "Mode invalide"}, status_code=400) + config_svc = request.app.state.config_service + config_svc.save_print_mode(mode) + request.app.state.config.print.mode = mode + return {"ok": True, "mode": mode} diff --git a/backend/api/system_api.py b/backend/api/system_api.py new file mode 100644 index 0000000..f98df58 --- /dev/null +++ b/backend/api/system_api.py @@ -0,0 +1,52 @@ +"""API de surveillance des ressources système.""" + +import logging +from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse + +logger = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/system/stats") +async def system_stats(request: Request): + """Ressources système en temps réel.""" + sys_svc = request.app.state.system_service + return sys_svc.get_stats() + + +@router.get("/system/services") +async def system_services(request: Request): + """Statut des services systemd.""" + sys_svc = request.app.state.system_service + return sys_svc.get_services_status() + + +@router.get("/system/photobooth") +async def photobooth_status(request: Request): + """Vérifie si photobooth-app répond.""" + pb = request.app.state.photobooth_service + alive = await pb.is_alive() + return {"alive": alive, "url": request.app.state.config.photobooth.base_url} + + +@router.post("/system/button/simulate") +async def simulate_button(request: Request, clicks: int = 1): + """Simule un appui bouton (dev/test uniquement).""" + btn = request.app.state.button_service + if clicks == 0: + btn.simulate_long_press() + else: + btn.simulate_click(clicks) + return {"ok": True, "simulated_clicks": clicks} + + +@router.post("/system/relay") +async def control_relay(request: Request, state: str = "on"): + """Force le relay ON/OFF.""" + btn = request.app.state.button_service + if state == "on": + btn.relay_on() + else: + btn.relay_off() + return {"ok": True, "relay": state} diff --git a/backend/api/webhooks.py b/backend/api/webhooks.py new file mode 100644 index 0000000..5d6dda4 --- /dev/null +++ b/backend/api/webhooks.py @@ -0,0 +1,92 @@ +"""Webhooks reçus de photobooth-app via plugin_commander. + +photobooth-app envoie un GET sur /api/webhook/photobooth?event_key=XXX&mediaitem_type=image +pour chaque événement de capture. + +Événements : + counting → countdown en cours (LEDs remplissage vert) + capture → flash photo (LEDs flash blanc) + captured → photo prise (LEDs violet) + finished → traitement terminé (LEDs bleu → retour idle + relay ON) + start/stop → démarrage/arrêt photobooth-app +""" + +import logging + +from fastapi import APIRouter, Request, Query + +logger = logging.getLogger(__name__) +router = APIRouter() + +# Durée par défaut du countdown (en secondes) — peut être overridé par action +DEFAULT_COUNTDOWN = 5.0 + + +@router.get("/webhook/photobooth") +async def photobooth_webhook( + request: Request, + event_key: str = Query(default=""), + mediaitem_type: str = Query(default=""), +): + led = request.app.state.led_service + btn = request.app.state.button_service + ws = request.app.state.ws_manager + + logger.info("Webhook photobooth: event=%s type=%s", event_key, mediaitem_type) + + await ws.broadcast({"type": "photobooth_event", "event": event_key, "media_type": mediaitem_type}) + + match event_key: + case "counting": + # Countdown en cours — remplissage LED vert + # On cherche la durée du countdown dans la config de l'action courante + countdown_duration = _get_countdown_duration(request) + led.play("countdown", countdown_duration=countdown_duration) + + case "capture": + # Flash photo + led.play("capture") + + case "captured": + # Photo prise, traitement en cours + led.play("captured") + + case "finished": + # Traitement terminé + led.play("finished") + # Réactive le relay après l'animation "finished" (durée ~2s gérée par le service LED) + import asyncio + asyncio.create_task(_delayed_relay_on(btn, 2.5)) + + case "start": + led.play("idle") + + case "stop": + led.play("off") + + case _: + logger.debug("Événement inconnu: %s", event_key) + + return {"ok": True, "event": event_key} + + +async def _delayed_relay_on(btn, delay: float): + import asyncio + await asyncio.sleep(delay) + if btn: + btn.relay_on() + + +def _get_countdown_duration(request: Request) -> float: + """ + Essaie de récupérer la durée du countdown depuis la config photobooth-app + en lisant le dernier index d'action déclenché par le bouton. + Retourne la valeur par défaut si indisponible. + """ + try: + btn = request.app.state.button_service + # On pourrait stocker le dernier index dans le button_service + # Pour l'instant on retourne 5s par défaut + return DEFAULT_COUNTDOWN + except Exception: + return DEFAULT_COUNTDOWN diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/__init__.py b/backend/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/services/button_service.py b/backend/services/button_service.py new file mode 100644 index 0000000..729c216 --- /dev/null +++ b/backend/services/button_service.py @@ -0,0 +1,219 @@ +"""Service de gestion du bouton physique GPIO23 + relay GPIO12. + +- Détection multi-clic (1 à 4 clics) avec timer +- Détection long appui (>1500ms) +- Contrôle du relay (désactive/active le bouton 12V) + +Mode réel : gpiozero (Raspberry Pi) +Mode mock : aucune action GPIO, simulation possible via API +""" + +from __future__ import annotations + +import asyncio +import logging +import threading +import time + +from backend.services.config_service import Config + +logger = logging.getLogger(__name__) + +try: + from gpiozero import Button, OutputDevice + HAS_GPIO = True +except (ImportError, Exception): + HAS_GPIO = False + logger.warning("gpiozero non disponible — mode mock bouton activé") + + +class ButtonService: + def __init__(self, config: Config, led_service, photobooth_service, ws_manager, loop: asyncio.AbstractEventLoop): + self._cfg = config.button + self._btn_actions = config.button_actions + self._led = led_service + self._pb = photobooth_service + self._ws = ws_manager + self._loop = loop + + self._button = None + self._relay = None + + self._click_count = 0 + self._press_time: float = 0.0 + self._click_timer: threading.Timer | None = None + self._long_press_fired = False + self._relay_enabled = True + self._lock = threading.Lock() + + # ── Lifecycle ───────────────────────────────────────────────────────────── + + def start(self): + if not HAS_GPIO: + logger.info("Mode mock bouton — GPIO non disponible") + return + + try: + self._relay = OutputDevice( + self._cfg.relay_pin, + active_high=True, + initial_value=True, + ) + self._button = Button( + self._cfg.pin, + pull_up=True, + bounce_time=self._cfg.debounce_ms / 1000, + ) + self._button.when_pressed = self._on_pressed + self._button.when_released = self._on_released + logger.info("Bouton GPIO%d, Relay GPIO%d initialisés", self._cfg.pin, self._cfg.relay_pin) + except Exception as e: + logger.error("Erreur init GPIO: %s", e) + + def stop(self): + if self._click_timer: + self._click_timer.cancel() + if self._button: + try: + self._button.close() + except Exception: + pass + if self._relay: + try: + self._relay.close() + except Exception: + pass + + # ── Relay ───────────────────────────────────────────────────────────────── + + def relay_on(self): + """Active le relay (bouton 12V allumé).""" + self._relay_enabled = True + if self._relay: + self._relay.on() + self._led.play("idle") + logger.debug("Relay ON") + + def relay_off(self): + """Désactive le relay (bouton 12V éteint pendant la capture).""" + self._relay_enabled = False + if self._relay: + self._relay.off() + self._led.play("disabled") + logger.debug("Relay OFF") + + # ── Simulation (pour mode mock / tests) ─────────────────────────────────── + + def simulate_click(self, count: int): + asyncio.run_coroutine_threadsafe(self._handle_click(count), self._loop) + + def simulate_long_press(self): + asyncio.run_coroutine_threadsafe(self._handle_long_press(), self._loop) + + # ── Callbacks GPIO ──────────────────────────────────────────────────────── + + def _on_pressed(self): + self._press_time = time.time() + self._long_press_fired = False + + # Lance un timer pour détecter le long appui + if self._click_timer: + self._click_timer.cancel() + + long_ms = self._cfg.long_press_ms / 1000 + self._long_timer = threading.Timer(long_ms, self._on_long_press_timer) + self._long_timer.start() + + def _on_released(self): + if hasattr(self, "_long_timer") and self._long_timer: + self._long_timer.cancel() + + if self._long_press_fired: + return # Long press déjà traité + + # Compte un clic + with self._lock: + self._click_count += 1 + count = self._click_count + + if self._click_timer: + self._click_timer.cancel() + + if count >= self._cfg.max_clicks: + # Dispatch immédiat si max atteint + self._click_timer = threading.Timer(0.05, self._dispatch_clicks) + else: + # Attente pour éventuel prochain clic + self._click_timer = threading.Timer( + self._cfg.double_click_ms / 1000, + self._dispatch_clicks, + ) + self._click_timer.start() + + def _on_long_press_timer(self): + self._long_press_fired = True + asyncio.run_coroutine_threadsafe(self._handle_long_press(), self._loop) + + def _dispatch_clicks(self): + with self._lock: + count = self._click_count + self._click_count = 0 + self._click_timer = None + + asyncio.run_coroutine_threadsafe(self._handle_click(count), self._loop) + + # ── Handlers async ──────────────────────────────────────────────────────── + + async def _handle_click(self, count: int): + if count < 1: + return + + count = min(count, self._cfg.max_clicks) + logger.info("Bouton: %d clic(s)", count) + + # Récupère l'index de l'action photobooth + action_info = self._btn_actions.get(count) or self._btn_actions.get(str(count)) + if not action_info: + logger.warning("Aucune action mappée pour %d clic(s)", count) + return + + pb_index = action_info.get("photobooth_index", 0) + label = action_info.get("label", f"Action {count}") + + await self._ws.broadcast({ + "type": "button_event", + "clicks": count, + "action": label, + "photobooth_index": pb_index, + }) + + # Désactive le relay + LED disabled + self.relay_off() + + # Déclenche l'action photobooth-app + try: + await self._pb.trigger_image_action(pb_index) + except Exception as e: + logger.error("Erreur déclenchement action %d: %s", pb_index, e) + self._led.play("error") + await asyncio.sleep(1) + self.relay_on() + + async def _handle_long_press(self): + logger.info("Bouton: long appui") + await self._ws.broadcast({"type": "button_event", "clicks": 0, "action": "long_press"}) + + if not self._cfg.print_enabled: + return + + # Déclenche la demande d'impression sur la dernière photo + try: + self._led.play("printing") + await self._pb.trigger_share_latest(0) + except Exception as e: + logger.error("Erreur long press impression: %s", e) + self._led.play("error") + + @property + def relay_state(self) -> bool: + return self._relay_enabled diff --git a/backend/services/config_service.py b/backend/services/config_service.py new file mode 100644 index 0000000..c3941f2 --- /dev/null +++ b/backend/services/config_service.py @@ -0,0 +1,183 @@ +"""Service de chargement et sauvegarde de la configuration YAML.""" + +from __future__ import annotations +import logging +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import yaml + +logger = logging.getLogger(__name__) + + +@dataclass +class AppConfig: + name: str = "JH Photomaton" + host: str = "0.0.0.0" + port: int = 8090 + debug: bool = False + secret_key: str = "" + admin_password: str = "PhotoBooth2026!" + + +@dataclass +class PhotoboothConfig: + base_url: str = "http://localhost:8083" + data_dir: str = "/home/pi/photobooth-data" + config_file: str = "/home/pi/.config/photobooth-app/config.json" + media_dir: str = "/home/pi/photobooth-data/media/processed_full" + userdata_dir: str = "/home/pi/photobooth-data/userdata" + + +@dataclass +class ButtonConfig: + pin: int = 23 + relay_pin: int = 12 + debounce_ms: int = 50 + double_click_ms: int = 400 + long_press_ms: int = 1500 + max_clicks: int = 4 + long_press_action: str = "print_last" + print_enabled: bool = True + + +@dataclass +class LEDEffectConfig: + color: list = field(default_factory=lambda: [0, 30, 80]) + mode: str = "solid" + speed: float = 0.05 + flashes: int = 2 + flash_duration: float = 0.1 + duration: float = 2.0 + + +@dataclass +class LEDConfig: + pin: int = 18 + count: int = 35 + brightness: int = 180 + freq_hz: int = 800000 + dma: int = 10 + strip_type: str = "WS2812" + effects: dict = field(default_factory=dict) + + def get_effect(self, name: str) -> LEDEffectConfig: + raw = self.effects.get(name, {}) + return LEDEffectConfig( + color=raw.get("color", [0, 30, 80]), + mode=raw.get("mode", "solid"), + speed=raw.get("speed", 0.05), + flashes=raw.get("flashes", 2), + flash_duration=raw.get("flash_duration", 0.1), + duration=raw.get("duration", 2.0), + ) + + +@dataclass +class PrintConfig: + mode: str = "validation" + script_path: str = "/home/pi/photobooth-data/script/script_print.sh" + default_copies: int = 1 + printers: list = field(default_factory=list) + + +@dataclass +class GalleryConfig: + public_enabled: bool = True + photos_per_page: int = 24 + qr_base_url: str = "https://photomaton.lessapinsduweb.com" + + +@dataclass +class Config: + app: AppConfig = field(default_factory=AppConfig) + photobooth: PhotoboothConfig = field(default_factory=PhotoboothConfig) + button: ButtonConfig = field(default_factory=ButtonConfig) + leds: LEDConfig = field(default_factory=LEDConfig) + print: PrintConfig = field(default_factory=PrintConfig) + gallery: GalleryConfig = field(default_factory=GalleryConfig) + button_actions: dict = field(default_factory=dict) + + +class ConfigService: + def __init__(self, path: Path): + self.path = path + self._config: Config | None = None + + def load(self) -> Config: + if not self.path.exists(): + logger.warning(f"Config introuvable: {self.path} — utilisation des valeurs par défaut") + self._config = Config() + return self._config + + with open(self.path, encoding="utf-8") as f: + raw: dict[str, Any] = yaml.safe_load(f) or {} + + cfg = Config() + + if "app" in raw: + cfg.app = AppConfig(**{k: v for k, v in raw["app"].items() if hasattr(AppConfig, k)}) + + if "photobooth" in raw: + cfg.photobooth = PhotoboothConfig(**{k: v for k, v in raw["photobooth"].items() if hasattr(PhotoboothConfig, k)}) + + if "button" in raw: + cfg.button = ButtonConfig(**{k: v for k, v in raw["button"].items() if hasattr(ButtonConfig, k)}) + + if "leds" in raw: + led_raw = raw["leds"] + cfg.leds = LEDConfig( + pin=led_raw.get("pin", 18), + count=led_raw.get("count", 35), + brightness=led_raw.get("brightness", 180), + freq_hz=led_raw.get("freq_hz", 800000), + dma=led_raw.get("dma", 10), + strip_type=led_raw.get("strip_type", "WS2812"), + effects=led_raw.get("effects", {}), + ) + + if "print" in raw: + cfg.print = PrintConfig(**{k: v for k, v in raw["print"].items() if hasattr(PrintConfig, k)}) + + if "gallery" in raw: + cfg.gallery = GalleryConfig(**{k: v for k, v in raw["gallery"].items() if hasattr(GalleryConfig, k)}) + + cfg.button_actions = raw.get("button_actions", { + 1: {"label": "Photo normale", "photobooth_index": 0}, + 2: {"label": "Photo étoile", "photobooth_index": 1}, + 3: {"label": "Photo cailloux", "photobooth_index": 2}, + 4: {"label": "Photo soirée", "photobooth_index": 3}, + }) + + self._config = cfg + logger.info("Configuration chargée depuis %s", self.path) + return cfg + + def save_button_actions(self, button_actions: dict): + """Met à jour uniquement la section button_actions dans settings.yaml.""" + with open(self.path, encoding="utf-8") as f: + raw = yaml.safe_load(f) or {} + + raw["button_actions"] = button_actions + + with open(self.path, "w", encoding="utf-8") as f: + yaml.dump(raw, f, allow_unicode=True, default_flow_style=False) + + if self._config: + self._config.button_actions = button_actions + + def save_print_mode(self, mode: str): + with open(self.path, encoding="utf-8") as f: + raw = yaml.safe_load(f) or {} + raw.setdefault("print", {})["mode"] = mode + with open(self.path, "w", encoding="utf-8") as f: + yaml.dump(raw, f, allow_unicode=True, default_flow_style=False) + if self._config: + self._config.print.mode = mode + + @property + def config(self) -> Config: + if self._config is None: + self.load() + return self._config diff --git a/backend/services/led_service.py b/backend/services/led_service.py new file mode 100644 index 0000000..c43af06 --- /dev/null +++ b/backend/services/led_service.py @@ -0,0 +1,308 @@ +"""Service de contrôle de l'anneau LED WS2812b (GPIO18, 35 LEDs). + +Mode réel : rpi_ws281x (Raspberry Pi, doit tourner en root ou avec /dev/mem) +Mode mock : log des opérations uniquement (développement) +""" + +from __future__ import annotations + +import asyncio +import logging +import threading +import time +from typing import Callable + +from backend.services.config_service import Config, LEDConfig + +logger = logging.getLogger(__name__) + +# ── Tentative d'import de rpi_ws281x ───────────────────────────────────────── +try: + from rpi_ws281x import PixelStrip, Color as WS_Color, ws + HAS_WS281X = True +except ImportError: + HAS_WS281X = False + logger.warning("rpi_ws281x non disponible — mode mock LED activé") + + class WS_Color: # type: ignore + def __init__(self, r: int, g: int, b: int): + self.r, self.g, self.b = r, g, b + def __repr__(self): + return f"Color({self.r},{self.g},{self.b})" + + class PixelStrip: # type: ignore + def __init__(self, *args, **kwargs): pass + def begin(self): pass + def show(self): pass + def setPixelColor(self, i, c): pass + def numPixels(self): return 35 + def setBrightness(self, b): pass + + +def _color(rgb: list[int]) -> WS_Color: + return WS_Color(rgb[0], rgb[1], rgb[2]) + + +def _lerp(a: int, b: int, t: float) -> int: + return int(a + (b - a) * t) + + +class LEDService: + """Contrôle l'anneau LED via un thread dédié + queue de commandes.""" + + EFFECTS = ("idle", "countdown", "capture", "captured", "finished", + "printing", "error", "disabled", "off") + + def __init__(self, config: Config): + self._cfg: LEDConfig = config.leds + self._strip: PixelStrip | None = None + self._thread: threading.Thread | None = None + self._stop_event = threading.Event() + self._cmd_event = threading.Event() + self._current_effect: str = "idle" + self._countdown_duration: float = 5.0 + self._lock = threading.Lock() + self._on_change_cb: Callable | None = None + self._loop: asyncio.AbstractEventLoop | None = None + + # ── Lifecycle ───────────────────────────────────────────────────────────── + + def start(self): + if HAS_WS281X: + self._strip = PixelStrip( + self._cfg.count, + self._cfg.pin, + self._cfg.freq_hz, + self._cfg.dma, + False, # invert + self._cfg.brightness, + 0, # channel + ) + try: + self._strip.begin() + logger.info("Strip WS2812b initialisé (%d LEDs, GPIO%d)", self._cfg.count, self._cfg.pin) + except Exception as e: + logger.error("Erreur init strip LED: %s", e) + self._strip = PixelStrip() # fallback mock + else: + self._strip = PixelStrip() + + self._stop_event.clear() + self._thread = threading.Thread(target=self._run, daemon=True, name="led-service") + self._thread.start() + + def stop(self): + self._stop_event.set() + self._cmd_event.set() + if self._thread: + self._thread.join(timeout=2) + self._all_off() + + def set_loop(self, loop: asyncio.AbstractEventLoop): + self._loop = loop + + def set_on_change(self, cb: Callable): + self._on_change_cb = cb + + # ── Commandes publiques (thread-safe) ──────────────────────────────────── + + def play(self, effect: str, countdown_duration: float = 5.0): + """Joue un effet nommé. Thread-safe.""" + with self._lock: + self._current_effect = effect + self._countdown_duration = countdown_duration + self._cmd_event.set() + logger.debug("LED effet: %s", effect) + self._notify_change(effect) + + def set_color(self, r: int, g: int, b: int): + """Couleur fixe immédiate.""" + self._fill(WS_Color(r, g, b)) + + # ── Thread principal ────────────────────────────────────────────────────── + + def _run(self): + effect_func = { + "idle": self._effect_idle, + "countdown": self._effect_countdown, + "capture": self._effect_capture, + "captured": self._effect_solid, + "finished": self._effect_finished, + "printing": self._effect_spin, + "error": self._effect_error, + "disabled": self._effect_solid, + "off": self._effect_off, + } + + while not self._stop_event.is_set(): + self._cmd_event.clear() + with self._lock: + current = self._current_effect + cd_dur = self._countdown_duration + + fn = effect_func.get(current, self._effect_idle) + + try: + if current in ("countdown",): + fn(cd_dur) + else: + fn() + except Exception as e: + logger.error("Erreur effet LED '%s': %s", current, e) + + # Si l'effet s'est terminé naturellement (ex: finished → retour idle) + # on vérifie si un nouveau cmd est arrivé + if not self._cmd_event.is_set() and not self._stop_event.is_set(): + with self._lock: + if self._current_effect == current: + # Retour automatique à idle après effets ponctuels + if current in ("capture", "captured", "finished", "error"): + self._current_effect = "idle" + self._cmd_event.wait(timeout=0.1) + + # ── Effets ─────────────────────────────────────────────────────────────── + + def _effect_idle(self): + """Respiration bleue douce.""" + cfg = self._cfg.get_effect("idle") + c = cfg.color + speed = cfg.speed + n = self._cfg.count + + step = 0 + while not self._cmd_event.is_set() and not self._stop_event.is_set(): + t = (1 + __import__("math").sin(step * 0.1)) / 2 # 0..1 + brightness = max(0.05, t) + color = WS_Color( + int(c[0] * brightness), + int(c[1] * brightness), + int(c[2] * brightness), + ) + self._fill(color) + time.sleep(speed) + step += 1 + + def _effect_countdown(self, duration: float = 5.0): + """Remplissage progressif vert LED par LED.""" + cfg = self._cfg.get_effect("countdown") + c = cfg.color + n = self._cfg.count + blank = WS_Color(0, 0, 0) + color = WS_Color(c[0], c[1], c[2]) + + self._fill(blank) + + t_start = time.time() + while not self._cmd_event.is_set() and not self._stop_event.is_set(): + elapsed = time.time() - t_start + ratio = min(elapsed / duration, 1.0) + leds_on = int(ratio * n) + + for i in range(n): + self._strip.setPixelColor(i, color if i < leds_on else blank) + self._strip.show() + + if ratio >= 1.0: + break + time.sleep(0.04) + + def _effect_capture(self): + """Flash blanc.""" + cfg = self._cfg.get_effect("capture") + white = WS_Color(255, 255, 255) + off = WS_Color(0, 0, 0) + for _ in range(cfg.flashes): + if self._cmd_event.is_set(): + break + self._fill(white) + time.sleep(cfg.flash_duration) + self._fill(off) + time.sleep(cfg.flash_duration) + + def _effect_solid(self): + """Couleur pleine selon l'effet courant.""" + with self._lock: + current = self._current_effect + cfg = self._cfg.get_effect(current) + self._fill(WS_Color(*cfg.color)) + # Attente jusqu'à prochain cmd + self._cmd_event.wait() + + def _effect_finished(self): + """Bleu fixe pendant duration secondes, puis retour idle.""" + cfg = self._cfg.get_effect("finished") + self._fill(WS_Color(*cfg.color)) + self._cmd_event.wait(timeout=cfg.duration) + + def _effect_spin(self): + """Rotation d'une traînée de LEDs.""" + cfg = self._cfg.get_effect("printing") + c = cfg.color + n = self._cfg.count + speed = cfg.speed + tail = 6 + + pos = 0 + while not self._cmd_event.is_set() and not self._stop_event.is_set(): + for i in range(n): + dist = (i - pos) % n + if dist < tail: + factor = (tail - dist) / tail + self._strip.setPixelColor( + i, + WS_Color( + int(c[0] * factor), + int(c[1] * factor), + int(c[2] * factor), + ), + ) + else: + self._strip.setPixelColor(i, WS_Color(0, 0, 0)) + self._strip.show() + pos = (pos + 1) % n + time.sleep(speed) + + def _effect_error(self): + """Flash rouge.""" + cfg = self._cfg.get_effect("error") + red = WS_Color(cfg.color[0], cfg.color[1], cfg.color[2]) + off = WS_Color(0, 0, 0) + for _ in range(cfg.flashes): + if self._cmd_event.is_set(): + break + self._fill(red) + time.sleep(0.15) + self._fill(off) + time.sleep(0.15) + + def _effect_off(self): + self._fill(WS_Color(0, 0, 0)) + self._cmd_event.wait() + + # ── Helpers ─────────────────────────────────────────────────────────────── + + def _fill(self, color: WS_Color): + if not HAS_WS281X: + logger.debug("LED mock fill: %s", color) + return + for i in range(self._cfg.count): + self._strip.setPixelColor(i, color) + self._strip.show() + + def _all_off(self): + try: + self._fill(WS_Color(0, 0, 0)) + except Exception: + pass + + def _notify_change(self, effect: str): + if self._on_change_cb and self._loop: + asyncio.run_coroutine_threadsafe( + self._on_change_cb({"type": "led_effect", "effect": effect}), + self._loop, + ) + + @property + def current_effect(self) -> str: + with self._lock: + return self._current_effect diff --git a/backend/services/photobooth_service.py b/backend/services/photobooth_service.py new file mode 100644 index 0000000..25bdb9e --- /dev/null +++ b/backend/services/photobooth_service.py @@ -0,0 +1,129 @@ +"""Client HTTP pour l'API de photobooth-app.""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from typing import Any + +import httpx + +from backend.services.config_service import Config + +logger = logging.getLogger(__name__) + + +class PhotoboothService: + def __init__(self, config: Config): + self._cfg = config.photobooth + self._base = self._cfg.base_url.rstrip("/") + self._client = httpx.AsyncClient(base_url=self._base, timeout=10.0) + + async def trigger_image_action(self, index: int): + """Déclenche l'action image à l'index donné.""" + r = await self._client.get(f"/api/actions/image/{index}") + r.raise_for_status() + logger.info("Action image %d déclenchée", index) + return r.json() if r.text else {} + + async def trigger_share_latest(self, share_index: int = 0): + """Déclenche l'action de partage (impression) sur la dernière photo.""" + r = await self._client.get(f"/api/share/actions/latest/{share_index}") + r.raise_for_status() + logger.info("Share action %d déclenchée", share_index) + return r.json() if r.text else {} + + async def get_media_collection(self, limit: int = 200) -> list[dict]: + """Retourne la liste des photos de la galerie.""" + try: + r = await self._client.get("/api/mediacollection/", params={"limit": limit}) + r.raise_for_status() + data = r.json() + # photobooth-app retourne soit une liste soit {"items": [...]} + if isinstance(data, list): + return data + return data.get("items", data.get("media_items", [])) + except Exception as e: + logger.error("Erreur récupération galerie: %s", e) + return [] + + async def get_latest_media(self) -> dict | None: + """Retourne les infos de la dernière photo.""" + items = await self.get_media_collection(limit=1) + return items[0] if items else None + + async def delete_media(self, media_id: str) -> bool: + """Supprime une photo via l'API photobooth-app.""" + try: + r = await self._client.delete(f"/api/mediacollection/{media_id}") + return r.status_code in (200, 204) + except Exception as e: + logger.error("Erreur suppression %s: %s", media_id, e) + return False + + async def is_alive(self) -> bool: + """Vérifie que photobooth-app répond.""" + try: + r = await self._client.get("/api/about", timeout=3.0) + return r.status_code == 200 + except Exception: + return False + + def media_url(self, identifier: str) -> str: + return f"{self._base}/media/full/{identifier}" + + def thumbnail_url(self, identifier: str) -> str: + return f"{self._base}/media/thumbnail/{identifier}" + + async def read_pb_config(self) -> dict: + """Lit le fichier config.json de photobooth-app.""" + cfg_path = Path(self._cfg.config_file) + if not cfg_path.exists(): + logger.warning("Config photobooth introuvable: %s", cfg_path) + return {} + with open(cfg_path, encoding="utf-8") as f: + return json.load(f) + + async def write_pb_config(self, config: dict): + """Écrit le fichier config.json de photobooth-app.""" + cfg_path = Path(self._cfg.config_file) + # Backup avant écriture + backup = cfg_path.with_suffix(f".json_backup_jh") + if cfg_path.exists(): + import shutil + shutil.copy2(cfg_path, backup) + + with open(cfg_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + logger.info("Config photobooth-app mise à jour") + + async def list_userdata_frames(self) -> list[str]: + """Liste les cadres PNG disponibles dans userdata.""" + return self._list_files(self._cfg.userdata_dir, "*.png", "frames") + + async def list_userdata_backgrounds(self) -> list[str]: + """Liste les fonds disponibles dans userdata.""" + exts = ["*.jpg", "*.jpeg", "*.png"] + files = [] + for ext in exts: + files.extend(self._list_files(self._cfg.userdata_dir, ext, "backgrounds")) + return sorted(set(files)) + + def _list_files(self, base: str, pattern: str, subdir_hint: str) -> list[str]: + base_path = Path(base) + if not base_path.exists(): + return [] + results = [] + for f in base_path.rglob(pattern): + if subdir_hint in f.parts or True: # liste tout + # Chemin relatif depuis data_dir pour passer à photobooth-app + try: + rel = f.relative_to(Path(self._cfg.data_dir)) + results.append(str(rel)) + except ValueError: + results.append(str(f)) + return sorted(results) + + async def close(self): + await self._client.aclose() diff --git a/backend/services/printer_service.py b/backend/services/printer_service.py new file mode 100644 index 0000000..75eb15e --- /dev/null +++ b/backend/services/printer_service.py @@ -0,0 +1,242 @@ +"""Service d'impression — file d'attente SQLite + appel script_print.sh.""" + +from __future__ import annotations + +import asyncio +import logging +import subprocess +import time +import uuid +from pathlib import Path +from typing import Literal + +import aiosqlite + +from backend.services.config_service import Config + +logger = logging.getLogger(__name__) + +PrintStatus = Literal["pending", "printing", "done", "cancelled", "error"] + +CREATE_SQL = """ +CREATE TABLE IF NOT EXISTS print_queue ( + id TEXT PRIMARY KEY, + filename TEXT NOT NULL, + thumb_url TEXT, + copies INTEGER DEFAULT 1, + status TEXT DEFAULT 'pending', + printer TEXT, + requested_at REAL, + processed_at REAL, + error_msg TEXT +); +""" + + +class PrinterService: + def __init__(self, config: Config): + self._cfg = config.print + self._db_path: Path | None = None + self._db: aiosqlite.Connection | None = None + self._lock = asyncio.Lock() + + async def init_db(self, db_path: Path): + db_path.parent.mkdir(parents=True, exist_ok=True) + self._db_path = db_path + self._db = await aiosqlite.connect(str(db_path)) + self._db.row_factory = aiosqlite.Row + await self._db.execute(CREATE_SQL) + await self._db.commit() + logger.info("Base print_queue initialisée: %s", db_path) + + async def close(self): + if self._db: + await self._db.close() + + # ── File d'attente ──────────────────────────────────────────────────────── + + async def add_request(self, filename: str, thumb_url: str = "", copies: int = 1) -> dict: + """Ajoute une demande d'impression dans la file. Retourne l'entrée créée.""" + entry_id = str(uuid.uuid4()) + now = time.time() + + await self._db.execute( + "INSERT INTO print_queue (id, filename, thumb_url, copies, status, requested_at) " + "VALUES (?, ?, ?, ?, 'pending', ?)", + (entry_id, filename, thumb_url, copies, now), + ) + await self._db.commit() + + entry = { + "id": entry_id, + "filename": filename, + "thumb_url": thumb_url, + "copies": copies, + "status": "pending", + "requested_at": now, + } + + # Mode direct : impression immédiate sans validation + if self._cfg.mode == "direct": + asyncio.create_task(self.execute_print(entry_id, copies)) + + logger.info("Demande d'impression ajoutée: %s (%s)", entry_id, filename) + return entry + + async def get_queue(self, status: str | None = None) -> list[dict]: + """Liste les entrées de la file d'attente.""" + if status: + cursor = await self._db.execute( + "SELECT * FROM print_queue WHERE status = ? ORDER BY requested_at DESC", + (status,), + ) + else: + cursor = await self._db.execute( + "SELECT * FROM print_queue ORDER BY requested_at DESC LIMIT 100" + ) + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + async def get_pending(self) -> list[dict]: + return await self.get_queue("pending") + + async def cancel(self, entry_id: str) -> bool: + async with self._lock: + cursor = await self._db.execute( + "SELECT status FROM print_queue WHERE id = ?", (entry_id,) + ) + row = await cursor.fetchone() + if not row or row["status"] not in ("pending",): + return False + await self._db.execute( + "UPDATE print_queue SET status='cancelled', processed_at=? WHERE id=?", + (time.time(), entry_id), + ) + await self._db.commit() + return True + + # ── Impression ──────────────────────────────────────────────────────────── + + async def execute_print(self, entry_id: str, copies: int = 1, printer: str = "") -> dict: + """Lance l'impression via script_print.sh.""" + async with self._lock: + cursor = await self._db.execute( + "SELECT * FROM print_queue WHERE id = ?", (entry_id,) + ) + entry = await cursor.fetchone() + if not entry: + return {"success": False, "error": "Entrée introuvable"} + if entry["status"] not in ("pending",): + return {"success": False, "error": f"Statut incompatible: {entry['status']}"} + + await self._db.execute( + "UPDATE print_queue SET status='printing', copies=? WHERE id=?", + (copies, entry_id), + ) + await self._db.commit() + + filename = entry["filename"] + script = self._cfg.script_path + + # Appel async du script d'impression + try: + result = await asyncio.to_thread( + self._run_print_script, script, filename, copies + ) + except Exception as e: + result = {"success": False, "error": str(e), "printer": ""} + + now = time.time() + if result["success"]: + await self._db.execute( + "UPDATE print_queue SET status='done', printer=?, processed_at=? WHERE id=?", + (result.get("printer", ""), now, entry_id), + ) + else: + await self._db.execute( + "UPDATE print_queue SET status='error', error_msg=?, processed_at=? WHERE id=?", + (result.get("error", ""), now, entry_id), + ) + await self._db.commit() + + logger.info("Impression %s: %s", entry_id, "OK" if result["success"] else result.get("error")) + return result + + def _run_print_script(self, script: str, filename: str, copies: int) -> dict: + """Appelle script_print.sh de façon synchrone.""" + if not Path(script).exists(): + return {"success": False, "error": f"Script introuvable: {script}"} + if not Path(filename).exists(): + return {"success": False, "error": f"Fichier introuvable: {filename}"} + + cmd = ["/bin/bash", script, filename, "image", "default", str(copies)] + try: + proc = subprocess.run( + cmd, capture_output=True, text=True, timeout=60 + ) + stdout = proc.stdout.strip() + if proc.returncode == 0 and "PRINTED:" in stdout: + parts = stdout.split(":") + printer = parts[1] if len(parts) > 1 else "" + return {"success": True, "printer": printer, "output": stdout} + else: + return {"success": False, "error": proc.stderr.strip() or stdout, "printer": ""} + except subprocess.TimeoutExpired: + return {"success": False, "error": "Timeout impression (60s)"} + except Exception as e: + return {"success": False, "error": str(e)} + + # ── Statut imprimantes CUPS ─────────────────────────────────────────────── + + async def get_printers_status(self) -> list[dict]: + """Retourne le statut des imprimantes CUPS configurées.""" + statuses = [] + for p in self._cfg.printers: + status = await asyncio.to_thread(self._get_printer_status, p["name"]) + statuses.append({ + "name": p["name"], + "label": p["label"], + **status, + }) + return statuses + + def _get_printer_status(self, printer_name: str) -> dict: + try: + result = subprocess.run( + ["lpstat", "-p", printer_name], + capture_output=True, text=True, timeout=5 + ) + output = result.stdout.lower() + if "idle" in output: + state = "idle" + elif "printing" in output or "processing" in output: + state = "printing" + elif "disabled" in output: + state = "disabled" + elif "not found" in output or result.returncode != 0: + state = "offline" + else: + state = "unknown" + + # Compte les jobs en attente + jobs_result = subprocess.run( + ["lpstat", "-o", printer_name], + capture_output=True, text=True, timeout=5 + ) + jobs = len([l for l in jobs_result.stdout.strip().splitlines() if l]) + + return {"state": state, "jobs": jobs} + except Exception as e: + return {"state": "error", "jobs": 0, "error": str(e)} + + async def cancel_cups_jobs(self, printer_name: str) -> bool: + """Annule tous les jobs CUPS pour une imprimante.""" + try: + result = subprocess.run( + ["cancel", "-a", printer_name], + capture_output=True, text=True, timeout=10 + ) + return result.returncode == 0 + except Exception as e: + logger.error("Erreur cancel CUPS: %s", e) + return False diff --git a/backend/services/system_service.py b/backend/services/system_service.py new file mode 100644 index 0000000..2a768db --- /dev/null +++ b/backend/services/system_service.py @@ -0,0 +1,117 @@ +"""Service de surveillance des ressources système du Raspberry Pi.""" + +from __future__ import annotations + +import logging +import subprocess +from pathlib import Path + +try: + import psutil + HAS_PSUTIL = True +except ImportError: + HAS_PSUTIL = False + logging.warning("psutil non disponible — stats système limitées") + +logger = logging.getLogger(__name__) + + +class SystemService: + def get_stats(self) -> dict: + stats: dict = {} + + if not HAS_PSUTIL: + return {"error": "psutil non installé"} + + # CPU + stats["cpu_percent"] = psutil.cpu_percent(interval=None) + + # RAM + mem = psutil.virtual_memory() + stats["ram_total_mb"] = round(mem.total / 1024 / 1024) + stats["ram_used_mb"] = round(mem.used / 1024 / 1024) + stats["ram_percent"] = mem.percent + stats["ram_available_mb"] = round(mem.available / 1024 / 1024) + + # Disque (racine) + disk = psutil.disk_usage("/") + stats["disk_total_gb"] = round(disk.total / 1024 ** 3, 1) + stats["disk_used_gb"] = round(disk.used / 1024 ** 3, 1) + stats["disk_percent"] = disk.percent + + # Température CPU (Raspberry Pi) + stats["cpu_temp"] = self._get_cpu_temp() + + # Uptime + import time + boot_time = psutil.boot_time() + uptime_sec = int(time.time() - boot_time) + stats["uptime"] = self._format_uptime(uptime_sec) + + return stats + + def _get_cpu_temp(self) -> float | None: + # Méthode 1 : fichier thermal du Pi + try: + temp_path = Path("/sys/class/thermal/thermal_zone0/temp") + if temp_path.exists(): + return round(int(temp_path.read_text().strip()) / 1000, 1) + except Exception: + pass + + # Méthode 2 : vcgencmd (Pi OS) + try: + result = subprocess.run( + ["vcgencmd", "measure_temp"], + capture_output=True, text=True, timeout=3 + ) + if result.returncode == 0: + # output: "temp=47.0'C" + val = result.stdout.strip().replace("temp=", "").replace("'C", "") + return float(val) + except Exception: + pass + + # Méthode 3 : psutil sensors (si disponible) + if hasattr(psutil, "sensors_temperatures"): + try: + temps = psutil.sensors_temperatures() + if temps: + first = next(iter(temps.values())) + if first: + return round(first[0].current, 1) + except Exception: + pass + + return None + + def _format_uptime(self, seconds: int) -> str: + days = seconds // 86400 + hours = (seconds % 86400) // 3600 + minutes = (seconds % 3600) // 60 + if days > 0: + return f"{days}j {hours:02d}h {minutes:02d}m" + return f"{hours:02d}h {minutes:02d}m" + + def get_services_status(self) -> list[dict]: + """Vérifie le statut des services systemd utiles.""" + services = [ + ("photobooth-app", "Photobooth App"), + ("jh-photomaton", "JH Photomaton"), + ("cups", "CUPS (Impression)"), + ] + result = [] + for svc_name, label in services: + active = self._is_service_active(svc_name) + result.append({"name": svc_name, "label": label, "active": active}) + return result + + def _is_service_active(self, service: str) -> bool: + try: + r = subprocess.run( + ["systemctl", "is-active", service], + capture_output=True, text=True, timeout=3 + ) + return r.stdout.strip() == "active" + except Exception: + return False diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/auth.py b/backend/utils/auth.py new file mode 100644 index 0000000..dde1644 --- /dev/null +++ b/backend/utils/auth.py @@ -0,0 +1,19 @@ +"""Authentification admin — session cookie simple.""" + +from functools import wraps +from fastapi import Request +from fastapi.responses import RedirectResponse + + +def is_authenticated(request: Request) -> bool: + return request.session.get("authenticated") is True + + +def require_auth(func): + """Décorateur pour les routes admin.""" + @wraps(func) + async def wrapper(request: Request, *args, **kwargs): + if not is_authenticated(request): + return RedirectResponse(url="/admin/login", status_code=302) + return await func(request, *args, **kwargs) + return wrapper diff --git a/backend/utils/ws_manager.py b/backend/utils/ws_manager.py new file mode 100644 index 0000000..49b5b6c --- /dev/null +++ b/backend/utils/ws_manager.py @@ -0,0 +1,45 @@ +"""Gestionnaire de connexions WebSocket.""" + +import json +import logging +from typing import Any + +from fastapi import WebSocket + +logger = logging.getLogger(__name__) + + +class WSManager: + def __init__(self): + self.active: list[WebSocket] = [] + + async def connect(self, ws: WebSocket): + await ws.accept() + self.active.append(ws) + logger.debug("WS connecté (%d total)", len(self.active)) + + def disconnect(self, ws: WebSocket): + self.active.discard(ws) if hasattr(self.active, "discard") else None + if ws in self.active: + self.active.remove(ws) + logger.debug("WS déconnecté (%d restants)", len(self.active)) + + async def broadcast(self, data: dict[str, Any]): + if not self.active: + return + msg = json.dumps(data, default=str) + dead = [] + for ws in self.active: + try: + await ws.send_text(msg) + except Exception: + dead.append(ws) + for ws in dead: + self.disconnect(ws) + + async def send(self, ws: WebSocket, data: dict[str, Any]): + try: + await ws.send_text(json.dumps(data, default=str)) + except Exception as e: + logger.debug("Erreur envoi WS: %s", e) + self.disconnect(ws) diff --git a/config/settings.yaml b/config/settings.yaml new file mode 100644 index 0000000..dc70c17 --- /dev/null +++ b/config/settings.yaml @@ -0,0 +1,96 @@ +# ============================================================ +# JH Photomaton — Configuration principale +# Association Les Sapins Du Web (LSDW) +# ============================================================ + +app: + name: "JH Photomaton" + host: "0.0.0.0" + port: 8090 + debug: false + secret_key: "photomaton-jh-2026-secret-change-me" + admin_password: "PhotoBooth2026!" + +# --- Intégration photobooth-app --- +photobooth: + base_url: "http://localhost:8083" + data_dir: "/home/pi/photobooth-data" + config_file: "/home/pi/.config/photobooth-app/config.json" + media_dir: "/home/pi/photobooth-data/media/processed_full" + userdata_dir: "/home/pi/photobooth-data/userdata" + +# --- Bouton physique --- +button: + pin: 23 # GPIO BCM (input, pull-up interne) + relay_pin: 12 # GPIO BCM (output, relay ON/OFF bouton 12V) + debounce_ms: 50 + double_click_ms: 400 # Délai max entre clics pour multi-clic + long_press_ms: 1500 # Durée min long appui + max_clicks: 4 # Nombre max de clics reconnus + long_press_action: "print_last" # print_last | none + print_enabled: true + +# --- Anneau LED WS2812b --- +leds: + pin: 18 # GPIO BCM (PWM hardware, pin physique 12) + count: 35 # Nombre de LEDs dans l'anneau + brightness: 180 # 0-255 + freq_hz: 800000 + dma: 10 + strip_type: "WS2812" + effects: + idle: + color: [0, 30, 80] + mode: "breathe" + speed: 0.025 + countdown: + color: [0, 200, 80] + mode: "fill_progressive" + capture: + color: [255, 255, 255] + mode: "flash" + flashes: 2 + flash_duration: 0.1 + captured: + color: [150, 0, 200] + mode: "solid" + finished: + color: [0, 100, 255] + mode: "solid" + duration: 2.0 + printing: + color: [0, 120, 255] + mode: "spin" + speed: 0.05 + error: + color: [220, 0, 0] + mode: "flash" + flashes: 4 + disabled: + color: [60, 0, 0] + mode: "solid" + +# --- Impression --- +print: + mode: "validation" # direct | gallery | validation + script_path: "/home/pi/photobooth-data/script/script_print.sh" + default_copies: 1 + printers: + - name: "Selphy_Blanche_WiFi" + label: "Selphy Blanche (WiFi)" + - name: "Selphy_Noire_WiFi" + label: "Selphy Noire (WiFi)" + +# --- Galerie --- +gallery: + public_enabled: true + photos_per_page: 24 + qr_base_url: "https://photomaton.lessapinsduweb.com" + +# --- Mapping clics bouton -> actions photobooth-app --- +# n clics -> photobooth_index dans la liste actions.image de config.json +button_actions: + 1: { label: "Photo normale", photobooth_index: 0 } + 2: { label: "Photo étoile", photobooth_index: 1 } + 3: { label: "Photo cailloux", photobooth_index: 2 } + 4: { label: "Photo soirée", photobooth_index: 3 } diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/CI-CD-SETUP.md b/docs/CI-CD-SETUP.md new file mode 100644 index 0000000..3c60e7e --- /dev/null +++ b/docs/CI-CD-SETUP.md @@ -0,0 +1,121 @@ +# CI/CD — Gitea Actions → Raspberry Pi + +## Ce que ça fait + +À chaque `git push` sur `main`, Gitea : +1. Vérifie la syntaxe Python (`lint`) +2. SSH sur le Pi → `git pull` + `pip install` + `systemctl restart` + +Déclenchement manuel possible (avec option "installation complète"). + +--- + +## 1. Préparer le Pi + +### Cloner le dépôt la première fois + +```bash +cd /home/pi +git clone https://gitea.lespatas.ovh/admin/photoBooth.git jh-photomaton +cd jh-photomaton +sudo bash scripts/install.sh +``` + +### Installer la règle sudoers (permet au CI de restart sans mot de passe) + +```bash +sudo cp scripts/sudoers-jh-photomaton /etc/sudoers.d/jh-photomaton +sudo chmod 440 /etc/sudoers.d/jh-photomaton +sudo visudo -c # doit afficher "parsed OK" +``` + +### Générer une clé SSH dédiée au CI/CD + +Sur le Pi : +```bash +ssh-keygen -t ed25519 -C "gitea-cicd-jh-photomaton" -f ~/.ssh/gitea_deploy -N "" +cat ~/.ssh/gitea_deploy.pub >> ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys +# Afficher la clé privée à copier dans Gitea : +cat ~/.ssh/gitea_deploy +``` + +--- + +## 2. Configurer les secrets dans Gitea + +`Settings → Secrets and Variables → Actions → New Secret` + +| Nom | Valeur | +|-----|--------| +| `PI_SSH_HOST` | IP du Pi (ex: `192.168.1.42`) ou hostname si résolvable depuis le serveur Gitea | +| `PI_SSH_USER` | `pi` | +| `PI_SSH_KEY` | Contenu de `~/.ssh/gitea_deploy` (clé **privée**, commence par `-----BEGIN...`) | +| `PI_SSH_PORT` | `22` (optionnel) | + +--- + +## 3. Configurer le runner Gitea Actions + +Le workflow nécessite un runner Gitea Actions avec le label `ubuntu-latest`. + +### Option A — Runner sur le serveur Gitea (recommandé) + +Sur le serveur qui héberge Gitea : +```bash +# Télécharger l'act runner Gitea +wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64 -O act_runner +chmod +x act_runner + +# Enregistrer le runner (token dans Gitea : Settings → Actions → Runners) +./act_runner register --instance https://gitea.lespatas.ovh \ + --token VOTRE_TOKEN --name "gitea-server" --labels "ubuntu-latest:docker://node:16-bullseye" + +# Démarrer +./act_runner daemon +``` + +### Option B — Runner directement sur le Pi (plus simple, pas de Docker) + +```bash +wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-arm64 -O act_runner +chmod +x act_runner +./act_runner register --instance https://gitea.lespatas.ovh \ + --token VOTRE_TOKEN --name "pi-runner" --labels "ubuntu-latest:host" +./act_runner daemon & +``` + +> Avec `ubuntu-latest:host`, les jobs s'exécutent directement sur le Pi sans Docker. +> Dans ce cas, le job SSH est superflu — on peut simplifier le workflow pour exécuter +> `scripts/update.sh` directement. + +--- + +## 4. Premier push et vérification + +```bash +git add . +git commit -m "feat: ajout CI/CD Gitea Actions" +git push origin main +``` + +Suivre l'exécution dans Gitea : `Repository → Actions`. + +--- + +## 5. Déclenchement manuel (avec installation forcée) + +Dans Gitea : `Actions → Deploy — JH Photomaton → Run workflow` +Choisir `force_install = true` pour relancer `install.sh` complet (re-pip, re-service). + +--- + +## Résolution de problèmes + +| Problème | Solution | +|----------|----------| +| `Host key verification failed` | Ajouter le Pi dans `~/.ssh/known_hosts` du runner | +| `Permission denied (publickey)` | Vérifier que la clé publique est dans `authorized_keys` du Pi | +| `sudo: systemctl: command not found` | Utiliser `/bin/systemctl` (déjà dans update.sh) | +| `git stash pop` conflit | SSH sur le Pi, résoudre manuellement : `cd /home/pi/jh-photomaton && git checkout -- config/settings.yaml` | +| Le runner ne démarre pas | Vérifier le token dans `gitea.lespatas.ovh/admin/photoBooth/settings/actions/runners` | diff --git a/docs/JH-Photomaton-Guide-Installation.docx b/docs/JH-Photomaton-Guide-Installation.docx new file mode 100644 index 0000000000000000000000000000000000000000..0d6380e5f46c4fc43b9bcbda81296dda3bfbf2ff GIT binary patch literal 32963 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 +Galerie admin +Impression +Actions +Galerie publique +Déconnexion +{% endblock %} + +{% block content %} +
+

Gestion des actions & mapping bouton

+ + +
+
Mapping bouton → actions photobooth-app
+
+

Associe chaque nombre de clics à une action dans photobooth-app. L'index correspond à la position dans la liste des actions (0 = première action).

+ + + + + + + + + + + + {% for n in range(1, 5) %} + {% set mapping = button_mapping.get(n) or button_mapping.get(n|string) or {} %} + + + + + + + + {% endfor %} + +
ClicsLabel affichéIndex action (photobooth)Action correspondante
+ {% if n == 1 %}1 clic{% else %}{{ n }} clics{% endif %} + + + + + + {% set idx = mapping.get('photobooth_index', 0) %} + {% if pb_actions and idx < pb_actions|length %} + {{ pb_actions[idx].name }} + {% else %}—{% endif %} + + +
+ +
+
+ + +
+
Actions image photobooth-app ({{ pb_actions|length }} au total)
+
+ {% for action in pb_actions %} +
+
+
+ Action {{ loop.index0 }} + {{ action.name }} +
+
+ + +
+
+ + +
+ ⏱ {{ action.jobcontrol.get('countdown_capture', '?') }}s + {% if action.processing.get('img_frame_file') %} + 🖼 {{ action.processing.img_frame_file.split('/')[-1] }} + {% endif %} + {% if action.processing.get('remove_background') %} + Remove BG + {% endif %} + {% if action.processing.get('img_background_file') %} + 🌄 {{ action.processing.img_background_file.split('/')[-1] }} + {% endif %} +
+ + + +
+ {% else %} +
⚙️
Aucune action configurée dans photobooth-app
+ {% endfor %} +
+
+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/frontend/templates/admin/dashboard.html b/frontend/templates/admin/dashboard.html new file mode 100644 index 0000000..c36cf35 --- /dev/null +++ b/frontend/templates/admin/dashboard.html @@ -0,0 +1,281 @@ +{% extends "base.html" %} +{% block title %}Dashboard — JH Photomaton{% endblock %} + +{% block nav_links %} +Dashboard +Galerie admin +Impression +Actions +Galerie publique +Déconnexion +{% endblock %} + +{% block content %} +
+
+

Dashboard

+
+ Mode impression : + +
+
+ + +
+
Ressources système
+
+
+
CPU
+
{{ stats.cpu_percent|default('—') }}%
+
+
+
+
RAM
+
{{ stats.ram_used_mb|default('—') }} Mo
+
sur {{ stats.ram_total_mb|default('?') }} Mo
+
+
+
+
Température
+
{{ stats.cpu_temp|default('—') }}°C
+
CPU
+
+
+
Disque
+
{{ stats.disk_used_gb|default('—') }} Go
+
sur {{ stats.disk_total_gb|default('?') }} Go
+
+
+
+
+ +
+ +
+
Anneau LED (35 LEDs · GPIO18)
+
+
+ {{ led_effect }} +
+
+
Effet actuel
+
+ {% for effect in ['idle','countdown','capture','captured','finished','printing','error','disabled','off'] %} + + {% endfor %} +
+
+ Couleur : + + +
+
+
+
+ + +
+
Bouton physique (GPIO23) · Relay (GPIO12)
+
+
+
Relay bouton 12V
+
+ + +
+
Statut : {{ 'ON' if relay_state else 'OFF' }}
+
+
+
Simulation (test)
+
+ + + + +
+
+
+
Dernier événement : —
+
+
+ + +
+
+
File d'impression ()
+ Voir tout → +
+ +
+ + +
+
Services
+
+ + + + {% for svc in services %} + + + + + {% endfor %} + +
ServiceStatut
{{ svc.label }}{{ 'Actif' if svc.active else 'Arrêté' }}
+
+
+ +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/frontend/templates/admin/gallery.html b/frontend/templates/admin/gallery.html new file mode 100644 index 0000000..7b7e126 --- /dev/null +++ b/frontend/templates/admin/gallery.html @@ -0,0 +1,151 @@ +{% extends "base.html" %} +{% block title %}Galerie admin — JH Photomaton{% endblock %} + +{% block nav_links %} +Dashboard +Galerie admin +Impression +Actions +Galerie publique +Déconnexion +{% endblock %} + +{% block content %} +
+
+

Galerie — Administration

+
+ Chargement… +
+ + +
+
+
+ + +
+ + Page 1 / 1 + +
+ +
+
Chargement…
+
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/frontend/templates/admin/login.html b/frontend/templates/admin/login.html new file mode 100644 index 0000000..d45721f --- /dev/null +++ b/frontend/templates/admin/login.html @@ -0,0 +1,45 @@ + + + + + + Connexion — JH Photomaton + + + + + + diff --git a/frontend/templates/admin/print.html b/frontend/templates/admin/print.html new file mode 100644 index 0000000..5aab022 --- /dev/null +++ b/frontend/templates/admin/print.html @@ -0,0 +1,190 @@ +{% extends "base.html" %} +{% block title %}Impression — JH Photomaton{% endblock %} + +{% block nav_links %} +Dashboard +Galerie admin +Impression +Actions +Galerie publique +Déconnexion +{% endblock %} + +{% block content %} +
+

Gestion de l'impression

+ + +
+
Imprimantes CUPS
+
+ + + + {% for p in printers %} + + + + + + + {% endfor %} + +
ImprimanteStatutJobs en attenteActions
{{ p.label }} + + {% if p.state == 'idle' %}Disponible + {% elif p.state == 'printing' %}Impression + {% elif p.state == 'disabled' %}Désactivée + {% elif p.state == 'offline' %}Hors ligne + {% else %}{{ p.state }}{% endif %} + + {{ p.jobs }} + +
+
+
+ + +
+
Mode d'impression
+
+
+ + + +
+

+ Direct : impression lancée immédiatement sans confirmation. Validation : l'admin valide chaque impression. Galerie : les demandes s'accumulent, impression via la galerie admin. +

+
+
+ + +
+
+
File d'attente ({{ queue|length }})
+ +
+
+
+ {% if not queue %} +
Aucune impression en attente
+ {% else %} + + + + {% for q in queue %} + + + + + + + + + {% endfor %} + +
AperçuFichierCopiesStatutDemandéActions
{{ q.filename.split('/')[-1] }} + + + {{ q.status }} + {{ q.requested_at|int }} + {% if q.status == 'pending' %} + + + {% elif q.status == 'done' %} + {{ q.printer or '—' }} + {% endif %} +
+ {% endif %} +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/frontend/templates/base.html b/frontend/templates/base.html new file mode 100644 index 0000000..d02ecdf --- /dev/null +++ b/frontend/templates/base.html @@ -0,0 +1,94 @@ + + + + + + {% block title %}JH Photomaton{% endblock %} + + {% block head %}{% endblock %} + + + +{% block navbar %} + +{% endblock %} + +
+ {% block content %}{% endblock %} +
+ + +
+ + + +{% block scripts %}{% endblock %} + + diff --git a/frontend/templates/public/gallery.html b/frontend/templates/public/gallery.html new file mode 100644 index 0000000..3147abe --- /dev/null +++ b/frontend/templates/public/gallery.html @@ -0,0 +1,119 @@ +{% extends "base.html" %} +{% block title %}Galerie photos — Photomaton LSDW{% endblock %} + +{% block nav_links %} +📷 Galerie photos +{% endblock %} + +{% block content %} +
+
+

📷 Vos photos

+
+ Chargement… + +
+
+

+ Retrouvez vos photos ci-dessous. Scannez le QR code depuis la galerie du photomaton pour télécharger votre photo directement sur votre téléphone. +

+ + +
+ + Page 1 / 1 + +
+ +
+
Chargement…
+
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/main.py b/main.py new file mode 100644 index 0000000..83b7c16 --- /dev/null +++ b/main.py @@ -0,0 +1,190 @@ +""" +JH Photomaton — Interface de gestion +Raspberry Pi 4 / Les Sapins Du Web (LSDW) + +Remplace Node-RED pour : + - Contrôle bouton GPIO23 (multi-clic + long press) + relay GPIO12 + - LEDs WS2812b GPIO18 (35 LEDs) — effets selon état + - Galerie publique et galerie admin + - File d'attente d'impression (SQLite) + - Dashboard admin avec monitoring système + +Port : 8090 (configurable dans config/settings.yaml) +""" + +import asyncio +import logging +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.staticfiles import StaticFiles +from starlette.middleware.sessions import SessionMiddleware + +from backend.services.config_service import ConfigService +from backend.services.led_service import LEDService +from backend.services.button_service import ButtonService +from backend.services.photobooth_service import PhotoboothService +from backend.services.printer_service import PrinterService +from backend.services.system_service import SystemService +from backend.utils.ws_manager import WSManager +from backend.api import ( + gallery, + admin_api, + admin_gallery_api, + leds_api, + system_api, + print_api, + actions_api, + webhooks, +) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) +logger = logging.getLogger(__name__) + +BASE_DIR = Path(__file__).parent + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # ── Chargement config ─────────────────────────────────────────────────── + config_svc = ConfigService(BASE_DIR / "config" / "settings.yaml") + config = config_svc.load() + + # ── Services ──────────────────────────────────────────────────────────── + ws_manager = WSManager() + photobooth_svc = PhotoboothService(config) + system_svc = SystemService() + printer_svc = PrinterService(config) + + await printer_svc.init_db(BASE_DIR / "data" / "print_queue.db") + + # LEDs + led_svc = LEDService(config) + led_svc.start() + + # Bouton GPIO — passe le loop asyncio pour les callbacks + loop = asyncio.get_event_loop() + led_svc.set_loop(loop) + led_svc.set_on_change(ws_manager.broadcast) + + button_svc = ButtonService(config, led_svc, photobooth_svc, ws_manager, loop) + button_svc.start() + + # ── État partagé dans l'app ────────────────────────────────────────────── + app.state.config = config + app.state.config_service = config_svc + app.state.ws_manager = ws_manager + app.state.led_service = led_svc + app.state.button_service = button_svc + app.state.photobooth_service = photobooth_svc + app.state.printer_service = printer_svc + app.state.system_service = system_svc + + # ── Tâche de fond : broadcast stats système ────────────────────────────── + stats_task = asyncio.create_task(_system_stats_loop(system_svc, ws_manager)) + + logger.info("JH Photomaton démarré — port %d", config.app.port) + led_svc.play("idle") + + yield # ← l'app tourne ici + + # ── Arrêt propre ───────────────────────────────────────────────────────── + stats_task.cancel() + button_svc.stop() + led_svc.stop() + await photobooth_svc.close() + await printer_svc.close() + logger.info("JH Photomaton arrêté") + + +async def _system_stats_loop(sys_svc: SystemService, ws: WSManager): + """Broadcast des stats système toutes les 5 secondes.""" + while True: + try: + stats = sys_svc.get_stats() + await ws.broadcast({"type": "system_stats", "data": stats}) + except Exception as e: + logger.debug("Stats loop: %s", e) + await asyncio.sleep(5) + + +# ── Application FastAPI ────────────────────────────────────────────────────── + +app = FastAPI( + title="JH Photomaton", + description="Interface de gestion du photomaton — Les Sapins Du Web", + version="1.0.0", + lifespan=lifespan, +) + +app.add_middleware( + SessionMiddleware, + secret_key="jh-photomaton-session-2026", # remplacé par config au démarrage + max_age=86400, # 24h +) + +# Fichiers statiques +app.mount( + "/static", + StaticFiles(directory=BASE_DIR / "frontend" / "static"), + name="static", +) + +# Routes +app.include_router(gallery.router) +app.include_router(admin_api.router) +app.include_router(admin_gallery_api.router) +app.include_router(leds_api.router, prefix="/api") +app.include_router(system_api.router, prefix="/api") +app.include_router(print_api.router, prefix="/api") +app.include_router(actions_api.router, prefix="/api") +app.include_router(webhooks.router, prefix="/api") + + +# ── WebSocket ──────────────────────────────────────────────────────────────── + +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + ws_manager: WSManager = websocket.app.state.ws_manager + await ws_manager.connect(websocket) + + # Envoie l'état initial à la connexion + try: + led = websocket.app.state.led_service + sys_svc = websocket.app.state.system_service + await ws_manager.send(websocket, {"type": "connected", "led_effect": led.current_effect}) + await ws_manager.send(websocket, {"type": "system_stats", "data": sys_svc.get_stats()}) + except Exception: + pass + + try: + while True: + data = await websocket.receive_text() + # Commandes entrantes du client (optionnel) + import json + try: + msg = json.loads(data) + if msg.get("type") == "ping": + await ws_manager.send(websocket, {"type": "pong"}) + except Exception: + pass + except WebSocketDisconnect: + ws_manager.disconnect(websocket) + + +# ── Point d'entrée ─────────────────────────────────────────────────────────── + +if __name__ == "__main__": + import uvicorn + cfg = ConfigService(BASE_DIR / "config" / "settings.yaml").load() + uvicorn.run( + "main:app", + host=cfg.app.host, + port=cfg.app.port, + reload=cfg.app.debug, + log_level="info", + ) diff --git a/photobooth-app/config/config.json b/photobooth-app/config/config.json new file mode 100644 index 0000000..f5098ce --- /dev/null +++ b/photobooth-app/config/config.json @@ -0,0 +1,548 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "BAPT - normal", + "jobcontrol": { + "countdown_capture": 3.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "BAPT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": true + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260626-114238 b/photobooth-app/config/config.json_backup-20260626-114238 new file mode 100644 index 0000000..8be35b9 --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260626-114238 @@ -0,0 +1,480 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal (copie)", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/MT/frames/photoMT-jeannehuguette.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-174236 b/photobooth-app/config/config.json_backup-20260709-174236 new file mode 100644 index 0000000..8a0705d --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-174236 @@ -0,0 +1,480 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/MT/frames/photoMT-jeannehuguette.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-174328 b/photobooth-app/config/config.json_backup-20260709-174328 new file mode 100644 index 0000000..c70279a --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-174328 @@ -0,0 +1,514 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal (copie)", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/MT/frames/photoMT-jeannehuguette.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-174548 b/photobooth-app/config/config.json_backup-20260709-174548 new file mode 100644 index 0000000..aedd242 --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-174548 @@ -0,0 +1,514 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "CLEM - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/MT/frames/photoMT-jeannehuguette.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-174642 b/photobooth-app/config/config.json_backup-20260709-174642 new file mode 100644 index 0000000..4d15fdf --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-174642 @@ -0,0 +1,514 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "BAPT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/MT/frames/photoMT-jeannehuguette.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-174655 b/photobooth-app/config/config.json_backup-20260709-174655 new file mode 100644 index 0000000..376ca63 --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-174655 @@ -0,0 +1,548 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "BAPT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "BAPT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/MT/frames/photoMT-jeannehuguette.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-175626 b/photobooth-app/config/config.json_backup-20260709-175626 new file mode 100644 index 0000000..a5000bd --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-175626 @@ -0,0 +1,548 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "BAPT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "BAPT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-181113 b/photobooth-app/config/config.json_backup-20260709-181113 new file mode 100644 index 0000000..e543515 --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-181113 @@ -0,0 +1,548 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "BAPT - normal", + "jobcontrol": { + "countdown_capture": 3.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "BAPT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-183641 b/photobooth-app/config/config.json_backup-20260709-183641 new file mode 100644 index 0000000..e543515 --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-183641 @@ -0,0 +1,548 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "BAPT - normal", + "jobcontrol": { + "countdown_capture": 3.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "BAPT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/config.json_backup-20260709-183701 b/photobooth-app/config/config.json_backup-20260709-183701 new file mode 100644 index 0000000..b45aa82 --- /dev/null +++ b/photobooth-app/config/config.json_backup-20260709-183701 @@ -0,0 +1,548 @@ +{ + "common": { + "admin_password": "PhotoBooth2026!", + "logging_level": "WARNING", + "users_delete_to_recycle_dir": true + }, + "actions": { + "image": [ + { + "name": "BAPT - normal", + "jobcontrol": { + "countdown_capture": 3.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "BAPT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": null, + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "MT - etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/MT/frames/photoMT-jeannehuguette.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "HnB - normal", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": false, + "fill_background_enable": false, + "fill_background_color": "#ededed", + "img_background_enable": false, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo normal", + "icon": "Photo", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Etoile", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#ededed", + "img_background_enable": true, + "img_background_file": "userdata/demoassets/backgrounds/background.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - voie lactée", + "icon": "star_shine", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Cailloux", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/Wall-with-large-and-small-stones.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/hnb cadre final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Photo - Mur de pierre", + "icon": "brick", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Hnb - Light", + "jobcontrol": { + "countdown_capture": 5.0 + }, + "processing": { + "remove_background": true, + "fill_background_enable": true, + "fill_background_color": "#f2f0f0", + "img_background_enable": true, + "img_background_file": "userdata/hopnbloc/backgrounds/2149243965.jpg", + "image_filter": "original", + "img_frame_enable": true, + "img_frame_file": "userdata/hopnbloc/frames/calque photos final.png", + "texts_enable": false, + "texts": [] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "HnB - Soirée branchée", + "icon": "nightlife", + "use_custom_color": true, + "custom_color": "#016911" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ], + "collage": [ + { + "name": "default action", + "jobcontrol": { + "countdown_capture": 2.0, + "countdown_capture_second_following": 1.0, + "ask_approval_each_capture": true, + "approve_autoconfirm_timeout": 15.0, + "show_individual_captures_in_gallery": true + }, + "processing": { + "capture_remove_background": true, + "capture_fill_background_enable": true, + "capture_fill_background_color": "white", + "capture_img_background_enable": false, + "capture_img_background_file": null, + "canvas_width": 1920, + "canvas_height": 1280, + "merge_definition": [ + { + "description": "left", + "pos_x": 160, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.earlybird" + }, + { + "description": "middle predefined", + "pos_x": 705, + "pos_y": 66, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": "userdata/demoassets/predefined_images/photobooth-collage-predefined-image.png", + "image_filter": "original" + }, + { + "description": "right", + "pos_x": 1245, + "pos_y": 220, + "pos_z": 0, + "width": 510, + "height": 725, + "rotate": 0, + "predefined_image": null, + "image_filter": "FilterPilgram2.reyes" + } + ], + "canvas_fill_background_enable": false, + "canvas_fill_background_color": "#2e6f40", + "canvas_img_background_enable": false, + "canvas_img_background_file": null, + "canvas_img_front_enable": true, + "canvas_img_front_file": "userdata/demoassets/frames/pixabay-poster-2871536_1920.png", + "canvas_texts_enable": true, + "canvas_texts": [ + { + "text": "Have a nice day :)", + "pos_x": 200, + "pos_y": 1100, + "rotate": 1, + "font_size": 40, + "font": "userdata/demoassets/fonts/Roboto-Bold.ttf", + "color": "#333" + } + ] + }, + "trigger": { + "ui_trigger": { + "show_button": false, + "title": "Collage", + "icon": "auto_awesome_mosaic", + "use_custom_color": false, + "custom_color": "#196cb0" + }, + "keyboard_trigger": { + "keycode": "c" + }, + "gpio_trigger": { + "pin": "22", + "trigger_on": "pressed" + } + } + } + ], + "animation": [], + "video": [], + "multicamera": [] + }, + "share": { + "sharing_enabled": true, + "actions": [ + { + "name": "Demande d'impression", + "handles_images_only": true, + "processing": { + "share_command": "curl http://127.0.0.1:1880/api/photobooth-custom/ask_print?filename={filename}", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [], + "share_blocked_time": 1, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Demander l'impression ?", + "icon": "photo_prints", + "use_custom_color": true, + "custom_color": "#ab0a78" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + }, + { + "name": "Impression", + "handles_images_only": true, + "processing": { + "share_command": "/home/pi/photobooth-data/script/script_print.sh \"{filename}\" \"{media_type}\" \"{action_config_name}\" \"{copies}\"", + "ask_user_for_parameter_input": false, + "parameters_dialog_caption": "Make your choice!", + "parameters_dialog_action_icon": "print", + "parameters_dialog_action_label": "GO", + "parameters": [ + { + "key": "copies", + "label": "Copies", + "ui_type": "int", + "default": "1", + "valid_min": "1", + "valid_max": "2" + } + ], + "share_blocked_time": 120, + "max_shares": 0 + }, + "trigger": { + "ui_trigger": { + "show_button": true, + "title": "Direct Print", + "icon": "print", + "use_custom_color": true, + "custom_color": "#b01992" + }, + "keyboard_trigger": { + "keycode": "" + }, + "gpio_trigger": { + "pin": "", + "trigger_on": "pressed" + } + } + } + ] + }, + "qrshare": { + "enabled": false, + "shareservice_url": "https://photobooth-app.org/extras/shareservice-landing/", + "shareservice_apikey": "changedefault!", + "enabled_custom": true, + "share_custom_qr_url": "https://photomaton.lessapinsduweb.com/sharepage/#?url=https://photomaton.lessapinsduweb.com/media/full/{identifier}" + }, + "filetransfer": { + "enabled": false, + "target_folder_name": "photobooth" + }, + "mediaprocessing": { + "full_still_length": 4608, + "preview_still_length": 2304, + "thumbnail_still_length": 960, + "video_bitrate": 3000, + "video_compatibility_mode": true, + "remove_background_model": "modnet", + "fileformat_animations": "webp", + "fileformat_multicamera": "mp4" + }, + "uisettings": { + "PRIMARY_COLOR": "#196cb0", + "SECONDARY_COLOR": "#4283b8", + "theme": "system", + "show_gallery_on_frontpage": true, + "show_admin_on_frontpage": true, + "admin_button_invisible": true, + "show_frontpage_timeout": 5, + "enable_automatic_slideshow": true, + "show_automatic_slideshow_timeout": 120, + "enable_livestream_when_idle": true, + "enable_livestream_when_active": true, + "livestream_mirror_effect": false, + "livestream_blurredbackground": false, + "livestream_blurredbackground_high_framerate": false, + "enable_livestream_frameoverlay": true, + "livestream_frameoverlay_image": "userdata/BAPTEME/frame/cadre baptème elise et diane.png", + "livestream_frameoverlay_mirror_effect": false, + "FRONTPAGE_TEXT": "
Bienvenue !
Appuyez sur le bouton pour prendre une photo
Photomaton proposé par Les Sapins Du Web. En l'utilisant, vous acceptez que les photos puissent etre utilisées par Les Sapins Du Web et ses partenaires.
\"Les
", + "TAKEPIC_MSG_TIME": 0.5, + "TAKEPIC_MSG_TEXT": "😃", + "AUTOCLOSE_NEW_ITEM_ARRIVED": 30, + "GALLERY_EMPTY_MSG": "
Empty, Zero, Nada! 🤷‍♂️
Let's take some pictures!
📷💕
", + "gallery_show_qrcode": true, + "qrcode_text_above": "⬇️ Télécharger la photo!", + "qrcode_text_below": "Scannez le QR code ci-dessus pour télécharger cette photo. ( Vous devez être connecté au Wifi Photomaton)", + "qrcode_link_codes": true, + "gallery_show_filter": false, + "gallery_show_download": false, + "gallery_show_delete": false, + "gallery_show_shareprint": false + }, + "backends": { + "enable_livestream": true, + "retry_capture": 3, + "countdown_camera_capture_offset": 0.2, + "index_backend_stills": 0, + "index_backend_video": 0, + "index_backend_multicam": 0, + "group_backends": [ + { + "enabled": true, + "description": "pi camera module V3", + "backend_config": { + "orientation": "1: 0°", + "backend_type": "Picamera2", + "camera_num": 0, + "CAPTURE_CAM_RESOLUTION_WIDTH": 4608, + "CAPTURE_CAM_RESOLUTION_HEIGHT": 3072, + "PREVIEW_CAM_RESOLUTION_WIDTH": 2304, + "PREVIEW_CAM_RESOLUTION_HEIGHT": 1536, + "LIVEVIEW_RESOLUTION_WIDTH": 750, + "LIVEVIEW_RESOLUTION_HEIGHT": 500, + "framerate_still_mode": 20, + "framerate_video_mode": 25, + "frame_skip_count": 4, + "optimized_lowlight_short_exposure": false, + "videostream_quality": "MEDIUM", + "original_still_quality": 100 + } + } + ] + }, + "hardwareinputoutput": { + "keyboard_input_enabled": false, + "gpio_enabled": false, + "gpio_pin_shutdown": 17, + "gpio_pin_reboot": 18, + "gpio_pin_job_next": 27, + "gpio_pin_job_reject": 22, + "gpio_pin_job_abort": 20 + }, + "misc": { + "secret_key": "d4c9e1591c96a07af8088489244bdca0d91f447ba6c2c2c6aa7bf04433863d95", + "cmd_shutdown": "shutdown now", + "cmd_reboot": "reboot" + } +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json b/photobooth-app/config/plugin_commander.json new file mode 100644 index 0000000..e78d124 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json @@ -0,0 +1,88 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - images", + "event": [ + "counting", + "captured", + "capture", + "finished" + ], + "filter_mediaitem_types": [ + "image" + ], + "delay_before": 2.0, + "wait_until_completed": true, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + }, + { + "enabled": true, + "name": "NR - init", + "event": [ + "init", + "start", + "stop" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260427-120604 b/photobooth-app/config/plugin_commander.json_backup-20260427-120604 new file mode 100644 index 0000000..7210f82 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260427-120604 @@ -0,0 +1,59 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - end", + "event": [ + "finished", + "counting", + "capture_still", + "capture_video", + "captured", + "capture", + "stop", + "start", + "init", + "capture_multicam" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": true, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} diff --git a/photobooth-app/config/plugin_commander.json_backup-20260427-120614 b/photobooth-app/config/plugin_commander.json_backup-20260427-120614 new file mode 100644 index 0000000..e30df86 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260427-120614 @@ -0,0 +1,59 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - end", + "event": [ + "finished", + "counting", + "capture_still", + "capture_video", + "captured", + "capture", + "stop", + "start", + "init", + "capture_multicam" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-175723 b/photobooth-app/config/plugin_commander.json_backup-20260709-175723 new file mode 100644 index 0000000..44c3060 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-175723 @@ -0,0 +1,59 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - all", + "event": [ + "finished", + "counting", + "capture_still", + "capture_video", + "captured", + "capture", + "stop", + "start", + "init", + "capture_multicam" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-181059 b/photobooth-app/config/plugin_commander.json_backup-20260709-181059 new file mode 100644 index 0000000..24edb28 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-181059 @@ -0,0 +1,59 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - all", + "event": [ + "finished", + "counting", + "capture_still", + "capture_video", + "captured", + "capture", + "stop", + "start", + "init", + "capture_multicam" + ], + "filter_mediaitem_types": [], + "delay_before": 1.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-181516 b/photobooth-app/config/plugin_commander.json_backup-20260709-181516 new file mode 100644 index 0000000..24edb28 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-181516 @@ -0,0 +1,59 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - all", + "event": [ + "finished", + "counting", + "capture_still", + "capture_video", + "captured", + "capture", + "stop", + "start", + "init", + "capture_multicam" + ], + "filter_mediaitem_types": [], + "delay_before": 1.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-181521 b/photobooth-app/config/plugin_commander.json_backup-20260709-181521 new file mode 100644 index 0000000..e5e5f41 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-181521 @@ -0,0 +1,59 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - all", + "event": [ + "finished", + "counting", + "capture_still", + "capture_video", + "captured", + "capture", + "stop", + "start", + "init", + "capture_multicam" + ], + "filter_mediaitem_types": [], + "delay_before": 1.0, + "wait_until_completed": true, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-182004 b/photobooth-app/config/plugin_commander.json_backup-20260709-182004 new file mode 100644 index 0000000..e5e5f41 --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-182004 @@ -0,0 +1,59 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - all", + "event": [ + "finished", + "counting", + "capture_still", + "capture_video", + "captured", + "capture", + "stop", + "start", + "init", + "capture_multicam" + ], + "filter_mediaitem_types": [], + "delay_before": 1.0, + "wait_until_completed": true, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-182052 b/photobooth-app/config/plugin_commander.json_backup-20260709-182052 new file mode 100644 index 0000000..4c392ce --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-182052 @@ -0,0 +1,55 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - images", + "event": [ + "counting", + "captured", + "capture", + "finished" + ], + "filter_mediaitem_types": [ + "image" + ], + "delay_before": 1.0, + "wait_until_completed": true, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-182115 b/photobooth-app/config/plugin_commander.json_backup-20260709-182115 new file mode 100644 index 0000000..92d015c --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-182115 @@ -0,0 +1,88 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - images", + "event": [ + "counting", + "captured", + "capture", + "finished" + ], + "filter_mediaitem_types": [ + "image" + ], + "delay_before": 1.0, + "wait_until_completed": true, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + }, + { + "enabled": true, + "name": "NR - init", + "event": [ + "init", + "start", + "stop" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander.json_backup-20260709-182616 b/photobooth-app/config/plugin_commander.json_backup-20260709-182616 new file mode 100644 index 0000000..92d015c --- /dev/null +++ b/photobooth-app/config/plugin_commander.json_backup-20260709-182616 @@ -0,0 +1,88 @@ +{ + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "NR - images", + "event": [ + "counting", + "captured", + "capture", + "finished" + ], + "filter_mediaitem_types": [ + "image" + ], + "delay_before": 1.0, + "wait_until_completed": true, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + }, + { + "enabled": true, + "name": "NR - init", + "event": [ + "init", + "start", + "stop" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:1880/api/photobooth/", + "method": "get", + "body_parameters_as_json": true, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + }, + { + "key": "demoparameter", + "value": "demoparameter", + "where": "query" + } + ] + } + ], + "tasks_commands": [ + { + "enabled": false, + "name": "demo command", + "event": [ + "finished" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "command": "echo this echoed on event {event} for mediaitem_type {mediaitem_type}!" + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_commander_jh.json b/photobooth-app/config/plugin_commander_jh.json new file mode 100644 index 0000000..0f4422e --- /dev/null +++ b/photobooth-app/config/plugin_commander_jh.json @@ -0,0 +1,67 @@ +{ + "_comment": "Config plugin_commander pour JH Photomaton — remplace les appels vers Node-RED (:1880) par notre app (:8090)", + "_instructions": "Copier ce fichier vers ~/.config/photobooth-app/plugin_commander.json sur le Pi, puis redémarrer photobooth-app", + "enable_tasks_processing": true, + "tasks_httprequests": [ + { + "enabled": true, + "name": "JH - events images", + "event": [ + "counting", + "captured", + "capture", + "finished" + ], + "filter_mediaitem_types": [ + "image" + ], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 5, + "url": "http://127.0.0.1:8090/api/webhook/photobooth", + "method": "get", + "body_parameters_as_json": false, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "{mediaitem_type}", + "where": "query" + } + ] + }, + { + "enabled": true, + "name": "JH - init/stop", + "event": [ + "init", + "start", + "stop" + ], + "filter_mediaitem_types": [], + "delay_before": 0.0, + "wait_until_completed": false, + "timeout": 3, + "url": "http://127.0.0.1:8090/api/webhook/photobooth", + "method": "get", + "body_parameters_as_json": false, + "parameter": [ + { + "key": "event_key", + "value": "{event}", + "where": "query" + }, + { + "key": "mediaitem_type", + "value": "", + "where": "query" + } + ] + } + ], + "tasks_commands": [] +} diff --git a/photobooth-app/config/plugin_filter_pilgram2.json b/photobooth-app/config/plugin_filter_pilgram2.json new file mode 100644 index 0000000..493c87f --- /dev/null +++ b/photobooth-app/config/plugin_filter_pilgram2.json @@ -0,0 +1,45 @@ +{ + "add_userselectable_filter": false, + "userselectable_filter": [ + "_1977", + "aden", + "amaro", + "ashby", + "brannan", + "brooklyn", + "charmes", + "clarendon", + "crema", + "dogpatch", + "earlybird", + "gingham", + "ginza", + "hefe", + "helena", + "hudson", + "inkwell", + "juno", + "kelvin", + "lark", + "lofi", + "ludwig", + "maven", + "mayfair", + "moon", + "nashville", + "perpetua", + "poprocket", + "reyes", + "rise", + "sierra", + "skyline", + "slumber", + "stinson", + "sutro", + "toaster", + "valencia", + "walden", + "willow", + "xpro2" + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_filter_pilgram2.json_backup-20260427-120540 b/photobooth-app/config/plugin_filter_pilgram2.json_backup-20260427-120540 new file mode 100644 index 0000000..1893a94 --- /dev/null +++ b/photobooth-app/config/plugin_filter_pilgram2.json_backup-20260427-120540 @@ -0,0 +1,45 @@ +{ + "add_userselectable_filter": true, + "userselectable_filter": [ + "_1977", + "aden", + "amaro", + "ashby", + "brannan", + "brooklyn", + "charmes", + "clarendon", + "crema", + "dogpatch", + "earlybird", + "gingham", + "ginza", + "hefe", + "helena", + "hudson", + "inkwell", + "juno", + "kelvin", + "lark", + "lofi", + "ludwig", + "maven", + "mayfair", + "moon", + "nashville", + "perpetua", + "poprocket", + "reyes", + "rise", + "sierra", + "skyline", + "slumber", + "stinson", + "sutro", + "toaster", + "valencia", + "walden", + "willow", + "xpro2" + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_gpiolights.json b/photobooth-app/config/plugin_gpiolights.json new file mode 100644 index 0000000..251238d --- /dev/null +++ b/photobooth-app/config/plugin_gpiolights.json @@ -0,0 +1,15 @@ +{ + "enabled": false, + "gpio_lights": [ + { + "enable": true, + "description": "main light", + "gpio_pin": 2, + "active_high": false, + "events": [ + "on@countdown_start", + "off@after_capture" + ] + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_synchronizer.json b/photobooth-app/config/plugin_synchronizer.json new file mode 100644 index 0000000..4f9b381 --- /dev/null +++ b/photobooth-app/config/plugin_synchronizer.json @@ -0,0 +1,27 @@ +{ + "common": { + "enabled": false, + "enable_share_links": false + }, + "backends": [ + { + "enabled": false, + "description": "demo tmp sync", + "enable_regular_sync": true, + "enable_immediate_sync": true, + "enable_share_link": true, + "backend_config": { + "backend_type": "filesystem", + "connector": { + "target_dir": "tmp" + }, + "share": { + "use_downloadportal": false, + "downloadportal_url": null, + "downloadportal_autoupload": false, + "media_url": null + } + } + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_synchronizer.json_backup-20260427-120639 b/photobooth-app/config/plugin_synchronizer.json_backup-20260427-120639 new file mode 100644 index 0000000..25fb4fa --- /dev/null +++ b/photobooth-app/config/plugin_synchronizer.json_backup-20260427-120639 @@ -0,0 +1,27 @@ +{ + "common": { + "enabled": false, + "enable_share_links": true + }, + "backends": [ + { + "enabled": false, + "description": "demo tmp sync", + "enable_regular_sync": true, + "enable_immediate_sync": true, + "enable_share_link": true, + "backend_config": { + "backend_type": "filesystem", + "connector": { + "target_dir": "tmp" + }, + "share": { + "use_downloadportal": false, + "downloadportal_url": null, + "downloadportal_autoupload": false, + "media_url": null + } + } + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_synchronizer_rclone.json b/photobooth-app/config/plugin_synchronizer_rclone.json new file mode 100644 index 0000000..f1e34e2 --- /dev/null +++ b/photobooth-app/config/plugin_synchronizer_rclone.json @@ -0,0 +1,33 @@ +{ + "common": { + "enabled": false, + "full_sync_interval": 5, + "enabled_share_links": true, + "enabled_custom_qr_url": false, + "custom_qr_url": "http://PiPhotobooth:8000/sharepage/#?url=http://PiPhotobooth:8000/media/full/{identifier}" + }, + "rclone_client_config": { + "rclone_enable_logging": true, + "rclone_log_level": "NOTICE", + "rclone_transfers": 4, + "rclone_checkers": 4, + "enable_webui": true + }, + "remotes": [ + { + "enabled": false, + "description": "demo localremote", + "name": "/", + "subdir": "tmp/localsync", + "enable_immediate_sync": true, + "enable_regular_sync": true, + "enable_sharepage_sync": true, + "shareconfig": { + "enabled": false, + "manual_public_link": null, + "use_sharepage": true, + "sharepage_url": null + } + } + ] +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_wled.json b/photobooth-app/config/plugin_wled.json new file mode 100644 index 0000000..ee5612b --- /dev/null +++ b/photobooth-app/config/plugin_wled.json @@ -0,0 +1,4 @@ +{ + "wled_enabled": false, + "wled_serial_port": "/dev/ttyS0" +} \ No newline at end of file diff --git a/photobooth-app/config/plugin_wled.json_backup-20260412-154117 b/photobooth-app/config/plugin_wled.json_backup-20260412-154117 new file mode 100644 index 0000000..ee5612b --- /dev/null +++ b/photobooth-app/config/plugin_wled.json_backup-20260412-154117 @@ -0,0 +1,4 @@ +{ + "wled_enabled": false, + "wled_serial_port": "/dev/ttyS0" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..989b22a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +fastapi>=0.110.0 +uvicorn[standard]>=0.27.0 +jinja2>=3.1.0 +python-multipart>=0.0.9 +itsdangerous>=2.1.0 +httpx>=0.27.0 +psutil>=5.9.0 +aiosqlite>=0.20.0 +pyyaml>=6.0.0 +aiofiles>=23.2.1 +pillow>=10.0.0 +qrcode[pil]>=7.4.0 + +# Pi-specific — installés séparément via install.sh +# rpi-ws281x>=5.0.0 +# gpiozero>=2.0 +# RPi.GPIO>=0.7.1 diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..f31ac3f --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# ============================================================================= +# JH Photomaton — Script d'installation +# Raspberry Pi 4 / Raspberry Pi OS (Debian Trixie) +# ============================================================================= +set -euo pipefail + +INSTALL_DIR="/home/pi/jh-photomaton" +SERVICE_USER="root" # rpi_ws281x nécessite /dev/mem (root) +VENV_DIR="$INSTALL_DIR/.venv" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " JH Photomaton — Installation" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# ── Vérifications ───────────────────────────────────────────────────────────── +if [[ $EUID -ne 0 ]]; then + echo "❌ Ce script doit être exécuté en root : sudo bash install.sh" + exit 1 +fi + +# ── Dépendances système ─────────────────────────────────────────────────────── +echo "" +echo "▶ Installation des dépendances système..." +apt-get update -qq +apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv python3-dev \ + python3-gpiozero python3-pigpio \ + imagemagick \ + cups cups-client \ + git curl + +# ── rpi_ws281x : dépendances build ─────────────────────────────────────────── +echo "" +echo "▶ Installation de rpi_ws281x (LEDs WS2812)..." +apt-get install -y --no-install-recommends \ + gcc make build-essential \ + scons swig + +# ── Désactiver audio si conflit GPIO18 ─────────────────────────────────────── +# WS2812 sur GPIO18 (PWM) entre en conflit avec le son +if ! grep -q "dtparam=audio=off" /boot/firmware/config.txt 2>/dev/null; then + echo "dtparam=audio=off" >> /boot/firmware/config.txt + echo "ℹ️ Audio désactivé dans /boot/firmware/config.txt (conflit GPIO18)" +fi + +# ── Copie des fichiers ──────────────────────────────────────────────────────── +echo "" +echo "▶ Copie des fichiers vers $INSTALL_DIR..." +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +mkdir -p "$INSTALL_DIR" +rsync -a --exclude='.venv' --exclude='__pycache__' --exclude='*.pyc' \ + --exclude='data/*.db' \ + "$PROJECT_DIR/" "$INSTALL_DIR/" + +mkdir -p "$INSTALL_DIR/data" + +# ── Environnement virtuel Python ────────────────────────────────────────────── +echo "" +echo "▶ Création de l'environnement virtuel Python..." +python3 -m venv "$VENV_DIR" --system-site-packages + +"$VENV_DIR/bin/pip" install --upgrade pip -q +"$VENV_DIR/bin/pip" install -r "$INSTALL_DIR/requirements.txt" -q + +# rpi_ws281x (nécessite les headers, installé depuis PyPI) +"$VENV_DIR/bin/pip" install rpi-ws281x -q || echo "⚠️ rpi_ws281x non installé (mode mock actif)" + +echo "" +echo "▶ Dépendances Python installées." + +# ── Permissions /dev/mem pour rpi_ws281x ───────────────────────────────────── +# Option : ajouter une règle udev pour éviter de tourner en root +# Ici on tourne en root via systemd pour simplifier + +# ── Service systemd ─────────────────────────────────────────────────────────── +echo "" +echo "▶ Installation du service systemd..." +cp "$INSTALL_DIR/systemd/jh-photomaton.service" /etc/systemd/system/ +systemctl daemon-reload +systemctl enable jh-photomaton.service + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " ✅ Installation terminée !" +echo "" +echo " Fichiers : $INSTALL_DIR" +echo " Config : $INSTALL_DIR/config/settings.yaml" +echo "" +echo " ▶ Démarrer le service :" +echo " sudo systemctl start jh-photomaton" +echo "" +echo " ▶ Voir les logs :" +echo " sudo journalctl -u jh-photomaton -f" +echo "" +echo " ▶ Interface admin :" +echo " http://:8090/admin" +echo " Mot de passe : PhotoBooth2026!" +echo "" +echo " ⚠️ Penser à mettre à jour plugin_commander.json" +echo " dans photobooth-app pour pointer vers notre webhook !" +echo " Voir : config/plugin_commander_jh.json" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..2a72e9f --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Démarrage de JH Photomaton +INSTALL_DIR="/home/pi/jh-photomaton" +VENV="$INSTALL_DIR/.venv/bin/python" + +cd "$INSTALL_DIR" +exec "$VENV" main.py diff --git a/scripts/sudoers-jh-photomaton b/scripts/sudoers-jh-photomaton new file mode 100644 index 0000000..ec5f522 --- /dev/null +++ b/scripts/sudoers-jh-photomaton @@ -0,0 +1,17 @@ +# ============================================================================= +# Règle sudoers — JH Photomaton +# +# INSTALLATION : +# sudo cp scripts/sudoers-jh-photomaton /etc/sudoers.d/jh-photomaton +# sudo chmod 440 /etc/sudoers.d/jh-photomaton +# sudo visudo -c # vérification syntaxe +# +# Permet à l'utilisateur 'pi' de redémarrer/vérifier le service jh-photomaton +# sans saisir de mot de passe sudo — requis pour le déploiement CI/CD. +# ============================================================================= + +pi ALL=(ALL) NOPASSWD: /bin/systemctl restart jh-photomaton, \ + /bin/systemctl start jh-photomaton, \ + /bin/systemctl stop jh-photomaton, \ + /bin/systemctl status jh-photomaton, \ + /bin/systemctl reload jh-photomaton diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100644 index 0000000..07df35e --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# ============================================================================= +# JH Photomaton — Script de mise à jour +# Appelé par le CI/CD Gitea Actions (deploy.yml) +# Peut aussi être exécuté manuellement : bash scripts/update.sh +# ============================================================================= +set -euo pipefail + +INSTALL_DIR="/home/pi/jh-photomaton" +VENV="$INSTALL_DIR/.venv/bin/pip" + +echo "▶ Mise à jour du code source..." +cd "$INSTALL_DIR" + +# Sauvegarder les modifications locales (ex: settings.yaml personnalisé) +# git stash ne fail pas si rien à stasher +git stash push --include-untracked --message "auto-stash avant deploy $(date +%Y%m%d-%H%M%S)" 2>/dev/null || true + +# Pull depuis origin/main +git pull origin main + +# Réappliquer les modifs locales (settings.yaml, etc.) +# Si conflit → les fichiers locaux prennent le dessus +git stash pop 2>/dev/null || true + +echo "▶ Mise à jour des dépendances Python..." +"$VENV" install -r "$INSTALL_DIR/requirements.txt" -q + +echo "▶ Redémarrage du service..." +sudo /bin/systemctl restart jh-photomaton + +# Attendre que le service soit UP +sleep 2 +sudo /bin/systemctl status jh-photomaton --no-pager -l + +echo "" +echo "✅ JH Photomaton mis à jour et redémarré." +echo " Logs : sudo journalctl -u jh-photomaton -f" diff --git a/systemd/jh-photomaton.service b/systemd/jh-photomaton.service new file mode 100644 index 0000000..e1db1ae --- /dev/null +++ b/systemd/jh-photomaton.service @@ -0,0 +1,28 @@ +[Unit] +Description=JH Photomaton — Interface de gestion (LSDW) +Documentation=https://github.com/your-repo/jh-photomaton +After=network.target photobooth-app.service +Wants=photobooth-app.service + +[Service] +Type=simple +User=root +WorkingDirectory=/home/pi/jh-photomaton +ExecStart=/home/pi/jh-photomaton/.venv/bin/python main.py +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=jh-photomaton + +# Donne accès à /dev/mem pour rpi_ws281x (LEDs) +# et /dev/gpiomem pour GPIO +PrivateDevices=no +SupplementaryGroups=gpio + +# Limite RAM pour protéger le Pi 2Go +MemoryMax=200M +MemorySwapMax=0 + +[Install] +WantedBy=multi-user.target