245975791f
Otto decisioni dal brainstorming, con il motivo di ognuna: motore su entrambi i lati con versione in lockstep, agente sottile, server che non importa mai VisionSuite, device a livello PC, esecuzione decisa dalla stazione, verdetto tenuto fuori dal grafo, sorgente immagine astratta. Modello dati, API, errori e le quattro domande che restano aperte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014BBnuACZSCJqXrMYC3LUMU
400 lines
19 KiB
Markdown
400 lines
19 KiB
Markdown
# 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.
|
|
|
|
## 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.
|
|
|
|
## 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.
|