first commit
This commit is contained in:
@@ -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 !"
|
||||
+42
@@ -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
|
||||
@@ -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 !
|
||||
#
|
||||
# =============================================================================
|
||||
@@ -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" }
|
||||
]
|
||||
@@ -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": "<div> <div class=\"fixed-right text-white text-right\" style=\"margin-top: 45px; text-shadow: 4px 4px 4px #666;\"> <div class=\"text-h5\"> \tSi vous utilisez le photomaton, vous nous autorisez à utiliser les photos. <br> \tCe photomaton est proposé en libre-service par 'Les Sapins Du Web'. <br> \tLes photos peuvent être imprimées sur demande et sur dons libre. (2 Photos maximum par personne) <br> \tMerci de votre compréhension. \t</div> <div class=\"text-h3 text-weight-bold\">Compét Hop'N Bloc !!!!!</div> </div> <div class=\"q-page-sticky row flex-center fixed-bottom-right q-page-sticky--shrink q-mb-lg q-mr-lg\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"LSDW icon\" style=\"width:160px;height:160px;\"> </div> </div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-right text-white text-right\" style=\"margin-top: 45px; text-shadow: 4px 4px 4px #666;\"> <div class=\"text-h5\"> \tSi vous utilisez le photomaton, vous nous autorisez \u00e0 utiliser les photos. <br> \tCe photomaton est propos\u00e9 en libre-service par 'Les Sapins Du Web'. <br> \tLes photos peuvent \u00eatre imprim\u00e9es sur demande et sur dons libre. (2 Photos maximum par personne) <br> \tMerci de votre compr\u00e9hension. \t</div> <div class=\"text-h3 text-weight-bold\">Comp\u00e9t Hop'N Bloc !!!!!</div> </div> <div class=\"q-page-sticky row flex-center fixed-bottom-right q-page-sticky--shrink q-mb-lg q-mr-lg\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"LSDW icon\" style=\"width:160px;height:160px;\"> </div> </div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "\ud83d\ude03",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Pas encore de photos ! \ud83e\udd37\u200d\u2642\ufe0f<br>Appuyez sur le bouton ! <br>\ud83d\udcf7\ud83d\udc95</div>",
|
||||
"_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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
#
|
||||
# =============================================================================
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"])
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 ""
|
||||
@@ -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 ""
|
||||
@@ -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='<div> <div class="fixed-top-right q-mt-lg q-mr-lg" style="pointer-events: none; max-width: 400px;"> <div class="q-pa-md" style="background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;"> <div class="text-h3 text-weight-bold" style="color: #335e7e;">Bienvenue !</div> <div style="font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;"> <div>Appui court = Photo</div> <div>Appui long (3s) = Demande d'\''impression</div> </div> </div> </div> <div class="fixed-bottom-left q-mb-sm q-ml-lg" style="pointer-events: none;"> <div class="q-pa-sm" style="background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;"> <span style="font-size: 0.85rem; color: #335e7e;"> 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. </span> </div> </div> <div class="fixed-bottom-right q-mb-lg q-mr-lg" style="pointer-events: none;"> <img src="/userdata/LSDW/logo/logo.jpeg" alt="Les Sapins Du Web" style="width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);"> </div></div>'
|
||||
|
||||
FRONTPAGE_DIRECT='<div> <div class="fixed-top-right q-mt-lg q-mr-lg" style="pointer-events: none; max-width: 400px;"> <div class="q-pa-md" style="background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;"> <div class="text-h3 text-weight-bold" style="color: #335e7e;">Bienvenue !</div> <div style="font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;"> <div>Appui court = Photo</div> <div>Appui long (3s) = Impression</div> </div> </div> </div> <div class="fixed-bottom-left q-mb-sm q-ml-lg" style="pointer-events: none;"> <div class="q-pa-sm" style="background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;"> <span style="font-size: 0.85rem; color: #335e7e;"> 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. </span> </div> </div> <div class="fixed-bottom-right q-mb-lg q-mr-lg" style="pointer-events: none;"> <img src="/userdata/LSDW/logo/logo.jpeg" alt="Les Sapins Du Web" style="width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);"> </div></div>'
|
||||
|
||||
FRONTPAGE_NOPRINT='<div> <div class="fixed-top-right q-mt-lg q-mr-lg" style="pointer-events: none; max-width: 400px;"> <div class="q-pa-md" style="background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;"> <div class="text-h3 text-weight-bold" style="color: #335e7e;">Bienvenue !</div> <div style="font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class="fixed-bottom-left q-mb-sm q-ml-lg" style="pointer-events: none;"> <div class="q-pa-sm" style="background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;"> <span style="font-size: 0.85rem; color: #335e7e;"> 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. </span> </div> </div> <div class="fixed-bottom-right q-mb-lg q-mr-lg" style="pointer-events: none;"> <img src="/userdata/LSDW/logo/logo.jpeg" alt="Les Sapins Du Web" style="width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);"> </div></div>'
|
||||
|
||||
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 <commande> [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 <nom> Sauvegarder les actions actuelles
|
||||
Ex: actions backup hopnbloc
|
||||
actions restore <nom> Restaurer un backup d'actions
|
||||
Ex: actions restore hopnbloc
|
||||
Note: necessite un restart apres
|
||||
actions backups Lister les backups disponibles
|
||||
actions show <nom> Voir le contenu d'un backup
|
||||
actions move <de> <vers> 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 <index> Rendre le bouton visible sur l'ecran tactile
|
||||
actions disable <index> Cacher le bouton de l'ecran tactile
|
||||
actions dup <index> Dupliquer une action (copie inseree apres)
|
||||
actions remove <i> [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 <nom>"
|
||||
echo " Exemple: $0 actions backup hopnbloc"
|
||||
exit 1
|
||||
fi
|
||||
actions_backup "$3"
|
||||
;;
|
||||
restore|load)
|
||||
if [ -z "${3:-}" ]; then
|
||||
echo "Usage: $0 actions restore <nom>"
|
||||
actions_list_backups
|
||||
exit 1
|
||||
fi
|
||||
actions_restore "$3"
|
||||
;;
|
||||
backups) actions_list_backups ;;
|
||||
show)
|
||||
if [ -z "${3:-}" ]; then
|
||||
echo "Usage: $0 actions show <nom>"
|
||||
actions_list_backups
|
||||
exit 1
|
||||
fi
|
||||
actions_show "$3"
|
||||
;;
|
||||
move|mv)
|
||||
if [ -z "${3:-}" ] || [ -z "${4:-}" ]; then
|
||||
echo "Usage: $0 actions move <from> <to>"
|
||||
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 <index>"
|
||||
actions_list
|
||||
exit 1
|
||||
fi
|
||||
actions_enable "$3"
|
||||
;;
|
||||
disable|off)
|
||||
if [ -z "${3:-}" ]; then
|
||||
echo "Usage: $0 actions disable <index>"
|
||||
actions_list
|
||||
exit 1
|
||||
fi
|
||||
actions_disable "$3"
|
||||
;;
|
||||
dup|duplicate|copy)
|
||||
if [ -z "${3:-}" ]; then
|
||||
echo "Usage: $0 actions dup <index>"
|
||||
actions_list
|
||||
exit 1
|
||||
fi
|
||||
actions_duplicate "$3"
|
||||
;;
|
||||
remove|rm|del|delete)
|
||||
if [ -z "${3:-}" ]; then
|
||||
echo "Usage: $0 actions remove <index> [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
|
||||
@@ -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
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# script_print.sh - Impression photo pour photomaton
|
||||
# =============================================================================
|
||||
# Usage: script_print.sh "<filename>" "<media_type>" "<action_config_name>" "<copies>"
|
||||
# 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:<printer_name>:<copies> en cas de succes
|
||||
# PRINT_ERROR:<printer_name> 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
|
||||
@@ -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 ""
|
||||
@@ -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
|
||||
<Location />
|
||||
Order allow,deny
|
||||
Allow localhost
|
||||
Allow 10.0.0.*
|
||||
Allow 192.168.*
|
||||
</Location>
|
||||
|
||||
<Location /admin>
|
||||
Order allow,deny
|
||||
Allow localhost
|
||||
</Location>
|
||||
|
||||
<Location /admin/conf>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order allow,deny
|
||||
Allow localhost
|
||||
</Location>
|
||||
|
||||
<Policy default>
|
||||
JobPrivateAccess default
|
||||
JobPrivateValues default
|
||||
SubscriptionPrivateAccess default
|
||||
SubscriptionPrivateValues default
|
||||
|
||||
<Limit Create-Job Print-Job Print-URI Validate-Job>
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job Cancel-My-Jobs Close-Job CUPS-Move-Job CUPS-Get-Document>
|
||||
Require user @OWNER @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default CUPS-Get-Devices>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit Pause-Printer Resume-Printer Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After Cancel-Jobs CUPS-Accept-Jobs CUPS-Reject-Jobs>
|
||||
AuthType Default
|
||||
Require user @SYSTEM
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
|
||||
<Limit All>
|
||||
Order deny,allow
|
||||
</Limit>
|
||||
</Policy>
|
||||
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 ""
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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
|
||||
@@ -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("<h1>Galerie désactivée</h1>", 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", ""))))
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 }
|
||||
@@ -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` |
|
||||
Binary file not shown.
@@ -0,0 +1,105 @@
|
||||
# Migration Node-RED → JH Photomaton
|
||||
|
||||
## Ce que remplace JH Photomaton
|
||||
|
||||
| Fonctionnalité | Node-RED (avant) | JH Photomaton (après) |
|
||||
|---|---|---|
|
||||
| Bouton GPIO23 multi-clic | `button-events` node | `ButtonService` (gpiozero) |
|
||||
| Relay GPIO12 | Nœud GPIO | `ButtonService.relay_on/off()` |
|
||||
| NeoPixels GPIO18 | `rpi-neopixels` node | `LEDService` (rpi_ws281x) |
|
||||
| Webhooks photobooth-app | `/api/photobooth/` | `/api/webhook/photobooth` |
|
||||
| File d'attente impression | SQLite Node-RED | SQLite via `PrinterService` |
|
||||
| Dashboard admin | Node-RED Dashboard | `/admin` — FastAPI + HTML |
|
||||
| RAM consommée | ~150–300 Mo | ~30–80 Mo (objectif) |
|
||||
|
||||
## Étapes de migration
|
||||
|
||||
### 1. Installer JH Photomaton
|
||||
|
||||
```bash
|
||||
cd /home/pi/jh-photomaton
|
||||
sudo bash scripts/install.sh
|
||||
```
|
||||
|
||||
### 2. Mettre à jour plugin_commander.json
|
||||
|
||||
Remplacer `~/.config/photobooth-app/plugin_commander.json` par notre version :
|
||||
|
||||
```bash
|
||||
cp /home/pi/jh-photomaton/photobooth-app/config/plugin_commander_jh.json \
|
||||
~/.config/photobooth-app/plugin_commander.json
|
||||
```
|
||||
|
||||
Redémarrer photobooth-app :
|
||||
```bash
|
||||
sudo systemctl restart photobooth-app
|
||||
```
|
||||
|
||||
### 3. Mettre à jour le share_command "Demande d'impression"
|
||||
|
||||
Dans `~/.config/photobooth-app/config.json`, modifier l'action de partage :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Demande d'impression",
|
||||
"processing": {
|
||||
"share_command": "curl 'http://127.0.0.1:8090/api/print/request?filename={filename}'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ou utiliser la page `/admin/actions` de JH Photomaton pour éditer directement.
|
||||
|
||||
### 4. Désactiver Node-RED
|
||||
|
||||
```bash
|
||||
sudo systemctl stop nodered
|
||||
sudo systemctl disable nodered
|
||||
```
|
||||
|
||||
Gain RAM : ~150-300 Mo libérés.
|
||||
|
||||
### 5. Mettre à jour Zoraxy (optionnel)
|
||||
|
||||
Changer le sous-domaine `photomaton-nodered.lessapinsduweb.com` pour pointer vers `:8090` au lieu de `:1880`.
|
||||
|
||||
### 6. Démarrer JH Photomaton
|
||||
|
||||
```bash
|
||||
sudo systemctl start jh-photomaton
|
||||
sudo systemctl status jh-photomaton
|
||||
```
|
||||
|
||||
Accès admin : `http://photomaton-nodered.lessapinsduweb.com/admin`
|
||||
(ou `http://10.3.141.1:8090/admin` depuis le WiFi Photomaton)
|
||||
|
||||
## Vérification
|
||||
|
||||
```bash
|
||||
# Logs
|
||||
sudo journalctl -u jh-photomaton -f
|
||||
|
||||
# Test bouton (simulation)
|
||||
curl -X POST http://localhost:8090/api/system/button/simulate?clicks=1
|
||||
|
||||
# Test LED
|
||||
curl -X POST http://localhost:8090/api/leds/effect?effect=countdown
|
||||
|
||||
# Test webhook
|
||||
curl "http://localhost:8090/api/webhook/photobooth?event_key=counting&mediaitem_type=image"
|
||||
|
||||
# Statut système
|
||||
curl http://localhost:8090/api/system/stats
|
||||
```
|
||||
|
||||
## Rollback (retour Node-RED)
|
||||
|
||||
```bash
|
||||
sudo systemctl stop jh-photomaton
|
||||
sudo systemctl disable jh-photomaton
|
||||
sudo systemctl start nodered
|
||||
sudo systemctl enable nodered
|
||||
# Restaurer plugin_commander.json original
|
||||
cp ~/.config/photobooth-app/plugin_commander.json_backup-* ~/.config/photobooth-app/plugin_commander.json
|
||||
sudo systemctl restart photobooth-app
|
||||
```
|
||||
@@ -0,0 +1,491 @@
|
||||
/* JH Photomaton — Styles principaux */
|
||||
/* Thème sombre, responsive, sans dépendances externes */
|
||||
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--surface2: #232736;
|
||||
--border: #2e3347;
|
||||
--primary: #196cb0;
|
||||
--primary-light: #2d8fd8;
|
||||
--accent: #4283b8;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--text: #e2e8f0;
|
||||
--text-muted: #94a3b8;
|
||||
--text-dim: #64748b;
|
||||
--led-green: #22c55e;
|
||||
--led-blue: #3b82f6;
|
||||
--led-purple: #a855f7;
|
||||
--led-white: #ffffff;
|
||||
--radius: 10px;
|
||||
--radius-sm: 6px;
|
||||
--shadow: 0 4px 20px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html, body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a { color: var(--primary-light); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Layout ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
.navbar {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar-brand span { color: var(--primary-light); }
|
||||
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-links a {
|
||||
color: var(--text-muted);
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.15s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.navbar-links a:hover,
|
||||
.navbar-links a.active {
|
||||
background: var(--surface2);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Cards ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card-sub {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Grid layouts ────────────────────────────────────────────────────────── */
|
||||
|
||||
.grid-4 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ── Progress bars ───────────────────────────────────────────────────────── */
|
||||
|
||||
.progress {
|
||||
height: 6px;
|
||||
background: var(--surface2);
|
||||
border-radius: 3px;
|
||||
margin-top: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.progress-bar.warn { background: var(--warning); }
|
||||
.progress-bar.danger { background: var(--error); }
|
||||
|
||||
/* ── Badges & Tags ───────────────────────────────────────────────────────── */
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-success { background: rgba(34,197,94,0.15); color: var(--success); }
|
||||
.badge-warning { background: rgba(245,158,11,0.15); color: var(--warning); }
|
||||
.badge-error { background: rgba(239,68,68,0.15); color: var(--error); }
|
||||
.badge-info { background: rgba(25,108,176,0.15); color: var(--primary-light); }
|
||||
.badge-muted { background: var(--surface2); color: var(--text-muted); }
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-primary { background: var(--primary); color: #fff; }
|
||||
.btn-primary:hover { background: var(--primary-light); }
|
||||
.btn-success { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
|
||||
.btn-success:hover { background: rgba(34,197,94,0.25); }
|
||||
.btn-danger { background: rgba(239,68,68,0.15); color: var(--error); border: 1px solid rgba(239,68,68,0.3); }
|
||||
.btn-danger:hover { background: rgba(239,68,68,0.25); }
|
||||
.btn-ghost { background: transparent; color: var(--text-muted); border: 1px solid var(--border); }
|
||||
.btn-ghost:hover { background: var(--surface2); color: var(--text); }
|
||||
.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.8rem; }
|
||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
/* ── Forms ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
.form-label { display: block; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.3rem; }
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
select.form-control option { background: var(--surface2); }
|
||||
|
||||
/* ── Tables ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
.table { width: 100%; border-collapse: collapse; }
|
||||
.table th, .table td { padding: 0.7rem 1rem; text-align: left; }
|
||||
.table th { font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); }
|
||||
.table td { border-bottom: 1px solid rgba(255,255,255,0.04); font-size: 0.9rem; }
|
||||
.table tr:last-child td { border-bottom: none; }
|
||||
.table tr:hover td { background: rgba(255,255,255,0.02); }
|
||||
|
||||
/* ── LED Status Widget ───────────────────────────────────────────────────── */
|
||||
|
||||
.led-ring {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.led-ring.idle { border-color: #1e4080; box-shadow: 0 0 15px rgba(30,64,128,0.5); color: #4080d0; }
|
||||
.led-ring.countdown { border-color: var(--led-green); box-shadow: 0 0 20px rgba(34,197,94,0.6); color: var(--led-green); animation: pulse-green 1s infinite; }
|
||||
.led-ring.capture { border-color: #fff; box-shadow: 0 0 30px rgba(255,255,255,0.8); color: #fff; }
|
||||
.led-ring.captured { border-color: var(--led-purple); box-shadow: 0 0 20px rgba(168,85,247,0.6); color: var(--led-purple); }
|
||||
.led-ring.finished { border-color: var(--led-blue); box-shadow: 0 0 20px rgba(59,130,246,0.6); color: var(--led-blue); }
|
||||
.led-ring.printing { border-color: var(--led-blue); box-shadow: 0 0 20px rgba(59,130,246,0.4); color: var(--led-blue); animation: spin-ring 1s linear infinite; }
|
||||
.led-ring.error { border-color: var(--error); box-shadow: 0 0 20px rgba(239,68,68,0.6); color: var(--error); animation: blink 0.3s infinite; }
|
||||
.led-ring.disabled { border-color: #4a0000; box-shadow: 0 0 10px rgba(100,0,0,0.3); color: #800000; }
|
||||
.led-ring.off { border-color: var(--border); }
|
||||
|
||||
@keyframes pulse-green { 0%,100% { box-shadow: 0 0 15px rgba(34,197,94,0.4); } 50% { box-shadow: 0 0 30px rgba(34,197,94,0.9); } }
|
||||
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.2; } }
|
||||
@keyframes spin-ring { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Status dots ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot-green { background: var(--success); box-shadow: 0 0 6px rgba(34,197,94,0.6); }
|
||||
.dot-red { background: var(--error); }
|
||||
.dot-yellow { background: var(--warning); }
|
||||
.dot-gray { background: var(--text-dim); }
|
||||
|
||||
/* ── Gallery grid ────────────────────────────────────────────────────────── */
|
||||
|
||||
.photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.photo-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
aspect-ratio: 3/2;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.photo-card:hover {
|
||||
transform: scale(1.02);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.photo-card img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.photo-card-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,0.85));
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.photo-card:hover .photo-card-actions { opacity: 1; }
|
||||
|
||||
/* ── Lightbox ────────────────────────────────────────────────────────────── */
|
||||
|
||||
.lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.92);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.lightbox.open { display: flex; }
|
||||
|
||||
.lightbox img {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.lightbox-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.lightbox-close:hover { color: var(--text); }
|
||||
|
||||
/* ── Login ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.login-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login-title h1 { font-size: 1.4rem; font-weight: 700; }
|
||||
.login-title p { color: var(--text-muted); font-size: 0.9rem; margin-top: 0.25rem; }
|
||||
|
||||
.error-msg {
|
||||
background: rgba(239,68,68,0.12);
|
||||
border: 1px solid rgba(239,68,68,0.3);
|
||||
color: var(--error);
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ── Print queue ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.print-thumb {
|
||||
width: 80px;
|
||||
height: 54px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: var(--surface2);
|
||||
}
|
||||
|
||||
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.section { margin-bottom: 2rem; }
|
||||
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-1 { gap: 0.5rem; }
|
||||
.gap-2 { gap: 1rem; }
|
||||
|
||||
.text-muted { color: var(--text-muted); }
|
||||
.text-sm { font-size: 0.85rem; }
|
||||
.text-xs { font-size: 0.75rem; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.mt-1 { margin-top: 0.5rem; }
|
||||
.mt-2 { margin-top: 1rem; }
|
||||
.mb-1 { margin-bottom: 0.5rem; }
|
||||
.mb-2 { margin-bottom: 1rem; }
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state .icon { font-size: 3rem; margin-bottom: 0.75rem; }
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.toast.success { border-color: rgba(34,197,94,0.4); color: var(--success); }
|
||||
.toast.error { border-color: rgba(239,68,68,0.4); color: var(--error); }
|
||||
|
||||
@keyframes slideIn { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
|
||||
/* ── Responsive ──────────────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container { padding: 1rem; }
|
||||
.navbar { flex-wrap: wrap; }
|
||||
.navbar-links { order: 3; width: 100%; flex-wrap: wrap; }
|
||||
.grid-4 { grid-template-columns: repeat(2, 1fr); }
|
||||
.photo-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Actions — JH Photomaton{% endblock %}
|
||||
|
||||
{% block nav_links %}
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/gallery">Galerie admin</a>
|
||||
<a href="/admin/print">Impression</a>
|
||||
<a href="/admin/actions" class="active">Actions</a>
|
||||
<a href="/gallery">Galerie publique</a>
|
||||
<a href="/admin/logout" style="margin-left:auto;color:var(--text-dim)">Déconnexion</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="page-title">Gestion des actions & mapping bouton</h1>
|
||||
|
||||
<!-- Mapping clics → actions -->
|
||||
<div class="section">
|
||||
<div class="card-title">Mapping bouton → actions photobooth-app</div>
|
||||
<div class="card">
|
||||
<p class="text-sm text-muted mb-2">Associe chaque nombre de clics à une action dans photobooth-app. L'index correspond à la position dans la liste des actions (0 = première action).</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Clics</th>
|
||||
<th>Label affiché</th>
|
||||
<th>Index action (photobooth)</th>
|
||||
<th>Action correspondante</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for n in range(1, 5) %}
|
||||
{% set mapping = button_mapping.get(n) or button_mapping.get(n|string) or {} %}
|
||||
<tr>
|
||||
<td class="font-bold">
|
||||
{% if n == 1 %}1 clic{% else %}{{ n }} clics{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="label-{{ n }}" class="form-control" style="max-width:200px"
|
||||
value="{{ mapping.get('label', '') }}" placeholder="Label…">
|
||||
</td>
|
||||
<td>
|
||||
<select id="index-{{ n }}" class="form-control" style="max-width:80px">
|
||||
{% for i in range(pb_actions|length) %}
|
||||
<option value="{{ i }}" {% if mapping.get('photobooth_index') == i %}selected{% endif %}>{{ i }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-sm text-muted" id="action-name-{{ n }}">
|
||||
{% set idx = mapping.get('photobooth_index', 0) %}
|
||||
{% if pb_actions and idx < pb_actions|length %}
|
||||
{{ pb_actions[idx].name }}
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm" onclick="testAction({{ n }})">▶ Test</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="btn btn-primary mt-2" onclick="saveMapping()">💾 Sauvegarder le mapping</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions photobooth-app -->
|
||||
<div class="section">
|
||||
<div class="card-title">Actions image photobooth-app ({{ pb_actions|length }} au total)</div>
|
||||
<div id="actions-list">
|
||||
{% for action in pb_actions %}
|
||||
<div class="card mb-1" id="action-{{ loop.index0 }}" style="margin-bottom:0.75rem">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div>
|
||||
<span class="badge badge-muted text-xs">Action {{ loop.index0 }}</span>
|
||||
<span class="font-bold" style="margin-left:0.5rem">{{ action.name }}</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleEdit({{ loop.index0 }})">✏ Modifier</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="triggerAction({{ loop.index0 }})">▶ Déclencher</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Infos résumées -->
|
||||
<div class="flex gap-2 text-sm text-muted" id="summary-{{ loop.index0 }}">
|
||||
<span>⏱ {{ action.jobcontrol.get('countdown_capture', '?') }}s</span>
|
||||
{% if action.processing.get('img_frame_file') %}
|
||||
<span>🖼 {{ action.processing.img_frame_file.split('/')[-1] }}</span>
|
||||
{% endif %}
|
||||
{% if action.processing.get('remove_background') %}
|
||||
<span class="badge badge-info">Remove BG</span>
|
||||
{% endif %}
|
||||
{% if action.processing.get('img_background_file') %}
|
||||
<span>🌄 {{ action.processing.img_background_file.split('/')[-1] }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'édition (masqué par défaut) -->
|
||||
<div class="edit-form mt-2" id="edit-{{ loop.index0 }}" style="display:none;border-top:1px solid var(--border);padding-top:1rem">
|
||||
<div class="grid-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nom de l'action</label>
|
||||
<input type="text" class="form-control" id="e-name-{{ loop.index0 }}" value="{{ action.name }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Countdown (secondes)</label>
|
||||
<input type="number" class="form-control" id="e-countdown-{{ loop.index0 }}"
|
||||
value="{{ action.jobcontrol.get('countdown_capture', 5) }}" min="1" max="30" step="0.5">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Cadre (img_frame_file)</label>
|
||||
<input type="text" class="form-control" id="e-frame-{{ loop.index0 }}"
|
||||
value="{{ action.processing.get('img_frame_file', '') or '' }}" placeholder="userdata/…/cadre.png">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Fond (img_background_file)</label>
|
||||
<input type="text" class="form-control" id="e-bg-{{ loop.index0 }}"
|
||||
value="{{ action.processing.get('img_background_file', '') or '' }}" placeholder="userdata/…/fond.jpg">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Filtre image</label>
|
||||
<select class="form-control" id="e-filter-{{ loop.index0 }}">
|
||||
<option value="original" {% if action.processing.get('image_filter','original') == 'original' %}selected{% endif %}>original</option>
|
||||
<option value="FilterPilgram2.earlybird" {% if 'earlybird' in (action.processing.get('image_filter','')) %}selected{% endif %}>Earlybird</option>
|
||||
<option value="FilterPilgram2.reyes" {% if 'reyes' in (action.processing.get('image_filter','')) %}selected{% endif %}>Reyes</option>
|
||||
<option value="FilterPilgram2.moon" {% if 'moon' in (action.processing.get('image_filter','')) %}selected{% endif %}>Moon</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label flex items-center gap-1">
|
||||
<input type="checkbox" id="e-rmbg-{{ loop.index0 }}" {% if action.processing.get('remove_background') %}checked{% endif %}>
|
||||
Remove Background (MODNet — ~400Mo RAM)
|
||||
</label>
|
||||
<label class="form-label flex items-center gap-1 mt-1">
|
||||
<input type="checkbox" id="e-bgena-{{ loop.index0 }}" {% if action.processing.get('img_background_enable') %}checked{% endif %}>
|
||||
Activer le fond
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-primary btn-sm" onclick="saveAction({{ loop.index0 }})">💾 Sauvegarder</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleEdit({{ loop.index0 }})">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><div class="icon">⚙️</div>Aucune action configurée dans photobooth-app</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ── Formulaires d'édition ─────────────────────────────────────────────────────
|
||||
function toggleEdit(idx) {
|
||||
const el = document.getElementById('edit-' + idx);
|
||||
const sum = document.getElementById('summary-' + idx);
|
||||
const open = el.style.display === 'none';
|
||||
el.style.display = open ? 'block' : 'none';
|
||||
sum.style.display = open ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
async function saveAction(idx) {
|
||||
const updates = {
|
||||
name: document.getElementById('e-name-' + idx).value,
|
||||
countdown_capture: parseFloat(document.getElementById('e-countdown-' + idx).value),
|
||||
img_frame_file: document.getElementById('e-frame-' + idx).value || null,
|
||||
img_background_file: document.getElementById('e-bg-' + idx).value || null,
|
||||
image_filter: document.getElementById('e-filter-' + idx).value,
|
||||
remove_background: document.getElementById('e-rmbg-' + idx).checked,
|
||||
img_background_enable: document.getElementById('e-bgena-' + idx).checked,
|
||||
};
|
||||
try {
|
||||
await api('PUT', `/api/actions/photobooth/${idx}`, updates);
|
||||
showToast('✅ Action sauvegardée — redémarrage photobooth-app requis', 'success', 6000);
|
||||
toggleEdit(idx);
|
||||
} catch(e) {
|
||||
showToast('❌ Erreur sauvegarde', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerAction(idx) {
|
||||
if (!confirm(`Déclencher l'action ${idx} (prise de photo) ?`)) return;
|
||||
try {
|
||||
await api('POST', `/api/actions/trigger/${idx}`);
|
||||
showToast(`▶ Action ${idx} déclenchée`, 'info');
|
||||
} catch(e) { showToast('Erreur déclenchement', 'error'); }
|
||||
}
|
||||
|
||||
// ── Mapping bouton ─────────────────────────────────────────────────────────────
|
||||
async function saveMapping() {
|
||||
const mapping = {};
|
||||
for (let n = 1; n <= 4; n++) {
|
||||
mapping[n] = {
|
||||
label: document.getElementById('label-' + n).value,
|
||||
photobooth_index: parseInt(document.getElementById('index-' + n).value),
|
||||
};
|
||||
}
|
||||
try {
|
||||
await api('PUT', '/api/actions/mapping', mapping);
|
||||
showToast('✅ Mapping sauvegardé', 'success');
|
||||
} catch(e) { showToast('❌ Erreur sauvegarde mapping', 'error'); }
|
||||
}
|
||||
|
||||
async function testAction(n) {
|
||||
const idx = parseInt(document.getElementById('index-' + n).value);
|
||||
if (!confirm(`Déclencher l'action ${idx} pour tester le ${n} clic ?`)) return;
|
||||
try {
|
||||
await api('POST', `/api/actions/trigger/${idx}`);
|
||||
showToast(`▶ Test ${n} clic → action ${idx}`, 'info');
|
||||
} catch(e) { showToast('Erreur', 'error'); }
|
||||
}
|
||||
|
||||
// Mise à jour du nom de l'action quand on change l'index
|
||||
{% for n in range(1, 5) %}
|
||||
document.getElementById('index-{{ n }}').addEventListener('change', function() {
|
||||
const names = {{ pb_actions | map(attribute='name') | list | tojson }};
|
||||
document.getElementById('action-name-{{ n }}').textContent = names[this.value] || '—';
|
||||
});
|
||||
{% endfor %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,281 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — JH Photomaton{% endblock %}
|
||||
|
||||
{% block nav_links %}
|
||||
<a href="/admin" class="active">Dashboard</a>
|
||||
<a href="/admin/gallery">Galerie admin</a>
|
||||
<a href="/admin/print">Impression</a>
|
||||
<a href="/admin/actions">Actions</a>
|
||||
<a href="/gallery">Galerie publique</a>
|
||||
<a href="/admin/logout" style="margin-left:auto;color:var(--text-dim)">Déconnexion</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h1 class="page-title" style="margin:0">Dashboard</h1>
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="text-sm text-muted">Mode impression :</span>
|
||||
<select id="print-mode-select" class="form-control" style="width:auto">
|
||||
<option value="direct" {% if print_mode=='direct' %}selected{% endif %}>Direct</option>
|
||||
<option value="validation" {% if print_mode=='validation' %}selected{% endif %}>Validation</option>
|
||||
<option value="gallery" {% if print_mode=='gallery' %}selected{% endif %}>Galerie</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ressources système -->
|
||||
<div class="section">
|
||||
<div class="card-title">Ressources système</div>
|
||||
<div class="grid-4">
|
||||
<div class="card">
|
||||
<div class="card-title">CPU</div>
|
||||
<div class="card-value" id="cpu-val">{{ stats.cpu_percent|default('—') }}%</div>
|
||||
<div class="progress mt-1"><div class="progress-bar" id="cpu-bar" style="width:{{ stats.cpu_percent|default(0) }}%"></div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">RAM</div>
|
||||
<div class="card-value" id="ram-val">{{ stats.ram_used_mb|default('—') }} Mo</div>
|
||||
<div class="card-sub" id="ram-sub">sur {{ stats.ram_total_mb|default('?') }} Mo</div>
|
||||
<div class="progress mt-1"><div class="progress-bar" id="ram-bar" style="width:{{ stats.ram_percent|default(0) }}%"></div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Température</div>
|
||||
<div class="card-value" id="temp-val">{{ stats.cpu_temp|default('—') }}°C</div>
|
||||
<div class="card-sub">CPU</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Disque</div>
|
||||
<div class="card-value" id="disk-val">{{ stats.disk_used_gb|default('—') }} Go</div>
|
||||
<div class="card-sub" id="disk-sub">sur {{ stats.disk_total_gb|default('?') }} Go</div>
|
||||
<div class="progress mt-1"><div class="progress-bar" id="disk-bar" style="width:{{ stats.disk_percent|default(0) }}%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 section">
|
||||
<!-- Contrôle LED -->
|
||||
<div class="card">
|
||||
<div class="card-title">Anneau LED (35 LEDs · GPIO18)</div>
|
||||
<div class="flex items-center gap-2 mt-1 mb-2">
|
||||
<div class="led-ring {{ led_effect }}" id="led-ring">
|
||||
<span id="led-label">{{ led_effect }}</span>
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<div class="text-sm text-muted mb-1">Effet actuel</div>
|
||||
<div class="flex gap-1" style="flex-wrap:wrap">
|
||||
{% for effect in ['idle','countdown','capture','captured','finished','printing','error','disabled','off'] %}
|
||||
<button class="btn btn-ghost btn-sm" onclick="setLedEffect('{{ effect }}')">{{ effect }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex gap-1 mt-1 items-center">
|
||||
<span class="text-sm text-muted">Couleur :</span>
|
||||
<input type="color" id="led-color" value="#1e5080" style="border:none;background:none;cursor:pointer;width:32px;height:28px">
|
||||
<button class="btn btn-ghost btn-sm" onclick="setLedColor()">Appliquer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statut bouton & relay -->
|
||||
<div class="card">
|
||||
<div class="card-title">Bouton physique (GPIO23) · Relay (GPIO12)</div>
|
||||
<div class="flex gap-2 mt-1 mb-2">
|
||||
<div>
|
||||
<div class="text-sm text-muted mb-1">Relay bouton 12V</div>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-success btn-sm" onclick="setRelay('on')">✅ Activer</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="setRelay('off')">🔴 Désactiver</button>
|
||||
</div>
|
||||
<div class="text-xs text-muted mt-1">Statut : <span id="relay-state">{{ 'ON' if relay_state else 'OFF' }}</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-muted mb-1">Simulation (test)</div>
|
||||
<div class="flex gap-1" style="flex-wrap:wrap">
|
||||
<button class="btn btn-ghost btn-sm" onclick="simulateClick(1)">1 clic</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="simulateClick(2)">2 clics</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="simulateClick(3)">3 clics</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="simulateClick(0)">Long press</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted" id="last-button-event">Dernier événement : —</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File d'impression en attente -->
|
||||
<div class="section">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="card-title" style="margin:0">File d'impression (<span id="queue-count">…</span>)</div>
|
||||
<a href="/admin/print" class="btn btn-ghost btn-sm">Voir tout →</a>
|
||||
</div>
|
||||
<div class="card" id="print-queue-container">
|
||||
<div class="text-muted text-sm">Chargement…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<div class="section">
|
||||
<div class="card-title">Services</div>
|
||||
<div class="card">
|
||||
<table class="table">
|
||||
<thead><tr><th>Service</th><th>Statut</th></tr></thead>
|
||||
<tbody id="services-tbody">
|
||||
{% for svc in services %}
|
||||
<tr>
|
||||
<td>{{ svc.label }}</td>
|
||||
<td><span class="badge {{ 'badge-success' if svc.active else 'badge-error' }}">{{ 'Actif' if svc.active else 'Arrêté' }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ── Live stats ────────────────────────────────────────────────────────────────
|
||||
function updateStats(s) {
|
||||
if (!s) return;
|
||||
if (s.cpu_percent !== undefined) {
|
||||
document.getElementById('cpu-val').textContent = s.cpu_percent + '%';
|
||||
const cpuBar = document.getElementById('cpu-bar');
|
||||
cpuBar.style.width = s.cpu_percent + '%';
|
||||
cpuBar.className = 'progress-bar' + (s.cpu_percent > 85 ? ' danger' : s.cpu_percent > 65 ? ' warn' : '');
|
||||
}
|
||||
if (s.ram_used_mb !== undefined) {
|
||||
document.getElementById('ram-val').textContent = s.ram_used_mb + ' Mo';
|
||||
document.getElementById('ram-sub').textContent = 'sur ' + s.ram_total_mb + ' Mo';
|
||||
document.getElementById('ram-bar').style.width = s.ram_percent + '%';
|
||||
}
|
||||
if (s.cpu_temp !== undefined && s.cpu_temp !== null) {
|
||||
document.getElementById('temp-val').textContent = s.cpu_temp + '°C';
|
||||
}
|
||||
if (s.disk_used_gb !== undefined) {
|
||||
document.getElementById('disk-val').textContent = s.disk_used_gb + ' Go';
|
||||
document.getElementById('disk-sub').textContent = 'sur ' + s.disk_total_gb + ' Go';
|
||||
document.getElementById('disk-bar').style.width = s.disk_percent + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// ── LED ───────────────────────────────────────────────────────────────────────
|
||||
function updateLed(effect) {
|
||||
const ring = document.getElementById('led-ring');
|
||||
ring.className = 'led-ring ' + effect;
|
||||
document.getElementById('led-label').textContent = effect;
|
||||
}
|
||||
|
||||
async function setLedEffect(effect) {
|
||||
try {
|
||||
await api('POST', `/api/leds/effect?effect=${effect}`);
|
||||
updateLed(effect);
|
||||
} catch(e) { showToast('Erreur LED', 'error'); }
|
||||
}
|
||||
|
||||
async function setLedColor() {
|
||||
const hex = document.getElementById('led-color').value;
|
||||
const r = parseInt(hex.slice(1,3),16);
|
||||
const g = parseInt(hex.slice(3,5),16);
|
||||
const b = parseInt(hex.slice(5,7),16);
|
||||
try {
|
||||
await api('POST', `/api/leds/color?r=${r}&g=${g}&b=${b}`);
|
||||
showToast('Couleur appliquée', 'success');
|
||||
} catch(e) { showToast('Erreur', 'error'); }
|
||||
}
|
||||
|
||||
// ── Relay ─────────────────────────────────────────────────────────────────────
|
||||
async function setRelay(state) {
|
||||
try {
|
||||
await api('POST', `/api/system/relay?state=${state}`);
|
||||
document.getElementById('relay-state').textContent = state.toUpperCase();
|
||||
showToast(`Relay ${state.toUpperCase()}`, 'success');
|
||||
} catch(e) { showToast('Erreur relay', 'error'); }
|
||||
}
|
||||
|
||||
// ── Simulation bouton ─────────────────────────────────────────────────────────
|
||||
async function simulateClick(n) {
|
||||
try {
|
||||
await api('POST', `/api/system/button/simulate?clicks=${n}`);
|
||||
showToast(n === 0 ? 'Long press simulé' : `${n} clic(s) simulé(s)`, 'info');
|
||||
} catch(e) { showToast('Erreur simulation', 'error'); }
|
||||
}
|
||||
|
||||
// ── Mode impression ────────────────────────────────────────────────────────────
|
||||
document.getElementById('print-mode-select').addEventListener('change', async (e) => {
|
||||
try {
|
||||
await api('POST', `/api/print/mode?mode=${e.target.value}`);
|
||||
showToast(`Mode: ${e.target.value}`, 'success');
|
||||
} catch(err) { showToast('Erreur changement mode', 'error'); }
|
||||
});
|
||||
|
||||
// ── File d'impression ─────────────────────────────────────────────────────────
|
||||
async function loadQueue() {
|
||||
try {
|
||||
const data = await api('GET', '/api/print/queue?status=pending');
|
||||
const queue = data.queue || [];
|
||||
document.getElementById('queue-count').textContent = queue.length;
|
||||
const container = document.getElementById('print-queue-container');
|
||||
if (queue.length === 0) {
|
||||
container.innerHTML = '<div class="text-muted text-sm">Aucune impression en attente</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `
|
||||
<table class="table">
|
||||
<thead><tr><th>Aperçu</th><th>Fichier</th><th>Copies</th><th>Demandé le</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
${queue.slice(0,5).map(q => `
|
||||
<tr>
|
||||
<td><img src="${q.thumb_url}" class="print-thumb" onerror="this.style.display='none'"></td>
|
||||
<td class="text-sm">${q.filename.split('/').pop()}</td>
|
||||
<td>${q.copies}</td>
|
||||
<td class="text-xs text-muted">${new Date(q.requested_at*1000).toLocaleTimeString('fr-FR')}</td>
|
||||
<td>
|
||||
<button class="btn btn-success btn-sm" onclick="executePrint('${q.id}',${q.copies})">🖨 Imprimer</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="cancelPrint('${q.id}')">✕</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function executePrint(id, copies) {
|
||||
try {
|
||||
const r = await api('POST', `/api/print/execute/${id}?copies=${copies}`);
|
||||
showToast(r.success ? '✅ Impression lancée' : '❌ ' + r.error, r.success ? 'success' : 'error');
|
||||
loadQueue();
|
||||
} catch(e) { showToast('Erreur impression', 'error'); }
|
||||
}
|
||||
|
||||
async function cancelPrint(id) {
|
||||
try {
|
||||
await api('POST', `/api/print/cancel/${id}`);
|
||||
showToast('Annulé', 'info');
|
||||
loadQueue();
|
||||
} catch(e) { showToast('Erreur annulation', 'error'); }
|
||||
}
|
||||
|
||||
// ── WebSocket handler ─────────────────────────────────────────────────────────
|
||||
function onWsMessage(msg) {
|
||||
switch(msg.type) {
|
||||
case 'system_stats': updateStats(msg.data); break;
|
||||
case 'led_effect': updateLed(msg.effect); break;
|
||||
case 'button_event':
|
||||
document.getElementById('last-button-event').textContent =
|
||||
`Dernier événement : ${msg.clicks === 0 ? 'Long press' : msg.clicks + ' clic(s)'} → ${msg.action || ''}`;
|
||||
break;
|
||||
case 'print_request': loadQueue(); break;
|
||||
case 'print_result': loadQueue(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
loadQueue();
|
||||
setInterval(loadQueue, 15000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,151 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Galerie admin — JH Photomaton{% endblock %}
|
||||
|
||||
{% block nav_links %}
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/gallery" class="active">Galerie admin</a>
|
||||
<a href="/admin/print">Impression</a>
|
||||
<a href="/admin/actions">Actions</a>
|
||||
<a href="/gallery">Galerie publique</a>
|
||||
<a href="/admin/logout" style="margin-left:auto;color:var(--text-dim)">Déconnexion</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h1 class="page-title" style="margin:0">Galerie — Administration</h1>
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="text-sm text-muted" id="photo-count">Chargement…</span>
|
||||
<div class="flex gap-1">
|
||||
<input type="number" id="copies-input" min="1" max="3" value="1" class="form-control" style="width:70px" title="Copies">
|
||||
<button class="btn btn-ghost btn-sm" onclick="refreshPhotos()">↻ Actualiser</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtres / navigation pages -->
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<button class="btn btn-ghost btn-sm" id="prev-btn" onclick="changePage(-1)" disabled>← Préc.</button>
|
||||
<span class="text-sm text-muted">Page <span id="page-cur">1</span> / <span id="page-total">1</span></span>
|
||||
<button class="btn btn-ghost btn-sm" id="next-btn" onclick="changePage(1)">Suiv. →</button>
|
||||
</div>
|
||||
|
||||
<div class="photo-grid" id="photo-grid">
|
||||
<div class="empty-state"><div class="icon">⏳</div>Chargement…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox admin -->
|
||||
<div class="lightbox" id="lightbox" onclick="closeLightbox(event)">
|
||||
<button class="lightbox-close" onclick="closeLightbox()">×</button>
|
||||
<img id="lightbox-img" src="" alt="">
|
||||
<div class="lightbox-actions">
|
||||
<button class="btn btn-success" id="lb-print-btn">🖨 Imprimer</button>
|
||||
<a class="btn btn-ghost" id="lb-download-btn" download>⬇ Télécharger</a>
|
||||
<button class="btn btn-danger" id="lb-delete-btn">🗑 Supprimer</button>
|
||||
</div>
|
||||
<div class="text-sm text-muted" id="lb-filename"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let currentPhotoId = null;
|
||||
|
||||
async function loadPhotos(page = 1) {
|
||||
try {
|
||||
const data = await api('GET', `/admin/api/gallery/photos?page=${page}&limit=24`);
|
||||
const { photos, total, pages } = data;
|
||||
|
||||
currentPage = page;
|
||||
totalPages = pages;
|
||||
|
||||
document.getElementById('photo-count').textContent = `${total} photo(s)`;
|
||||
document.getElementById('page-cur').textContent = page;
|
||||
document.getElementById('page-total').textContent = pages;
|
||||
document.getElementById('prev-btn').disabled = page <= 1;
|
||||
document.getElementById('next-btn').disabled = page >= pages;
|
||||
|
||||
const grid = document.getElementById('photo-grid');
|
||||
if (!photos.length) {
|
||||
grid.innerHTML = '<div class="empty-state"><div class="icon">📷</div>Aucune photo</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = photos.map(p => {
|
||||
const pid = p.id || p.filename || p.uid || '';
|
||||
return `
|
||||
<div class="photo-card" onclick="openLightbox('${pid}', '${p.full_url}', '${p.thumb_url}')">
|
||||
<img src="${p.thumb_url}" loading="lazy" alt="">
|
||||
<div class="photo-card-actions">
|
||||
<button class="btn btn-success btn-sm" onclick="event.stopPropagation();printPhoto('${pid}')">🖨</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation();deletePhoto('${pid}')">🗑</button>
|
||||
<a class="btn btn-ghost btn-sm" href="${p.full_url}" download onclick="event.stopPropagation()">⬇</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch(e) {
|
||||
document.getElementById('photo-grid').innerHTML = '<div class="empty-state"><div class="icon">❌</div>Erreur de chargement</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function changePage(delta) {
|
||||
loadPhotos(currentPage + delta);
|
||||
}
|
||||
|
||||
function refreshPhotos() { loadPhotos(currentPage); }
|
||||
|
||||
// ── Lightbox ──────────────────────────────────────────────────────────────────
|
||||
function openLightbox(pid, fullUrl, thumbUrl) {
|
||||
currentPhotoId = pid;
|
||||
document.getElementById('lightbox-img').src = fullUrl || thumbUrl;
|
||||
document.getElementById('lb-download-btn').href = fullUrl;
|
||||
document.getElementById('lb-filename').textContent = pid;
|
||||
document.getElementById('lb-print-btn').onclick = () => printPhoto(pid);
|
||||
document.getElementById('lb-delete-btn').onclick = () => deletePhoto(pid);
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
}
|
||||
|
||||
function closeLightbox(e) {
|
||||
if (e && e.target !== document.getElementById('lightbox') && e.type !== 'click') return;
|
||||
document.getElementById('lightbox').classList.remove('open');
|
||||
currentPhotoId = null;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox(); });
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────────
|
||||
async function printPhoto(pid) {
|
||||
const copies = parseInt(document.getElementById('copies-input').value) || 1;
|
||||
if (!confirm(`Imprimer ${copies} copie(s) ?`)) return;
|
||||
try {
|
||||
const r = await api('POST', `/admin/api/gallery/print/${pid}?copies=${copies}`);
|
||||
if (r.success) showToast('✅ Impression lancée sur ' + r.printer, 'success');
|
||||
else if (r.ok) showToast('📋 Demande ajoutée à la file', 'info');
|
||||
else showToast('❌ ' + (r.error || 'Erreur'), 'error');
|
||||
} catch(e) { showToast('Erreur impression', 'error'); }
|
||||
}
|
||||
|
||||
async function deletePhoto(pid) {
|
||||
if (!confirm('Supprimer cette photo définitivement ?')) return;
|
||||
try {
|
||||
const r = await api('DELETE', `/admin/api/gallery/${pid}`);
|
||||
showToast(r.ok ? '🗑 Photo supprimée' : '❌ Erreur suppression', r.ok ? 'success' : 'error');
|
||||
if (r.ok) {
|
||||
document.getElementById('lightbox').classList.remove('open');
|
||||
loadPhotos(currentPage);
|
||||
}
|
||||
} catch(e) { showToast('Erreur suppression', 'error'); }
|
||||
}
|
||||
|
||||
function onWsMessage(msg) {
|
||||
if (msg.type === 'photo_deleted') loadPhotos(currentPage);
|
||||
if (msg.type === 'print_result') showToast(msg.result.success ? '✅ Impression OK' : '❌ ' + msg.result.error, msg.result.success ? 'success' : 'error');
|
||||
}
|
||||
|
||||
loadPhotos(1);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion — JH Photomaton</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrapper">
|
||||
<div class="login-card">
|
||||
<div class="login-title">
|
||||
<div style="font-size:3rem;margin-bottom:.5rem">📷</div>
|
||||
<h1>JH Photomaton</h1>
|
||||
<p>Dashboard d'administration</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="error-msg">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/login">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Mot de passe</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
placeholder="••••••••••"
|
||||
autofocus
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;padding:.65rem">
|
||||
Connexion
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-xs text-muted mt-2" style="text-align:center">
|
||||
<a href="/gallery">← Galerie publique</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,190 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Impression — JH Photomaton{% endblock %}
|
||||
|
||||
{% block nav_links %}
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/gallery">Galerie admin</a>
|
||||
<a href="/admin/print" class="active">Impression</a>
|
||||
<a href="/admin/actions">Actions</a>
|
||||
<a href="/gallery">Galerie publique</a>
|
||||
<a href="/admin/logout" style="margin-left:auto;color:var(--text-dim)">Déconnexion</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="page-title">Gestion de l'impression</h1>
|
||||
|
||||
<!-- Imprimantes -->
|
||||
<div class="section">
|
||||
<div class="card-title">Imprimantes CUPS</div>
|
||||
<div class="card">
|
||||
<table class="table" id="printers-table">
|
||||
<thead><tr><th>Imprimante</th><th>Statut</th><th>Jobs en attente</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{% for p in printers %}
|
||||
<tr>
|
||||
<td class="font-bold">{{ p.label }}</td>
|
||||
<td>
|
||||
<span class="badge
|
||||
{% if p.state == 'idle' %}badge-success
|
||||
{% elif p.state == 'printing' %}badge-info
|
||||
{% elif p.state == 'disabled' %}badge-warning
|
||||
{% else %}badge-error{% endif %}
|
||||
">
|
||||
{% 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 %}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ p.jobs }}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-sm" onclick="cancelCupsJobs('{{ p.name }}')">✕ Vider file</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode impression -->
|
||||
<div class="section">
|
||||
<div class="card-title">Mode d'impression</div>
|
||||
<div class="card">
|
||||
<div class="flex gap-1 mb-1">
|
||||
<label class="flex items-center gap-1" style="cursor:pointer">
|
||||
<input type="radio" name="print-mode" value="direct" {% if config.print.mode=='direct' %}checked{% endif %}> Direct (impression automatique)
|
||||
</label>
|
||||
<label class="flex items-center gap-1" style="cursor:pointer">
|
||||
<input type="radio" name="print-mode" value="validation" {% if config.print.mode=='validation' %}checked{% endif %}> Validation (admin confirme)
|
||||
</label>
|
||||
<label class="flex items-center gap-1" style="cursor:pointer">
|
||||
<input type="radio" name="print-mode" value="gallery" {% if config.print.mode=='gallery' %}checked{% endif %}> Galerie (file uniquement)
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-muted">
|
||||
<b>Direct</b> : impression lancée immédiatement sans confirmation. <b>Validation</b> : l'admin valide chaque impression. <b>Galerie</b> : les demandes s'accumulent, impression via la galerie admin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File d'attente -->
|
||||
<div class="section">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="card-title" style="margin:0">File d'attente (<span id="pending-count">{{ queue|length }}</span>)</div>
|
||||
<button class="btn btn-ghost btn-sm" onclick="refreshQueue()">↻ Actualiser</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div id="queue-container">
|
||||
{% if not queue %}
|
||||
<div class="empty-state"><div class="icon">✅</div>Aucune impression en attente</div>
|
||||
{% else %}
|
||||
<table class="table" id="queue-table">
|
||||
<thead><tr><th>Aperçu</th><th>Fichier</th><th>Copies</th><th>Statut</th><th>Demandé</th><th>Actions</th></tr></thead>
|
||||
<tbody id="queue-tbody">
|
||||
{% for q in queue %}
|
||||
<tr id="row-{{ q.id }}">
|
||||
<td><img src="{{ q.thumb_url }}" class="print-thumb" onerror="this.style.opacity=0"></td>
|
||||
<td class="text-sm">{{ q.filename.split('/')[-1] }}</td>
|
||||
<td>
|
||||
<input type="number" value="{{ q.copies }}" min="1" max="3" id="copies-{{ q.id }}" style="width:50px;background:var(--surface2);border:1px solid var(--border);color:var(--text);border-radius:4px;padding:2px 6px">
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge
|
||||
{% if q.status == 'pending' %}badge-warning
|
||||
{% elif q.status == 'done' %}badge-success
|
||||
{% elif q.status == 'printing' %}badge-info
|
||||
{% else %}badge-error{% endif %}
|
||||
">{{ q.status }}</span>
|
||||
</td>
|
||||
<td class="text-xs text-muted">{{ q.requested_at|int }}</td>
|
||||
<td>
|
||||
{% if q.status == 'pending' %}
|
||||
<button class="btn btn-success btn-sm" onclick="executePrint('{{ q.id }}')">🖨 Imprimer</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="cancelPrint('{{ q.id }}')">✕</button>
|
||||
{% elif q.status == 'done' %}
|
||||
<span class="text-sm text-muted">{{ q.printer or '—' }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ── Mode impression ────────────────────────────────────────────────────────────
|
||||
document.querySelectorAll('input[name="print-mode"]').forEach(r => {
|
||||
r.addEventListener('change', async (e) => {
|
||||
try {
|
||||
await api('POST', `/api/print/mode?mode=${e.target.value}`);
|
||||
showToast(`Mode: ${e.target.value}`, 'success');
|
||||
} catch(e) { showToast('Erreur', 'error'); }
|
||||
});
|
||||
});
|
||||
|
||||
// ── File d'attente ─────────────────────────────────────────────────────────────
|
||||
async function refreshQueue() {
|
||||
try {
|
||||
const data = await api('GET', '/api/print/queue');
|
||||
const queue = data.queue || [];
|
||||
const pending = queue.filter(q => q.status === 'pending');
|
||||
document.getElementById('pending-count').textContent = pending.length;
|
||||
|
||||
const tbody = document.getElementById('queue-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
// Mise à jour des statuts existants
|
||||
queue.forEach(q => {
|
||||
const row = document.getElementById('row-' + q.id);
|
||||
if (row) {
|
||||
const badge = row.querySelector('.badge');
|
||||
if (badge) badge.textContent = q.status;
|
||||
}
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function executePrint(id) {
|
||||
const copiesEl = document.getElementById('copies-' + id);
|
||||
const copies = copiesEl ? parseInt(copiesEl.value) : 1;
|
||||
try {
|
||||
const r = await api('POST', `/api/print/execute/${id}?copies=${copies}`);
|
||||
showToast(r.success ? '✅ Impression lancée sur ' + r.printer : '❌ ' + r.error, r.success ? 'success' : 'error');
|
||||
setTimeout(refreshQueue, 1000);
|
||||
} catch(e) { showToast('Erreur impression', 'error'); }
|
||||
}
|
||||
|
||||
async function cancelPrint(id) {
|
||||
try {
|
||||
await api('POST', `/api/print/cancel/${id}`);
|
||||
showToast('Annulé', 'info');
|
||||
const row = document.getElementById('row-' + id);
|
||||
if (row) row.remove();
|
||||
} catch(e) { showToast('Erreur', 'error'); }
|
||||
}
|
||||
|
||||
async function cancelCupsJobs(printer) {
|
||||
if (!confirm(`Vider la file de ${printer} ?`)) return;
|
||||
try {
|
||||
const r = await api('POST', `/api/print/cups/cancel/${printer}`);
|
||||
showToast(r.ok ? 'File vidée' : 'Erreur', r.ok ? 'success' : 'error');
|
||||
refreshQueue();
|
||||
} catch(e) { showToast('Erreur', 'error'); }
|
||||
}
|
||||
|
||||
function onWsMessage(msg) {
|
||||
if (['print_request','print_result','print_cancelled'].includes(msg.type)) refreshQueue();
|
||||
}
|
||||
|
||||
setInterval(refreshQueue, 10000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}JH Photomaton{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% block navbar %}
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">📷 <span>JH</span> Photomaton</div>
|
||||
<div class="navbar-links">
|
||||
{% block nav_links %}{% endblock %}
|
||||
</div>
|
||||
<div class="navbar-status" id="ws-status">
|
||||
<span class="dot dot-gray" id="ws-dot"></span>
|
||||
<span id="ws-label">Connexion…</span>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Toast container -->
|
||||
<div class="toast-container" id="toasts"></div>
|
||||
|
||||
<script>
|
||||
// ── WebSocket global ─────────────────────────────────────────────────────────
|
||||
let ws = null;
|
||||
let wsReconnectTimer = null;
|
||||
|
||||
function connectWS() {
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
||||
|
||||
ws.onopen = () => {
|
||||
document.getElementById('ws-dot').className = 'dot dot-green';
|
||||
document.getElementById('ws-label').textContent = 'Connecté';
|
||||
clearTimeout(wsReconnectTimer);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
document.getElementById('ws-dot').className = 'dot dot-red';
|
||||
document.getElementById('ws-label').textContent = 'Déconnecté';
|
||||
wsReconnectTimer = setTimeout(connectWS, 3000);
|
||||
};
|
||||
|
||||
ws.onerror = () => ws.close();
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
try {
|
||||
const msg = JSON.parse(e.data);
|
||||
handleWsMessage(msg);
|
||||
} catch(err) {}
|
||||
};
|
||||
}
|
||||
|
||||
function handleWsMessage(msg) {
|
||||
// Override dans chaque page si besoin
|
||||
if (typeof onWsMessage === 'function') onWsMessage(msg);
|
||||
}
|
||||
|
||||
// ── Toast notifications ──────────────────────────────────────────────────────
|
||||
function showToast(text, type = 'info', duration = 4000) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `toast ${type}`;
|
||||
el.textContent = text;
|
||||
document.getElementById('toasts').appendChild(el);
|
||||
setTimeout(() => el.remove(), duration);
|
||||
}
|
||||
|
||||
// ── API helper ───────────────────────────────────────────────────────────────
|
||||
async function api(method, url, body = null) {
|
||||
const opts = { method, headers: {} };
|
||||
if (body) {
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const r = await fetch(url, opts);
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
connectWS();
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Galerie photos — Photomaton LSDW{% endblock %}
|
||||
|
||||
{% block nav_links %}
|
||||
<a href="/gallery" class="active">📷 Galerie photos</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h1 class="page-title" style="margin:0">📷 Vos photos</h1>
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="text-sm text-muted" id="photo-count">Chargement…</span>
|
||||
<button class="btn btn-ghost btn-sm" onclick="loadPhotos(1)">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-muted mb-2">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<!-- Navigation pages -->
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<button class="btn btn-ghost btn-sm" id="prev-btn" onclick="changePage(-1)" disabled>← Préc.</button>
|
||||
<span class="text-sm text-muted">Page <span id="page-cur">1</span> / <span id="page-total">1</span></span>
|
||||
<button class="btn btn-ghost btn-sm" id="next-btn" onclick="changePage(1)">Suiv. →</button>
|
||||
</div>
|
||||
|
||||
<div class="photo-grid" id="photo-grid">
|
||||
<div class="empty-state"><div class="icon">⏳</div>Chargement…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox -->
|
||||
<div class="lightbox" id="lightbox" onclick="closeLightbox(event)">
|
||||
<button class="lightbox-close">×</button>
|
||||
<img id="lightbox-img" src="" alt="Photo">
|
||||
<div class="lightbox-actions">
|
||||
<a class="btn btn-primary" id="lb-download" download>⬇ Télécharger la photo</a>
|
||||
</div>
|
||||
<div class="text-xs text-muted">Connectez-vous au WiFi Photomaton pour télécharger</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
|
||||
async function loadPhotos(page = 1) {
|
||||
try {
|
||||
const data = await api('GET', `/api/gallery/photos?page=${page}&limit=24`);
|
||||
const { photos, total, pages } = data;
|
||||
|
||||
currentPage = page;
|
||||
totalPages = pages;
|
||||
|
||||
document.getElementById('photo-count').textContent = `${total} photo(s)`;
|
||||
document.getElementById('page-cur').textContent = page;
|
||||
document.getElementById('page-total').textContent = pages;
|
||||
document.getElementById('prev-btn').disabled = page <= 1;
|
||||
document.getElementById('next-btn').disabled = page >= pages;
|
||||
|
||||
const grid = document.getElementById('photo-grid');
|
||||
|
||||
if (!photos || !photos.length) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<div class="icon">📷</div>
|
||||
Aucune photo pour l'instant — prenez la pose !
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = photos.map(p => `
|
||||
<div class="photo-card" onclick="openLightbox('${p.full_url}', '${p.download_url}')">
|
||||
<img src="${p.thumb_url}" loading="lazy" alt="Photo" onerror="this.src='${p.full_url}'">
|
||||
<div class="photo-card-actions">
|
||||
<a class="btn btn-primary btn-sm" href="${p.download_url}" download onclick="event.stopPropagation()">⬇ Télécharger</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch(e) {
|
||||
document.getElementById('photo-grid').innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<div class="icon">⚠️</div>
|
||||
Impossible de charger les photos. Êtes-vous connecté au WiFi Photomaton ?
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function changePage(delta) { loadPhotos(currentPage + delta); }
|
||||
|
||||
function openLightbox(fullUrl, downloadUrl) {
|
||||
document.getElementById('lightbox-img').src = fullUrl;
|
||||
document.getElementById('lb-download').href = downloadUrl;
|
||||
document.getElementById('lightbox').classList.add('open');
|
||||
}
|
||||
|
||||
function closeLightbox(e) {
|
||||
if (!e || e.target.id === 'lightbox' || e.target.classList.contains('lightbox-close')) {
|
||||
document.getElementById('lightbox').classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('.lightbox-close').addEventListener('click', closeLightbox);
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox({}); });
|
||||
|
||||
function onWsMessage(msg) {
|
||||
// Rafraîchit la galerie quand une nouvelle photo est ajoutée
|
||||
if (msg.type === 'photobooth_event' && msg.event === 'finished') {
|
||||
setTimeout(() => loadPhotos(1), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
loadPhotos(1);
|
||||
// Auto-refresh toutes les 30 secondes
|
||||
setInterval(() => loadPhotos(1), 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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": "<div> <div class=\"fixed-top-right q-mt-lg q-mr-lg\" style=\"pointer-events: none; max-width: 400px;\"> <div class=\"q-pa-md\" style=\"background: rgba(226,235,242,0.85); border-radius: 12px; backdrop-filter: blur(4px); border: 2px solid #89a9c2;\"> <div class=\"text-h3 text-weight-bold\" style=\"color: #335e7e;\">Bienvenue !</div> <div style=\"font-size: 1.3rem; line-height: 1.6; color: #335e7e; margin-top: 10px;\"> <div>Appuyez sur le bouton pour prendre une photo</div> </div> </div> </div> <div class=\"fixed-bottom-left q-mb-sm q-ml-lg\" style=\"pointer-events: none;\"> <div class=\"q-pa-sm\" style=\"background: rgba(226,235,242,0.75); border-radius: 8px; border: 1px solid #89a9c2; white-space: nowrap;\"> <span style=\"font-size: 0.85rem; color: #335e7e;\"> 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. </span> </div> </div> <div class=\"fixed-bottom-right q-mb-lg q-mr-lg\" style=\"pointer-events: none;\"> <img src=\"/userdata/LSDW/logo/logo.jpeg\" alt=\"Les Sapins Du Web\" style=\"width:180px; height:180px; border-radius: 12px; box-shadow: 2px 2px 10px rgba(0,0,0,0.5);\"> </div></div>",
|
||||
"TAKEPIC_MSG_TIME": 0.5,
|
||||
"TAKEPIC_MSG_TEXT": "😃",
|
||||
"AUTOCLOSE_NEW_ITEM_ARRIVED": 30,
|
||||
"GALLERY_EMPTY_MSG": "<div class=\"fixed-center text-h2 text-weight-bold text-center text-white\" style=\"text-shadow: 4px 4px 4px #666;\">Empty, Zero, Nada! 🤷♂️<br>Let's take some pictures! <br>📷💕</div>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}!"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"wled_enabled": false,
|
||||
"wled_serial_port": "/dev/ttyS0"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"wled_enabled": false,
|
||||
"wled_serial_port": "/dev/ttyS0"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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://<ip-du-pi>: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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user