Compare commits
4 Commits
0477831679
...
d3a71a9866
| Author | SHA1 | Date | |
|---|---|---|---|
| d3a71a9866 | |||
| 0b4c5f9430 | |||
| 1364b62297 | |||
| 0a918666b3 |
@@ -82,9 +82,11 @@ class Camera:
|
|||||||
try:
|
try:
|
||||||
self.camera = gp.Camera()
|
self.camera = gp.Camera()
|
||||||
self.camera.init()
|
self.camera.init()
|
||||||
|
# Viewfinder activé AVANT de rendre la caméra visible au thread preview
|
||||||
|
self._activer_viewfinder_init()
|
||||||
self.connectee = True
|
self.connectee = True
|
||||||
self.mode = "gphoto2"
|
self.mode = "gphoto2"
|
||||||
log.info("Camera DSLR connectee")
|
log.info("Camera DSLR connectee (LiveView actif)")
|
||||||
return True
|
return True
|
||||||
except gp.GPhoto2Error as e:
|
except gp.GPhoto2Error as e:
|
||||||
log.warning(f"Pas de DSLR : {e}")
|
log.warning(f"Pas de DSLR : {e}")
|
||||||
@@ -157,7 +159,9 @@ class Camera:
|
|||||||
)
|
)
|
||||||
tmp_path = str(chemin_dest) + ".tmp"
|
tmp_path = str(chemin_dest) + ".tmp"
|
||||||
fichier_camera.save(tmp_path)
|
fichier_camera.save(tmp_path)
|
||||||
img = PILImage.open(tmp_path)
|
self.activer_viewfinder() # Miroir relevé immédiatement après la prise
|
||||||
|
from PIL import ImageOps as PILImageOps
|
||||||
|
img = PILImageOps.exif_transpose(PILImage.open(tmp_path))
|
||||||
if img.width > 4000:
|
if img.width > 4000:
|
||||||
ratio = 4000 / img.width
|
ratio = 4000 / img.width
|
||||||
img = img.resize((4000, int(img.height * ratio)), PILImage.LANCZOS)
|
img = img.resize((4000, int(img.height * ratio)), PILImage.LANCZOS)
|
||||||
@@ -198,36 +202,36 @@ class Camera:
|
|||||||
else:
|
else:
|
||||||
return None # Pas de simulation ni webcam en dehors du mode dedie
|
return None # Pas de simulation ni webcam en dehors du mode dedie
|
||||||
|
|
||||||
def activer_viewfinder(self):
|
def _activer_viewfinder_init(self):
|
||||||
"""Active le LiveView sur les Canon."""
|
"""Active le LiveView pendant l'init (pas de lock, appelé avant que le thread démarre)."""
|
||||||
if self.mode != "gphoto2" or not self.connectee:
|
|
||||||
return
|
|
||||||
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(1)
|
vf.set_value(1)
|
||||||
self.camera.set_config(cfg)
|
self.camera.set_config(cfg)
|
||||||
log.info("Viewfinder activé")
|
log.info("Viewfinder activé (init)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"Viewfinder non supporté : {e}")
|
log.warning(f"Viewfinder non supporté : {e}")
|
||||||
|
|
||||||
def desactiver_viewfinder(self):
|
def activer_viewfinder(self):
|
||||||
"""Désactive le LiveView."""
|
"""Active le LiveView (miroir levé) depuis le thread — thread-safe."""
|
||||||
if self.mode != "gphoto2" or not self.connectee:
|
if self.mode != "gphoto2" or not self.connectee:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
cfg = self.camera.get_config()
|
with self._gp_lock:
|
||||||
vf = cfg.get_child_by_name("viewfinder")
|
cfg = self.camera.get_config()
|
||||||
vf.set_value(0)
|
vf = cfg.get_child_by_name("viewfinder")
|
||||||
self.camera.set_config(cfg)
|
vf.set_value(1)
|
||||||
log.info("Viewfinder désactivé")
|
self.camera.set_config(cfg)
|
||||||
|
log.info("Viewfinder activé")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"Viewfinder non supporté : {e}")
|
log.warning(f"Viewfinder non supporté : {e}")
|
||||||
|
|
||||||
def _preview_gphoto2(self) -> bytes | None:
|
def _preview_gphoto2(self) -> bytes | None:
|
||||||
try:
|
try:
|
||||||
fichier = self.camera.capture_preview()
|
with self._gp_lock:
|
||||||
donnees = bytes(fichier.get_data_and_size())
|
fichier = self.camera.capture_preview()
|
||||||
|
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)
|
||||||
if img is not None:
|
if img is not None:
|
||||||
h, w = img.shape[:2]
|
h, w = img.shape[:2]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ except ImportError:
|
|||||||
gp = None
|
gp = None
|
||||||
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
|
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
|
||||||
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, lister_cadres, FILTRES
|
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, lister_cadres, FILTRES
|
||||||
from backend.collage import creer_strip, creer_collage, creer_impression_strip
|
from backend.collage import creer_strip, creer_collage
|
||||||
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password
|
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password
|
||||||
from backend.printer import lister_imprimantes, imprimer
|
from backend.printer import lister_imprimantes, imprimer
|
||||||
from backend.mailer import envoyer_photo
|
from backend.mailer import envoyer_photo
|
||||||
@@ -48,11 +48,16 @@ _preview_lock = threading.Lock()
|
|||||||
_loop_principal: asyncio.AbstractEventLoop | None = None # loop asyncio principal
|
_loop_principal: asyncio.AbstractEventLoop | None = None # loop asyncio principal
|
||||||
|
|
||||||
|
|
||||||
|
_preview_none_count = 0
|
||||||
|
_PREVIEW_GRACE_FRAMES = 15 # ~500ms avant de declarer une erreur
|
||||||
|
|
||||||
|
|
||||||
def _thread_preview():
|
def _thread_preview():
|
||||||
"""Thread de fond : capture les frames DSLR en continu et les stocke."""
|
"""Thread de fond : capture les frames DSLR en continu.
|
||||||
global _derniere_frame_preview, _preview_actif
|
Tourne TOUJOURS quand le DSLR est connecte pour maintenir le miroir leve."""
|
||||||
|
global _derniere_frame_preview
|
||||||
while True:
|
while True:
|
||||||
if not _preview_actif or capture_en_cours:
|
if capture_en_cours or camera.mode != "gphoto2" or not camera.connectee:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@@ -67,20 +72,26 @@ def _thread_preview():
|
|||||||
|
|
||||||
async def _pusher_preview():
|
async def _pusher_preview():
|
||||||
"""Tache asyncio : pousse la derniere frame a tous les clients WS abonnes."""
|
"""Tache asyncio : pousse la derniere frame a tous les clients WS abonnes."""
|
||||||
global _preview_actif
|
global _preview_actif, _preview_none_count
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(0.05) # 20 fps max envoye aux clients
|
await asyncio.sleep(0.05) # 20 fps max envoye aux clients
|
||||||
|
if not _preview_actif:
|
||||||
|
_preview_none_count = 0
|
||||||
|
continue
|
||||||
if not clients_ws:
|
if not clients_ws:
|
||||||
_preview_actif = False
|
_preview_actif = False
|
||||||
continue
|
continue
|
||||||
with _preview_lock:
|
with _preview_lock:
|
||||||
donnees = _derniere_frame_preview
|
donnees = _derniere_frame_preview
|
||||||
if donnees is None:
|
if donnees is None:
|
||||||
# Signaler l'erreur si le DSLR est sense etre connecte
|
_preview_none_count += 1
|
||||||
if camera.mode == "gphoto2" and _preview_actif:
|
# Grace period : attendre plusieurs frames avant de signaler l'erreur
|
||||||
|
if camera.mode == "gphoto2" and _preview_none_count > _PREVIEW_GRACE_FRAMES:
|
||||||
await diffuser_ws({"type": "camera_erreur", "message": "DSLR ne repond pas au preview"})
|
await diffuser_ws({"type": "camera_erreur", "message": "DSLR ne repond pas au preview"})
|
||||||
_preview_actif = False
|
_preview_actif = False
|
||||||
|
_preview_none_count = 0
|
||||||
continue
|
continue
|
||||||
|
_preview_none_count = 0
|
||||||
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}"})
|
||||||
@@ -90,7 +101,8 @@ async def surveiller_dslr():
|
|||||||
global _dslr_erreurs
|
global _dslr_erreurs
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
if capture_en_cours or _preview_actif:
|
# Ne pas interférer pendant que le thread preview utilise la camera
|
||||||
|
if capture_en_cours or (camera.mode == "gphoto2" and camera.connectee):
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
if GPHOTO2_DISPONIBLE:
|
if GPHOTO2_DISPONIBLE:
|
||||||
@@ -384,6 +396,38 @@ async def api_camera_statut():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/camera/debug")
|
||||||
|
async def api_camera_debug():
|
||||||
|
"""Diagnostic complet de la caméra (preview, viewfinder, erreurs gphoto2)."""
|
||||||
|
info = {
|
||||||
|
"mode": camera.mode,
|
||||||
|
"connectee": camera.connectee,
|
||||||
|
"preview_actif": _preview_actif,
|
||||||
|
"derniere_frame_ok": _derniere_frame_preview is not None,
|
||||||
|
"preview_none_count": _preview_none_count,
|
||||||
|
"capture_en_cours": capture_en_cours,
|
||||||
|
}
|
||||||
|
if camera.mode == "gphoto2" and camera.connectee:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
try:
|
||||||
|
frame = await loop.run_in_executor(None, camera._preview_gphoto2)
|
||||||
|
info["test_preview"] = "ok" if frame else "retourne None"
|
||||||
|
info["test_preview_taille"] = len(frame) if frame else 0
|
||||||
|
except Exception as e:
|
||||||
|
info["test_preview"] = f"ERREUR : {e}"
|
||||||
|
try:
|
||||||
|
cfg_names = []
|
||||||
|
with camera._gp_lock:
|
||||||
|
cfg = camera.camera.get_config()
|
||||||
|
for i in range(cfg.count_children()):
|
||||||
|
child = cfg.get_child(i)
|
||||||
|
cfg_names.append(child.get_name())
|
||||||
|
info["config_widgets_top"] = cfg_names
|
||||||
|
except Exception as e:
|
||||||
|
info["config_widgets_top"] = f"ERREUR : {e}"
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/camera/reconnecter")
|
@app.post("/api/camera/reconnecter")
|
||||||
async def api_camera_reconnecter(body: dict = {}):
|
async def api_camera_reconnecter(body: dict = {}):
|
||||||
camera.deconnecter()
|
camera.deconnecter()
|
||||||
@@ -475,13 +519,13 @@ async def api_strip(donnees: dict):
|
|||||||
return JSONResponse({"erreur": f"Photo introuvable : {n}"}, status_code=404)
|
return JSONResponse({"erreur": f"Photo introuvable : {n}"}, status_code=404)
|
||||||
chemins.append(c)
|
chemins.append(c)
|
||||||
chemin_strip = creer_strip(chemins)
|
chemin_strip = creer_strip(chemins)
|
||||||
# Creer aussi la page d'impression (2 bandes sur 10x15 paysage)
|
|
||||||
chemin_print = creer_impression_strip(chemin_strip)
|
|
||||||
return {
|
return {
|
||||||
"nom": chemin_strip.name,
|
"nom": chemin_strip.name,
|
||||||
"chemin": f"/data/exports/{chemin_strip.name}",
|
"chemin": f"/data/exports/{chemin_strip.name}",
|
||||||
"impression": chemin_print.name,
|
# La strip elle-même est envoyée avec format 10x15-2up :
|
||||||
"chemin_impression": f"/data/exports/{chemin_print.name}",
|
# le K60 duplique et coupe automatiquement via -div2
|
||||||
|
"impression": chemin_strip.name,
|
||||||
|
"format_impression": "10x15-2up",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -640,8 +684,9 @@ async def api_imprimer(donnees: dict):
|
|||||||
chemin = DOSSIER_PHOTOS / nom
|
chemin = DOSSIER_PHOTOS / nom
|
||||||
if not chemin.exists():
|
if not chemin.exists():
|
||||||
return JSONResponse({"erreur": "Photo introuvable"}, status_code=404)
|
return JSONResponse({"erreur": "Photo introuvable"}, status_code=404)
|
||||||
cadre_override = donnees.get("cadre") # cadre choisi en session, prioritaire sur le config
|
cadre_override = donnees.get("cadre")
|
||||||
resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override)
|
format_papier = donnees.get("format_papier") or None
|
||||||
|
resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override, format_papier=format_papier)
|
||||||
if resultat.get("succes"):
|
if resultat.get("succes"):
|
||||||
distribuer_photo(chemin, imprimee=True)
|
distribuer_photo(chemin, imprimee=True)
|
||||||
return resultat
|
return resultat
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ wsOnMessage('camera_ok', () => {
|
|||||||
async function lancerCapture() {
|
async function lancerCapture() {
|
||||||
photosSession = [];
|
photosSession = [];
|
||||||
photoImpression = null;
|
photoImpression = null;
|
||||||
|
formatImpression = null;
|
||||||
|
|
||||||
document.getElementById('photo-resultat').src = '';
|
document.getElementById('photo-resultat').src = '';
|
||||||
document.getElementById('photo-partage').src = '';
|
document.getElementById('photo-partage').src = '';
|
||||||
@@ -111,12 +112,12 @@ async function lancerCapture() {
|
|||||||
compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`;
|
compteurEl.textContent = `Photo ${i + 1} / ${nbPhotos}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compte a rebours SANS preview (libere la camera)
|
lancerPreview(); // Miroir live pendant le compte à rebours
|
||||||
await compteARebours();
|
await compteARebours();
|
||||||
|
arreterPreview(); // Libère le bus USB gphoto2 pour la capture
|
||||||
|
|
||||||
afficherFlash();
|
afficherFlash();
|
||||||
|
|
||||||
// Preview arrete -> capture propre sans conflit gphoto2
|
|
||||||
const promesseCapture = apiPost('/api/capturer').catch(() => null);
|
const promesseCapture = apiPost('/api/capturer').catch(() => null);
|
||||||
let resultat = await promesseCapture;
|
let resultat = await promesseCapture;
|
||||||
let erreurReseau = false;
|
let erreurReseau = false;
|
||||||
@@ -249,7 +250,8 @@ function afficherFlash() {
|
|||||||
setTimeout(() => flash.classList.add('cache'), 400);
|
setTimeout(() => flash.classList.add('cache'), 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
let photoImpression = null; // Nom du fichier d'impression (2 bandes sur 10x15)
|
let photoImpression = null; // Fichier à imprimer (strip ou photo finale)
|
||||||
|
let formatImpression = null; // Format CUPS à utiliser (ex: "10x15-2up")
|
||||||
|
|
||||||
async function traiterCapture() {
|
async function traiterCapture() {
|
||||||
if (photosSession.length === 0) {
|
if (photosSession.length === 0) {
|
||||||
@@ -258,6 +260,7 @@ async function traiterCapture() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
photoImpression = null;
|
photoImpression = null;
|
||||||
|
formatImpression = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (modeActuel === 'multi') {
|
if (modeActuel === 'multi') {
|
||||||
@@ -270,6 +273,7 @@ async function traiterCapture() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resultat.impression) photoImpression = resultat.impression;
|
if (resultat.impression) photoImpression = resultat.impression;
|
||||||
|
if (resultat.format_impression) formatImpression = resultat.format_impression;
|
||||||
} else {
|
} else {
|
||||||
resultat = await apiPost('/api/collage', { photos: photosSession });
|
resultat = await apiPost('/api/collage', { photos: photosSession });
|
||||||
if (resultat.erreur) {
|
if (resultat.erreur) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ async function lancerImpression() {
|
|||||||
photo: fichierImpression,
|
photo: fichierImpression,
|
||||||
copies: nbExemplaires,
|
copies: nbExemplaires,
|
||||||
cadre: cadreChoisi || undefined,
|
cadre: cadreChoisi || undefined,
|
||||||
|
format_papier: formatImpression || undefined,
|
||||||
});
|
});
|
||||||
if (resultat.succes) {
|
if (resultat.succes) {
|
||||||
afficherStatut(`${nbExemplaires} exemplaire(s) envoye(s) a l'imprimante !`, 'succes');
|
afficherStatut(`${nbExemplaires} exemplaire(s) envoye(s) a l'imprimante !`, 'succes');
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ xset s noblank
|
|||||||
# Automount USB (pour la photostation)
|
# Automount USB (pour la photostation)
|
||||||
udiskie --no-notify --no-appindicator &
|
udiskie --no-notify --no-appindicator &
|
||||||
|
|
||||||
|
# Libérer le DSLR : gvfsd-gphoto2 (GNOME) le monopolise sinon
|
||||||
|
pkill -f gvfsd-gphoto2 2>/dev/null || true
|
||||||
|
pkill -f gvfs-gphoto2-volume-monitor 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
# Backend en boucle : redémarre automatiquement s'il plante
|
# Backend en boucle : redémarre automatiquement s'il plante
|
||||||
_backend_loop() {
|
_backend_loop() {
|
||||||
while true; do
|
while true; do
|
||||||
|
|||||||
Reference in New Issue
Block a user