Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25f8b4d609 | |||
| 552f96bb47 | |||
| 71daab184f | |||
| 9beaf2ed67 | |||
| 800ec41254 |
+237
-31
@@ -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); }
|
||||||
@@ -156,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">
|
||||||
@@ -179,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">
|
||||||
@@ -224,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>
|
||||||
@@ -231,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">
|
||||||
@@ -248,6 +268,22 @@ PAGINA = r"""<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- #95: calibrazione manuale, mm per pixel impostato a mano (nessuna scacchiera) -->
|
||||||
|
<section class="scheda" id="sezioneManuale">
|
||||||
|
<h2>Calibrazione manuale</h2>
|
||||||
|
<p class="nota" style="margin-top:0">Senza scacchiera: si scrive a mano quanti millimetri vale un pixel. Vale solo come scala, nessuna correzione dell'ottica.</p>
|
||||||
|
<div class="riga" style="margin-top:8px">
|
||||||
|
<div><label for="mmPx">mm per pixel</label><input id="mmPx" type="text" inputmode="decimal" size="10" autocomplete="off"></div>
|
||||||
|
<div class="nota" id="pxMmCalc" style="margin:0 0 8px">= - px/mm</div>
|
||||||
|
</div>
|
||||||
|
<div class="riga" style="margin-top:6px">
|
||||||
|
<div><label for="nomeManuale">Nome</label><input id="nomeManuale" type="text" maxlength="60" size="22" autocomplete="off"></div>
|
||||||
|
<button id="salvaManuale">Salva</button>
|
||||||
|
</div>
|
||||||
|
<div id="notaManuale" class="nota"></div>
|
||||||
|
<div id="msgManuale" class="messaggio"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="scheda" id="sezioneSalvate">
|
<section class="scheda" id="sezioneSalvate">
|
||||||
<h2>Calibrazioni salvate</h2>
|
<h2>Calibrazioni salvate</h2>
|
||||||
<div id="stato" class="stato">-</div>
|
<div id="stato" class="stato">-</div>
|
||||||
@@ -266,6 +302,7 @@ var ultimo = null; // ultimo calcolo di calibrazione, per «Salva»
|
|||||||
|
|
||||||
// numero all'italiana (virgola decimale); cifre = decimali fissi, omesso = fino a 4
|
// numero all'italiana (virgola decimale); cifre = decimali fissi, omesso = fino a 4
|
||||||
function fmt(v, cifre) {
|
function fmt(v, cifre) {
|
||||||
|
if (v === null || v === undefined || v === "") return "-"; // #95: campi null (rms_px, square_mm della manuale) = «-», non 0
|
||||||
var n = Number(v);
|
var n = Number(v);
|
||||||
if (!isFinite(n)) return "-";
|
if (!isFinite(n)) return "-";
|
||||||
var o = cifre === undefined ? { maximumFractionDigits: 4 } : { minimumFractionDigits: cifre, maximumFractionDigits: cifre };
|
var o = cifre === undefined ? { maximumFractionDigits: 4 } : { minimumFractionDigits: cifre, maximumFractionDigits: cifre };
|
||||||
@@ -423,6 +460,7 @@ 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
|
A.calAttiva = null; A.bloccoCal = false; // #92: stato calibrazione della nuova sorgente ancora da leggere
|
||||||
@@ -437,8 +475,8 @@ function cambioSorgente(salva) {
|
|||||||
|
|
||||||
// ---- Anteprima (#87)
|
// ---- Anteprima (#87)
|
||||||
|
|
||||||
// #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, 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, calAttiva: null, bloccoCal: false, nitMax: 0, ris: null };
|
var A = { attiva: false, gen: 0, inVolo: false, url: null, urlCal: null, seqCal: 0, bloccata: false, bloccando: 0, 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;
|
||||||
@@ -459,20 +497,28 @@ 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)
|
||||||
function mostraCalibrata(blob, nome) {
|
function mostraCalibrata(blob, nome, soloScala) {
|
||||||
var vecchio = A.urlCal;
|
var vecchio = A.urlCal;
|
||||||
A.urlCal = URL.createObjectURL(blob);
|
A.urlCal = URL.createObjectURL(blob);
|
||||||
$("immagineCal").src = A.urlCal;
|
$("immagineCal").src = A.urlCal;
|
||||||
if (vecchio) URL.revokeObjectURL(vecchio);
|
if (vecchio) URL.revokeObjectURL(vecchio);
|
||||||
$("corniceCal").hidden = false;
|
$("corniceCal").hidden = false;
|
||||||
$("vuotoCal").hidden = true;
|
$("vuotoCal").hidden = true;
|
||||||
$("nomeCalibrata").textContent = nome ? "«" + nome + "»" : "";
|
$("nomeCalibrata").textContent = (nome ? "«" + nome + "»" : "") + (soloScala ? " — solo scala, nessuna correzione ottica" : ""); // #95: senza mappa il frame e' grezzo
|
||||||
}
|
}
|
||||||
function messaggioCalibrata(testo) {
|
function messaggioCalibrata(testo) {
|
||||||
$("corniceCal").hidden = true;
|
$("corniceCal").hidden = true;
|
||||||
@@ -485,6 +531,7 @@ 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
|
||||||
@@ -514,7 +561,7 @@ async function aggiornaCalibrata(lato) {
|
|||||||
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();
|
||||||
if (n !== A.seqCal) return;
|
if (n !== A.seqCal) return;
|
||||||
mostraCalibrata(blob, nome);
|
mostraCalibrata(blob, nome, r.headers.get("X-Raddrizzato") === "no"); // #95
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (n === A.seqCal) messaggioCalibrata("Servizio non raggiungibile.");
|
if (n === A.seqCal) messaggioCalibrata("Servizio non raggiungibile.");
|
||||||
}
|
}
|
||||||
@@ -582,6 +629,8 @@ async function unaAnteprima(lato) {
|
|||||||
async function ciclo(g) {
|
async function ciclo(g) {
|
||||||
while (A.inVolo) await pausa(50);
|
while (A.inVolo) await pausa(50);
|
||||||
while (A.attiva && g === A.gen) {
|
while (A.attiva && g === A.gen) {
|
||||||
|
while (A.bloccando) await pausa(20); // #94: durante «Blocca immagine» nessuna nuova anteprima
|
||||||
|
if (!A.attiva || g !== A.gen) return;
|
||||||
var t0 = Date.now();
|
var t0 = Date.now();
|
||||||
var esito = await unaAnteprima(1280);
|
var esito = await unaAnteprima(1280);
|
||||||
if (g !== A.gen) return;
|
if (g !== A.gen) return;
|
||||||
@@ -607,18 +656,75 @@ 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);
|
||||||
|
while (A.bloccando) await pausa(20); // #94: niente scatto mentre si blocca l'immagine
|
||||||
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 = null; // #94: lo si fissa dopo l'attesa, sul frame davvero mostrato
|
||||||
|
var n = ++A.seqBlocco;
|
||||||
|
$("scattaCal").disabled = true;
|
||||||
|
dire("msgBlocco", "Blocco in corso...", "");
|
||||||
|
A.bloccando++; // #94: il ciclo e «Scatta» non chiedono nuove anteprime finche' la copia non e' fatta
|
||||||
|
try {
|
||||||
|
for (var k = 0; A.inVolo && k < 150; k++) await pausa(20); // #94: attende l'anteprima in volo (max 3 s): il frame copiato e' l'ultimo mostrato
|
||||||
|
if (n !== A.seqBlocco) return; // spenta o cambiata sorgente durante l'attesa: niente copia
|
||||||
|
if (A.inVolo) { sbloccaLocale(); dire("msgBlocco", "Anteprima ancora in corso: riprovare.", "errore"); return; }
|
||||||
|
A.urlBlocco = A.url; // #94: il frame mostrato in questo momento e' quello che il servizio copia
|
||||||
|
mostraInTabCal(A.url);
|
||||||
|
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"); }
|
||||||
|
} finally {
|
||||||
|
A.bloccando--; // #94: anche su errore o uscita anticipata
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
$("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;
|
||||||
@@ -855,9 +961,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
|
||||||
}
|
}
|
||||||
@@ -875,6 +979,11 @@ $("tabCal").onclick = function () { scegliTab("cal", true); };
|
|||||||
// ---- Calibrazione (logica di #83 e #84; #87: lato precompilato, data italiana, conferma in pagina)
|
// ---- Calibrazione (logica di #83 e #84; #87: lato precompilato, data italiana, conferma in pagina)
|
||||||
|
|
||||||
function righe(d) {
|
function righe(d) {
|
||||||
|
if (d.metodo === "manuale") return [ // #95: solo scala impostata a mano, niente griglia, residui o lato dello scacco
|
||||||
|
["Scala", fmt(d.px_per_mm, 4) + " px/mm (" + fmt(d.mm_per_px, 5) + " mm/px)"],
|
||||||
|
["Metodo", "manuale (mm/px impostato a mano, nessuna correzione della distorsione)"],
|
||||||
|
["Immagine", fmt(d.image_width, 0) + " x " + fmt(d.image_height, 0) + " px"]
|
||||||
|
];
|
||||||
return [
|
return [
|
||||||
["Scala", fmt(d.px_per_mm, 4) + " px/mm (" + fmt(d.mm_per_px, 5) + " mm/px)"],
|
["Scala", fmt(d.px_per_mm, 4) + " px/mm (" + fmt(d.mm_per_px, 5) + " mm/px)"],
|
||||||
["Metodo", d.metodo !== "densa" ? "planare (senza correzione della distorsione)" : "densa (immagine raddrizzata)"],
|
["Metodo", d.metodo !== "densa" ? "planare (senza correzione della distorsione)" : "densa (immagine raddrizzata)"],
|
||||||
@@ -931,6 +1040,7 @@ async function aggiornaStato() {
|
|||||||
var stato = $("stato");
|
var stato = $("stato");
|
||||||
annullaConferma();
|
annullaConferma();
|
||||||
$("calSorgente").textContent = sorgente ? $("sorgente").options[$("sorgente").selectedIndex].textContent : "-"; // #91
|
$("calSorgente").textContent = sorgente ? $("sorgente").options[$("sorgente").selectedIndex].textContent : "-"; // #91
|
||||||
|
statoManuale(); // #95: «Salva» della calibrazione manuale spento senza sorgente
|
||||||
if (!sorgente) { stato.textContent = "-"; $("statoSorgente").textContent = "-"; caricaCalibrazioni(); return; }
|
if (!sorgente) { stato.textContent = "-"; $("statoSorgente").textContent = "-"; caricaCalibrazioni(); return; }
|
||||||
try {
|
try {
|
||||||
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente));
|
var r = await fetch("/camera/calibration?source=" + encodeURIComponent(sorgente));
|
||||||
@@ -956,7 +1066,7 @@ async function aggiornaStato() {
|
|||||||
stato.appendChild(t);
|
stato.appendChild(t);
|
||||||
stato.appendChild(quando);
|
stato.appendChild(quando);
|
||||||
$("statoSorgente").textContent = "calibrata: " + (d.nome || "(senza nome)") + " · " + fmt(d.px_per_mm, 2) + " px/mm"; // #92
|
$("statoSorgente").textContent = "calibrata: " + (d.nome || "(senza nome)") + " · " + fmt(d.px_per_mm, 2) + " px/mm"; // #92
|
||||||
if (d.square_mm) $("quadrato").value = fmtCasella(d.square_mm); // #87: parte dal lato gia' usato
|
if (Number(d.square_mm) > 0) $("quadrato").value = fmtCasella(d.square_mm); // #87: parte dal lato gia' usato
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
stato.textContent = "Servizio non raggiungibile.";
|
stato.textContent = "Servizio non raggiungibile.";
|
||||||
@@ -995,6 +1105,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";
|
||||||
@@ -1026,7 +1137,7 @@ function disegnaElenco() {
|
|||||||
riga2.textContent = [
|
riga2.textContent = [
|
||||||
c.image_width && c.image_height ? fmt(c.image_width, 0) + "×" + fmt(c.image_height, 0) : "-",
|
c.image_width && c.image_height ? fmt(c.image_width, 0) + "×" + fmt(c.image_height, 0) : "-",
|
||||||
fmt(c.px_per_mm, 3) + " px/mm",
|
fmt(c.px_per_mm, 3) + " px/mm",
|
||||||
c.metodo === "densa" ? "densa" : "planare",
|
c.metodo === "densa" || c.metodo === "manuale" ? c.metodo : "planare", // #95: «manuale» mostrato come tale
|
||||||
c.calibrated_at ? dataItaliana(c.calibrated_at) : "-",
|
c.calibrated_at ? dataItaliana(c.calibrated_at) : "-",
|
||||||
descriviOrigine(c.origine)
|
descriviOrigine(c.origine)
|
||||||
].join(" · ");
|
].join(" · ");
|
||||||
@@ -1035,7 +1146,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);
|
||||||
@@ -1050,41 +1161,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");
|
||||||
A.bloccoCal = false; // #92
|
A.bloccoCal = false; // #92
|
||||||
await aggiornaStato(); // prima lo stato nuovo, poi il riquadro calibrata con la nuova calibrazione
|
await aggiornaStato(); // prima lo stato nuovo (rilegge anche l'elenco, #93: schede e combo allineate), poi il riquadro calibrata
|
||||||
aggiornaCalibrata();
|
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");
|
||||||
}
|
}
|
||||||
A.bloccoCal = false; // #92
|
A.bloccoCal = false; // #92
|
||||||
await aggiornaStato();
|
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'
|
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);
|
||||||
@@ -1121,11 +1271,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();
|
||||||
@@ -1133,7 +1285,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", "");
|
||||||
@@ -1183,6 +1335,60 @@ async function salvaNome() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- #95: calibrazione manuale (mm/px a mano)
|
||||||
|
var M = { nome: null, timer: null }; // secondo clic su «Sovrascrivi» entro 4 s
|
||||||
|
function statoManuale() {
|
||||||
|
var s = $("sorgente").value;
|
||||||
|
$("salvaManuale").disabled = !s;
|
||||||
|
$("notaManuale").textContent = s ? "" : "Scegliere una sorgente nella tab Acquisizione: serve per registrare la risoluzione dell'immagine.";
|
||||||
|
}
|
||||||
|
function aggiornaManuale() {
|
||||||
|
var v = leggiNumero($("mmPx").value);
|
||||||
|
$("pxMmCalc").textContent = v > 0 ? "= " + fmt(1 / v, 4) + " px/mm" : "= - px/mm";
|
||||||
|
}
|
||||||
|
function annullaSovrascriviManuale() {
|
||||||
|
clearTimeout(M.timer);
|
||||||
|
M.timer = null;
|
||||||
|
M.nome = null;
|
||||||
|
$("salvaManuale").textContent = "Salva";
|
||||||
|
}
|
||||||
|
async function salvaManuale() {
|
||||||
|
var sorgente = $("sorgente").value;
|
||||||
|
if (!sorgente) { dire("msgManuale", "Scegliere una sorgente nella tab Acquisizione.", "errore"); return; }
|
||||||
|
var mm = leggiNumero($("mmPx").value);
|
||||||
|
if (!(mm >= 0.00001 && mm <= 100)) { dire("msgManuale", "mm/px deve essere un numero positivo (da 0,00001 a 100).", "errore"); return; }
|
||||||
|
var nome = $("nomeManuale").value.trim();
|
||||||
|
if (!nome) { dire("msgManuale", "Scrivere un nome per la calibrazione.", "errore"); return; }
|
||||||
|
if (nome.length > 60) { dire("msgManuale", "Il nome può avere al massimo 60 caratteri.", "errore"); return; }
|
||||||
|
var sovrascrivi = M.timer !== null && M.nome === nome.toLowerCase(); // secondo clic sullo stesso nome
|
||||||
|
annullaSovrascriviManuale();
|
||||||
|
$("salvaManuale").disabled = true;
|
||||||
|
try {
|
||||||
|
var r = await fetch("/camera/calibrazioni/manuale", {
|
||||||
|
method: "POST", headers: JSON_H,
|
||||||
|
body: JSON.stringify({ source: sorgente, nome: nome, mm_per_px: mm, sovrascrivi: sovrascrivi }) // al servizio sempre il numero, mai il testo con la virgola
|
||||||
|
});
|
||||||
|
if (r.status === 409) {
|
||||||
|
M.nome = nome.toLowerCase();
|
||||||
|
$("salvaManuale").textContent = "Sovrascrivi «" + nome + "»? Clic ancora";
|
||||||
|
M.timer = setTimeout(annullaSovrascriviManuale, 4000);
|
||||||
|
dire("msgManuale", await dettaglio(r), "errore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.ok) { dire("msgManuale", await dettaglio(r), "errore"); return; }
|
||||||
|
dire("msgManuale", "Calibrazione «" + nome + "» salvata: per usarla «Carica» o CONFERMA.", "ok");
|
||||||
|
caricaCalibrazioni();
|
||||||
|
} catch (e) {
|
||||||
|
dire("msgManuale", "Servizio non raggiungibile.", "errore");
|
||||||
|
} finally {
|
||||||
|
statoManuale();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$("mmPx").oninput = aggiornaManuale;
|
||||||
|
$("salvaManuale").onclick = salvaManuale;
|
||||||
|
$("nomeManuale").onkeydown = function (e) { if (e.key === "Enter") salvaManuale(); };
|
||||||
|
statoManuale();
|
||||||
|
|
||||||
$("acquisisci").onclick = calcola;
|
$("acquisisci").onclick = calcola;
|
||||||
$("salva").onclick = salvaNome;
|
$("salva").onclick = salvaNome;
|
||||||
$("nomeCal").onkeydown = function (e) { if (e.key === "Enter") salvaNome(); };
|
$("nomeCal").onkeydown = function (e) { if (e.key === "Enter") salvaNome(); };
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ Calibrazione a scacchiera (#83, logica in scripts/calibrazione.py).
|
|||||||
calibrated_at, attiva_su}], attiva: nome|null} (dalla piu' recente)
|
calibrated_at, attiva_su}], attiva: nome|null} (dalla piu' recente)
|
||||||
POST /camera/calibrazioni json {calcolo_id, nome, sovrascrivi=false} -> 201
|
POST /camera/calibrazioni json {calcolo_id, nome, sovrascrivi=false} -> 201
|
||||||
metadati. 404 calcolo scaduto, 400 nome non valido, 409 nome gia' usato
|
metadati. 404 calcolo scaduto, 400 nome non valido, 409 nome gia' usato
|
||||||
|
POST /camera/calibrazioni/manuale json {source, nome, mm_per_px, sovrascrivi=false}
|
||||||
|
-> 201 metadati (#95: mm/px impostato a mano, metodo "manuale", nessuna
|
||||||
|
mappa; non la attiva). 400 mm/px fuori 0.00001..100 o nome non valido,
|
||||||
|
409 nome gia' usato; la risoluzione e' quella del frame tenuto o di un grab
|
||||||
DELETE /camera/calibrazioni?nome=X -> 204 (toglie anche la mappa e la scollega
|
DELETE /camera/calibrazioni?nome=X -> 204 (toglie anche la mappa e la scollega
|
||||||
da ogni sorgente) | 404
|
da ogni sorgente) | 404
|
||||||
PUT /camera/calibrazione-attiva json {source, nome}: legge un frame dalla
|
PUT /camera/calibrazione-attiva json {source, nome}: legge un frame dalla
|
||||||
@@ -84,6 +88,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import math # #95: mm/px finito nella calibrazione manuale
|
||||||
import os
|
import os
|
||||||
import re # #92: nome sicuro per le immagini caricate
|
import re # #92: nome sicuro per le immagini caricate
|
||||||
import sys
|
import sys
|
||||||
@@ -156,6 +161,7 @@ def create_app(folder: Path | str | None = 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)
|
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)
|
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
|
||||||
@@ -223,6 +229,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
with lock:
|
with lock:
|
||||||
s = aperte.pop(nome, None)
|
s = aperte.pop(nome, None)
|
||||||
grezzi.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:
|
if s is not None:
|
||||||
try:
|
try:
|
||||||
s.close()
|
s.close()
|
||||||
@@ -368,6 +375,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:
|
||||||
@@ -375,6 +383,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)
|
||||||
@@ -397,7 +412,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
|
||||||
|
|
||||||
@@ -429,6 +445,35 @@ def create_app(folder: Path | str | None = None,
|
|||||||
except calibrazione.NomeGiaUsato as e:
|
except calibrazione.NomeGiaUsato as e:
|
||||||
raise HTTPException(409, str(e))
|
raise HTTPException(409, str(e))
|
||||||
|
|
||||||
|
@app.post("/camera/calibrazioni/manuale", status_code=201)
|
||||||
|
def calibrazioni_manuale(corpo: dict = Body(default=None)):
|
||||||
|
"""#95: calibrazione con mm/px impostato a mano (nessuna scacchiera, nessuna mappa); NON la attiva."""
|
||||||
|
corpo = corpo if isinstance(corpo, dict) else {}
|
||||||
|
sorgente = _sorgente(corpo.get("source"))
|
||||||
|
mm_px = corpo.get("mm_per_px")
|
||||||
|
if (isinstance(mm_px, bool) or not isinstance(mm_px, (int, float))
|
||||||
|
or not math.isfinite(mm_px) or not 0.00001 <= mm_px <= 100):
|
||||||
|
raise HTTPException(400, "mm/px deve essere un numero positivo")
|
||||||
|
sovrascrivi = corpo.get("sovrascrivi", False)
|
||||||
|
if not isinstance(sovrascrivi, bool):
|
||||||
|
raise HTTPException(400, "«sovrascrivi» deve essere true o false")
|
||||||
|
try:
|
||||||
|
nome = calibrazione.pulisci_nome(corpo.get("nome"))
|
||||||
|
except calibrazione.NomeNonValido as e:
|
||||||
|
raise HTTPException(400, str(e))
|
||||||
|
with lock:
|
||||||
|
frame = grezzi.get(sorgente)
|
||||||
|
if frame is None: # nessun frame tenuto: un grab come /camera/grab (404/503/504)
|
||||||
|
frame = _grab(sorgente, 5000)
|
||||||
|
alt, larg = frame.shape[:2] # serve al controllo W×H di PUT /camera/calibrazione-attiva
|
||||||
|
dati = {"px_per_mm": 1.0 / mm_px, "mm_per_px": float(mm_px), "metodo": "manuale",
|
||||||
|
"image_width": larg, "image_height": alt, "origine": sorgente,
|
||||||
|
"calibrated_at": calibrazione.ora_iso(), "rms_px": None, "square_mm": None}
|
||||||
|
try:
|
||||||
|
return archivio.salva_nome(nome, dati, None, sovrascrivi)
|
||||||
|
except calibrazione.NomeGiaUsato as e:
|
||||||
|
raise HTTPException(409, str(e))
|
||||||
|
|
||||||
@app.delete("/camera/calibrazioni", status_code=204)
|
@app.delete("/camera/calibrazioni", status_code=204)
|
||||||
def calibrazioni_elimina(nome: str = ""):
|
def calibrazioni_elimina(nome: str = ""):
|
||||||
if not archivio.elimina_nome(nome):
|
if not archivio.elimina_nome(nome):
|
||||||
@@ -531,9 +576,28 @@ 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")
|
@app.post("/camera/anteprima/calibrata")
|
||||||
def camera_anteprima_calibrata(corpo: dict = Body(default=None)):
|
def camera_anteprima_calibrata(corpo: dict = Body(default=None)):
|
||||||
"""#92: l'ultimo frame grezzo dell'anteprima, raddrizzato; NON legge un frame nuovo."""
|
"""#92: l'ultimo frame grezzo dell'anteprima, raddrizzato; NON legge un frame nuovo. #95: senza mappa esce grezzo (X-Raddrizzato: no)."""
|
||||||
corpo = corpo if isinstance(corpo, dict) else {}
|
corpo = corpo if isinstance(corpo, dict) else {}
|
||||||
nome = _sorgente(corpo.get("source"))
|
nome = _sorgente(corpo.get("source"))
|
||||||
lato = corpo.get("lato_max", 1280)
|
lato = corpo.get("lato_max", 1280)
|
||||||
@@ -547,14 +611,22 @@ def create_app(folder: Path | str | None = None,
|
|||||||
if attiva is None:
|
if attiva is None:
|
||||||
raise HTTPException(409, "nessuna calibrazione attiva su questa sorgente")
|
raise HTTPException(409, "nessuna calibrazione attiva su questa sorgente")
|
||||||
mappa = archivio.leggi_mappa(nome)
|
mappa = archivio.leggi_mappa(nome)
|
||||||
if mappa is None: # planare: ha solo scala, nessuna mappa da applicare
|
if mappa is None: # #95: senza mappa (manuale o planare) solo scala: frame grezzo, nessuna correzione
|
||||||
raise HTTPException(409, f"la calibrazione «{attiva}» non ha una mappa di correzione (planare)")
|
dati = archivio.leggi(nome) or {}
|
||||||
|
cw, ch = dati.get("image_width"), dati.get("image_height")
|
||||||
|
alt, larg = frame.shape[:2]
|
||||||
|
if isinstance(cw, int) and isinstance(ch, int) and (cw, ch) != (larg, alt):
|
||||||
|
raise HTTPException(409, f"la calibrazione «{attiva}» è per immagini {cw}×{ch}, "
|
||||||
|
f"il frame è {larg}×{alt}")
|
||||||
|
corretto, raddrizzato = frame, "no"
|
||||||
|
else:
|
||||||
corretto = calibrazione.raddrizza(frame, mappa)
|
corretto = calibrazione.raddrizza(frame, mappa)
|
||||||
if corretto is None: # risoluzione diversa da quella calibrata
|
if corretto is None: # risoluzione diversa da quella calibrata
|
||||||
ch, cw = mappa.shape[:2]
|
ch, cw = mappa.shape[:2]
|
||||||
alt, larg = frame.shape[:2]
|
alt, larg = frame.shape[:2]
|
||||||
raise HTTPException(409, f"la calibrazione «{attiva}» è per immagini {cw}×{ch}, "
|
raise HTTPException(409, f"la calibrazione «{attiva}» è per immagini {cw}×{ch}, "
|
||||||
f"il frame è {larg}×{alt}")
|
f"il frame è {larg}×{alt}")
|
||||||
|
raddrizzato = "si"
|
||||||
alt, larg = corretto.shape[:2]
|
alt, larg = corretto.shape[:2]
|
||||||
if corretto.dtype != np.uint8: # il JPEG vuole 8 bit
|
if corretto.dtype != np.uint8: # il JPEG vuole 8 bit
|
||||||
corretto = _grigio8(corretto)
|
corretto = _grigio8(corretto)
|
||||||
@@ -566,7 +638,7 @@ def create_app(folder: Path | str | None = None,
|
|||||||
if not ok:
|
if not ok:
|
||||||
raise HTTPException(500, "codifica JPEG fallita")
|
raise HTTPException(500, "codifica JPEG fallita")
|
||||||
return Response(buf.tobytes(), media_type="image/jpeg", headers={
|
return Response(buf.tobytes(), media_type="image/jpeg", headers={
|
||||||
"X-Raddrizzato": "si", "X-Larghezza": str(larg), "X-Altezza": str(alt),
|
"X-Raddrizzato": raddrizzato, "X-Larghezza": str(larg), "X-Altezza": str(alt),
|
||||||
"X-Calibrazione": urllib.parse.quote(attiva, safe="")})
|
"X-Calibrazione": urllib.parse.quote(attiva, safe="")})
|
||||||
|
|
||||||
@app.get("/camera/parametri")
|
@app.get("/camera/parametri")
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""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()")
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_blocco_attende_l_anteprima_in_volo():
|
||||||
|
# #94: il frame bloccato e' quello mostrato: flag A.bloccando, attesa di A.inVolo (20 ms, max 3 s), poi POST, flag tolto in finally
|
||||||
|
blocco = PAGINA.split('$("bloccaImg").onchange')[1].split('$("avvia").onclick')[0]
|
||||||
|
assert "A.bloccando++" in blocco and "A.bloccando--" in blocco and "finally" in blocco
|
||||||
|
assert "k < 150" in blocco and "pausa(20)" in blocco
|
||||||
|
assert blocco.index("A.inVolo") < blocco.index('fetch("/camera/blocca"')
|
||||||
|
ciclo = PAGINA.split("async function ciclo")[1].split("function avviaAnteprima")[0]
|
||||||
|
assert "while (A.bloccando)" in ciclo and ciclo.index("A.bloccando") < ciclo.index("unaAnteprima(1280)")
|
||||||
|
scatta = PAGINA.split("async function scatta")[1].split("// #94: «Blocca immagine»")[0]
|
||||||
|
assert "while (A.bloccando)" in scatta and scatta.index("A.bloccando") < scatta.index("unaAnteprima(4096)")
|
||||||
@@ -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,148 @@
|
|||||||
|
"""Test di #95: calibrazione manuale (mm/px impostato a mano) in scripts/camera_service.py e nella PAGINA.
|
||||||
|
|
||||||
|
Uso: uv run --python 3.13 --extra vision-worker --extra dev \
|
||||||
|
--with-editable vendor/visionsuite/packages/vs-camera pytest scripts/tests -q
|
||||||
|
Sorgenti = immagini caricate in tmp_path: niente camera vera, niente ProgramData.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from test_calibrazione import _png, cartella, client # fixture della calibrazione
|
||||||
|
from test_immagini_affiancate import _anteprima, _calibrata, _carica
|
||||||
|
from test_pagina_acquisizione import PAGINA
|
||||||
|
|
||||||
|
__all__ = ["cartella", "client"] # le fixture importate restano usate
|
||||||
|
|
||||||
|
SRC = "banco.png" # 64x48
|
||||||
|
ALTRA = "grande.png" # 80x60, risoluzione diversa
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sorgenti(client):
|
||||||
|
assert _carica(client, SRC, np.full((48, 64), 120, np.uint8)).status_code == 201
|
||||||
|
assert _carica(client, ALTRA, np.full((60, 80), 120, np.uint8)).status_code == 201
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _manuale(c, nome="Manuale", mm=0.05, source=SRC, **extra):
|
||||||
|
return c.post("/camera/calibrazioni/manuale",
|
||||||
|
json={"source": source, "nome": nome, "mm_per_px": mm, **extra})
|
||||||
|
|
||||||
|
|
||||||
|
# ---- servizio
|
||||||
|
|
||||||
|
def test_manuale_201_con_scala_e_risoluzione_del_frame(sorgenti):
|
||||||
|
r = _manuale(sorgenti, mm=0.05)
|
||||||
|
assert r.status_code == 201, r.text
|
||||||
|
m = r.json()
|
||||||
|
assert m["nome"] == "Manuale" and m["metodo"] == "manuale"
|
||||||
|
assert m["px_per_mm"] == pytest.approx(20.0) and m["mm_per_px"] == pytest.approx(0.05)
|
||||||
|
assert (m["image_width"], m["image_height"]) == (64, 48)
|
||||||
|
assert m["origine"] == SRC and m["square_mm"] is None and m["attiva_su"] == [] # non la attiva
|
||||||
|
voci = sorgenti.get("/camera/calibrazioni").json()["calibrazioni"]
|
||||||
|
assert [v["nome"] for v in voci] == ["Manuale"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_manuale_usa_il_frame_tenuto_se_c_e(sorgenti):
|
||||||
|
assert _anteprima(sorgenti, SRC).status_code == 200 # tiene il grezzo 64x48
|
||||||
|
r = _manuale(sorgenti)
|
||||||
|
assert r.status_code == 201 and (r.json()["image_width"], r.json()["image_height"]) == (64, 48)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("valore", [0, -1, 0.000001, 100.5, "abc", None, True])
|
||||||
|
def test_manuale_mm_non_valido_400(sorgenti, valore):
|
||||||
|
r = _manuale(sorgenti, mm=valore)
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "mm/px deve essere un numero positivo" in r.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_manuale_senza_source_o_nome_400_e_sorgente_ignota_404(sorgenti):
|
||||||
|
assert sorgenti.post("/camera/calibrazioni/manuale", json={"nome": "x", "mm_per_px": 0.1}).status_code == 400
|
||||||
|
assert _manuale(sorgenti, nome=" ").status_code == 400
|
||||||
|
assert _manuale(sorgenti, source="nonesiste.png").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_manuale_409_nome_doppio_e_sovrascrivi(sorgenti):
|
||||||
|
assert _manuale(sorgenti, mm=0.05).status_code == 201
|
||||||
|
r = _manuale(sorgenti, mm=0.1)
|
||||||
|
assert r.status_code == 409 and "esiste già una calibrazione" in r.json()["detail"]
|
||||||
|
r = _manuale(sorgenti, mm=0.1, sovrascrivi=True)
|
||||||
|
assert r.status_code == 201 and r.json()["px_per_mm"] == pytest.approx(10.0)
|
||||||
|
assert _manuale(sorgenti, mm=0.1, sovrascrivi="si").status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_attiva_manuale_ok_su_stessa_risoluzione_e_legge_px_per_mm(sorgenti):
|
||||||
|
assert _manuale(sorgenti, mm=0.05).status_code == 201
|
||||||
|
r = sorgenti.put("/camera/calibrazione-attiva", json={"source": SRC, "nome": "Manuale"})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
g = sorgenti.get("/camera/calibration", params={"source": SRC})
|
||||||
|
assert g.status_code == 200 and g.json()["px_per_mm"] == pytest.approx(20.0)
|
||||||
|
assert g.json()["nome"] == "Manuale" and g.json()["metodo"] == "manuale"
|
||||||
|
|
||||||
|
|
||||||
|
def test_attiva_manuale_409_su_risoluzione_diversa(sorgenti):
|
||||||
|
assert _manuale(sorgenti).status_code == 201
|
||||||
|
r = sorgenti.put("/camera/calibrazione-attiva", json={"source": ALTRA, "nome": "Manuale"})
|
||||||
|
assert r.status_code == 409 and "64×48" in r.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_anteprima_calibrata_con_manuale_200_grezza(sorgenti):
|
||||||
|
assert _manuale(sorgenti).status_code == 201
|
||||||
|
assert sorgenti.put("/camera/calibrazione-attiva", json={"source": SRC, "nome": "Manuale"}).status_code == 200
|
||||||
|
assert _anteprima(sorgenti, SRC).status_code == 200
|
||||||
|
r = _calibrata(sorgenti, SRC)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert r.headers["X-Raddrizzato"] == "no" and r.headers["X-Calibrazione"] == "Manuale"
|
||||||
|
assert (r.headers["X-Larghezza"], r.headers["X-Altezza"]) == ("64", "48")
|
||||||
|
assert r.headers["content-type"] == "image/jpeg"
|
||||||
|
|
||||||
|
|
||||||
|
def test_anteprima_calibrata_manuale_409_se_il_frame_ha_altra_risoluzione(sorgenti):
|
||||||
|
assert _manuale(sorgenti).status_code == 201
|
||||||
|
assert sorgenti.put("/camera/calibrazione-attiva", json={"source": SRC, "nome": "Manuale"}).status_code == 200
|
||||||
|
assert _anteprima(sorgenti, ALTRA).status_code == 200
|
||||||
|
# stessa sorgente attiva, ma il frame tenuto cambia (immagine sostituita con altra misura)
|
||||||
|
assert _carica(sorgenti, SRC, np.full((60, 80), 9, np.uint8)).status_code == 201
|
||||||
|
assert _anteprima(sorgenti, SRC).status_code == 200
|
||||||
|
r = _calibrata(sorgenti, SRC)
|
||||||
|
assert r.status_code == 409 and "64×48" in r.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---- pagina
|
||||||
|
|
||||||
|
def test_pagina_scheda_manuale_id_e_testi():
|
||||||
|
ids = set(re.findall(r'id="([^"]+)"', PAGINA))
|
||||||
|
assert {"mmPx", "pxMmCalc", "nomeManuale", "salvaManuale", "msgManuale"} <= ids
|
||||||
|
tab = PAGINA.split('id="pannelloCal"')[1]
|
||||||
|
assert tab.index('id="sezioneCalibrazione"') < tab.index('id="sezioneManuale"') < tab.index('id="sezioneSalvate"')
|
||||||
|
assert "Calibrazione manuale" in PAGINA and "mm per pixel" in PAGINA
|
||||||
|
assert "salvata: per usarla «Carica» o CONFERMA" in PAGINA
|
||||||
|
assert "Sovrascrivi «" in PAGINA.split("async function salvaManuale")[1] # stessa conferma del Salva scacchiera
|
||||||
|
assert "/camera/calibrazioni/manuale" in PAGINA and "alert(" not in PAGINA and "confirm(" not in PAGINA
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_manuale_senza_sorgente_pulsante_spento_con_nota():
|
||||||
|
stato = PAGINA.split("function statoManuale")[1].split("function aggiornaManuale")[0]
|
||||||
|
assert 'salvaManuale").disabled = !s' in stato and "Scegliere una sorgente" in stato
|
||||||
|
assert "statoManuale()" in PAGINA.split("async function aggiornaStato")[1].split("async function caricaCalibrazioni")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_px_mm_calcolato_a_ogni_tasto():
|
||||||
|
assert '$("mmPx").oninput = aggiornaManuale' in PAGINA
|
||||||
|
assert '"= " + fmt(1 / v, 4) + " px/mm"' in PAGINA
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_metodo_manuale_e_campi_null():
|
||||||
|
assert 'c.metodo === "densa" || c.metodo === "manuale" ? c.metodo : "planare"' in PAGINA
|
||||||
|
righe = PAGINA.split("function righe")[1].split("function tabella")[0]
|
||||||
|
assert 'd.metodo === "manuale"' in righe and "Lato dello scacco" in righe # la manuale ha il suo elenco, le altre il solito
|
||||||
|
assert 'v === null || v === undefined' in PAGINA.split("function fmt")[1].split("function fmtCasella")[0]
|
||||||
|
stato = PAGINA.split("async function aggiornaStato")[1].split("async function caricaCalibrazioni")[0]
|
||||||
|
assert "Number(d.square_mm) > 0" in stato
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagina_calibrata_senza_raddrizzamento_dice_solo_scala():
|
||||||
|
assert "solo scala, nessuna correzione ottica" in PAGINA
|
||||||
|
assert 'r.headers.get("X-Raddrizzato") === "no"' in PAGINA
|
||||||
@@ -282,10 +282,11 @@ def test_calibrata_risoluzione_diversa_409(client):
|
|||||||
assert r.json()["detail"] == "la calibrazione «Banco» è per immagini 640×480, il frame è 320×240"
|
assert r.json()["detail"] == "la calibrazione «Banco» è per immagini 640×480, il frame è 320×240"
|
||||||
|
|
||||||
|
|
||||||
def test_calibrata_planare_senza_mappa_409(client):
|
def test_calibrata_planare_senza_mappa_200_grezza(client):
|
||||||
|
# #95: senza mappa (planare) non e' piu' 409: esce il frame grezzo, X-Raddrizzato: no
|
||||||
_carica(client, PIANA, una_scacchiera())
|
_carica(client, PIANA, una_scacchiera())
|
||||||
_calibra_e_attiva(client, PIANA, "Piana", cols="9", rows="6")
|
_calibra_e_attiva(client, PIANA, "Piana", cols="9", rows="6")
|
||||||
assert _anteprima(client, PIANA).status_code == 200
|
assert _anteprima(client, PIANA).status_code == 200
|
||||||
r = _calibrata(client, PIANA)
|
r = _calibrata(client, PIANA)
|
||||||
assert r.status_code == 409
|
assert r.status_code == 200
|
||||||
assert r.json()["detail"] == "la calibrazione «Piana» non ha una mappa di correzione (planare)"
|
assert r.headers["X-Raddrizzato"] == "no" and unquote(r.headers["X-Calibrazione"]) == "Piana"
|
||||||
|
|||||||
@@ -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():
|
||||||
@@ -124,3 +123,21 @@ def test_elenco_sorgenti_senza_attese_e_calibrata_solo_se_serve():
|
|||||||
assert "nessuna calibrazione attiva su questa sorgente" 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 "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
|
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
|
||||||
|
|||||||
@@ -541,6 +541,55 @@ def api_list_stations_for_maker():
|
|||||||
return jsonify(out), 200
|
return jsonify(out), 200
|
||||||
|
|
||||||
|
|
||||||
|
def _camera_services() -> list[dict]:
|
||||||
|
"""#97: servizi di acquisizione distinti delle stazioni che hanno una camera_url."""
|
||||||
|
services: dict[str, list[str]] = {}
|
||||||
|
for st in _stations_for_maker():
|
||||||
|
url = (st.get("camera_url") or "").strip().rstrip("/")
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
label = f"{st.get('code') or ''} - {st.get('name') or ''}"
|
||||||
|
services.setdefault(url, []).append(label)
|
||||||
|
return [{"camera_url": u, "stazioni": s} for u, s in services.items()]
|
||||||
|
|
||||||
|
|
||||||
|
@maker_bp.route("/api/camera/servizi", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
@maker_or_admin_required
|
||||||
|
def api_camera_services():
|
||||||
|
"""#97: servizi di acquisizione fra cui scegliere la camera di un task."""
|
||||||
|
return jsonify({"servizi": _camera_services()}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@maker_bp.route("/api/camera/sorgenti", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
@maker_or_admin_required
|
||||||
|
def api_camera_sources():
|
||||||
|
"""#97: proxy di GET {url}/camera/sources; solo per i servizi delle stazioni."""
|
||||||
|
url = (request.args.get("url") or "").strip().rstrip("/")
|
||||||
|
# Niente richieste verso indirizzi arbitrari: url ammesso solo se e' un servizio noto
|
||||||
|
if not url or url not in {s["camera_url"] for s in _camera_services()}:
|
||||||
|
return jsonify({
|
||||||
|
"error": True,
|
||||||
|
"detail": _("Servizio di acquisizione non ammesso"),
|
||||||
|
}), 400
|
||||||
|
try:
|
||||||
|
resp = http_requests.get(f"{url}/camera/sources", timeout=15)
|
||||||
|
body = resp.json() if resp.status_code == 200 else None
|
||||||
|
except (http_requests.RequestException, ValueError) as exc:
|
||||||
|
return jsonify({
|
||||||
|
"error": True,
|
||||||
|
"detail": _("Servizio camera non raggiungibile: %(detail)s", detail=str(exc)),
|
||||||
|
}), 502
|
||||||
|
sources = body.get("sources") if isinstance(body, dict) else None
|
||||||
|
if not isinstance(sources, list):
|
||||||
|
return jsonify({
|
||||||
|
"error": True,
|
||||||
|
"detail": _("Risposta del servizio camera non valida (%(status)s)", status=resp.status_code),
|
||||||
|
}), 502
|
||||||
|
return jsonify({"sources": sources}), 200
|
||||||
|
|
||||||
|
|
||||||
# Copia minima della logica di measure.api_vision_grab, ma per una stazione scelta per id
|
# Copia minima della logica di measure.api_vision_grab, ma per una stazione scelta per id
|
||||||
@maker_bp.route("/api/stations/<int:station_id>/camera/grab", methods=["GET", "POST"])
|
@maker_bp.route("/api/stations/<int:station_id>/camera/grab", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// «Acquisizione» dell'editor task (#97): camera (servizio + sorgente) e modo di analisi
|
||||||
|
// («impulsiva» / «in linea») dei task camera_measure e dxf_compare.
|
||||||
|
// Legge /maker/api/camera/servizi e /maker/api/camera/sorgenti?url=. Lo stato scelto sta
|
||||||
|
// nel componente padre (editTaskData.acquisition): questo componente carica solo gli elenchi.
|
||||||
|
// Uso: x-data="taskAcquisition(() => editTaskData.acquisition, msgs)". Dipende da Alpine.js.
|
||||||
|
function taskAcquisition(getAcq, msgs) {
|
||||||
|
return {
|
||||||
|
msgs: msgs || {},
|
||||||
|
servizi: [],
|
||||||
|
sources: [],
|
||||||
|
loading: false,
|
||||||
|
error: '',
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.loadServizi();
|
||||||
|
// Task con camera gia' scelta: elenco sorgenti del suo servizio
|
||||||
|
const acq = getAcq();
|
||||||
|
if (acq && acq.camera_url) await this.loadSources(acq.camera_url);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadServizi() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/maker/api/camera/servizi');
|
||||||
|
if (resp.ok) this.servizi = (await resp.json()).servizi || [];
|
||||||
|
} catch (e) { console.error('taskAcquisition servizi', e); }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Servizio salvato nel task ma non piu' fra quelli delle stazioni: resta selezionabile
|
||||||
|
serviziConCorrente() {
|
||||||
|
const acq = getAcq();
|
||||||
|
const out = this.servizi.slice();
|
||||||
|
if (acq && acq.camera_url && !out.some(s => s.camera_url === acq.camera_url)) {
|
||||||
|
out.push({ camera_url: acq.camera_url, stazioni: [] });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cambio servizio: la sorgente scelta non vale piu'
|
||||||
|
async onServizio() {
|
||||||
|
const acq = getAcq();
|
||||||
|
acq.camera_source = '';
|
||||||
|
this.sources = [];
|
||||||
|
this.error = '';
|
||||||
|
if (acq.camera_url) await this.loadSources(acq.camera_url);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSources(url) {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/maker/api/camera/sorgenti?url=' + encodeURIComponent(url));
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
if (resp.ok) this.sources = data.sources || [];
|
||||||
|
else this.error = data.detail || this.msgs.genericError || '';
|
||||||
|
} catch (e) {
|
||||||
|
this.error = this.msgs.genericError || String(e);
|
||||||
|
} finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sorgente salvata ma non piu' elencata dal servizio: resta visibile
|
||||||
|
sourcesConCorrente() {
|
||||||
|
const acq = getAcq();
|
||||||
|
const out = this.sources.slice();
|
||||||
|
if (acq && acq.camera_source && !out.some(s => s.name === acq.camera_source)) {
|
||||||
|
out.push({ name: acq.camera_source, label: acq.camera_source });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -547,6 +547,71 @@
|
|||||||
|
|
||||||
<div class="tmf-card-body space-y-4">
|
<div class="tmf-card-body space-y-4">
|
||||||
|
|
||||||
|
<!-- ========== Acquisizione (#97): camera e modo di analisi, solo camera_measure / dxf_compare ========== -->
|
||||||
|
<template x-if="task.task_type === 'camera_measure' || task.task_type === 'dxf_compare'">
|
||||||
|
<div class="p-3 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)]">
|
||||||
|
<p class="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wide mb-2">
|
||||||
|
{{ _('Acquisizione') }}
|
||||||
|
</p>
|
||||||
|
<!-- Lettura: camera e modo salvati nel task -->
|
||||||
|
<template x-if="editingTask !== task.id">
|
||||||
|
<div class="text-xs space-y-1">
|
||||||
|
<template x-if="task.acquisition_json && task.acquisition_json.camera_url">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p><span class="text-[var(--text-secondary)]">{{ _('Servizio di acquisizione') }}:</span>
|
||||||
|
<span class="font-mono" x-text="task.acquisition_json.camera_url"></span></p>
|
||||||
|
<p><span class="text-[var(--text-secondary)]">{{ _('Sorgente') }}:</span>
|
||||||
|
<span class="font-mono" x-text="task.acquisition_json.camera_source"></span></p>
|
||||||
|
<p><span class="text-[var(--text-secondary)]">{{ _('Modo di analisi') }}:</span>
|
||||||
|
<span x-text="task.acquisition_json.modo === 'in_linea' ? {{ _('Analisi in linea')|tojson_attr }} : {{ _('Analisi impulsiva')|tojson_attr }}"></span></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p x-show="!(task.acquisition_json && task.acquisition_json.camera_url)" class="text-[var(--text-muted)]">
|
||||||
|
{{ _("Camera della stazione dell'operatore") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- Modifica: servizio, sorgente, modo (salvati con «Salva» del task) -->
|
||||||
|
<template x-if="editingTask === task.id">
|
||||||
|
<div class="space-y-3"
|
||||||
|
x-data="taskAcquisition(() => editTaskData.acquisition, {{ {'genericError': _('Errore durante il caricamento delle sorgenti')}|tojson_attr }})">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="tmf-label text-xs">{{ _('Servizio di acquisizione') }}</label>
|
||||||
|
<select class="tmf-input text-xs" x-model="editTaskData.acquisition.camera_url" @change="onServizio()">
|
||||||
|
<option value="">{{ _("Camera della stazione dell'operatore") }}</option>
|
||||||
|
<template x-for="sv in serviziConCorrente()" :key="sv.camera_url">
|
||||||
|
<option :value="sv.camera_url"
|
||||||
|
x-text="sv.camera_url + (sv.stazioni.length ? ' (' + sv.stazioni.join(', ') + ')' : '')"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div x-show="editTaskData.acquisition.camera_url">
|
||||||
|
<label class="tmf-label text-xs">{{ _('Sorgente') }}</label>
|
||||||
|
<select class="tmf-input text-xs" x-model="editTaskData.acquisition.camera_source" :disabled="loading">
|
||||||
|
<option value="">{{ _('Scegli la sorgente') }}</option>
|
||||||
|
<template x-for="src in sourcesConCorrente()" :key="src.name">
|
||||||
|
<option :value="src.name" x-text="src.label || src.name"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" class="text-xs text-red-500" x-text="error"></p>
|
||||||
|
<div x-show="editTaskData.acquisition.camera_url" class="flex flex-wrap items-center gap-4 text-xs">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" :name="'acq-modo-' + task.id" value="impulsiva" x-model="editTaskData.acquisition.modo">
|
||||||
|
{{ _('Analisi impulsiva') }}
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" :name="'acq-modo-' + task.id" value="in_linea" x-model="editTaskData.acquisition.modo">
|
||||||
|
{{ _('Analisi in linea') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- ========== Ricetta visione (#46): solo camera_measure / dxf_compare ========== -->
|
<!-- ========== Ricetta visione (#46): solo camera_measure / dxf_compare ========== -->
|
||||||
<template x-if="task.task_type === 'camera_measure' || task.task_type === 'dxf_compare'">
|
<template x-if="task.task_type === 'camera_measure' || task.task_type === 'dxf_compare'">
|
||||||
<div class="p-3 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)]"
|
<div class="p-3 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)]"
|
||||||
@@ -1335,6 +1400,7 @@
|
|||||||
<script src="{{ url_for('static', filename='js/vision_recipe.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/vision_recipe.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/task_acquisition.js') }}"></script>{# #97 #}
|
||||||
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||||
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';</script>
|
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
@@ -1389,7 +1455,7 @@ function taskEditor() {
|
|||||||
|
|
||||||
// ---- Task CRUD state ----
|
// ---- Task CRUD state ----
|
||||||
editingTask: null,
|
editingTask: null,
|
||||||
editTaskData: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
editTaskData: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [], acquisition: { camera_url: '', camera_source: '', modo: 'impulsiva' } }, // #97
|
||||||
newTask: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
newTask: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
||||||
MAX_NOTE_FIELDS: 20, // #55: limite campi per nota (stesso del backend)
|
MAX_NOTE_FIELDS: 20, // #55: limite campi per nota (stesso del backend)
|
||||||
_fieldUid: 0,
|
_fieldUid: 0,
|
||||||
@@ -1575,6 +1641,18 @@ function taskEditor() {
|
|||||||
this.newTask = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
this.newTask = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// #97: acquisition_json del task; null = camera della stazione (voce vuota o tipo senza camera)
|
||||||
|
_acquisitionPayload(obj) {
|
||||||
|
const a = obj.acquisition || {};
|
||||||
|
const usesCamera = obj.task_type === 'camera_measure' || obj.task_type === 'dxf_compare';
|
||||||
|
if (!usesCamera || !a.camera_url) return { ok: true, value: null };
|
||||||
|
if (!a.camera_source) {
|
||||||
|
this.errorMessage = {{ _("Scegli la sorgente della camera")|tojson }};
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
return { ok: true, value: { camera_url: a.camera_url, camera_source: a.camera_source, modo: a.modo === 'in_linea' ? 'in_linea' : 'impulsiva' } };
|
||||||
|
},
|
||||||
|
|
||||||
// ---- #53 #55: ciclo di misura e campi della nota ----
|
// ---- #53 #55: ciclo di misura e campi della nota ----
|
||||||
|
|
||||||
// #53: default di "nel ciclo" per tipo (stessa regola del backend)
|
// #53: default di "nel ciclo" per tipo (stessa regola del backend)
|
||||||
@@ -1785,7 +1863,13 @@ function taskEditor() {
|
|||||||
in_loop: task.in_loop != null ? !!task.in_loop : this.loopDefault(task.task_type || 'note'),
|
in_loop: task.in_loop != null ? !!task.in_loop : this.loopDefault(task.task_type || 'note'),
|
||||||
in_loop_touched: false,
|
in_loop_touched: false,
|
||||||
at_close: !!task.at_close,
|
at_close: !!task.at_close,
|
||||||
fields: this._fieldsToState(task.fields)
|
fields: this._fieldsToState(task.fields),
|
||||||
|
// #97: camera e modo di analisi del task (acquisition_json), vuoti = camera della stazione
|
||||||
|
acquisition: {
|
||||||
|
camera_url: (task.acquisition_json && task.acquisition_json.camera_url) || '',
|
||||||
|
camera_source: (task.acquisition_json && task.acquisition_json.camera_source) || '',
|
||||||
|
modo: (task.acquisition_json && task.acquisition_json.modo) || 'impulsiva'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Ensure expanded
|
// Ensure expanded
|
||||||
this.expandedTask = task.id;
|
this.expandedTask = task.id;
|
||||||
@@ -1793,7 +1877,7 @@ function taskEditor() {
|
|||||||
|
|
||||||
cancelEditTask() {
|
cancelEditTask() {
|
||||||
this.editingTask = null;
|
this.editingTask = null;
|
||||||
this.editTaskData = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
this.editTaskData = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [], acquisition: { camera_url: '', camera_source: '', modo: 'impulsiva' } }; // #97
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTask(taskId) {
|
async updateTask(taskId) {
|
||||||
@@ -1801,6 +1885,8 @@ function taskEditor() {
|
|||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
const fp = this._fieldsPayload(this.editTaskData); // #55
|
const fp = this._fieldsPayload(this.editTaskData); // #55
|
||||||
if (!fp.ok) return;
|
if (!fp.ok) return;
|
||||||
|
const acq = this._acquisitionPayload(this.editTaskData); // #97
|
||||||
|
if (!acq.ok) return;
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1818,7 +1904,8 @@ function taskEditor() {
|
|||||||
// #53 #55: ciclo di misura, compilazione a fine produzione, campi della nota
|
// #53 #55: ciclo di misura, compilazione a fine produzione, campi della nota
|
||||||
in_loop: !!this.editTaskData.in_loop,
|
in_loop: !!this.editTaskData.in_loop,
|
||||||
at_close: this.editTaskData.task_type === 'note' && !!this.editTaskData.at_close,
|
at_close: this.editTaskData.task_type === 'note' && !!this.editTaskData.at_close,
|
||||||
fields: fp.fields
|
fields: fp.fields,
|
||||||
|
acquisition_json: acq.value // #97
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""gitea:Adriano/TieMeasureFlow#97 - camera e modo di analisi nel task visione (lato Maker).
|
||||||
|
|
||||||
|
api_client e requests sono finti: nessun servizio camera vero.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from tests.conftest import api_get_router
|
||||||
|
|
||||||
|
ST1 = {"id": 4, "code": "ST-4", "name": "Banco", "camera_url": "http://acq:8765/", "camera_source": "camera:1"}
|
||||||
|
ST2 = {"id": 5, "code": "ST-5", "name": "Linea", "camera_url": "http://acq:8765", "camera_source": "camera:2"}
|
||||||
|
ST3 = {"id": 6, "code": "ST-6", "name": "Altra", "camera_url": "http://altro:9000", "camera_source": "x"}
|
||||||
|
NO_CAMERA = {"id": 7, "code": "ST-7", "name": "Senza", "camera_url": None, "camera_source": None}
|
||||||
|
SOURCES = {"sources": [
|
||||||
|
{"name": "camera:601020", "kind": "camera", "label": "Basler 601020"},
|
||||||
|
{"name": "pezzo.png", "kind": "image", "label": "immagine - pezzo.png"},
|
||||||
|
]}
|
||||||
|
|
||||||
|
|
||||||
|
def _stations(mock_api_client, stations=(ST1, ST2, ST3, NO_CAMERA)):
|
||||||
|
mock_api_client.get.side_effect = api_get_router({"/api/stations": list(stations)})
|
||||||
|
|
||||||
|
|
||||||
|
def _fake(status=200, body=None):
|
||||||
|
resp = MagicMock(status_code=status, text="")
|
||||||
|
if body is None:
|
||||||
|
resp.json.side_effect = ValueError("no json")
|
||||||
|
else:
|
||||||
|
resp.json.return_value = body
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
class TestCameraServizi:
|
||||||
|
def test_distinct_urls_with_stations(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/servizi")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json() == {"servizi": [
|
||||||
|
{"camera_url": "http://acq:8765", "stazioni": ["ST-4 - Banco", "ST-5 - Linea"]},
|
||||||
|
{"camera_url": "http://altro:9000", "stazioni": ["ST-6 - Altra"]},
|
||||||
|
]}
|
||||||
|
|
||||||
|
def test_no_stations_with_camera(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client, [NO_CAMERA])
|
||||||
|
assert logged_in_client.get("/maker/api/camera/servizi").get_json() == {"servizi": []}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCameraSorgenti:
|
||||||
|
def test_lists_sources(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get", return_value=_fake(200, SOURCES)) as get:
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://acq:8765/")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json() == SOURCES
|
||||||
|
args, kwargs = get.call_args
|
||||||
|
assert args[0] == "http://acq:8765/camera/sources"
|
||||||
|
assert kwargs["timeout"] == 15
|
||||||
|
|
||||||
|
def test_url_not_a_station_service_is_400(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get") as get:
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://evil:1")
|
||||||
|
assert resp.status_code == 400
|
||||||
|
get.assert_not_called()
|
||||||
|
|
||||||
|
def test_missing_url_is_400(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get") as get:
|
||||||
|
assert logged_in_client.get("/maker/api/camera/sorgenti").status_code == 400
|
||||||
|
get.assert_not_called()
|
||||||
|
|
||||||
|
def test_service_down_is_502(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get",
|
||||||
|
side_effect=requests.ConnectionError("refused")):
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://acq:8765")
|
||||||
|
assert resp.status_code == 502
|
||||||
|
assert "refused" in resp.get_json()["detail"]
|
||||||
|
|
||||||
|
def test_service_error_status_is_502(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get", return_value=_fake(500, {"detail": "x"})):
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://acq:8765")
|
||||||
|
assert resp.status_code == 502
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskProxyForwardsAcquisition:
|
||||||
|
def test_put_forwards_acquisition_json(self, logged_in_client, mock_api_client):
|
||||||
|
acq = {"camera_url": "http://acq:8765", "camera_source": "camera:1", "modo": "in_linea"}
|
||||||
|
mock_api_client.put.return_value = {"id": 11, "title": "T", "acquisition_json": acq}
|
||||||
|
resp = logged_in_client.put("/maker/api/tasks/11", json={"title": "T", "acquisition_json": acq})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert mock_api_client.put.call_args.kwargs["data"]["acquisition_json"] == acq
|
||||||
|
|
||||||
|
def test_put_forwards_null(self, logged_in_client, mock_api_client):
|
||||||
|
mock_api_client.put.return_value = {"id": 11, "title": "T", "acquisition_json": None}
|
||||||
|
logged_in_client.put("/maker/api/tasks/11", json={"title": "T", "acquisition_json": None})
|
||||||
|
assert mock_api_client.put.call_args.kwargs["data"]["acquisition_json"] is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskEditorAcquisitionUi:
|
||||||
|
def _html(self, client, mock_api_client, task_type="camera_measure"):
|
||||||
|
mock_api_client.get.side_effect = api_get_router({
|
||||||
|
"/api/recipes/3": {"id": 3, "code": "R", "name": "R"},
|
||||||
|
"/api/recipes/3/tasks": [{
|
||||||
|
"id": 11, "title": "T", "task_type": task_type, "subtasks": [],
|
||||||
|
"file_path": None, "order_index": 1,
|
||||||
|
"acquisition_json": {"camera_url": "http://acq:8765", "camera_source": "camera:1",
|
||||||
|
"modo": "in_linea"},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
return client.get("/maker/recipes/3/tasks").get_data(as_text=True)
|
||||||
|
|
||||||
|
def test_section_and_fields(self, logged_in_client, mock_api_client):
|
||||||
|
html = self._html(logged_in_client, mock_api_client)
|
||||||
|
assert "taskAcquisition(" in html
|
||||||
|
assert "js/task_acquisition.js" in html
|
||||||
|
assert "Servizio di acquisizione" in html
|
||||||
|
assert "Sorgente" in html
|
||||||
|
assert "Analisi impulsiva" in html and "Analisi in linea" in html
|
||||||
|
assert 'value="impulsiva"' in html and 'value="in_linea"' in html
|
||||||
|
assert "Camera della stazione dell'operatore" in html or \
|
||||||
|
"Camera della stazione dell'operatore" in html
|
||||||
|
|
||||||
|
def test_payload_carries_acquisition_json(self, logged_in_client, mock_api_client):
|
||||||
|
html = self._html(logged_in_client, mock_api_client)
|
||||||
|
assert "acquisition_json: acq.value" in html
|
||||||
|
assert "_acquisitionPayload(this.editTaskData)" in html
|
||||||
|
# il task salvato riempie lo stato di modifica
|
||||||
|
assert "task.acquisition_json.camera_url" in html
|
||||||
|
|
||||||
|
def test_static_js_endpoints(self):
|
||||||
|
import os
|
||||||
|
path = os.path.join(os.path.dirname(__file__), "..", "static", "js", "task_acquisition.js")
|
||||||
|
src = open(path, encoding="utf-8").read()
|
||||||
|
assert "/maker/api/camera/servizi" in src
|
||||||
|
assert "/maker/api/camera/sorgenti?url=" in src
|
||||||
Reference in New Issue
Block a user