Compare commits
2 Commits
182042350d
...
4e431507fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e431507fc | |||
| 48f54ab70a |
@@ -79,19 +79,31 @@ class Camera:
|
|||||||
|
|
||||||
if source == "gphoto2" or source is None:
|
if source == "gphoto2" or source is None:
|
||||||
if GPHOTO2_DISPONIBLE:
|
if GPHOTO2_DISPONIBLE:
|
||||||
|
cam = None
|
||||||
try:
|
try:
|
||||||
self.camera = gp.Camera()
|
cam = gp.Camera()
|
||||||
self.camera.init()
|
try:
|
||||||
# Desactiver la mise en veille auto du DSLR (sinon il se deconnecte tout seul)
|
cam.exit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
cam.init()
|
||||||
|
self.camera = cam
|
||||||
self._desactiver_veille_init()
|
self._desactiver_veille_init()
|
||||||
# Viewfinder activé AVANT de rendre la caméra visible au thread preview
|
|
||||||
self._activer_viewfinder_init()
|
self._activer_viewfinder_init()
|
||||||
self.connectee = True
|
self.connectee = True
|
||||||
self.mode = "gphoto2"
|
self.mode = "gphoto2"
|
||||||
|
self.preview_dslr_ok = True
|
||||||
log.info("Camera DSLR connectee (LiveView actif)")
|
log.info("Camera DSLR connectee (LiveView actif)")
|
||||||
return True
|
return True
|
||||||
except gp.GPhoto2Error as e:
|
except Exception as e:
|
||||||
log.warning(f"Pas de DSLR : {e}")
|
log.warning(f"Pas de DSLR : {e}")
|
||||||
|
if cam is not None and cam is not self.camera:
|
||||||
|
try:
|
||||||
|
cam.exit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
del cam
|
||||||
|
self.camera = None
|
||||||
|
|
||||||
# Pas de DSLR disponible : mode erreur, pas de fallback
|
# Pas de DSLR disponible : mode erreur, pas de fallback
|
||||||
log.warning("Aucun DSLR disponible, camera non connectee")
|
log.warning("Aucun DSLR disponible, camera non connectee")
|
||||||
@@ -116,25 +128,30 @@ class Camera:
|
|||||||
|
|
||||||
def deconnecter(self, timeout: float = 3.0):
|
def deconnecter(self, timeout: float = 3.0):
|
||||||
"""Deconnecte l'appareil photo avec timeout pour eviter le blocage PTP."""
|
"""Deconnecte l'appareil photo avec timeout pour eviter le blocage PTP."""
|
||||||
if self.camera and GPHOTO2_DISPONIBLE:
|
cam_ref = self.camera
|
||||||
|
ctx_ref = self.contexte
|
||||||
|
if cam_ref and GPHOTO2_DISPONIBLE:
|
||||||
def _exit():
|
def _exit():
|
||||||
try:
|
try:
|
||||||
self.camera.exit()
|
cam_ref.exit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
t = threading.Thread(target=_exit, daemon=True)
|
t = threading.Thread(target=_exit, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
t.join(timeout=timeout)
|
t.join(timeout=timeout)
|
||||||
if t.is_alive():
|
if t.is_alive():
|
||||||
log.warning("camera.exit() bloque — abandon (le FD sera libere au GC)")
|
log.warning("camera.exit() bloque — force del camera/contexte pour liberer le FD USB")
|
||||||
if self.webcam:
|
if self.webcam:
|
||||||
self.webcam.release()
|
self.webcam.release()
|
||||||
self.camera = None
|
self.camera = None
|
||||||
self.contexte = None
|
self.contexte = None
|
||||||
|
del cam_ref
|
||||||
|
del ctx_ref
|
||||||
self.webcam = None
|
self.webcam = None
|
||||||
self.webcam_index = -1
|
self.webcam_index = -1
|
||||||
self.connectee = False
|
self.connectee = False
|
||||||
self.mode = "simulation"
|
self.mode = "simulation"
|
||||||
|
self.preview_dslr_ok = False
|
||||||
log.info("Camera deconnectee")
|
log.info("Camera deconnectee")
|
||||||
|
|
||||||
def capturer(self) -> Path | None:
|
def capturer(self) -> Path | None:
|
||||||
@@ -160,13 +177,16 @@ class Camera:
|
|||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
try:
|
try:
|
||||||
with self._gp_lock:
|
with self._gp_lock:
|
||||||
# Désactiver LiveView : miroir abaissé → AF phase-détection Canon disponible
|
if not self.camera:
|
||||||
|
log.error("Camera None pendant capture")
|
||||||
|
self.preview_dslr_ok = False
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
cfg = self.camera.get_config()
|
cfg = self.camera.get_config()
|
||||||
vf = cfg.get_child_by_name("viewfinder")
|
vf = cfg.get_child_by_name("viewfinder")
|
||||||
vf.set_value(0)
|
vf.set_value(0)
|
||||||
self.camera.set_config(cfg)
|
self.camera.set_config(cfg)
|
||||||
time.sleep(0.2) # Laisser le miroir descendre
|
time.sleep(0.2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
log.info(f"Déclenchement capture DSLR (tentative {attempt+1}/3)")
|
log.info(f"Déclenchement capture DSLR (tentative {attempt+1}/3)")
|
||||||
@@ -177,8 +197,8 @@ class Camera:
|
|||||||
)
|
)
|
||||||
tmp_path = str(chemin_dest) + ".tmp"
|
tmp_path = str(chemin_dest) + ".tmp"
|
||||||
fichier_camera.save(tmp_path)
|
fichier_camera.save(tmp_path)
|
||||||
self.activer_viewfinder() # Miroir relevé immédiatement après la prise
|
self.activer_viewfinder()
|
||||||
self._chauffer_liveview() # Absorbe la stabilisation LiveView ici (spinner visible), pas sur le countdown suivant
|
self._chauffer_liveview()
|
||||||
from PIL import ImageOps as PILImageOps
|
from PIL import ImageOps as PILImageOps
|
||||||
img = PILImageOps.exif_transpose(PILImage.open(tmp_path))
|
img = PILImageOps.exif_transpose(PILImage.open(tmp_path))
|
||||||
if img.width > 4000:
|
if img.width > 4000:
|
||||||
@@ -194,6 +214,11 @@ class Camera:
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
continue
|
continue
|
||||||
log.error(f"Erreur capture DSLR : {e}")
|
log.error(f"Erreur capture DSLR : {e}")
|
||||||
|
self.preview_dslr_ok = False
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Erreur fatale capture DSLR : {e}")
|
||||||
|
self.preview_dslr_ok = False
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _chauffer_liveview(self, tentatives: int = 8, pause: float = 0.15):
|
def _chauffer_liveview(self, tentatives: int = 8, pause: float = 0.15):
|
||||||
@@ -308,6 +333,8 @@ class Camera:
|
|||||||
def _preview_gphoto2(self) -> bytes | None:
|
def _preview_gphoto2(self) -> bytes | None:
|
||||||
try:
|
try:
|
||||||
with self._gp_lock:
|
with self._gp_lock:
|
||||||
|
if not self.camera:
|
||||||
|
return None
|
||||||
fichier = self.camera.capture_preview()
|
fichier = self.camera.capture_preview()
|
||||||
donnees = bytes(fichier.get_data_and_size())
|
donnees = bytes(fichier.get_data_and_size())
|
||||||
img = cv2.imdecode(np.frombuffer(donnees, dtype=np.uint8), cv2.IMREAD_COLOR)
|
img = cv2.imdecode(np.frombuffer(donnees, dtype=np.uint8), cv2.IMREAD_COLOR)
|
||||||
@@ -318,7 +345,7 @@ class Camera:
|
|||||||
_, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 75])
|
_, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 75])
|
||||||
return buf.tobytes()
|
return buf.tobytes()
|
||||||
return donnees
|
return donnees
|
||||||
except gp.GPhoto2Error as e:
|
except Exception as e:
|
||||||
log.error(f"Erreur preview DSLR : {e}")
|
log.error(f"Erreur preview DSLR : {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False):
|
|||||||
if dest.get("ftp", False):
|
if dest.get("ftp", False):
|
||||||
envoyer_ftp(chemin_photo, dest, sous_dossier)
|
envoyer_ftp(chemin_photo, dest, sous_dossier)
|
||||||
|
|
||||||
# Incrementer le compteur
|
# Incrementer le compteur (photos imprimees seulement)
|
||||||
|
if imprimee:
|
||||||
compteur = config.get("compteur", {})
|
compteur = config.get("compteur", {})
|
||||||
if compteur.get("actif", False):
|
if compteur.get("actif", False):
|
||||||
compteur["photos_prises"] = compteur.get("photos_prises", 0) + 1
|
compteur["photos_prises"] = compteur.get("photos_prises", 0) + 1
|
||||||
@@ -98,47 +99,43 @@ def envoyer_ftp(chemin_photo: Path, config_dest: dict, sous_dossier: str | None
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def envoyer_booth(chemin_photo: Path, config_booth: dict):
|
def _upload_booth(url: str, api_key: str, event_id: str, chemin_photo: Path) -> bool:
|
||||||
"""Envoie une photo vers la galerie live booth."""
|
|
||||||
url = config_booth.get("url", "").rstrip("/")
|
|
||||||
api_key = config_booth.get("api_key", "")
|
|
||||||
event_id = config_booth.get("event_id", "default")
|
|
||||||
|
|
||||||
if not url:
|
|
||||||
log.warning("Booth non configure (URL manquante)")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import mimetypes
|
|
||||||
boundary = "----BoothUpload"
|
boundary = "----BoothUpload"
|
||||||
filename = chemin_photo.name
|
filename = chemin_photo.name
|
||||||
|
|
||||||
with open(chemin_photo, "rb") as f:
|
with open(chemin_photo, "rb") as f:
|
||||||
file_data = f.read()
|
file_data = f.read()
|
||||||
|
|
||||||
body = (
|
body = (
|
||||||
f"--{boundary}\r\n"
|
f"--{boundary}\r\n"
|
||||||
f'Content-Disposition: form-data; name="photo"; filename="{filename}"\r\n'
|
f'Content-Disposition: form-data; name="photo"; filename="{filename}"\r\n'
|
||||||
f"Content-Type: image/jpeg\r\n\r\n"
|
f"Content-Type: image/jpeg\r\n\r\n"
|
||||||
).encode() + file_data + f"\r\n--{boundary}--\r\n".encode()
|
).encode() + file_data + f"\r\n--{boundary}--\r\n".encode()
|
||||||
|
req = Request(f"{url}/admin/gallery/{event_id}/upload", data=body, method="POST")
|
||||||
req = Request(
|
|
||||||
f"{url}/api/{event_id}/upload",
|
|
||||||
data=body,
|
|
||||||
method="POST",
|
|
||||||
)
|
|
||||||
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
|
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
|
||||||
req.add_header("X-Api-Key", api_key)
|
req.add_header("X-Api-Key", api_key)
|
||||||
|
|
||||||
with urlopen(req, timeout=10) as resp:
|
with urlopen(req, timeout=10) as resp:
|
||||||
if resp.status == 200:
|
return resp.status == 200
|
||||||
log.info(f"Photo envoyee au booth : {filename}")
|
|
||||||
return True
|
|
||||||
else:
|
def envoyer_booth(chemin_photo: Path, config_booth: dict):
|
||||||
log.error(f"Booth erreur HTTP {resp.status}")
|
"""Envoie une photo vers la galerie live booth."""
|
||||||
|
url = config_booth.get("url", "").rstrip("/")
|
||||||
|
url_tunnel = config_booth.get("url_tunnel", "").rstrip("/")
|
||||||
|
api_key = config_booth.get("api_key", "")
|
||||||
|
config = charger_config()
|
||||||
|
event_id = config.get("evenement", {}).get("event_id") or config_booth.get("event_id", "default")
|
||||||
|
|
||||||
|
if not url and not url_tunnel:
|
||||||
|
log.warning("Booth non configure (URL manquante)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
for tentative_url in [u for u in (url_tunnel, url) if u]:
|
||||||
|
try:
|
||||||
|
if _upload_booth(tentative_url, api_key, event_id, chemin_photo):
|
||||||
|
log.info(f"Photo envoyee au booth via {tentative_url} : {chemin_photo.name}")
|
||||||
|
return True
|
||||||
|
log.error(f"Booth erreur HTTP via {tentative_url}")
|
||||||
except (URLError, OSError) as e:
|
except (URLError, OSError) as e:
|
||||||
log.error(f"Erreur envoi booth : {e}")
|
log.warning(f"Echec envoi booth via {tentative_url} : {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -147,21 +144,19 @@ def recuperer_booth_password() -> dict:
|
|||||||
config = charger_config()
|
config = charger_config()
|
||||||
booth = config.get("booth", {})
|
booth = config.get("booth", {})
|
||||||
url = booth.get("url", "").rstrip("/")
|
url = booth.get("url", "").rstrip("/")
|
||||||
|
url_tunnel = booth.get("url_tunnel", "").rstrip("/")
|
||||||
api_key = booth.get("api_key", "")
|
api_key = booth.get("api_key", "")
|
||||||
event_id = booth.get("event_id", "default")
|
event_id = config.get("evenement", {}).get("event_id") or booth.get("event_id", "default")
|
||||||
|
|
||||||
if not url:
|
|
||||||
return {"password": None}
|
|
||||||
|
|
||||||
|
for tentative_url in [u for u in (url_tunnel, url) if u]:
|
||||||
try:
|
try:
|
||||||
req = Request(f"{url}/api/{event_id}/info")
|
req = Request(f"{tentative_url}/admin/gallery/{event_id}/info")
|
||||||
req.add_header("X-Api-Key", api_key)
|
req.add_header("X-Api-Key", api_key)
|
||||||
with urlopen(req, timeout=5) as resp:
|
with urlopen(req, timeout=5) as resp:
|
||||||
import json
|
import json
|
||||||
data = json.loads(resp.read())
|
return json.loads(resp.read())
|
||||||
return data
|
|
||||||
except (URLError, OSError) as e:
|
except (URLError, OSError) as e:
|
||||||
log.error(f"Erreur recuperation booth info : {e}")
|
log.warning(f"Echec recuperation booth info via {tentative_url} : {e}")
|
||||||
return {"password": None}
|
return {"password": None}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
114
backend/eclairage.py
Normal file
114
backend/eclairage.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"""Analyse de luminosité sur les visages détectés dans le preview DSLR.
|
||||||
|
|
||||||
|
Produit une note d'éclairage (0-100) et une recommandation (allumer/ok/sombre).
|
||||||
|
Échantillonne toutes les ~3 secondes pour ne pas charger le CPU.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
log = logging.getLogger("photobooth.eclairage")
|
||||||
|
|
||||||
|
_CASCADE_CANDIDATES = [
|
||||||
|
"/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml",
|
||||||
|
"/usr/share/opencv/haarcascades/haarcascade_frontalface_default.xml",
|
||||||
|
"/usr/local/share/opencv4/haarcascades/haarcascade_frontalface_default.xml",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
_CASCADE_PATH = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
|
||||||
|
except AttributeError:
|
||||||
|
_CASCADE_PATH = next((p for p in _CASCADE_CANDIDATES if Path(p).exists()), _CASCADE_CANDIDATES[0])
|
||||||
|
_face_cascade = cv2.CascadeClassifier(_CASCADE_PATH)
|
||||||
|
|
||||||
|
SEUIL_SOMBRE = 45
|
||||||
|
SEUIL_CORRECT = 65
|
||||||
|
|
||||||
|
_dernier_analyse: float = 0
|
||||||
|
_INTERVALLE = 3.0
|
||||||
|
_dernier_resultat: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def analyser_frame(jpeg_data: bytes) -> dict | None:
|
||||||
|
"""Analyse une frame JPEG du preview.
|
||||||
|
|
||||||
|
Retourne None si l'intervalle n'est pas écoulé (throttle).
|
||||||
|
Sinon retourne:
|
||||||
|
{
|
||||||
|
"score": int 0-100,
|
||||||
|
"action": "allumer" | "attention" | "ok",
|
||||||
|
"visages": int,
|
||||||
|
"detail": str,
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
global _dernier_analyse, _dernier_resultat
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - _dernier_analyse < _INTERVALLE:
|
||||||
|
return None
|
||||||
|
_dernier_analyse = now
|
||||||
|
|
||||||
|
try:
|
||||||
|
arr = np.frombuffer(jpeg_data, dtype=np.uint8)
|
||||||
|
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
||||||
|
if img is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||||
|
faces = _face_cascade.detectMultiScale(
|
||||||
|
gray, scaleFactor=1.2, minNeighbors=4, minSize=(60, 60)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(faces) == 0:
|
||||||
|
_dernier_resultat = {
|
||||||
|
"score": -1,
|
||||||
|
"action": "no_face",
|
||||||
|
"visages": 0,
|
||||||
|
"detail": "Aucun visage détecté",
|
||||||
|
}
|
||||||
|
return _dernier_resultat
|
||||||
|
|
||||||
|
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
||||||
|
scores = []
|
||||||
|
|
||||||
|
for (x, y, w, h) in faces:
|
||||||
|
pad_x = int(w * 0.1)
|
||||||
|
pad_y = int(h * 0.1)
|
||||||
|
x1 = max(0, x + pad_x)
|
||||||
|
y1 = max(0, y + pad_y)
|
||||||
|
x2 = min(img.shape[1], x + w - pad_x)
|
||||||
|
y2 = min(img.shape[0], y + h - pad_y)
|
||||||
|
roi_v = hsv[y1:y2, x1:x2, 2]
|
||||||
|
score = int(np.mean(roi_v) / 2.55)
|
||||||
|
scores.append(score)
|
||||||
|
|
||||||
|
score_moyen = int(np.mean(scores))
|
||||||
|
|
||||||
|
if score_moyen < SEUIL_SOMBRE:
|
||||||
|
action = "allumer"
|
||||||
|
detail = f"Visages sombres ({score_moyen}/100)"
|
||||||
|
elif score_moyen < SEUIL_CORRECT:
|
||||||
|
action = "attention"
|
||||||
|
detail = f"Éclairage limite ({score_moyen}/100)"
|
||||||
|
else:
|
||||||
|
action = "ok"
|
||||||
|
detail = f"Éclairage correct ({score_moyen}/100)"
|
||||||
|
|
||||||
|
_dernier_resultat = {
|
||||||
|
"score": score_moyen,
|
||||||
|
"action": action,
|
||||||
|
"visages": len(faces),
|
||||||
|
"detail": detail,
|
||||||
|
}
|
||||||
|
return _dernier_resultat
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Erreur analyse éclairage : {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def dernier_resultat() -> dict | None:
|
||||||
|
return _dernier_resultat
|
||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -36,6 +37,7 @@ from backend.evenements import (
|
|||||||
lister_cadres_event, set_cadre_event, cadre_actif_pour_impression,
|
lister_cadres_event, set_cadre_event, cadre_actif_pour_impression,
|
||||||
)
|
)
|
||||||
from backend.evenements import DOSSIER_EVENEMENTS
|
from backend.evenements import DOSSIER_EVENEMENTS
|
||||||
|
from backend.eclairage import analyser_frame as analyser_eclairage, dernier_resultat as dernier_eclairage
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s")
|
||||||
log = logging.getLogger("photobooth")
|
log = logging.getLogger("photobooth")
|
||||||
@@ -63,6 +65,7 @@ def _thread_preview():
|
|||||||
Tourne TOUJOURS quand le DSLR est connecte pour maintenir le miroir leve."""
|
Tourne TOUJOURS quand le DSLR est connecte pour maintenir le miroir leve."""
|
||||||
global _derniere_frame_preview
|
global _derniere_frame_preview
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
if capture_en_cours or camera.mode != "gphoto2" or not camera.connectee:
|
if capture_en_cours or camera.mode != "gphoto2" or not camera.connectee:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
continue
|
continue
|
||||||
@@ -74,6 +77,9 @@ def _thread_preview():
|
|||||||
with _preview_lock:
|
with _preview_lock:
|
||||||
_derniere_frame_preview = None
|
_derniere_frame_preview = None
|
||||||
time.sleep(0.033) # ~30 fps max
|
time.sleep(0.033) # ~30 fps max
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"_thread_preview crash: {e}")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
async def _pusher_preview():
|
async def _pusher_preview():
|
||||||
@@ -103,6 +109,10 @@ async def _pusher_preview():
|
|||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
b64 = await loop.run_in_executor(None, lambda d=donnees: base64.b64encode(d).decode("ascii"))
|
b64 = await loop.run_in_executor(None, lambda d=donnees: base64.b64encode(d).decode("ascii"))
|
||||||
await diffuser_ws({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"})
|
await diffuser_ws({"type": "preview", "image": f"data:image/jpeg;base64,{b64}"})
|
||||||
|
if not capture_en_cours:
|
||||||
|
eclairage = await loop.run_in_executor(None, analyser_eclairage, donnees)
|
||||||
|
if eclairage is not None:
|
||||||
|
await diffuser_ws({"type": "eclairage", **eclairage})
|
||||||
|
|
||||||
def _usb_reset_canon():
|
def _usb_reset_canon():
|
||||||
"""Reset USB ioctl du Canon EOS (vendor 04a9) pour débloquer un port stall."""
|
"""Reset USB ioctl du Canon EOS (vendor 04a9) pour débloquer un port stall."""
|
||||||
@@ -206,7 +216,7 @@ async def surveiller_dslr():
|
|||||||
_echecs_connexion = 0
|
_echecs_connexion = 0
|
||||||
else:
|
else:
|
||||||
_dslr_erreurs += 1
|
_dslr_erreurs += 1
|
||||||
if _dslr_erreurs >= 3 and not _preview_actif:
|
if _dslr_erreurs >= 3:
|
||||||
log.warning(f"camera_erreur : DSLR preview KO depuis {_dslr_erreurs * 5}s, reconnexion forcee...")
|
log.warning(f"camera_erreur : DSLR preview KO depuis {_dslr_erreurs * 5}s, reconnexion forcee...")
|
||||||
_dslr_erreurs = 0
|
_dslr_erreurs = 0
|
||||||
_echecs_connexion += 1
|
_echecs_connexion += 1
|
||||||
@@ -264,6 +274,24 @@ def _appliquer_config_camera():
|
|||||||
camera.configurer_flash(flash_integre)
|
camera.configurer_flash(flash_integre)
|
||||||
|
|
||||||
|
|
||||||
|
async def _watchdog_systemd():
|
||||||
|
"""Notifie systemd que le service est vivant + surveille la santé."""
|
||||||
|
try:
|
||||||
|
import socket
|
||||||
|
addr = os.environ.get("NOTIFY_SOCKET")
|
||||||
|
if not addr:
|
||||||
|
return
|
||||||
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
if addr[0] == "@":
|
||||||
|
addr = "\0" + addr[1:]
|
||||||
|
sock.sendto(b"READY=1", addr)
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
sock.sendto(b"WATCHDOG=1", addr)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Demarrage et arret de l'application."""
|
"""Demarrage et arret de l'application."""
|
||||||
@@ -278,10 +306,12 @@ async def lifespan(app: FastAPI):
|
|||||||
task_dslr = asyncio.create_task(surveiller_dslr())
|
task_dslr = asyncio.create_task(surveiller_dslr())
|
||||||
task_push = asyncio.create_task(_pusher_preview())
|
task_push = asyncio.create_task(_pusher_preview())
|
||||||
task_spool = asyncio.create_task(tache_spool_demarrage())
|
task_spool = asyncio.create_task(tache_spool_demarrage())
|
||||||
|
task_watchdog = asyncio.create_task(_watchdog_systemd())
|
||||||
yield
|
yield
|
||||||
task_dslr.cancel()
|
task_dslr.cancel()
|
||||||
task_push.cancel()
|
task_push.cancel()
|
||||||
task_spool.cancel()
|
task_spool.cancel()
|
||||||
|
task_watchdog.cancel()
|
||||||
log.info("Arret du photobooth")
|
log.info("Arret du photobooth")
|
||||||
camera.deconnecter()
|
camera.deconnecter()
|
||||||
|
|
||||||
@@ -302,6 +332,18 @@ async def page_admin():
|
|||||||
return FileResponse(str(RACINE / "frontend" / "index.html"))
|
return FileResponse(str(RACINE / "frontend" / "index.html"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
async def api_health():
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"camera": camera.mode if camera.connectee else "disconnected",
|
||||||
|
"clients": len(clients_ws),
|
||||||
|
"uptime": int(time.time() - _start_time),
|
||||||
|
}
|
||||||
|
|
||||||
|
_start_time = time.time()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def page_principale(request: Request):
|
async def page_principale(request: Request):
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
@@ -409,6 +451,9 @@ async def api_capturer():
|
|||||||
capture_en_cours = False
|
capture_en_cours = False
|
||||||
log.info(f"Capture terminée : {chemin}")
|
log.info(f"Capture terminée : {chemin}")
|
||||||
if chemin is None:
|
if chemin is None:
|
||||||
|
if camera.mode == "gphoto2" and not camera.preview_dslr_ok:
|
||||||
|
log.warning("Capture echouee + preview KO → reconnexion DSLR imminente")
|
||||||
|
await diffuser_ws({"type": "camera_erreur", "message": "Erreur capture — reconnexion en cours"})
|
||||||
return JSONResponse({"erreur": "Echec capture"}, status_code=500)
|
return JSONResponse({"erreur": "Echec capture"}, status_code=500)
|
||||||
nom = chemin.name
|
nom = chemin.name
|
||||||
|
|
||||||
@@ -547,6 +592,14 @@ async def api_camera_statut():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/eclairage")
|
||||||
|
async def api_eclairage():
|
||||||
|
resultat = dernier_eclairage()
|
||||||
|
if resultat is None:
|
||||||
|
return {"score": -1, "action": "no_data", "detail": "Aucune analyse disponible"}
|
||||||
|
return resultat
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/camera/debug")
|
@app.get("/api/camera/debug")
|
||||||
async def api_camera_debug():
|
async def api_camera_debug():
|
||||||
"""Diagnostic complet de la caméra (preview, viewfinder, erreurs gphoto2)."""
|
"""Diagnostic complet de la caméra (preview, viewfinder, erreurs gphoto2)."""
|
||||||
|
|||||||
@@ -258,14 +258,15 @@ def imprimer(
|
|||||||
largeur, hauteur = fmt["px"]
|
largeur, hauteur = fmt["px"]
|
||||||
|
|
||||||
fmt_base = format_papier.replace("-2up", "")
|
fmt_base = format_papier.replace("-2up", "")
|
||||||
cadre = cadre_override if cadre_override is not None else conf_imp.get("cadres_actifs", {}).get(fmt_base)
|
is_strip = "-2up" in format_papier
|
||||||
|
cadre = cadre_override if cadre_override is not None else (None if is_strip else conf_imp.get("cadres_actifs", {}).get(fmt_base))
|
||||||
orientation = conf_imp.get("orientations", {}).get(fmt_base, "portrait")
|
orientation = conf_imp.get("orientations", {}).get(fmt_base, "portrait")
|
||||||
|
|
||||||
if orientation == "paysage" and hauteur > largeur:
|
if orientation == "paysage" and hauteur > largeur:
|
||||||
largeur, hauteur = hauteur, largeur
|
largeur, hauteur = hauteur, largeur
|
||||||
|
|
||||||
event_id = config.get("evenement", {}).get("event_id")
|
event_id = config.get("evenement", {}).get("event_id")
|
||||||
if event_id and cadre_override is None:
|
if event_id and cadre_override is None and not is_strip:
|
||||||
from backend.evenements import cadre_actif_pour_impression
|
from backend.evenements import cadre_actif_pour_impression
|
||||||
ev_cadre, ev_mode = cadre_actif_pour_impression(fmt_base)
|
ev_cadre, ev_mode = cadre_actif_pour_impression(fmt_base)
|
||||||
if ev_mode == "impose" and ev_cadre:
|
if ev_mode == "impose" and ev_cadre:
|
||||||
|
|||||||
@@ -373,6 +373,7 @@
|
|||||||
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
||||||
<button class="onglet" data-onglet="evenement">Evenement</button>
|
<button class="onglet" data-onglet="evenement">Evenement</button>
|
||||||
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
||||||
|
<button class="onglet" data-onglet="eclairage" onclick="demarrerEclairageLive()">Eclairage</button>
|
||||||
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -691,6 +692,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panneau Infos systeme -->
|
<!-- Panneau Infos systeme -->
|
||||||
|
<div class="admin-panneau" id="panneau-eclairage">
|
||||||
|
<div style="display:flex;gap:1rem;align-items:flex-start;flex-wrap:wrap">
|
||||||
|
<div style="flex:1;min-width:280px">
|
||||||
|
<img id="eclairage-preview" style="width:100%;border-radius:8px;background:#111" alt="Preview">
|
||||||
|
</div>
|
||||||
|
<div style="flex:0 0 200px;text-align:center">
|
||||||
|
<div id="eclairage-score-box" style="font-size:3rem;font-weight:800;padding:1.5rem;border-radius:16px;background:#222;margin-bottom:1rem">--</div>
|
||||||
|
<div id="eclairage-action" style="font-size:1.2rem;font-weight:600;margin-bottom:.5rem">--</div>
|
||||||
|
<div id="eclairage-visages" style="font-size:.9rem;color:#aaa">--</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="admin-panneau" id="panneau-infos">
|
<div class="admin-panneau" id="panneau-infos">
|
||||||
<div class="infos-systeme-header">
|
<div class="infos-systeme-header">
|
||||||
<h3>Informations systeme</h3>
|
<h3>Informations systeme</h3>
|
||||||
@@ -772,9 +786,26 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/websocket.js?v=3"></script>
|
<!-- Ecran de pause (deconnexion backend) -->
|
||||||
<script src="/js/app.js?v=6"></script>
|
<div id="ecran-pause" style="display:none;position:fixed;inset:0;z-index:9999;background:#0a0a0f;flex-direction:column;align-items:center;justify-content:center;text-align:center">
|
||||||
<script src="/js/camera.js?v=13"></script>
|
<div style="font-size:6rem;animation:pause-bob 2s ease-in-out infinite">☕</div>
|
||||||
|
<h1 style="color:#fff;font-size:2.2rem;margin:1rem 0 .5rem;font-weight:700">Petite pause...</h1>
|
||||||
|
<p style="color:#888;font-size:1.2rem;max-width:400px;line-height:1.5">Le photobooth revient dans un instant.<br>Pas de panique, on s'occupe de tout !</p>
|
||||||
|
<div style="margin-top:2rem;display:flex;gap:8px">
|
||||||
|
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out infinite"></span>
|
||||||
|
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out .2s infinite"></span>
|
||||||
|
<span class="pause-dot" style="width:12px;height:12px;border-radius:50%;background:#e91e63;animation:pause-dot .6s ease-in-out .4s infinite"></span>
|
||||||
|
</div>
|
||||||
|
<p id="pause-status" style="color:#555;font-size:.85rem;margin-top:2rem">Reconnexion en cours...</p>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
@keyframes pause-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-20px)} }
|
||||||
|
@keyframes pause-dot { 0%,100%{opacity:.2;transform:scale(.8)} 50%{opacity:1;transform:scale(1.2)} }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="/js/websocket.js?v=16"></script>
|
||||||
|
<script src="/js/app.js?v=15"></script>
|
||||||
|
<script src="/js/camera.js?v=15"></script>
|
||||||
<script src="/js/effects.js?v=4"></script>
|
<script src="/js/effects.js?v=4"></script>
|
||||||
<script src="/js/gallery.js?v=3"></script>
|
<script src="/js/gallery.js?v=3"></script>
|
||||||
<script src="/js/share.js?v=7"></script>
|
<script src="/js/share.js?v=7"></script>
|
||||||
|
|||||||
@@ -118,9 +118,34 @@ function allerA(ecran) {
|
|||||||
chargerGalerie();
|
chargerGalerie();
|
||||||
} else if (ecran === 'admin') {
|
} else if (ecran === 'admin') {
|
||||||
chargerAdmin();
|
chargerAdmin();
|
||||||
|
demarrerTimeoutAdmin();
|
||||||
|
} else {
|
||||||
|
arreterTimeoutAdmin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _adminTimeoutId = null;
|
||||||
|
const ADMIN_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
|
function demarrerTimeoutAdmin() {
|
||||||
|
arreterTimeoutAdmin();
|
||||||
|
_adminTimeoutId = setTimeout(() => { allerA('accueil'); }, ADMIN_TIMEOUT_MS);
|
||||||
|
const ecranAdmin = document.getElementById('ecran-admin');
|
||||||
|
ecranAdmin.removeEventListener('pointerdown', _resetAdminTimeout);
|
||||||
|
ecranAdmin.addEventListener('pointerdown', _resetAdminTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _resetAdminTimeout() {
|
||||||
|
if (_adminTimeoutId) {
|
||||||
|
clearTimeout(_adminTimeoutId);
|
||||||
|
_adminTimeoutId = setTimeout(() => { allerA('accueil'); }, ADMIN_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function arreterTimeoutAdmin() {
|
||||||
|
if (_adminTimeoutId) { clearTimeout(_adminTimeoutId); _adminTimeoutId = null; }
|
||||||
|
}
|
||||||
|
|
||||||
function recommencer() {
|
function recommencer() {
|
||||||
photosSession = [];
|
photosSession = [];
|
||||||
photoFinale = null;
|
photoFinale = null;
|
||||||
|
|||||||
@@ -48,13 +48,19 @@ function lancerPreview() {
|
|||||||
if (previewActif) return;
|
if (previewActif) return;
|
||||||
previewActif = true;
|
previewActif = true;
|
||||||
afficherErreurPreview(false);
|
afficherErreurPreview(false);
|
||||||
// Flou le temps que la 1ere frame fraiche arrive (le miroir DSLR doit remonter
|
|
||||||
// apres la capture precedente) : evite l'impression d'image figee sur l'ancienne photo.
|
|
||||||
document.getElementById('img-preview')?.classList.add('preview-chargement');
|
document.getElementById('img-preview')?.classList.add('preview-chargement');
|
||||||
wsEnvoyer({ type: 'preview_start' });
|
wsEnvoyer({ type: 'preview_start' });
|
||||||
resetPreviewTimeout();
|
resetPreviewTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function demarrerEclairageLive() {
|
||||||
|
if (!previewActif) {
|
||||||
|
previewActif = true;
|
||||||
|
wsEnvoyer({ type: 'preview_start' });
|
||||||
|
resetPreviewTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function arreterPreview() {
|
function arreterPreview() {
|
||||||
if (!previewActif) return;
|
if (!previewActif) return;
|
||||||
previewActif = false;
|
previewActif = false;
|
||||||
@@ -81,6 +87,10 @@ wsOnMessage('preview', (msg) => {
|
|||||||
};
|
};
|
||||||
img.src = msg.image;
|
img.src = msg.image;
|
||||||
}
|
}
|
||||||
|
const adminPrev = document.getElementById('eclairage-preview');
|
||||||
|
if (adminPrev && document.getElementById('panneau-eclairage')?.classList.contains('actif')) {
|
||||||
|
adminPrev.src = msg.image;
|
||||||
|
}
|
||||||
afficherErreurPreview(false);
|
afficherErreurPreview(false);
|
||||||
resetPreviewTimeout();
|
resetPreviewTimeout();
|
||||||
});
|
});
|
||||||
@@ -173,6 +183,36 @@ wsOnMessage('camera_ok', () => {
|
|||||||
afficherErreurPreview(false);
|
afficherErreurPreview(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
wsOnMessage('eclairage', (msg) => {
|
||||||
|
if (captureEnCours) return;
|
||||||
|
// Admin panel eclairage
|
||||||
|
const scoreBox = document.getElementById('eclairage-score-box');
|
||||||
|
const actionEl = document.getElementById('eclairage-action');
|
||||||
|
const visagesEl = document.getElementById('eclairage-visages');
|
||||||
|
if (scoreBox) {
|
||||||
|
const score = msg.score >= 0 ? msg.score : '--';
|
||||||
|
scoreBox.textContent = score;
|
||||||
|
if (msg.action === 'allumer') {
|
||||||
|
scoreBox.style.background = '#dc2626'; scoreBox.style.color = '#fff';
|
||||||
|
actionEl.textContent = 'ALLUMER LA LUMIERE';
|
||||||
|
actionEl.style.color = '#f87171';
|
||||||
|
} else if (msg.action === 'attention') {
|
||||||
|
scoreBox.style.background = '#d97706'; scoreBox.style.color = '#fff';
|
||||||
|
actionEl.textContent = 'Eclairage faible';
|
||||||
|
actionEl.style.color = '#fbbf24';
|
||||||
|
} else if (msg.action === 'ok') {
|
||||||
|
scoreBox.style.background = '#16a34a'; scoreBox.style.color = '#fff';
|
||||||
|
actionEl.textContent = 'Eclairage OK';
|
||||||
|
actionEl.style.color = '#4ade80';
|
||||||
|
} else {
|
||||||
|
scoreBox.style.background = '#222'; scoreBox.style.color = '#888';
|
||||||
|
actionEl.textContent = 'Aucun visage';
|
||||||
|
actionEl.style.color = '#888';
|
||||||
|
}
|
||||||
|
visagesEl.textContent = msg.visages > 0 ? msg.visages + ' visage' + (msg.visages > 1 ? 's' : '') + ' detecte' + (msg.visages > 1 ? 's' : '') : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- Capture ---
|
// --- Capture ---
|
||||||
|
|
||||||
async function lancerCapture() {
|
async function lancerCapture() {
|
||||||
|
|||||||
@@ -1,30 +1,38 @@
|
|||||||
/* Communication WebSocket avec le backend */
|
/* Communication WebSocket avec le backend — reconnexion résiliente */
|
||||||
|
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let wsReconnectTimer = null;
|
let _wsReconnectDelay = 1000;
|
||||||
|
let _wsWasConnected = false;
|
||||||
const wsCallbacks = {};
|
const wsCallbacks = {};
|
||||||
|
|
||||||
function wsConnecter() {
|
function wsConnecter() {
|
||||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
try { ws = new WebSocket(`${proto}//${location.host}/ws`); } catch(e) { _wsScheduleReconnect(); return; }
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('WebSocket connecte');
|
console.log('WebSocket connecte');
|
||||||
if (wsReconnectTimer) {
|
_wsReconnectDelay = 1000;
|
||||||
clearInterval(wsReconnectTimer);
|
if (_wsWasConnected) {
|
||||||
wsReconnectTimer = null;
|
_masquerPause();
|
||||||
|
// Recharger la config pour resynchroniser l'état
|
||||||
|
if (typeof chargerConfig === 'function') chargerConfig();
|
||||||
}
|
}
|
||||||
|
_wsWasConnected = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
const cbs = wsCallbacks[msg.type];
|
const cbs = wsCallbacks[msg.type];
|
||||||
if (cbs) cbs.forEach(cb => cb(msg));
|
if (cbs) cbs.forEach(cb => cb(msg));
|
||||||
|
} catch(e) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
console.log('WebSocket deconnecte, rechargement dans 5s...');
|
console.log('WebSocket deconnecte');
|
||||||
setTimeout(() => { location.reload(); }, 5000);
|
ws = null;
|
||||||
|
_afficherPause();
|
||||||
|
_wsScheduleReconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
@@ -32,6 +40,26 @@ function wsConnecter() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _wsScheduleReconnect() {
|
||||||
|
const delay = Math.min(_wsReconnectDelay, 10000);
|
||||||
|
_wsReconnectDelay = Math.min(_wsReconnectDelay * 1.5, 10000);
|
||||||
|
setTimeout(wsConnecter, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _afficherPause() {
|
||||||
|
const el = document.getElementById('ecran-pause');
|
||||||
|
if (el) el.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _masquerPause() {
|
||||||
|
const el = document.getElementById('ecran-pause');
|
||||||
|
if (el) { el.style.display = 'none'; }
|
||||||
|
// Relancer le preview si on était sur l'accueil
|
||||||
|
if (typeof lancerPreview === 'function' && document.getElementById('ecran-accueil')?.classList.contains('actif')) {
|
||||||
|
lancerPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function wsEnvoyer(msg) {
|
function wsEnvoyer(msg) {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify(msg));
|
ws.send(JSON.stringify(msg));
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Photobooth
|
Description=Photobooth backend
|
||||||
After=network.target graphical.target
|
After=network.target graphical.target
|
||||||
|
Wants=graphical.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=notify
|
||||||
User=jules
|
User=jules
|
||||||
WorkingDirectory=/home/jules/Documents/Projet/photobooth-app
|
WorkingDirectory=/home/jules/photobooth
|
||||||
ExecStart=/home/jules/Documents/Projet/photobooth-app/scripts/start.sh
|
ExecStart=/home/jules/photobooth/scripts/start.sh
|
||||||
|
ExecStopPost=/bin/bash -c 'pkill -9 -f "python.*backend.main" 2>/dev/null; sleep 1'
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=3
|
||||||
|
WatchdogSec=30
|
||||||
|
KillMode=control-group
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
TimeoutStopSec=10
|
||||||
|
StartLimitIntervalSec=120
|
||||||
|
StartLimitBurst=10
|
||||||
Environment=DISPLAY=:0
|
Environment=DISPLAY=:0
|
||||||
|
Environment=XAUTHORITY=/home/jules/.Xauthority
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=graphical.target
|
WantedBy=graphical.target
|
||||||
|
|||||||
6
scripts/wifi-watchdog.service
Normal file
6
scripts/wifi-watchdog.service
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=WiFi watchdog check
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/home/jules/photobooth/scripts/wifi-watchdog.sh
|
||||||
28
scripts/wifi-watchdog.sh
Normal file
28
scripts/wifi-watchdog.sh
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Surveille la connexion WiFi et la rétablit si elle tombe.
|
||||||
|
# Conçu pour Surface Pro (wlan0) — lancé par cron ou systemd timer.
|
||||||
|
|
||||||
|
IFACE="wlan0"
|
||||||
|
GATEWAY=$(ip route show default dev "$IFACE" 2>/dev/null | awk '{print $3}' | head -1)
|
||||||
|
LOG_TAG="wifi-watchdog"
|
||||||
|
|
||||||
|
if [ -z "$GATEWAY" ]; then
|
||||||
|
logger -t "$LOG_TAG" "Pas de gateway sur $IFACE, tentative reconnexion..."
|
||||||
|
nmcli device connect "$IFACE" 2>&1 | logger -t "$LOG_TAG"
|
||||||
|
sleep 5
|
||||||
|
GATEWAY=$(ip route show default dev "$IFACE" 2>/dev/null | awk '{print $3}' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$GATEWAY" ]; then
|
||||||
|
if ! ping -c 2 -W 3 -I "$IFACE" "$GATEWAY" > /dev/null 2>&1; then
|
||||||
|
logger -t "$LOG_TAG" "Ping gateway $GATEWAY echoue, reconnexion WiFi..."
|
||||||
|
nmcli device disconnect "$IFACE" 2>&1 | logger -t "$LOG_TAG"
|
||||||
|
sleep 2
|
||||||
|
nmcli device connect "$IFACE" 2>&1 | logger -t "$LOG_TAG"
|
||||||
|
sleep 5
|
||||||
|
if ! ping -c 2 -W 3 -I "$IFACE" "$GATEWAY" > /dev/null 2>&1; then
|
||||||
|
logger -t "$LOG_TAG" "Toujours pas de connexion, restart NetworkManager..."
|
||||||
|
systemctl restart NetworkManager
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
9
scripts/wifi-watchdog.timer
Normal file
9
scripts/wifi-watchdog.timer
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=WiFi watchdog - check every minute
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=30
|
||||||
|
OnUnitActiveSec=60
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in New Issue
Block a user