diff --git a/packaging/windows/avvio.py b/packaging/windows/avvio.py index 7611f5a..4c4d175 100644 --- a/packaging/windows/avvio.py +++ b/packaging/windows/avvio.py @@ -348,6 +348,69 @@ def _nessun_utente(db: Path) -> bool: return False +def camere_da_scaldare(db: Path) -> list[tuple[str, str]]: + """Coppie (camera_url, camera_source) configurate nel db (stazioni e task di ricetta), senza doppioni. + Solo sorgenti "camera:" che aprono hardware (non "camera:folder"). Errore sqlite -> lista vuota (#108).""" + coppie: list[tuple[str, str]] = [] + + def aggiungi(url, src) -> None: + # #108: valida la coppia e scarta duplicati e sorgenti che non aprono hardware + if not (isinstance(url, str) and isinstance(src, str) and url.strip() and src.strip()): + return + if not src.startswith("camera:") or src == "camera:folder": + return + if (url, src) not in coppie: + coppie.append((url, src)) + + try: + con = sqlite3.connect(f"file:{db.as_posix()}?mode=ro", uri=True, timeout=2) + try: + for url, src in con.execute("SELECT camera_url, camera_source FROM stations"): + aggiungi(url, src) + for (blob,) in con.execute("SELECT acquisition_json FROM recipe_tasks WHERE acquisition_json IS NOT NULL"): + try: + cfg = json.loads(blob) + except (TypeError, ValueError): + continue # #108: JSON non valido, si salta + if isinstance(cfg, dict): + aggiungi(cfg.get("camera_url"), cfg.get("camera_source")) + finally: + con.close() + except Exception: + return [] + return coppie + + +def scalda_camere(db: Path) -> None: + """Apre ogni camera configurata con un grab a vuoto, cosi' il primo "Acquisisci" non aspetta ~25 s (#108). + Gira in un thread daemon: nessuna eccezione esce.""" + try: + camere = camere_da_scaldare(db) + if not camere: + return + for _ in range(300): # aspetta il servizio camera (max ~60 s), come _apri_browser_quando_pronto + if _porta_aperta(PORTA_CAMERA): + break + time.sleep(0.2) + else: + log.warning("scalda camere: servizio camera non raggiungibile, salto") + return + for url, src in camere: + t0 = time.monotonic() + try: + richiesta = urllib.request.Request( + f"{url.rstrip('/')}/camera/grab", + data=json.dumps({"source": src, "trigger": "continuo", "timeout_ms": 5000}).encode("utf-8"), + headers={"Content-Type": "application/json"}, method="POST") + with urllib.request.urlopen(richiesta, timeout=90) as r: + esito = f"HTTP {r.status}" + log.info("scalda camera %s: %s in %.1f s", src, esito, time.monotonic() - t0) + except Exception as e: # #108: una camera che non risponde non ferma le altre + log.warning("scalda camera %s: %s dopo %.1f s", src, e, time.monotonic() - t0) + except Exception: + log.exception("scalda camere non riuscito") + + def _apri_browser_quando_pronto(url: str, porte: tuple[int, ...]) -> None: # Aspetta che i server ascoltino (max ~60 s), poi apre il browser predefinito for _ in range(300): @@ -509,6 +572,8 @@ def main(argv=None) -> int: registra_strumenti() # prima del server: il worker di visione legge registry() gia' popolato (#82) thread = avvia_server(args.host, dati) + # #108: apre le camere configurate con un grab a vuoto, con o senza browser + threading.Thread(target=scalda_camere, args=(dati / "tmf.db",), daemon=True).start() if not args.senza_browser: # database senza utenti: si apre la pagina di setup del backend (SETUP_PASSWORD in config.env) url = (f"http://127.0.0.1:{PORTA_BACKEND}/api/setup" if _nessun_utente(dati / "tmf.db")