first commit
This commit is contained in:
@@ -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",
|
||||
)
|
||||
Reference in New Issue
Block a user