Merge branch 'lavoro/daitarn-tmf-97c' into lavoro/daitarn-tmf-dxfcal
This commit is contained in:
@@ -11,6 +11,13 @@
|
||||
* taskType, subtasks, labels.
|
||||
* Run context (lot, serial, production run) is read lazily through `ctx()`,
|
||||
* which the template binds to the parent task component.
|
||||
*
|
||||
* #97: with a camera (cameraAvailable, the task's or the station's) the task opens
|
||||
* on a frame already grabbed and analysed (/vision/check, nothing saved: «preview»).
|
||||
* modo «impulsiva»: «Acquisisci» redoes grab+check. modo «in_linea»: continuous
|
||||
* loop grab+check, one request at a time, standing still while paused / page hidden
|
||||
* / leaving / saving. «Registra» saves the LAST frame shown through /vision/execute.
|
||||
* Extra config: cameraAvailable, modo, checkUrl.
|
||||
*/
|
||||
function visionExecute() {
|
||||
// Overlay colour per role (VisionSuite roles: reference, roi, found).
|
||||
@@ -24,6 +31,125 @@ function visionExecute() {
|
||||
imageSrc: '', // object URL of the acquired image (shown under the overlay)
|
||||
ctx: () => ({}), // replaced by the template: {lot, serial, runId}
|
||||
|
||||
// #97: preview / loop state
|
||||
preview: false, // result on screen is an analysis only, not saved
|
||||
saved: false, // «Registra» done: the saved result replaces the preview
|
||||
lastBlob: null, // last frame shown, the one «Registra» saves
|
||||
paused: false, // operator paused the in-line loop
|
||||
registering: false, // «Registra» in progress: the loop stands still
|
||||
leaving: false, // page is being left: no more requests
|
||||
looping: false, // an in-line loop is running
|
||||
gen: 0, // bumped by «Registra»: answers of older requests are dropped
|
||||
|
||||
// #97: start on a frame already acquired and analysed; hook visibility and exit.
|
||||
init() {
|
||||
if (!this.hasCamera) return;
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) this.kick(); });
|
||||
const leave = () => { this.leaving = true; };
|
||||
window.addEventListener('pagehide', leave);
|
||||
window.addEventListener('beforeunload', leave);
|
||||
// After x-init has bound ctx(): in-line starts the loop, impulsive grabs once.
|
||||
this.$nextTick(() => { if (this.inLinea) this.loop(); else this.grab(); });
|
||||
},
|
||||
|
||||
get hasCamera() { return !!(window.__visionCfg || {}).cameraAvailable; },
|
||||
get inLinea() { return this.hasCamera && (window.__visionCfg || {}).modo === 'in_linea'; },
|
||||
// «Registra» needs a frame on screen that is not saved yet
|
||||
get canRecord() { return !!this.lastBlob && !this.saved && !this.registering; },
|
||||
|
||||
shouldRun() {
|
||||
return this.inLinea && !this.paused && !this.saved && !this.registering
|
||||
&& !this.leaving && !document.hidden;
|
||||
},
|
||||
|
||||
// Page visible again: restart the loop unless the operator paused it.
|
||||
kick() { if (this.shouldRun() && !this.looping) this.loop(); },
|
||||
|
||||
// In-line loop: one request at a time, at least 300 ms between two of them
|
||||
// (longer after an error, so a dead camera is not hammered).
|
||||
async loop() {
|
||||
if (this.looping) return;
|
||||
this.looping = true;
|
||||
try {
|
||||
while (this.shouldRun()) {
|
||||
const ok = await this.grab();
|
||||
await new Promise((r) => setTimeout(r, ok ? 300 : 2000));
|
||||
}
|
||||
} finally {
|
||||
this.looping = false;
|
||||
}
|
||||
},
|
||||
|
||||
// «Pausa» / «Riprendi»; after «Registra» only «Riprendi» restarts the loop.
|
||||
togglePause() {
|
||||
if (this.paused || this.saved) {
|
||||
this.paused = false;
|
||||
this.saved = false;
|
||||
this.loop();
|
||||
} else {
|
||||
this.paused = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Analysis only (/vision/check): the frame becomes the preview, nothing is saved.
|
||||
async analyze(blob) {
|
||||
const cfg = window.__visionCfg || {};
|
||||
if (!blob || !cfg.checkUrl) return false;
|
||||
const gen = this.gen;
|
||||
this.busy = true;
|
||||
this.error = '';
|
||||
try {
|
||||
const c = this.ctx() || {};
|
||||
const fd = new FormData();
|
||||
fd.append('task_id', cfg.taskId);
|
||||
fd.append('image', blob, blob.name || 'capture.jpg');
|
||||
if (c.runId) fd.append('production_run_id', c.runId);
|
||||
const csrf = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
const resp = await fetch(cfg.checkUrl, {
|
||||
method: 'POST', headers: { 'X-CSRFToken': csrf }, body: fd,
|
||||
});
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
if (gen !== this.gen) return false; // «Registra» took over
|
||||
if (!resp.ok || body.error) {
|
||||
this.error = body.detail || (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
return false;
|
||||
}
|
||||
if (this.imageSrc) URL.revokeObjectURL(this.imageSrc);
|
||||
this.imageSrc = URL.createObjectURL(blob);
|
||||
this.lastBlob = blob;
|
||||
this.result = body;
|
||||
this.preview = true;
|
||||
this.saved = false;
|
||||
this.$nextTick(() => this.drawOverlay());
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (gen === this.gen) this.error = (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
return false;
|
||||
} finally {
|
||||
if (gen === this.gen) this.busy = false;
|
||||
}
|
||||
},
|
||||
|
||||
// «Registra»: save the LAST frame shown with the usual /vision/execute.
|
||||
async record() {
|
||||
if (!this.canRecord) return;
|
||||
const blob = this.lastBlob;
|
||||
this.registering = true;
|
||||
this.gen++; // drop whatever request is still in flight
|
||||
this.busy = false;
|
||||
this.grabbing = false;
|
||||
try {
|
||||
await this.acquire(blob);
|
||||
if (!this.error && this.result && this.result.vision_result_id) {
|
||||
this.preview = false;
|
||||
this.saved = true;
|
||||
}
|
||||
} finally {
|
||||
this.registering = false;
|
||||
this.kick(); // in-line: stays still while saved, restarts if the save failed
|
||||
}
|
||||
},
|
||||
|
||||
// Entry point shared with the future agent camera.
|
||||
async acquire(blob) {
|
||||
if (!blob || this.busy) return;
|
||||
@@ -64,37 +190,52 @@ function visionExecute() {
|
||||
|
||||
// Station camera (#72): the Flask proxy answers a PNG, which goes through
|
||||
// acquire() like a picked file. Errors show as the execute ones do.
|
||||
// #97: sends task_id (the task's camera wins over the station's); with a camera
|
||||
// the frame goes to the preview analysis. Returns true when the frame was analysed.
|
||||
async grab() {
|
||||
const cfg = window.__visionCfg || {};
|
||||
if (this.busy || this.grabbing || !cfg.grabUrl) return;
|
||||
if (this.busy || this.grabbing || this.registering || !cfg.grabUrl) return false;
|
||||
const gen = this.gen;
|
||||
this.grabbing = true;
|
||||
this.error = '';
|
||||
let blob = null;
|
||||
try {
|
||||
const csrf = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
const fd = new FormData();
|
||||
fd.append('task_id', cfg.taskId);
|
||||
const resp = await fetch(cfg.grabUrl, {
|
||||
method: 'POST', headers: { 'X-CSRFToken': csrf },
|
||||
method: 'POST', headers: { 'X-CSRFToken': csrf }, body: fd,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const body = await resp.json().catch(() => ({}));
|
||||
this.error = body.detail || (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
return;
|
||||
if (gen === this.gen) this.error = body.detail || (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
return false;
|
||||
}
|
||||
blob = await resp.blob();
|
||||
} catch (e) {
|
||||
this.error = (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
return;
|
||||
if (gen === this.gen) this.error = (cfg.labels && cfg.labels.requestFailed) || 'Error';
|
||||
return false;
|
||||
} finally {
|
||||
this.grabbing = false;
|
||||
if (gen === this.gen) this.grabbing = false;
|
||||
}
|
||||
if (blob) await this.acquire(blob);
|
||||
if (!blob || gen !== this.gen) return false;
|
||||
if (this.hasCamera) return await this.analyze(blob);
|
||||
await this.acquire(blob);
|
||||
return !this.error;
|
||||
},
|
||||
|
||||
// Input change: take the file and allow picking the same one again.
|
||||
// #97: with a camera the file is a preview too (the loop pauses), «Registra» saves it.
|
||||
onPick(ev) {
|
||||
const f = ev.target.files && ev.target.files[0];
|
||||
ev.target.value = '';
|
||||
if (f) this.acquire(f);
|
||||
if (!f) return;
|
||||
if (this.hasCamera) {
|
||||
if (this.inLinea) this.paused = true;
|
||||
this.analyze(f);
|
||||
} else {
|
||||
this.acquire(f);
|
||||
}
|
||||
},
|
||||
|
||||
// Image shown: the saved one from the server when available, else the local one.
|
||||
|
||||
Reference in New Issue
Block a user