# 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](ssh://git@git.tielogic.xyz:222/Adriano/visionsuite.git), 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: ```python 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_json` esiste, ma nessun > percorso la scrive — nemmeno `preview`, 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: «`preview` può promuovere le proprie uscite a > `expected_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. ```toml [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 dichiara `vs-dxf` non avviabile quando l'import di `common.calibration` non 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-dxf` dipende 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.