Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9beaf2ed67 | |||
| 800ec41254 | |||
| 9c45a51770 | |||
| 4a48af2ae3 | |||
| b879563738 | |||
| 632fd51050 |
+192
-33
@@ -46,6 +46,8 @@ PAGINA = r"""<!doctype html>
|
|||||||
select:focus, input:focus, button:focus-visible { outline: 2px solid var(--accento); outline-offset: 1px; }
|
select:focus, input:focus, button:focus-visible { outline: 2px solid var(--accento); outline-offset: 1px; }
|
||||||
input[type=range] { width: 100%; accent-color: var(--accento); }
|
input[type=range] { width: 100%; accent-color: var(--accento); }
|
||||||
input[type=checkbox] { accent-color: var(--accento); }
|
input[type=checkbox] { accent-color: var(--accento); }
|
||||||
|
.scelta-cal { margin-top: 8px; align-items: center; } /* #93 */
|
||||||
|
.scelta-cal select { flex: 1 1 200px; min-width: 0; } /* #93: nessuno scorrimento a 400 px */
|
||||||
.riga { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
|
.riga { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
|
||||||
.nota { font-size: .82rem; color: var(--secondario); margin: 8px 0 0; }
|
.nota { font-size: .82rem; color: var(--secondario); margin: 8px 0 0; }
|
||||||
button { padding: 7px 13px; font-size: .95rem; border: 0; border-radius: 6px; background: var(--accento); color: #fff;
|
button { padding: 7px 13px; font-size: .95rem; border: 0; border-radius: 6px; background: var(--accento); color: #fff;
|
||||||
@@ -69,7 +71,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
.mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri */
|
.mirino { position: absolute; inset: 0; pointer-events: none; /* #92: classe, vale per i due riquadri */
|
||||||
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
|
background: linear-gradient(#fbbf24, #fbbf24) center / 2px 100% no-repeat,
|
||||||
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; }
|
linear-gradient(#fbbf24, #fbbf24) center / 100% 2px no-repeat; opacity: .85; }
|
||||||
#vuoto, #vuotoCal { padding: 30px 16px; color: var(--secondario); text-align: center; }
|
#vuoto, #vuotoCal, #vuotoCalTab { padding: 30px 16px; color: var(--secondario); text-align: center; }
|
||||||
/* #92: due riquadri affiancati (grezza | calibrata); in colonna sotto i 900 px */
|
/* #92: due riquadri affiancati (grezza | calibrata); in colonna sotto i 900 px */
|
||||||
.riquadri { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 10px; }
|
.riquadri { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 10px; }
|
||||||
.riquadri.doppio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
.riquadri.doppio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
||||||
@@ -140,6 +142,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
<label for="sorgente">Camera o immagine</label>
|
<label for="sorgente">Camera o immagine</label>
|
||||||
<select id="sorgente"></select>
|
<select id="sorgente"></select>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="aggiornaSorgenti" class="secondario" type="button" title="Cerca di nuovo le camere collegate">Aggiorna</button>
|
||||||
<button id="caricaImmagine" class="secondario" type="button">Carica immagine da file...</button>
|
<button id="caricaImmagine" class="secondario" type="button">Carica immagine da file...</button>
|
||||||
<input id="fileImmagine" type="file" accept="image/*" hidden>
|
<input id="fileImmagine" type="file" accept="image/*" hidden>
|
||||||
<button id="rimuoviImmagine" class="secondario" type="button" hidden>Rimuovi immagine</button>
|
<button id="rimuoviImmagine" class="secondario" type="button" hidden>Rimuovi immagine</button>
|
||||||
@@ -155,7 +158,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
<button id="avvia">Avvia anteprima</button>
|
<button id="avvia">Avvia anteprima</button>
|
||||||
<button id="ferma" class="secondario" disabled>Ferma anteprima</button>
|
<button id="ferma" class="secondario" disabled>Ferma anteprima</button>
|
||||||
<button id="scatta" class="secondario">Scatta</button>
|
<button id="scatta" class="secondario">Scatta</button>
|
||||||
<label style="margin:0 0 6px"><input id="mostraCal" type="checkbox"> Mostra immagine calibrata</label>
|
<label style="margin:0 0 6px"><input id="mostraCal" type="checkbox"> Attiva calibrazione</label>
|
||||||
<label style="margin:0 0 6px"><input id="mirinoCasella" type="checkbox"> Mirino centrale</label>
|
<label style="margin:0 0 6px"><input id="mirinoCasella" type="checkbox"> Mirino centrale</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="riquadri" id="riquadri">
|
<div class="riquadri" id="riquadri">
|
||||||
@@ -178,6 +181,12 @@ PAGINA = r"""<!doctype html>
|
|||||||
<div id="mirinoCal" class="mirino" hidden></div>
|
<div id="mirinoCal" class="mirino" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #93: scelta della calibrazione da attivare sulla sorgente, con CONFERMA -->
|
||||||
|
<div class="riga scelta-cal">
|
||||||
|
<select id="sceltaCal" aria-label="Calibrazione da attivare" disabled></select>
|
||||||
|
<button id="confermaCal" type="button" disabled>CONFERMA</button>
|
||||||
|
</div>
|
||||||
|
<div id="msgSceltaCal" class="messaggio"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="indicatori">
|
<div class="indicatori">
|
||||||
@@ -223,6 +232,18 @@ PAGINA = r"""<!doctype html>
|
|||||||
<section class="scheda" id="sezioneCalibrazione">
|
<section class="scheda" id="sezioneCalibrazione">
|
||||||
<h2>Calibrazione</h2>
|
<h2>Calibrazione</h2>
|
||||||
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> — immagine grezza dalla tab Acquisizione</div>
|
<div class="stato" style="margin:0 0 10px">Sorgente: <strong id="calSorgente">-</strong> — immagine grezza dalla tab Acquisizione</div>
|
||||||
|
<!-- #94: immagine viva (stesso frame dell'anteprima) e «Blocca immagine» per calcolare su quella -->
|
||||||
|
<div class="palco" style="margin-top:0">
|
||||||
|
<div id="vuotoCalTab">Nessuna immagine: «Scatta» o avviare l'anteprima nella tab Acquisizione</div>
|
||||||
|
<div class="cornice" id="corniceCalTab" hidden>
|
||||||
|
<img id="immagineCalTab" alt="Immagine grezza della sorgente">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="riga" style="margin-top:8px">
|
||||||
|
<button id="scattaCal" class="secondario" type="button">Scatta</button>
|
||||||
|
<label style="margin:0 0 6px"><input id="bloccaImg" type="checkbox"> Blocca immagine</label>
|
||||||
|
</div>
|
||||||
|
<div id="msgBlocco" class="messaggio"></div>
|
||||||
<div class="riga" style="margin-top:10px">
|
<div class="riga" style="margin-top:10px">
|
||||||
<div><label for="quadrato">Lato dello scacco (mm)</label><input id="quadrato" type="text" inputmode="decimal" value="2" size="8"></div>
|
<div><label for="quadrato">Lato dello scacco (mm)</label><input id="quadrato" type="text" inputmode="decimal" value="2" size="8"></div>
|
||||||
<button id="acquisisci" title="Acquisisci e calibra">Calcola</button>
|
<button id="acquisisci" title="Acquisisci e calibra">Calcola</button>
|
||||||
@@ -230,7 +251,7 @@ PAGINA = r"""<!doctype html>
|
|||||||
<p class="nota">
|
<p class="nota">
|
||||||
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
|
Servono una scacchiera che riempie tutto il campo della camera, piana, ferma e ben illuminata, e il lato di uno scacco.
|
||||||
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
|
La griglia si trova da sola; il risultato corregge anche la distorsione dell'ottica.
|
||||||
Il frame è sempre quello grezzo della sorgente; l'anteprima resta sospesa finché questa tab è aperta.</p>
|
Il frame è sempre quello grezzo della sorgente; l'anteprima continua anche qui e si può bloccare l'immagine per calcolare su quella.</p>
|
||||||
<div id="messaggio" class="messaggio"></div>
|
<div id="messaggio" class="messaggio"></div>
|
||||||
|
|
||||||
<div id="risultato" hidden style="margin-top:10px">
|
<div id="risultato" hidden style="margin-top:10px">
|
||||||
@@ -312,12 +333,13 @@ async function dettaglio(risposta) {
|
|||||||
// ---- Sorgente
|
// ---- Sorgente
|
||||||
|
|
||||||
// #92: scegli = nome da selezionare (immagine appena caricata); senza, l'ultima scelta ricordata
|
// #92: scegli = nome da selezionare (immagine appena caricata); senza, l'ultima scelta ricordata
|
||||||
async function caricaSorgenti(scegli) {
|
// #92: aggiorna = true forza la ricerca camere del servizio (camera appena collegata); di norma e' in cache
|
||||||
|
async function caricaSorgenti(scegli, aggiorna) {
|
||||||
var sel = $("sorgente");
|
var sel = $("sorgente");
|
||||||
var prima = sel.value;
|
var prima = sel.value;
|
||||||
sel.textContent = "";
|
sel.textContent = "";
|
||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/sources");
|
var r = await fetch("/camera/sources" + (aggiorna ? "?aggiorna=true" : ""));
|
||||||
var j = await r.json();
|
var j = await r.json();
|
||||||
(j.sources || []).forEach(function (s) {
|
(j.sources || []).forEach(function (s) {
|
||||||
var o = document.createElement("option");
|
var o = document.createElement("option");
|
||||||
@@ -341,6 +363,27 @@ function sorgenteImmagine() {
|
|||||||
return sel.selectedIndex >= 0 && sel.options[sel.selectedIndex].dataset.kind === "image";
|
return sel.selectedIndex >= 0 && sel.options[sel.selectedIndex].dataset.kind === "image";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #92: aggiunge (o aggiorna) la voce dell'immagine {source, label, kind} nella select e la seleziona
|
||||||
|
function aggiungiSorgente(j) {
|
||||||
|
var sel = $("sorgente");
|
||||||
|
var o = null;
|
||||||
|
for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value === j.source) o = sel.options[i];
|
||||||
|
if (!o) { o = document.createElement("option"); o.value = j.source; sel.appendChild(o); }
|
||||||
|
o.textContent = j.label || j.source;
|
||||||
|
o.dataset.kind = j.kind || "image";
|
||||||
|
sel.value = j.source;
|
||||||
|
cambioSorgente(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #92: «Aggiorna» = ricerca camere nuova (sul servizio puo' durare ~10 s)
|
||||||
|
$("aggiornaSorgenti").onclick = async function () {
|
||||||
|
$("aggiornaSorgenti").disabled = true;
|
||||||
|
dire("msgSorgente", "Ricerca delle camere in corso...", "");
|
||||||
|
await caricaSorgenti(undefined, true);
|
||||||
|
dire("msgSorgente", "Elenco aggiornato.", "ok");
|
||||||
|
$("aggiornaSorgenti").disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
// #92: «Carica immagine da file...» -> POST /camera/immagini, poi la nuova immagine diventa la sorgente
|
// #92: «Carica immagine da file...» -> POST /camera/immagini, poi la nuova immagine diventa la sorgente
|
||||||
$("caricaImmagine").onclick = function () { $("fileImmagine").click(); };
|
$("caricaImmagine").onclick = function () { $("fileImmagine").click(); };
|
||||||
$("fileImmagine").onchange = async function () {
|
$("fileImmagine").onchange = async function () {
|
||||||
@@ -355,7 +398,7 @@ $("fileImmagine").onchange = async function () {
|
|||||||
var r = await fetch("/camera/immagini", { method: "POST", body: f });
|
var r = await fetch("/camera/immagini", { method: "POST", body: f });
|
||||||
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
||||||
var j = await r.json();
|
var j = await r.json();
|
||||||
await caricaSorgenti(j.source);
|
aggiungiSorgente(j); // #92: la ricerca camere puo' durare ~10 s: la voce nuova si aggiunge senza rileggere l'elenco
|
||||||
dire("msgSorgente", "Immagine «" + j.source + "» caricata: premere «Scatta» per vederla.", "ok");
|
dire("msgSorgente", "Immagine «" + j.source + "» caricata: premere «Scatta» per vederla.", "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
||||||
@@ -385,7 +428,10 @@ $("rimuoviImmagine").onclick = async function () {
|
|||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/immagini?nome=" + encodeURIComponent(nome), { method: "DELETE" });
|
var r = await fetch("/camera/immagini?nome=" + encodeURIComponent(nome), { method: "DELETE" });
|
||||||
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
if (!r.ok) { dire("msgSorgente", await dettaglio(r), "errore"); return; }
|
||||||
await caricaSorgenti("");
|
var sel = $("sorgente"); // #92: voce tolta in locale, senza rileggere l'elenco dal servizio
|
||||||
|
sel.removeChild(sel.options[sel.selectedIndex]);
|
||||||
|
if (sel.options.length) sel.selectedIndex = 0;
|
||||||
|
cambioSorgente(true);
|
||||||
dire("msgSorgente", "Immagine «" + nome + "» rimossa.", "ok");
|
dire("msgSorgente", "Immagine «" + nome + "» rimossa.", "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
dire("msgSorgente", "Servizio non raggiungibile.", "errore");
|
||||||
@@ -397,8 +443,10 @@ function cambioSorgente(salva) {
|
|||||||
if (salva) memoria("tmf.acq.sorgente", $("sorgente").value);
|
if (salva) memoria("tmf.acq.sorgente", $("sorgente").value);
|
||||||
var era = A.attiva;
|
var era = A.attiva;
|
||||||
fermaAnteprima();
|
fermaAnteprima();
|
||||||
|
if (A.bloccata) rilasciaBlocco(); // #94: casella spenta e DELETE sulla sorgente vecchia (prima di svuotare i riquadri)
|
||||||
azzeraIndicatori();
|
azzeraIndicatori();
|
||||||
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
|
svuotaRiquadri(); // #92: il frame vecchio non e' della nuova sorgente
|
||||||
|
A.calAttiva = null; A.bloccoCal = false; // #92: stato calibrazione della nuova sorgente ancora da leggere
|
||||||
annullaRimuovi();
|
annullaRimuovi();
|
||||||
$("rimuoviImmagine").hidden = !sorgenteImmagine();
|
$("rimuoviImmagine").hidden = !sorgenteImmagine();
|
||||||
dire("msgAnteprima", "");
|
dire("msgAnteprima", "");
|
||||||
@@ -410,8 +458,8 @@ function cambioSorgente(salva) {
|
|||||||
|
|
||||||
// ---- Anteprima (#87)
|
// ---- Anteprima (#87)
|
||||||
|
|
||||||
// #91: ris = {w, h} dell'ultimo frame; #92: urlCal/seqCal = riquadro calibrata, riprendi = ciclo sospeso dalla tab Calibrazione, lato = ultimo lato_max
|
// #91: ris = {w, h} dell'ultimo frame; #92: calAttiva = la sorgente ha una calibrazione attiva (null = non nota), bloccoCal = un 409 ha fermato le richieste calibrata; urlCal/seqCal = riquadro calibrata, bloccata/sorgenteBloccata/urlBlocco/seqBlocco = #94 immagine bloccata per il calcolo (il riquadro della tab Calibrazione non segue piu' il ciclo), lato = ultimo lato_max
|
||||||
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, riprendi: false, lato: 1280, nitMax: 0, ris: null };
|
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, bloccata: false, sorgenteBloccata: null, urlBlocco: null, seqBlocco: 0, lato: 1280, calAttiva: null, bloccoCal: false, nitMax: 0, ris: null };
|
||||||
|
|
||||||
function azzeraIndicatori() {
|
function azzeraIndicatori() {
|
||||||
A.nitMax = 0;
|
A.nitMax = 0;
|
||||||
@@ -432,9 +480,17 @@ function mostraImmagine(blob) {
|
|||||||
var vecchio = A.url;
|
var vecchio = A.url;
|
||||||
A.url = URL.createObjectURL(blob);
|
A.url = URL.createObjectURL(blob);
|
||||||
$("immagine").src = A.url;
|
$("immagine").src = A.url;
|
||||||
if (vecchio) URL.revokeObjectURL(vecchio); // niente memoria che cresce a ogni frame
|
if (vecchio && vecchio !== A.urlBlocco) URL.revokeObjectURL(vecchio); // niente memoria che cresce a ogni frame; #94: non quello bloccato
|
||||||
$("cornice").hidden = false;
|
$("cornice").hidden = false;
|
||||||
$("vuoto").hidden = true;
|
$("vuoto").hidden = true;
|
||||||
|
if (!A.bloccata) mostraInTabCal(A.url); // #94: lo stesso blob URL va anche nella tab Calibrazione, salvo immagine bloccata
|
||||||
|
}
|
||||||
|
|
||||||
|
// #94: riquadro «immagine viva» della tab Calibrazione (url null = vuoto)
|
||||||
|
function mostraInTabCal(url) {
|
||||||
|
$("immagineCalTab").src = url || "";
|
||||||
|
$("corniceCalTab").hidden = !url;
|
||||||
|
$("vuotoCalTab").hidden = !!url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #92: riquadro «Calibrata»: l'immagine raddrizzata oppure il motivo per cui non c'e' (404/409 del servizio)
|
// #92: riquadro «Calibrata»: l'immagine raddrizzata oppure il motivo per cui non c'e' (404/409 del servizio)
|
||||||
@@ -458,12 +514,19 @@ function svuotaRiquadri() {
|
|||||||
A.seqCal++; // una risposta calibrata ancora in volo non vale piu'
|
A.seqCal++; // una risposta calibrata ancora in volo non vale piu'
|
||||||
$("cornice").hidden = true;
|
$("cornice").hidden = true;
|
||||||
$("vuoto").hidden = false;
|
$("vuoto").hidden = false;
|
||||||
|
mostraInTabCal(null); // #94: svuotato anche il riquadro della tab Calibrazione
|
||||||
messaggioCalibrata("Premere «Avvia anteprima» o «Scatta».");
|
messaggioCalibrata("Premere «Avvia anteprima» o «Scatta».");
|
||||||
}
|
}
|
||||||
// #92: raddrizza l'ULTIMO frame grezzo tenuto dal servizio (nessuna lettura nuova); chiamata dopo ogni anteprima, dopo Carica/Scollega e alla casella
|
// #92: raddrizza l'ULTIMO frame grezzo tenuto dal servizio (nessuna lettura nuova); chiamata dopo ogni anteprima, dopo Carica/Scollega e alla casella
|
||||||
async function aggiornaCalibrata(lato) {
|
async function aggiornaCalibrata(lato) {
|
||||||
var sorgente = $("sorgente").value;
|
var sorgente = $("sorgente").value;
|
||||||
if (!$("mostraCal").checked || !sorgente) return;
|
if (!$("mostraCal").checked || !sorgente) return;
|
||||||
|
if (A.calAttiva === false) { // #92: lo stato dice che non c'e' calibrazione: niente richiesta al servizio
|
||||||
|
A.seqCal++;
|
||||||
|
messaggioCalibrata("nessuna calibrazione attiva su questa sorgente");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (A.bloccoCal) return; // #92: dopo un 409 (risoluzione diversa, planare) si richiede solo dopo Carica/Scollega o cambio sorgente
|
||||||
var n = ++A.seqCal;
|
var n = ++A.seqCal;
|
||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/anteprima/calibrata", {
|
var r = await fetch("/camera/anteprima/calibrata", {
|
||||||
@@ -472,7 +535,11 @@ async function aggiornaCalibrata(lato) {
|
|||||||
body: JSON.stringify({ source: sorgente, lato_max: lato || A.lato })
|
body: JSON.stringify({ source: sorgente, lato_max: lato || A.lato })
|
||||||
});
|
});
|
||||||
if (n !== A.seqCal) return; // risposta superata da una piu' recente o da un cambio sorgente
|
if (n !== A.seqCal) return; // risposta superata da una piu' recente o da un cambio sorgente
|
||||||
if (!r.ok) { messaggioCalibrata(await dettaglio(r)); return; } // l'anteprima grezza continua
|
if (!r.ok) { // l'anteprima grezza continua
|
||||||
|
if (r.status === 409) A.bloccoCal = true; // #92: inutile richiedere a ogni frame
|
||||||
|
messaggioCalibrata(await dettaglio(r));
|
||||||
|
return;
|
||||||
|
}
|
||||||
var nome = "";
|
var nome = "";
|
||||||
try { nome = decodeURIComponent(r.headers.get("X-Calibrazione") || ""); } catch (e) { /* intestazione non decodificabile: nessun nome */ }
|
try { nome = decodeURIComponent(r.headers.get("X-Calibrazione") || ""); } catch (e) { /* intestazione non decodificabile: nessun nome */ }
|
||||||
var blob = await r.blob();
|
var blob = await r.blob();
|
||||||
@@ -570,18 +637,66 @@ function fermaAnteprima() {
|
|||||||
// «Scatta»: ferma il ciclo e mostra un frame solo, alla risoluzione piena (fino a 4096 px)
|
// «Scatta»: ferma il ciclo e mostra un frame solo, alla risoluzione piena (fino a 4096 px)
|
||||||
async function scatta() {
|
async function scatta() {
|
||||||
fermaAnteprima();
|
fermaAnteprima();
|
||||||
$("scatta").disabled = true;
|
$("scatta").disabled = $("scattaCal").disabled = true; // #94: anche il «Scatta» della tab Calibrazione
|
||||||
dire("msgAnteprima", "Scatto in corso...", "");
|
dire("msgAnteprima", "Scatto in corso...", "");
|
||||||
while (A.inVolo) await pausa(50);
|
while (A.inVolo) await pausa(50);
|
||||||
await unaAnteprima(4096);
|
await unaAnteprima(4096);
|
||||||
$("scatta").disabled = false;
|
$("scatta").disabled = false;
|
||||||
|
$("scattaCal").disabled = A.bloccata; // #94: con l'immagine bloccata resta spento
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #94: «Blocca immagine»: il servizio tiene una copia del frame grezzo e «Calcola» usa quella
|
||||||
|
function sbloccaLocale() { // casella spenta, il riquadro della tab Calibrazione torna a seguire il ciclo
|
||||||
|
A.bloccata = false;
|
||||||
|
A.sorgenteBloccata = null;
|
||||||
|
A.seqBlocco++; // una risposta di blocco ancora in volo non vale piu'
|
||||||
|
var b = A.urlBlocco;
|
||||||
|
A.urlBlocco = null;
|
||||||
|
if (b && b !== A.url) URL.revokeObjectURL(b);
|
||||||
|
$("bloccaImg").checked = false;
|
||||||
|
$("scattaCal").disabled = $("scatta").disabled;
|
||||||
|
mostraInTabCal($("cornice").hidden ? null : A.url);
|
||||||
|
}
|
||||||
|
function rilasciaBlocco() { // DELETE idempotente: l'esito non conta
|
||||||
|
var s = A.sorgenteBloccata;
|
||||||
|
sbloccaLocale();
|
||||||
|
dire("msgBlocco", "");
|
||||||
|
if (s) fetch("/camera/blocca?source=" + encodeURIComponent(s), { method: "DELETE" }).catch(function () { /* niente da fare */ });
|
||||||
|
}
|
||||||
|
$("bloccaImg").onchange = async function () {
|
||||||
|
if (!this.checked) { rilasciaBlocco(); return; }
|
||||||
|
var sorgente = $("sorgente").value;
|
||||||
|
if (!sorgente || $("cornice").hidden) {
|
||||||
|
$("bloccaImg").checked = false;
|
||||||
|
dire("msgBlocco", "Nessuna immagine da bloccare: «Scatta» o avviare l'anteprima.", "errore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
A.bloccata = true; // subito: il riquadro smette di seguire il ciclo
|
||||||
|
A.sorgenteBloccata = sorgente;
|
||||||
|
A.urlBlocco = A.url;
|
||||||
|
var n = ++A.seqBlocco;
|
||||||
|
$("scattaCal").disabled = true;
|
||||||
|
dire("msgBlocco", "Blocco in corso...", "");
|
||||||
|
try {
|
||||||
|
var r = await fetch("/camera/blocca", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ source: sorgente }) });
|
||||||
|
if (n !== A.seqBlocco) { // spenta o cambiata sorgente nel frattempo: la copia appena fatta si toglie
|
||||||
|
if (r.ok) fetch("/camera/blocca?source=" + encodeURIComponent(sorgente), { method: "DELETE" }).catch(function () { /* niente da fare */ });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.ok) { var d = await dettaglio(r); sbloccaLocale(); dire("msgBlocco", d, "errore"); return; }
|
||||||
|
var b = await r.json();
|
||||||
|
dire("msgBlocco", "Immagine bloccata " + b.larghezza + "×" + b.altezza + ": «Calcola» usa questa.", "ok");
|
||||||
|
} catch (e) {
|
||||||
|
if (n === A.seqBlocco) { sbloccaLocale(); dire("msgBlocco", "Servizio non raggiungibile.", "errore"); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$("avvia").onclick = avviaAnteprima;
|
$("avvia").onclick = avviaAnteprima;
|
||||||
$("ferma").onclick = fermaAnteprima;
|
$("ferma").onclick = fermaAnteprima;
|
||||||
$("scatta").onclick = scatta;
|
$("scatta").onclick = scatta;
|
||||||
|
$("scattaCal").onclick = scatta; // #94
|
||||||
$("azzeraMax").onclick = function () { A.nitMax = 0; $("nitidezzaMax").textContent = "-"; $("barraNitidezza").style.width = "0"; };
|
$("azzeraMax").onclick = function () { A.nitMax = 0; $("nitidezzaMax").textContent = "-"; $("barraNitidezza").style.width = "0"; };
|
||||||
// #92: il mirino vale per entrambi i riquadri; la casella «Mostra immagine calibrata» apre il riquadro e raddrizza il frame gia' tenuto
|
// #92: il mirino vale per entrambi i riquadri; #93: la casella «Attiva calibrazione» apre il riquadro e raddrizza il frame gia' tenuto
|
||||||
$("mirinoCasella").onchange = function () { $("mirino").hidden = $("mirinoCal").hidden = !$("mirinoCasella").checked; };
|
$("mirinoCasella").onchange = function () { $("mirino").hidden = $("mirinoCal").hidden = !$("mirinoCasella").checked; };
|
||||||
function mostraRiquadroCal() {
|
function mostraRiquadroCal() {
|
||||||
var si = $("mostraCal").checked;
|
var si = $("mostraCal").checked;
|
||||||
@@ -818,9 +933,7 @@ function scegliTab(nome, salva) {
|
|||||||
$("tabCal").tabIndex = cal ? 0 : -1;
|
$("tabCal").tabIndex = cal ? 0 : -1;
|
||||||
$("pannelloAcq").hidden = cal;
|
$("pannelloAcq").hidden = cal;
|
||||||
$("pannelloCal").hidden = !cal;
|
$("pannelloCal").hidden = !cal;
|
||||||
// #92: nella tab Calibrazione il ciclo anteprima si sospende; tornando in Acquisizione riprende se era attivo
|
// #94: il ciclo anteprima non si sospende piu' aprendo la tab Calibrazione (come faceva #92)
|
||||||
if (cal && A.attiva) { A.riprendi = true; fermaAnteprima(); }
|
|
||||||
if (!cal && A.riprendi) { A.riprendi = false; avviaAnteprima(); }
|
|
||||||
if (!cal && salva) aggiornaCalibrata(); // il riquadro calibrata riflette la calibrazione attiva di adesso
|
if (!cal && salva) aggiornaCalibrata(); // il riquadro calibrata riflette la calibrazione attiva di adesso
|
||||||
if (salva) { memoria("tmf.acq.tab", nome); if (cal) caricaCalibrazioni(); } // l'elenco si rilegge entrando
|
if (salva) { memoria("tmf.acq.tab", nome); if (cal) caricaCalibrazioni(); } // l'elenco si rilegge entrando
|
||||||
}
|
}
|
||||||
@@ -901,10 +1014,12 @@ async function aggiornaStato() {
|
|||||||
if (r.status === 404) {
|
if (r.status === 404) {
|
||||||
stato.textContent = "Sorgente non calibrata.";
|
stato.textContent = "Sorgente non calibrata.";
|
||||||
$("statoSorgente").textContent = "non calibrata";
|
$("statoSorgente").textContent = "non calibrata";
|
||||||
|
A.calAttiva = false; // #92
|
||||||
} else if (!r.ok) {
|
} else if (!r.ok) {
|
||||||
stato.textContent = await dettaglio(r);
|
stato.textContent = await dettaglio(r);
|
||||||
} else {
|
} else {
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
|
A.calAttiva = true; // #92
|
||||||
stato.textContent = "";
|
stato.textContent = "";
|
||||||
var titolo = document.createElement("div");
|
var titolo = document.createElement("div");
|
||||||
titolo.textContent = "Calibrazione attiva: " + (d.nome ? "«" + d.nome + "»" : "(senza nome)");
|
titolo.textContent = "Calibrazione attiva: " + (d.nome ? "«" + d.nome + "»" : "(senza nome)");
|
||||||
@@ -956,6 +1071,7 @@ function disegnaElenco() {
|
|||||||
annullaConferma();
|
annullaConferma();
|
||||||
t.textContent = "";
|
t.textContent = "";
|
||||||
C.celle = [];
|
C.celle = [];
|
||||||
|
disegnaSceltaCal(); // #93: la combo sotto la calibrata segue l'elenco, anche se vuoto
|
||||||
if (!C.elenco.length) {
|
if (!C.elenco.length) {
|
||||||
var vuota = document.createElement("div");
|
var vuota = document.createElement("div");
|
||||||
vuota.className = "nota";
|
vuota.className = "nota";
|
||||||
@@ -996,7 +1112,7 @@ function disegnaElenco() {
|
|||||||
var bc = bottone("Carica", "", function () { caricaNome(c.nome); });
|
var bc = bottone("Carica", "", function () { caricaNome(c.nome); });
|
||||||
bc.disabled = !$("sorgente").value;
|
bc.disabled = !$("sorgente").value;
|
||||||
az.appendChild(bc);
|
az.appendChild(bc);
|
||||||
if (attiva) az.appendChild(bottone("Scollega", "secondario", scollega));
|
if (attiva) az.appendChild(bottone("Scollega", "secondario", function () { scollega("msgElenco"); })); // #93: senza l'evento come parametro
|
||||||
az.appendChild(bottone("Elimina", "secondario", function (e) { eliminaNome(c.nome, e.currentTarget); }));
|
az.appendChild(bottone("Elimina", "secondario", function (e) { eliminaNome(c.nome, e.currentTarget); }));
|
||||||
sc.appendChild(riga1);
|
sc.appendChild(riga1);
|
||||||
sc.appendChild(riga2);
|
sc.appendChild(riga2);
|
||||||
@@ -1011,39 +1127,80 @@ function segnaRisoluzioni() {
|
|||||||
(C.celle || []).forEach(function (c) {
|
(C.celle || []).forEach(function (c) {
|
||||||
c.span.hidden = !(A.ris && c.w > 0 && c.h > 0 && (A.ris.w !== c.w || A.ris.h !== c.h));
|
c.span.hidden = !(A.ris && c.w > 0 && c.h > 0 && (A.ris.w !== c.w || A.ris.h !== c.h));
|
||||||
});
|
});
|
||||||
|
// #93: stesso avviso nelle voci della combo; si cambia solo il testo, cosi' scelta e menu aperto restano
|
||||||
|
var sel = $("sceltaCal");
|
||||||
|
for (var i = 0; i < sel.options.length; i++) if (sel.options[i].value) sel.options[i].textContent = testoVoceCal(sel.options[i].dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #93: testo di una voce della combo: «nome · W×H · px/mm», con «risoluzione diversa» se il frame noto non combacia
|
||||||
|
function testoVoceCal(d) {
|
||||||
|
var w = Number(d.w), h = Number(d.h);
|
||||||
|
var t = d.nome + " · " + (w && h ? fmt(w, 0) + "×" + fmt(h, 0) : "-") + " · " + fmt(Number(d.px), 3) + " px/mm";
|
||||||
|
if (A.ris && w > 0 && h > 0 && (A.ris.w !== w || A.ris.h !== h)) t += " — risoluzione diversa";
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
// #93: ridisegna la combo (ogni ricarica dell'elenco); preselezionata la calibrazione attiva, senza sorgente tutto spento
|
||||||
|
function disegnaSceltaCal() {
|
||||||
|
var sel = $("sceltaCal");
|
||||||
|
sel.textContent = "";
|
||||||
|
var nessuna = document.createElement("option");
|
||||||
|
nessuna.value = "";
|
||||||
|
nessuna.textContent = "— nessuna (non calibrata) —";
|
||||||
|
sel.appendChild(nessuna);
|
||||||
|
(C.elenco || []).forEach(function (c) {
|
||||||
|
var o = document.createElement("option");
|
||||||
|
o.value = c.nome;
|
||||||
|
o.dataset.nome = c.nome; o.dataset.w = c.image_width || 0; o.dataset.h = c.image_height || 0; o.dataset.px = c.px_per_mm || 0;
|
||||||
|
o.textContent = testoVoceCal(o.dataset);
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
sel.value = C.attiva !== null && C.attiva !== undefined ? C.attiva : "";
|
||||||
|
if (sel.selectedIndex < 0) sel.selectedIndex = 0;
|
||||||
|
sel.disabled = $("confermaCal").disabled = !$("sorgente").value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// «Carica»: la calibrazione diventa quella attiva della sorgente. Un errore (es. risoluzione diversa) non ferma l'anteprima.
|
// «Carica»: la calibrazione diventa quella attiva della sorgente. Un errore (es. risoluzione diversa) non ferma l'anteprima.
|
||||||
async function caricaNome(nome) {
|
async function caricaNome(nome, idMsg) {
|
||||||
|
idMsg = idMsg || "msgElenco"; // #93: il messaggio puo' stare sotto la calibrata (msgSceltaCal)
|
||||||
var sorgente = $("sorgente").value;
|
var sorgente = $("sorgente").value;
|
||||||
if (!sorgente) { dire("msgElenco", "Scegliere una sorgente nella tab Acquisizione.", "errore"); return; }
|
if (!sorgente) { dire(idMsg, "Scegliere una sorgente nella tab Acquisizione.", "errore"); return; }
|
||||||
dire("msgElenco", "Caricamento di «" + nome + "»...", "");
|
dire(idMsg, "Caricamento di «" + nome + "»...", "");
|
||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/calibrazione-attiva", { method: "PUT", headers: JSON_H, body: JSON.stringify({ source: sorgente, nome: nome }) });
|
var r = await fetch("/camera/calibrazione-attiva", { method: "PUT", headers: JSON_H, body: JSON.stringify({ source: sorgente, nome: nome }) });
|
||||||
if (sorgente !== $("sorgente").value) return;
|
if (sorgente !== $("sorgente").value) return;
|
||||||
if (!r.ok) { dire("msgElenco", await dettaglio(r), "errore"); return; }
|
if (!r.ok) { dire(idMsg, await dettaglio(r), "errore"); disegnaSceltaCal(); return; } // #93: la combo torna sull'attiva
|
||||||
dire("msgElenco", "Calibrazione «" + nome + "» caricata sulla sorgente.", "ok");
|
dire(idMsg, "Calibrazione «" + nome + "» caricata sulla sorgente.", "ok");
|
||||||
aggiornaStato();
|
A.bloccoCal = false; // #92
|
||||||
aggiornaCalibrata(); // #92: il riquadro calibrata usa subito la nuova calibrazione
|
await aggiornaStato(); // prima lo stato nuovo (rilegge anche l'elenco, #93: schede e combo allineate), poi il riquadro calibrata
|
||||||
|
aggiornaCalibrata();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
dire(idMsg, "Servizio non raggiungibile.", "errore");
|
||||||
|
disegnaSceltaCal(); // #93
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// «Scollega»: la sorgente torna senza calibrazione, la libreria resta
|
// «Scollega»: la sorgente torna senza calibrazione, la libreria resta
|
||||||
async function scollega() {
|
async function scollega(idMsg) {
|
||||||
|
idMsg = idMsg || "msgElenco"; // #93
|
||||||
var sorgente = $("sorgente").value;
|
var sorgente = $("sorgente").value;
|
||||||
if (!sorgente) return;
|
if (!sorgente) return;
|
||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" });
|
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente), { method: "DELETE" });
|
||||||
dire("msgElenco", r.ok ? "Calibrazione scollegata dalla sorgente (resta fra le salvate)." : await dettaglio(r), r.ok ? "ok" : "errore");
|
dire(idMsg, r.ok ? "Calibrazione scollegata dalla sorgente (resta fra le salvate)." : await dettaglio(r), r.ok ? "ok" : "errore");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dire("msgElenco", "Servizio non raggiungibile.", "errore");
|
dire(idMsg, "Servizio non raggiungibile.", "errore");
|
||||||
}
|
}
|
||||||
aggiornaStato();
|
A.bloccoCal = false; // #92
|
||||||
aggiornaCalibrata(); // #92: senza calibrazione il riquadro mostra il 409 del servizio
|
await aggiornaStato(); // #93: rilegge anche l'elenco, quindi la combo torna sull'attiva anche dopo un errore
|
||||||
|
aggiornaCalibrata(); // #92: senza calibrazione il riquadro dice che non c'e'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #93: CONFERMA sotto la calibrata: voce scelta -> PUT calibrazione-attiva, «nessuna» -> DELETE calibration
|
||||||
|
$("confermaCal").onclick = function () {
|
||||||
|
var nome = $("sceltaCal").value;
|
||||||
|
if (nome) caricaNome(nome, "msgSceltaCal"); else scollega("msgSceltaCal");
|
||||||
|
};
|
||||||
|
|
||||||
// «Elimina» di una riga: conferma in pagina con un secondo clic entro 4 s (niente finestre del browser)
|
// «Elimina» di una riga: conferma in pagina con un secondo clic entro 4 s (niente finestre del browser)
|
||||||
function annullaConferma() {
|
function annullaConferma() {
|
||||||
clearTimeout(C.confermaTimer);
|
clearTimeout(C.confermaTimer);
|
||||||
@@ -1080,11 +1237,13 @@ async function calcola() {
|
|||||||
var f = new FormData();
|
var f = new FormData();
|
||||||
f.append("square_mm", String(lato)); // al servizio sempre con il punto
|
f.append("square_mm", String(lato)); // al servizio sempre con il punto
|
||||||
f.append("source", sorgente);
|
f.append("source", sorgente);
|
||||||
fermaAnteprima(); // #92: in questa tab il ciclo e' gia' sospeso; qui si aspetta anche l'eventuale richiesta ancora in volo
|
var bloccata = A.bloccata; // #94: immagine bloccata = calcolo sulla copia del servizio, l'anteprima puo' continuare
|
||||||
|
if (bloccata) f.append("bloccata", "true");
|
||||||
|
else fermaAnteprima(); // #92: si ferma il ciclo; sotto si aspetta anche l'eventuale richiesta ancora in volo
|
||||||
messaggio("Calcolo in corso...", "");
|
messaggio("Calcolo in corso...", "");
|
||||||
["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; });
|
["acquisisci", "salva"].forEach(function (i) { $(i).disabled = true; });
|
||||||
try {
|
try {
|
||||||
while (A.inVolo) await pausa(50);
|
while (!bloccata && A.inVolo) await pausa(50);
|
||||||
var r = await fetch("/camera/calibrate", { method: "POST", body: f });
|
var r = await fetch("/camera/calibrate", { method: "POST", body: f });
|
||||||
if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; }
|
if (!r.ok) { messaggio(await dettaglio(r), "errore"); return; }
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
@@ -1092,7 +1251,7 @@ async function calcola() {
|
|||||||
tabella($("tabella"), d);
|
tabella($("tabella"), d);
|
||||||
$("overlay").src = "data:image/png;base64," + d.overlay_png;
|
$("overlay").src = "data:image/png;base64," + d.overlay_png;
|
||||||
$("avviso").textContent = avvisi(d);
|
$("avviso").textContent = avvisi(d);
|
||||||
$("origineCalcolo").textContent = "Origine: " + descriviOrigine(d.origine);
|
$("origineCalcolo").textContent = "Origine: " + (d.bloccata ? "immagine bloccata — " : "") + descriviOrigine(d.origine); // #94
|
||||||
$("risultato").hidden = false;
|
$("risultato").hidden = false;
|
||||||
annullaSovrascrivi();
|
annullaSovrascrivi();
|
||||||
dire("msgSalva", "");
|
dire("msgSalva", "");
|
||||||
|
|||||||
+165
-8
@@ -68,6 +68,9 @@ Uso (dalla radice del repo):
|
|||||||
[--host 127.0.0.1] [--port 8765] [--folder <cartella immagini>] \
|
[--host 127.0.0.1] [--port 8765] [--folder <cartella immagini>] \
|
||||||
[--config-dir <cartella calibrazioni>]
|
[--config-dir <cartella calibrazioni>]
|
||||||
--folder abilita la sorgente finta `camera:folder` (loop sulle immagini).
|
--folder abilita la sorgente finta `camera:folder` (loop sulle immagini).
|
||||||
|
Immagini caricate (#92): POST/DELETE /camera/immagini, in <config-dir>/immagini; sono sorgenti
|
||||||
|
a pieno titolo (/camera/sources, grab, anteprima, calibrate). POST /camera/anteprima/calibrata
|
||||||
|
raddrizza l'ultimo frame grezzo dell'anteprima con la calibrazione attiva, senza leggerne uno nuovo.
|
||||||
Da Docker su Linux: --host 172.17.0.1 (gateway di docker0).
|
Da Docker su Linux: --host 172.17.0.1 (gateway di docker0).
|
||||||
|
|
||||||
Dipende da: venv di VisionSuite (`uv sync --all-packages` in vendor/visionsuite):
|
Dipende da: venv di VisionSuite (`uv sync --all-packages` in vendor/visionsuite):
|
||||||
@@ -82,8 +85,11 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re # #92: nome sicuro per le immagini caricate
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time # #92: scadenza della cache delle camere
|
||||||
|
import urllib.parse # #92: nome calibrazione nell'header X-Calibrazione
|
||||||
import uuid # #91 id dei calcoli in attesa di nome
|
import uuid # #91 id dei calcoli in attesa di nome
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
@@ -96,7 +102,7 @@ from fastapi.concurrency import run_in_threadpool # #83: il grab bloccante fuor
|
|||||||
from fastapi.responses import HTMLResponse, Response
|
from fastapi.responses import HTMLResponse, Response
|
||||||
|
|
||||||
from visionsuite.camera.camera_interface import CameraError, CameraGrabError, CameraStateError
|
from visionsuite.camera.camera_interface import CameraError, CameraGrabError, CameraStateError
|
||||||
from visionsuite.camera.sources import SourceNotFound, list_sources, open_source
|
from visionsuite.camera.sources import _ESTENSIONI, SourceNotFound, list_sources, open_source # #92: _ESTENSIONI = tipi ammessi per le immagini caricate
|
||||||
|
|
||||||
# #83: calibrazione e pagina stanno accanto a questo file (che non e' un pacchetto)
|
# #83: calibrazione e pagina stanno accanto a questo file (che non e' un pacchetto)
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
@@ -105,6 +111,7 @@ from calibrazione_pagina import PAGINA # noqa: E402
|
|||||||
|
|
||||||
_TRIGGER = ("continuo",)
|
_TRIGGER = ("continuo",)
|
||||||
_MAX_CALCOLI = 8 # #91: calcoli tenuti in memoria in attesa di un nome
|
_MAX_CALCOLI = 8 # #91: calcoli tenuti in memoria in attesa di un nome
|
||||||
|
_CACHE_CAMERE_S = 60.0 # #92: durata della cache della ricerca camere
|
||||||
_log = logging.getLogger("camera_service") # #87: errori non bloccanti (settaggi)
|
_log = logging.getLogger("camera_service") # #87: errori non bloccanti (settaggi)
|
||||||
# #87: i quattro parametri regolabili, nell'ordine di presentazione
|
# #87: i quattro parametri regolabili, nell'ordine di presentazione
|
||||||
_PARAMETRI = ("esposizione", "esposizione_auto", "guadagno", "guadagno_auto")
|
_PARAMETRI = ("esposizione", "esposizione_auto", "guadagno", "guadagno_auto")
|
||||||
@@ -148,6 +155,9 @@ def create_app(folder: Path | str | None = None,
|
|||||||
archivio = calibrazione.Archivio(config_dir if config_dir is not None
|
archivio = calibrazione.Archivio(config_dir if config_dir is not None
|
||||||
else _config_dir_default()) # #83
|
else _config_dir_default()) # #83
|
||||||
aperte: dict = {}
|
aperte: dict = {}
|
||||||
|
grezzi: dict = {} # #92: sorgente -> ultimo frame pieno GREZZO dell'anteprima (per anteprima/calibrata)
|
||||||
|
bloccati: dict = {} # #94: sorgente -> copia del frame grezzo pieno «bloccata» per il calcolo (stesso lock di grezzi)
|
||||||
|
cartella_immagini = archivio.cartella / "immagini" # #92: immagini caricate dalla pagina (creata al bisogno)
|
||||||
lock = threading.Lock() # protegge SOLO i dizionari, mai un'apertura
|
lock = threading.Lock() # protegge SOLO i dizionari, mai un'apertura
|
||||||
lock_sorgenti: dict = {} # nome sorgente -> Lock che serializza apertura e grab
|
lock_sorgenti: dict = {} # nome sorgente -> Lock che serializza apertura e grab
|
||||||
calcoli: OrderedDict = OrderedDict() # #91: calcolo_id -> (dati, mappa); gli ultimi _MAX_CALCOLI
|
calcoli: OrderedDict = OrderedDict() # #91: calcolo_id -> (dati, mappa); gli ultimi _MAX_CALCOLI
|
||||||
@@ -162,8 +172,32 @@ def create_app(folder: Path | str | None = None,
|
|||||||
with lock:
|
with lock:
|
||||||
return lock_sorgenti.setdefault(nome, threading.Lock())
|
return lock_sorgenti.setdefault(nome, threading.Lock())
|
||||||
|
|
||||||
|
# #92: la ricerca camere di list_sources dura ~10 s: il risultato (gia' filtrato) resta 60 s in cache
|
||||||
|
cache_camere: dict = {"voci": None, "t": 0.0}
|
||||||
|
lock_ricerca = threading.Lock() # due richieste insieme non lanciano due ricerche
|
||||||
|
|
||||||
|
def _camere(aggiorna: bool = False) -> list:
|
||||||
|
with lock_ricerca:
|
||||||
|
fresca = cache_camere["voci"] is not None and time.monotonic() - cache_camere["t"] < _CACHE_CAMERE_S
|
||||||
|
if fresca and not aggiorna:
|
||||||
|
return cache_camere["voci"]
|
||||||
|
voci = list_sources(None, cameras=True, config=_config())
|
||||||
|
# camera:file (replay) non e' mai configurato in TMF -> fuori dall'elenco
|
||||||
|
cache_camere["voci"] = [{"name": v.name, "kind": v.kind, "label": v.label}
|
||||||
|
for v in voci if v.kind in ("camera", "virtual") and v.name != "camera:file"]
|
||||||
|
cache_camere["t"] = time.monotonic()
|
||||||
|
return cache_camere["voci"]
|
||||||
|
|
||||||
|
def _prima_ricerca() -> None:
|
||||||
|
try:
|
||||||
|
_camere()
|
||||||
|
except Exception: # noqa: BLE001 - e' solo un riscaldamento: la prima richiesta riprova
|
||||||
|
_log.exception("prima ricerca camere fallita")
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _vita(_app):
|
async def _vita(_app):
|
||||||
|
# #92: la prima ricerca parte all'avvio, cosi' la prima pagina non aspetta
|
||||||
|
threading.Thread(target=_prima_ricerca, daemon=True, name="ricerca-camere").start()
|
||||||
yield
|
yield
|
||||||
with lock: # alla chiusura rilascia le camere aperte
|
with lock: # alla chiusura rilascia le camere aperte
|
||||||
sorgenti = list(aperte.values())
|
sorgenti = list(aperte.values())
|
||||||
@@ -177,17 +211,69 @@ def create_app(folder: Path | str | None = None,
|
|||||||
app = FastAPI(title="TMF camera service", lifespan=_vita)
|
app = FastAPI(title="TMF camera service", lifespan=_vita)
|
||||||
|
|
||||||
@app.get("/camera/sources")
|
@app.get("/camera/sources")
|
||||||
def camera_sources():
|
def camera_sources(aggiorna: bool = False): # #92: aggiorna=true forza la ricerca (camera appena collegata)
|
||||||
voci = list_sources(None, cameras=True, config=_config())
|
elenco = list(_camere(aggiorna))
|
||||||
return {"sources": [{"name": v.name, "kind": v.kind, "label": v.label}
|
# #92: in coda le immagini caricate, sempre fresche dalla cartella (costano nulla)
|
||||||
for v in voci if v.kind in ("camera", "virtual")]}
|
for v in list_sources(cartella_immagini, cameras=False):
|
||||||
|
elenco.append({"name": v.name, "kind": "image", "label": f"immagine — {v.name}"})
|
||||||
|
return {"sources": elenco}
|
||||||
|
|
||||||
|
def _chiudi(nome: str) -> None:
|
||||||
|
"""#92: chiude la sorgente aperta e scorda il frame tenuto (immagine sostituita o tolta)."""
|
||||||
|
with _lock_di(nome):
|
||||||
|
with lock:
|
||||||
|
s = aperte.pop(nome, None)
|
||||||
|
grezzi.pop(nome, None)
|
||||||
|
bloccati.pop(nome, None) # #94: tolta l'immagine/chiusa la sorgente, anche la copia bloccata
|
||||||
|
if s is not None:
|
||||||
|
try:
|
||||||
|
s.close()
|
||||||
|
except Exception: # noqa: BLE001 - la chiusura non deve far fallire la rotta
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _nome_immagine(grezzo: str) -> str:
|
||||||
|
"""#92: solo il nome del file, coi caratteri sicuri di Archivio._file; "" se non ammesso."""
|
||||||
|
base = Path((grezzo or "").replace("\\", "/")).name
|
||||||
|
pulito = re.sub(r"[^A-Za-z0-9._-]+", "_", base).strip("._")
|
||||||
|
radice, punto, estensione = pulito.rpartition(".")
|
||||||
|
if not punto or not radice or f".{estensione.lower()}" not in _ESTENSIONI:
|
||||||
|
return ""
|
||||||
|
return f"{radice[:100]}.{estensione}"
|
||||||
|
|
||||||
|
@app.post("/camera/immagini", status_code=201)
|
||||||
|
async def immagini_carica(image: UploadFile | None = File(None)):
|
||||||
|
if image is None:
|
||||||
|
raise HTTPException(400, "manca il file «image»")
|
||||||
|
nome = _nome_immagine(image.filename or "")
|
||||||
|
if not nome:
|
||||||
|
raise HTTPException(400, "estensione non ammessa: " + ", ".join(_ESTENSIONI))
|
||||||
|
contenuto = await image.read()
|
||||||
|
# #92: 422 se non e' un'immagine; la verifica sta PRIMA della scrittura, cosi' non resta nulla su disco
|
||||||
|
if not contenuto or cv2.imdecode(np.frombuffer(contenuto, np.uint8), cv2.IMREAD_UNCHANGED) is None:
|
||||||
|
raise HTTPException(422, "il file caricato non è leggibile come immagine")
|
||||||
|
cartella_immagini.mkdir(parents=True, exist_ok=True)
|
||||||
|
(cartella_immagini / nome).write_bytes(contenuto) # stesso nome = sostituisce
|
||||||
|
await run_in_threadpool(_chiudi, nome) # se era aperta, si riapre sul file nuovo
|
||||||
|
return {"source": nome, "label": f"immagine — {nome}", "kind": "image"}
|
||||||
|
|
||||||
|
@app.delete("/camera/immagini", status_code=204)
|
||||||
|
def immagini_elimina(nome: str = ""):
|
||||||
|
percorso = cartella_immagini / nome
|
||||||
|
# #92: solo il nome di un file dentro la cartella, niente percorsi
|
||||||
|
if not nome or Path(nome).name != nome or not percorso.is_file():
|
||||||
|
raise HTTPException(404, "immagine non trovata")
|
||||||
|
_chiudi(nome)
|
||||||
|
percorso.unlink()
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
def _apri(nome: str):
|
def _apri(nome: str):
|
||||||
"""#87: sorgente aperta (o da aprire); da chiamare SOLO sotto _lock_di(nome)."""
|
"""#87: sorgente aperta (o da aprire); da chiamare SOLO sotto _lock_di(nome)."""
|
||||||
with lock:
|
with lock:
|
||||||
sorgente = aperte.get(nome)
|
sorgente = aperte.get(nome)
|
||||||
if sorgente is None:
|
if sorgente is None:
|
||||||
sorgente = open_source(nome, None, config=_config())
|
sorgente = open_source(nome, cartella_immagini, config=_config()) # #92: un nome di file = immagine caricata
|
||||||
|
if hasattr(sorgente, "loop"): # #92: l'immagine singola non deve "finire" al secondo grab (anteprima continua)
|
||||||
|
sorgente.loop = True
|
||||||
# TieVision-Core#341 / #89: free-run, serve la scena di adesso, non il frame vecchio in coda
|
# TieVision-Core#341 / #89: free-run, serve la scena di adesso, non il frame vecchio in coda
|
||||||
if hasattr(sorgente, "solo_ultimo_frame"):
|
if hasattr(sorgente, "solo_ultimo_frame"):
|
||||||
try:
|
try:
|
||||||
@@ -284,6 +370,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
rows: int | None = Form(None),
|
rows: int | None = Form(None),
|
||||||
image: UploadFile | None = File(None),
|
image: UploadFile | None = File(None),
|
||||||
save: bool = Form(False), # #91: ignorato, il calcolo non salva mai (si salva con nome)
|
save: bool = Form(False), # #91: ignorato, il calcolo non salva mai (si salva con nome)
|
||||||
|
bloccata: bool = Form(False), # #94: true = calcola sulla copia bloccata, senza grab
|
||||||
):
|
):
|
||||||
contenuto = await image.read() if image is not None else b""
|
contenuto = await image.read() if image is not None else b""
|
||||||
if contenuto:
|
if contenuto:
|
||||||
@@ -291,6 +378,13 @@ def create_app(folder: Path | str | None = None,
|
|||||||
if frame is None:
|
if frame is None:
|
||||||
raise HTTPException(422, "l'immagine caricata non è leggibile")
|
raise HTTPException(422, "l'immagine caricata non è leggibile")
|
||||||
origine = "file:" + (Path((image.filename or "").replace("\\", "/")).name or "immagine")
|
origine = "file:" + (Path((image.filename or "").replace("\\", "/")).name or "immagine")
|
||||||
|
elif bloccata: # #94: copia bloccata, nessun grab e nessun lock camera
|
||||||
|
nome = _sorgente(source)
|
||||||
|
with lock:
|
||||||
|
frame = bloccati.get(nome)
|
||||||
|
if frame is None:
|
||||||
|
raise HTTPException(409, "nessuna immagine bloccata su questa sorgente")
|
||||||
|
origine = nome
|
||||||
else:
|
else:
|
||||||
nome = _sorgente(source)
|
nome = _sorgente(source)
|
||||||
frame = await run_in_threadpool(_grab, nome, 5000) # come /camera/grab (404/503/504)
|
frame = await run_in_threadpool(_grab, nome, 5000) # come /camera/grab (404/503/504)
|
||||||
@@ -313,7 +407,8 @@ def create_app(folder: Path | str | None = None,
|
|||||||
calcoli[calcolo_id] = (corpo, mappa)
|
calcoli[calcolo_id] = (corpo, mappa)
|
||||||
while len(calcoli) > _MAX_CALCOLI:
|
while len(calcoli) > _MAX_CALCOLI:
|
||||||
calcoli.popitem(last=False)
|
calcoli.popitem(last=False)
|
||||||
return {**corpo, "calcolo_id": calcolo_id, "overlay_png": risultato["overlay_png"]}
|
return {**corpo, "calcolo_id": calcolo_id, "overlay_png": risultato["overlay_png"],
|
||||||
|
"bloccata": bool(bloccata and not contenuto)} # #94: solo nella risposta (non nel salvato): true se il frame e' la copia bloccata
|
||||||
|
|
||||||
# ---- #91: archivio unico di calibrazioni con nome
|
# ---- #91: archivio unico di calibrazioni con nome
|
||||||
|
|
||||||
@@ -414,7 +509,12 @@ def create_app(folder: Path | str | None = None,
|
|||||||
timeout = corpo.get("timeout_ms", 5000)
|
timeout = corpo.get("timeout_ms", 5000)
|
||||||
if isinstance(timeout, bool) or not isinstance(timeout, int) or not 1 <= timeout <= 10000:
|
if isinstance(timeout, bool) or not isinstance(timeout, int) or not 1 <= timeout <= 10000:
|
||||||
raise HTTPException(400, "«timeout_ms» deve essere un intero fra 1 e 10000")
|
raise HTTPException(400, "«timeout_ms» deve essere un intero fra 1 e 10000")
|
||||||
frame = _grab(nome, timeout)
|
def leggi(s): # #92: il frame grezzo si tiene sotto il lock della sorgente
|
||||||
|
f = s.grab(timeout)
|
||||||
|
with lock:
|
||||||
|
grezzi[nome] = f.copy()
|
||||||
|
return f
|
||||||
|
frame = _con_sorgente(nome, leggi)
|
||||||
raddrizzato = False
|
raddrizzato = False
|
||||||
if not grezzo: # come /camera/grab
|
if not grezzo: # come /camera/grab
|
||||||
mappa = archivio.leggi_mappa(nome)
|
mappa = archivio.leggi_mappa(nome)
|
||||||
@@ -442,6 +542,63 @@ def create_app(folder: Path | str | None = None,
|
|||||||
"X-Nitidezza": f"{nitidezza:.1f}", "X-Saturati-Pct": f"{saturati:.2f}",
|
"X-Nitidezza": f"{nitidezza:.1f}", "X-Saturati-Pct": f"{saturati:.2f}",
|
||||||
"X-Media": f"{media:.1f}"})
|
"X-Media": f"{media:.1f}"})
|
||||||
|
|
||||||
|
@app.post("/camera/blocca")
|
||||||
|
def camera_blocca(corpo: dict = Body(default=None)):
|
||||||
|
"""#94: copia l'ultimo frame grezzo della sorgente; /camera/calibrate con bloccata=true calcola su quella."""
|
||||||
|
corpo = corpo if isinstance(corpo, dict) else {}
|
||||||
|
nome = _sorgente(corpo.get("source"))
|
||||||
|
with lock:
|
||||||
|
frame = grezzi.get(nome)
|
||||||
|
if frame is None:
|
||||||
|
raise HTTPException(404, "nessun frame: avviare l'anteprima o scattare")
|
||||||
|
bloccati[nome] = frame.copy()
|
||||||
|
alt, larg = frame.shape[:2]
|
||||||
|
return {"source": nome, "larghezza": larg, "altezza": alt}
|
||||||
|
|
||||||
|
@app.delete("/camera/blocca", status_code=204)
|
||||||
|
def camera_sblocca(source: str = ""):
|
||||||
|
with lock: # #94: idempotente, 204 anche se non c'era nulla
|
||||||
|
bloccati.pop(source.strip(), None)
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
@app.post("/camera/anteprima/calibrata")
|
||||||
|
def camera_anteprima_calibrata(corpo: dict = Body(default=None)):
|
||||||
|
"""#92: l'ultimo frame grezzo dell'anteprima, raddrizzato; NON legge un frame nuovo."""
|
||||||
|
corpo = corpo if isinstance(corpo, dict) else {}
|
||||||
|
nome = _sorgente(corpo.get("source"))
|
||||||
|
lato = corpo.get("lato_max", 1280)
|
||||||
|
if isinstance(lato, bool) or not isinstance(lato, int) or not 320 <= lato <= 4096:
|
||||||
|
raise HTTPException(400, "«lato_max» deve essere un intero fra 320 e 4096")
|
||||||
|
with lock:
|
||||||
|
frame = grezzi.get(nome)
|
||||||
|
if frame is None:
|
||||||
|
raise HTTPException(404, "nessun frame: avviare l'anteprima o scattare")
|
||||||
|
attiva = archivio.attiva_di(nome)
|
||||||
|
if attiva is None:
|
||||||
|
raise HTTPException(409, "nessuna calibrazione attiva su questa sorgente")
|
||||||
|
mappa = archivio.leggi_mappa(nome)
|
||||||
|
if mappa is None: # planare: ha solo scala, nessuna mappa da applicare
|
||||||
|
raise HTTPException(409, f"la calibrazione «{attiva}» non ha una mappa di correzione (planare)")
|
||||||
|
corretto = calibrazione.raddrizza(frame, mappa)
|
||||||
|
if corretto is None: # risoluzione diversa da quella calibrata
|
||||||
|
ch, cw = mappa.shape[:2]
|
||||||
|
alt, larg = frame.shape[:2]
|
||||||
|
raise HTTPException(409, f"la calibrazione «{attiva}» è per immagini {cw}×{ch}, "
|
||||||
|
f"il frame è {larg}×{alt}")
|
||||||
|
alt, larg = corretto.shape[:2]
|
||||||
|
if corretto.dtype != np.uint8: # il JPEG vuole 8 bit
|
||||||
|
corretto = _grigio8(corretto)
|
||||||
|
scala = lato / max(alt, larg)
|
||||||
|
if scala < 1.0: # mai ingrandita
|
||||||
|
corretto = cv2.resize(corretto, (max(1, round(larg * scala)), max(1, round(alt * scala))),
|
||||||
|
interpolation=cv2.INTER_AREA)
|
||||||
|
ok, buf = cv2.imencode(".jpg", corretto, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
||||||
|
if not ok:
|
||||||
|
raise HTTPException(500, "codifica JPEG fallita")
|
||||||
|
return Response(buf.tobytes(), media_type="image/jpeg", headers={
|
||||||
|
"X-Raddrizzato": "si", "X-Larghezza": str(larg), "X-Altezza": str(alt),
|
||||||
|
"X-Calibrazione": urllib.parse.quote(attiva, safe="")})
|
||||||
|
|
||||||
@app.get("/camera/parametri")
|
@app.get("/camera/parametri")
|
||||||
def parametri_leggi(source: str = ""):
|
def parametri_leggi(source: str = ""):
|
||||||
nome = _sorgente(source)
|
nome = _sorgente(source)
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""Test di #94: «Blocca immagine» (scripts/camera_service.py) e riquadro vivo nella tab Calibrazione (PAGINA).
|
||||||
|
|
||||||
|
Uso: uv run --python 3.13 --extra vision-worker --extra dev \
|
||||||
|
--with-editable vendor/visionsuite/packages/vs-camera pytest scripts/tests -q
|
||||||
|
Sorgente finta che conta i grab: niente camera vera, niente ProgramData.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from test_calibrazione import _png, cartella, client, una_scacchiera # fixture e helper della calibrazione
|
||||||
|
from test_immagini_affiancate import _anteprima, _carica, _modulo
|
||||||
|
from test_pagina_acquisizione import PAGINA
|
||||||
|
|
||||||
|
__all__ = ["cartella", "client"] # le fixture importate restano usate
|
||||||
|
|
||||||
|
FINTA = "finta"
|
||||||
|
CAMPI = {"cols": "9", "rows": "6", "square_mm": "2"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def finta(client, monkeypatch):
|
||||||
|
"""Sorgente «finta»: il primo grab da' la scacchiera, i successivi un frame bianco (non calibrabile)."""
|
||||||
|
letture = []
|
||||||
|
scacchiera = una_scacchiera()
|
||||||
|
bianco = np.full(scacchiera.shape, 255, np.uint8)
|
||||||
|
|
||||||
|
class Finta:
|
||||||
|
def grab(self, timeout=1000):
|
||||||
|
letture.append(1)
|
||||||
|
return scacchiera.copy() if len(letture) == 1 else bianco.copy()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.setitem(_modulo(client), "open_source", lambda *a, **k: Finta())
|
||||||
|
client.letture = letture
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _blocca(c, nome=FINTA):
|
||||||
|
return c.post("/camera/blocca", json={"source": nome})
|
||||||
|
|
||||||
|
|
||||||
|
def _calcola(c, **extra):
|
||||||
|
return c.post("/camera/calibrate", data={"source": FINTA, **CAMPI, **extra})
|
||||||
|
|
||||||
|
|
||||||
|
# ---- servizio
|
||||||
|
|
||||||
|
def test_blocca_senza_frame_404(finta):
|
||||||
|
r = _blocca(finta)
|
||||||
|
assert r.status_code == 404 and r.json()["detail"] == "nessun frame: avviare l'anteprima o scattare"
|
||||||
|
|
||||||
|
|
||||||
|
def test_blocca_senza_source_400(finta):
|
||||||
|
assert finta.post("/camera/blocca", json={}).status_code == 400
|
||||||
|
assert finta.post("/camera/blocca").status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_anteprima_poi_blocca_200_con_misure(finta):
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200
|
||||||
|
r = _blocca(finta)
|
||||||
|
alt, larg = una_scacchiera().shape[:2]
|
||||||
|
assert r.status_code == 200 and r.json() == {"source": FINTA, "larghezza": larg, "altezza": alt}
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_bloccata_usa_la_copia_senza_grab(finta):
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200 # frame 1: la scacchiera
|
||||||
|
assert _blocca(finta).status_code == 200
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200 # frame 2: bianco, il grezzo cambia ma la copia no
|
||||||
|
prima = len(finta.letture)
|
||||||
|
r = _calcola(finta, bloccata="true")
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["bloccata"] is True and r.json()["origine"] == FINTA
|
||||||
|
assert len(finta.letture) == prima # nessun grab per il calcolo
|
||||||
|
# senza il blocco il calcolo legge un frame nuovo (bianco): non calibrabile
|
||||||
|
assert _calcola(finta).status_code == 422
|
||||||
|
assert len(finta.letture) == prima + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_normale_dice_bloccata_false(client):
|
||||||
|
r = client.post("/camera/calibrate", data={"source": "camera:folder", **CAMPI})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["bloccata"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_bloccata_senza_copia_409(finta):
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200 # il grezzo c'e', ma non e' bloccato
|
||||||
|
r = _calcola(finta, bloccata="true")
|
||||||
|
assert r.status_code == 409 and r.json()["detail"] == "nessuna immagine bloccata su questa sorgente"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sblocca_204_idempotente_e_toglie_la_copia(finta):
|
||||||
|
assert finta.delete("/camera/blocca", params={"source": FINTA}).status_code == 204 # niente da togliere
|
||||||
|
assert _anteprima(finta, FINTA).status_code == 200
|
||||||
|
assert _blocca(finta).status_code == 200
|
||||||
|
assert finta.delete("/camera/blocca", params={"source": FINTA}).status_code == 204
|
||||||
|
assert _calcola(finta, bloccata="true").status_code == 409
|
||||||
|
assert finta.delete("/camera/blocca", params={"source": FINTA}).status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_rimuovere_l_immagine_toglie_la_copia_bloccata(client):
|
||||||
|
assert _carica(client, "foto.png", una_scacchiera()).status_code == 201
|
||||||
|
assert _anteprima(client, "foto.png").status_code == 200
|
||||||
|
assert _blocca(client, "foto.png").status_code == 200
|
||||||
|
assert client.delete("/camera/immagini", params={"nome": "foto.png"}).status_code == 204
|
||||||
|
assert _carica(client, "foto.png", una_scacchiera()).status_code == 201
|
||||||
|
r = client.post("/camera/calibrate", data={"source": "foto.png", "bloccata": "true", **CAMPI})
|
||||||
|
assert r.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
# ---- pagina (contenuto statico)
|
||||||
|
|
||||||
|
def test_pagina_riquadro_e_casella_nella_tab_calibrazione():
|
||||||
|
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||||
|
assert {"immagineCalTab", "vuotoCalTab", "scattaCal", "bloccaImg", "msgBlocco"} <= ids
|
||||||
|
assert "Nessuna immagine: «Scatta» o avviare l'anteprima nella tab Acquisizione" in PAGINA
|
||||||
|
assert "Blocca immagine" in PAGINA
|
||||||
|
tab = PAGINA.split('id="pannelloCal"')[1].split('id="sezioneSalvate"')[0]
|
||||||
|
assert "immagineCalTab" in tab and "bloccaImg" in tab # nella sezione Calibrazione, non altrove
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_scegli_tab_non_ferma_piu_il_ciclo():
|
||||||
|
assert "A.riprendi" not in PAGINA
|
||||||
|
scegli = PAGINA.split("function scegliTab")[1].split("$(\"tabAcq\").onclick")[0]
|
||||||
|
assert "fermaAnteprima" not in scegli and "avviaAnteprima" not in scegli
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_calcola_manda_bloccata_e_non_ferma_il_ciclo():
|
||||||
|
calcola = PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
|
||||||
|
assert 'f.append("bloccata", "true")' in calcola
|
||||||
|
assert "if (bloccata)" in calcola and "else fermaAnteprima();" in calcola
|
||||||
|
assert "immagine bloccata" in calcola
|
||||||
|
assert "l'anteprima resta sospesa" not in PAGINA and "si può bloccare l'immagine" in PAGINA
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_blocco_rotte_e_cambio_sorgente():
|
||||||
|
assert '"/camera/blocca"' in PAGINA and '"/camera/blocca?source="' in PAGINA
|
||||||
|
assert "method: \"DELETE\"" in PAGINA
|
||||||
|
cambio = PAGINA.split("function cambioSorgente")[1].split("// ---- Anteprima")[0]
|
||||||
|
assert "rilasciaBlocco()" in cambio
|
||||||
|
assert cambio.index("rilasciaBlocco()") < cambio.index("svuotaRiquadri()")
|
||||||
@@ -169,7 +169,7 @@ def test_c2_immagine_caricata_senza_salvare(client):
|
|||||||
files={"image": ("s.png", _png(una_scacchiera()), "image/png")})
|
files={"image": ("s.png", _png(una_scacchiera()), "image/png")})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
corpo = r.json()
|
corpo = r.json()
|
||||||
assert CHIAVI_PLANARE | {"overlay_png", "calcolo_id"} == set(corpo) # #91: niente "saved", niente "source"
|
assert CHIAVI_PLANARE | {"overlay_png", "calcolo_id", "bloccata"} == set(corpo) # #94: + bloccata; #91: niente "saved", niente "source"
|
||||||
assert corpo["metodo"] == "planare" and corpo["distorsione"] is False # #84: con cols/rows nulla cambia
|
assert corpo["metodo"] == "planare" and corpo["distorsione"] is False # #84: con cols/rows nulla cambia
|
||||||
assert corpo["origine"] == "file:s.png"
|
assert corpo["origine"] == "file:s.png"
|
||||||
assert corpo["px_per_mm"] == pytest.approx(20.0, rel=0.005)
|
assert corpo["px_per_mm"] == pytest.approx(20.0, rel=0.005)
|
||||||
@@ -337,7 +337,7 @@ def test_c2_senza_cols_rows_e_densa(client_denso):
|
|||||||
r = client_denso.post("/camera/calibrate", data={"source": SORGENTE, "square_mm": "2"})
|
r = client_denso.post("/camera/calibrate", data={"source": SORGENTE, "square_mm": "2"})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
corpo = r.json()
|
corpo = r.json()
|
||||||
assert CHIAVI_DENSA | {"overlay_png", "calcolo_id"} == set(corpo)
|
assert CHIAVI_DENSA | {"overlay_png", "calcolo_id", "bloccata"} == set(corpo)
|
||||||
assert corpo["metodo"] == "densa" and corpo["distorsione"] is True
|
assert corpo["metodo"] == "densa" and corpo["distorsione"] is True
|
||||||
assert (corpo["cols"], corpo["rows"]) == (15, 11)
|
assert (corpo["cols"], corpo["rows"]) == (15, 11)
|
||||||
assert corpo["px_per_mm"] == pytest.approx(1.0 / corpo["mm_per_px"])
|
assert corpo["px_per_mm"] == pytest.approx(1.0 / corpo["mm_per_px"])
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
"""Test di #92: immagini caricate come sorgenti e anteprima calibrata (scripts/camera_service.py).
|
||||||
|
|
||||||
|
Uso: uv run --python 3.13 --extra vision-worker --extra dev \
|
||||||
|
--with-editable vendor/visionsuite/packages/vs-camera pytest scripts/tests -q
|
||||||
|
Solo tmp_path: niente ProgramData, niente camera vera. Le immagini le scrive il test.
|
||||||
|
"""
|
||||||
|
from urllib.parse import quote, unquote
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from test_calibrazione import ( # fixture e helper della calibrazione (#83/#84)
|
||||||
|
CAMPI, _png, carica_su, cartella, client, salva_con_nome, una_scacchiera,
|
||||||
|
una_scacchiera_densa,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ["cartella", "client"] # le fixture importate restano usate
|
||||||
|
|
||||||
|
DENSA = "densa.png"
|
||||||
|
PIANA = "piana.png"
|
||||||
|
|
||||||
|
|
||||||
|
def _modulo(c) -> dict:
|
||||||
|
"""Il dict globale di camera_service (per sostituire open_source con una spia)."""
|
||||||
|
return next(r.endpoint for r in c.app.routes if r.path == "/camera/grab").__globals__
|
||||||
|
|
||||||
|
|
||||||
|
def _carica(c, nome: str, img: np.ndarray, **extra):
|
||||||
|
"""POST /camera/immagini con un'immagine codificata PNG (o il contenuto dato)."""
|
||||||
|
return c.post("/camera/immagini", files={"image": (nome, _png(img), "image/png")}, **extra)
|
||||||
|
|
||||||
|
|
||||||
|
def _densa(**kw) -> np.ndarray:
|
||||||
|
return una_scacchiera_densa(k=1e-7, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def _calibra_e_attiva(c, nome_immagine: str, nome_calibrazione: str, **campi):
|
||||||
|
"""Calcola dalla sorgente, salva con nome e la rende attiva su quella sorgente."""
|
||||||
|
r = c.post("/camera/calibrate", data={"source": nome_immagine, "square_mm": "2", **campi})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert salva_con_nome(c, r.json(), nome_calibrazione).status_code == 201
|
||||||
|
assert carica_su(c, nome_immagine, nome_calibrazione).status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def _anteprima(c, nome: str, **extra):
|
||||||
|
return c.post("/camera/anteprima", json={"source": nome, "grezzo": True, **extra})
|
||||||
|
|
||||||
|
|
||||||
|
def _calibrata(c, nome: str, **extra):
|
||||||
|
return c.post("/camera/anteprima/calibrata", json={"source": nome, **extra})
|
||||||
|
|
||||||
|
|
||||||
|
# ---- POST /camera/immagini
|
||||||
|
|
||||||
|
def test_carica_201_salva_in_cartella_immagini(client):
|
||||||
|
r = _carica(client, "calibration_135_1.bmp", una_scacchiera())
|
||||||
|
assert r.status_code == 201, r.text
|
||||||
|
assert r.json() == {"source": "calibration_135_1.bmp",
|
||||||
|
"label": "immagine — calibration_135_1.bmp", "kind": "image"}
|
||||||
|
assert (client.config / "immagini" / "calibration_135_1.bmp").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_carica_ripulisce_il_nome_e_toglie_il_percorso(client):
|
||||||
|
r = _carica(client, "C:\\foto\\ban co è1.PNG", una_scacchiera())
|
||||||
|
assert r.status_code == 201, r.text
|
||||||
|
assert r.json()["source"] == "ban_co_1.PNG" # niente percorso, caratteri sicuri, estensione intatta
|
||||||
|
assert (client.config / "immagini" / "ban_co_1.PNG").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_carica_stesso_nome_sostituisce(client):
|
||||||
|
_carica(client, "a.png", np.full((48, 64), 10, np.uint8))
|
||||||
|
assert _carica(client, "a.png", np.full((30, 40), 200, np.uint8)).status_code == 201
|
||||||
|
assert [f.name for f in (client.config / "immagini").iterdir()] == ["a.png"]
|
||||||
|
img = cv2.imread(str(client.config / "immagini" / "a.png"), cv2.IMREAD_UNCHANGED)
|
||||||
|
assert img.shape[:2] == (30, 40)
|
||||||
|
|
||||||
|
|
||||||
|
def test_carica_senza_file_400(client):
|
||||||
|
assert client.post("/camera/immagini").status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_carica_estensione_non_ammessa_400(client):
|
||||||
|
r = client.post("/camera/immagini", files={"image": ("note.txt", b"ciao", "text/plain")})
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert not (client.config / "immagini").exists() or not list((client.config / "immagini").iterdir())
|
||||||
|
|
||||||
|
|
||||||
|
def test_carica_file_illeggibile_422_e_niente_su_disco(client):
|
||||||
|
r = client.post("/camera/immagini", files={"image": ("rotta.png", b"non e una immagine", "image/png")})
|
||||||
|
assert r.status_code == 422
|
||||||
|
assert not (client.config / "immagini").exists() or not list((client.config / "immagini").iterdir())
|
||||||
|
|
||||||
|
|
||||||
|
# ---- GET /camera/sources
|
||||||
|
|
||||||
|
def test_sources_con_immagini_e_senza_camera_file(client):
|
||||||
|
_carica(client, "b.png", una_scacchiera())
|
||||||
|
_carica(client, "a.png", una_scacchiera())
|
||||||
|
voci = client.get("/camera/sources").json()["sources"]
|
||||||
|
nomi = [v["name"] for v in voci]
|
||||||
|
assert "camera:file" not in nomi
|
||||||
|
assert "camera:folder" in nomi
|
||||||
|
assert nomi[-2:] == ["a.png", "b.png"] # le immagini stanno in coda
|
||||||
|
assert voci[-1] == {"name": "b.png", "kind": "image", "label": "immagine — b.png"}
|
||||||
|
assert nomi.index("camera:folder") < nomi.index("a.png")
|
||||||
|
|
||||||
|
|
||||||
|
def test_sources_camere_in_cache_e_aggiorna(client):
|
||||||
|
# #92: la ricerca camere (~10 s) si fa una volta e resta 60 s; aggiorna=true la rilancia; le immagini sono sempre fresche
|
||||||
|
g = _modulo(client)
|
||||||
|
originale = g["list_sources"]
|
||||||
|
chiamate = []
|
||||||
|
|
||||||
|
def finto(cartella, cameras=True, **kw):
|
||||||
|
if cameras:
|
||||||
|
chiamate.append(1)
|
||||||
|
return originale(cartella, cameras=cameras, **kw)
|
||||||
|
|
||||||
|
g["list_sources"] = finto
|
||||||
|
try:
|
||||||
|
client.get("/camera/sources") # se la ricerca d'avvio e' ancora in corso aspetta il suo lock
|
||||||
|
base = len(chiamate)
|
||||||
|
_carica(client, "nuova.png", una_scacchiera())
|
||||||
|
voci = client.get("/camera/sources").json()["sources"]
|
||||||
|
client.get("/camera/sources")
|
||||||
|
assert len(chiamate) == base # seconda e terza chiamata: nessuna nuova ricerca
|
||||||
|
assert voci[-1]["name"] == "nuova.png" # ma l'immagine appena caricata c'e'
|
||||||
|
client.get("/camera/sources", params={"aggiorna": "true"})
|
||||||
|
assert len(chiamate) == base + 1 # aggiorna=true la rilancia
|
||||||
|
client.get("/camera/sources")
|
||||||
|
assert len(chiamate) == base + 1 # e il risultato nuovo torna in cache
|
||||||
|
finally:
|
||||||
|
g["list_sources"] = originale
|
||||||
|
|
||||||
|
|
||||||
|
def test_sources_senza_immagini_non_crea_la_cartella(client):
|
||||||
|
voci = client.get("/camera/sources").json()["sources"]
|
||||||
|
assert all(v["kind"] != "image" for v in voci)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- grab / anteprima / calibrate su un'immagine caricata
|
||||||
|
|
||||||
|
def test_grab_anteprima_e_grab_ripetuti_su_immagine(client):
|
||||||
|
_carica(client, "foto.png", una_scacchiera())
|
||||||
|
for _ in range(3): # l'anteprima continua non deve esaurire un'immagine sola
|
||||||
|
r = client.post("/camera/grab", json={"source": "foto.png"})
|
||||||
|
assert r.status_code == 200 and r.headers["content-type"] == "image/png"
|
||||||
|
for _ in range(3):
|
||||||
|
r = _anteprima(client, "foto.png")
|
||||||
|
assert r.status_code == 200 and r.headers["content-type"] == "image/jpeg"
|
||||||
|
assert (r.headers["X-Larghezza"], r.headers["X-Altezza"]) == ("520", "400")
|
||||||
|
|
||||||
|
|
||||||
|
def test_grab_immagine_inesistente_404(client):
|
||||||
|
assert client.post("/camera/grab", json={"source": "non_c_e.png"}).status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrate_su_immagine_origine_e_il_nome(client):
|
||||||
|
_carica(client, "calibration_135_1.bmp", una_scacchiera())
|
||||||
|
r = client.post("/camera/calibrate", data={"source": "calibration_135_1.bmp", **CAMPI})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.json()["origine"] == "calibration_135_1.bmp"
|
||||||
|
|
||||||
|
|
||||||
|
# ---- DELETE /camera/immagini
|
||||||
|
|
||||||
|
def test_elimina_204_poi_404_e_sparisce_dalle_sorgenti(client):
|
||||||
|
_carica(client, "foto.png", una_scacchiera())
|
||||||
|
assert client.delete("/camera/immagini", params={"nome": "foto.png"}).status_code == 204
|
||||||
|
assert not (client.config / "immagini" / "foto.png").exists()
|
||||||
|
assert client.delete("/camera/immagini", params={"nome": "foto.png"}).status_code == 404
|
||||||
|
assert "foto.png" not in [v["name"] for v in client.get("/camera/sources").json()["sources"]]
|
||||||
|
assert client.post("/camera/grab", json={"source": "foto.png"}).status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_elimina_nome_con_percorso_404(client):
|
||||||
|
_carica(client, "foto.png", una_scacchiera())
|
||||||
|
assert client.delete("/camera/immagini", params={"nome": "../immagini/foto.png"}).status_code == 404
|
||||||
|
assert client.delete("/camera/immagini").status_code == 404
|
||||||
|
assert (client.config / "immagini" / "foto.png").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_elimina_chiude_la_sorgente_aperta(client, monkeypatch):
|
||||||
|
chiuse = []
|
||||||
|
modulo = _modulo(client)
|
||||||
|
reale = modulo["open_source"]
|
||||||
|
|
||||||
|
def spia(*a, **k):
|
||||||
|
s = reale(*a, **k)
|
||||||
|
chiusura = s.close
|
||||||
|
s.close = lambda: (chiuse.append(a[0]), chiusura())[1]
|
||||||
|
return s
|
||||||
|
|
||||||
|
monkeypatch.setitem(modulo, "open_source", spia)
|
||||||
|
_carica(client, "foto.png", una_scacchiera())
|
||||||
|
assert client.post("/camera/grab", json={"source": "foto.png"}).status_code == 200
|
||||||
|
assert client.delete("/camera/immagini", params={"nome": "foto.png"}).status_code == 204
|
||||||
|
assert chiuse == ["foto.png"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---- POST /camera/anteprima/calibrata
|
||||||
|
|
||||||
|
def test_calibrata_senza_frame_404(client):
|
||||||
|
_carica(client, DENSA, _densa())
|
||||||
|
r = _calibrata(client, DENSA)
|
||||||
|
assert r.status_code == 404 and r.json()["detail"] == "nessun frame: avviare l'anteprima o scattare"
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrata_senza_attiva_409(client):
|
||||||
|
_carica(client, DENSA, _densa())
|
||||||
|
assert _anteprima(client, DENSA).status_code == 200
|
||||||
|
r = _calibrata(client, DENSA)
|
||||||
|
assert r.status_code == 409 and r.json()["detail"] == "nessuna calibrazione attiva su questa sorgente"
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrata_validazione_ingresso(client):
|
||||||
|
assert client.post("/camera/anteprima/calibrata", json={}).status_code == 400
|
||||||
|
assert _calibrata(client, DENSA, lato_max=100).status_code == 400
|
||||||
|
assert _calibrata(client, DENSA, lato_max=True).status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrata_200_densa_senza_nuova_lettura(client, monkeypatch):
|
||||||
|
modulo = _modulo(client)
|
||||||
|
reale = modulo["open_source"]
|
||||||
|
letture = []
|
||||||
|
|
||||||
|
def conta(*a, **k):
|
||||||
|
s = reale(*a, **k)
|
||||||
|
originale = s.grab
|
||||||
|
s.grab = lambda t=1000: (letture.append(1), originale(t))[1]
|
||||||
|
return s
|
||||||
|
|
||||||
|
monkeypatch.setitem(modulo, "open_source", conta)
|
||||||
|
_carica(client, DENSA, _densa())
|
||||||
|
_calibra_e_attiva(client, DENSA, "Banco àè 1")
|
||||||
|
assert _anteprima(client, DENSA).status_code == 200
|
||||||
|
prima = len(letture)
|
||||||
|
for _ in range(3):
|
||||||
|
r = _calibrata(client, DENSA)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.headers["content-type"] == "image/jpeg"
|
||||||
|
assert r.headers["X-Raddrizzato"] == "si"
|
||||||
|
assert (r.headers["X-Larghezza"], r.headers["X-Altezza"]) == ("640", "480")
|
||||||
|
assert unquote(r.headers["X-Calibrazione"]) == "Banco àè 1"
|
||||||
|
assert r.headers["X-Calibrazione"] == quote("Banco àè 1", safe="") # percent-encoded UTF-8
|
||||||
|
img = cv2.imdecode(np.frombuffer(r.content, np.uint8), cv2.IMREAD_UNCHANGED)
|
||||||
|
assert img.shape[:2] == (480, 640)
|
||||||
|
assert len(letture) == prima # la calibrata non legge mai un frame nuovo
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrata_ridotta_a_lato_max(client):
|
||||||
|
_carica(client, DENSA, _densa())
|
||||||
|
_calibra_e_attiva(client, DENSA, "Banco")
|
||||||
|
assert _anteprima(client, DENSA).status_code == 200
|
||||||
|
r = _calibrata(client, DENSA, lato_max=320)
|
||||||
|
assert r.status_code == 200
|
||||||
|
img = cv2.imdecode(np.frombuffer(r.content, np.uint8), cv2.IMREAD_UNCHANGED)
|
||||||
|
assert max(img.shape[:2]) == 320
|
||||||
|
assert (r.headers["X-Larghezza"], r.headers["X-Altezza"]) == ("640", "480") # misure del frame pieno
|
||||||
|
|
||||||
|
|
||||||
|
def test_anteprima_grezza_e_calibrata_usano_lo_stesso_frame_grezzo(client):
|
||||||
|
# anche con anteprima NON grezza la copia tenuta e' quella prima di raddrizzare
|
||||||
|
_carica(client, DENSA, _densa())
|
||||||
|
_calibra_e_attiva(client, DENSA, "Banco")
|
||||||
|
r = client.post("/camera/anteprima", json={"source": DENSA}) # calibrata: raddrizzata
|
||||||
|
assert r.status_code == 200 and r.headers["X-Raddrizzato"] == "si"
|
||||||
|
r = _calibrata(client, DENSA)
|
||||||
|
assert r.status_code == 200 and r.headers["X-Raddrizzato"] == "si" # raddrizzata una volta sola
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrata_risoluzione_diversa_409(client):
|
||||||
|
_carica(client, DENSA, _densa())
|
||||||
|
_calibra_e_attiva(client, DENSA, "Banco")
|
||||||
|
# stessa sorgente, immagine sostituita con un'altra risoluzione: la calibrazione resta attiva
|
||||||
|
assert _carica(client, DENSA, _densa(w=320, h=240)).status_code == 201
|
||||||
|
assert _calibrata(client, DENSA).status_code == 404 # sostituire l'immagine scorda il frame vecchio
|
||||||
|
assert _anteprima(client, DENSA).status_code == 200
|
||||||
|
r = _calibrata(client, DENSA)
|
||||||
|
assert r.status_code == 409
|
||||||
|
assert r.json()["detail"] == "la calibrazione «Banco» è per immagini 640×480, il frame è 320×240"
|
||||||
|
|
||||||
|
|
||||||
|
def test_calibrata_planare_senza_mappa_409(client):
|
||||||
|
_carica(client, PIANA, una_scacchiera())
|
||||||
|
_calibra_e_attiva(client, PIANA, "Piana", cols="9", rows="6")
|
||||||
|
assert _anteprima(client, PIANA).status_code == 200
|
||||||
|
r = _calibrata(client, PIANA)
|
||||||
|
assert r.status_code == 409
|
||||||
|
assert r.json()["detail"] == "la calibrazione «Piana» non ha una mappa di correzione (planare)"
|
||||||
@@ -42,7 +42,7 @@ def test_id_principali():
|
|||||||
def test_sezioni_e_testi_italiani():
|
def test_sezioni_e_testi_italiani():
|
||||||
for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione",
|
for testo in ("Sorgente", "Anteprima", "Parametri camera", "Calibrazione",
|
||||||
"Salva come predefiniti", "Carica immagine da file...", "Questa sorgente non ha parametri regolabili",
|
"Salva come predefiniti", "Carica immagine da file...", "Questa sorgente non ha parametri regolabili",
|
||||||
"Mostra immagine calibrata"): # #92
|
"Attiva calibrazione"): # #92 #93
|
||||||
assert testo in PAGINA, testo
|
assert testo in PAGINA, testo
|
||||||
assert "it-IT" in PAGINA # numeri con la virgola
|
assert "it-IT" in PAGINA # numeri con la virgola
|
||||||
assert "Choose File" not in PAGINA
|
assert "Choose File" not in PAGINA
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def test_sorgente_in_cima_poi_riquadri():
|
|||||||
assert i_acq < i_sorg < i_ant < i_riq < PAGINA.index('id="sezioneParametri"') < i_cal
|
assert i_acq < i_sorg < i_ant < i_riq < PAGINA.index('id="sezioneParametri"') < i_cal
|
||||||
assert PAGINA.index('id="sezioneCalibrazione"') > i_cal
|
assert PAGINA.index('id="sezioneCalibrazione"') > i_cal
|
||||||
# barra: casella calibrata e mirino; la vecchia casella «Immagine grezza» non c'e' piu'
|
# barra: casella calibrata e mirino; la vecchia casella «Immagine grezza» non c'e' piu'
|
||||||
assert 'id="mostraCal"' in PAGINA and "Mostra immagine calibrata" in PAGINA
|
assert 'id="mostraCal"' in PAGINA and "Attiva calibrazione" in PAGINA
|
||||||
assert 'id="grezzo"' not in PAGINA and "Immagine grezza (senza correzione)" not in PAGINA
|
assert 'id="grezzo"' not in PAGINA and "Immagine grezza (senza correzione)" not in PAGINA
|
||||||
# riquadri: «Immagine» sempre visibile, «Calibrata» nascosta finche' la casella e' spenta
|
# riquadri: «Immagine» sempre visibile, «Calibrata» nascosta finche' la casella e' spenta
|
||||||
assert re.search(r'<div class="riquadro" id="riquadroCal" hidden>', PAGINA)
|
assert re.search(r'<div class="riquadro" id="riquadroCal" hidden>', PAGINA)
|
||||||
@@ -98,12 +98,11 @@ def test_pagina_stretta():
|
|||||||
assert "Lettura dei parametri..." in PAGINA
|
assert "Lettura dei parametri..." in PAGINA
|
||||||
|
|
||||||
|
|
||||||
def test_ciclo_sospeso_nella_tab_calibrazione():
|
def test_ciclo_non_sospeso_nella_tab_calibrazione():
|
||||||
# #92: entrando in Calibrazione il ciclo si ferma e poi riprende; Calcola aspetta le richieste in volo
|
# #94: il ciclo non si ferma piu' entrando in Calibrazione (prima #92: A.riprendi); Calcola senza blocco aspetta le richieste in volo
|
||||||
assert "A.riprendi = true; fermaAnteprima();" in PAGINA
|
assert "A.riprendi" not in PAGINA
|
||||||
assert "if (!cal && A.riprendi)" in PAGINA
|
|
||||||
calcola = PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
|
calcola = PAGINA.split("async function calcola")[1].split("async function salvaNome")[0]
|
||||||
assert "fermaAnteprima();" in calcola and "while (A.inVolo)" in calcola
|
assert "fermaAnteprima();" in calcola and "A.inVolo) await pausa(50)" in calcola
|
||||||
|
|
||||||
|
|
||||||
def test_calibrata_dopo_anteprima_e_dopo_carica():
|
def test_calibrata_dopo_anteprima_e_dopo_carica():
|
||||||
@@ -112,3 +111,33 @@ def test_calibrata_dopo_anteprima_e_dopo_carica():
|
|||||||
assert PAGINA.count("aggiornaCalibrata();") >= 3
|
assert PAGINA.count("aggiornaCalibrata();") >= 3
|
||||||
assert "messaggioCalibrata(await dettaglio(r))" in PAGINA
|
assert "messaggioCalibrata(await dettaglio(r))" in PAGINA
|
||||||
assert 'decodeURIComponent(r.headers.get("X-Calibrazione")' in PAGINA
|
assert 'decodeURIComponent(r.headers.get("X-Calibrazione")' in PAGINA
|
||||||
|
|
||||||
|
|
||||||
|
def test_elenco_sorgenti_senza_attese_e_calibrata_solo_se_serve():
|
||||||
|
# #92: la voce dell'immagine caricata si aggiunge in pagina (niente rilettura dell'elenco); «Aggiorna» forza la ricerca camere
|
||||||
|
assert "aggiungiSorgente(j);" in PAGINA and "caricaSorgenti(j.source)" not in PAGINA
|
||||||
|
assert 'id="aggiornaSorgenti"' in PAGINA and ">Aggiorna</button>" in PAGINA
|
||||||
|
assert '"?aggiorna=true"' in PAGINA
|
||||||
|
# nessuna calibrazione attiva: il riquadro lo dice senza chiamare il servizio; dopo un 409 non si richiede piu'
|
||||||
|
assert "if (A.calAttiva === false)" in PAGINA
|
||||||
|
assert "nessuna calibrazione attiva su questa sorgente" in PAGINA
|
||||||
|
assert "if (r.status === 409) A.bloccoCal = true;" in PAGINA and "if (A.bloccoCal) return;" in PAGINA
|
||||||
|
assert PAGINA.count("A.bloccoCal = false;") >= 3 # cambio sorgente, Carica, Scollega
|
||||||
|
|
||||||
|
|
||||||
|
def test_attiva_calibrazione_con_scelta_e_conferma():
|
||||||
|
# #93: sotto la calibrata una combo e CONFERMA; la casella cambia nome
|
||||||
|
for i in ('id="sceltaCal"', 'id="confermaCal"', 'id="msgSceltaCal"'):
|
||||||
|
assert i in PAGINA, i
|
||||||
|
assert "Attiva calibrazione" in PAGINA and ">CONFERMA</button>" in PAGINA
|
||||||
|
assert "Mostra immagine calibrata" not in PAGINA
|
||||||
|
assert PAGINA.index('id="riquadroCal"') < PAGINA.index('id="sceltaCal"') < PAGINA.index('id="confermaCal"') < PAGINA.index('id="msgSceltaCal"')
|
||||||
|
# il gestore di CONFERMA usa PUT calibrazione-attiva (via caricaNome) o DELETE calibration (via scollega), messaggio sotto la calibrata
|
||||||
|
gestore = PAGINA[PAGINA.index('$("confermaCal").onclick'):]
|
||||||
|
gestore = gestore[:gestore.index("};")]
|
||||||
|
assert 'caricaNome(nome, "msgSceltaCal")' in gestore and 'scollega("msgSceltaCal")' in gestore
|
||||||
|
assert 'method: "PUT"' in PAGINA and "/camera/calibrazione-attiva" in PAGINA
|
||||||
|
assert 'method: "DELETE"' in PAGINA and '"/camera/calibration?source="' in PAGINA
|
||||||
|
# voce vuota «nessuna», suffisso di risoluzione, combo ridisegnata con l'elenco e spenta senza sorgente
|
||||||
|
assert "— nessuna (non calibrata) —" in PAGINA and "— risoluzione diversa" in PAGINA
|
||||||
|
assert "disegnaSceltaCal();" in PAGINA and "= !$(\"sorgente\").value" in PAGINA
|
||||||
|
|||||||
Reference in New Issue
Block a user