Compare commits

...

2 Commits

Author SHA1 Message Date
Adriano b2e3867370 Test scalda camere. Refs #108
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 18:16:40 +01:00
Adriano cab419bdc7 Avvio: scalda le camere configurate con un grab a vuoto. Refs #108
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 18:16:31 +01:00
2 changed files with 139 additions and 0 deletions
+65
View File
@@ -348,6 +348,69 @@ def _nessun_utente(db: Path) -> bool:
return False 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: def _apri_browser_quando_pronto(url: str, porte: tuple[int, ...]) -> None:
# Aspetta che i server ascoltino (max ~60 s), poi apre il browser predefinito # Aspetta che i server ascoltino (max ~60 s), poi apre il browser predefinito
for _ in range(300): 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) registra_strumenti() # prima del server: il worker di visione legge registry() gia' popolato (#82)
thread = avvia_server(args.host, dati) 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: if not args.senza_browser:
# database senza utenti: si apre la pagina di setup del backend (SETUP_PASSWORD in config.env) # 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") url = (f"http://127.0.0.1:{PORTA_BACKEND}/api/setup" if _nessun_utente(dati / "tmf.db")
+74
View File
@@ -271,3 +271,77 @@ def test_avvio_normale_registra_gli_strumenti_prima_del_server(avvio, tmp_path,
assert avvio.main(["--senza-finestre", "--senza-browser"]) == 0 assert avvio.main(["--senza-finestre", "--senza-browser"]) == 0
dati = tmp_path / "Tielogic" / "TieMeasureFlow" dati = tmp_path / "Tielogic" / "TieMeasureFlow"
assert avvio.chiamate == ["migrazioni", "strumenti", ("127.0.0.1", dati)] assert avvio.chiamate == ["migrazioni", "strumenti", ("127.0.0.1", dati)]
# ---------------------------------------------------------------- scalda camere (#108)
def _db_camere(percorso):
# #108: db minimo con le due tabelle lette da camere_da_scaldare
con = sqlite3.connect(percorso)
con.execute("CREATE TABLE stations (id INTEGER PRIMARY KEY, camera_url TEXT, camera_source TEXT)")
con.execute("CREATE TABLE recipe_tasks (id INTEGER PRIMARY KEY, acquisition_json TEXT)")
con.executemany("INSERT INTO stations (camera_url, camera_source) VALUES (?, ?)", [
("http://127.0.0.1:8765", "camera:601020"), # valida
(None, None), # stazione senza camera
("http://127.0.0.1:8765", "camera:folder"), # non apre hardware
])
con.executemany("INSERT INTO recipe_tasks (acquisition_json) VALUES (?)", [
(json.dumps({"camera_url": "http://127.0.0.1:8765", "camera_source": "camera:601020"}),), # duplicato
(json.dumps({"camera_url": "http://10.0.0.5:8765/", "camera_source": "camera:777"}),), # valida
("{rotto",), # JSON non valido
(None,),
])
con.commit()
con.close()
def test_camere_da_scaldare_filtra_e_deduplica(avvio, tmp_path):
db = tmp_path / "tmf.db"
_db_camere(db)
assert avvio.camere_da_scaldare(db) == [
("http://127.0.0.1:8765", "camera:601020"),
("http://10.0.0.5:8765/", "camera:777"),
]
def test_camere_da_scaldare_db_inesistente(avvio, tmp_path):
assert avvio.camere_da_scaldare(tmp_path / "non_esiste.db") == []
def test_scalda_camere_una_chiamata_per_coppia(avvio, tmp_path, monkeypatch):
db = tmp_path / "tmf.db"
_db_camere(db)
monkeypatch.setattr(avvio, "_porta_aperta", lambda porta: True)
chiamate = []
class Risposta:
status = 200
def __enter__(self): return self
def __exit__(self, *a): return False
def finto_urlopen(richiesta, timeout=None):
chiamate.append((richiesta.full_url, json.loads(richiesta.data), richiesta.get_method(), timeout))
return Risposta()
monkeypatch.setattr(avvio.urllib.request, "urlopen", finto_urlopen)
avvio.scalda_camere(db)
corpo = {"trigger": "continuo", "timeout_ms": 5000}
assert chiamate == [
("http://127.0.0.1:8765/camera/grab", {"source": "camera:601020", **corpo}, "POST", 90),
("http://10.0.0.5:8765/camera/grab", {"source": "camera:777", **corpo}, "POST", 90),
]
def test_scalda_camere_eccezione_urlopen_non_esce(avvio, tmp_path, monkeypatch):
db = tmp_path / "tmf.db"
_db_camere(db)
monkeypatch.setattr(avvio, "_porta_aperta", lambda porta: True)
tentativi = []
def urlopen_rotto(richiesta, timeout=None):
tentativi.append(richiesta.full_url)
raise OSError("camera irraggiungibile")
monkeypatch.setattr(avvio.urllib.request, "urlopen", urlopen_rotto)
avvio.scalda_camere(db) # non solleva e passa alla camera successiva
assert len(tentativi) == 2