Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2e3867370 | |||
| cab419bdc7 |
@@ -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")
|
||||
|
||||
@@ -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
|
||||
dati = tmp_path / "Tielogic" / "TieMeasureFlow"
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user