Il gate del fuori tolleranza vale per l'acquisizione e non per la quota: la regola era pensata per la misura sequenziale e su un evento simultaneo produceva un blocco permanente. Piu' il bi-ambiente Python, la versione del motore timbrata al build, la motivazione della divisione degli extra corretta, e due cose promesse e non mantenute -- expected_json e image_path -- dichiarate come consegne del piano 1b invece che lasciate come intenzioni. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
23 KiB
Integrazione di VisionSuite in TieMeasureFlow — design
Progetto: TieMeasureFlow V3.0.0 → V3.1.0 Data: 2026-08-16 Stato: design approvato, piano di implementazione da scrivere
Perché
TieMeasureFlow oggi registra misure prese a mano, con il calibro USB o con il tastierino. Il passo successivo che il cliente chiede è misurare con una camera: acquisire l'immagine di un pezzo e ricavarne le quote, oppure confrontarne il profilo con il disegno DXF di riferimento.
La visione artificiale non va scritta qui. Tielogic ha già VisionSuite, un monorepo di moduli riusabili il cui README indica esplicitamente TieMeasureFlow fra i consumatori previsti. Questo documento descrive come agganciarlo.
Il terreno è in parte già preparato. La migrazione 007_task_type ha introdotto
il tipo di task dichiarato e l'enum contiene fin da allora due valori che nessuno
produce né consuma ancora:
TASK_TYPES = ("note", "measure", "drawing", "xf_compare", "camera_measure")
MEASURING_TASK_TYPES = ("measure", "camera_measure")
Questo lavoro li riempie.
Che cosa entra e che cosa resta fuori
Entra: l'acquisizione da camera, l'esecuzione di un programma di visione su quell'immagine — pattern matching più misure, oppure pattern matching più confronto DXF — e l'ingresso dei risultati nel modello di misura che già esiste.
Resta fuori, per ora: i semafori USB. Sono previsti dall'architettura — è l'agente di stazione a possederli, insieme alle camere — ma non fanno parte di questa consegna. Il confine è disegnato perché ci entrino senza ristrutturazioni.
Le otto decisioni
Sono le scelte prese in fase di brainstorming, con il motivo per cui sono state prese. Chi legge fra sei mesi ha bisogno del motivo più della scelta.
1. La visione gira sia sul server sia sulla stazione
Il motore è un componente unico distribuito in due posti: accanto al server e dentro l'agente di stazione. Questo dà la resilienza — se cade il collegamento, la stazione continua a misurare — e permette al server di elaborare in fase di produzione, di comporre ricette per conto del Maker e di rifare analisi su immagini archiviate.
È coerente con la decisione D-3, chiusa il 28/07: dati sul server, app di stazione su ogni PC.
2. Server e stazione montano la stessa identica versione di VisionSuite
Non è una raccomandazione di build: è un vincolo metrologico. Se il server monta un commit e una stazione ne monta un altro, lo stesso pezzo con lo stesso grafo può dare due numeri diversi, e nessuno se ne accorge finché non arriva una contestazione.
Da qui tre obblighi che il design deve garantire:
- VisionSuite entra come sottomodulo git su un commit preciso, lo stesso per entrambi — la forma che il README di VisionSuite prescrive
- ogni misura porta scritto con quale versione del motore è stata prodotta
- una stazione che monta una versione diversa da quella del server si rifiuta di misurare e lo dichiara, invece di produrre numeri plausibili e sbagliati
3. L'agente di stazione è sottile
L'agente parla ai device, consegna immagini e — più avanti — piloterà i semafori. Non contiene decisioni di visione.
La conseguenza pratica è l'impacchettamento: un agente che non contiene VisionSuite non contiene nemmeno PyTorch. Distribuirlo come eseguibile su Windows o come container su Linux diventa realistico, e la scelta su driver e formato può restare tardiva — che è bene, perché dipende dalla D-4, ancora aperta.
L'agente incorpora il motore solo quando la stazione è configurata per elaborare in locale, e in quel caso resta comunque un guscio: riceve un grafo, lo esegue, restituisce le uscite.
4. Il server non importa mai VisionSuite
La visione sul lato server vive in un servizio separato nello stesso compose, il vision worker. Il server FastAPI continua a fare quello che fa — autenticazione, ricette, misure, statistica — e resta leggero.
Quattro ragioni concrete: l'immagine dell'API non passa da poche centinaia di megabyte a cinque gigabyte; un aggiornamento di VisionSuite non richiede di riavviare l'API in produzione; un'elaborazione che va in crash non porta giù le richieste degli altri tablet; il giorno in cui la visione chiede una GPU o una macchina propria si sposta senza toccare il resto.
Il confine va difeso: se un domani il server importasse VisionSuite per una scorciatoia, il vantaggio si perde in silenzio.
5. La configurazione dei device sta sul PC
Le camere — e domani i semafori — appartengono alla macchina, non alla ricetta. Il server ne tiene uno specchio in sola lettura, popolato dall'agente quando si connette, perché il Maker deve poter scegliere un device da un elenco stando alla scrivania.
Un device che l'agente non dichiara più è offline, non cancellato: una ricetta che lo referenzia deve continuare a esistere e a spiegare perché non può girare.
6. Dove si esegue lo decide la configurazione della stazione
Ogni PC dichiara se elabora in locale o delega al server, nella stessa configurazione dove già stanno i device. Il default è il server.
Le macchine capaci si prendono il carico e non mandano immagini in rete; quelle deboli delegano. Se il server non risponde e la stazione ha il motore, la stazione ripiega in locale e lo segna nel risultato. Un impianto misto si governa senza toccare le ricette.
La ricetta non dice mai dove gira: legare una scelta di impianto a un documento di produzione significa dover rimettere mano alle ricette per spostare il carico.
7. Il verdetto lo dà TieMeasureFlow, non il grafo
Gli strumenti di quota di vs-task hanno anche loro nominal, plus, minus e
un outcome. Quell'esito viene ignorato e quei limiti restano vuoti: il grafo
produce numeri, measurement_service decide se sono buoni.
Due giudici sullo stesso valore è il modo più rapido per ottenere un CSV che si contraddice e una contestazione che non si sa come chiudere.
La conseguenza è che tutto il resto continua a funzionare senza modifiche: pass/warning/fail, gate del fuori tolleranza con autorizzazione del capoturno, Cp/Cpk, carta di controllo, colonne del CSV di produzione. Una quota da camera è una misura come le altre.
8. La sorgente immagine è un'astrazione a tre facce
Il motore riceve un'immagine e non sa da dove venga. Le tre sorgenti sono: dal vivo da un device tramite l'agente, un'immagine di riferimento salvata sul server, un file caricato dal Maker.
Non è un'astrazione nostra: è quella che VisionSuite si è già data — «espongono una sorgente, non una telecamera: è questo che permette di collaudare un impianto a cinque telecamere senza avere le cinque telecamere sul tavolo, e di scrivere prove che danno sempre lo stesso risultato».
Sblocca tre cose che altrimenti non ci sarebbero:
- il Maker compone alla scrivania, senza tenere occupata una stazione né avere il pezzo in mano
- una ricetta si prova prima di andare in produzione, invece di scoprirne i difetti misurando
- diventa possibile una prova di non regressione: salvando accanto all'immagine il risultato atteso, si riesegue l'intero parco ricette dopo un aggiornamento di VisionSuite e si vede quali quote si sono mosse
Quest'ultima è la rete di sicurezza che rende sostenibile aggiornare il sottomodulo, con un motore che sta su due host.
Non ancora consegnata. La colonna
expected_jsonesiste, ma nessun percorso la scrive — nemmenopreview, che è l'unico posto dove un grafo gira davvero su un'immagine. La prova di non regressione è quindi promessa da questo documento e non mantenuta da nessuna riga di codice. Diventa una consegna nominata del piano 1b: «previewpuò promuovere le proprie uscite aexpected_json». Finché non è fatta, questa decisione non va dichiarata completa.
Architettura
I componenti
| Componente | Nuovo | Dove vive | Responsabilità |
|---|---|---|---|
vendor/visionsuite |
sì | sottomodulo git | i pacchetti vs-core, vs-camera, vs-pm2d, vs-measure, vs-dxf, su un commit fisso |
| Vision runner | sì | libreria condivisa | immagine + grafo + calibrazione → quote e artefatti. Unica implementazione, due host |
| Vision worker | sì | container nel compose | espone il runner dietro una API interna. Il container pesante |
| Agente di stazione | sì | PC di reparto | possiede i device, acquisisce, apre la connessione verso il server, incorpora il runner se configurato |
| Backend FastAPI | modificato | container esistente | tipi di task, grafo sul task, registro device, inoltro, ingresso risultati. Nessun import di VisionSuite |
| Frontend Flask | modificato | container esistente | Maker: device, fotogramma, aggancio quote. Measure: schermata del task con camera |
L'aggancio del sottomodulo segue la forma prescritta dal README di VisionSuite: dipendenza dai soli pacchetti che servono, dichiarati in path editable.
[tool.uv.sources]
visionsuite = { path = "vendor/visionsuite/packages/vs-core", editable = true }
vs-camera = { path = "vendor/visionsuite/packages/vs-camera", editable = true }
I due host non montano lo stesso insieme, ed è il punto dell'intera struttura di VisionSuite:
| Pacchetto | Worker | Agente | Perché |
|---|---|---|---|
vs-core |
sì | sì | strutture di base e calibrazione, servono a entrambi |
vs-pm2d |
sì | solo se elabora in locale | pattern matching |
vs-measure |
sì | solo se elabora in locale | primitive e quote |
vs-dxf |
sì | solo se elabora in locale | confronto col disegno. È quello che porta PyTorch |
vs-camera |
no | sì | il worker non tocca hardware: riceve immagini già acquisite |
Un agente su una stazione che delega al server monta quindi due soli pacchetti,
vs-core e vs-camera, e resta leggero. È la differenza fra un eseguibile
distribuibile e un'installazione da cinque gigabyte su ogni PC di reparto.
I nomi di importazione non sono uniformi — vs-core si importa come
visionsuite, vs-pm2d come pm2d, vs-dxf come dxf_compare — ed è una
stortura nota e dichiarata a monte, non un errore da correggere qui.
Il flusso di una misura, esecuzione sul server
operatore apre il task
→ client chiede al server di eseguire
→ server chiede all'agente un fotogramma sul device dichiarato
→ agente acquisisce e carica l'immagine
→ server passa immagine e grafo al worker
→ worker restituisce le uscite
→ server applica pass/fail, gate del fuori tolleranza, salva in measurements
→ client mostra esito e immagine con le zone dove ha guardato
Il flusso con esecuzione in locale
Il giro si accorcia: il server manda il grafo all'agente, l'agente acquisisce ed esegue, e rimanda uscite più immagine. Da lì in poi il server fa le stesse identiche cose. Il verdetto, il gate e il salvataggio non cambiano mai posto: cambia chi calcola i numeri, non chi decide se sono buoni.
È questo che rende le due strade equivalenti a valle, e che permette di cambiare la configurazione di una stazione senza toccare né il frontend né le ricette.
Modello dati
Il grafo di visione
recipe_tasks prende vision_json (JSON, nullable): il grafo di vs-task
serializzato, opaco al server.
Il versionamento arriva gratis. Il copy-on-write delle ricette copia già i task
in profondità, quindi una modifica al grafo su una ricetta con misure produce una
versione nuova come qualunque altra modifica; in recipe_service è un campo in
più nella copia, non una logica nuova.
Non si riusa annotations_json, che sta lì accanto: le annotazioni sono un
disegno per l'operatore, il grafo è un programma da eseguire. Hanno cicli di vita
e regole di validazione diversi, e mescolarli significa non poter validare né
l'uno né l'altro.
Il legame fra quota del grafo e quota della ricetta
recipe_subtasks prende vision_output (String, nullable): la coppia
«identificativo dello strumento, nome della sua uscita».
È la stessa forma di legame che vs-task usa internamente, e per la stessa
ragione dichiarata nel suo README: «Il legame è un ID, non un puntatore» — un
riferimento in memoria non si scrive su disco.
Tutto il resto della subtask — nominale, UTL, UWL, LWL, LTL, unità — resta com'è e continua a significare quello che significa oggi.
Le misure
input_method passa da ("usb_caliper", "manual") a
("usb_caliper", "manual", "camera").
Il contorno della visione non va su measurements. Quella tabella la leggono
la statistica e l'export a ogni giro, e appesantirla la rovina. Ma soprattutto
sarebbe sbagliato di modello: una sola acquisizione produce N quote, e immagine,
overlay, device e versione del motore sono gli stessi per tutte.
Nasce quindi vision_results, una riga per esecuzione:
| Campo | Perché c'è |
|---|---|
image_path, overlay_path |
l'immagine, e quella con sopra le zone dove ha guardato |
engine_version |
il commit di VisionSuite che ha prodotto i numeri |
executed_on |
server o station |
station_id, device_id |
da quale macchina e quale camera |
calibration_snapshot |
la taratura in vigore in quel momento, copiata |
graph_snapshot |
il grafo effettivamente eseguito, copiato |
duration_ms, executed_at |
quanto è costata, quando |
Su measurements una sola colonna nuova: vision_result_id, FK nullable.
Calibrazione e grafo si copiano invece di essere referenziati per la stessa ragione: una taratura rifatta sei mesi dopo non deve riscrivere il significato di una misura già presa.
Il registro dei device
station_devices: station_id, code (l'identificativo locale), kind
(camera oggi, light domani), label, capabilities (JSON), calibration
(JSON) con calibration_taken_at, last_seen_at.
Si popola dall'agente. Il server lo riceve e non lo modifica.
Le immagini di riferimento
vision_reference_images: task_id, path, e la provenienza per intero —
station_id, device_id, snapshot della calibrazione, engine_version,
acquired_at, una nota — più expected_json per l'atteso della prova di non
regressione.
La provenienza non è rimandabile. Senza, si riesegue il grafo mesi dopo, escono numeri diversi, e non c'è modo di distinguere un motore aggiornato da un'ottica spostata.
Migrazione 011
Cinque cose: vision_json su recipe_tasks, vision_output su
recipe_subtasks, camera in input_method_enum, vision_result_id su
measurements, e le tre tabelle nuove.
Più una sesta, da fare adesso o mai: task_type_enum contiene xf_compare,
refuso per dxf_compare. Oggi nessuna riga usa quel valore e correggerlo costa
una riga. Dal primo task salvato in poi diventa una migrazione di dati, e il
refuso finisce nelle API pubbliche.
Come le migrazioni 008 e 010, va scritta in batch mode per la compatibilità
SQLite dei test e verificata in resa MySQL con alembic upgrade X:Y --sql prima
del deploy: è la procedura che il progetto si è già dato.
API
L'agente apre lui la connessione. Il server non bussa mai a un PC, e questo evita di dover aprire porte sulle macchine di reparto — che è materia della D-4.
| Verso | Endpoint | Cosa fa |
|---|---|---|
| agente → server | WS /api/stations/agent |
si autentica con la chiave di stazione, dichiara i device, resta in ascolto |
| server → agente | comando acquire |
«scatta sul device X»; l'agente carica con POST /api/vision/frames |
| server → agente | comando execute |
esecuzione in locale: manda il grafo, riceve le uscite |
| client → server | POST /api/vision/execute |
esegui il task per la produzione in corso; il server sceglie dove |
| client → server | POST /api/vision/preview |
authoring: esegui su una sorgente qualsiasi, non salvare nulla |
| client → server | GET /api/stations/{code}/devices |
l'elenco per il Maker |
| client → server | POST /api/vision/reference-images |
salva un fotogramma come riferimento |
| server → worker | POST /run |
interno, mai esposto: immagine e grafo → uscite e overlay |
Un solo endpoint decide dove si esegue, ed è execute. Il client non sa e non
deve sapere se ha misurato il server o la stazione: chiede una misura e riceve un
esito.
Errori e casi limite
| Caso | Comportamento |
|---|---|
| Versione del motore diversa fra stazione e server | la stazione si rifiuta di misurare e lo dichiara. Mai produrre numeri con un motore non concorde |
| Agente non raggiungibile | il task di visione non è eseguibile e lo dice. Da decidere con il cliente se la stazione debba consentire il ripiego sul calibro |
| Server non raggiungibile, stazione con motore | la stazione esegue in locale e lo segna in executed_on |
| Device dichiarato dalla ricetta ma offline | la ricetta resta valida, il task spiega perché non può girare |
| Il grafo non produce un'uscita attesa da una subtask | la misura non si salva, l'errore nomina l'uscita mancante |
| Elaborazione in crash o oltre il tempo | il worker isola il guasto, l'API resta in piedi, il task riporta l'errore |
| Device senza calibrazione | le quote in millimetri non si producono. Mai convertire con un fattore implicito |
Prove
Ai tre file di test che il progetto ha già e che leggono i sorgenti invece di
renderizzare — test_offline.py, test_layout_shell.py,
test_template_js_syntax.py — se ne aggiunge la stessa specie:
- una prova che il server non importi VisionSuite. È il confine su cui poggia la decisione 4, e senza qualcuno che lo controlli si perde in silenzio
- una prova che il commit del sottomodulo dichiarato coincida con quello che server e agente montano
Per il resto, il runner si prova su immagini di riferimento salvate — che è precisamente ciò per cui l'astrazione della sorgente esiste. Nessuna prova richiede una camera collegata.
Quello che questo design non risolve
- Il dimensionamento del server. Il pattern matching è pesante e, con l'esecuzione lato server come default, la CPU è contesa fra tutte le stazioni. Va aggiunto alla D-4, che finora chiedeva solo quale macchina e quanto disco: ora chiede anche quanti core e quanta RAM.
- La conservazione delle immagini. Due politiche distinte, entrambe da portare in D-4: le immagini di riferimento sono poche e vivono quanto la ricetta; quelle di produzione sono una per pezzo e crescono senza limite, e vogliono una scadenza o una regola del tipo «si conservano solo quelle dei pezzi fuori tolleranza».
- Che cosa fa la stazione se la rete cade e non è configurata per elaborare in locale: si blocca, o consente il calibro e segna il task come non eseguibile? È una domanda per il cliente.
- L'impacchettamento dell'agente. Le macchine possono essere Linux o Windows e i driver possono stare sulla macchina o nel container. La decisione è volutamente tardiva; il design la rende tale tenendo l'agente sottile e neutro.
Correzioni imposte dall'esecuzione
Il piano derivato da questo documento è stato eseguito il 16/08/2026. Sei cose che qui erano scritte male o taciute, corrette dai fatti.
Il gate del fuori tolleranza vale per l'acquisizione, non per la quota
La decisione 7 e il testo del punto 5 parlano di «la quota successiva». Quella regola era stata pensata per la misura manuale e sequenziale, dove esiste davvero un momento successivo in cui l'operatore potrebbe tirare avanti con una lettura cattiva irrisolta.
Una camera produce tutte le N quote nello stesso istante. Dentro un'acquisizione non esiste un «tirare avanti», quindi lì non c'è nulla da presidiare — e applicare la regola sequenziale a un evento simultaneo produceva un blocco permanente: la lettura fuori tolleranza non veniva mai registrata, quindi nessuno poteva autorizzarla, e rifare la stessa foto ripeteva l'errore.
Il gate si valuta una volta per acquisizione: se la misura pendente è fra le quote che questa acquisizione riprodurrà non blocca — riacquisire è rimisurare quelle quote, che questo documento già dichiara la via d'uscita che non richiede il capoturno. Fra due acquisizioni, e alla chiusura del ciclo, blocca come prima.
Il repository è deliberatamente bi-ambiente
vs-task, vs-measure e vs-pm2d dichiarano requires-python >= 3.13;
vs-core si ferma a 3.10. Backend e frontend restano su 3.11, l'albero della
visione gira su 3.13, e l'extra vision porta i marker d'ambiente che
tengono separate le due risoluzioni.
Questo rafforza la decisione 4 più di come era argomentata: con VisionSuite dentro il server FastAPI, questa scoperta avrebbe costretto l'intero backend a 3.13.
La versione del motore si timbra al build
vendor/visionsuite/.git è un file che rimanda a ../../.git/modules/..., che
nel contesto di build non esiste: interrogare git dentro il container non
funziona. engine_version() legge VISION_ENGINE_VERSION dall'ambiente, ripiega
su git nei checkout di sviluppo, e alza un errore se non risolve né l'uno né
l'altro. Il build fallisce se la variabile manca.
La divisione degli extra compra esplicitezza, non peso
Gli extra sono due — vision per il runner, vision-worker per il servizio web
— ed è la forma giusta. Ma la motivazione che sembrava ovvia è falsa: vs-pm2d
dichiara di suo fastapi, uvicorn, python-multipart e pillow, quindi
un agente di stazione che fa pattern matching se li porterà dietro comunque. La
correzione vera è a monte — una libreria non dovrebbe dipendere da un server web
— e va aggiunta alle verifiche su VisionSuite qui sotto.
L'immagine di produzione non viene ancora conservata
vision_results.image_path e overlay_path migrano e restano NULL: nulla le
scrive. Il flusso descritto in questo documento si chiude con «il client mostra
esito e immagine», e finché quelle colonne sono vuote una misura contestata non
ha prova materiale. Consegna nominata del piano 1b, insieme all'overlay.
Prima di applicare le migrazioni in produzione
La 011 rinomina un valore di enum su una tabella in esercizio. Va contato prima
che nessuna riga usi xf_compare: in strict mode una riga sopravvissuta fa
fallire l'ALTER a metà deploy, fuori strict mode diventa '' in silenzio.
Attenzione anche alla finestra: il MODIFY su recipe_tasks è una rinomina di
valore e userà ALGORITHM=COPY, mentre quello su measurements.input_method
aggiunge in coda e dovrebbe restare INPLACE — da confermare sulle dimensioni
reali, perché measurements è la tabella grossa.
Da verificare prima di cominciare
VisionSuite è stato lavorato fino al 15/08/2026 e il suo ultimo commit è «Rassegna delle firme pubbliche prima che si congelino»: le API si stanno stabilizzando proprio ora. Prima di fissare il commit del sottomodulo:
- il suo README è indietro rispetto al codice. Dichiara
vs-measure«da scrivere» quando invece esiste, con circa 4.865 righe e tredici file di test, e dichiaravs-dxfnon avviabile quando l'import dicommon.calibrationnon c'è più. Va riletto lo stato reale, non quello documentato. - il test di calibrazione dato per rosso — la focale recuperata a 373 contro 456 attesi, con tolleranza al 5% — va verificato sullo stato attuale. Il README di VisionSuite avverte di guardarlo prima di costruirci sopra misure in millimetri, che è esattamente ciò che questo lavoro fa. Le calibrazioni sono state toccate il 13/08 e lo stato potrebbe essere cambiato.
- il peso dell'ambiente.
vs-dxfdipende da PyTorch e l'installazione completa arriva a circa 5 GB. È la ragione per cui il worker è un container a parte, e va confermato che il solo sottoinsieme necessario pesi meno.