Worker di visione configurabile per stazione (vision_worker_url) #45

Closed
opened 2026-10-07 08:53:44 +00:00 by Adriano · 4 comments
Owner

COSA: il worker di visione si può far girare dove si vuole (PC di stazione o server). Il backend sceglie quale chiamare in base alla stazione. La ricetta resta sul server.
PERCHÉ: una camera su ogni PC di stazione per misura dimensionale o DXF compare, senza caricare il server e senza riscrivere il worker. Decisione di Adriano del 07/10/2026: versione minima, poco tempo per consegnare.

STATO VERIFICATO su origin/V3.0.0 a829bf3:

  • src/backend/config.py:77 vision_worker_url è un URL unico globale (env VISION_WORKER_URL, docker-compose.yml:38).
  • src/backend/services/vision_service.py:49 _call_worker(image_bytes, graph) fa POST a {settings.vision_worker_url}/run; chiamata a :213 (senza stazione) e a :380 (percorso con station_id, :300).
  • src/backend/models/orm/vision.py VisionResult ha già engine_version, executed_on (server|station), station_id, device_code: la tracciabilità c'è.
  • src/backend/models/orm/station.py:19 Station: nessun campo per il worker.

DA FARE:

  1. Migrazione 016: colonna stations.vision_worker_url String(500), nullable. Vuota = worker centrale di settings.vision_worker_url.
  2. _call_worker riceve l'URL (o la stazione) invece di leggere solo settings. Il chiamante con station_id usa l'URL della stazione se c'è.
  3. executed_on = station quando ha risposto il worker della stazione, server altrimenti. Si salva anche l'URL usato, in una colonna vision_results.worker_url nullable (stessa migrazione).
  4. Il campo si modifica dall'admin nella schermata Stazioni.
  5. Nessun fallback automatico al centrale se il worker di stazione non risponde: errore con nome, come oggi (test_vision_worker_transport.py). Un fallback silenzioso mescolerebbe versioni del motore.

TEST: stazione con URL → la POST va a quell'URL; stazione senza URL → va al centrale; executed_on e worker_url scritti giusti; migrazione 016 su e giù.

NON TOCCARE: src/vision_worker/ (stesso container, stesso contratto /run), src/vision/runner.py.

DOPO LA CONSEGNA (non in questa issue):

  • pull/coda/affitti/scelta per capacità: solo se il server non raggiunge il PC di stazione (il push richiede una porta aperta sul PC di stazione, in LAN);
  • worker Halcon con lo stesso contratto /run: quando c'è un cliente e la licenza runtime in Docker è chiarita (legata a MAC o dongle, PRESUNTO, da verificare con iMAGE S);
  • 16 bit (convert("L") in vision_worker, aperto da #43): solo se la camera scelta li produce.

COLLEGATA: #3 (agente di stazione e camera).
CRITERIO DI FATTO: due stazioni con URL di worker diversi misurano la stessa ricetta; ogni riga di vision_results dice quale worker ha risposto.

COSA: il worker di visione si può far girare dove si vuole (PC di stazione o server). Il backend sceglie quale chiamare in base alla stazione. La ricetta resta sul server. PERCHÉ: una camera su ogni PC di stazione per misura dimensionale o DXF compare, senza caricare il server e senza riscrivere il worker. Decisione di Adriano del 07/10/2026: versione minima, poco tempo per consegnare. STATO VERIFICATO su origin/V3.0.0 a829bf3: - src/backend/config.py:77 `vision_worker_url` è un URL unico globale (env `VISION_WORKER_URL`, docker-compose.yml:38). - src/backend/services/vision_service.py:49 `_call_worker(image_bytes, graph)` fa POST a `{settings.vision_worker_url}/run`; chiamata a :213 (senza stazione) e a :380 (percorso con `station_id`, :300). - src/backend/models/orm/vision.py `VisionResult` ha già `engine_version`, `executed_on` (server|station), `station_id`, `device_code`: la tracciabilità c'è. - src/backend/models/orm/station.py:19 `Station`: nessun campo per il worker. DA FARE: 1. Migrazione 016: colonna `stations.vision_worker_url` String(500), nullable. Vuota = worker centrale di `settings.vision_worker_url`. 2. `_call_worker` riceve l'URL (o la stazione) invece di leggere solo `settings`. Il chiamante con `station_id` usa l'URL della stazione se c'è. 3. `executed_on` = `station` quando ha risposto il worker della stazione, `server` altrimenti. Si salva anche l'URL usato, in una colonna `vision_results.worker_url` nullable (stessa migrazione). 4. Il campo si modifica dall'admin nella schermata Stazioni. 5. Nessun fallback automatico al centrale se il worker di stazione non risponde: errore con nome, come oggi (test_vision_worker_transport.py). Un fallback silenzioso mescolerebbe versioni del motore. TEST: stazione con URL → la POST va a quell'URL; stazione senza URL → va al centrale; `executed_on` e `worker_url` scritti giusti; migrazione 016 su e giù. NON TOCCARE: src/vision_worker/ (stesso container, stesso contratto /run), src/vision/runner.py. DOPO LA CONSEGNA (non in questa issue): - pull/coda/affitti/scelta per capacità: solo se il server non raggiunge il PC di stazione (il push richiede una porta aperta sul PC di stazione, in LAN); - worker Halcon con lo stesso contratto /run: quando c'è un cliente e la licenza runtime in Docker è chiarita (legata a MAC o dongle, PRESUNTO, da verificare con iMAGE S); - 16 bit (`convert("L")` in vision_worker, aperto da #43): solo se la camera scelta li produce. COLLEGATA: #3 (agente di stazione e camera). CRITERIO DI FATTO: due stazioni con URL di worker diversi misurano la stessa ricetta; ogni riga di vision_results dice quale worker ha risposto.
Adriano added the sviluppo label 2026-10-07 08:53:44 +00:00
Author
Owner

ESITO: richiesta
COSA: numerazione migrazioni riassegnata il 07/10: 016 = #46, 017 = #59 (user_pin), 018 = #53/#54/#55 (task loop e task_results). La migrazione di questa issue diventa 019 (down_revision = la testa al momento del lavoro).
PROSSIMO: controllare alembic heads prima di scrivere la migrazione.

ESITO: richiesta COSA: numerazione migrazioni riassegnata il 07/10: 016 = #46, 017 = #59 (user_pin), 018 = #53/#54/#55 (task loop e task_results). La migrazione di questa issue diventa 019 (down_revision = la testa al momento del lavoro). PROSSIMO: controllare `alembic heads` prima di scrivere la migrazione.
Adriano added the assegnata:daitarn label 2026-10-11 17:35:03 +00:00
Author
Owner

🔵 daitarn — preso in carico 2026-10-11T17:35:02Z
ramo: lavoro/daitarn · capacità: build-windows, gpu

🔵 daitarn — **preso in carico** 2026-10-11T17:35:02Z ramo: `lavoro/daitarn` · capacità: build-windows, gpu
Author
Owner

ESITO: richiesta
RIF: gitea:Adriano/TieMeasureFlow#45
COSA: analisi (passo 1 di 3) sul ramo lavoro/daitarn-45 da origin/V3.0.0 9306fbf. Stazione presa dalla sessione di produzione (production_run_id → ProductionRun.station_id), non da un nuovo parametro.
VERIFICATO: migrazioni in testa → 018_task_loop_results (down 017_user_pin); la nuova è 019.
VERIFICATO: execute_task (src/backend/services/vision_service.py:321) non ha station_id; riceve production_run_id; VisionResult oggi scritto con executed_on="server" e station_id assente (:435-444).
VERIFICATO: i fake di _call_worker nei test accettano **kwargs → un kwarg nuovo non li rompe.

MODIFICHE:

  1. src/backend/migrations/versions/019_station_vision_worker.py — revision '019_station_vision_worker', down_revision '018_task_loop_results'. upgrade: stations.vision_worker_url String(500) nullable; vision_results.worker_url String(500) nullable. downgrade: drop di entrambe (batch_alter_table se lo usano le migrazioni vicine, per SQLite).
  2. src/backend/models/orm/station.py Station.vision_worker_url: Mapped[Optional[str]] String(500). src/backend/models/orm/vision.py VisionResult.worker_url String(500) nullable.
  3. src/backend/models/api/station.py — vision_worker_url: Optional[str] (max_length 500) in StationCreate, StationUpdate, StationResponse. Validatore: stringa vuota/spazi → None; altrimenti deve iniziare con http:// o https:// (422 se no); rstrip("/"). src/backend/services/station_service.py create_station passa il campo (update già generico).
  4. vision_service._call_worker(..., worker_url: str | None = None): base = (worker_url or settings.vision_worker_url).rstrip("/"), POST a {base}/run. Nei detail di 502/504 aggiungere in coda ({base}) per dire quale worker non risponde; gli attesi esistenti di src/backend/tests/test_vision_worker_transport.py non si cambiano. Nessun fallback al centrale.
  5. vision_service.execute_task: se production_run_id c'è, leggere la ProductionRun (e la sua Station); station_url = station.vision_worker_url or None. Passare worker_url=station_url a _call_worker. VisionResult: executed_on = "station" if station_url else "server", station_id = run.station_id (se run), worker_url = station_url or settings.vision_worker_url (URL effettivamente chiamato, senza "/" finale). Run inesistente: comportamento attuale (non si aggiunge una 404 nuova se oggi non c'è; se la run è già validata altrove riusare quella lettura).
  6. Frontend admin: src/frontend/flask_app/templates/admin/stations.html — campo «Worker visione (URL)» nel form crea/modifica, vuoto = worker centrale; il valore va nel JSON già proxato da blueprints/admin.py (api_create_station, api_update_station: verificare che passino i campi così come sono). Stringhe nuove con _(), voci in translations/it e en (.po) e compilate come fa il repo.

NON TOCCARE: src/vision_worker/, src/vision/runner.py, vendor/visionsuite, preview e validate_recipe (il Maker compone sul worker centrale), docker-compose.yml.

TEST:

  • src/backend/tests/test_vision_station_worker.py nuovo: stazione con URL + run aperta → _call_worker riceve worker_url della stazione, VisionResult executed_on=station, worker_url=URL stazione, station_id=stazione; stazione senza URL → worker_url None in chiamata, executed_on=server, worker_url=settings.vision_worker_url; senza run → server.
  • _call_worker con worker_url: httpx finto (stesso stile di test_vision_worker_transport.py) → POST a <url>/run; con ConnectError → 502 col nome dell'URL, una sola POST (nessun fallback).
  • API stazioni: create/update con URL valido, con "" → null, con "ftp://x" → 422; response espone il campo.
  • migrazione 019 su e giù nello stile di src/backend/tests/test_migrations.py.
  • frontend: test del form se src/frontend/flask_app/tests ha già test della pagina stazioni.
  • Suite: uv run --extra server --extra client --extra dev pytest src/backend/tests src/frontend/flask_app/tests -q -p no:cacheprovider verde.

CRITERIO DI FATTO: come nel corpo dell'issue.

ESITO: richiesta RIF: gitea:Adriano/TieMeasureFlow#45 COSA: analisi (passo 1 di 3) sul ramo lavoro/daitarn-45 da origin/V3.0.0 9306fbf. Stazione presa dalla sessione di produzione (`production_run_id` → `ProductionRun.station_id`), non da un nuovo parametro. VERIFICATO: migrazioni in testa → 018_task_loop_results (down 017_user_pin); la nuova è 019. VERIFICATO: `execute_task` (src/backend/services/vision_service.py:321) non ha `station_id`; riceve `production_run_id`; `VisionResult` oggi scritto con executed_on="server" e station_id assente (:435-444). VERIFICATO: i fake di `_call_worker` nei test accettano `**kwargs` → un kwarg nuovo non li rompe. MODIFICHE: 1. src/backend/migrations/versions/019_station_vision_worker.py — revision '019_station_vision_worker', down_revision '018_task_loop_results'. upgrade: `stations.vision_worker_url` String(500) nullable; `vision_results.worker_url` String(500) nullable. downgrade: drop di entrambe (batch_alter_table se lo usano le migrazioni vicine, per SQLite). 2. src/backend/models/orm/station.py `Station.vision_worker_url: Mapped[Optional[str]]` String(500). src/backend/models/orm/vision.py `VisionResult.worker_url` String(500) nullable. 3. src/backend/models/api/station.py — `vision_worker_url: Optional[str]` (max_length 500) in StationCreate, StationUpdate, StationResponse. Validatore: stringa vuota/spazi → None; altrimenti deve iniziare con http:// o https:// (422 se no); `rstrip("/")`. src/backend/services/station_service.py create_station passa il campo (update già generico). 4. vision_service.`_call_worker(..., worker_url: str | None = None)`: `base = (worker_url or settings.vision_worker_url).rstrip("/")`, POST a `{base}/run`. Nei detail di 502/504 aggiungere in coda ` ({base})` per dire quale worker non risponde; gli attesi esistenti di src/backend/tests/test_vision_worker_transport.py non si cambiano. Nessun fallback al centrale. 5. vision_service.`execute_task`: se `production_run_id` c'è, leggere la ProductionRun (e la sua Station); `station_url = station.vision_worker_url or None`. Passare `worker_url=station_url` a `_call_worker`. VisionResult: `executed_on = "station" if station_url else "server"`, `station_id = run.station_id` (se run), `worker_url = station_url or settings.vision_worker_url` (URL effettivamente chiamato, senza "/" finale). Run inesistente: comportamento attuale (non si aggiunge una 404 nuova se oggi non c'è; se la run è già validata altrove riusare quella lettura). 6. Frontend admin: src/frontend/flask_app/templates/admin/stations.html — campo «Worker visione (URL)» nel form crea/modifica, vuoto = worker centrale; il valore va nel JSON già proxato da blueprints/admin.py (api_create_station, api_update_station: verificare che passino i campi così come sono). Stringhe nuove con `_()`, voci in translations/it e en (.po) e compilate come fa il repo. NON TOCCARE: src/vision_worker/, src/vision/runner.py, vendor/visionsuite, `preview` e `validate_recipe` (il Maker compone sul worker centrale), docker-compose.yml. TEST: - src/backend/tests/test_vision_station_worker.py nuovo: stazione con URL + run aperta → `_call_worker` riceve worker_url della stazione, VisionResult executed_on=station, worker_url=URL stazione, station_id=stazione; stazione senza URL → worker_url None in chiamata, executed_on=server, worker_url=settings.vision_worker_url; senza run → server. - `_call_worker` con worker_url: httpx finto (stesso stile di test_vision_worker_transport.py) → POST a `<url>/run`; con ConnectError → 502 col nome dell'URL, una sola POST (nessun fallback). - API stazioni: create/update con URL valido, con "" → null, con "ftp://x" → 422; response espone il campo. - migrazione 019 su e giù nello stile di src/backend/tests/test_migrations.py. - frontend: test del form se src/frontend/flask_app/tests ha già test della pagina stazioni. - Suite: `uv run --extra server --extra client --extra dev pytest src/backend/tests src/frontend/flask_app/tests -q -p no:cacheprovider` verde. CRITERIO DI FATTO: come nel corpo dell'issue.
Author
Owner

Soluzione

ESITO: fatto
RIF: gitea:Adriano/TieMeasureFlow#45
COSA: worker di visione per stazione. La stazione si ricava dalla sessione di produzione (production_run_id → ProductionRun.station_id). Se la stazione ha un URL si chiama quello, altrimenti il centrale settings.vision_worker_url. Nessun ripiego.
FILE: src/backend/migrations/versions/019_station_vision_worker.py — stations.vision_worker_url e vision_results.worker_url, String(500) nullable, down 018_task_loop_results
FILE: src/backend/models/orm/station.py, models/orm/vision.py — campi nuovi
FILE: src/backend/models/api/station.py — campo in Create, Update e Response; vuoto → null; deve iniziare con http(s), altrimenti 422; tolta la / finale
FILE: src/backend/services/station_service.py — create_station passa il campo
FILE: src/backend/services/vision_service.py — _call_worker(worker_url=...), l'URL compare nel detail di 502 e 504; execute_task scrive executed_on station|server, station_id e worker_url effettivo
FILE: src/frontend/flask_app/templates/admin/stations.html, translations it/en — campo «Worker visione (URL)»
COMMIT: 95d643d 275bbd1 40eed5c 631ba4e, merge ac8604e su V3.0.0
PROVA: suite goldrake su lavoro/daitarn-45 (base 9306fbf = base del merge) → 848 passed 2 skipped (pytest 3.11 backend+frontend), 29 passed (pytest 3.13 vision), docker build vision ok. Test nuovi: src/backend/tests/test_vision_station_worker.py (6), test_stations_api.py (+3), test_migrations.py (+1 per la 019 su e giù), frontend test_admin_stations.py (+3)
MANCA: preview e validate_recipe del Maker restano sul worker centrale (voluto). Migrazione 019 da applicare al deploy (alembic upgrade head). Criterio «due stazioni misurano la stessa ricetta» provato con un worker finto, non con due worker veri. Trappola: con i .mo compilati in locale (pybabel compile) 5 test frontend esistenti falliscono perché asseriscono stringhe in italiano; non dipende da questa issue.

**Soluzione** ESITO: fatto RIF: gitea:Adriano/TieMeasureFlow#45 COSA: worker di visione per stazione. La stazione si ricava dalla sessione di produzione (production_run_id → ProductionRun.station_id). Se la stazione ha un URL si chiama quello, altrimenti il centrale settings.vision_worker_url. Nessun ripiego. FILE: src/backend/migrations/versions/019_station_vision_worker.py — stations.vision_worker_url e vision_results.worker_url, String(500) nullable, down 018_task_loop_results FILE: src/backend/models/orm/station.py, models/orm/vision.py — campi nuovi FILE: src/backend/models/api/station.py — campo in Create, Update e Response; vuoto → null; deve iniziare con http(s), altrimenti 422; tolta la / finale FILE: src/backend/services/station_service.py — create_station passa il campo FILE: src/backend/services/vision_service.py — _call_worker(worker_url=...), l'URL compare nel detail di 502 e 504; execute_task scrive executed_on station|server, station_id e worker_url effettivo FILE: src/frontend/flask_app/templates/admin/stations.html, translations it/en — campo «Worker visione (URL)» COMMIT: 95d643d 275bbd1 40eed5c 631ba4e, merge ac8604e su V3.0.0 PROVA: suite goldrake su lavoro/daitarn-45 (base 9306fbf = base del merge) → 848 passed 2 skipped (pytest 3.11 backend+frontend), 29 passed (pytest 3.13 vision), docker build vision ok. Test nuovi: src/backend/tests/test_vision_station_worker.py (6), test_stations_api.py (+3), test_migrations.py (+1 per la 019 su e giù), frontend test_admin_stations.py (+3) MANCA: preview e validate_recipe del Maker restano sul worker centrale (voluto). Migrazione 019 da applicare al deploy (alembic upgrade head). Criterio «due stazioni misurano la stessa ricetta» provato con un worker finto, non con due worker veri. Trappola: con i .mo compilati in locale (pybabel compile) 5 test frontend esistenti falliscono perché asseriscono stringhe in italiano; non dipende da questa issue.
Adriano removed the assegnata:daitarn label 2026-10-11 18:04:09 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Adriano/TieMeasureFlow#45