Progetti analizzati: Sistec.5315 (LAG) — HMI Pressopiegatrice / Cella di Produzione
+ Sistec.5309 (FAEL) — HMI Cella di Produzione
Data: Luglio 2026
Scopo: Analisi trasversale dell'architettura Sistec HMI, identificazione degli antipattern comuni, e proposta di un'architettura modulare unificata applicabile a qualsiasi commessa
Le codebase 5315_LAG e 5309_FAEL condividono lo stesso software di base HMI per celle di produzione Sistec. Ogni commessa (LAG, FAEL, future) presenta un layout impianto diverso con macchinari differenti, ma il fulcro applicativo — interfaccia PLC, gestione produzione, robot KUKA, presse, tracciamento job — è comune.
Le commesse esistenti (LAG, FAEL) non vengono refattorizzate. L'architettura target descritta in questo documento è il blueprint per la prossima commessa, da sviluppare ex-novo seguendo i principi di stack verticali, NuGet, DI, Layout Engine e Avalonia.
| Codice esistente | Nuova commessa |
|---|---|
| Manutenzione correttiva soltanto | Sviluppo greenfield |
| Nessun refactoring strutturale | Architettura a stack verticali |
| WinForms legacy | Avalonia cross-platform |
| God Project Common | NuGet packages verticali |
| Statico/globale | DI Container |
| Password in chiaro | BCrypt + badge RFID |
| Dimensione | 5315 LAG | 5309 FAEL |
|---|---|---|
Progetti .csproj |
14 | 53 |
Soluzioni .sln |
6 | 22 |
| File C# totali | ~684 | ~800 (solo main) + 35+ test |
| Varianti HMI | 1 (monolitica) | 3 (AB, C, BS) |
| Pressa | Safan (TCP) | ESA (Modbus) |
| Robot | KUKA (in Common) | KUKA (libreria separata) |
| CNC | Sinumerik / ONE | Assente |
| Bus messaggi | Assente | Zebus (Sistec.Bus) |
| Test automatici | NUnit 4.3.2 (net10.0) | Prevalentemente manuali/UI |
| God Project | Sistec.5315/Common |
Sistec.HMI/Common |
| God Class | MainForm (19 partial) |
FrmHMI (~5.156 righe AB) |
flowchart TB
subgraph Foundation["LIBRERIE FONDAZIONE (identiche)"]
Core["Sistec.Core (netstandard2.1)
Modelli · Interfacce · Enum · Utils
215-219 file"]
Controls["Sistec.Controls (net8.0-windows)
Bottoni, LED, Motori, Valvole
Controlli generici WinForms"]
UI["Sistec.UI (net8.0-windows)
Layout pagine, dialoghi base
44 file"]
OpcUa["Sistec.Opc.Ua (netstandard2.1)
Wrapper OPC UA Foundation SDK
19 file"]
end
subgraph LAG_Only["SOLO 5315 LAG"]
Safan["Sistec.Safan (netstandard2.1)
Client TCP pressa Safan
Test NUnit + Simulator"]
Sinumerik["Sistec.Sinumerik (netstandard2.1)
Adattatore CNC Siemens ONE
LagNx1525 + Simulazione"]
end
subgraph FAEL_Only["SOLO 5309 FAEL"]
KukaClient["Kuka.Client (netstandard2.1)
Client TCP KRC robot
11 file"]
EsaClient["Esa.Client (netstandard2.1)
Client Modbus pressa ESA
6 file"]
EasyModbus["EasyModbus (netstandard2.1)
Stack Modbus TCP puro"]
Bus["Sistec.Bus (net8.0-windows)
Zebus messaging pub/sub"]
end
Foundation --> LAG_Only
Foundation --> FAEL_Only
Sistec.HMI/Common / Sistec.5315/Commonflowchart LR
subgraph Common["⚠️ PROGETTO COMUNE (God Project)"]
CL["Logic/
Business logic"]
CC["Controls/
User controls applicativi"]
CDB["DB/
Repository implementazioni"]
CDTO["DTO/
Data Transfer Objects"]
CDUT["DUT/
Data Unit Type (OPC UA)"]
CModel["Model/
Modelli dominio applicativi"]
CCells["Cells/
Viste cella"]
CDialogs["Dialogs/
Finestre dialogo"]
CLayout["Layout/
Pagine layout"]
CFrmHMI["FrmHMI/
LogicFactory, LogicCollection"]
end
Common -->|LAG| LAG_Refs["Sistec.Controls, Core, OpcUa,
Safan, Sinumerik"]
Common -->|FAEL| FAEL_Refs["Kuka.Client, Bus, Controls,
Core, OpcUa"]
Metriche comuni:
| Metrica | LAG | FAEL |
|---|---|---|
| Directory in Common | 15+ | 12+ |
| File C# in Common | ~270 | ~270 |
| Progetti referenziati da Common | 5 | 5 |
| RootNamespace incoerente | Sistec (uguale a HMI) |
Sistec.Common |
flowchart LR
subgraph LAG["5315 LAG"]
MF["MainForm
19 partial file
Orchestrazione centrale:
PLC, KUKA, Safan,
Sinumerik, Watchdog,
Job, pagine, popup"]
end
subgraph FAEL["5309 FAEL"]
FH["FrmHMI (AB)
24 partial file, ~5.156 righe
Orchestrazione centrale:
PLC, KUKA, Modbus,
ESA, DB, Watchdog,
Produzione, Manutenzione"]
FHC["FrmHMI (C)
17 partial file, ~2.499 righe
Stesso pattern,
copia ridotta"]
end
LAG -->|Stesso anti-pattern| FAEL
In entrambi i casi: il costruttore del Form principale inizializza manualmente TUTTO (OPC UA, tag PLC, logics, watchdog, eventi) senza DI container.
Il robot KUKA è l'esempio perfetto di bassa coesione in entrambe le codebase:
flowchart TB
subgraph Legend["Legenda"]
LAG_STYLE["LAG: in Sistec.Core/Interfaces + Sistec.5315/Common/Logic"]
FAEL_STYLE["FAEL: in Kuka.Client separato + Sistec.HMI/Common/Logic"]
end
subgraph Scattered["KUKA — FRAMMENTATO"]
Interfaces["Interfacce
LAG: Sistec.Core/Interfaces/IRobotLogic
FAEL: (mancante, uso diretto)"]
Client["Client TCP KRC
LAG: assente (mescolato)
FAEL: Kuka.Client ✅"]
Logic["Logica Robot
LAG: Common/Logic/RobotLogicKuka
FAEL: Common/Logic (misto)"]
U1["UI Controls
LAG: Sistec.Controls/Robots +
Common/Controls/RobotCommands"]
U2["FAEL: AB/Robots/ +
AB/FrmHMI/FrmHMI.Robot.cs"]
Config["Config
LAG: HMI/Config/Kuka.ini
FAEL: AB/Config/ + Common"]
end
Interfaces --> Logic
Client --> Logic
Logic --> U1
Logic --> U2
Logic --> Config
Per capire KUKA servono 5 progetti diversi in entrambe le codebase.
| # | Anti-Pattern | LAG | FAEL |
|---|---|---|---|
| 1 | God Project | Sistec.5315/Common (21 dir) |
Sistec.HMI/Common (12+ dir) |
| 2 | God Class | MainForm (19 partial) |
FrmHMI (24 partial, ~5.156 righe) |
| 3 | No DI Container | Tutto new, .Use() fluente |
Tutto new, LogicCollection come service locator |
| 4 | Service Locator | Configuration.PlcConfig[...] statico |
ObjectUsageMonitor.Instance, LogicCollection |
| 5 | Concrete Coupling | CellLogic(SafanPressBrakeLogic, ...) |
FrmHMI referenzia OpcUaClient, KrcClient direttamente |
| 6 | Business Logic in UI | MainForm.cs:90-368 costruttore |
FrmHMI.cs costruttore ~490 righe |
| 7 | Technology Lock-In | DUT OPC UA EncodeableBase |
DUT OPC UA EncodeableBase |
| 8 | DB Access Sparso | Repository in Core + Common | Repository in Core + Common |
| 9 | Magic Strings | Tag name hardcoded | Tag name hardcoded, Shared.cs ha 5 costanti |
| 10 | Lava Flow | Contracts\** escluso da compilazione |
FrmHMI.Substations.cs ~80% commentato |
| 11 | Shotgun Surgery | Aggiungere device: modifiche a N file | Aggiungere device: modifiche a N file |
| 12 | SafeInvoke | Pattern Control.SafeInvoke |
Pattern Control.SafeInvoke in Bus.cs, KrcClientCollection |
| 13 | Namespace Incoerente | RootNamespace = Sistec in HMI e Common |
DUT in namespace Sistec.DUT invece di Sistec.Common.DUT |
La libreria Sistec.Opc.Ua è il wrapper attorno all'OPC Foundation SDK (v1.5.376.244) usato da entrambe le commesse per tutta la comunicazione col PLC. È condivisa (stesso codice base) ma ha divergenze tra LAG e FAEL.
| Metrica | LAG | FAEL |
|---|---|---|
File .cs |
19 | 19 |
| Linee totali | 2.445 | 2.170 |
OpcUaClient (god class) |
769 righe | 757 righe |
OpcUaTagValue<T> |
422 righe | 199 righe |
UAClient (wrapper SDK) |
411 righe | 411 righe |
ClientAction enum |
Renew, Shutdown |
None, Shutdown |
Implementa IDisposable? |
❌ No | ❌ No |
| Classi / Interfacce | 14 / 3 | 14 / 3 |
| Dipendenze NuGet | OPC Foundation + Serilog + MEL 10.x | identico |
| Target framework | netstandard2.1 |
netstandard2.1 |
Giudizio sintetico: Stabile in funzionamento nominale, fragile in scenari di recovery, riavvio e carico.
| # | Problema | Dove | Impatto |
|---|---|---|---|
| 1 | OpcUaClient non IDisposable |
OpcUaClient.cs (intera classe) |
Possiede UAClient (IDisposable) + CancellationTokenSource. Risorse rilasciate solo dentro _StartClient che può non essere chiamato o terminare anormalmente. |
| 2 | Eccezioni tutte inghiottite | OpcUaClient.cs:226-229 |
_StartClient cattura Exception ex e fa solo debug log. Il loop di connessione non segnala mai fallimenti irreversibili. |
| 3 | Busy-wait 100% CPU | OpcUaClientCollection.cs:57-60 |
while (opcUaClient.IsConnected) { } — zero attesa, brucia un core. |
| 4 | Event handler lambda leak | OpcUaClient.cs:174-183 |
Disconnected/Reconnected con lambda mai unsubscribed. Ogni ciclo di connessione crea nuove lambda; il vecchio UAClient non viene GC'd. |
| 5 | WRITE_TIMEOUT statico mutabile |
OpcUaClient.cs:31 |
public static int — chiunque può cambiarlo, affetta tutte le istanze globalmente. |
| 6 | Renew path dead code (solo LAG) | OpcUaClient.cs:755-759 |
ClientAction.Renew mai assegnato. ResubscribeForwardingAsync e tutto il ramo renew sono irraggiungibili. |
| 7 | 17/26 await senza ConfigureAwait(false) | OpcUaClient.cs (sparso) |
Rischio deadlock se usato da contesto synchronized (UI thread). |
| 8 | HashSet<Subscription> non thread-safe |
OpcUaClient.cs:26 |
_pendingSubscriptions accesso da keepalive e registrazione in concorrenza — corruzione dati possibile. |
| # | Problema | Dove | Impatto |
|---|---|---|---|
| 9 | Overflow bug in waitTime | OpcUaClient.cs:123 |
int.MaxValue - elapsed wrappa a negativo quando timeout è default. |
| 10 | Fire-and-forget in StartClient | OpcUaClient.cs:323-334 |
Task.Run(async () => ...) senza gestione eccezioni → UnobservedTaskException. |
| 11 | Dati hardcoded sovrascrivono config | OpcUaClient.cs:135-172 |
SessionLifeTime=120s, ReconnectPeriod=5s, KeepAliveInterval=5s ignorano file XML. |
| 12 | UseWindowsForms=true inutile |
.csproj:8 |
Flag presente ma nessun controllo WinForms usato. Artefatto. |
| 13 | God class + god method | OpcUaClient (769 righe) + _StartClient (~170 righe) |
SRP violato, impossibile testare isolatamente. |
| 14 | GlobalContext.MaxArrayLength modificato |
Converter.FromBus.cs:86 |
Stato globale mutato da una libreria, side-effect su tutta l'app. |
| Aspetto | LAG | FAEL | Migliore |
|---|---|---|---|
SamplingInterval |
1 (fastest possible) |
-1 (usa PublishingInterval) |
FAEL — meno pressione sul PLC |
QueueSize |
10 |
2 |
FAEL — sufficiente con DiscardOldest |
LifetimeCount / MinLifetimeInterval |
0 / 120_000 |
15 / 60_000 |
FAEL — rispetta OPC UA Part 4 §5.13 |
StackOverflow fix in RegisterTags |
❌ Nessuno | ✅ Skip se session disconnessa | FAEL |
Null-safety ReadNodeAsync |
❌ NRE possibile | ✅ Task.FromResult(default) |
FAEL |
[Obsolete] su Subscribe |
❌ No | ✅ Sì | FAEL |
OpcUaTagValue<T> |
422 righe, Monitor.TryEnter + sync-over-async |
199 righe, snello | FAEL |
Conclusione: FAEL ha ricevuto bugfix sostanziali che LAG non ha. La versione FAEL è la baseline consigliata per il nuovo Sistec.Stack.PLC.
Attenzione — principio chiave: Sistec.Library.OpcUa è una libreria orizzontale, NON una libreria verticale del solo PLC. Può essere usata da PIÙ stack (PLC, Sinumerik, e potenzialmente robot KUKA se via OPC UA). Non va duplicata. Lo stesso vale per Sistec.Library.Modbus e Sistec.Library.Tcp.
Nell'architettura a stack verticali (§2), Sistec.Opc.Ua si decompone in:
| Progetto | Tipo | Ruolo | Ereditato da |
|---|---|---|---|
Sistec.Library.OpcUa |
Orizzontale (condivisa) | UAClient, session management, certificati, subscription lifecycle — protocollo OPC UA puro, zero logica di dominio |
UAClient.cs (buono), OpcUaClient.cs (solo parte connection/session) |
Sistec.Stack.PLC.Client |
Verticale (solo PLC) | OpcUaClientCollection, strategia di registrazione tag PLC, autodiscovery — costruito SOPRA Sistec.Library.OpcUa |
OpcUaClientCollection.cs (da rifare senza busy-wait) |
Sistec.Stack.PLC.Driver |
Verticale | IPlcTagProvider, OpcUaTagFactory, tag DUT (da codegen) |
OpcUaTagValue.cs (FAEL), OpcUaTagFactory.cs |
Sistec.Stack.PLC.Services |
Verticale | PlcConnectionService, WatchdogService, ModeService |
Da scrivere |
Sistec.Stack.PLC.UI |
Verticale | PlcStatusControl, WatchdogIndicator |
Da scrivere |
Sistec.Stack.PLC.Simulator |
Verticale | FakeOpcUa unico (non 13 copie) |
Da scrivere |
Sistec.Stack.Sinumerik.Client |
Verticale (solo CNC) | Wrapper OPC UA per CNC Siemens ONE — costruito SOPRA Sistec.Library.OpcUa |
Da scrivere |
Regola: Se un domani KUKA dovesse supportare OPC UA, Sistec.Stack.Kuka.Client dipenderà da Sistec.Library.OpcUa — senza duplicare nulla.
La logica di connessione, certificati, riconnessione va mantenuta ma con:
IDisposable pattern standardCancellationToken canonicamente usato (non Task.Delay(Infinite))SemaphoreSlim o ConcurrentQueue per _pendingSubscriptionsReconnectionPolicy configurabile via IOptions<T>| Principio | Applicazione |
|---|---|
| Vertical Slicing | Ogni macchina/dispositivo è uno stack autonomo (Client → Driver → Services → UI → Simulator) |
| Dependency Inversion | Tutto dipende da interfacce, mai da classi concrete |
| DI Container | Microsoft.Extensions.DependencyInjection per composizione e lifetime |
| Sep. Responsabilità | Ogni progetto ha UN solo scopo. 5 layer per stack |
| Coesione | Tutto KUKA in Sistec.Stack.Kuka.* |
| Anti-Corruption Layer | Ogni stack traduce dal protocollo nativo al dominio |
| Configurabilità | IOptions<T> invece di Configuration statica |
| Code Generation | DUT generati da definizioni CODESYS |
| Hexagonal Architecture (Ports & Adapters) | Il Core (Sistec.Core) non importa mai framework, DB, HTTP o librerie di terze parti. Gli Adapter (Client TCP, Persistence Dapper, Simulator) sono intercambiabili senza toccare il Core. Il Core dipende solo da primitive del linguaggio e dalle interfacce che definisce (Ports). |
| Test Standard | NUnit per ogni stack + Simulator per test di integrazione |
Ogni dispositivo/dominio diventa uno stack verticale con 5 layer:
flowchart TB
subgraph Legend["LEGENDA RELAZIONI"]
DEP_DOTTED["● - - -> Dipende da libreria
orizzontale (condivisa)"]
DEP_SOLID["● ──→ Dipende da layer interno
dello stesso stack"]
end
subgraph Stack["Sistec..Stack (es. Sistec.Stack.Kuka)"]
direction TB
UI["Sistec..Stack.UI
UserControls, Pages
WinForms view layer"]
SVC["Sistec..Stack.Services
Orchestrators, Use Cases
Logica applicativa"]
DRV["Sistec..Stack.Driver
ITagProvider, TagValue<T>
Astrazione device → dominio"]
CLI["Sistec..Stack.Client
Logica client DEVICE-SPECIFICA
(usa libreria orizzontale
per il protocollo)"]
SIM["Sistec..Stack.Simulator
Server mock / device finto
Test e sviluppo offline"]
end
subgraph Horizontal["LIBRERIE ORIZZONTALI
(condivise, MAI duplicate)"]
TcpLib["Sistec.Library.Tcp
Socket pool, reconnect,
message framing"]
ModbusLib["Sistec.Library.Modbus
Client/Server Modbus TCP"]
OpcUaLib["Sistec.Library.OpcUa
UAClient, Session,
MonitoredItem"]
end
UI --> SVC
SVC --> DRV
DRV --> CLI
SIM --> CLI
CLI -.-o TcpLib
CLI -.-o ModbusLib
CLI -.-o OpcUaLib
style Horizontal fill:#e0f7fa,stroke:#006064,stroke-dasharray: 5 5
NB: Il layer Client di ogni stack NON reimplementa il protocollo — usa le librerie orizzontali Sistec.Library.Tcp, Sistec.Library.Modbus, Sistec.Library.OpcUa. Queste sono un'istanza unica, condivisa tra tutti gli stack che ne hanno bisogno (es. Sistec.Library.OpcUa è usata da PLC e Sinumerik, e opzionalmente da KUKA).
Sistec.Library.Redis è la libreria trasversale per stato condiviso veloce — usata da tutti gli stack che necessitano hot data (sessioni, allarmi, pallet, contatori, metriche). Opzionale: se Redis non è configurato, i servizi degradano graceful su MySQL (§19.13).
flowchart TB
subgraph Fondazione["FONDAZIONE"]
Core["Sistec.Core
Modelli dominio, Interfacce pure
Enum, Eccezioni, Eventi
netstandard2.1"]
Controls["Sistec.Controls
Controlli WinForms generici
Led, Button, Container, Motors, Valves
Nessuna logica di dominio"]
SistecUI["Sistec.UI
Layout pagine, menu,
alarm banner, user management"]
end
subgraph Comunicazione["LIBRERIE ORIZZONTALI (condivise, MAI duplicate per stack)"]
TcpLib["Sistec.Library.Tcp
Socket pool, reconnect
message framing
(usata da KUKA, Safan)"]
OpcUaLib["Sistec.Library.OpcUa
UAClient, Session,
MonitoredItem
(usata da PLC, Sinumerik,
opzionalmente KUKA)"]
ModbusLib["Sistec.Library.Modbus
EasyModbus puro
Client/Server Modbus TCP
(usata da ESA)"]
BusLib["Sistec.Library.Bus
Zebus pub/sub o HTTP+Redis
Comunicazione inter-pannello"]
RedisLib["Sistec.Library.Redis
Hot data layer trasversale
Sessioni, Allarmi, Pallet,
Traduzioni, Metriche
(opzionale)"]
end
subgraph Macchine["STACK VERTICALI — MACCHINE"]
Kuka["Sistec.Stack.Kuka
Robot KUKA KRC
Client · Driver · Services · UI · Simulator"]
Safan["Sistec.Stack.Safan
Pressa Safan (TCP)
Client · Driver · Services · UI · Simulator"]
Esa["Sistec.Stack.Esa
Pressa ESA (Modbus)
Client · Driver · Services · UI · Simulator"]
PlcInt["Sistec.Stack.PLC
Interfaccia PLC (OPC UA)
Client · Driver · Services · UI · Simulator"]
Sinumerik["Sistec.Stack.Sinumerik
CNC Siemens ONE
Client · Driver · Services · UI · Simulator"]
end
subgraph Applicazione["STACK VERTICALI — APPLICAZIONE"]
Production["Sistec.Stack.Production
Orchestrazione produzione
Services + UI"]
JobMgmt["Sistec.Stack.JobManagement
Job lifecycle, tracking
Services + UI"]
Maintenance["Sistec.Stack.Maintenance
Manutenzione, safety
Services + UI"]
Alarms["Sistec.Stack.Alarms
Alarm journal, notifiche
Services + UI"]
end
subgraph Persistence["PERSISTENZA (multi-progetto)"]
PersAbst["Sistec.Infra.Persistence
Interfacce repository,
Unit of Work contract"]
PersDapper["Sistec.Infra.Persistence.Dapper
Implementazione Dapper"]
PersMySql["Sistec.Infra.Persistence.MySql
MySqlConnector"]
PersSqlSrv["Sistec.Infra.Persistence.SqlServer
Microsoft.Data.SqlClient"]
PersUoW["Sistec.Infra.Persistence.UnitOfWork
Transaction scope"]
end
subgraph Config["CONFIGURAZIONE"]
ConfigLib["Sistec.Configuration
Options pattern, INI/XML reader
IOptions<T>"]
end
subgraph CodeGen["CODE GENERATION"]
DUTGen["Sistec.Infra.CodeGen
dotnet tool
CODESYS → POCO + OPC UA + TagConstants"]
end
Production -->|usa| Kuka
Production -->|usa| Safan
Production -->|usa| Esa
Production -->|usa| PlcInt
Production -->|usa| Sinumerik
Production -->|usa| JobMgmt
Kuka -.->|TCP| TcpLib
Kuka -.->|opzionale OPC UA| OpcUaLib
Safan -.->|TCP| TcpLib
Esa -.->|Modbus| ModbusLib
PlcInt -.->|OPC UA| OpcUaLib
Sinumerik -.->|OPC UA| OpcUaLib
Kuka --> Core
Safan --> Core
Esa --> Core
PlcInt --> Core
Sinumerik --> Core
PersAbst --> Core
PersDapper --> PersAbst
PersMySql --> PersDapper
PersSqlSrv --> PersDapper
PersUoW --> PersDapper
ConfigLib --> Core
DUTGen --> PlcInt
classDef foundation fill:#e1f5e1,stroke:#2e7d32
classDef comm fill:#fce4ec,stroke:#c62828
classDef machine fill:#e8f5e9,stroke:#2e7d32
classDef app fill:#fff8e1,stroke:#f57f17
classDef data fill:#f3e5f5,stroke:#7b1fa2
classDef infra fill:#e0f7fa,stroke:#006064
class Core,Controls,SistecUI foundation
class TcpLib,OpcUaLib,ModbusLib,BusLib comm
class Kuka,Safan,Esa,PlcInt,Sinumerik machine
class Production,JobMgmt,Maintenance,Alarms app
class PersAbst,PersDapper,PersMySql,PersSqlSrv,PersUoW data
class ConfigLib,DUTGen infra
Le librerie orizzontali (Sistec.Library.Tcp, Sistec.Library.Modbus, Sistec.Library.OpcUa) sono un'unica istanza condivisa tra tutti gli stack. Non vanno mai replicate:
flowchart LR
subgraph Orizzontali["LIBRERIE ORIZZONTALI — UNA SOLA COPIA"]
Tcp["Sistec.Library.Tcp"]
Opc["Sistec.Library.OpcUa"]
Mod["Sistec.Library.Modbus"]
end
subgraph Stacks["STACK CHE LE USANO"]
Kuka_Client["Sistec.Stack.Kuka.Client
KrcClient, KrcConnectionPool"]
Safan_Client["Sistec.Stack.Safan.Client
SafanClient"]
Plc_Client["Sistec.Stack.PLC.Client
OpcUaClientCollection"]
Sinu_Client["Sistec.Stack.Sinumerik.Client
CNC OPC UA wrapper"]
Esa_Client["Sistec.Stack.Esa.Client
EsaModbusClient"]
end
Tcp ---> Kuka_Client
Tcp ---> Safan_Client
Opc ---> Plc_Client
Opc ---> Sinu_Client
Opc -..->|opzionale| Kuka_Client
Mod ---> Esa_Client
style Orizzontali fill:#e0f7fa,stroke:#006064,stroke-dasharray: 5 5
Cosa contengono le librerie orizzontali:
| Libreria | Contenuto | NON contiene |
|---|---|---|
Sistec.Library.Tcp |
ISocketClient, ReconnectPolicy, MessageFramer, ArrayPool<T> helper, connection pool |
Logica KRC, comandi Safan, stato robot |
Sistec.Library.Modbus |
IModbusClient, ModbusReadRequest/WriteRequest, IModbusServer (per simulatore) |
Logica pressa ESA, registri specifici ESA |
Sistec.Library.OpcUa |
UAClient, session management, certificati X.509, MonitoredItem, subscription lifecycle |
Tag PLC, DUT, logica watchdog, strategia di registrazione |
Cosa contengono i Client verticali (esempi):
| Stack | Client | Logica device-specifica |
|---|---|---|
| KUKA | Sistec.Stack.Kuka.Client |
Protocollo KRC (header/checksum/payload), polling comandi, KrcConnectionPool |
| Safan | Sistec.Stack.Safan.Client |
Protocollo Safan, comandi pressa, gestione errori Safan |
| PLC | Sistec.Stack.PLC.Client |
OpcUaClientCollection, strategia di registrazione tag, autodiscovery PLC |
| Sinumerik | Sistec.Stack.Sinumerik.Client |
Wrapper OPC UA per CNC Siemens ONE, nodi CNC specifici |
| ESA | Sistec.Stack.Esa.Client |
Mappatura registri Modbus ESA, comandi pressa ESA |
Regola pratica: se un pezzo di codice parla il protocollo (TCP framing, OPC UA session, Modbus PDU) → sta nella libreria orizzontale. Se parla con un dispositivo specifico (comando KUKA, registro ESA, nodo CNC) → sta nel Client verticale.
| Layer | Progetto | Contenuto | Dipende da |
|---|---|---|---|
| Client | Sistec.Stack.Kuka.Client |
KrcClient, KrcConnectionPool, protocollo TCP KRC |
Sistec.Library.Tcp (socket pool, reconnect) |
| Driver | Sistec.Stack.Kuka.Driver |
IKukaTagProvider, KukaTagValue<T>, mappatura tag → nomi KUKA |
Sistec.Stack.Kuka.Client, Sistec.Core |
| Services | Sistec.Stack.Kuka.Services |
KukaRobotLogic, RobotFollowService, CommandService, OverrideService |
Sistec.Stack.Kuka.Driver, Sistec.Core |
| UI | Sistec.Stack.Kuka.UI |
ucKukaInfo, KukaOverrideControl, ConnectionStatusControl |
Sistec.Stack.Kuka.Services, Sistec.Controls |
| Simulator | Sistec.Stack.Kuka.Simulator |
Server KRC falso (WinForms/Console) | Sistec.Stack.Kuka.Client |
| Layer | Progetto | Contenuto | Dipende da |
|---|---|---|---|
| Client | Sistec.Stack.Safan.Client |
SafanClient (TCP Winsock), ISafanClient |
Sistec.Library.Tcp (socket pool, reconnect) |
| Driver | Sistec.Stack.Safan.Driver |
ISafanPressBrakeLogic (migliorato), mappatura comandi Safan |
Sistec.Stack.Safan.Client, Sistec.Core |
| Services | Sistec.Stack.Safan.Services |
SafanPressBrakeLogic (business logic pura), BendingStatusService |
Sistec.Stack.Safan.Driver, Sistec.Core |
| UI | Sistec.Stack.Safan.UI |
SafanBrakeView, BendCycleMonitor |
Sistec.Stack.Safan.Services, Sistec.Controls |
| Simulator | Sistec.Stack.Safan.Simulator |
SafanPressSimulator (esistente, da restructure) |
Sistec.Stack.Safan.Client |
| Layer | Progetto | Contenuto | Dipende da |
|---|---|---|---|
| Client | Sistec.Stack.Esa.Client |
EsaModbusClient, protocollo Modbus ESA |
Sistec.Library.Modbus |
| Driver | Sistec.Stack.Esa.Driver |
IPressBrakeTagProvider, mappatura registri Modbus |
Sistec.Stack.Esa.Client, Sistec.Core |
| Services | Sistec.Stack.Esa.Services |
PressOrchestrator, BendingProgramService, PressStateService |
Sistec.Stack.Esa.Driver, Sistec.Core |
| UI | Sistec.Stack.Esa.UI |
PressBrakeView, ProgramSelectionControl, PressConfigDialog |
Sistec.Stack.Esa.Services, Sistec.Controls |
| Simulator | Sistec.Stack.Esa.Simulator |
Server Modbus falso (ESA-compatible) | Sistec.Stack.Esa.Client |
| Layer | Progetto | Contenuto | Dipende da |
|---|---|---|---|
| Client | Sistec.Stack.PLC.Client |
OpcUaClientCollection, IUAClient, autodiscovery |
Sistec.Library.OpcUa |
| Driver | Sistec.Stack.PLC.Driver |
IPlcTagProvider, OpcUaTagFactory, tag DUT (da codegen) |
Sistec.Stack.PLC.Client, Sistec.Core |
| Services | Sistec.Stack.PLC.Services |
PlcConnectionService, WatchdogService, SheetMonitorService, ModeService |
Sistec.Stack.PLC.Driver, Sistec.Core |
| UI | Sistec.Stack.PLC.UI |
PlcStatusControl, WatchdogIndicator, ModeSelector, viste sensori |
Sistec.Stack.PLC.Services, Sistec.Controls |
| Simulator | Sistec.Stack.PLC.Simulator |
FakeOpcUa, server OPC UA falso (CODESYS emulation) |
Sistec.Stack.PLC.Client |
| Layer | Progetto | Contenuto | Dipende da |
|---|---|---|---|
| Client | Sistec.Stack.Sinumerik.Client |
Wrapper OPC UA ONE CNC | Sistec.Library.OpcUa |
| Driver | Sistec.Stack.Sinumerik.Driver |
ISinumerikTagProvider, mappatura tag CNC |
Sistec.Stack.Sinumerik.Client, Sistec.Core |
| Services | Sistec.Stack.Sinumerik.Services |
LagNx1525, PunchingProgramService, ProgramContentGetter |
Sistec.Stack.Sinumerik.Driver, Sistec.Core |
| UI | Sistec.Stack.Sinumerik.UI |
PunchingProgramView, SinumerikStatusControl |
Sistec.Stack.Sinumerik.Services, Sistec.Controls |
| Simulator | Sistec.Stack.Sinumerik.Simulator |
LagNx1525Simulation (esistente, da restructure) |
Sistec.Stack.Sinumerik.Client |
| Layer | Progetto | Contenuto | Dipende da |
|---|---|---|---|
| Services | Sistec.Stack.Production.Services |
CellLogic, ProductionOrchestrator, PressRobotTeamService, PlcPunchingTeamService |
Tutti gli stack macchina, Sistec.Core |
| UI | Sistec.Stack.Production.UI |
JobView, CutPlanView, PanelTrackingView, PalletStateView |
Sistec.Stack.Production.Services, Sistec.Controls |
| Services | Sistec.Stack.JobManagement.Services |
JobManager, ProgramLogic, TrackerService |
Sistec.Core, Sistec.Infra.Persistence |
| UI | Sistec.Stack.JobManagement.UI |
JobDialog, ProgramSelectionView |
Sistec.Stack.JobManagement.Services, Sistec.Controls |
flowchart LR
subgraph HMI_Apps["APPLICAZIONI HMI (Thin Shells)"]
LAG_HMI["Sistec.5315.HMI
Composition Root +
pagine specifiche LAG"]
FAEL_AB["Sistec.5309.HMI.AB
Composition Root +
pagine specifiche AB"]
FAEL_C["Sistec.5309.HMI.C
Composition Root +
pagine specifiche C"]
BS["Sistec.5309.HMI.BS
Composition Root +
pagine specifiche BS"]
end
subgraph Shell["SISTEC.HMI.SHELL (shared)"]
CR["Program.cs + FrmHMI thin
DI Container + IOptions"]
end
subgraph Prod_Stacks["STACK APPLICATIVI"]
Prod["Sistec.Stack.Production"]
Job["Sistec.Stack.JobManagement"]
Maint["Sistec.Stack.Maintenance"]
Alarm["Sistec.Stack.Alarms"]
end
subgraph Machine_Stacks["STACK MACCHINA"]
Kuka["Sistec.Stack.Kuka"]
Safan["Sistec.Stack.Safan"]
Esa["Sistec.Stack.Esa"]
Plc["Sistec.Stack.PLC"]
Sinu["Sistec.Stack.Sinumerik"]
end
subgraph Libraries["LIBRERIE FONDAZIONE"]
Core["Sistec.Core"]
Controls["Sistec.Controls"]
SUI["Sistec.UI"]
OPC["Sistec.Library.OpcUa"]
Modbus["Sistec.Library.Modbus"]
Bus["Sistec.Library.Bus
(Zebus o HTTP+Redis)"]
RLib["Sistec.Library.Redis
(Data Layer Trasversale)"]
end
subgraph Infra["INFRASTRUTTURA"]
Config["Sistec.Configuration"]
Pers["Sistec.Infra.Persistence*"]
CodeGen["Sistec.Infra.CodeGen"]
end
LAG_HMI --> Shell
FAEL_AB --> Shell
FAEL_C --> Shell
BS --> Shell
Shell --> Prod_Stacks
Shell --> Alarm
Shell --> Controls
Shell --> SUI
Prod_Stacks --> Machine_Stacks
Prod_Stacks --> Job
Maint --> Plc
Kuka --> OPC
Kuka --> Core
Safan --> Core
Esa --> Modbus
Esa --> Core
Plc --> OPC
Plc --> Core
Sinu --> OPC
Sinu --> Core
Machine_Stacks --> Config
Prod_Stacks --> Pers
Job --> Pers
Controls --> Core
SUI --> Controls
SUI --> Core
Bus --> Core
classDef app fill:#e3f2fd,stroke:#1565c0
classDef shell fill:#e3f2fd,stroke:#1565c0,stroke-width:3px
classDef prod fill:#fff8e1,stroke:#f57f17
classDef machine fill:#e8f5e9,stroke:#2e7d32
classDef foundation fill:#e1f5e1,stroke:#2e7d32
classDef infra fill:#f3e5f5,stroke:#7b1fa2
class LAG_HMI,FAEL_AB,FAEL_C,BS app
class Shell shell
class Prod,Job,Maint,Alarm prod
class Kuka,Safan,Esa,Plc,Sinu machine
class Core,Controls,SUI,OPC,Modbus,Bus foundation
class Config,Pers,CodeGen infra
I file DUT (tag mapping OPC UA) sono attualmente scritti a mano in entrambe le codebase con commenti //order matters: use the same order used in the codesys structure — estremamente fragili e fonte di bug.
Soluzione: Code generation da definizioni CODESYS:
flowchart LR
INPUT["CODESYS .txt / .xml"] --> GEN["Sistec.Infra.CodeGen
(dotnet tool)"]
GEN --> POCO["Sistec.Stack.PLC.DTO (generated/)
← POCO puri (senza EncodeableBase)"]
GEN --> OPCENC["Sistec.Stack.PLC.OPCUA (generated/)
← Encodeable version"]
GEN --> TAGS["Sistec.Stack.PLC.Tags (generated/)
← TagConstants.Main.ProductionValid"]
GEN --> BIND["Sistec.Stack.PLC.Binding (generated/)
← Bind() tipizzato"]
Vantaggi:
"Main_ProductionValid" ma TagConstants.Main.ProductionValid)EncodeableBase — rompe il technology lock-in OPC UAFAEL presenta 3 varianti (AB, C, BS) con ~45-55% di codice duplicato. LAG ha una singola variante.
Problema identificato ma non risolto nel presente documento: La strategia per unificare le varianti HMI (feature flags, configurazione, o mantenimento di progetti separati snelli) va discussa separatamente, in quanto:
Raccomandazione preliminare: Iniziare comunque con la creazione degli stack verticali (Fase 1-2). La gestione varianti può essere affrontata in Fase 3 quando gli stack saranno stabili e le differenze tra varianti saranno più visibili.
// Program.cs — Composition Root unificato
public static void Main()
{
var builder = Host.CreateApplicationBuilder(args);
// Configurazione
builder.Services.Configure<PlcOptions>(builder.Configuration.GetSection("Plc"));
builder.Services.Configure<KukaOptions>(builder.Configuration.GetSection("Kuka"));
builder.Services.Configure<SafanOptions>(builder.Configuration.GetSection("Safan"));
builder.Services.Configure<EsaOptions>(builder.Configuration.GetSection("Esa"));
// Stack macchina (attivazione condizionale per variante)
builder.Services.AddKukaStack();
builder.Services.AddSafanStack(); // LAG
// builder.Services.AddEsaStack(); // FAEL (alternativo a Safan)
builder.Services.AddPlcStack();
builder.Services.AddSinumerikStack(); // LAG
// Stack applicativi
builder.Services.AddProductionStack();
builder.Services.AddJobManagementStack();
builder.Services.AddMaintenanceStack();
builder.Services.AddAlarmsStack();
// UI
builder.Services.AddSingleton<FrmHMI>();
var host = builder.Build();
var form = host.Services.GetRequiredService<FrmHMI>();
Application.Run(form);
}
// Metodo extension per stack KUKA
public static IServiceCollection AddKukaStack(this IServiceCollection services)
{
services.AddSingleton<IKrcClient, KrcClient>();
services.AddSingleton<IKukaTagProvider, KukaTagProvider>();
services.AddSingleton<IKukaRobotLogic, KukaRobotLogic>();
services.AddSingleton<IRobotFollowService, RobotFollowService>();
return services;
}
| Layer | Tipo Test | Framework | Tool |
|---|---|---|---|
| Client | Unit test + Integration | NUnit 4.x | Simulator dello stack |
| Driver | Unit test con mock | NUnit 4.x | Mock del Client |
| Services | Unit test con mock | NUnit 4.x | Mock del Driver |
| UI | Test manuali / visuali | — | WinForms test runner |
| Simulator | Integration test end-to-end | NUnit 4.x | Simulator → Client |
| Cross-stack | Integration test | NUnit 4.x | Tutti gli stack + simulatori |
Standard: NUnit 4.3.2 (già presente in LAG per Safan su net10.0) come framework unico per tutti i test automatici. coverlet.collector per code coverage. Ogni stack ha il suo progetto di test.
L'architettura target adotta esplicitamente i seguenti pattern creazionali e strutturali nei layer indicati:
| Pattern | Tipo | Layer | Applicazione | Esempio |
|---|---|---|---|---|
| Factory Method | Creazionale | Client / Driver | Crea driver/client senza accoppiamento al tipo concreto. La configurazione (IOptions<T>) decide quale protocollo istanziare. |
IClientFactory.Create(StackType.Kuka) restituisce un IKrcClient senza che il chiamante sappia se è TCP reale o Simulator |
| Facade | Strutturale | Services | Ogni Services layer nasconde la complessità del sottosistema macchina (Client + Driver + stato + riconnessione) esponendo solo metodi di dominio. | KukaRobotLogic.ExecuteProgram(programId) incapsula comandi TCP, polling stato, gestione errori, riconnessione |
| Proxy | Strutturale | Client | Virtual proxy per connessione lazy a macchine costose; Protection proxy per controllo accessi; Logging proxy per monitoraggio performance. | KrcClientProxy inizializza la connessione TCP solo al primo comando. LoggedKrcClient registra latenza e throughput in ILogger |
| Mediator | Comportamentale | Orchestration / Production | Disaccoppia la comunicazione tra UI Views e servizi incrociati. Un mediator centralizza le interazioni senza che le parti si conoscano. | ProductionOrchestrator media tra KukaRobotLogic, SafanPressBrakeLogic, PlcConnectionService e JobManager — le UI parlano solo con l'Orchestrator |
Dove nascono:
TagFactory che produce TagValue<T> tipizzati dal DTO CODESYS. Anche nel Layout Engine: ControlFactory (sez. 8.4) istanzia controlli UI da nome dichiarato in layout.json.Sistec.*.Stack.Services. Il Client sa solo di byte/stream, il Driver sa di tag/mapping, il Services orchesta e presenta una superficie di dominio pulita.Sistec.*.Stack.Simulator) è un Proxy dell'implementazione reale. La DI sceglie quale registrare: services.AddSingleton<KrcClient> (produzione) vs services.AddSingleton<IKrcClient, SimulatorKrcClient> (test).Sistec.Stack.Production (sez. 2.9) è di fatto un Mediator tra tutti gli stack macchina, senza che KUKA conosca Safan o viceversa.L'architettura corrente (LAG e FAEL) non usa DI container. Il risultato è sotto gli occhi di tutti: costruttori da centinaia di righe, God Class, service locator sparsi, e l'impossibilità di testare un singolo layer senza istanziare mezzo sistema.
IoC e DI non sono una moda .NET. Sono un principio ingegneristico trasversale — la Dependency Inversion Principle (DIP) è uno dei 5 SOLID — adottato da ogni ecosistema mainstream:
| Ecosistema | Container / Meccanismo DI | Anno standardizzazione |
|---|---|---|
| .NET | Microsoft.Extensions.DI (built-in) |
2016 |
| Java | Spring DI / Jakarta CDI / Google Guice | 2004 (Spring) |
| Python | dependency-injector / FastAPI Depends() |
2018+ |
| Rust | shaku / orion / trait injection pattern |
2020+ |
| Go | uber/fx / google/wire (codegen) |
2018+ |
| TypeScript | Angular DI / NestJS / tsyringe |
2016+ |
| C++ | Qt DI / Boost.DI | pattern, non standard |
Tutti i framework moderni di ogni linguaggio si aspettano DI. I template dotnet new generano già Program.cs con builder.Services. Le librerie .NET di terze parti (Serilog, EF Core, MediatR, FluentValidation) offrono extension methods su IServiceCollection. L'unico posto dove non si usa DI è il codice legacy Sistec — e questo isolamento è la causa diretta di molti anti-pattern elencati in §1.5.
Cosa risolve DI, concretamente, per Sistec:
| Problema attuale | Con DI |
|---|---|
MainForm sa di TcpClient, OPC UA, Safan, KUKA, DB, Watchdog |
MainForm riceve già tutto costruito — zero conoscenza interna |
| Aggiungere device = new in 5 punti del costruttore | Aggiungere device = 1 riga nel Composition Root |
Service locator ObjectUsageMonitor.Instance, LogicCollection |
Ogni dipendenza arriva via costruttore, nessun global state |
| Lifetime gestito a mano (chi dispose cosa?) | Container chiama Dispose in ordine inverso alla costruzione |
| Test: devi mockare tutto a mano o fare integration test | new TcpClient vs new SimulatorTcpClient = 1 riga nel Composition Root |
| Ordine di costruzione manuale (se A serve B, new B prima) | Container risolve l'ordine topologico automaticamente |
| Dipendenze opzionali (Safan vs ESA) = if/else sparsi | AddSafanStack() / AddEsaStack() commentato nel Composition Root |
| Condivisione librerie orizzontali = copia incolla in ogni stack | AddSingleton<Sistec.Library.Tcp> — unica istanza per tutti |
Esempio concreto — oggi vs domani:
// OGGI — LAG MainForm.cs
public MainForm()
{
_safanClient = new SafanClient("Safan_0"); // dipende da TcpClient
_safanLogic = new SafanPressBrakeLogic(_safanClient).Use(...); // dipende da SafanClient
_plcLogic = new PlcLogic(GVL_HMI); // dipende da OpcUaClient
_cellLogic = new CellLogic(_plcLogic, _punchingLogic, // dipende da tutto
_safanLogic, _robotLogic);
// 278 righe di new, configurazioni, event wiring
}
// DOMANI — Composition Root
builder.Services.AddSafanStack(); // 4 righe registrano Client+Driver+Services
builder.Services.AddPlcStack();
builder.Services.AddProductionStack();
public class FrmHMI( // costruttore vuoto,
CellLogic cellLogic, // DI risolve tutto
SafanPressBrakeLogic safanLogic, ...);
public class SafanPressBrakeLogic( // anche Services è testabile:
ISafanClient client, // può ricevere un SafanSimulator
IOptions<SafanOptions> options); // o il SafanClient reale
Non usare DI oggi significa ignorare un principio adottato trasversalmente da Java (2004), .NET (2016), TypeScript (2016), Go (2018), Python (2018), Rust (2020). Non esiste un progetto enterprise in alcun linguaggio moderno che gestisca dipendenze con new a mano. Anche Avalonia, il framework HMI target, si integra nativamente con IServiceCollection.
In sintesi: IoC e DI sono il collante architetturale di tutta la proposta. Senza DI, gli stack verticali sarebbero ancora legati da new espliciti — perderemmo testabilità, sostituibilità e la capacità di attivare/disattivare stack per commessa. Con DI, ogni stack è un pacchetto NuGet che si auto-registra, e il Composition Root è l'unico posto dove si decide cosa usare.
L'architettura target descritta nelle sezioni successive non si applica alle commesse LAG e FAEL esistenti, che rimangono in manutenzione correttiva con WinForms legacy. Viene invece costruita ex-novo per la prossima commessa, partendo da zero con le fondazioni condivise e aggiungendo via via gli stack macchina necessari.
┌─────────────────────────────────────────────────────┐
│ NUOVA COMMESSA (GREENFIELD) │
│ │
│ Fase 1 ─── Fondazioni (interfacce + infrastruttura) │
│ Fase 2 ─── Stack macchina necessari alla commessa │
│ Fase 3 ─── Applicativi + UI │
│ Fase 4 ─── Deploy + collaudo │
│ │
│ Esistenti (LAG, FAEL) restano invariati │
└─────────────────────────────────────────────────────┘
Tutto ciò che serve a qualsiasi commessa — indipendente dalla macchina specifica.
Sistec.Infra.* ← NuGet packages
├── Sistec.Infra.Persistence Interfacce + Dapper + MySql + SqlServer
├── Sistec.Infra.Configuration Options pattern (sostituisce Configuration statica)
├── Sistec.Infra.Authentication BCrypt + badge RFID + Employee/Role management
├── Sistec.Infra.Logging ILogger<T> strutturato, output JSON
└── Sistec.Infra.CodeGen dotnet tool → DUT da CODESYS → POCO + TagConstants
Sistec.Platform.* ← NuGet packages
├── Sistec.Platform.Controls Controlli base (Button, Led, Numeric, Valve, Motor)
├── Sistec.Platform.OpcUa Wrapper OPC UA Foundation SDK
├── Sistec.Platform.Modbus Stack Modbus TCP/RTU
└── Sistec.Platform.Cloud MQTT connector opzionale
Solo gli stack necessari alla commessa in corso. Ogni stack è un pacchetto NuGet indipendente.
Sistec.Stack.Kuka.* ← Se la commessa ha KUKA
├── Client → TCP/IP KRC
├── Driver → Comandi robot
├── Services → Logica robot
├── UI → Pagine Avalonia
└── Simulator → Test
Sistec.Stack.Safan.* ← Se la commessa ha pressa Safan
├── Client → TCP/IP Winsock
├── Driver → Comandi pressa
├── Services → Logica pressa
├── UI → Pagine Avalonia
└── Simulator → Test
... stesso pattern per ESA, Sinumerik, PLC, Bus ...
Sistec.Stack.Production ← Orchestrazione cella
Sistec.Stack.JobManagement ← Cicli di vita job
Sistec.Stack.Maintenance ← Manutenzione predittiva (ONNX)
Sistec.Stack.Alarms ← Alarm journal
Sistec.RecipeEngine ← Workflow configurabile JSON
Sistec.PalletStateMachine ← Macchina a stati pallet
Sistec.HMI.Shell ← Composition Root Avalonia
├── LayoutEngine ← layout.json → UI
└── EmployeeStatsPage ← Statistiche produzione per dipendente
Ansible playbook ← Setup PC industriale
├── roles/common ← Hostname, utenti, firewall
├── roles/mysql ← MySQL + seed DB
├── roles/hmi_app ← Deploy HMI + servizio Windows
└── roles/codesys ← WSL2 + Docker + container CODESYS
Test ricevimento ← Collaudo in fabbrica
├── smoke test su ogni stack
├── test layout.json
└── test statistiche operatore
| Fase | Durata | Cosa si ottiene |
|---|---|---|
| Fase 1 — Fondazioni | 4-5 settimane | NuGet infrastruttura, pronto per qualsiasi stack |
| Fase 2 — Stack macchina | 2-3 settimane/stack | Solo gli stack necessari alla commessa |
| Fase 3 — Applicativi + UI | 3-4 settimane | HMI completo + Layout Engine |
| Fase 4 — Deploy | 1-2 settimane | PC industriale configurato + collaudo |
| Totale prima commessa | ~12-16 settimane | Prima HMI greenfield, con tutti i fondamenti |
Le commesse successive riutilizzano Fase 1 + gli stack già esistenti, riducendo i tempi a 6-10 settimane (solo Fase 2-4).
| Aspetto | Oggi | Domani |
|---|---|---|
| KUKA Robot | 5 progetti, frammentato | Sistec.Stack.Kuka (Client→UI) |
| Pressa | Safan (TCP) in LAG, ESA (Modbus) in FAEL | Stack specifico per protocollo, interfaccia comune |
| PLC | DUT manuali, legati a EncodeableBase | DUT generati, POCO puri + mapping |
| MainForm/FrmHMI | 19-24 partial file, migliaia di righe | Thin orchestrator, ~300 righe |
| Common Project | God Project da eliminare | Smantellato, logica nei moduli |
| Configurazione | Configuration.PlcConfig[...] statico |
IOptions<T> con DI |
| DB | Repository in 2 layer, no Unit of Work | Persistence multi-progetto con UoW |
| Messaggi | Solo in FAEL (Zebus) | Sistec.Library.Bus opzionale |
| Test | 2 progetti NUnit (LAG), test manuali (FAEL) | NUnit per ogni stack + Simulator |
| Code Generation | Assente | DUT generati da CODESYS |
| Onboarding | "Da dove inizio?" | "Leggi Sistec.Stack.Kuka" |
| Vantaggio | Spiegazione |
|---|---|
| Coesione | Ogni macchina è un unico stack verticale. KUKA in Sistec.Stack.Kuka.* |
| Manutenibilità | Modifica della pressa Safan? Solo Sistec.Stack.Safan.* |
| Testabilità | Ogni layer testabile isolatamente con mock/simulatore |
| Riutilizzo | Uno stack può servire più commesse (LAG e FAEL condividono Kuka.Stack) |
| Configurabilità | Per commessa: quali stack attivare, configurazione via IOptions |
| Technology Switch | Sostituire OPC UA con gRPC? Solo Sistec.Stack.PLC.Client |
| Code Quality | Nessun DUT manuale, tag type-safe, nessuna dipendenza OPC UA nei modelli |
| Caricamento Mentale | Basso: 1 stack = 1 macchina, 5 layer ben definiti |
flowchart LR
subgraph PROBLEMI["ANTI-PATTERN TRASVERSALI"]
AP1["① God Project
Common (LAG + FAEL)"]
AP2["② God Class
MainForm / FrmHMI"]
AP3["③ Service Locator
Configuration + LogicCollection"]
AP4["④ No DI Container
new + .Use()"]
AP5["⑤ Accoppiamento
a classi concrete"]
AP6["⑥ Stack frammentato
KUKA in 5 progetti"]
AP7["⑦ Technology Lock-In
DUT → EncodeableBase"]
AP8["⑧ Magic Strings
Tag name hardcoded"]
AP9["⑨ DB Access Sparso
No Unit of Work"]
AP10["⑩ Lava Flow / Dead Code
Contracts\**, commenti"]
AP11["⑪ SafeInvoke
WinForms nei layer bassi"]
AP12["⑫ Duplicazione Varianti
AB vs C vs BS (FAEL)"]
AP13["⑬ Fire-and-Forget
Forget() + Task.Run ovunque"]
AP14["⑭ Hub Dipendenze
Common → 6 progetti"]
AP15["⑮ Version Drift
Sistec.Core → LAG vs FAEL"]
AP16["⑯ Missing IDisposable
TcpClient, KrcClient, ..."]
AP17["⑰ Blocking in Async
.Result + spin-wait"]
AP18["⑱ Duplicazione Test
13× FakeOpcUa identici"]
end
subgraph SOLUZIONI["SOLUZIONI"]
S1["Stack verticali
Smantellare Common"]
S2["DI Container +
FrmHMI thin orchestrator"]
S3["IOptions<T>
+ DI"]
S4["Microsoft.Extensions
DependencyInjection"]
S5["Dipendere da
interfacce"]
S6["Sistec.Stack.Kuka
modulo unico verticale"]
S7["Code Generation DUT
POCO + EncodeableBase separati"]
S8["TagConstants generati
type-safe"]
S9["Sistec.Infra.Persistence
con Unit of Work"]
S10["Eliminare dead code
pulizia sistematica"]
S11["ISyncContext astratto
no Control.SafeInvoke"]
S12["Feature flags /
discussione aperta"]
S13["await esplicito +
error tracking"]
S14["Estrarre interfacce
di Common in un progetto API"]
S15["Baseline unico
partire da FAEL + cherry-pick"]
S16["IDisposable + using
su tutte le classi I/O"]
S17["Task.Wait() vietato
await in tutto il path async"]
S18["Test共享 library
FakeOpcUa → NuGet test helper"]
end
AP1 --> S1
AP2 --> S2
AP3 --> S3
AP4 --> S4
AP5 --> S5
AP6 --> S6
AP7 --> S7
AP8 --> S8
AP9 --> S9
AP10 --> S10
AP11 --> S11
AP12 --> S12
AP13 --> S13
AP14 --> S14
AP15 --> S15
AP16 --> S16
AP17 --> S17
AP18 --> S18
style PROBLEMI fill:#ffebee,stroke:#c62828
style SOLUZIONI fill:#e8f5e9,stroke:#2e7d32
| # | Anti-Pattern | Gravità | Codebase | Dove | Dettaglio |
|---|---|---|---|---|---|
| ⑬ | Fire-and-Forget con Forget() |
🔴 CRITICA | Entrambe | TaskExtensions.cs, EasyModbus\Extensions.cs |
Extension Forget() su Task scarta ogni eccezione. Usata in Watchdog reconnect, TagValue writes (LAG: Task.Run senza error handling), PropertyNotifier, RealTimeAlarmEvents. FAEL ha risolto parzialmente con TagValueBase<T> (coda writes SemaphoreSlim), LAG usa ancora Task.Run fire-and-forget. |
| ⑭ | Hub Dipendenze (Common) | 🔴 CRITICA | LAG | Sistec.5315/Common |
Common reference 6 progetti interni (Controls, Core, OpcUa, Safan, Sinumerik, UI). Crea un diamante di dipendenze: impossibile testare uno stack senza trascinare tutto. La soluzione è estrarre interfacce in un progetto API snello e spostare le implementazioni negli stack verticali. |
| ⑮ | Sistec.Core Version Drift | 🔴 CRITICA | LAG vs FAEL | Sistec.Core/ intero |
Le due codebase condividono nominalmente Sistec.Core ma hanno divergenze sostanziali: (a) TagValue: FAEL ha TagValueBase<T> con coda writes SemaphoreSlim (latest-wins), LAG ha Task.Run fire-and-forget. (b) Motori/Valvole: FAEL ha CommandType<T> + regex parsing, LAG ha struct semplici inline. (c) SafeInvoke: in LAG in Extensions.cs, in FAEL spostato in Utilities.cs. (d) Dialogs: FAEL ha IContainedControl, LAG no. La riconciliazione branch (Fase 0a) deve decidere da che baseline partire. |
| ⑯ | Missing IDisposable | 🟠 ALTA | Entrambe | TcpClient, KrcClient, KrcClientLogic, ModbusConnector, LagNx1525 |
Classi critiche di rete/device non implementano IDisposable. In una HMI che gira 24/7 su PC industriali, socket e connessioni non rilasciati causano memory leak certi. Necessario: Dispose(bool) pattern su tutte, con using nei consumer. |
| ⑰ | Blocco in Path Async | 🟠 ALTA | Entrambe | ModbusClient.cs:1596, PressBrakeCollection.cs:33, Sistec.Tcp\Program.cs:144 |
ModbusClient.Dispose() usa .GetAwaiter().GetResult() (deadlock risk in contesto UI). PressBrakeCollection.Close() ha spin-wait while (pressBrake.IsConnected). Sistec.Tcp (simulatore) ha Thread.Sleep(270) in metodo async. Vietare Task.Wait()/.Result in tutto il codebase, usare await in ogni path. |
| ⑱ | Touch Subsystem solo in FAEL | 🟠 ALTA | Solo FAEL | Sistec.Controls/Touch/ (5 file) |
FAEL ha implementato un sottosistema completo di input touch Win32 (penna/dito, long-press, swipe). LAG non ha nulla. Se un cliente LAG richiedesse touch screen, l'implementazione andrebbe riscritta da zero. Nella migrazione ad Avalonia il touch è nativo, ma fino ad allora LAG è esposto. |
| ⑲ | Buffer Management Assente | 🟡 MEDIA | Entrambe | TcpClient.cs, EasyModbus\DataBuffer.cs, Kuka.Client\Utils.cs |
Nessun pooling dei buffer: TcpClient alloca char[2048] a ogni connessione; DataBuffer.AddBytes() crea nuovo byte[] a ogni aggiunta ricopiando tutto; Kuka alloca stringhe a ogni messaggio. Usare ArrayPool<T>.Shared o MemoryPool<T>. |
| ⑳ | Magic Numbers nel Parsing Kuka | 🟡 MEDIA | FAEL | Kuka.Client\Utils.cs:59-65 |
Offset fissi hardcoded (14, 18, message.Length - 18, message.Length - 4) per estrarre header/payload/checksum dal protocollo KRC. Un cambiamento nel protocollo KUKA richiederebbe riscrittura manuale. Sostituire con costanti nominate + test di parsing. |
| ㉑ | Duplicazione FakeOpcUa (13 copie) | 🟡 MEDIA | Entrambe | 13 progetti di test | Stessa identica classe mock/fake FakeOpcUa replicata in 13 progetti di test diversi (Test/*/FakeOpcUa.cs, Sistec.HMI/*/FakeOpcUa.cs). Ogni modifica va propagata manualmente. Estrarre in un pacchetto NuGet Sistec.TestHelpers o in una shared library di test. |
| ㉒ | Monitor.TryEnter in KukaTagValue | 🟡 MEDIA | FAEL | Kuka.Client\KukaTagValue.cs:90-107 |
Pattern fragile: Monitor.TryEnter + return dentro il lock. Tecnicamente corretto ma estremamente delicato per manutenzione futura. Refactoring banale in lock statement. |
| ㉓ | COM/Interop Legacy (Kuka) | 🟡 MEDIA | Solo simulatore | KukaServerSimulator\WBC_KrcLib\*.cs |
8 interfacce [ComImport] per KUKA KRC Win32 COM. Blocca la migrazione a Linux del simulatore. Valutare sostituzione con protocollo TCP nativo o舍弃 del simulatore se il Kuka.Stack.Client ha già un simulatore C# puro. |
| ㉔ | ReconnectAgent Timeout Hardcoded | 🟡 MEDIA | Entrambe | Sistec.Core\Utils\ReconnectAgent.cs:24 |
s_cts.CancelAfter(10_000) — ogni tentativo di riconnessione ha timeout massimo fissato a 10 secondi, non configurabile. Rendere parametro di IReconnectionPolicy. |
| ㉕ | Resource.Designer.cs Giganti | 🟢 BASSA | Entrambe | *.Resources.Designer.cs (4 file > 1000 righe) |
File Resources.Designer.cs di Controls (1838 righe), UI (1417), Common (1322), HMI (1172). Auto-generati ma sintomo di risorse embedded non ottimizzate. Valutare Resource Manager personalizzato o lazy loading. |
L'analisi della gestione utenti rivela un sistema funzionante ma insufficiente per gli standard moderni e per le richieste dei clienti.
La user management è identica in entrambe le codebase e si basa su:
| Componente | File | Descrizione |
|---|---|---|
| Modello | Sistec.Core\Model\Account.cs |
POCO con Dapper.Contrib: ID, UserLevel (int), UserPassword (plaintext), account name in 3 lingue |
| Enum ruoli | Sistec.Core\Enums\UserLevel.cs |
NoUser(0) < Operator(1) < Maintenance(2) < Expert(3) < Sistec(4) |
| Repository | Sistec.Core\DB\AccountRepositoryAsync.cs |
Dapper CRUD su tabella MySQL accounts |
| Login dialog | Sistec.UI\frmLayout\frmChangeUser.cs |
ComboBox utenti + password textbox + Login/Logout button |
| User management | Sistec.UI\frmLayout\frmUserManagement.cs |
CRUD utenti con SQL injection (string interpolation) — parzialmente disabilitato (operazioni commentate) |
| Stato globale | Sistec.UI\ClassUtility\Globals_Standard.cs |
LoggedUser, UserLevel (PropertyNotifier), Users dictionary, auto-logout timer |
| Controllo ruoli | Sistec.Core\Extensions.cs |
Metodi extension IsOperator(), IsMaintenance(), IsExpert(), IsSistec() con confronto gerarchico >= |
| Auto-login backdoor | loginSistec.ls (file vuoto) |
Se presente nella directory dell'app, logga automaticamente come Sistec senza password |
Niente utenti reali, solo ruoli — Il sistema ha 4 livelli (Operator, Maintenance, Expert, Sistec) ma nessun legame tra un dipendente reale e la sua produzione. Non esiste una tabella employees o operators. L'Account memorizza solo un livello e una password, non un nome dipendente, badge, turno, o matricola.
Password in chiaro — UserPassword memorizzata in plaintext su MySQL. Confronto in chiaro: txtuserPassword.Text != selectedUser.UserPassword. Hardcoded Sistec password "747832" nel codice sorgente (Globals_Standard.cs:213).
SQL injection — frmUserManagement.cs costruisce query con interpolazione diretta: $"INSERT INTO accounts ... VALUES ('{txtuserPassword.Text}')". Operazioni CRUD attualmente commentate (non funzionanti).
Nessuna produzione per utente — Impossibile rispondere a domande come "quanti pezzi ha prodotto Mario Rossi oggi?", "qual è il tempo medio per cambio utensile di questo operatore?", "quanti scarti ha prodotto il turno notturno?".
Auto-login backdoor — File loginSistec.ls bypassa completamente l'autenticazione.
Login dialog rudimentale — Nessun supporto per badge RFID, nessun login con tessere, nessun PIN rapido (solo password testuale).
Nessun audit trail — Non c'è traccia di chi ha fatto cosa (chi ha modificato un parametro, chi ha forzato un reset, chi ha cambiato la configurazione).
Nessun principal .NET — Thread.CurrentPrincipal mai impostato, niente ClaimsPrincipal, niente autorizzazione dichiarativa [Authorize].
flowchart LR
subgraph NOW["OGGI — Solo Ruoli"]
A1["accounts table
ID | UserLevel | Password | Name_it"]
A2["Login: scegli ruolo
da ComboBox"]
A3["Produzione: anonima
nessun legame operatore"]
end
subgraph FUTURE["DOMANI — Utenti Reali"]
B1["employees table
BadgeId | Nome | Turno | Role"]
B2["Login: badge RFID
o selezione dipendente"]
B3["Produzione: tracciata
per operatore"]
B4["Statistiche: pezzi/h,
scarti, tempi morti"]
end
NOW -->|Refactoring| FUTURE
Il sistema proposto:
employees con: BadgeId (PK), FirstName, LastName, Shift (Mattina/Pomeriggio/Notte), Role (FK → roles), PinCode (hashato), IsActiveroles con permessi granulari: CanEditParameters, CanResetMachine, CanAccessReports, CanManageUsers, ecc.OperatorId FK su ogni Job, ProductionEvent, ScrapEvent, AlarmAck, ParameterChange"AutoLogin": false, "RFIDReader": "COM3", "PinMinLength": 4Questa modifica è indipendente dagli stack macchina — può essere realizzata in parallelo alle fasi 0-1 e non blocca la migrazione a stack verticali.
| Rischio | Probabilità | Impatto | Mitigazione |
|---|---|---|---|
| Regressione durante refactoring | Alta | Alto | Test NUnit + Simulator prima del deploy. Fase pilota KUKA per validare |
| Resistenza al cambiamento | Media | Medio | Migrazione incrementale, non big-bang. Dimostrare valore con pilota |
| Complessità iniziale DI | Media | Basso | Iniziare con KUKA Stack come pilota (3-4 settimane) |
| Code Generation non copre tutti i casi DUT | Media | Medio | Generatore estensibile con override manuali |
| Differenze reali LAG vs FAEL troppo grandi | Bassa | Alto | L'analisi mostra che il 70%+ dell'architettura è identica |
| Stack troppo granulosi (5 layer × N stack) | Media | Basso | 5 layer è il massimo; stack semplici possono avere 2-3 layer |
| Performance DI Container | Molto Bassa | Basso | Overhead trascurabile in contesto HMI |
L'architettura a stack verticali risolve la frammentazione del codice macchina, ma non basta per consegnare un nuovo impianto senza scrivere codice. Il problema aperto è il layout HMI: la disposizione delle pagine, la posizione dei controlli, i collegamenti tra schermate cambiano per ogni commessa e attualmente richiedono pagine WinForms scritte a mano.
Nuova commessa = cartella con:
├── manifest.json ← Quali stack attivare, versione firmware
├── layout.json ← Struttura pagine, zone, navigazione
├── config/ ← Parametri macchina (Kuka.ini, PLC.json, Safan.json)
├── tags/ ← DUT generati da CODESYS
├── resources/ ← Immagini, icone, traduzioni
└── (nessun file .cs nuovo)
flowchart TB
subgraph Manifest["MANIFEST COMMESSA"]
MJ["manifest.json
Versione, Variante,
Stack attivi, Riferimenti"]
end
subgraph LayoutDef["DEFINIZIONE LAYOUT"]
LJ["layout.json
Albero pagine, zone,
posizioni, binding tag"]
LT["layouts/
Template page (opzionali)
WinForms designer export"]
end
subgraph Engine["SISTEC.LAYOUT.ENGINE"]
LP["LayoutParser
Legge JSON → albero pagine"]
LB["LayoutBuilder
Costruisce UserControl
da definizioni"]
LBind["LayoutBinder
Collega controlli a tag PLC
via ITagProvider"]
LNav["LayoutNavigation
Menu, breadcrumb,
pulsantiere"]
end
subgraph Rendering["RENDERING"]
RZ["ZoneRenderer
Contenitore zona →
disposizione controlli"]
RC["ControlFactory
Nome controllo →
istanza UserControl dallo stack"]
RD["DialogFactory
Dialoghi (manutenzione,
job, popup)"]
end
subgraph Stacks["STACK VERTICALI"]
Kuka["Sistec.Stack.Kuka.UI"]
Safan["Sistec.Stack.Safan.UI"]
Esa["Sistec.Stack.Esa.UI"]
Plc["Sistec.Stack.PLC.UI"]
Sinumerik["Sistec.Stack.Sinumerik.UI"]
Prod["Sistec.Stack.Production.UI"]
Maint["Sistec.Stack.Maintenance.UI"]
Jobs["Sistec.Stack.JobManagement.UI"]
end
Manifest --> Engine
LayoutDef --> Engine
Engine --> Rendering
Rendering -->|ControlFactory cerca| Stacks
layout.json (Proposta){
"$schema": "https://sistec.it/schemas/layout-v1.json",
"version": "1.0",
"plant": "LAG-5315",
"variant": "LAG",
"screens": [
{
"id": "home",
"title": "Home",
"type": "grid",
"rows": 2, "cols": 3,
"cells": [
{ "row": 0, "col": 0, "rowSpan": 2,
"control": "Kuka.Views.ucKukaInfo",
"binding": { "robot": "Kuka_0" }
},
{ "row": 0, "col": 1,
"control": "Safan.Views.SafanStatusView",
"binding": { "press": "Safan_0" }
},
{ "row": 0, "col": 2,
"control": "PLC.Views.PlcStatusControl",
"binding": { "plc": "PLC_0" }
},
{ "row": 1, "col": 1,
"control": "JobManagement.Views.JobSummary",
"binding": {}
},
{ "row": 1, "col": 2,
"control": "Alarms.Views.AlarmBanner",
"binding": { "limit": 5 }
}
]
},
{
"id": "production",
"title": "Produzione",
"type": "tabbed",
"tabs": [
{ "title": "Job", "control": "Production.Views.JobView" },
{ "title": "CutPlan", "control": "Production.Views.CutPlanView" },
{ "title": "Pannelli", "control": "Production.Views.PanelTrackingView" }
]
},
{
"id": "maintenance",
"title": "Manutenzione",
"type": "page",
"control": "Maintenance.Views.MaintenanceView",
"binding": {}
}
],
"navigation": {
"type": "sidebar",
"items": [
{ "label": "Home", "icon": "home.png", "screen": "home" },
{ "label": "Produzione", "icon": "production.png","screen": "production" },
{ "label": "Manutenzione", "icon": "wrench.png", "screen": "maintenance" }
]
},
"dialogs": [
{
"id": "job-create",
"control": "JobManagement.Views.CreateJobDialog",
"size": { "width": 800, "height": 600 }
},
{
"id": "alarm-history",
"control": "Alarms.Views.AlarmHistoryDialog",
"size": { "width": 1024, "height": 768 }
}
]
}
Pattern: Factory Method — la creazione del controllo è delegata a una factory registrata da ogni stack, senza che il Layout Engine conosca il tipo concreto.
Ogni stack verticale registra i propri controlli in un dizionario globale al momento della registrazione nel DI container:
// In Sistec.Stack.Kuka.UI/ModuleRegistration.cs
public static void RegisterControls(ControlRegistry registry)
{
registry.Register("Kuka.Views.ucKukaInfo", () => new ucKukaInfo());
registry.Register("Kuka.Views.KukaOverride", () => new KukaOverrideControl());
registry.Register("Kuka.Views.RobotDropOffset", () => new RobotDropOffsetView());
}
// In Sistec.Stack.PLC.UI/ModuleRegistration.cs
public static void RegisterControls(ControlRegistry registry)
{
registry.Register("PLC.Views.PlcStatusControl", () => new PlcStatusControl());
registry.Register("PLC.Views.WatchdogIndicator",() => new WatchdogIndicator());
}
Il ControlFactory nel LayoutEngine cerca il nome dal JSON, istanzia il controllo e applica i binding:
public class ControlFactory
{
private readonly Dictionary<string, Func<UserControl>> _registry = new();
public UserControl Create(string controlName, Dictionary<string, object> binding)
{
if (!_registry.TryGetValue(controlName, out var factory))
throw new UnknownControlException(controlName);
var control = factory();
ApplyBinding(control, binding); // setta proprietà via reflection / ITagProvider
return control;
}
}
flowchart LR
A["Cliente ordina
impianto"] --> B["Ingegnere compila
manifest.json layout.json config/"]
B --> C["Sistec.Infra.CodeGen
genera DUT da CODESYS"]
C --> D["Collaudo: si lancia
l'HMI con la cartella
della commessa"]
D --> E["L'HMI:
1. Legge manifest.json
2. Attiva gli stack necessari
3. Carica layout.json
4. Carica risorse
5. Avvia"]
E --> F{"Tutto OK?"}
F -->|Sì| G["Consegnato.
Zero righe di codice scritte."]
F -->|No| H["Si modifica solo
layout.json o config/
mai codice C#"]
H --> D
| Scenario | Cosa fare | Frequenza |
|---|---|---|
| Nuovo macchinario mai visto | Scrivere il nuovo Sistec.<Nuovo>.Stack completo |
Rara (1-2 volte/anno) |
| Layout pagina completamente nuovo | Scrivere template page WinForms e registrarlo | Poco frequente |
| Flusso produttivo inedito | Nuovo ProductionOrchestrator o estensione |
Occasionale |
| Stesso macchinario, layout diverso | Solo layout.json |
Ogni commessa |
| Parametri diversi | Solo config/*.json |
Ogni commessa |
| Immagini / icone diverse | Solo resources/ |
Ogni commessa |
| Traduzioni | Solo file JSON | Ogni commessa |
Il Layout Engine è una Fase 6 che si aggiunge dopo la Fase 5:
gantt
title Roadmap con Layout Engine
dateFormat YYYY-MM-DD
axisFormat %d/%m
section Fase 6 — Layout Engine (3-4 settimane)
Progettare formato layout.json + schema JSON :f6a, 2026-09-01, 3d
Implementare LayoutParser :f6b, after f6a, 3d
Implementare ControlFactory + ControlRegistry :f6c, after f6b, 4d
Implementare LayoutBuilder + ZoneRenderer :f6d, after f6c, 4d
Implementare LayoutNavigation (menu, breadcrumb) :f6e, after f6d, 3d
Integrare con manifest.json + startup sequence :f6f, after f6e, 3d
Creare validatore schema JSON per layout :f6g, after f6f, 2d
Strumento designer (anteprima layout) :f6h, optional, 5d
Importante: Il Layout Engine ha senso SOLO dopo che gli stack verticali esistono. Senza stack verticali, i controlli UI sono sparsi in 5 progetti e il ControlFactory non saprebbe dove cercarli. Le fasi 0-5 sono prerequisito obbligatorio.
Nelle codebase legacy (LAG/FAEL), le chiavi di traduzione sono accoppiate ai nomi dei controlli WinForms:
uc2_ThicknessCheck_grpCalibrations → "Calibrations"
uc2_Sistec_grpAutoOverRide → "Auto Override"
btnCancel → "Cancel"
Cancel → "Cancel" (chiave diversa, stessa parola)
#Cancel → "Cancel" (ancora diversa)
Stessa parola "Cancel" appare con 3 chiavi diverse a seconda del contesto. Ogni impianto ha una copia locale della tabella translations nel suo MySQL — manutenzione N volte, rischio divergenza.
Quando l'impianto è connesso a Internet, la tabella delle traduzioni è online e unica per tutti gli impianti. Le chiavi sono parlanti e seguono la convenzione {dominio}.{entità}.{proprietà}.
Regole fondamentali:
ui.btn.cancel ovunque, non 10 chiavi diverseplant5315.robot.customAlarmMessageSchema tabella translations centralizzata:
| Key (PK) | It | En | Other | Scope |
|---|---|---|---|---|
ui.btn.cancel |
Annulla | Cancel | ... | Shared |
ui.btn.ok |
OK | OK | ... | Shared |
robot.status.running |
In esecuzione | Running | ... | Shared |
press.alarms.active |
Allarme attivo | Alarm active | ... | Shared |
plant5315.robot.customAlarm |
Messaggio specifico 5315 | ... | ... | Plant |
Gerarchia chiavi proposta:
ui. ← Controlli generici (btn, dialog, menu)
robot. ← Robot KUKA, stato, comandi
press. ← Pressa, allarmi, parametri
production. ← Job, ricette, tracking
maintenance. ← Manutenzione, diagnostica
alarm. ← Allarmi generici
plant{ID}. ← Override specifici impianto
Esempio codice — confronto legacy vs greenfield:
// PRIMA (legacy): 10 chiavi diverse per "Cancel"
btnCancel.Text = t.GetOrDefault("btnCancel", "#Cancel");
btn2.Text = t.GetOrDefault("Cancel", "#Cancel");
btn1.Text = t.GetOrDefault("btnCancel", "#Cancel");
// DOPO (greenfield): chiave unica parlante
btnCancel.Text = t.Get("ui.btn.cancel");
// PRIMA (legacy): chiave accoppiata al componente
grpCalibrations.Text = t.GetOrDefault("uc2_ThicknessCheck_grpCalibrations", "#Calibrations");
// DOPO (greenfield): chiave basata sul significato
grpCalibrations.Text = t.Get("press.calibrations.title");
Deployment e sincronizzazione:
| Componente | Ruolo |
|---|---|
| DB cloud | MySQL/MariaDB centralizzato, accessibile via internet da tutti gli impianti |
| Cache locale (default) | SQLite con chiavi ui.*, alarm.*, robot.* (critiche per operabilità) |
| Cache distribuita (opzionale) | Redis + TTL 1h: sistec:{plant}:i18n:{key} — aggiornamento immediato su tutti i pannelli, refresh automatico |
| Sync periodico | ogni X minuti quando online; offline lavora con cache |
| Aggiornamento | UPDATE sulla tabella cloud → tutti gli impianti vedono il cambiamento al prossimo sync |
| Logging mancanti | ITranslationsLogger registra chiavi non trovate → dashboard cloud per identificare chiavi da aggiungere |
Con Redis attivo, una traduzione corretta è visibile su tutti i pannelli in tempo reale via Pub/Sub, senza attendere il sync periodico. Il TTL garantisce refresh forzato periodico contro il DB cloud.
Vantaggi:
plant{ID} per esigenze locali senza inquinare le chiavi condiviseStesso problema del layout, ma sulla logica applicativa: ricette, pallet, flussi di produzione sono attualmente hardcoded in CellLogic/ProductionOrchestrator con if/else su variante impianto. LAG ha una logica, FAEL un'altra. La soluzione è identica: estrarre la logica in dati configurabili.
Oggi: BendingProgramService, CutPlanService, PressRobotTeamService contengono sequenze hardcoded con if/else su CellType.AB vs CellType.C.
// OGGI — logica hardcoded in CellLogic
public async Task ExecuteProductionStep(StepType step)
{
if (_cellType == CellType.LAG)
{
await _kuka.LoadPanel("vacuum_a");
await _safan.Bend("P123", 90);
await _kuka.Unload("pallet_1");
}
else if (_cellType == CellType.FAEL)
{
await _kuka.LoadPanel("gripper_b");
await _esa.Press("PROG_456");
await _kuka.Unload("pallet_2");
await _conveyor.Advance(); // FAEL ha un nastro in più
}
}
Proposta: Recipe = workflow configurabile in JSON, eseguito da un RecipeEngine generico che non conosce la variante impianto.
{
"recipeId": "BEND_PANEL_LAG_001",
"displayName": "Piegatura pannello standard LAG",
"plantVariant": "LAG",
"steps": [
{
"id": "load_panel",
"machine": "Kuka_0",
"action": "LOAD_PANEL",
"params": { "gripper": "vacuum_a", "pickFrom": "infeed" },
"timeout": 30,
"retry": 2
},
{
"id": "bend",
"machine": "Safan_0",
"action": "BEND",
"params": { "program": "P123", "angle": 90, "speed": "fast" },
"timeout": 60,
"dependsOn": ["load_panel"]
},
{
"id": "unload",
"machine": "Kuka_0",
"action": "UNLOAD",
"params": { "destination": "pallet_1", "orientation": "stacked" },
"dependsOn": ["bend"]
}
],
"rules": {
"retryOnError": true,
"maxRetries": 3,
"requireApproval": false,
"abortOnFailure": true,
"logEachStep": true
}
}
Il RecipeEngine esegue la sequenza senza sapere cosa fa ogni macchina — chiama i metodi sullo stack appropriato via interfacce:
// Sistec.Stack.RecipeEngine — generico, zero if/else su variante
public class RecipeEngine : IRecipeEngine
{
private readonly IMachineRegistry _machines;
private readonly ILogger<RecipeEngine> _logger;
public async Task<RecipeResult> ExecuteAsync(Recipe recipe, CancellationToken ct)
{
var context = new RecipeContext(recipe);
foreach (var step in recipe.Steps.OrderBy(s => s.Order))
{
await WaitForDependencies(step, context, ct);
var machine = _machines.Resolve(step.Machine); // interfaccia IMachineAction
var result = await machine.ExecuteActionAsync(step.Action, step.Params, ct);
context.SetStepResult(step.Id, result);
if (!result.Success && recipe.Rules.AbortOnFailure)
return RecipeResult.Failed(step, result.Error);
}
return RecipeResult.Completed(context);
}
}
Ogni stack macchina registra le azioni che sa eseguire:
// In Sistec.Stack.Kuka.Services
public class KukaRobotActionProvider : IMachineActionProvider
{
public string MachineName => "Kuka_0";
public Dictionary<string, Func<StepParams, Task<ActionResult>>> Actions => new()
{
["LOAD_PANEL"] = async p => { await _robot.LoadPanel(p.Get<string>("gripper")); return ActionResult.Ok(); },
["UNLOAD"] = async p => { await _robot.Unload(p.Get<string>("destination")); return ActionResult.Ok(); },
};
}
📁 Sistec.5315.LAG/
├── config/
│ └── recipes/
│ ├── bend_panel_lag.json ← LAG: Kuka + Safan
│ └── punch_program.json ← LAG: con Sinumerik
📁 Sistec.5309.FAEL.AB/
├── config/
│ └── recipes/
│ ├── bend_panel_fael.json ← FAEL: Kuka + Esa + conveyor
│ └── cut_plan.json ← FAEL: senza Sinumerik
Stesso RecipeEngine, stessi stack macchina, ricette diverse in JSON. Zero codice toccato.
Oggi: PalletLogic in FAEL (100 righe) vs assente in LAG (o implementato diversamente in ProductionOrchestrator). If/else su numero pallet, tipi, stati, transizioni.
Proposta: macchina a stati finita configurabile in JSON, ogni impianto definisce stati, transizioni, vincoli.
{
"palletConfig": {
"count": 9,
"types": [
{ "id": "standard", "label": "Standard", "capacity": 50, "maxWeight": 500 },
{ "id": "oversize", "label": "Oversize", "capacity": 10, "maxWeight": 1000 }
],
"layout": "3x3",
"initialState": "EMPTY"
},
"states": [
{ "id": "EMPTY", "label": "Vuoto" },
{ "id": "LOADING", "label": "In carico" },
{ "id": "FULL", "label": "Pieno" },
{ "id": "QUALITY_CHECK", "label": "Controllo qualità" },
{ "id": "SHIPPED", "label": "Spedito" },
{ "id": "REWORK", "label": "Da rilavorare" }
],
"transitions": [
{ "from": "EMPTY", "to": "LOADING", "trigger": "panel_placed" },
{ "from": "LOADING", "to": "FULL", "trigger": "capacity_reached" },
{ "from": "LOADING", "to": "LOADING", "trigger": "panel_placed" },
{ "from": "FULL", "to": "QUALITY_CHECK", "trigger": "request_qc" },
{ "from": "FULL", "to": "REWORK", "trigger": "qc_failed" },
{ "from": "QUALITY_CHECK", "to": "SHIPPED", "trigger": "qc_passed" },
{ "from": "QUALITY_CHECK", "to": "REWORK", "trigger": "qc_failed" },
{ "from": "REWORK", "to": "LOADING", "trigger": "rework_done" },
{ "from": "SHIPPED", "to": "EMPTY", "trigger": "pallet_cleared" }
],
"rules": {
"maxPalletsPerType": { "standard": 6, "oversize": 3 },
"requireQcForType": ["oversize"],
"autoAdvanceOnFull": true,
"notifyOnStateChange": ["FULL", "QUALITY_CHECK"]
}
}
// Sistec.Stack.PalletTracking — generico, zero if/else
public class PalletStateMachine
{
private readonly PalletConfig _config;
private readonly Dictionary<string, Pallet> _pallets = new();
public bool TryTransition(string palletId, string trigger, out string error)
{
var pallet = _pallets[palletId];
var transition = FindValidTransition(pallet.State, trigger);
if (transition == null)
{
error = $"Transition '{trigger}' not allowed from state '{pallet.State}'";
return false;
}
pallet.State = transition.To;
OnStateChanged?.Invoke(pallet);
error = null;
return true;
}
}
Con Redis opzionale, lo stato pallet è persistito in un Hash Redis: sistec:{plant}:pallet:{id} con campi state, type, partCount, currentJob. Se l'HMI riavvia, lo stato recupera da Redis invece di ripartire da EMPTY. TTL 24h per cleanup automatico pallet non usati. Il DB MySQL resta source of truth definitiva.
| Aspetto | LAG | FAEL AB | FAEL C |
|---|---|---|---|
| Numero pallet | 9 | 9 | 3 |
| Tipi pallet | standard + oversize | standard | standard |
| QC obbligatorio | solo oversize | mai | mai |
| Stati extra | QUALITY_CHECK, REWORK | — | — |
| Transizioni custom | pallet_cleared → EMPTY | — | — |
Tutto in JSON. Zero if/else su CellType.
| Stack | Sempre presente? | Layer | Dipende da |
|---|---|---|---|
Sistec.Stack.RecipeEngine |
✅ Sempre | Services + UI | Tutti gli stack macchina (via IMachineActionProvider) |
Sistec.Stack.PalletTracking |
✅ Sempre | Services + UI | Sistec.Stack.JobManagement |
Seguono lo stesso pattern: Client? → Driver? → Services → UI → Simulator. Services è il cuore (RecipeEngine, PalletStateMachine). UI fornisce viste di监控/controllo. Simulator permette test offline.
1. Cliente ordina impianto con 4 pallet, controllo qualità obbligatorio
2. Ingegnere copia template cartella commessa
3. Modifica:
- config/pallets/config.json: count: 4, requireQcForType: ["standard", "oversize"]
- config/recipes/*.json: sequenze specifiche
4. Collauda con RecipeEngineSimulator + PalletSimulator
5. Consegna. Zero codice.
Fase 3 (aggiornata): Stack applicativi (3 settimane)
├── Sistec.Stack.Production
├── Sistec.Stack.RecipeEngine ← NUOVO
├── Sistec.Stack.PalletTracking ← NUOVO
└── Sistec.Stack.JobManagement, Maintenance, Alarms
La manutenzione attuale è time-based ("cambia olio ogni 1000 ore", "sostituisci cinghia ogni 6 mesi") — non riflette l'usura reale. Un macchinario fermo 3 mesi si ritrova una manutenzione schedulata inutilmente, mentre uno stressato può rompersi prima del previsto.
La proposta: predictive maintenance basata su modello ML on-device, dove ogni stack macchina pubblica features operative (cicli, torque, temperature, vibrazioni) e il Maintenance.Stack le consuma per stimare la Remaining Useful Life (RUL) di ogni componente.
flowchart LR
subgraph Stacks["STACK MACCHINA → TELEMETRY"]
Kuka["Sistec.Stack.Kuka
pubblica: cicli gripper,
torque giunti, ore moto
via IMachineTelemetryProvider"]
Safan["Sistec.Stack.Safan
pubblica: numero pieghe,
forza cilindri, errori"]
Plc["Sistec.Stack.PLC
pubblica: ore macchina,
temperature motori, vibrazioni"]
end
subgraph Maint["Sistec.Stack.Maintenance (migliorato)"]
Collect["TelemetryCollector
raccoglie features
da ogni stack via provider"]
Feat["FeatureTransformer
aggrega (avg, max, sum)
per finestra temporale"]
ML["OnnxRuntimeEngine
carica modello .onnx
→ RUL per componente"]
Sched["PredictiveScheduler
RUL < threshold →
crea task manutenzione
con priorità"]
Alert["AlertService
notifica operatore
giallo/rosso"]
end
subgraph Config["CONFIGURAZIONE"]
Models["models/*.onnx
modello addestrato
per macchinario-specifico"]
Thresholds["thresholds.json
soglie RUL per
ogni componente
{warning, critical}"]
SchedCfg["schedules.json
manutenzione time-based
di fallback"]
end
Kuka -->|IMachineTelemetryProvider| Collect
Safan -->|IMachineTelemetryProvider| Collect
Plc -->|IMachineTelemetryProvider| Collect
Collect --> Feat --> ML
Config --> ML
Config --> Sched
ML --> Sched
Sched --> Alert
Maint -.->|telemetria aggregata| Cloud["Sistec.Stack.Cloud
opzionale: invia features
a cloud per ri-addestramento"]
{
"components": [
{
"id": "kuka_joint_1",
"displayName": "Giunto KUKA 1",
"machine": "Kuka_0",
"modelFile": "models/kuka_joint_rul.onnx",
"features": [
{ "source": "Kuka_0", "tag": "Joint1.Torque", "aggregate": "avg_last_hour" },
{ "source": "Kuka_0", "tag": "Joint1.Temperature", "aggregate": "max_last_hour" },
{ "source": "Kuka_0", "tag": "Joint1.Cycles", "aggregate": "sum_since_last_maint" }
],
"thresholds": {
"warning": { "rul": 100, "level": "yellow" },
"critical": { "rul": 20, "level": "red" }
},
"fallbackSchedule": { "everyHours": 2000 }
},
{
"id": "safan_cylinder",
"displayName": "Cilindro pressa Safan",
"machine": "Safan_0",
"modelFile": "models/safan_cylinder_rul.onnx",
"features": [
{ "source": "Safan_0", "tag": "Cylinder.Force", "aggregate": "avg" },
{ "source": "Safan_0", "tag": "Cylinder.Cycles", "aggregate": "sum" }
],
"thresholds": {
"warning": { "rul": 500, "level": "yellow" },
"critical": { "rul": 100, "level": "red" }
}
}
]
}
Ogni stack macchina espone un'interfaccia che il Maintenance.Stack consuma:
// Sistec.Core.Interfaces — contratto condiviso
public interface IMachineTelemetryProvider
{
string MachineId { get; }
Task<Dictionary<string, double>> CollectFeaturesAsync(
IEnumerable<string> tagNames, TimeSpan window, CancellationToken ct);
}
// Sistec.Stack.Kuka.Services — implementazione
public class KukaTelemetryProvider : IMachineTelemetryProvider
{
public string MachineId => "Kuka_0";
public async Task<Dictionary<string, double>> CollectFeaturesAsync(
IEnumerable<string> tagNames, TimeSpan window, CancellationToken ct)
{
// Legge dai tag OPC UA / KRC, calcola medie, massimi, somme
return new Dictionary<string, double>
{
["Joint1.Torque"] = await _kukaClient.ReadAverageAsync("Joint1.Torque", window),
["Joint1.Temperature"] = await _kukaClient.ReadMaxAsync("Joint1.Temp", window),
["Joint1.Cycles"] = await _cycleCounter.GetSinceLastMaintenanceAsync(),
};
}
}
1. Addestramento (cloud o PC dev)
├── Raccogliere dati storici: features + timestamp rottura/sostituzione
├── Addestrare modello (scikit-learn, PyTorch, LightGBM)
└── Esportare in ONNX → modello.onnx
2. Deploy
├── Il file .onnx va in config/models/ della commessa
└── OnnxRuntimeEngine lo carica all'avvio
3. Inferenza (sull'HMI, ogni ora)
├── TelemetryCollector raccoglie features da tutti gli stack
├── FeatureTransformer normalizza
├── OnnxRuntimeEngine esegui → RUL per ogni componente
└── PredictiveScheduler: RUL < soglia → crea task manutenzione
4. Miglioramento continuo
├── Le features + l'esito effettivo della manutenzione
│ (rotto/sostituito/sano) tornano indietro via Cloud.Stack
└── Modello ri-addestrato periodicamente con nuovi dati
| Livello | RUL residua | Azione |
|---|---|---|
| Verde | > soglia warning | Normale monitoraggio |
| Giallo | < warning | Notifica operatore, pianificare manutenzione |
| Rosso | < critical | Allarme, fermata programmata |
| Fallback | tempo scaduto | Manutenzione forzata (se ML non disponibile) |
| Aspetto | Oggi (time-based) | Domani (predictive ML) |
|---|---|---|
| Cambio olio KUKA | Ogni 2000 ore → anche se fermo 3 mesi | Quando il modello vede degradazione lubrificante |
| Cilindro pressa | Ogni 6 mesi → anche se ha fatto 100 cicli | Quando cicli + forza indicano usura reale |
| Manutenzione inutile | Fatta comunque (spreco ricambi + ore) | Solo quando serve |
| Rottura imprevista | Frequente (il timer non basta) | Rara (RUL avvisa con 100+ ore di anticipo) |
| Dati per migliorare | Nessuno | Storico features + esiti → modello sempre migliore |
Sistec.Stack.Maintenance si arricchisce: aggiunge PredictiveEngine, OnnxRuntimeEngine, TelemetryCollector. Resta un pacchetto NuGet sempre presente.IMachineTelemetryProvider — zero cambiamenti alla logica esistente, si aggiunge solo un provider..onnx sono dati di configurazione, non codice. Vanno in config/models/ della commessa.Il Digital Twin è il trend #1 dell'HMI industriale 2026: una replica 3D real-time della macchina che mostra stato, movimenti e anomalie in tempo reale. Non è un render decorativo — è uno strumento operativo che riduce i tempi di diagnostica del 40-60%.
Vista Digital Twin nell'HMI:
┌─────────────────────────────────────────────┐
│ [3D Viewport — KUKA Robot + Pressa] │
│ │
│ ╔══╗ │
│ ║K1║───🤖 KUKA KR210 │
│ ║ ║ ● Online | Programma: P123 │
│ ║ ║ 🔴 Joint 3 overload (allarme) │
│ ║S ║ │
│ ║a ║───🏭 Pressa Safan │
│ ║f ║ ● Running | Ciclo: 38/120 │
│ ║a ║ │
│ ║n║───📦 Pallet 1: EMPTY │
│ ╚══╝ Pallet 2: FULL (QC pending) │
│ │
│ [Stati] 🟢 Running ⚠️ Warning 🔴 Fault │
└─────────────────────────────────────────────┘
| Livello | Descrizione | Tecnologia | Sforzo |
|---|---|---|---|
| L1 — Schematico 2.5D | Vista dall'alto con icone 3D semplici. Ogni macchina è un rettangolo con stato colore. | Avalonia Canvas + SVG | 2-3 giorni |
| L2 — Low-poly 3D | Modello 3D semplificato (200-500 poligoni) con colori di stato. Movimenti animati (robot che ruota, pressa che scende). | SkiaSharp 3D / Avalonia 3D | 1-2 settimane |
| L3 — Photorealistic | Modello 3D dettagliato dal CAD OEM con texture reali. Richiede GPU. | Unity Embedded / Unreal Engine | ❌ Troppo pesante per PC industriale |
Target: Livello 1 (minimo) + Livello 2 (obiettivo) per prima commessa greenfield.
Il Digital Twin non è solo estetico — è il cruscotto visivo della predictive maintenance (§9.6):
// Maintenance.Stack → DigitalTwinService
public class DigitalTwinService
{
// Colora componenti in base alla RUL predetta
public RulColor GetComponentColor(string componentId)
{
var rul = _predictiveEngine.GetRul(componentId);
return rul switch
{
> 500 => RulColor.Green, // Normale
> 100 => RulColor.Yellow, // Attenzione
> 20 => RulColor.Orange, // Pianificare manutenzione
_ => RulColor.Red // Sostituire immediatamente
};
}
}
flowchart LR
subgraph Stacks["STACK MACCHINA"]
Kuka["Sistec.Stack.Kuka
→ posizione giunti, stato"]
Safan["Sistec.Stack.Safan
→ corsa cilindro, forza"]
Plc["Sistec.Stack.PLC
→ sensori, allarmi"]
end
subgraph DT["Sistec.Stack.DigitalTwin (NUOVO)"]
DT_State["StateCollector
Aggrega stato da tutti gli stack"]
DT_3D["SceneRenderer
SkiaSharp 3D / Avalonia 3D
→ viewport interattivo"]
DT_Anim["AnimationEngine
Movimenti basati su dati reali"]
end
subgraph UI["UI Avalonia"]
DT_View["DigitalTwinView
Control registrato in ControlRegistry
layout.json: fullscreen o card"]
end
Kuka --> DT_State
Safan --> DT_State
Plc --> DT_State
DT_State --> DT_3D
DT_3D --> DT_Anim
DT_Anim --> DT_View
style DT fill:#e3f2fd
| Layer | Progetto | Contenuto |
|---|---|---|
| Services | Sistec.Stack.DigitalTwin.Services |
StateCollector (polling ogni 100ms), SceneGraph (albero gerarchico macchina), AnimationEngine |
| UI | Sistec.Stack.DigitalTwin.UI |
DigitalTwinView (Avalonia control), CameraController (pan/zoom/rotate) |
| Model | Sistec.Stack.DigitalTwin.Models |
MachineModel, JointState, ComponentColor, AnimationKeyframe |
{
"id": "digital-twin",
"title": "Digital Twin",
"type": "page",
"control": "DigitalTwin.Views.DigitalTwinView",
"binding": {
"machines": ["Kuka_0", "Safan_0", "PLC_0"],
"refreshMs": 200,
"camera": { "initialZoom": 1.0, "allowRotate": true }
}
}
Fase 3 (nuova): Stack applicativi (4-5 settimane)
├── Sistec.Stack.Production
├── Sistec.Stack.RecipeEngine
├── Sistec.Stack.PalletTracking
├── Sistec.Stack.Maintenance (predictive ML)
├── Sistec.Stack.DigitalTwin (Livello 1-2) ← NUOVO
└── Sistec.Stack.JobManagement, Alarms
Il Digital Twin è un differenziatore competitivo — nessun competitor Sistec (PMI italiana) lo offre. Per la prima commessa greenfield, il Livello 1 (schematico 2.5D) è sufficiente per dimostrare il concept.
Fase 3 (aggiornata): Stack applicativi (3-4 settimane)
├── Sistec.Stack.Production
├── Sistec.Stack.RecipeEngine
├── Sistec.Stack.PalletTracking
├── Sistec.Stack.Maintenance (predictive ML + Digital Twin) ← MIGLIORATO
└── Sistec.Stack.JobManagement, Alarms
Il logger statico Utilities.Logger è esattamente lo stesso anti-pattern di Configuration.PlcConfig[...] — service locator globale, impossibile da mockare, sostituire o configurare per contesto.
// ANTI-PATTERN: service locator statico
Utilities.Logger?.Debug($"{log.Context}: {log.Message}");
// → plain text file, zero struttura, zero ricercabilità
// → impossibile testare (mockare un campo statico è un incubo)
// → thread safety? affidato a Serilog internamente, ma zero controllo
// Tre astrazioni diverse per la stessa cosa:
Utilities.Logger?.Debug("..."); // Serilog diretto
ISistecLogger<LogItem>.Log("..."); // custom wrapper
Globals_Standard.Log("..."); // delegazione UI
// Nel Composition Root (Program.cs)
builder.Host.UseSerilog((ctx, cfg) =>
cfg.ReadFrom.Configuration(ctx.Configuration)
.Enrich.WithMachineName()
.Enrich.WithProperty("Application", "Sistec.5315.LAG")
.WriteTo.Console()
.WriteTo.File(
path: "Logs/sistec-.json",
rollingInterval: RollingInterval.Day,
formatter: new Serilog.Formatting.Json.JsonFormatter()) // JSON!
.WriteTo.Debug());
// In ogni classe: costruttore, non statico
public class KukaRobotLogic : IKukaRobotLogic
{
private readonly ILogger<KukaRobotLogic> _logger;
public KukaRobotLogic(..., ILogger<KukaRobotLogic> logger)
{
_logger = logger; // ← iniettato dal container DI
}
public async Task LoadPanelAsync(string gripper)
{
_logger.LogInformation("Loading panel with {Gripper}", gripper);
// ^ named placeholder, non $"..."!
}
}
Output JSON:
{
"@t": "2026-07-02T14:30:00.123Z",
"@l": "Information",
"@mt": "Loading panel with {Gripper}",
"Gripper": "vacuum_a",
"MachineName": "SPV-LAG-01",
"Application": "Sistec.5315.LAG"
}
Vantaggi del JSON strutturato:
jq 'select(.Gripper == "vacuum_a")' o Elastic/Kibana{Gripper} = "vacuum_a" && {Temperature} > 80 → Slack/PagerDuty| Aspetto | Oggi | Domani |
|---|---|---|
| Accesso logger | Utilities.Logger?.Debug(...) statico |
ILogger<T> via costruttore DI |
| Formato output | Plain text SPV_5315_20260702.log |
JSON sistec-20260702.json |
| Placeholder | $"{ctx}: {msg}" (persi in plain text) |
"{Context}: {Message}" (estratti in JSON) |
| Tre astrazioni | Utilities.Logger + ISistecLogger + Globals_Standard | Una sola: Microsoft.Extensions.Logging.ILogger<T> |
| log4net (Sistec.Bus) | Framework diverso | Migrato a Serilog come tutto il resto |
| Debug.Print (~150x) | Zero in Release build | Convertito in LogTrace() / LogDebug() |
| Mockabilità | Impossibile (statico) | Mock.Of<ILogger<KukaRobotLogic>>() |
| Config | Hardcoded in Utilities.CreateLogger(path) |
appsettings.json + IOptions<LoggerFilterOptions> |
| Testabilità | I log restano nei file di produzione | Il DI container inietta logger diversi per test |
Non serve uno stack separato. Si risolve in Fase 0b (Fondazione):
Fase 0b (aggiornata): Fondazione (3-4 settimane)
├── DI Container + Configuration
├── Sostituire Utilities.Logger con ILogger<T> via DI
├── Eliminare ISistecLogger<LogItem> e Globals_Standard.Log()
├── Migrare log4net (Sistec.Bus) a Serilog
├── Convertire Debug.Print() in ILogger.LogTrace()
├── Configurare output JSON + rolling file
├── Persistence multi-progetto
└── CodeGen prototipo
La migrazione non è blocante: si può fare gradualmente. Un nuovo stack (es. Kuka.Stack) nasce già con ILogger<T> via DI. I vecchi file continuano a usare lo statico finché non vengono refactorati.
Ogni stack verticale diventa un pacchetto NuGet pubblicato su un feed interno (NuGet.Server / GitHub Packages / Azure Artifacts). La commessa diventa un progetto CSProj minimale che referenzia solo i pacchetti necessari.
I pacchetti si dividono in due categorie: sempre presenti (platform) e per commessa (macchinario).
flowchart TB
subgraph Always["✅ SEMPRE PRESENTI (Platform)"]
Core["Sistec.Core
netstandard2.1"]
Controls["Sistec.Controls
net8.0-windows"]
SistecUI["Sistec.UI
net8.0-windows"]
LayoutEngine["Sistec.LayoutEngine
net8.0-windows"]
ConfigLib["Sistec.Configuration
netstandard2.1"]
Persistence["Sistec.Infra.Persistence*
netstandard2.1"]
Plc["Sistec.Stack.PLC
net8.0-windows"]
Alarms["Sistec.Stack.Alarms
net8.0-windows"]
Maintenance["Sistec.Stack.Maintenance
net8.0-windows"]
JobMgmt["Sistec.Stack.JobManagement
net8.0-windows"]
Production["Sistec.Stack.Production
net8.0-windows"]
end
subgraph Optional["🔧 PER COMMESSA (Machine)"]
Kuka["Sistec.Stack.Kuka"]
Fanuc["Sistec.Stack.Fanuc
(futuro)"]
OtherRobots["⋯ altro robot"]
Safan["Sistec.Stack.Safan"]
Gade["Sistec.Stack.Gade
(futuro)"]
Esa["Sistec.Stack.Esa"]
OtherPress["⋯ altra pressa"]
Sinumerik["Sistec.Stack.Sinumerik"]
OpcUaLib["Sistec.Library.OpcUa"]
ModbusLib["Sistec.Library.Modbus"]
BusLib["Sistec.Library.Bus"]
end
classDef always fill:#e1f5e1,stroke:#2e7d32
classDef optional fill:#fff8e1,stroke:#f57f17
class Core,Controls,SistecUI,LayoutEngine,ConfigLib,Persistence always
class Plc,Alarms,Maintenance,JobMgmt,Production always
class Kuka,Fanuc,OtherRobots,Safan,Gade,Esa,OtherPress,Sinumerik,OpcUaLib,ModbusLib,BusLib optional
| Categoria | Pacchetto | Sempre presente? | Note |
|---|---|---|---|
| Platform | Sistec.Core |
✅ Sempre | Fondamenta di tutto |
Sistec.Controls |
✅ Sempre | Controlli WinForms generici | |
Sistec.UI |
✅ Sempre | Layout menu, pagine base | |
Sistec.LayoutEngine |
✅ Sempre | Motore layout da JSON | |
Sistec.Configuration |
✅ Sempre | Options pattern | |
Sistec.Infra.Persistence.* |
✅ Sempre | DB access | |
Sistec.Stack.PLC |
✅ Sempre | Ogni impianto ha un PLC | |
Sistec.Stack.Alarms |
✅ Sempre | Allarmi e notifiche | |
Sistec.Stack.Maintenance |
✅ Sempre | Manutenzione | |
Sistec.Stack.JobManagement |
✅ Sempre | Job lifecycle, ricette | |
Sistec.Stack.Production |
✅ Sempre | Orchestrazione produzione | |
| Machine | Sistec.Stack.Kuka |
❌ Se robot KUKA | TCP KRC |
Sistec.Stack.Fanuc |
❌ Se robot Fanuc | Futuro | |
Sistec.Stack.Safan |
❌ Se pressa Safan | TCP | |
Sistec.Stack.Gade |
❌ Se pressa Gade | Futuro | |
Sistec.Stack.Esa |
❌ Se pressa ESA | Modbus | |
Sistec.Stack.Sinumerik |
❌ Se CNC Siemens | OPC UA | |
| Lib | Sistec.Library.OpcUa |
❌ Solo se serve OPC UA | Dipende dai macchinari |
Sistec.Library.Modbus |
❌ Solo se serve Modbus | Dipende dai macchinari | |
Sistec.Library.Bus |
❌ Solo se serve Zebus | Dipende dall'impianto |
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- 7 riferimenti a progetto, non pacchetti -->
<ProjectReference Include="..\Sistec.Core\Sistec.Core.csproj" />
<ProjectReference Include="..\Sistec.Controls\Sistec.Controls.csproj" />
<ProjectReference Include="..\Sistec.UI\Sistec.UI.csproj" />
<ProjectReference Include="..\Sistec.Opc.Ua\Sistec.Opc.Ua.csproj" />
<ProjectReference Include="..\Sistec.Safan\Source\Sistec.Safan.csproj" />
<ProjectReference Include="..\Sistec.Sinumerik\Sistec.Sinumerik.csproj" />
<ProjectReference Include="..\..\Common\Sistec.Common.csproj" />
</ItemGroup>
</Project>
dotnet new sistec-hmi<!-- Template base: sempre gli stessi, ogni commessa li ha -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<AssemblyName>Sistec.$(Commessa)</AssemblyName>
</PropertyGroup>
<ItemGroup>
<!-- Platform — sempre presenti -->
<PackageReference Include="Sistec.Core" Version="4.3.*" />
<PackageReference Include="Sistec.Controls" Version="4.3.*" />
<PackageReference Include="Sistec.UI" Version="4.3.*" />
<PackageReference Include="Sistec.LayoutEngine" Version="1.0.*" />
<PackageReference Include="Sistec.Configuration" Version="1.0.*" />
<PackageReference Include="Sistec.Infra.Persistence" Version="1.0.*" />
<PackageReference Include="Sistec.Stack.PLC" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Alarms" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Maintenance" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.JobManagement" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Production" Version="2.1.*" />
<!-- 🔧 Qui l'ingegnere aggiunge i pacchetti macchinario -->
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<AssemblyName>Sistec.5315.LAG</AssemblyName>
</PropertyGroup>
<ItemGroup>
<!-- Platform (sempre, dal template) -->
<PackageReference Include="Sistec.Core" Version="4.3.*" />
<PackageReference Include="Sistec.Controls" Version="4.3.*" />
<PackageReference Include="Sistec.UI" Version="4.3.*" />
<PackageReference Include="Sistec.LayoutEngine" Version="1.0.*" />
<PackageReference Include="Sistec.Configuration" Version="1.0.*" />
<PackageReference Include="Sistec.Infra.Persistence" Version="1.0.*" />
<PackageReference Include="Sistec.Stack.PLC" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Alarms" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Maintenance" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.JobManagement" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Production" Version="2.1.*" />
<!-- Machine (aggiunti per LAG) -->
<PackageReference Include="Sistec.Stack.Kuka" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Safan" Version="2.1.*" />
<PackageReference Include="Sistec.Stack.Sinumerik" Version="2.1.*" />
<PackageReference Include="Sistec.Library.OpcUa" Version="2.1.*" />
</ItemGroup>
<ItemGroup>
<!-- Soli file di configurazione e risorse -->
<Content Include="manifest.json" CopyToOutputDirectory="Always" />
<Content Include="layout.json" CopyToOutputDirectory="Always" />
<Content Include="config\**\*.json" CopyToOutputDirectory="Always" />
<Content Include="resources\**\*" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<AssemblyName>Sistec.5309.C</AssemblyName>
</PropertyGroup>
<ItemGroup>
<!-- Platform (sempre, dal template) -->
<!-- ... identico al template ... -->
<!-- Machine (aggiunti per FAEL C) -->
<PackageReference Include="Sistec.Stack.Kuka" Version="2.1.*" />
<PackageReference Include="Sistec.Library.OpcUa" Version="2.1.*" />
<!-- Nota: senza Safan.Stack, senza Esa.Stack, senza Sinumerik.Stack -->
<!-- Non serve Modbus.Library (nessuna pressa) -->
</ItemGroup>
</Project>
Tutti i pacchetti di uno stack verticale condividono la stessa versione (es. Sistec.Stack.Kuka.Client 2.1.0, Sistec.Stack.Kuka.Driver 2.1.0, Sistec.Stack.Kuka.Services 2.1.0, ecc.). Questo garantisce che i layer siano compatibili.
<!-- Directory.Build.props nel repo dello stack -->
<Project>
<PropertyGroup>
<VersionPrefix>2.1.0</VersionPrefix>
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
<Authors>Sistec AM</Authors>
<PackageProjectUrl>https://sistec.it</PackageProjectUrl>
<RepositoryType>git</RepositoryType>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
</Project>
flowchart LR
A["📋 Contratto
commessa"] --> B["🏗️ Ingegnere
crea progetto da template
`dotnet new sistec-hmi`"]
B --> C["Il template include già
tutti i pacchetti platform
(PLC, Alarms, JobMgmt,
Production, Maintenance)"]
C --> D["📦 Aggiunge solo
pacchetti macchinario
`dotnet add package Sistec.Stack.Kuka`"]
D --> E["⚙️ Compila manifest.json
layout.json config/"]
E --> F["🧪 Collaudo
`dotnet run`"]
F -->|OK| G["📦 Pubblica
`dotnet publish`
→ EXE + DLL + JSON"]
F -->|Ko| H["Corregge solo
JSON/INI/immagini"]
H --> E
subgraph CI["CI/CD Pipeline"]
I["Repo Fondazione
→ build → test →
publish NuGet (platform)"]
J["Repo Stack Macchina
→ build → test →
publish NuGet (machine)"]
end
I -.->|NuGet platform| C
J -.->|NuGet machine| D
| Ruolo | Oggi | Domani |
|---|---|---|---|
| Sviluppatore di stack (es. Kuka) | Scrive codice frammentato in 5 progetti, impact analysis su tutto | Scrive codice dentro un unico repo stack, test NUnit, publish NuGet |
| Ingegnere di commessa | Fork di soluzione, copia file, modifica codice C#, rischi regressione | dotnet new sistec-hmi (ha già tutto il platform), aggiunge 2-3 pacchetti macchina, scrive JSON |
| Messa in servizio | Build da sorgente, dipende dal dev che ha scritto quella commessa | dotnet publish con pacchetti già testati, rollback = cambiare versione NuGet |
| Bug fixing | Apri soluzione commessa, cerca in 5 progetti, fix, rebuild tutto | Fix nel repo dello stack, nuova versione NuGet, commessa aggiorna versione |
| Nuovo macchinario | Scrittura codice sparsa, difficile da standardizzare | Nuovo repo stack verticale, primo pacchetto NuGet, poi configurabile |
| Allineamento versioni | Ogni commessa ha versioni diverse degli stessi file | Platform versionato centralmente, tutte le commesse sulla stessa base |
Non serve convertire tutto in una volta. Si procede per fasi, partendo da ciò che è sempre uguale:
Fase 1: Platform NuGet (sempre presenti)
├── Estrarre in repo separati: Sistec.Core, Sistec.Controls, Sistec.UI
├── Estrarre: Sistec.Stack.PLC, Sistec.Stack.Alarms, Sistec.Stack.Maintenance
├── Estrarre: Sistec.Stack.JobManagement, Sistec.Stack.Production
├── CI/CD per ognuno: build → test NUnit → publish NuGet
├── Template `dotnet new sistec-hmi` con TUTTI questi pacchetti già inclusi
└── LAG e FAEL iniziano a referenziarli via NuGet
Fase 2: Machine NuGet (per commessa)
├── Sistec.Stack.Kuka come pilota
├── Estrarre dal monolite in repo separato
├── LAG e FAEL aggiungono solo Kuka.Stack → primo stack condiviso
├── Poi Safan.Stack, Esa.Stack, Sinumerik.Stack...
└── Nuovi macchinari (Fanuc, Gade, ...) nascono già come NuGet
Fase 3: Smantellamento Common
├── Tutti gli stack sono NuGet
├── Common non contiene più nulla di vivo
├── Eliminare progetto Common
└── Correggere namespace
Fase 4: Layout Engine
├── Sistec.LayoutEngine come NuGet platform
├── layout.json sostituisce le pagine WinForms custom
└── Nuova commessa = solo JSON + immagini
Vantaggio chiave: già dalla Fase 1 tutte le commesse condividono la stessa base platform via NuGet. Non si aspetta la fine del refactoring per vedere benefici.
milestons/4.5 vs FAEL mileston/v3.25)Oggi il versioning è di fatto assente: lo stesso Sistec.Core esiste in due branch divergenti e incompatibili. È il sintomo più evidente dell'assenza di un modello di distribuzione.
flowchart LR
subgraph Oggi["OGGI — branch divergenti"]
direction TB
LAG["LAG: milestons/4.5
Core versione 4.5.*"]
FAEL["FAEL: mileston/v3.25
Core versione 3.25.*"]
LAG -.->|API incompatibili| FAEL
end
subgraph Domani["DOMANI — NuGet unificato"]
direction TB
FEED["Feed NuGet Sistec
Una sola Sistec.Core nuget"]
LAGv2["LAG: PackageReference
Sistec.Core 5.0.0"]
FAELv2["FAEL: PackageReference
Sistec.Core 5.0.0"]
end
Oggi -->|"Passaggio obbligato: riconciliazione"| Domani
Il modello NuGet risolve il problema a regime (una volta pubblicato, non si può più divergere), ma richiede un passaggio intermedio di riconciliazione dei due branch.
Prima di pubblicare qualsiasi pacchetto NuGet, i due branch devono essere riconciliati in un unico main. Non esistono scorciatoie.
| Opzione | Sforzo | Rischio | Descrizione |
|---|---|---|---|
| Merge manuale | Alto (2-4 settimane) | Medio | Prendere i due branch, fare diff file per file, scegliere la versione più recente o fondere |
| Adapter/Shim | Medio (1-2 settimane) | Basso | Mantenere entrambe le API, una che wrappa l'altra. Utile se le differenze sono poche |
| Big-Bang su Kuka.Stack pilota | Basso (1 settimana) | Alto | Ignorare il problema, estrarre Kuka.Stack da LAG (il più recente) e forzare FAEL ad adattarsi |
| Standardizzare su LAG | Medio | Medio | LAG è più recente (v4.5 vs v3.25) e ha già test NUnit. FAEL si adatta |
| Standardizzare su FAEL | Medio-Alto | Alto | FAEL ha 3 varianti e più progetti, ma Core è più vecchio |
Raccomandazione: Combinare le opzioni — standardizzare su LAG (più recente, già con NUnit su net10.0, architettura più pulita) e usare una finestra di compatibilità dove FAEL wrappa le differenze minori con adapter temporanei.
| Aspetto | Oggi (branch divergenti) | Domani (NuGet) |
|---|---|---|
| Quante versioni di Core esistono? | 2+ (milestons/4.5, mileston/v3.25) | 1 (feed NuGet, versioni semantiche) |
| Una commessa nuova che versione usa? | Dipende da quale branch forkare | L'ultima stabile sul feed |
| Bug fix in Core | Applicato a un solo branch, l'altro resta indietro | Pubblicato su feed, tutte le commesse aggiornano quando vogliono |
| API breaking change | Silenzioso, ogni branch evolve da sé | major version bump semantico, tutte le commesse vedono la breaking change |
| Rollback | Impossibile (git revert caotico) | dotnet add package Sistec.Core --version 4.3.5 |
| Chi decide la versione? | Nessuno (ogni repo fa da sé) | CI/CD pipeline → NuGet feed → tutte le commesse |
La riconciliazione dei branch è un prerequisito della Fase 1. Senza, non si può pubblicare nulla su NuGet:
Fase 0: Riconciliazione (2-4 settimane) ← NUOVA
├── Analizzare diff tra milestons/4.5 (LAG) e mileston/v3.25 (FAEL)
├── Scegliere base comune (raccomandato: LAG, più recente)
├── Creare adapter temporanei per API divergenti in FAEL
├── Verificare che entrambe le commesse compilino sulla stessa base
└── Git: unificare in unico main/master
Fase 1: Platform NuGet (dopo riconciliazione)
├── Ora ha senso: si parte da un'unica base condivisa
└── Template `dotnet new sistec-hmi` usa la base riconciliata
In sintesi: sì, il modello NuGet risolve il problema del versioning divergente a regime. Ma prima va fatto un lavoro di riconciliazione tra i due branch, altrimenti si pubblicherebbe su NuGet una delle due versioni (e l'altra diventerebbe inutilizzabile). La buona notizia: LAG è più recente (v4.5 vs v3.25) e il divario è colmabile con adapter temporanei.
WinForms è una tecnologia di 22 anni (.NET Framework 1.0, 2003). Anche WPF ha 18 anni. L'architettura a stack verticali proposta separa già la UI (Sistec.<Nome>.Stack.UI), per cui il passo successivo naturale è sostituire WinForms con un framework moderno — senza dover riscrivere i layer Client, Driver e Services.
| Problema | Impatto |
|---|---|
| DPI scaling | Gestione manuale, problemi su schermi 4K/industriali |
| Touch input | Nessun supporto nativo a gesture, multitouch |
| Theming/stili | Assente, ogni controllo va dipinto a mano |
| Data binding | Legacy, no {Binding} dichiarativo, tutto in code-behind |
| Threading | InvokeRequired / SafeInvoke pattern anti-ergonomico |
| Linux | Non supportato |
| Sviluppo attivo Microsoft | Solo bug fix, nessuna nuova feature dal 2020 |
| Framework | Windows | Linux | macOS | Maturità industriale | Note |
|---|---|---|---|---|---|
| WinUI 3 | ✅ Nativo | ❌ | ❌ | Media (stabile da WinAppSDK 1.5) | Moderno, ma ancora Windows-only |
| Avalonia UI | ✅ | ✅ | ✅ | Alta (v11, usato in ambito industriale) | Raccomandato per cross-platform |
| .NET MAUI | ✅ | ⚠️ Community | ✅ | Media (v8, ma Linux acerbo) | Nato per mobile, Linux non ufficiale |
| Uno Platform | ✅ (WinUI) | ✅ | ✅ | Media | WinUI API identica su tutti i target |
Raccomandazione: Avalonia UI — è il framework .NET cross-platform più maturo per applicazioni desktop industriali:
La separazione UI (layer Stack.UI WinForms) già prevista nell'architettura a stack verticali rende la migrazione incrementale e isolata:
flowchart LR
subgraph Oggi["OGGI — WinForms"]
UI_WF["Sistec.Stack.Kuka.UI
(UserControl WinForms)"]
SVC["Sistec.Stack.Kuka.Services
(logica pura)"]
DRV["Sistec.Stack.Kuka.Driver
(tag provider)"]
CLI["Sistec.Stack.Kuka.Client
(TCP KRC)"]
end
subgraph Transizione["TRANSIZIONE — side-by-side"]
UI_WF2["Sistec.Stack.Kuka.UI.WinForms
(vecchio, legacy)"]
UI_AV["Sistec.Stack.Kuka.UI.Avalonia
(nuovo, moderno)"]
SVC2["Sistec.Stack.Kuka.Services
(identico, nessuna modifica)"]
end
subgraph Domani["DOMANI — Avalonia"]
UI_AV2["Sistec.Stack.Kuka.UI
(solo Avalonia, WinForms rimosso)"]
SVC3["Sistec.Stack.Kuka.Services
(identico)"]
end
Oggi --> Transizione
Transizione --> Domani
Punti chiave:
LayoutEngine (sezione 8) diventa ancora più potente — i controlli registrati nel ControlRegistry vengono da Avalonia invece che da WinForms// OGGI — WinForms
public class ucKukaInfo : UserControl
{
private Label _statusLabel;
public ucKukaInfo()
{
_statusLabel = new Label { Location = new Point(10, 10) };
Controls.Add(_statusLabel);
}
public void UpdateStatus(string status)
{
if (InvokeRequired) { Invoke(() => _statusLabel.Text = status); return; }
_statusLabel.Text = status;
}
}
// DOMANI — Avalonia
public class KukaInfoView : UserControl
{
// Binding dichiarativo, niente code-behind
// Niente InvokeRequired (Avalonia gestisce il thread)
}
<!-- DOMANI — Avalonia XAML -->
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:vm="clr-namespace:Sistec.Stack.Kuka.ViewModels">
<StackPanel>
<TextBlock Text="{Binding RobotName}" FontSize="18" />
<TextBlock Text="{Binding Status}"
Foreground="{Binding StatusColor}" />
<Button Command="{Binding StartProgramCommand}"
Content="Avvia Programma" />
</StackPanel>
</UserControl>
Fase 0: Setup (1 settimana)
├── Aggiungere Avalonia al solution (progetti esistenti non toccati)
├── Creare Sistec.Core.UI.Abstraction — interfacce UI neutre
│ (es. IViewProvider, IDialogService, INavigationService)
└── Template progetto Avalonia + WinForms side-by-side
Fase 1: Kuka.Stack.UI pilota (2-3 settimane)
├── Riscrivere ucKukaInfo, KukaOverride in Avalonia
├── ViewModel per ogni view (logica UI, non di dominio)
├── Testare side-by-side con WinForms
└── Rilasciare Sistec.Stack.Kuka.UI.Avalonia come pacchetto NuGet
Fase 2: Altri stack (2-3 settimane per stack)
├── Safan.Stack.UI → Avalonia
├── PLC.Stack.UI → Avalonia
├── Sinumerik.Stack.UI → Avalonia
└── Production.Stack.UI → Avalonia
Fase 3: LayoutEngine su Avalonia (1-2 settimane)
├── ControlRegistry ora emette controlli Avalonia
├── layout.json funziona identico (cambia solo il renderer)
└── layout.json può includere binding Avalonia-style
Fase 4: Rimozione WinForms (1 settimana)
├── Verificare che tutti gli stack siano migrati
├── Eliminare i progetti Stack.UI.WinForms
├── Eliminare Sistec.Controls (sostituito da controlli Avalonia nativi)
└── Eliminare dipendenza da System.Windows.Forms
| Rischio | Probabilità | Mitigazione |
|---|---|---|
| Performance Avalonia su Linux embedded | Bassa | Test su HW target prima della migrazione. Avalonia ha rendering GPU via Skia. |
| Binding complessi (OPC UA in tempo reale) | Media | ViewModel con INotifyPropertyChanged standard, ReactiveUI per stream asincroni |
| Terze parti WinForms non migrabili | Media | WebView + HTML per controlli legacy, oppure wrapper interop finestra |
| Resistenza del team ("tanto funziona") | Alta | Dimostrazione con Kuka.Stack pilota: stesso codice sottostante, UI moderna, touch funzionante |
| Avalonia EOL / abbandono | Bassa | Open source, community attiva. In caso estremo, migrazione a Uno Platform (stessa sintassi XAML) |
gantt
title Roadmap Modernizzazione UI
dateFormat YYYY-MM-DD
axisFormat %d/%m
section Fase UI 0 — Setup
Setup Avalonia + side-by-side template :u0a, 2026-10-01, 5d
Interfacce UI astratte :u0b, after u0a, 3d
section Fase UI 1 — Pilota Kuka
Kuka.Stack.UI in Avalonia :u1a, after u0b, 10d
ViewModel layer + test :u1b, after u1a, 5d
Rilascio NuGet Kuka.Stack.UI.Avalonia :u1c, after u1b, 2d
section Fase UI 2 — Migrazione stack
Safan.Stack.UI → Avalonia :u2a, after u1c, 10d
PLC.Stack.UI → Avalonia :u2b, after u2a, 10d
Production.Stack.UI → Avalonia :u2c, after u2b, 10d
Alarms + Maintenance + Job UI → Avalonia :u2d, after u2c, 10d
section Fase UI 3 — Layout Engine
LayoutEngine su Avalonia :u3a, after u2d, 7d
layout.json con binding Avalonia :u3b, after u3a, 5d
section Fase UI 4 — Cleanup
Rimuovere WinForms e Sistec.Controls :u4a, after u3b, 5d
Avalonia UI può compilare il target WebAssembly (WASM), consentendo all'HMI di girare in un browser senza installazione. Questo abilita scenari che l'EXE nativo non copre.
flowchart LR
subgraph Native["EXE NATIVO (Pannello Industriale)"]
EXE["Sistec.HMI.exe
Avalonia su Windows/Linux
→ Controllo macchine"]
end
subgraph Browser["WEBASSEMBLY (Browser)"]
WASM1["PC Ufficio Tecnico
sistec-hmi.local:5000
→ Monitoraggio + parametri"]
WASM2["Tablet Capoturno
→ Stato produzione"]
WASM3["TV Reparto
→ KPI fullscreen"]
end
EXE -->|REST API localhost| WASM1
EXE -->|REST API localhost| WASM2
EXE -->|REST API localhost| WASM3
| Scenario | Nativo (EXE) | WASM (Browser) |
|---|---|---|
| Pannello industriale | ✅ Macchine, robot, PLC | ❌ Troppo lento per real-time |
| Ufficio tecnico | ❌ Deve installare | ✅ Apre browser, monitora parametri |
| Capoturno con tablet | ❌ Solo Windows | ✅ Funziona su iPad/Android |
| TV reparto con KPI | ❌ PC dedicato | ✅ Schermo HDMI + browser fullscreen |
| Cliente remoto | ❌ VPN + installazione | ✅ Link HTTPS, zero installazione |
La versione WASM è read-only e non sostituisce l'EXE nativo:
EXE nativo:
├── Controlla macchine (KUKA, PLC, pressa)
├── Scrive tag OPC UA
├── Esegue produzione
└── Gestisce aggiornamenti automatici
WASM browser:
├── Legge stato da API REST
├── Mostra KPI, trend, allarmi
├── Modifica parametri non critici (con autorizzazione)
└── Mai comandi diretti alle macchine
<!-- Sistec.HMI.Shell — target multiplo -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0-windows;net10.0-browserwasm</TargetFrameworks>
</PropertyGroup>
</Project>
La compilazione WASM produce file statici (.wasm, .js, .html) servibili da Nginx o dalla REST API dell'HMI nativo su localhost:5000/wasm/.
| Vantaggio | Dettaglio |
|---|---|
| Zero installazione | Apri browser, carica HMI |
| Cross-device | PC, tablet, smartphone — qualsiasi OS |
| Stesso codice | Stessa UI Avalonia, stessi ViewModel, binding identici |
| Update istantaneo | Ricarica browser = nuova versione (nessun UpdateAgent) |
| Integrazione dashboard §19.12 | La dashboard React e l'HMI WASM condividono le stesse API REST |
| Rischio | Mitigazione |
|---|---|
| Performance | WASM più lento di nativo (Skia su canvas). Non adatto per animazioni real-time o polling OPC UA frequente |
| Accesso hardware | No seriale, no badge RFID, no OPC UA client nativo. Solo HTTP/WebSocket |
| Dimensione download | WASM + .NET runtime = ~5MB iniziali. Accettabile su LAN industriale |
Fase UI 5 — WebAssembly (2-3 settimane, dopo Fase UI 3)
├── Configurare target net10.0-browserwasm
├── Estrarre API REST comune (riusa §19.2)
├── Adattare UI per schermo touch browser (da §21.6)
├── Deploy WASM come file statici serviti dall'HMI nativo
└── Test su tablet + TV reparto
L'attuale sistema utenti è rudimentale: solo 4 livelli gerarchici (Operator < Maintenance < Expert < Sistec) memorizzati in una tabella accounts con password in chiaro, nessun legame con dipendenti reali, nessuna statistica di produzione per operatore. Per i clienti moderni serve un sistema che tracci chi produce cosa e permetta report di produttività per dipendente.
flowchart LR
subgraph AUTH["Autenticazione"]
RFID["Badge RFID
Lettore USB seriale"]
PIN["PIN rapido
4-6 cifre"]
SEL["Selezione da lista
touch screen"]
end
subgraph DB["Database"]
EMP["employees
BadgeId | Nome | Turno | RoleId | PinHash"]
ROL["roles
Permission flags"]
LOG["production_log
OperatorId | JobId | Timestamp | EventType"]
end
subgraph UI["UI"]
LOGIN["LoginPage
Pick operatore + PIN"]
DASH["DashboardStat
Pezzi/h, scarti %, tempi ciclo"]
ADMIN["AdminPage
Gestione dipendenti e ruoli"]
end
AUTH --> LOGIN
LOGIN --> EMP
ROL -->|"CanEditParameters"| UI
EMP -->|"FK"| LOG
LOG --> DASH
-- Ruoli con permessi granulari (bit flags)
CREATE TABLE roles (
Id INT PRIMARY KEY AUTO_INCREMENT,
Name VARCHAR(50) NOT NULL, -- "Supervisore", "Operatore", "Manutenzione"
Permissions BIGINT NOT NULL DEFAULT 0, -- Bitmask: 1=CanEditParams, 2=CanReset, 4=CanManageUsers, ...
IsActive BOOLEAN NOT NULL DEFAULT TRUE
);
-- Dipendenti reali
CREATE TABLE employees (
BadgeId INT PRIMARY KEY AUTO_INCREMENT, -- 4-6 cifre, leggibile su badge RFID
FirstName VARCHAR(50) NOT NULL,
LastName VARCHAR(50) NOT NULL,
Shift TINYINT NOT NULL DEFAULT 1, -- 1=Mattina, 2=Pomeriggio, 3=Notte
RoleId INT NOT NULL,
PinHash VARCHAR(256) NOT NULL, -- BCrypt/Argon2 hash
PinSalt VARCHAR(128) NOT NULL,
IsActive BOOLEAN NOT NULL DEFAULT TRUE,
CreatedAt DATETIME NOT NULL DEFAULT NOW(),
LastLogin DATETIME NULL,
FOREIGN KEY (RoleId) REFERENCES roles(Id)
);
-- Log produzione (esempi)
CREATE TABLE production_log (
Id BIGINT PRIMARY KEY AUTO_INCREMENT,
OperatorId INT NOT NULL,
JobId INT NULL,
EventType VARCHAR(50) NOT NULL, -- "JobStarted", "JobCompleted", "Scrap", "ParamChange", "AlarmAck"
Timestamp DATETIME NOT NULL DEFAULT NOW(),
Details JSON NULL, -- Dettagli evento (es. {parameter: "pressure", old: 10, new: 12})
FOREIGN KEY (OperatorId) REFERENCES employees(BadgeId)
);
[Flags]
public enum Permission : long
{
None = 0,
ViewProduction = 1 << 0,
EditParameters = 1 << 1,
ResetMachine = 1 << 2,
ManageJobs = 1 << 3,
ManageUsers = 1 << 4,
ViewReports = 1 << 5,
Admin = 1 << 6,
All = ~0L
}
public record Role(int Id, string Name, Permission Permissions, bool IsActive);
public record Employee(
int BadgeId,
string FirstName,
string LastName,
Shift Shift,
Role Role,
bool IsActive,
DateTime? LastLogin
);
public enum Shift { Morning = 1, Afternoon = 2, Night = 3 }
| Servizio | Responsabilità | Metodi chiave |
|---|---|---|
IAuthenticationService |
Login/Logout, validazione PIN | LoginAsync(badgeId, pin), LoginAsync(rfidToken), LogoutAsync(), GetCurrentUser() |
IEmployeeRepository |
CRUD dipendenti | GetByIdAsync(), GetByShiftAsync(), GetActiveAsync(), CreateAsync(), UpdatePinAsync() |
IRoleRepository |
CRUD ruoli + permessi | GetAllAsync(), GetWithPermissionsAsync(), UpdatePermissionsAsync() |
Con Redis opzionale, la sessione operatore vive in sistec:{plant}:session:{badge} con TTL 30min e heartbeat estensibile. L'operatore che badgea su AB è riconosciuto anche su C — senza rilogin. RedisSessionStore pubblica su Pub/Sub i cambi di stato (login/logout) per aggiornare in tempo reale la lista operatori attivi su tutti i pannelli. Auto-logout centralizzato: se Redis scade la sessione, tutti i pannelli vedono l'operatore sloggato contemporaneamente.
| IProductionAnalyticsService | Statistiche per operatore | GetPiecesPerHourAsync(operatorId, range), GetScrapRateAsync(operatorId, range), GetShiftComparisonAsync(range) |
| IAuditService | Log eventi operatore | LogEventAsync(operatorId, eventType, details), GetEventsAsync(filters) |
loginSistec.ls viene eliminatoEmployee con ruolo Admin nel seed del database — stessa sicurezza degli altri utenti{
"Authentication": {
"Provider": "Pin", // "Pin" | "Rfid" | "Both"
"RfidReaderPort": "COM3",
"RfidReaderBaudRate": 9600,
"PinMinLength": 4,
"PinMaxLength": 6,
"BcryptCostFactor": 10,
"AutoLogoutMinutes": 30,
"BruteForceLockout": { "MaxAttempts": 5, "LockoutMinutes": 15 }
}
}
La dashboard EmployeeStatsPage mostra:
| Metrica | Fonte Dati | Aggiornamento |
|---|---|---|
| Pezzi prodotti (oggi/settimana/mese) | production_log WHERE EventType='JobCompleted' |
Real-time |
| Scarti (oggi/settimana/mese) | production_log WHERE EventType='Scrap' |
Real-time |
| Tempo ciclo medio | JobCompleted - JobStarted per operatore |
Per job |
| Produttività % | Tempo produttivo / tempo totale turno | Ogni ora |
| Confronto turni | Mattina vs Pomeriggio vs Notte | Giornaliero |
| Eventi principali | Alarm Ack, Param Changes, Reset | Real-time |
La gestione utenti è indipendente dagli stack macchina — può essere sviluppata e versionata come pacchetto Sistec.Infra.Authentication NuGet, consumato da tutti gli stack UI.
Sistec.Infra.Authentication ← NuGet
├── IAuthenticationService.cs ← Interfaccia
├── IEmployeeRepository.cs
├── IRoleRepository.cs
├── IProductionAnalyticsService.cs
├── Models/
│ ├── Employee.cs
│ ├── Role.cs
│ └── Permission.cs
├── Services/
│ ├── AuthenticationService.cs ← BCrypt verify + session
│ ├── ProductionAnalyticsService.cs
│ └── AuditService.cs
└── Data/
├── EmployeeRepository.cs ← Dapper
└── RoleRepository.cs
Fase A: Foundation (1 settimana)
Modello dati + migration DB
Repository + servizi DI
Password hashing con BCrypt
Fase B: Login UI (1-2 settimane)
LoginPage Avalonia (PIN + badge)
Session management + auto-logout
Role-based authorization nei view-model
Fase C: Admin (1 settimana)
CRUD dipendenti + ruoli
Reset PIN
Fase D: Statistiche (2-3 settimane)
ProductionAnalyticsService
EmployeeStatsPage + grafici
Esportazione CSV/PDF
| Prima | Dopo |
|---|---|
| "Mario Rossi preme un bottone e produce" | "Mario Rossi badgea, l'HMI sa chi è, statistiche real-time sulla sua produttività" |
| "Non sappiamo chi ha causato lo scarto" | "Scarto tracciato con operatore, data, ora e dettagli" |
| "Il cliente chiede report di produttività — risposta: non li abbiamo" | "Il cliente apre la dashboard e vede pezzi/h per operatore, confronto turni, scarti %" |
| "Password in chiaro su MySQL" | "PIN hashato con BCrypt, niente segreti nel codice" |
| "CRUD utenti rotto (query commentate)" | "AdminPage funzionante con permessi granulari" |
Con stack verticali, NuGet e Avalonia la domanda sorge spontanea. La risposta è: per un HMI industriale, i microservizi risolverebbero problemi che non hai, creandone di nuovi.
| Beneficio microservizi | Utile in HMI? | Perché |
|---|---|---|
| Scalabilità orizzontale | ❌ No | L'HMI serve 1 operatore. Non hai 10.000 utenti simultanei. |
| Deploy indipendente | ⚠️ Già risolto | Lo hai già con NuGet + DI Container. Un EXE unico, versioni diverse per ogni stack package. |
| Isolamento guasti | ❌ Controproducente | Se Kuka.Stack crasha in un processo separato, l'HMI mostra "KUKA offline". Se crasha in-process, riavvi lo stack. Il risultato è identico, ma in-process è più semplice da gestire. |
| Team autonomy | ⚠️ Già risolto | Stack separati in repo NuGet diversi. Team A fa Kuka, Team B fa Safan, si incontrano sulle interfacce di Sistec.Core. |
| Polyglot tech | ❌ No | Sei già .NET. Mischiare linguaggi in un HMI industriale è pura complessità. |
| Costo | Impatto su HMI |
|---|---|
| Latenza di rete | Robot KUKA, pressa Safan, PLC: già comunicano via TCP/OPC UA. Metti un hop applicativo in mezzo e每 comandi che devono arrivare in <10ms iniziano a soffrire. |
| Fallimento rete = macchina ferma | Con un EXE unico, se tutto va in crash riavvi. Con 5 microservizi, uno dei 5 potrebbe morire silenziosamente e la pressa parte lo stesso senza il robot che la serve — danni fisici. |
| Container su pannello industriale | Un pannello industriale ha un Celeron o un ARM. Docker + orchestrator su quella roba è follia. |
| Complessità operativa | Service discovery, health check, distributed tracing, message broker — per un HMI che deve solo partire e funzionare quando dai corrente. |
| Stato in-memory | I tag OPC UA hanno valore istantaneo. Se li distribuisci su più processi, ogni servizio deve riconnettersi al PLC o avere un cache distribuito — pura complessità senza beneficio. |
flowchart TB
subgraph ModularMonolith["✅ MODULAR MONOLITH (proposto)"]
MM_PROC["1 processo
Sistec.HMI.exe"]
MM_KUKA["Sistec.Stack.Kuka
(DLL in-process)"]
MM_SAFAN["Sistec.Stack.Safan
(DLL in-process)"]
MM_PLC["Sistec.Stack.PLC
(DLL in-process)"]
MM_PRODUCTION["Sistec.Stack.Production
(DLL in-process)"]
MM_LAYOUT["LayoutEngine + Avalonia UI"]
MM_KUKA -.->|chiamate dirette, interfacce| MM_PRODUCTION
MM_SAFAN -.->|stesso processo| MM_PRODUCTION
MM_PLC -.->|stessa memoria| MM_KUKA
end
subgraph Microservices["❌ MICROSERVIZI"]
MS_KUKA["Kuka.Service
(processo separato)"]
MS_SAFAN["Safan.Service
(processo separato)"]
MS_PLC["Plc.Service
(processo separato)"]
MS_PRODUCTION["Production.Service
(processo separato)"]
MS_HMI["HMI (Avalonia)
(processo separato)"]
MS_BROKER["Message Broker
(RabbitMQ/gRPC)"]
MS_KUKA -.->|REST/gRPC| MS_BROKER
MS_SAFAN -.->|REST/gRPC| MS_BROKER
MS_PLC -.->|REST/gRPC| MS_BROKER
MS_PRODUCTION -.->|REST/gRPC| MS_BROKER
MS_HMI -.->|REST/gRPC| MS_BROKER
end
subgraph Comparison["CONFRONTO"]
C1["Monolith: 1 EXE, 1 cartella, doppio click"]
C2["Microservizi: 5+ servizi, container, docker-compose, orchestrazione"]
C3["Monolith: chiamate dirette, zero serializzazione, zero latenza"]
C4["Microservizi: gRPC/JSON, serializzazione, latenza di rete"]
C5["Monolith: crash = riavvio. Microservizi: crash parziale = stato incoerente"]
end
L'architettura proposta in questo documento è un Modular Monolith:
Un unico EXE, composto da pacchetti NuGet, con confini netti tra stack.
| Caratteristica | Modular Monolith | Microservizi |
|---|---|---|
| Deploy | 1 EXE + DLL | 5+ container |
| Latenza inter-stack | Zero (in-process) | RPC/gRPC + serializzazione |
| Isolamento | Assembly (non processo) | Processo + rete |
| Versioning | NuGet (stessa risoluzione) | API versioning + compatibilità |
| Debuggabilità | Visual Studio, F5 | Docker compose, distributed tracing |
| Boot time | 1-2 secondi | Minuti (container startup) |
| HW requirements | Pannello industriale standard | Server / cluster |
| Stato condiviso | Memoria condivisa, lock | Cache distribuita, eventual consistency |
La regola pratica: se non hai un team di 5+ persone per servizio e traffico da gestire, i microservizi sono over-engineering. Il Modular Monolith ti dà il 90% dei benefici di isolamento con il 10% della complessità.
Microservizi avrebbero senso solo se tra 3-5 anni:
Fino ad allora, Modular Monolith + NuGet + Avalonia è la scelta giusta.
Un discorso diverso vale per la raccolta dati verso cloud — che ha senso ed è ortogonale all'architettura principale.
flowchart LR
subgraph HMI["HMI (1 processo, Modular Monolith)"]
Kuka["Sistec.Stack.Kuka"]
Safan["Sistec.Stack.Safan"]
PLC["Sistec.Stack.PLC"]
Prod["Sistec.Stack.Production"]
Alarms["Sistec.Stack.Alarms"]
Jobs["Sistec.Stack.JobManagement"]
subgraph Cloud["Sistec.Stack.Cloud (NUOVO)"]
Collector["CloudDataCollector
Si sottoscrive a eventi
degli altri stack"]
MQTT["MqttPublisher
Invia a broker MQTT
o HTTP API"]
Buffer["OfflineBuffer
Coda locale se cloud
non disponibile"]
end
end
Cloud -->|MQTT / HTTPS| CloudBroker["Cloud Broker
(Azure / AWS / on-prem)"]
CloudBroker --> Dashboard["Dashboard centrale
KPI, OEE, trend"]
CloudBroker --> Analytics["Analytics / ML"]
CloudBroker --> ERP["ERP / MES"]
Kuka -.->|eventi| Cloud
Safan -.->|eventi| Cloud
PLC -.->|eventi| Cloud
Prod -.->|eventi| Cloud
Alarms -.->|eventi| Cloud
Jobs -.->|eventi| Cloud
| Aspetto | Dettaglio |
|---|---|
| Cos'è | Sistec.Stack.Cloud — un pacchetto NuGet come tutti gli altri |
| Cosa fa | Ascolta eventi dagli altri stack e li invia a un broker cloud |
| Cosa NON fa | Non comanda l'impianto. Se il cloud è giù, la produzione continua |
| Protocollo | MQTT (standard IIoT), OPC UA PubSub, o HTTP REST |
| Dati tipici | Conteggio pezzi, allarmi, job completati, stato macchine, OEE |
| Buffer (default) | Coda su disco locale (SQLite) se cloud non raggiungibile — svuota quando torna online |
| Buffer (Redis, opzionale) | Redis Streams sistec:{plant}:cloud:stream — persistente via RDB/AOF, consumer groups per partizione dati, ripartenza senza perdita |
| Configurazione | cloud.json: endpoint, topic, credenziali, intervallo pubblicazione |
| Opzionale | Se non includi il pacchetto NuGet, l'HMI funziona identico — senza cloud |
dotnet add package Sistec.Stack.CloudCloudSimulator (broker MQTT finto) come qualsiasi altro stack// Sistec.Stack.Cloud/CloudDataCollector.cs
public class CloudDataCollector : ICloudDataCollector
{
private readonly MqttPublisher _mqtt;
private readonly OfflineBuffer _buffer;
public CloudDataCollector(
IAlarmService alarms, // da Sistec.Stack.Alarms
IJobTracker jobs, // da Sistec.Stack.JobManagement
IProductionTracker prod, // da Sistec.Stack.Production
MqttPublisher mqtt,
IOptions<CloudOptions> options)
{
alarms.OnAlarmRaised += (alarm) =>
PublishAsync("alarms/raised", alarm);
jobs.OnJobCompleted += (job) =>
PublishAsync("jobs/completed", job);
prod.OnPartProduced += (part) =>
PublishAsync("production/part", part);
}
private async Task PublishAsync(string topic, object data)
{
if (_mqtt.IsConnected)
await _mqtt.PublishAsync(topic, data);
else
await _buffer.EnqueueAsync(topic, data); // salva in SQLite locale
}
}
| Scenario | Cloud Stack |
|---|---|
| Cliente vuole dashboard centrale con OEE di 10 impianti | ✅ Aggiungi Sistec.Stack.Cloud |
| Manutenzione predittiva su cloud | ✅ Idem |
| Integrazione con ERP/MES | ✅ Idem |
| Necessario per la produzione (cloud comanda l'impianto) | ❌ Non farlo. Il cloud non deve comandare un HMI industriale in real-time |
| Raccolta dati per analytics | ✅ Perfetto |
Il Cloud.Stack pubblica eventi in formato MQTT Sparkplug B v3.1 (febbraio 2026), lo standard Eclipse Foundation per IIoT. Sparkplug B definisce:
| Elemento | Ruolo |
|---|---|
| Topic Namespace | spBv1.0/{group_id}/{edge_node}/{type} — struttura standardizzata |
| Birth/Death Certificate | Ogni edge node pubblica stato completo all'avvio (BIRTH) e alla disconnessione (DEATH) |
| Payload Protobuf | Dati serializzati in formato binario efficiente, con metadati (timestamp, data type, qualità) |
| Seq Number | Numerazione sequenziale dei messaggi per rilevamento perdite |
Vantaggi rispetto a MQTT "piano":
| MQTT generico | MQTT Sparkplug B |
|---|---|
| Topic arbitrari, nessuna convenzione | Topic namespace standardizzato |
| Nessun discovery — configurazione manuale | Auto-discovery via BIRTH message |
| Nessuna notifica di disconnessione | DEATH message automatico su disconnessione |
| Payload JSON/XML non strutturato | Payload Protobuf con schema tipizzato |
Lo standard de facto 2026 per l'architettura IIoT è il modello ibrido:
flowchart LR
subgraph Edge["RETE MACCHINE (Edge)"]
PLC["PLC
OPC UA Server"]
HMI["HMI
OPC UA Client
(real-time control)"]
PLC -->|OPC UA Client/Server| HMI
end
subgraph Bridge["BRIDGE"]
SM["Sistec.Stack.Cloud
OPC UA PubSub Reader
→ Sparkplug B Edge Node"]
end
subgraph Cloud["NORTHBOUND (IT/Cloud)"]
MQTT["MQTT Broker
(Mosquitto / EMQX)"]
DASH["Dashboard
Sparkplug Primary App"]
ERP["ERP / MES
Sparkplug Consumer"]
end
HMI -->|OPC UA| SM
SM -->|Sparkplug B| MQTT
MQTT --> DASH
MQTT --> ERP
| Layer | Protocollo | Perché |
|---|---|---|
| Edge (PLC ↔ HMI) | OPC UA Client/Server | Real-time, information model ricco, sicurezza X.509 |
| Bridge (HMI → Broker) | Sparkplug B v3.1 | Standard IIoT, auto-discovery, birth/death, payload binario |
| Northbound (Broker → Cloud) | MQTT + Sparkplug B | Scalabilità, Unified Namespace, multi-consumer |
Il Unified Namespace estende il pattern ibrido: tutti i dati di produzione (PLC, KUKA, Safan, HMI, Alarms, Job) vengono pubblicati in un unico namespace MQTT Sparkplug, accessibile da qualunque applicazione senza integrazioni point-to-point.
Tutti i dati in un unico namespace:
spBv1.0/sistec-lag-5315/
├── PLC/DATA ← tag OPC UA in tempo reale
├── KUKA/DATA ← stato robot, programma in esecuzione
├── Safan/DATA ← stato pressa, ciclo corrente
├── Production/DATA← job attivo, pezzi prodotti, scarti
├── Alarms/DATA ← allarmi attivi, storico
└── HMI/DATA ← heartbeat, operatore loggato
Vantaggio: aggiungere una dashboard, un MES o un ERP = si sottoscrive al namespace. Zero nuove integrazioni.
Impatto: Nuovo componente Sistec.Stack.Cloud.SparkplugNode che implementa l'Edge Node Sparkplug con birth/death/data. Attivabile via configurazione.
In sintesi: il Cloud Connector è uno stack verticale come gli altri, non un microservizio. Si aggiunge con un NuGet package, è opzionale, e non cambia l'architettura di base.
Ogni repo stack ha una pipeline CI/CD identica nel pattern: build → unit test → integration test → quality control → publish NuGet. L'HMI Shell (commessa) ha una pipeline finale che produce l'EXE.
flowchart TB
subgraph Repos["REPOS PER STACK"]
CoreRepo["sistec-core
Sistec.Core"]
KukaRepo["sistec-kuka-stack
Sistec.Stack.Kuka"]
SafanRepo["sistec-safan-stack
Sistec.Stack.Safan"]
PLCRepo["sistec-plc-stack
Sistec.Stack.PLC"]
ProdRepo["sistec-production-stack
Sistec.Stack.Production"]
CloudRepo["sistec-cloud-stack
Sistec.Stack.Cloud"]
end
subgraph Pipelines["PIPELINE TIPO per ogni stack"]
direction TB
P1["① Build + Restore"]
P2["② Unit Test (NUnit)"]
P3["③ Integration Test (Simulator)"]
P4["④ SonarQube Analysis"]
P5["⑤ NuGet Pack + Publish"]
P1 --> P2 --> P3 --> P4 --> P5
end
subgraph Feed["NuGet Feed"]
CorePkg["Sistec.Core 5.1.0"]
KukaPkg["Sistec.Stack.Kuka 2.3.0"]
SafanPkg["Sistec.Stack.Safan 2.3.0"]
PLCPkg["Sistec.Stack.PLC 2.3.0"]
CloudPkg["Sistec.Stack.Cloud 1.0.0"]
end
subgraph HMI_Release["PIPELINE HMI COMMESSA"]
HR1["Restore NuGet packages"]
HR2["Build + Avalonia UI"]
HR3["Integration Test HMI"]
HR4["SonarQube"]
HR5["Publish EXE + DLL"]
HR1 --> HR2 --> HR3 --> HR4 --> HR5
end
CoreRepo --> Pipelines
KukaRepo --> Pipelines
SafanRepo --> Pipelines
PLCRepo --> Pipelines
ProdRepo --> Pipelines
CloudRepo --> Pipelines
Pipelines -->|pubblica| Feed
Feed -->|referenziato da| HMI_Release
# azure-pipelines.yml — template condiviso per tutti gli stack Sistec
name: $(MajorMinor).$(DayOfYear)$(Rev:r)
trigger:
branches:
include:
- main
- develop
- release/*
paths:
exclude:
- 'docs/*'
- '*.md'
pool:
vmImage: 'windows-latest'
variables:
- group: Sistec-NuGet-Auth
- name: MajorMinor
value: '5.1' # aggiornato a ogni major/minor release
- name: ProjectName
value: 'Sistec.Stack.Kuka'
stages:
- stage: Build
displayName: '🔨 Build'
jobs:
- job: Build
steps:
- task: NuGetToolInstaller@1
- task: NuGetCommand@2
inputs:
restoreSolution: 'src/**/*.csproj'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: 'src/**/*.csproj'
arguments: '--configuration Release --no-restore'
displayName: 'Build'
- stage: UnitTests
displayName: '🧪 Unit Test'
dependsOn: Build
condition: succeeded()
jobs:
- job: UnitTests
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: 'tests/**/*.UnitTests.csproj'
arguments: '--configuration Release --no-restore
--filter "Category=Unit"
--collect:"XPlat Code Coverage"
--results-directory $(Build.ArtifactStagingDirectory)/coverage'
displayName: 'Run NUnit Unit Tests'
- task: PublishCodeCoverageResults@2
inputs:
summaryFileLocation: '$(Build.ArtifactStagingDirectory)/coverage/**/coverage.cobertura.xml'
failIfCoverageEmpty: true
- stage: IntegrationTests
displayName: '🔌 Integration Test'
dependsOn: Build
condition: succeeded()
jobs:
- job: IntegrationTests
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: 'tests/**/*.IntegrationTests.csproj'
arguments: '--configuration Release --no-restore
--filter "Category=Integration"
--collect:"XPlat Code Coverage"'
displayName: 'Run Integration Tests (with Simulator)'
- stage: QualityGate
displayName: '🔍 SonarQube'
dependsOn: [UnitTests, IntegrationTests]
condition: succeeded()
jobs:
- job: SonarQube
steps:
- task: SonarQubePrepare@7
inputs:
SonarQube: 'SistecSonarQube'
scannerMode: 'dotnet'
projectKey: 'sistec-$(ProjectName)'
projectName: '$(ProjectName)'
extraProperties: |
sonar.coverage.exclusions=**/tests/**
sonar.cs.opencover.reportsPaths=$(Build.ArtifactStagingDirectory)/coverage/**/coverage.opencover.xml
sonar.qualitygate.wait=true
- task: SonarQubeAnalyze@7
- task: SonarQubePublish@7
inputs:
pollingTimeoutSec: '300'
- stage: Publish
displayName: '📦 NuGet Publish'
dependsOn: QualityGate
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: Publish
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
projects: 'src/$(ProjectName)/$(ProjectName).csproj'
arguments: '--configuration Release --no-build
-p:PackageVersion=$(MajorMinor).$(Build.BuildNumber)
-p:RepositoryUrl=$(Build.Repository.Uri)
-p:PackageReleaseNotes=$(Build.SourceVersionMessage)'
displayName: 'NuGet Pack'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
publishFeedCredentials: 'SistecNuGetFeed'
allowPackageConflicts: false
displayName: 'NuGet Push'
Gates applicati a ogni build su main:
| Gate | Soglia | Azione se fallisce |
|---|---|---|
| Code Coverage | ≥ 70% (unit) + ≥ 50% (integration) | Blocca pubblicazione NuGet |
| Duplicated Lines | ≤ 3% | Blocca |
| Code Smells | 0 nuovi | Blocca |
| Security Hotspots | 0 nuovi | Blocca |
| Vulnerabilities | 0 | Blocca |
| Maintainability Rating | A | Blocca |
| Reliability Rating | A | Blocca |
| Security Rating | A | Blocca |
# Version scheme: Major.Minor.DayOfYear.Revision
# Esempio: Sistec.Stack.Kuka 2.3.215.1 (build 215 del 2026, revisione 1)
#
# Major.Minor:
# - Si aggiorna manualmente nel file azure-pipelines.yml
# - Major: breaking change nelle interfacce pubbliche
# - Minor: nuova feature, backward compatible
#
# DayOfYear.Revision:
# - Automatico: giorno dell'anno (001-366) + revisione build del giorno
# - Garantisce versioni sempre crescenti e ordinabili
Tutti i layer di uno stack (Client, Driver, Services, UI, Simulator) condividono la stessa versione:
<!-- Directory.Build.props — stesso valore per tutti i progetti dello stack -->
<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
<Authors>Sistec AM</Authors>
<Company>Sistec AM</Company>
<RepositoryType>git</RepositoryType>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>
<!-- Source Link per debugging NuGet -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
</Project>
sistec-kuka-stack/
├── azure-pipelines.yml ← Pipeline CI/CD
├── Directory.Build.props ← Versioning condiviso
├── src/
│ ├── Sistec.Stack.Kuka.Client/ ← .csproj -> Sistec.Stack.Kuka.Client.nupkg
│ ├── Sistec.Stack.Kuka.Driver/ ← .csproj -> Sistec.Stack.Kuka.Driver.nupkg
│ ├── Sistec.Stack.Kuka.Services/ ← .csproj -> Sistec.Stack.Kuka.Services.nupkg
│ ├── Sistec.Stack.Kuka.UI/ ← .csproj -> Sistec.Stack.Kuka.UI.nupkg
│ └── Sistec.Stack.Kuka.Simulator/ ← .csproj -> Sistec.Stack.Kuka.Simulator.nupkg
├── tests/
│ ├── Sistec.Stack.Kuka.UnitTests/ ← NUnit, Category=Unit
│ ├── Sistec.Stack.Kuka.IntegrationTests/ ← NUnit, Category=Integration
│ └── Sistec.Stack.Kuka.SimulatorTests/ ← Test contro il Simulator
├── docs/
├── README.md
└── CHANGELOG.md
Diversa dagli stack: non produce NuGet, produce l'EXE finale. Le versioni dei pacchetti NuGet sono fissate nel csproj.
# azure-pipelines-hmi.yml — per ogni commessa (es. Sistec.5315.LAG)
name: $(Build.SourceBranchName).$(DayOfYear)$(Rev:r)
trigger:
branches:
include:
- main
paths:
include:
- 'Sistec.5315.LAG/*'
variables:
- group: Sistec-NuGet-Auth
- name: HmiProject
value: 'Sistec.5315.LAG'
stages:
- stage: NuGetRestore
displayName: '📦 Restore NuGet'
jobs:
- job: Restore
steps:
- task: NuGetCommand@2
inputs:
restoreSolution: '$(HmiProject)/$(HmiProject).csproj'
feedsToUse: 'config'
nugetConfigPath: '$(HmiProject)/nuget.config'
- stage: Build
displayName: '🔨 Build HMI'
dependsOn: NuGetRestore
jobs:
- job: Build
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '$(HmiProject)/$(HmiProject).csproj'
arguments: '--configuration Release
-p:Version=$(Build.BuildNumber)'
- stage: IntegrationTest
displayName: '🔌 HMI Integration Test'
dependsOn: Build
jobs:
- job: IntegrationTest
steps:
- script: |
# Avvia simulatori, testa binding layout.json
dotnet test $(HmiProject)/tests/*.IntegrationTests.csproj
displayName: 'Run HMI Integration Tests'
- stage: SonarQube
displayName: '🔍 SonarQube'
dependsOn: IntegrationTest
jobs:
- job: SonarQube
steps:
- task: SonarQubePrepare@7
inputs:
SonarQube: 'SistecSonarQube'
projectKey: 'sistec-$(HmiProject)'
projectName: '$(HmiProject)'
- task: SonarQubeAnalyze@7
- task: SonarQubePublish@7
- stage: Publish
displayName: '🚀 Deploy HMI'
dependsOn: SonarQube
condition: succeeded()
jobs:
- job: Publish
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
projects: '$(HmiProject)/$(HmiProject).csproj'
arguments: '--configuration Release
--output $(Build.ArtifactStagingDirectory)/hmi-release
-p:Version=$(Build.BuildNumber)'
- task: CopyFiles@2
inputs:
contents: |
$(HmiProject)/manifest.json
$(HmiProject)/layout.json
$(HmiProject)/config/**/*
$(HmiProject)/resources/**/*
targetFolder: '$(Build.ArtifactStagingDirectory)/hmi-release'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)/hmi-release'
artifactName: 'HMI-Release'
Feed: Sistec-NuGet-Feed
├── Sistec.Core 5.1.0, 5.1.1, 5.2.0
├── Sistec.Controls 4.3.0, 4.3.1
├── Sistec.UI 4.3.0
├── Sistec.LayoutEngine 1.0.0, 1.1.0
├── Sistec.Configuration 2.0.0
├── Sistec.Infra.Persistence 2.0.0
├── Sistec.Infra.Persistence.Dapper 2.0.0
├── Sistec.Infra.Persistence.MySql 2.0.0
├── Sistec.Infra.Persistence.SqlServer 2.0.0
├── Sistec.Infra.CodeGen 1.0.0 (dotnet tool)
├── Sistec.Library.OpcUa 3.2.0
├── Sistec.Library.Modbus 2.1.0
├── Sistec.Library.Bus 1.5.0
│
├── Sistec.Stack.Kuka.Client 2.3.0
├── Sistec.Stack.Kuka.Driver 2.3.0 ← stessa versione
├── Sistec.Stack.Kuka.Services 2.3.0 ← stessa versione
├── Sistec.Stack.Kuka.UI 2.3.0 ← stessa versione
├── Sistec.Stack.Kuka.Simulator 2.3.0 ← stessa versione
│
├── Sistec.Stack.Safan.* 2.1.0
├── Sistec.Stack.Esa.* 1.0.0
├── Sistec.Stack.PLC.* 3.0.0
├── Sistec.Stack.Sinumerik.* 2.0.0
├── Sistec.Stack.Production.* 1.2.0
├── Sistec.Stack.JobManagement.* 2.0.0
├── Sistec.Stack.Maintenance.* 1.1.0
├── Sistec.Stack.Alarms.* 2.0.0
└── Sistec.Stack.Cloud.* 1.0.0 (optional)
| Evento | Cosa succede |
|---|---|
| Push su main (stack) | Build → Test → SonarQube → NuGet publish automatico |
| Push su develop (stack) | Build → Test → SonarQube (analisi, nessun publish) |
| Pull Request (stack) | Build → Test → SonarQube (quality gate bloccante) |
| Push su main (HMI commessa) | Build → Test → SonarQube → EXE pubblicato come artifact |
Nuovo tag v* (stack) |
Build → Test → NuGet publish con versione dal tag (override) |
| Notte (tutti) | Build programmata di tutti gli stack su main, report dashboard |
| Strumento | Ruolo | Alternativa |
|---|---|---|
| Azure DevOps / GitHub Actions | CI/CD orchestrator | GitLab CI, Jenkins |
| SonarQube | Quality gate, code analysis | SonarCloud (SaaS) |
| NuGet Server / Azure Artifacts | Feed pacchetti | GitHub Packages, ProGet |
| NUnit 4.x | Unit + Integration test | xUnit |
| coverlet | Code coverage | dotCover, OpenCover |
| Moq / NSubstitute | Mocking (unit test) | FakeItEasy |
| Testcontainers | Integration test con DB reali | — |
I controlli più veloci vanno primi nella pipeline, per dare feedback immediato:
Stage 1: Lint + Code Style Analysis ← 30 secondi
Stage 2: Build ← 1-2 minuti
Stage 3: Unit Tests ← 2-3 minuti
Stage 4: Quality Gate (SonarQube) ← 3-4 minuti
Stage 5: Integration Tests ← 5-10 minuti
Stage 6: NuGet Publish / Deploy ← (solo su main)
Un controllo di stile bloccante va scoperto in 30 secondi, non dopo 15 minuti di integrazione. La pipeline è configurata con condition: failed() per fermarsi al primo stage che fallisce.
# Evita di scaricare NuGet a ogni build
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
path: '$(NUGET_PACKAGES)'
displayName: 'NuGet Cache'
Il restore passa da 2-3 minuti a 5-10 secondi con la cache calda.
| Metrica | Target | Se superato |
|---|---|---|
| Tempo totale (commit→feedback) | < 15 minuti | Ottimizzare stage lenti |
| Unit test | < 3 minuti | Parallelizzare per progetto |
| Integration test | < 5 minuti | Ridurre container superflui |
| SonarQube | < 4 minuti | Pre-caricare analysis cache |
| NuGet pack + push | < 1 minuto | — |
Oltre i 20 minuti gli sviluppatori smettono di aspettare il feedback — la pipeline perde efficacia.
Gate applicato automaticamente a ogni PR su main:
├── Coverage ≥ 70% su codice nuovo (unit)
├── Coverage ≥ 50% su codice nuovo (integration)
├── 0 nuovi bug critici / blocker
├── 0 vulnerabilità critiche
├── Duplicazione < 3%
└── Maintainability Rating = A
Nessun gate bypassabile manualmente. Se il gate fallisce, la PR non si merge.
La pipeline CI/CD non è una fase separata: si costruisce insieme a ogni stack.
Fase 1: Kuka.Stack pilota
├── Codice: Sistec.Stack.Kuka.*
├── Test: NUnit UnitTests + IntegrationTests + SimulatorTests
├── Pipeline: azure-pipelines.yml (build → test → SQ → NuGet)
└── SonarQube: quality gate attivo
Fase 2: Ogni nuovo stack
├── Template pipeline già pronto (si copia/incolla dal pilota)
├── Test specifici dello stack
└── SonarQube: progetto separato per stack
Fase 6: HMI Commessa
├── Pipeline HMI già costruita (usa i pacchetti NuGet)
├── Solo Integration Test HMI (layout.json, simulatori)
└── SonarQube: analisi del codice HMI (minimo, è solo config)
La strategia di test per la nuova commessa greenfield deve garantire feedback rapido, copertura mirata e test che durano nel tempo, evitando gli errori visti nelle codebase legacy (13 copie di FakeOpcUa, pochissimi test automatici, nessun quality gate strutturato).
┌──────┐
│ E2E │ ← 1-2 per flusso critico (login, job completo)
┌┴──────┴┐
│Integr. │ ← Con Simulator (Kuka, Safan, PLC)
┌┴────────┴┐
│ Unit │ ← Logica pura: Services, Driver parsing
┌┴──────────┴┐
│ Static │ ← Lint, code style, SonarQube analysis
└────────────┘
| Livello | Cosa testa | Tecnologia | Quantità indicativa |
|---|---|---|---|
| Static Analysis | Code style, security hotspots, duplicazione | SonarQube, Roslyn analyzers | Sempre |
| Unit Test | Logica pura: algoritmi, parsing, validazione, state machine | NUnit + Moq/NSubstitute | ~70% del totale |
| Integration Test | Stack completo con Simulator (es. Kuka.Client → KukaSimulator) | NUnit + Testcontainers | ~20% del totale |
| E2E / System Test | Flusso HMI reale (login → produzione → logout) | Playwright (Avalonia) | ~10% del totale |
Regola d'oro: 50-100 unit test : 5-10 integration test : 1 E2E test.
| Principio | Significato | Applicazione in HMI |
|---|---|---|
| Fast | Un test in millisecondi | Se un unit test supera 100ms, non è un unit test puro |
| Independent | Nessun test dipende dallo stato di un altro | Ogni test crea il proprio contesto: setUp + tearDown |
| Repeatable | Stesso risultato su qualunque macchina | Niente dipendenze da sistema operativo, ora, path assoluti |
| Self-Validating | PASS/FAIL con assertion, mai print() |
Assert su result, eccezione, o interazione mock |
| Timely | Scritto prima o insieme al codice, non dopo | Il test guida il design, non viceversa |
[Test]
public void CalculateBendAngle_WhenValidInput_ReturnsCorrectAngle()
{
// Arrange
var service = new BendingCalculator();
double thickness = 2.5;
double radius = 8.0;
// Act
double result = service.CalculateBendAngle(thickness, radius);
// Assert
Assert.That(result, Is.EqualTo(88.3).Within(0.1)); // ← messaggio implicito
}
method_scenario_expectedResult
oppure
should_result_when_scenario
Esempi:
ParseKukaPayload_WhenChecksumInvalid_ThrowsChecksumException()ShouldReturnNull_WhenJobListIsEmpty()| Regola | Perché |
|---|---|
| Mock solo ciò che possiedi | Mockare HttpClient (Microsoft) direttamente lega il test all'implementazione. Wrappalo in IKrcClient e mocka quello. |
| Un test, un comportamento | Non verificare 5 interazioni in un solo test — dividi. |
| Il mock verifica interazioni, non implementazioni | Non controllare l'ordine interno delle chiamate |
| Setup > 20 righe = problema di design | Refactoring prima di aggiungere altri mock |
| Aggiungi sempre test con eccezione | side_effect=Exception per verificare la gestione errori |
| Double | Quando usarlo |
|---|---|
| Stub | Serve un dato di input controllato (es. IKrcClient che restituisce una risposta fissa) |
| Fake | La logica interna conta (es. InMemoryJobRepository, FakeOpcUa) |
| Mock | Si vuole verificare un'interazione (es. mock.Verify(x => x.SaveAsync(...))) |
| Spy | Legacy code senza DI — non serve nella nuova commessa greenfield |
// ✅ CORRETTO: mocko l'interfaccia che possiedo
var krcClientMock = new Mock<IKrcClient>();
krcClientMock
.Setup(x => x.SendCommandAsync(It.IsAny<KrcCommand>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new KrcResponse(Success: true, Payload: "OK"));
// ❌ SBAGLIATO: mocko TcpClient di System.Net.Sockets (non lo possiedo)
var tcpMock = new Mock<TcpClient>(); // fragile, legato all'implementazione
Il ciclo RED → GREEN → REFACTOR è ideale per la logica pura (Services, Driver, algoritmi di parsing, validazione):
| Adatto a TDD | NON adatto a TDD |
|---|---|
| Parsing protocollo Kuka | UI Layout (Avalonia) |
| State machine pallet | Configurazione layout.json |
| Calcoli pressa (angolo, forza) | Prototipi esplorativi |
| Validazione input | Simulator (spike tecnico) |
| Recipe Engine | Script usa-e-getta |
Ciclo baby steps:
YAGNI (You Ain't Gonna Need It): scrivi solo il codice necessario a far passare il test. Niente "tanto lo aggiungo già che ci sono".
| Metrica | Target | Strumento |
|---|---|---|
| Complessità ciclomatica | ≤ 10 per metodo | SonarQube |
| Duplicazione | < 3% | SonarQube |
| Code Coverage (unit) | ≥ 70% su codice nuovo | coverlet |
| Code Coverage (integration) | ≥ 50% su codice nuovo | coverlet |
| Bug / Vulnerabilità | 0 critici o blocker | SonarQube |
| Maintainability Rating | A | SonarQube |
Attenzione: coverage ≠ qualità. Il 100% di coverage non garantisce codice corretto — misura solo che ogni riga sia stata eseguita almeno una volta. Meglio 70% di test significativi che 100% di "assert al ribasso".
| Anti-Pattern | Problema | Soluzione |
|---|---|---|
| Metodi statici ovunque | Non mockabili, dipendenze nascoste | DI + interfacce |
| Singleton globali | Test accoppiati tra loro (stato condiviso) | DI con lifetime Scoped |
| God Class | Violazione SRP | Dividere per responsabilità |
| Magic numbers/strings | Intento opaco, refactoring pericoloso | Costanti nominate |
| Over-mocking | Mock inutili (se senza mock il test non cambia) | Usare Fake/Stub quando basta |
| Test fragili | Test che si rompono a ogni refactoring anche senza cambio comportamento | Testare API pubblica, non implementazione |
| Livello | Comportamento |
|---|---|
| 1 — Iniziale | Test scritti dopo il codice, pochi, spesso rotti |
| 2 — Consapevole | Test parte della Definition of Done, revisionati in code review |
| 3 — Proattivo | TDD per logica pura, mutation testing, property-based testing valutato |
| 4 — Data-Driven | SLO / Error Budget guidano gli investimenti in test, synthetic monitoring |
La nuova commessa parte dal livello 2, tendendo al 3 entro la prima release.
Una feature è completa SOLO quando:
Un test flaky (a volte passa, a volte no):
I layer Services degli stack verticali consumano interfacce definite in Sistec.Core e da altri stack. Quando uno stack evolve, può rompere i consumatori.
Soluzione: Contract Testing (Pact) per le interfacce pubbliche:
// Consumer (Sistec.Stack.Production) definisce il contratto
[Test]
public void Production_Expects_KukaService_Contract()
{
var contract = new MessagePactBuilder()
.Given("Kuka robot is online")
.UponReceiving("a request to load panel")
.With(Request("POST", "/kuka/load-panel", body: new { gripper = "vacuum_a" }))
.WillRespondWith(Response(200, body: new { status = "loading" }));
// Il contract viene pubblicato — Kuka.Stack deve verificarlo in CI
}
Questo evita rotture silenziose: se Kuka.Stack cambia la firma di ILoadPanelService, la pipeline di Production.Stack fallisce prima del deploy.
L'HMI comanda macchinari industriali. Alcuni principi aggiuntivi:
L'osservabilità per un HMI industriale non è solo logging. Un sistema che deve funzionare 24/7 su PC industriali, spesso senza connessione di rete, deve poter essere diagnosticato da remoto, monitorato automaticamente e analizzato a posteriori senza intervento umano.
La sezione §9.7 ha descritto la migrazione dal logger statico a ILogger<T> via DI. Questa sezione estende il quadro a tutte e 4 le dimensioni dell'osservabilità — logs, metrics, traces, health checks — più la diagnostica di campo.
flowchart LR
subgraph Stacks["STACK VERTICALI"]
K["Sistec.Stack.Kuka"]
S["Sistec.Stack.Safan"]
P["Sistec.Stack.PLC"]
A["Sistec.Stack.Alarms"]
J["Sistec.Stack.JobMgmt"]
Pr["Sistec.Stack.Production"]
end
subgraph Observability["OSSERVABILITÀ"]
Logs["ILogger<T>
JSON strutturato"]
Metrics["System.Diagnostics.Metrics
Counter / Histogram"]
Traces["ActivitySource
OpenTelemetry"]
Health["IHealthCheck
Microsoft.Extensions"]
end
subgraph Storage["MEMORIZZAZIONE"]
LogFile["File JSON
rotazione giornaliera"]
MetricsCache["Metriche in memoria
esportazione periodica"]
TraceColl["OTLP Collector
(opzionale)"]
HealthAgg["Health Aggregator
stato composito"]
end
subgraph Consumatori["CONSUMATORI"]
Dashboard["Dashboard locale
Avalonia"]
Cloud["Cloud.Stack
MQTT / HTTP"]
Alert["Alerting
Critical → sveglia turnista"]
Field["Field Diagnostics
USB key / API"]
end
Stacks -->|ILogger| Logs
Stacks -->|Meter| Metrics
Stacks -->|Activity| Traces
Stacks -->|IHealthCheck| Health
Logs --> LogFile
Metrics --> MetricsCache
Traces --> TraceColl
Health --> HealthAgg
LogFile --> Dashboard
LogFile --> Field
MetricsCache --> Dashboard
MetricsCache --> Cloud
TraceColl --> Cloud
HealthAgg --> Dashboard
HealthAgg --> Alert
Già coperto in §9.7. Riepilogo punti chiave:
ILogger<T> via DI, non statico{Gripper} non interpolazione $"..."appsettings.jsonOgni evento di business segue uno schema standardizzato per garantire consistenza tra stack:
public record IndustrialEvent(
string CorrelationId, // Sessione operatore
string OperatorId, // Badge operatore (se logged)
string MachineId, // "Kuka_0", "Safan_1"
string EventType, // "CommandSent", "JobStarted", "AlarmRaised"
string Category, // "Production", "Alarm", "Maintenance", "Security"
IReadOnlyDictionary<string, object> Payload,
DateTime Timestamp
);
{
"@t": "2026-07-02T14:30:00.123Z",
"@l": "Information",
"EventType": "CommandSent",
"Category": "Production",
"MachineId": "Kuka_0",
"CorrelationId": "a1b2c3d4e5f6",
"OperatorId": "42",
"Payload": {
"command": "LoadPanel",
"gripper": "vacuum_a",
"duration_ms": 2340
}
}
Questo schema permette query cross-stack: "tutti gli eventi di MachineId=Kuka_0 tra le 14:00 e le 15:00".
Ogni sessione operatore (dalla login alla logout) ha un Correlation ID che segue tutte le operazioni:
public class OperatorSessionMiddleware
{
private readonly ILogger _logger;
public void BeginSession(Employee employee)
{
var correlationId = Guid.NewGuid().ToString("N");
using var _ = _logger.BeginScope(new { CorrelationId = correlationId });
_logger.LogInformation("Session started: {OperatorId} {BadgeId}",
employee.BadgeId, employee.BadgeId);
}
}
Output:
{
"@t": "2026-07-02T14:30:00.123Z",
"@l": "Information",
"@mt": "Loading panel with {Gripper}",
"Gripper": "vacuum_a",
"CorrelationId": "a1b2c3d4e5f6789012345678abcdef90",
"OperatorId": 42,
"MachineName": "SPV-LAG-01"
}
Vantaggio: si può tracciare esattamente cosa ha fatto un operatore durante il suo turno — "quale comando ha causato l'allarme?", "quanto tempo tra login e primo job?".
Il trace context attraversa i layer dello stack (UI → Services → Driver → Client → PLC). System.Diagnostics.Activity gestisce il propagation automaticamente se i layer sono nello stesso processo:
// Services layer
public async Task ExecuteProgramAsync(string programId)
{
using var activity = SistecDiagnostics.ActivitySource.StartActivity("ExecuteProgram");
activity?.SetTag("program.id", programId);
try
{
await _driver.ExecuteProgramAsync(programId);
activity?.SetStatus(ActivityStatusCode.Ok);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
Per chiamate cross-process (API REST a MES/ERP), il trace context va propagato via W3C TraceContext header.
| Livello | Uso | In produzione |
|---|---|---|
| TRACE | Dettaglio fine: ogni singolo pacchetto TCP, polling OPC UA | ❌ Disabilitato (solo debug locale) |
| DEBUG | Dettaglio sviluppo: chiamate interne, valori intermedi | ❌ Disabilitato (si attiva su richiesta) |
| INFORMATION | Eventi di business: login, job completato, comando inviato | ✅ Default |
| WARNING | Situazioni anomale ma recuperabili: riconnessione, buffer quasi pieno | ✅ Default |
| ERROR | Operazione fallita: comando non eseguito, DB non raggiungibile | ✅ Default |
| CRITICAL | Sistema inutilizzabile: crash, OPC UA down, HMI deve riavviare | ✅ Sempre, anche se logging è down |
Regola: passare a DEBUG solo durante la risoluzione incidenti, mai permanentemente.
| MAI loggare | Perché |
|---|---|
| Password o PIN | Anche hashate — non devono mai apparire in output log |
| Badge RFID completo | Solo ultime 4 cifre se serve audit |
| Token di sessione | Equivalente alla password |
| Dati personali (PII) | Nome completo, email, matricola interna |
| Chiavi crittografiche | Mai, in nessun formato |
Ogni stack implementa IHealthCheck da Microsoft.Extensions.Diagnostics.HealthChecks. Il Composition Root aggrega tutti gli health check in un cruscotto unificato:
flowchart LR
subgraph Stacks["HEALTH CHECKS PER STACK"]
KUKA["KukaHealthCheck
Ping KRC TCP"]
SAFAN["SafanHealthCheck
Stato connessione pressa"]
PLC["PlcHealthCheck
OPC UA session state"]
DB["DbHealthCheck
SELECT 1 su MySQL"]
ALARM["AlarmHealthCheck
Alarm flood detector"]
end
AGG["HealthAggregator
Composizione risultati"]
DASH["Dashboard locale
Avalonia HealthPage"]
ALERT["Alert engine
Critical → azione immediata"]
KUKA --> AGG
SAFAN --> AGG
PLC --> AGG
DB --> AGG
ALARM --> AGG
AGG --> DASH
AGG --> ALERT
public class KukaHealthCheck : IHealthCheck
{
private readonly IKrcClient _krcClient;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct)
{
try
{
bool connected = await _krcClient.PingAsync(ct);
return connected
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("KUKA KRC not reachable");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("KUKA check failed", ex);
}
}
}
public static IServiceCollection AddHealthChecks(this IServiceCollection services)
{
services.AddHealthChecks()
.AddCheck<KukaHealthCheck>("KUKA", tags: ["kuka", "robot"])
.AddCheck<SafanHealthCheck>("Safan", tags: ["press", "critical"])
.AddCheck<PlcHealthCheck>("PLC", tags: ["plc", "critical"])
.AddCheck<DbHealthCheck>("Database", tags: ["infra"])
.AddCheck<AlarmHealthCheck>("Alarms", tags: ["alarm"]);
return services;
}
| Stato | Colore | Azione |
|---|---|---|
| Healthy | Verde | Normale operatività |
| Degraded | Giallo | Performance ridotta, ma operativo |
| Unhealthy | Rosso | Componente non funzionante → alert |
Il mapping è esposto nella dashboard locale e via API REST (GET /api/v1/health).
Riferimento codebase: ReconnectAgent.cs, KrcClient.cs, ModbusConnector.cs, OpcUaClient.cs — nessuno implementa health check standardizzato.
Ogni stack espone metriche via System.Diagnostics.Metrics. FAEL referenzia già System.Diagnostics.DiagnosticSource 10.0.6 in Directory.Packages.props:29 ma non lo usa.
public static class SistecMetrics
{
public static readonly Meter ProductionMeter = new("Sistec.Production", "1.0.0");
public static readonly Counter<int> PartsProduced =
ProductionMeter.CreateCounter<int>("parts.produced",
description: "Pezzi prodotti");
public static readonly Histogram<double> CycleTime =
ProductionMeter.CreateHistogram<double>("cycle.time_ms",
unit: "ms", description: "Tempo ciclo per pezzo");
public static readonly Counter<int> CommandsSent =
ProductionMeter.CreateCounter<int>("commands.sent",
description: "Comandi inviati ai macchinari");
}
// Uso in Production.Stack
public async Task RecordPartProduced(string machineId, double cycleTimeMs)
{
SistecMetrics.PartsProduced.Add(1,
new KeyValuePair<string, object?>("machine", machineId));
SistecMetrics.CycleTime.Record(cycleTimeMs,
new KeyValuePair<string, object?>("machine", machineId));
}
Con Redis opzionale, i contatori produzione usano `INCR` atomico su `sistec:{plant}:production:{counter}` (es. `parts.produced`, `scrap.count`). Due pannelli che producono simultaneamente non generano race condition. Il valore Redis viene flushato su MySQL periodicamente. La dashboard web (§19.12) legge contatori in tempo reale da Redis senza impattare il DB.
| Stack | Metriche | Frequenza |
|---|---|---|
| Kuka | commands.sent, command.latency_ms, connection.state (0/1) |
Per comando |
| PLC | tags.read_count, tags.write_count, session.age_sec |
1s |
| Production | parts.produced, cycle.time_ms, scrap.count |
Per evento |
| JobMgmt | jobs.completed, jobs.failed, jobs.duration_ms |
Per job |
| Alarms | alarms.raised, alarms.ack_time_ms |
Per allarme |
| System | memory.mb, cpu.percent, disk.free_mb |
10s |
Le metriche sono esposte in due modalità:
GET /api/v1/metrics) per la dashboard HMI internaSistec.Stack.Cloud, esportate via OpenTelemetry Protocol (OTLP) o Prometheus scraping{
"Metrics": {
"ExportMode": "Local",
"OtlpEndpoint": "",
"OtlpHeaders": {},
"LocalPort": 5001
}
}
Riferimento codebase: Sistec.Core/Utils/PropertyNotifier.cs già notifica cambi valore — punto di hook naturale per metriche senza modificare i consumer.
public static class SistecDiagnostics
{
public static readonly ActivitySource ActivitySource = new("Sistec", "1.0.0");
}
Ogni operazione cross-stack (UI → Services → Driver → Client → PLC) è un trace:
public async Task ExecuteProgramAsync(string programId)
{
using var activity = SistecDiagnostics.ActivitySource.StartActivity(
ActivityKind.Internal,
tags: new[] {
KeyValuePair.Create<string, object?>("program.id", programId),
KeyValuePair.Create<string, object?>("machine.type", "Safan")
});
try
{
await _safanClient.ExecuteProgramAsync(programId);
activity?.SetStatus(ActivityStatusCode.Ok);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error);
activity?.SetTag("error.message", ex.Message);
throw;
}
}
{stack}.{layer}.{operation}
kuka.driver.loadPanel
safan.client.executeProgram
plc.services.readTags
production.services.recordPart
Il collector OpenTelemetry è opzionale. Sul PC industriale, si deploya come container Docker o processo Windows separato:
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 1s
memory_limiter:
limit_mib: 256
exporters:
logging:
loglevel: warn
otlp:
endpoint: "cloud-collector:4317"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [logging, otlp]
Gli operatori di campo (collaudatori, manutentori Sistec) hanno bisogno di diagnostica senza deployare una build speciale.
public enum DiagnosticsLevel
{
None, // Produzione normale — solo INFORMATION+
Basic, // + DEBUG per stack specifici
Verbose, // + TRACE per tutto
CrashDump // Cattura minidump su crash
}
public class DiagnosticsService
{
private DiagnosticsLevel _level = DiagnosticsLevel.None;
public void SetLevel(DiagnosticsLevel level, string? stackFilter = null)
{
_level = level;
var switch = new LoggingLevelSwitch
{
MinimumLevel = level switch
{
DiagnosticsLevel.Verbose => LogEventLevel.Verbose,
DiagnosticsLevel.Basic => LogEventLevel.Debug,
_ => LogEventLevel.Information
}
};
}
}
| Metodo | Come |
|---|---|
| File flag | diagnostics.ls nella directory app (come loginSistec.ls) |
| API REST | POST /api/v1/diagnostics/level { "level": "Verbose" } (autenticata) |
| USB key | Inserendo USB Sistec-DIAG → abilita diagnostics e scrive log sulla USB |
| Remote | Via Cloud.Stack, se configurato |
Ogni stack produce un heartbeat periodico. Se manca per più del timeout, scatta allarme:
public class HeartbeatService : BackgroundService
{
private readonly ILogger<HeartbeatService> _logger;
private readonly IHealthAggregator _health;
private readonly TimeSpan _interval = TimeSpan.FromSeconds(30);
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(_interval, ct);
var status = await _health.GetCompositeStatusAsync(ct);
_logger.LogInformation("Heartbeat: {Status} {Timestamp:R}",
status, DateTime.UtcNow);
if (status == CompositeHealthStatus.Unhealthy)
_logger.LogCritical("System unhealthy: {Report}",
await _health.GetReportAsync(ct));
}
}
}
All'avvio, l'HMI esegue self-diagnostics:
IHealthCheck di ogni stack — tutti Healthy?Risultato: report JSON salvato in startup.diagnostics.json, visibile in dashboard.
| SLI | SLO | Misura |
|---|---|---|
| Uptime HMI | ≥ 99.5% (99.9% target) | Processo HMI attivo / tempo totale |
| Command latency | p95 ≤ 500ms | Tempo UI → risposta macchina |
| Tag read freshness | ≤ 100ms | Tempo tra lettura PLC e UI aggiornata |
| Alarm notification | p99 ≤ 2s | Tempo tra evento e UI alarm banner |
| Crash frequency | ≤ 1/30 giorni | Crash non dovuti a HW |
Il PC industriale deve continuare a funzionare anche dopo un crash:
public class CrashHandler
{
public void Attach()
{
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
{
var dumpPath = Path.Combine(
AppContext.BaseDirectory, "crashdumps",
$"crash_{DateTime.UtcNow:yyyyMMdd_HHmmss}.dmp");
Directory.CreateDirectory(Path.GetDirectoryName(dumpPath)!);
MiniDumpWriter.Write(dumpPath, MiniDumpType.WithDataSegs);
_logger.LogCritical("Crash: {Exception}, dump: {Path}",
args.ExceptionObject, dumpPath);
if (args.IsTerminating)
RestartApplication();
};
}
}
| Aspetto | Dettaglio |
|---|---|
| Dump type | Minidump con data segments |
| Retention | Ultimi 10 crash dump, cancella i più vecchi |
| Auto-restart | Servizio Windows FailureAction = Restart |
| Cloud | Se configurato, upload dump → cloud storage |
| Security | Cifrare dump prima di upload (può contenere dati sensibili) |
Ogni allarme in Sistec.Stack.Alarms è correlato ai log tramite CorrelationId:
public record AlarmEvent(
string Id,
string CorrelationId, // ← link ai log
DateTime RaisedAt,
// ...
);
Flusso di diagnostica:
alarms/{id} → mostra log correlatiCorrelationId → tutti gli eventi prima/dopo l'allarme| Criterio | Logs | Metrics | Traces | Crash Dump |
|---|---|---|---|---|
| Retention hot | 30 giorni | 7 giorni | 1 ora | 30 giorni |
| Retention cold | 1 anno | 3 mesi (aggregate) | — | 1 anno |
| Rotazione | Giornaliera | Sovrascrittura | Sovrascrittura | Per evento |
| Dimensione max | 500 MB per file | 50 MB per snapshot | 100 MB buffer | 100 MB per dump |
Cloud (opzionale): se Cloud.Stack presente, invia log critici e metriche aggregate a Elasticsearch / Prometheus.
| Operazione | Target | Budget | Oltre budget |
|---|---|---|---|
| Comando KUKA → risposta | ≤ 300ms | DEBUG | WARNING |
| Lettura tag PLC → UI | ≤ 100ms | DEBUG | WARNING |
| Scrittura tag PLC | ≤ 50ms | DEBUG | WARNING |
| Login operatore | ≤ 1s | INFORMATION | WARNING |
| Salvataggio job DB | ≤ 200ms | INFORMATION | WARNING |
Il budget è monitorato dalle metriche (command.latency_ms) e visibile in dashboard.
PC Industriale (Windows 10/11 IoT)
├── WSL2 (Debian)
│ ├── Docker Engine
│ │ ├── Container CODESYS (Next, vPLC1, vPLC2, ...)
│ │ └── Container KUKA / Sinumerik / edge
│ └── codesys.sh (script gestione)
├── HMI C# (servizio Windows / applicazione)
├── MySQL (database locale)
├── Firewall (regole manuali)
└── Utenti (configurazione manuale)
Criticità: setup manuale (2-4 ore), documentazione assente, variabilità tra PC, nessun rollback.
flowchart LR
subgraph Tech["Tecnico / CI/CD"]
A1["Lancia playbook
`ansible-playbook site.yml`"]
end
subgraph Repo["Repository Ansible
(versionato su git)"]
R1["inventory/hosts.ini
IP, credenziali vault"]
R2["roles/common/
hostname, utenti, firewall"]
R3["roles/wsl_docker/
WSL2 + Docker + CODESYS"]
R4["roles/mysql/
MySQL install + DB + utenti"]
R5["roles/hmi_app/
Deploy HMI + servizio Windows"]
R6["roles/monitoring/
Heartbeat, log forwarding"]
end
subgraph PC["PC Industriale (WinRM)"]
P1["Hostname + IP statico"]
P2["Firewall + Utenti"]
P3["WSL2 + Docker + containers"]
P4["MySQL + database + dump"]
P5["HMI .exe + appsettings.json + servizio"]
P6["Certificati + monitoring"]
end
A1 --> Repo
Repo -->|WinRM| PC
| Componente | Modulo Ansible | Stato |
|---|---|---|
| Hostname, IP, DNS | win_hostname, win_ip_address |
✅ Automabile |
| Firewall | win_firewall_rule |
✅ Automabile |
| Utenti Windows | win_user, win_group |
✅ Automabile |
| MySQL install DB utenti dump | win_chocolatey + mysql_db + mysql_user |
✅ Automabile |
| .NET Runtime | win_chocolatey |
✅ Automabile |
| Deploy HMI + servizio | win_copy + win_template + win_service |
✅ Automabile |
| appsettings.json | win_template (parametrizzato) |
✅ Automabile |
| WSL2 | win_dism + win_shell |
✅ Automabile (con reboot) |
| Container CODESYS | win_template (compose) + win_shell |
✅ Automabile |
| Certificati TLS | win_certificate |
✅ Automabile |
| Attivazione licenza CODESYS | — | ❌ Manuale (License Manager GUI) |
| Deploy progetto PLC | — | ❌ Manuale (CODESYS IDE) |
| BIOS/UEFI | — | ❌ Manuale |
ansible-industrial-pc/
├── inventory/
│ ├── production/
│ │ ├── hosts.ini
│ │ └── group_vars/
│ │ ├── industrial_pcs.yml # vars comuni
│ │ └── secrets.yml # vault-crittografato
│ └── staging/
│ └── hosts.ini
├── roles/
│ ├── common/ # hostname, IP, utenti, firewall
│ ├── wsl_docker/ # WSL2 + Docker + CODESYS containers
│ ├── mysql/ # MySQL install + DB + utenti
│ ├── hmi_app/ # deploy HMI C# + servizio
│ └── monitoring/ # heartbeat, log forwarding
├── playbooks/
│ ├── site.yml # playbook principale
│ ├── reset-demo-timer.yml
│ └── healthcheck.yml
├── ansible.cfg
├── requirements.yml
└── vault-password # (escluso da git)
# inventory/production/hosts.ini
[industrial_pcs]
pc-linea-01 ansible_host=192.168.1.101 ansible_connection=winrm
pc-linea-02 ansible_host=192.168.1.102 ansible_connection=winrm
[industrial_pcs:vars]
ansible_user=Administrator
ansible_password=!vault |
ansible_winrm_transport=ntlm
ansible_winrm_server_cert_validation=ignore
# inventory/production/group_vars/industrial_pcs.yml
# --- Rete ---
dns_servers:
- 192.168.1.10
# --- Firewall ---
firewall_rules:
- { name: CODESYS OPC-UA, port: 4840, protocol: tcp }
- { name: MySQL, port: 3306, protocol: tcp }
- { name: HMI API, port: 5000, protocol: tcp }
# --- CODESYS ---
codesys_containers:
- { name: vPLC1, ip: 172.40.0.3, port: 4851 }
- { name: vPLC2, ip: 172.40.0.4, port: 4852 }
- { name: Next, ip: 172.40.0.9, port: 4857 }
# --- HMI per commessa ---
hmi_app_name: Sistec.5315.LAG
hmi_app_path: C:\Program Files\Sistec\5315.LAG
hmi_port: 5000
hmi_nuget_version: 4.3.*
# playbooks/site.yml
- name: Configurazione completa PC industriale
hosts: industrial_pcs
gather_facts: yes
roles:
- role: common
tags: [common, always]
- role: mysql
tags: [mysql]
when: install_mysql | default(true)
- role: hmi_app
tags: [hmi]
when: install_hmi | default(true)
- role: wsl_docker
tags: [wsl, docker, codesys]
when: install_codesys | default(true)
- role: monitoring
tags: [monitoring]
hmi_app — Deploy HMI- name: Installa .NET Runtime 8.0
community.windows.win_chocolatey:
name: dotnet-8.0-runtime
state: present
- name: Deploy applicazione HMI
ansible.windows.win_copy:
src: "files/{{ hmi_app_name }}/"
dest: "{{ hmi_app_path }}\\"
force: yes
- name: Template appsettings.json
ansible.windows.win_template:
src: appsettings.json.j2
dest: "{{ hmi_app_path }}\\appsettings.json"
- name: Installa servizio Windows
ansible.windows.win_shell: |
sc create {{ hmi_app_name }} binPath="{{ hmi_app_path }}\{{ hmi_app_name }}.exe --serve"
sc failure {{ hmi_app_name }} reset=86400 actions=restart/5000/restart/10000/restart/30000
- name: Avvia servizio
ansible.windows.win_service:
name: "{{ hmi_app_name }}"
state: started
start_mode: auto
wsl_docker — CODESYS Containers- name: Abilita WSL
ansible.windows.win_dism:
features:
- VirtualMachinePlatform
- Microsoft-Windows-Subsystem-Linux
state: present
- name: Deploy docker-compose files
ansible.windows.win_template:
src: "compose.{{ item.name }}.j2"
dest: "C:\\ProgramData\\Sistec\\codesys\\{{ item.name }}.yaml"
loop: "{{ codesys_containers }}"
- name: Avvia container CODESYS
ansible.windows.win_shell: |
wsl -d Debian docker compose -f /mnt/c/ProgramData/Sistec/codesys/{{ item.name }}.yaml up -d
loop: "{{ codesys_containers }}"
common — Hostname + Utenti + Firewall- name: Imposta hostname
ansible.windows.win_hostname:
name: "{{ inventory_hostname | upper }}"
- name: Crea utenti locali
ansible.windows.win_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
groups: "{{ item.groups }}"
password_never_expires: yes
loop: "{{ local_users }}"
- name: Regole firewall
ansible.windows.win_firewall_rule:
name: "{{ item.name }}"
localport: "{{ item.port }}"
protocol: tcp
action: allow
loop: "{{ firewall_rules }}"
La pipeline HMI commessa (sezione 13.6) si conclude con la pubblicazione dell'artifact. Ansible può prelevare quell'artifact e deployarlo:
flowchart LR
CI["CI/CD: build HMI"] -->|publish artifact| Artifact["Release EXE + DLL + JSON"]
Artifact -->|Ansible win_copy| PC1["PC Industriale 1"]
Artifact -->|Ansible win_copy| PC2["PC Industriale 2"]
Artifact -->|Ansible win_copy| PC3["PC Industriale 3"]
# In pipeline CI/CD (stage finale)
- task: Ansible@1
inputs:
playbook: 'deploy-hmi.yml'
inventory: 'inventory/production/hosts.ini'
args: |
--extra-vars "hmi_version=$(Build.BuildNumber)"
--vault-password-file vault-password
| Aspetto | Manuale | Ansible |
|---|---|---|
| Tempo setup PC | 2-4 ore | 10-15 minuti |
| Ripetibilità | ~70% | 100% |
| Documentazione | Wiki/email sparse | Playbook versionato su git |
| Rollback | Reinstallare tutto | git checkout + esecuzione |
| Onboarding tecnico | Settimane | 1-2 giorni |
| Errori umani | Frequenti | Quasi zero |
wsl --install va fatto almeno una volta a manoL'HMI deve supportare aggiornamenti automatici sul campo: il tecnico Sistec pubblica un rilascio, i PC industriali in fabbrica si aggiornano da soli. Nessun rilascio deve mai rompere API pubbliche — ogni stack NuGet pubblicato con major uguale è 100% compatibile all'indietro.
CI/CD ──► NuGet Feed ──► Release Pipeline ──► Installer ──► Update Server
│ │
│ ▼
│ PC Industriale
│ ├── HMI.exe (running)
│ ├── Sistec.*.Stack.dll
│ └── UpdateAgent.exe
│
└── NuGet versionato (SemVer) ──────────► App castiga versione
| Componente | Ruolo | Tecnologia |
|---|---|---|
| Installer | Prima installazione + registrazione servizio/aggiornamenti | WiX Toolset / Inno Setup |
| UpdateAgent | Servizio Windows che controlla, scarica e applica aggiornamenti | Squirrel.Windows o custom |
| Update Server | Serve il manifest delle versioni (appcast.xml) | Static file HTTP / CDN |
| Rollback | Backup dell'installazione precedente | Cartella previous/ + restore point |
Regola assoluta: ogni pacchetto NuGet con stesso Major.Minor è backward-compatible.
| Scenario | Versione | Compatibilità |
|---|---|---|
| Bug fix | 2.3.0 → 2.3.1 |
100% — solo internals |
| Nuova feature | 2.3.0 → 2.4.0 |
100% — aggiunte, mai rimosse |
| Breaking change | 2.3.0 → 3.0.0 |
Solo con major bump esplicito |
❌ Rimuovere un metodo pubblico
❌ Cambiare firma di un metodo pubblico
❌ Rimuovere o rinominare una classe pubblica
❌ Cambiare tipo di una proprietà pubblica
❌ Aggiungere un parametro obbligatorio a un'interfaccia
❌ Cambiare default di un comportamento esistente
✅ Aggiungere nuovi metodi (con default implementation se interfaccia)
✅ Aggiungere nuove classi/enum/interfacce
✅ Deprecare (ma non rimuovere) con [Obsolete]
✅ Cambiare implementazione interna (se il comportamento esterno è identico)
✅ Aggiungere parametri opzionali
# Stage: ApiCompat — blocca la build se rileva breaking changes
- stage: ApiCompat
displayName: '🔒 API Compatibility Check'
jobs:
- job: CheckCompat
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'custom'
custom: 'package'
arguments: 'src/$(ProjectName)/$(ProjectName).csproj
-o $(Build.ArtifactStagingDirectory)/pkg'
- script: |
# Confronta API pubbliche con l'ultima versione pubblicata
dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool
dotnet api-compat check --base-nuget $(ProjectName) --base-version $(LastMajor).$(LastMinor).0
displayName: 'Check API Compatibility'
Se ApiCompat fallisce e Major non è stato incrementato → build bloccata.
┌─────────────────────────────────────────────────────────┐
│ RILASCIO │
│ │
│ 1. Commit su main → build pipeline │
│ 2. Test unitari + integrazione + SonarQube │
│ 3. API Compat Check (contro ultima versione NuGet) │
│ 4. Quality Gate superato? → NuGet publish │
│ │ │
│ ├── Se Major invariato → pacchetto backward-compat. │
│ └── Se Major incrementato → breaking change │
│ esplicito (richiede approval manuale) │
│ │
│ 5. Release Pipeline: │
│ ├── Aggiorna appcast.xml (versione + download URL) │
│ ├── Crea installer .msi/.exe │
│ └── Pubblica su Update Server │
│ │
│ 6. PC Industriali (UpdateAgent): │
│ ├── Ogni ora: GET appcast.xml │
│ ├── Versione locale < versione remota? → download │
│ └── Applica aggiornamento (HMI si riavvia) │
└─────────────────────────────────────────────────────────┘
// UpdateAgent.exe — servizio Windows
public class UpdateAgent : BackgroundService
{
private readonly HttpClient _http;
private readonly IConfiguration _config;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var manifest = await FetchManifestAsync(ct);
if (manifest.Version > CurrentVersion)
{
_logger.LogInformation("Update found: {Old} → {New}",
CurrentVersion, manifest.Version);
// Scarica nuovo installer
var pkgPath = await DownloadAsync(manifest.DownloadUrl, ct);
// Backup installazione corrente
Directory.Move(InstallDir, BackupDir);
// Esegui nuovo installer (silent)
using var proc = Process.Start(new ProcessStartInfo
{
FileName = pkgPath,
Arguments = "/verysilent /dir=" + InstallDir,
Wait = true
});
// Se successo → riavvia HMI
// Se fallito → restore da backup
if (proc.ExitCode == 0)
{
RestartHmi();
}
else
{
Rollback(BackupDir, InstallDir);
}
}
await Task.Delay(CheckInterval, ct); // default: 1 ora
}
}
}
| Aspetto | Scelta | Perché |
|---|---|---|
| Tecnologia | WiX Toolset 5 (o Inno Setup 6) | Maturo, silent install, integrazione CI/CD |
| Cosa installa | HMI.exe + DLL + config + servizio UpdateAgent | Un unico pacchetto .msi |
| Registro di sistema | HKLM\Software\Sistec\HMI\InstallDir, HKLM\Software\Sistec\HMI\Version |
L'UpdateAgent legge la versione dal registro |
| Servizio Windows | UpdateAgent.exe installato come servizio | Parte all'avvio del PC, controlla aggiornamenti |
| Shortcut | Start Menu + Desktop (opzionale) | Per avvio manuale |
| Firewall | Regola in ingresso/uscita per UpdateAgent | Comunicazione HTTP(S) con Update Server |
| Silent install | msiexec /i Sistec.HMI.msi /qn |
Deploy automatizzato via Ansible |
Minimal setup: un file appcast.xml su un server HTTP statico (NAS aziendale, bucket S3, o anche una share di rete).
<?xml version="1.0" encoding="utf-8"?>
<appcast>
<item>
<title>Sistec HMI 2.4.1</title>
<version>2.4.1</version>
<pubDate>2026-07-15</pubDate>
<enclosure url="https://updates.sistec.it/hmi/Sistec.HMI.2.4.1.msi"
length="42873621" type="application/octet-stream" />
<releaseNotes>Bug fix reconnect KUKA, nuove statistiche operatore</releaseNotes>
<critical>false</critical> <!-- Se true, aggiornamento forzato -->
</item>
</appcast>
| Deployment | Pro | Contro |
|---|---|---|
| NAS aziendale | Zero costi cloud, LAN veloce | Non accessibile da remoto |
| S3 / CDN | Accessibile ovunque, versioning, logging | Costo mensile |
| Share di rete | Semplice, nessun server | Richiede VPN per remoto |
| Canale | Versione | UpdateAgent controlla | Uso |
|---|---|---|---|
| Stable | Major.minor.patch | Ogni ora | Produzione |
| Beta | Major.minor.patch-betaN | Solo se flag beta=true in config |
Collaudo in fabbrica |
| Nightly | Build del giorno | Mai (solo sviluppo) | Test interni |
// appsettings.json dell'UpdateAgent
{
"UpdateAgent": {
"CheckIntervalMinutes": 60,
"UpdateServerUrl": "https://updates.sistec.it/hmi/appcast.xml",
"Channel": "stable", // "stable" | "beta" | "nightly"
"AutoRestart": true,
"AutoRestartWindow": "02:00-04:00", // Riavvio solo in finestra notturna
"RollbackRetention": 2 // Tiene ultime 2 versioni per rollback
}
}
Se un aggiornamento fallisce o viene rilevato come instabile:
Scenario:
1. UpdateAgent scarica v2.4.1, backup v2.4.0 in C:\Sistec\HMI\previous\v2.4.0
2. Installa v2.4.1
3. HMI si riavvia
4. Se HMI crasha entro 5 minuti → UpdateAgent ripristina v2.4.0 automaticamente
5. Se HMI funziona → previous\v2.4.0 viene rimosso dopo 7 giorni
Rollback manuale (da remoto, tramite Ansible):
- name: Rollback HMI to previous version
win_shell: |
$backup = Get-ChildItem "C:\Sistec\HMI\previous" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($backup) {
Stop-Service SistecHmi
Stop-Service UpdateAgent
Remove-Item "C:\Sistec\HMI\current" -Recurse -Force
Copy-Item $backup.FullName "C:\Sistec\HMI\current" -Recurse
Start-Service SistecHmi
Start-Service UpdateAgent
}
# Aggiunto alla pipeline HMI Commessa (sezione 14.6)
- stage: CreateInstaller
displayName: '📦 Create Installer'
dependsOn: Publish
jobs:
- job: BuildInstaller
steps:
- task: NuGetToolInstaller@1
- script: |
# Compila WiX project
candle.exe installer/SistecHmi.wixproj
light.exe installer/SistecHmi.wixobj -out $(Build.ArtifactStagingDirectory)/Sistec.HMI.$(Version).msi
displayName: 'Build MSI Installer'
- stage: UpdateAppcast
displayName: '📰 Update Appcast'
dependsOn: CreateInstaller
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: UpdateAppcast
steps:
- script: |
# Aggiorna appcast.xml con nuova versione + upload a S3
$appcast = Join-Path $(Build.ArtifactStagingDirectory) "appcast.xml"
@"
<?xml version="1.0" encoding="utf-8"?>
<appcast>
<item>
<version>$(Version)</version>
<enclosure url="https://updates.sistec.it/hmi/Sistec.HMI.$(Version).msi"
length="$(InstallerSize)" type="application/octet-stream" />
<releaseNotes>$(Build.SourceVersionMessage)</releaseNotes>
</item>
</appcast>
"@ | Set-Content $appcast
aws s3 cp $appcast s3://sistec-updates/hmi/appcast.xml
aws s3 cp "$(Build.ArtifactStagingDirectory)/Sistec.HMI.$(Version).msi" s3://sistec-updates/hmi/
displayName: 'Publish to Update Server'
- stage: Deploy
displayName: '🚀 Ansible Deploy'
dependsOn: UpdateAppcast
jobs:
- job: TriggerAnsible
steps:
- script: |
# Ansible legge appcast.xml → deploya la nuova versione
ansible-playbook -i inventory/hosts.ini playbooks/deploy-hmi.yml
displayName: 'Trigger Ansible Deploy'
Fase 5 — Installer + Update System (3-4 settimane)
├── Setup WiX / Inno Setup project
├── UpdateAgent servizio Windows
├── appcast.xml + Update Server
├── API Compat Check nella pipeline
├── Rollback automatico e manuale
└── Canali stable/beta/nightly
Tempo stimato: 3-4 settimane, da eseguire in parallelo agli stack applicativi (Fase 3). L'UpdateAgent può essere sviluppato e testato come progetto separato fin da Fase 1 (Fondazioni) perché non dipende da nessuno stack macchina.
L'analisi approfondita delle codebase LAG e FAEL ha rivelato ulteriori aree di miglioramento non coperte nelle sezioni precedenti. Questa sezione raccoglie idee complementari all'architettura a stack verticali, verificate sul codice esistente.
Oggi: ReconnectAgent.cs (Sistec.Core/Utils/ReconnectAgent.cs:24) ha timeout 10.000ms hardcoded e retry lineare. Nessun circuit breaker. Le classi di rete (TcpClient, KrcClient, ModbusConnector) espongono solo IsConnected booleano sparso.
Proposta: Sostituire ReconnectAgent con Polly CircuitBreakerPolicy + RetryPolicy + FallbackPolicy. Health check system spostato in §16.6.
Riferimento codebase: ReconnectAgent.cs, KrcClient.cs, ModbusConnector.cs, OpcUaClient.cs — nessuno implementa circuit breaker standardizzato.
Oggi: l'HMI è un sistema completamente chiuso. Nessun HttpClient, nessun Controller, nessun [Route] in nessuna delle due codebase. Per integrarsi con un MES o ERP serve accedere direttamente al database MySQL — accoppiamento forte.
Proposta: Minimal API ASP.NET Core in-process su localhost:5000:
// Sistec.Infra.Api — leggero, esposto solo su localhost
app.MapGet("/api/v1/production/active-job", async (IJobTracker jobs) =>
await jobs.GetActiveAsync());
app.MapGet("/api/v1/machines/kuka/status", async (IKukaRobotLogic kuka) =>
new { connected = await kuka.IsConnectedAsync(), program = await kuka.GetCurrentProgramAsync() });
app.MapGet("/api/v1/alarms", async (IAlarmService alarms) =>
await alarms.GetActiveAlarmsAsync());
Sistec.Bus (FAEL, Zebus 3.14.1) può fare da bus interno tra stack e API, evitando accoppiamento diretto.
Riferimento codebase: Zero API esistenti. Zebus in Sistec.Bus/ (FAEL) sarebbe il bus naturale.
Il problema aperto di §2.13 (gestione varianti AB/C/BS) è oggi risolto con progetti .csproj separati e if/else su CellType. FAEL ha PalletLogic_AB.cs vs PalletLogic_C.cs (in Common/Logic/), codici pressa diversi, logiche di produzione differenti.
Proposta: Microsoft.FeatureManagement o un IFeatureFlagService custom:
{
"features": {
"HasEsaPress": false,
"HasConveyor": false,
"HasQualityCheck": true,
"PalletCount": 9,
"CellType": "LAG"
}
}
public async Task ExecuteProductionStep(StepType step)
{
await _kuka.LoadPanel("vacuum_a");
if (await _features.IsEnabledAsync("HasEsaPress"))
await _esa.Press("PROG_456");
else
await _safan.Bend("P123", 90);
if (await _features.IsEnabledAsync("HasConveyor"))
await _conveyor.Advance();
}
Vantaggio: un unico progetto HMI, zero copie. Ogni commessa = file JSON, non un nuovo .csproj. Il RecipeEngine (§9.1) assorbe naturalmente questa logica.
Riferimento codebase: Common/Logic/PalletLogic_AB.cs, PalletLogic_C.cs (FAEL); if/else su CellType sparsi.
Oggi: la configurazione è letta una volta all'avvio da file INI/XML (Config/Kuka.ini, PLC.ini, PressBrake.ini). Per cambiare un parametro serve riavviare l'HMI → fermo produzione.
Microsoft.Extensions.Configuration è già referenziato da Sistec.Core/Sistec.Core.csproj:22, ma nessun codice usa IOptionsMonitor<T> o .GetReloadToken().
Proposta: File JSON con reloadOnChange: true. IOptionsMonitor<T> propaga le modifiche a caldo:
public class KukaOptions
{
public int ReconnectTimeoutMs { get; set; } = 10000;
public int CommandTimeoutMs { get; set; } = 5000;
}
// IOptionsMonitor notifica quando il file cambia su disco
public class KukaRobotLogic
{
public KukaRobotLogic(IOptionsMonitor<KukaOptions> options)
{
options.OnChange(opt => _logger.LogInformation("Kuka options reloaded"));
}
}
Riferimento codebase: AB/Config/Kuka.ini, PLC.ini, PressBrake.ini letti staticamente; nessun IOptionsMonitor.
Il contenuto di questa sezione è stato spostato in §16.7 (Metrics) e §16.8 (Distributed Tracing). La presente sezione è mantenuta come riferimento per i codebase LAG/FAEL.
Riferimento codebase: Directory.Packages.props:29 (FAEL, DiagnosticSource già presente); Sistec.Core/Utils/PropertyNotifier.cs già notifica cambi valore — punto di hook naturale per metriche senza modificare i consumer.
LAG ha 2 violazioni in produzione che bloccano il thread pool:
Sistec.Core/Tags/TagValue.cs:234: Task.Run(async () => { ... }).Result;Sistec.Opc.Ua/OpcUaTagValue.cs:212: Task.Run(async () => { ... }).Result;FAEL ha risolto con TagValueBase<T> (coda asincrona con SemaphoreSlim), ma LAG no.
Proposta: Refactor del write path per essere async fino in fondo. Allineare LAG alla soluzione FAEL (TagValueBase<T>). Nella greenfield: vietare .Result/.Wait() con analizzatore Roslyn custom:
<Analyzer Include="$(NuGetPackageRoot)\Microsoft.CodeAnalysis.BannedApiAnalyzers\*\analyzers\*.dll">
<BannedSymbols>Task.Result;Task.Wait;Task.GetAwaiter().GetResult</BannedSymbols>
</Analyzer>
Riferimento codebase: TagValue.cs:234 (LAG), OpcUaTagValue.cs:212 (LAG); TagValueBase.cs:29-41 (FAEL, soluzione già pronta).
Il documento §6.1㉑ segnala 13 copie di FakeOpcUa.cs in FAEL. La verifica ha confermato esattamente 13 file in:
Test/ResetSeq/, Test/ProgramManagement/, Test/Maintenance/DUT/, Test/LinkedRollerTest/DVars/, Test/PalletsState/, Test/CutPlan/, Test/BS_PLC/, Test/Test_Safety_AB/FakePlc/, Test/AnalogScaling/, Test/Test_Safety/, Test/Test_DialogTemplate/, Sistec.HMI/C/FakePlc/, Sistec.HMI/BS/Proposta: Sistec.Infra.TestHelpers (NuGet di test):
// Unico FakeOpcUa, usato da tutti
public class FakeOpcUa : IUAClient
{
public Dictionary<string, object> Tags { get; } = new();
public Task<bool> ConnectAsync(string endpoint) => Task.FromResult(true);
public Task<T> ReadTagAsync<T>(string tag) => Task.FromResult((T)Tags[tag]);
public Task WriteTagAsync<T>(string tag, T value) { Tags[tag] = value; return Task.CompletedTask; }
}
Include anche: FakeKrcClient, FakePlcClient, MockLogger<T>, FakeSynchronizationContext. [InternalsVisibleTo] nei progetti stack per mock senza reflection.
Riferimento codebase: 13 file FakeOpcUa.cs sparsi; Test/ (FAEL) ha 48 sottodirectory.
FAEL ha implementato una coda writes personalizzata in TagValueBase.cs:29-41 con SemaphoreSlim + _pendingWrite + segnalazione manuale. Il commento al rigo 32 dice:
"Dependency-free (no System.Threading.Channels, which is a separately-versioned framework assembly and is not in-box for netstandard2.1)"
Per net8.0 (o net10.0 per la greenfield), System.Threading.Channels è in-box. Sostituzione:
// OGGI: SemaphoreSlim custom
private readonly SemaphoreSlim _writeSignal = new(0, 1);
private T? _pendingWrite;
private bool _hasPendingWrite;
// DOMANI: Channel<T> standard
private readonly Channel<T> _writeChannel = Channel.CreateBounded<T>(
new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest });
Stessa semantica latest-wins, meno codice, API standard e testata.
Riferimento codebase: TagValueBase.cs:29-41 (FAEL); note app.config binding redirects per System.Threading.Channels già presenti.
Confermato: D:\DEV\5309_FAEL\Sistec.Modbus\ duplica D:\DEV\5309_FAEL\Esa.Client\ con varianti minori:
Sistec.Modbus/PressBrakes.cs vs Esa.Client/PressBrakeCollection.csSistec.Modbus/PressBrakeGade29.cs vs Esa.Client/PressLogicGade29.csSistec.Modbus ha Serilog 4.2.0 (vecchio), Esa.Client non ha SerilogIl progetto HMI AB referenzia solo Esa.Client\Esa.Client.csproj. Sistec.Modbus è morto.
Proposta: Verificare che nessun .csproj referenzi Sistec.Modbus, recuperare eventuali differenze utili (PressBrakes.cs), eliminare la directory.
Riferimento codebase: 5309_FAEL/Sistec.Modbus/ (9 file); 5309_FAEL/Esa.Client/ (implementazione attiva).
Le librerie fondazione (Sistec.Core, Sistec.Opc.Ua, Sistec.Safan, Sistec.Sinumerik) dichiarano netstandard2.1, ma di fatto girano solo su .NET 8+ (i consumer sono net8.0-windows). netstandard2.1 è un vincolo fittizio che:
System.Threading.Channels nativoapp.configrequired, record, primary constructorsProposta: Portare a net8.0 (o net10.0 per la greenfield).
| Codice | Libreria | Uso |
|---|---|---|
Sistec.Common (LAG) |
protobuf-net 3.2.56 |
Protocollo Safan |
Sistec.Core (entrambe) |
System.Text.Json 10.0.6 |
Generico |
| FAEL | Newtonsoft.Json 13.0.3 |
Referenziato ma non importato nei .cs |
Proposta: System.Text.Json per tutto (config JSON, API, persistenza). protobuf-net solo per protocollo binario. Eliminare Newtonsoft.Json.
Oggi i log sono messaggi free-text non aggregabili. Con le OpenTelemetry Semantic Conventions:
// Invece di: _logger.LogInformation("Kuka reconnect attempt {n}", n);
_logger.LogWarning("Machine connection lost, reconnecting");
_metrics.ConnectionLostCounter.Add(1, new TagList { { "sistec.machine.id", "Kuka_0" } });
Convenzioni proposte per Sistec: sistec.machine.id, sistec.cell.job_id, sistec.operator.badge, sistec.alarm.severity.
Nessuna delle due codebase ha .editorconfig, Directory.Build.props, o GlobalUsings.cs. FAEL ha tracce di SonarQube (.sonarqube/out/) ma nessun quality gate integrato nel build.
Proposta:
# .editorconfig — root del repo stack
root = true
[*.cs]
indent_style = space
indent_size = 4
csharp_style_var_for_built_in_types = true:warning
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
dotnet_diagnostic.CA2007.severity = none # ConfigureAwait non necessario per HMI
<!-- Directory.Build.props — versionamento + analizzatori -->
<Project>
<PropertyGroup>
<VersionPrefix>2.3.0</VersionPrefix>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-all</AnalysisLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SonarAnalyzer.CSharp" Version="*" PrivateAssets="all" />
</ItemGroup>
</Project>
Riferimento codebase: Nessun .editorconfig, nessun Directory.Build.props; .sonarqube/out/ (FAEL) come unica traccia di analisi.
Oggi in FAEL, AB e C sono due pultipite distinti nella stesso impianto. Comunicano via Zebus (Sistec.Bus, Zebus 3.14.1) per scambiarsi stato produzione, job, allarmi. Zebus funziona (è in produzione) ma è un framework poco conosciuto, difficile da manutenere e diagnosticare.
Proposta: Affiancare HTTP + Redis a Zebus come alternativa parallela, non sostitutiva. Ogni commessa potrà scegliere il bus più adatto o mantenerli entrambi.
flowchart LR
subgraph Oggi["OGGI — Zebus pub/sub"]
AB1["Pulpito AB
FrmHMI AB"] <-->|Zebus bus| C1["Pulpito C
FrmHMI C"]
BS1["Pulpito BS
Form1 BS"] <-->|Zebus bus| AB1
end
subgraph Domani["DOMANI — Zebus + Redis opzionale"]
AB2["Pulpito AB"] <-->|HTTP REST / SSE| Redis["REDIS
(Stato condiviso
Job, Allarmi,
Produzione)"]
C2["Pulpito C"] <-->|HTTP REST / SSE| Redis
BS2["Pulpito BS"] <-->|HTTP REST / SSE| Redis
AB2 <-->|Zebus bus| C2
BS2 <-->|Zebus bus| AB2
end
Redis funge da stato condiviso in-memory accessibile da tutti i pannelli via rete locale. Ogni pannello pubblica eventi (job completato, allarme, cambio stato) via HTTP e si sottoscrive a canali Redis Pub/Sub per aggiornamenti in tempo reale.
| Componente | Ruolo | Tecnologia |
|---|---|---|
| Redis | Cache distribuita + Pub/Sub per aggiornamenti real-time | stackexchange.redis (NuGet) |
| HTTP API | CRUD su stato condiviso (job, produzione, allarmi) | Minimal API ASP.NET Core su localhost (stesso processo HMI) |
| SSE (Server-Sent Events) | Notifiche push da Redis Pub/Sub al pannello | Microsoft.AspNetCore.ResponseCompression |
| Health Check | Verifica connettività tra pannelli | GET /api/v1/health su ogni pannello |
Redis Key → Value
─────────────────────────────────────────────────────────────
sistec:{plant}:job:active → { "jobId": 42, ... }
sistec:{plant}:job:history → [ {jobId: 41}, {jobId: 40} ]
sistec:{plant}:alarms:active → [ {alarmId: 7}, {alarmId: 8} ]
sistec:{plant}:production:counter → 1547
sistec:{plant}:machines:kuka:status → "running" (da ogni pannello via HTTP)
sistec:{plant}:panels:ab:last-heartbeat → 2026-07-03T10:30:00Z
// AB — dopo aver completato un job
public class JobCompletionPublisher(IConnectionMultiplexer redis)
{
public async Task PublishJobCompletedAsync(JobData job)
{
var db = redis.GetDatabase();
// 1. Salva job in Redis (stato condiviso)
await db.StringSetAsync($"sistec:{_plant}:job:active", JsonSerializer.Serialize(job));
// 2. Pubblica evento su canale Redis → tutti i pannelli ricevono notifica
await db.PublishAsync("channel:job:completed", JsonSerializer.Serialize(new
{
job.Id, job.PartCount, CompletedAt = DateTime.UtcNow, CompletedBy = _currentOperator
}));
}
}
// C — sottoscrizione all'avvio
public class JobEventSubscriber(IConnectionMultiplexer redis)
{
public void Subscribe()
{
var sub = redis.GetSubscriber();
sub.Subscribe("channel:job:completed", (channel, message) =>
{
var evt = JsonSerializer.Deserialize<JobCompletedEvent>(message);
_logger.LogInformation("C sees: Job {Id} completed by operator {Op}", evt.Id, evt.CompletedBy);
_dispatcher.Invoke(() => _jobView.Refresh());
});
}
}
Ogni pannello scrive il proprio heartbeat ogni 5 secondi su Redis con TTL 15 secondi:
// In un BackgroundService su ogni pannello
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await _db.StringSetAsync(
$"sistec:{_plant}:panels:{_panelId}:last-heartbeat",
DateTime.UtcNow.ToString("O"),
expiry: TimeSpan.FromSeconds(15)); // TTL = 15s
await Task.Delay(5000, ct);
}
}
Se il heartbeat scade, gli altri pannelli sanno che il pannello è offline e possono mostrare un warning.
| Aspetto | Zebus | HTTP + Redis |
|---|---|---|
| Framework | Zebus 3.14.1 (nicchia) | stackexchange.redis (standard industry, 900M+ download) |
| Logging | log4net (#1597) | Serilog (unificato con tutto il resto) |
| Diagnostica | "dov'è il bug?" | redis-cli MONITOR, redis-cli PUBSUB CHANNELS |
| Persistenza | Solo in-memory | RDB/AOF opzionale per recovery |
| Integrazione | Solo .NET | Client Redis in qualsiasi linguaggio |
| Velocità | 1-5ms (serializzazione Zebus) | <1ms (Redis in-memory, protocollo binario RESP) |
| Debug | Source non disponibile | Open source, tooling maturo (RedisInsight, redis-cli) |
| Operazioni | Zero visibilità | redis-cli INFO, redis-cli SLOWLOG, metriche esportabili |
| Dipendenza esterna | Nessuna (libreria embedded) | Servizio Redis dedicato da installare e monitorare |
| Resilienza | Peer-to-peer, nessun SPOF | Punto centrale di guasto (se Redis cade, tutto si ferma) |
| Complessità | NuGet → funziona | Redis + password + persistenza + HA opzionale |
| Scenario | Scelta |
|---|---|
| Commessa semplice, 2 pannelli, vuoi zero configurazione | Zebus |
| Multi-pannello (3+), vuoi diagnostica e monitoring | HTTP + Redis |
| Impianto critico, tolleranza guasti massima | Entrambi attivi (Zebus primario, Redis fallback) |
Fase 1: Redis side-by-side — Zebus continua, Redis si aggiunge in lettura
Fase 2: Doppia scrittura — Ogni evento va sia a Zebus che a Redis (opzionale, solo se servono entrambi)
Redis si attiva senza mai disabilitare Zebus. Ogni commessa sceglie la configurazione al deploy.
Riferimento codebase: D:\DEV\5309_FAEL\Sistec.Bus\ (15 file, log4net + Zebus 3.14.1); Sistec.HMI\Common\Bus.cs (227 righe) — wrapper Zebus con SafeInvoke.
Le REST API esposte dall'HMI (§19.2) abilitano un caso d'uso ulteriore: una dashboard web leggibile da qualsiasi PC in azienda per monitorare lo stato dell'impianto in tempo reale, senza installare l'HMI.
flowchart TB
subgraph Plant["REPARTO PRODUZIONE"]
HMI["PC Industriale
HMI (Avalonia)
+ REST API localhost:5000"]
Redis["REDIS
Stato condiviso
(opzionale)"]
HMI <--> Redis
end
subgraph Company["RETE AZIENDALE"]
D1["PC Ufficio Tecnico
React SPA su browser
sistec-dashboard.local:3000"]
D2["Tablet Capoturno
React SPA
stato produzione"]
D3["TV Reparto
KPI produzione
(Schermo grande)"]
end
HMI -->|HTTP GET /api/v1/*| D1
HMI -->|HTTP GET /api/v1/*| D2
HMI -->|HTTP GET /api/v1/*| D3
| Vista | Dati | API |
|---|---|---|
| Stato macchine | KUKA online/offline, pressa in esecuzione, PLC connesso | GET /api/v1/machines/status |
| Job attivo | Programma in esecuzione, pezzi prodotti, scarti | GET /api/v1/production/active-job |
| Produzione giornaliera | Conteggio pezzi/h, confronto turni, OEE | GET /api/v1/production/stats?range=today |
| Allarmi attivi | Lista allarmi, severità, durata | GET /api/v1/alarms |
| Stato pannelli | AB, C, BS online/offline (da heartbeat Redis) | GET /api/v1/panels/status |
| Operatori | Chi è loggato, da quanto tempo | GET /api/v1/operators/active |
Per aggiornamenti live senza polling, la dashboard si sottoscrive a un endpoint SSE:
// Endpoint SSE sull'HMI
app.MapGet("/api/v1/events", async (HttpContext ctx, ICancellationToken ct) =>
{
ctx.Response.ContentType = "text/event-stream";
while (!ct.IsCancellationRequested)
{
// Invia aggiornamento ogni volta che Redis Pub/Sub pubblica un evento
var evt = await _eventChannel.Reader.ReadAsync(ct);
await ctx.Response.WriteAsync($"event: {evt.Type}\ndata: {JsonSerializer.Serialize(evt.Payload)}\n\n");
await ctx.Response.Body.FlushAsync();
}
});
Il client React si connette con EventSource:
// React — useEventSource hook
const evtSource = new EventSource("/api/v1/events");
evtSource.addEventListener("job:completed", (e) => {
const job = JSON.parse(e.data);
setLastJob(job);
});
evtSource.addEventListener("alarm:raised", (e) => {
const alarm = JSON.parse(e.data);
setAlarms(prev => [...prev, alarm]);
});
| Layer | Tecnologia | Perché |
|---|---|---|
| Build | Vite + TypeScript | Fast, moderno, HMR per sviluppo |
| UI | React 19 + Tailwind CSS | Componenti riutilizzabili, stile utility-first |
| State | React Query (TanStack Query) | Cache HTTP, retry, stale-while-revalidate |
| Real-time | EventSource (SSE nativo) |
Nessuna libreria extra, supporto HTTP nativo |
| Grafici | Chart.js / D3.js | OEE trend, pezzi/h, confronto turni |
| Deploy | Container singolo (Nginx + static build) | docker run -p 3000:80 sistec-dashboard |
La dashboard è read-only — può solo leggere stato, non inviare comandi. L'autenticazione è gestita da:
Opzione A: Basic Auth + HTTPS (semplice, sufficiente per LAN di fabbrica)
Opzione B: Token JWT rilasciato dall'HMI (riusa il login operatore)
Opzione C: Nessuna autenticazione (solo se la VLAN è isolata)
Vincolo: l'endpoint SSE e le API sono esposti solo su interfaccia di rete aziendale (non su quella del PLC/macchinari). L'HMI ha due schede di rete:
| Interfaccia | Rete | Espone |
|---|---|---|
eth0 |
Rete macchine (PLC, KUKA, Safan) | Niente API HTTP |
eth1 |
Rete aziendale | REST API + SSE su porta 5000 |
| Aspetto | Oggi | Con Web Dashboard |
|---|---|---|
| Capoturno vuole vedere produzione | Va al pannello in reparto | Apre browser dal suo PC/tablet |
| Ufficio tecnico vuole debug | Remote desktop sul PC industriale | curl localhost:5000/api/v1/machines/status |
| TV reparto con KPI | Schermo HDMI collegato al PC HMI | Schermo standalone con browser fullscreen |
| Integrazione MES | Accesso diretto a MySQL | API HTTP stabile e versionata |
| Nuova visualizzazione | Riscrivere pagina WinForms/Avalonia | Aggiungere componente React, deploy npm |
La dashboard web è solo monitoraggio. Non deve mai:
❌ Inviare comandi alle macchine (start/stop/reset)
❌ Modificare parametri di produzione
❌ Sovrascrivere job in esecuzione
❌ Sostituire l'HMI come interfaccia primaria
Il confine è netto: l'HMI comanda, la dashboard osserva. Questo garantisce che un bug nella dashboard non possa mai causare danni fisici.
Riferimento codebase: Stessa REST API di §19.2; Zero codice web nelle codebase attuali — è un'aggiunta greenfield.
Oltre alla comunicazione inter-pannello (§19.11), Redis può funzionare da hot data layer condiviso tra tutti gli stack verticali: stato veloce, notifiche real-time, contatori atomici. MySQL resta source of truth. Redis è un cache/distributed state layer che migliora performance e consistenza multi-pannello senza sostituire il DB.
sistec:{plant}:{domain}:{entity}:{id}
| Dominio | Key Pattern | TTL | Uso |
|---|---|---|---|
| Sessioni | sistec:{plant}:session:{badge} |
30m | Operatore loggato visibile su tutti i pannelli |
| Allarmi attivi | sistec:{plant}:alarms:{id} |
∞ (fino ack/shelve) | Stato allarme condiviso, shelving con TTL |
| Contatori produzione | sistec:{plant}:production:{counter} |
∞ | INCR atomico, nessuna race multi-pannello |
| Pallet | sistec:{plant}:pallet:{id} |
24h | Stato pallet in-memory, ripresa veloce post-crash |
| Traduzioni | sistec:{plant}:i18n:{key} |
1h | Cache distribuita, refresh automatico via TTL |
| Ricette | sistec:{plant}:recipe:{id} |
∞ | Cache ricette, evita read DB a ogni ciclo |
| Metriche real-time | sistec:{plant}:metrics:* |
5m | Sliding window per alarm rate, OEE, flood detection |
| Config override | sistec:{plant}:config:{key} |
variabile | Hot-reload parametri propagato a tutti i pannelli |
| OT/IT buffer | sistec:{plant}:cloud:stream |
∞ (RDB/AOF) | Redis Streams come buffer eventi prima di MQTT |
| Aspetto | Oggi (solo MySQL) | Con Redis |
|---|---|---|
| Contatore pezzi | UPDATE ... SET counter = counter + 1 con lock |
INCR atomico, zero lock, zero race |
| Allarme attivo su AB | Solo AB lo vede finché non fa refresh | Pub/Sub → tutti i pannelli notificati in tempo reale |
| Cache traduzioni | SQLite locale per pannello, sync periodico | Redis condiviso, update immediato, TTL per refresh |
| Sessione operatore | Solo sul pannello dove ha badgeato | Visibile su tutti i pannelli, auto-logout centralizzato |
| Stato pallet | Solo in memoria del processo | Redis Hash, ripresa immediata se HMI riavvia |
| Override configurazione | Riavvio HMI per applicare | Pub/Sub → cambio runtime senza riavvio |
| Eventi verso cloud | Coda SQLite locale | Redis Streams persistente, consumer groups, ripartenza senza perdita |
Sistec.Library.Redis
├── RedisPanelState.cs — heartbeat + stato pannello
├── RedisSessionStore.cs — session store operatore
├── RedisAlarmStore.cs — stato allarmi condiviso
├── RedisProductionCounter.cs — INCR contatori atomici
├── RedisPalletStore.cs — Hash stato pallet
├── RedisCloudBuffer.cs — Streams per eventi cloud
├── RedisConfigWatcher.cs — Pub/Sub per hot-reload configurazione
└── RedisI18nCache.cs — cache traduzioni TTL
La libreria è opzionale — ogni commessa decide quali moduli Redis attivare. I servizi cadono graceful su MySQL se Redis non è configurato (NullObject pattern).
NuGet: stackexchange.redis (900M+ download, standard industry)
Configurazione: redis.json — endpoint, porta, password, DB index
Pattern di connessione: ConnectionMultiplexer singleton, DI container
| Idea | Dove si inserisce | Sforzo | Priorità |
|---|---|---|---|
| Health Checks + Circuit Breaker | Fase 2 (ogni stack) | 2-3gg/stack | Alta |
| REST API MES/ERP | Fase 3 (applicativi) | 1-2 settimane | Media |
| Feature Flags | Fase 1 (fondazioni) | 3-5gg | Alta (risolve §2.13) |
| Config Hot-Reload | Fase 1 (fondazioni) | 2-3gg | Alta |
| Metrics + Tracing | Fase 3 (applicativi) | 1-2 settimane | Media |
| Async Audit | Fase 0 (prerequisito) | 2-3gg | Alta (bug latente) |
| Test Helpers condivisi | Fase 1 (fondazioni) | 2-3gg | Alta |
| System.Threading.Channels | Fase 1 (fondazioni) | 1gg | Bassa |
| Sistec.Modbus cleanup | Immediato | 1gg | Media |
| Standardizzazione (net8.0, serializzazione, .editorconfig) | Fase 1 (fondazioni) | 3-5gg | Media |
| HTTP + Redis (inter-panel) | Fase 3 (applicativi) | 1-2 settimane | Alta (alternativa parallela a Zebus) |
| Redis Data Layer (sessioni, allarmi, pallet, traduzioni, contatori) | Fase 3 (applicativi) | 2-3 settimane | Media (alto valore trasversale) |
| Web Dashboard (React) | Fase 3 (applicativi) | 2-3 settimane | Media (alto valore) |
Le codebase LAG (5315) e FAEL (5309) condividono la stessa architettura di fondo: un monolite WinForms cresciuto organicamente attorno a Sistec.Core, con un God Project (Common/), God Class (MainForm/FrmHMI), nessun DI container, DUT legati a OPC UA, branch divergenti, e codebase frammentate per device.
Non ha senso refattorizzarle. Le commesse esistenti rimangono in manutenzione correttiva. Questo documento è il blueprint per la prossima commessa, sviluppata greenfield:
Sistec.<Nome>.Stack.* (5 layer per dispositivo)Microsoft.Extensions.DependencyInjection.editorconfig + Directory.Build.props + Roslyn analyzers dalla Fase 1
Fase 1 — Fondazioni Condivise (4-5 settimane)
Sistec.Infra.* + Sistec.Platform.* NuGet packages
├── Persistence (Dapper + MySql + SqlServer)
├── Configuration (Options pattern + Hot-Reload)
├── Authentication (BCrypt + Employee/Role + statistiche)
├── Logging (JSON strutturato, ILogger<T>, Correlation ID)
├── Feature Flags (IFeatureFlagService per varianti)
├── CodeGen (DUT → POCO + TagConstants)
├── OpcUa + Modbus libraries
├── Controls base + Cloud MQTT
├── Testing infrastructure (NUnit, Moq, Simulator template)
├── Test Helpers condivisi (FakeOpcUa unico, FakeKrcClient)
├── .editorconfig + Directory.Build.props + Roslyn analyzers
├── Async Audit: eliminare .Result nei path esistenti
├── HCI: Sistec.Theme (design system industrial, colori, tipografia, spaziatura)
├── HCI: Sistec.Controls.Touch (touch targets ≥48px per guanti industriali)
├── HCI: Messaggi errore localizzati per ogni eccezione notevole
└── SonarQube quality gate attivo dal giorno 1
Fase 2 — Stack Macchina della Commessa (2-3 settimane/stack)
Solo gli stack necessari (Kuka, Safan, ESA, PLC, Sinumerik...)
Ogni stack: Client → Driver → Services → UI → Simulator
Health Check + Circuit Breaker (Polly) per ogni stack
TDD per logica pura (Services, Driver parsing)
Unit test + Integration test + Contract test
SonarQube quality gate bloccante
HCI: Vincoli di stato per ogni controllo interattivo (CanStart, CanReset, CanEdit)
Fase 3 — Applicativi + UI (3-4 settimane)
Production, JobManagement, Maintenance, Alarms
RecipeEngine + PalletStateMachine
Layout Engine (layout.json → UI)
EmployeeStatsPage (statistiche produzione per dipendente)
Composition Root Avalonia
REST API per MES/ERP (minimal API localhost)
Metrics + Tracing (OpenTelemetry)
Monitoring dashboard + dead man's switch
HCI: Prototipazione 3 schermate core (Home, Production, Maintenance)
HCI: Valutazione euristica interna (checklist §21.1-§21.6)
HCI: Test con 2 operatori reali prima del rilascio
HCI: Telemetry usabilità (tempo operazioni, errori, allarmi per operatore)
Fase 4 — Deploy + Collaudo (1-2 settimane)
Ansible playbook per PC industriale
Smoke test + collaudo in fabbrica
HCI: Verifica usabilità su pannello industriale reale
Verifica retention policy log
Fase 5 (parallela) — Installer + Aggiornamenti Automatici (3-4 settimane)
WiX / Inno Setup project
UpdateAgent servizio Windows
appcast.xml + Update Server
API Compat Check in CI/CD (NON si rompono API pubbliche)
Rollback automatico su crash
UpdateAgent sviluppabile già in Fase 1 (nessuna dipendenza da stack)
Fase 6 (pulizia) — Allineamento Codebase Legacy (1-2gg, non bloccante)
Eliminare Sistec.Modbus (duplicato morto in FAEL)
Unificare serializzazione (System.Text.Json)
Portare Sistec.Core a net8.0+ se utile per la greenfield
--- Tempo stimato prima commessa: 14-18 settimane (con Fase 5-6 in parallelo) ---
--- Commesse successive: 7-11 settimane (riusano Fase 1) ---
Primo passo consigliato: Iniziare con la Fase 1 — Fondazioni, partendo da:
Sistec.Infra.Persistence (interfacce, Dapper, Unit of Work)Sistec.Infra.Authentication (Employee, Role, BCrypt)Sistec.Infra.FeatureFlags (IFeatureFlagService + JSON config)Sistec.Infra.TestHelpers (FakeOpcUa unico) + SonarQube gate attivo dal primo commitLa testing infrastructure non è opzionale: va impostata prima di scrivere codice di produzione. Questo garantisce che ogni stack nasca già con coverage, quality gate e pipeline CI/CD pronti — evitando di ritrovarsi come LAG/FAEL con 13 copie di FakeOpcUa e zero test automatici.
L'architettura a stack verticali risolve i problemi tecnici (frammentazione, coupling, testabilità), ma non garantisce che l'interfaccia sia usabile dall'operatore. Questa sezione applica i principi fondamentali dell'HCI (Human-Computer Interaction) all'HMI Sistec, identificando i gap tra le codebase legacy e la soluzione greenfield.
I 5 principi di Norman definiscono le fondamenta dell'interazione uomo-macchina. Ogni principio viene mappato allo stato attuale e alla proposta architetturale.
| Principio | Stato Attuale (LAG/FAEL) | Gap Critico | Soluzione Greenfield |
|---|---|---|---|
| Affordance chiare e signifiers visibili | WinForms bottone/LED custom senza stati visibili distinti (clickable, disabled, processing) | Nessuna distinzione visiva tra "questo pulsante è cliccabile" e "questo pulsante è disabilitato" | Avalonia VisualStates + binding dichiarativo su IsEnabled, IsChecked, IsPointerOver |
| Feedback immediato per ogni azione | Pattern SafeInvoke: l'utente preme, nulla cambia visivamente per 100-500ms |
Nessun indicatore di stato "processing" — l'operatore non sa se il comando è stato ricevuto | Command pattern con IsExecuting property → spinner o disabilitazione temporanea del controllo |
| Mapping naturale tra controlli ed effetti | Bottoni generici (Sistec.Controls/Button) senza relazione spaziale con il macchinario |
Operatore deve ricordare cosa fa ogni pulsante — mapping arbitrario | Layout Engine: disposizione controlli che riflette la disposizione fisica dell'impianto via layout.json (grid rows/cols) |
| Vincoli (constraints) che prevengono errori | Nessun vincolo UI: bottone "Reset" attivo anche quando il robot è in movimento, "Start" attivo durante allarme | Rischio fisico reale — comando inviato in stato non valido | State machine in Services layer che espone CanReset, CanStart, CanEdit → UI disabilita pulsanti quando false |
| Consistenza interna ed esterna | 3 varianti HMI (AB, C, BS) con layout, navigazione e posizionamento controlli diversi | L'operatore che passa da AB a C non riconosce l'interfaccia — errore umano | layout.json uniforme, stessi controlli da ControlRegistry, stessa navigazione in ogni variante |
Implementazione nei controlli interattivi — esempio per KUKA:
// Sistec.Stack.Kuka.Services — espone stati vincolati
public interface IKukaRobotState
{
bool CanStart { get; }
bool CanStop { get; }
bool CanReset { get; }
bool CanLoadPanel { get; }
}
// binding in layout.json
{
"control": "Kuka.Views.StartButton",
"binding": { "enabled": "Kuka_0.CanStart" }
}
| Regola | Valutazione Attuale | Gap | Implementazione Proposta |
|---|---|---|---|
| Coerenza in comandi, terminologia, layout | ✅ Buona in singola variante, ❌ tra varianti AB/C/BS | Nomi diversi per la stessa azione | Avalonia Theme + DataTemplate unici per tutti gli stack, IStringLocalizer per terminologia consistente |
| Scorciatoie per utenti esperti | ❌ Assente — ogni azione richiede navigazione through menu | Utente esperto è lento quanto il principiante | Hotkeys in Avalonia: KeyBinding in Window.KeyBindings, es. F5=Start, F6=Stop, Ctrl+L=Login |
| Feedback informativo proporzionato all'azione | ⚠️ Parziale — solo LED on/off, nessun indicatore di progresso | Operazione lunga (load panel, connect) senza feedback | IProgress<T> nei Services → ProgressBar in UI per operazioni con durata > 500ms |
| Dialoghi con chiusura chiara (inizio-fine) | ⚠️ MessageBox.Show() senza callback strutturato |
Dialoghi che non comunicano outcome all'utente | Avalonia ContentDialog con PrimaryButton/SecondaryButton + return type strutturato |
| Gestione semplice degli errori, linguaggio chiaro | ❌ try/catch con messaggi tecnici ("Connection refused 192.168.1.100:4840") |
Utente non capisce cosa fare | IErrorLocalizer → messaggi in lingua operatore: "Robot KUKA non raggiungibile. Verificare cavo di rete e riprovare." |
| Azioni reversibili (undo/redo) | ❌ Nessuna reversibilità — parametri modificati sono permanenti | Errore di parametrizzazione = produzione compromessa | ParameterChangeService con history stack: ultimo valore salvato, "Annulla" ripristina |
| Locus of control interno (l'utente inizia) | ⚠️ L'HMI "subisce" update e riconnessioni forzate | Operatore perde controllo senza preavviso | Aggiornamenti solo in finestra notturna (AutoRestartWindow), notifica 5 minuti prima di azione |
| Ridurre carico sulla memoria a breve termine (7±2) | ❌ Pagine sovraccariche (FrmHMI: 24 partial files, tutto in una schermata) | Informazioni troppe = rumore = errori | layout.json con navigazione a 3-5 voci principali (sidebar), info-card con max 5-7 campi visibili per vista |
Regola 7±2 applicata alla dashboard Home:
MASSIMO per la schermata Home:
- 5 macchine visibili (KUKA, Safan, PLC, Job, Alarmi)
- Ogni card mostra max 7 valori (stato, parametro principale, 5 indicatori)
- Navigazione sidebar: max 5 voci (Home, Produzione, Manutenzione, Alarmi, Info)
Ogni euristica viene valutata con severità: 🔴 CRITICA (blocca l'uso), 🟠 ALTA (causa errori frequenti), 🟡 MEDIA (riduce efficienza), 🟢 BASSA (miglioramento marginale).
| # | Euristica | Severità | Analisi | Soluzione Architetturale |
|---|---|---|---|---|
| 1 | Visibilità dello stato del sistema | 🔴 CRITICA | L'operatore non sa se il PLC è connesso finché non preme un bottone e fallisce. Nessun indicatore globale di stato. | PlcStatusControl sempre visibile in header o sidebar, colore dinamico via binding: 🟢 connesso, 🟡 connesso con warning, 🔴 disconnesso |
| 2 | Corrispondenza sistema-mondo reale | 🟠 ALTA | Terminologia tecnica esposta all'utente ("OPC UA Session State", "Zebus pub/sub", "TagValue |
Localizzazione via resources/*.json, lingua operatore (nessuna terminologia protocollo), nomi di dominio ("Robot", "Pressa", "PLC") |
| 3 | Controllo e libertà dell'utente | 🟠 ALTA | Nessun "undo" su parametri, reset che parte subito senza conferma | Emergency stop button sempre visibile + conferma obbligatoria per azioni critiche (reset, modifica parametri, cambio programma) |
| 4 | Consistenza e standard di piattaforma | 🟡 MEDIA | WinForms non ha design system — ogni sviluppatore crea controlli ad hoc | Avalonia Fluent theme come base + Sistec.Theme package come design system proprietario industriale |
| 5 | Prevenzione degli errori | 🔴 CRITICA | Input fields senza validazione, parametri fuori range accettati senza warning | IValidatable<T> in Services layer, validazione client-side ( Avalonia ) + server-side ( Services ), range limits in appsettings.json |
| 6 | Riconoscimento piuttosto che ricordo | 🟠 ALTA | Menu testuali senza icone, nessun breadcrumb, l'utente deve ricordare dove si trova | layout.json con icon per ogni navigazione item + breadcrumb in LayoutNavigation |
| 7 | Flessibilità ed efficienza d'uso | 🟡 MEDIA | Nessuna personalizzazione — stesso layout per tutti gli operatori, nessuna scorciatoia | User preferences in employees table: layout personalizzato, lingua,快捷键, ordine voci sidebar |
| 8 | Design estetico e minimalista | 🟠 ALTA | FrHMI: 5156 righe = interfaccia caotica con informazioni non priorizzate | Guideline: max 5-7 elementi per vista, whitespace generoso, gerarchia visiva con dimensione/colore/posizione |
| 9 | Aiuto a riconoscere/diagnosticare/recuperare dagli errori | 🔴 CRITICA | Errori mostrati come stack trace o codici esadecimali, nessuna guida al recupero | ErrorRecoveryService: ogni errore ha {messaggio leggibile, azione suggerita, contatto supporto} |
| 10 | Documentazione e help accessibili | 🟡 MEDIA | Nessun help integrato — tecnico deve consultare manuale cartaceo o chiamare Sistec | Avalonia ToolTip contestuali + HelpService con FAQ integrata, accessibile da ogni schermata |
Implementazione ErrorRecoveryService:
public record ErrorInfo(
string TechnicalMessage, // per log/debug
string UserMessage, // per operatore ("Robot KUKA non raggiungibile")
string SuggestedAction, // "Verificare cavo di rete e riprovare"
string SupportContact, // "Contattare Sistec: +39 0XX XXXXXXX"
ErrorSeverity Severity // Info, Warning, Error, Critical
);
public interface IErrorRecoveryService
{
ErrorInfo GetRecoveryInfo(Exception ex);
}
| Principio | Applicazione Sistec | Note |
|---|---|---|
| Gerarchia visiva chiara | Dashboard: KPI primari (pezzi/h, stato macchina) in card grandi, secondari in card piccole | layout.json supporta rowSpan/colSpan per dimensionamento differenziato |
| Grigie e allineamento coerenti | layout.json già utilizza grid layout (rows, cols, cells) |
Consistenza garantita dalla definizione JSON — nessun allineamento manuale |
| Aesthetic-Usability Effect | Avalonia Fluent theme = professionale out-of-the-box, zero custom CSS per base | Tema base already rispetta principi di design moderno |
| Tipografia leggibile | Pannelli industriali: 15-21" touchscreen, distanza di visione 50-80cm | Font size ≥16px, font weight semibold per labels, monospace per valori numerici |
| Colore funzionale, mai unico veicolo d'informazione | Stato macchina: 🟢 running, 🟡 warning, 🔴 fault | Mai usare solo colore — sempre colore + icona + testo ("Running" / ⚠️ Warning / 🔴 Fault) |
Piano colori industriale consigliato:
| Ruolo | Colore | Uso |
|---|---|---|
| Sfondo primario | #1E1E1E (dark) o #F5F5F5 (light) |
Background applicazione |
| Testo primario | #F0F0F0 (dark) o #212121 (light) |
Testo leggibile, contrasto ≥ 7:1 |
| Stato OK | #4CAF50 (verde) |
Macchina in funzione, nessun allarme |
| Stato Warning | #FFC107 (giallo) |
Attenzione richiesta, manutenzione imminente |
| Stato Fault | #F44336 (rosso) |
Allarme attivo, macchina ferma |
| Stato Idle | #9E9E9E (grigio) |
Macchina ferma, nessun allarme |
| Accent primario | #2196F3 (blu) |
Azioni principali, pulsanti attivi |
| Focus | #FFD600 (giallo brillante) |
Bordo controllo con focus da tastiera |
Configurazione Avalonia — tema industriale:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sistec.HMI.App">
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Sistec.Theme/Industrial.axaml"/>
</Application.Styles>
</Application>
| WCAG | Requisito Standard | Contesto Industriale Sistec | Implementazione |
|---|---|---|---|
| Contrasto testo ≥ 4.5:1 | Testo normale su background | Pannelli industriali: luminosità variabile, polvere, angolazione | Tema scuro: sfondo #1E1E1E, testo #F0F0F0 = contrasto 15.5:1 |
| Contrasto testo grande ≥ 3:1 | Testo ≥ 18pt o ≥ 14pt bold | Titoli e valori principali | Sempre superato con tema scuro |
| Target touch minimo 44×44px | Area interattiva minima | Operatori con guanti industriali (spessore 2-3mm) | MinWidth="48" MinHeight="48" su tutti i Button/Interactive elements — 48px > 44px WCAG |
| Alt-text per immagini informative | Descrizione testuale | Isolemacchine, schemi impianto, icone stato | layout.json supporta proprietà aria-label per ogni controllo |
| Focus da tastiera sempre visibile | Indicatore di focus visibile | Panel touch + tastiera USB come fallback | Avalonia FocusVisualStyle con border giallo brillante (#FFD600, 3px) |
| Navigazione completabile solo con tastiera | Tutte le funzionalità accessibili da tastiera | Backup se touch screen guasto | TabIndex ordinato in ogni view, KeyDown handler per shortcuts |
Vincolo critico ambientale: I pannelli industriali tipicamente hanno touch + mouse USB. La tastiera è rara ma va supportata come fallback. Avalonia gestisce nativamente keyboard navigation con Tab, Enter, Escape, Arrow keys.
Regola pratica per touch industriale:
GESTI CONSENTITI (affidabili con guanti):
✅ Single tap (il solo gesto da usare)
✅ Long tap: NON usarlo (lento, rischio attivazione accidentale)
✅ Swipe: NON usarlo (rischio attivazione accidentale con guanti)
✅ Double tap: NON usarlo (frustrante con guanti)
✅ Pinch-to-zoom: NON usarlo (poca utilità su HMI industriale)
| Principio | Applicazione HMI | Nota |
|---|---|---|
| Bottom navigation 3-5 voci | layout.json navigation: Home, Produzione, Manutenzione, Alarmi (4 voci) |
Sidebar orizzontale in basso (touch-friendly) o laterale (desktop-friendly) |
| Form a colonna singola | LoginPage, CreateJobDialog, ParameterEditDialog | StackPanel Orientation="Vertical" con 1 campo per riga, spaziatura ≥ 16px |
| Input specializzati per tipo di dato | PIN input (numeric keypad 4-6 cifre), job selection (dropdown), parameter edit (numeric stepper) | Custom Avalonia controls: NumericKeypad, JobPicker, ParameterEditor |
| Mobile-first design | Pannelli industriali: risoluzioni 1280×800 - 1920×1080 | Design per 1280×800 (più piccolo), scale up a risoluzioni maggiori |
Gerarchia di navigazione proposta:
Livello 0: Shell (sempre visibile)
├── Header: stato PLC, stato KUKA, stato Pressa, ora, operatore
├── Sidebar/Sotto: Home | Produzione | Manutenzione | Alarmi
└── Footer: allarme banner (se attivo)
Livello 1: Pagina (contenuto principale)
├── Home: card macchine (max 5)
├── Produzione: tabbed (Job, CutPlan, Pannelli)
├── Manutenzione: vista lista componenti con RUL
└── Alarmi: lista allarmi attivi + storico
Livello 2: Dettaglio (dialog modale)
├── Parametri macchina
├── Modifica job
├── Storico allarmi
└── Statistiche operatore
| Gap | Impatto HCI | Soluzione | Sforzo Stimato |
|---|---|---|---|
| Nessun ViewModel layer | Business logic in UI (anti-pattern §1.5 #6): 490 righe di costruttore FrmHMI | MVVM con CommunityToolkit.Mvvm in ogni Stack.UI |
2-3 giorni/stack |
| Nessun state management | UI non riflette stato macchine in real-time | IObservable<T> chain: Driver → Services → ViewModel → View |
1-2 giorni/stack |
| Nessun error boundary | Crash non gestito = HMI down senza diagnosi | Dispatcher.UnhandledException + ErrorBoundary Avalonia |
1 giorno totale |
| SafeInvoke pattern | Thread safety manuale, errori persi silenziosamente | Avalonia: niente InvokeRequired, binding automatico thread-safe |
Eliminato con migrazione Avalonia |
| Nessuna localizzazione | Italiano hardcoded, impossibile internazionalizzare | IStringLocalizer + resources/*.json per lingua |
2-3 giorni totale |
| Nessun design system | Ogni sviluppatore crea controlli ad hoc, nessuna coerenza | Sistec.Theme package con stili, colori, tipografia, spaziatura |
3-5 giorni (una tantum) |
Implementazione MVVM — modello per ogni Stack.UI:
// Sistec.Stack.Kuka.UI/ViewModels/KukaInfoViewModel.cs
public partial class KukaInfoViewModel : ObservableObject
{
private readonly IKukaRobotState _robotState;
private readonly ILogger<KukaInfoViewModel> _logger;
[ObservableProperty] private string _status = "Offline";
[ObservableProperty] private bool _canStart;
[ObservableProperty] private bool _isExecuting;
public KukaInfoViewModel(IKukaRobotState robotState, ILogger<KukaInfoViewModel> logger)
{
_robotState = robotState;
_logger = logger;
// Reactive: stato cambia → UI si aggiorna automaticamente
robotState.StateChanged += OnStateChanged;
}
[RelayCommand]
private async Task StartAsync()
{
if (!CanStart) return;
IsExecuting = true;
try { await _robotState.StartAsync(); }
catch (Exception ex) { _logger.LogError(ex, "Start failed"); }
finally { IsExecuting = false; }
}
}
| Attività | Quando nella Roadmap | Dove | Responsabile |
|---|---|---|---|
| Focus precoce e continuo sull'utente | Prima della Fase 3 (UI) | User interviews con operatori LAG/FAEL durante Fase 1-2 | Product Owner + Ingegnere di commessa |
| Iterazione design → prototipo → test | Durante Fase 3 | Avalonia hot-reload + layout.json live preview | Sviluppatore UI |
| Misurazione empirica | Dopo deploy Fase 4 | Telemetry: tempo operazioni, errori, allarmi per operatore | Sviluppatore + collaudatore |
| Valutazione euristica 3-5 esperti | Prima del test utente reale | Checklist §21.1-§21.6, 3 sviluppatori Sistec + 2 operatori esperti | Team |
Processo iterativo proposto per ogni nuova vista:
1. Sketch su carta (5 minuti)
└── Definire: cosa serve all'operatore? Quali info sono prioritarie?
2. Prototipo Avalonia XAML (1-2 ore)
└── Versione funzionante con dati fittizi
3. Valutazione euristica interna (30 minuti, 3 persone)
└── Applicare checklist §21.1-§21.6,documentare violazioni
4. Test con 1-2 operatori reali (30 minuti)
└── Assegnare task: "Vuoi avviare il robot? Come fai?"
└── Osservare dove si bloccano, cosa non capiscono
5. Raffinamento (1-2 ore)
└── Correggere i problemi trovati
6. Integrazione in layout.json
└── La vista diventa parte del sistema
Metriche di usabilità da raccogliere in produzione:
| Metrica | Come si misura | Target |
|---|---|---|
| Tempo per completare un'operazione | Telemetry: timestamp inizio → fine operazione | < 30 secondi per operazioni comuni |
| Tasso di errore utente | Conteggio input invalidi / parametri fuori range | < 5% delle operazioni |
| Tempo di risposta percepito | Telemetry: tempo tra comando e feedback visivo | < 500ms per ogni azione |
| Number of steps per operazione | Conteggio click/tap necessari | ≤ 3 per operazioni frequenti |
| Allarmi per causa umana | Analisi allarmi: quanti sono dovuti a errore operatore | < 10% del totale allarmi |
Priorità alta — da fare in Fase 1-2 (prima del codice UI):
| # | Azione | Sforzo | Impatto |
|---|---|---|---|
| 1 | Definire Sistec.Theme package con design system industrial (colori, tipografia, spaziatura, stili controlli) |
3-5 giorni | Garantisce coerenza visiva da subito |
| 2 | Creare Sistec.Controls.Touch con touch targets ≥ 48px per ogni controllo interattivo |
2-3 giorni | Usabilità con guanti industriali |
| 3 | Aggiungere vincoli di stato a ogni controllo interattivo (CanStart, CanReset, CanEdit) |
1-2 giorni/stack | Previene errori pericolosi |
| 4 | Definire messaggi di errore localizzati per ogni eccezione notevole | 2-3 giorni | Operatori capiscono cosa fare |
Priorità media — da fare in Fase 3 (UI):
| # | Azione | Sforzo | Impatto |
|---|---|---|---|
| 5 | Prototipare 3 schermate core con valutazione euristica (Home, Production, Maintenance) | 1-2 settimane | Verifica usabilità prima del deploy |
| 6 | Testare con 2 operatori reali prima del rilascio | 1 giorno | Identificare problemi reali |
| 7 | Aggiungere telemetry per misurare usabilità reale (tempo operazioni, errori) | 2-3 giorni | Misurazione empirica continua |
Priorità bassa — da fare in Fase 4+ (dopo il deploy):
| # | Azione | Sforzo | Impatto |
|---|---|---|---|
| 8 | Hotkeys per utenti esperti (F5=Start, F6=Stop, Ctrl+L=Login) | 1 giorno | Efficienza per operatori esperti |
| 9 | Dashboard personalizzabile per operatore (ordine voci, lingua, tema) | 3-5 giorni | Personalizzazione utente |
| 10 | Help contestuale integrato (ToolTip + FAQ) | 2-3 giorni | Riduzione errori, auto-formazione |
| Categoria HCI | Principi Chiave | Componente Architetturale | Fase Roadmap |
|---|---|---|---|
| Norman | Affordance, Feedback, Mapping, Constraints, Consistenza | ControlFactory, IKukaRobotState, layout.json |
Fase 2 (stack), Fase 3 (UI) |
| Shneiderman | Coerenza, Scorciatoie, Feedback, Dialoghi, Undo, Controllo, Memoria | Avalonia KeyBinding, ContentDialog, ParameterChangeService |
Fase 3 (UI) |
| Nielsen | Stato, Mondo reale, Controllo, Standard, Errori, Riconoscimento, Flessibilità, Estetica, Help | PlcStatusControl, ErrorRecoveryService, IStringLocalizer |
Fase 1 (theme), Fase 3 (UI) |
| Visual Design | Gerarchia, Griglie, Estetica, Tipografia, Colore | Sistec.Theme, Avalonia Fluent, layout.json grid |
Fase 1 (theme) |
| Accessibilità | Contrasto, Target 44px, Focus, Tastiera | Sistec.Controls.Touch, Avalonia FocusVisualStyle |
Fase 1 (controls) |
| Interazione | Navigation 3-5 voci, Form colonna singola, Input specializzati | LayoutNavigation, custom controls, layout.json navigation |
Fase 3 (UI) |
| Processo | Focus utente, Iterazione, Misurazione, Valutazione | Telemetry, user testing, heuristic evaluation | Continuo (Fase 1-4) |
L'ASM Consortium (Abnormal Situation Management Consortium) è il gruppo di ricerca che ha prodotto le linee guida per le High-Performance HMIs (HP-HMI), oggi lo standard de facto per i settori oil & gas, chimico, pharma e power generation. Le raccomandazioni ASM sono alla base dello standard ISA-101 e vanno oltre i suoi requisiti minimi.
Il documento §21 copre Norman, Shneiderman e Nielsen — fondamentali per l'HCI generale — ma non menziona la metodologia ASM che è specifica per HMIs industriali. La differenza è sostanziale:
| Principio | HCI Generale (§21) | High-Performance HMI (ASM) |
|---|---|---|
| Colore | Palette estetica, 7 stati colore | Filosofia grigia: il 90% dello schermo è grigio, colore solo per anomalie |
| Layout | Griglia, spaziatura, gerarchia | Exception-based: mostra solo ciò che è fuori norma. Normale = invisibile |
| Dati | 5-7 campi per vista | Trend + valore: non il numero istantaneo, ma il trend nelle ultime N unità |
| Navigazione | Sidebar 3-5 voci | Livelli ISA-101: Overview → Unit → Detail → Diagnostic |
| Allarmi | Lista priorizzata | Integrazione stato: allarme non è un popup — è il processo che cambia colore quando qualcosa è sbagliato |
Il modello di Endsley è il fondamento dell'HP-HMI:
1. PERCEZIONE — "Cosa sta succedendo?"
└── Operatore vede: KUKA Online, Pressa Running, 5 allarmi
└── Metriche: colore, posizione, dimensione
2. COMPRENSIONE — "Cosa significa?"
└── Operatore sa: 3 allarmi sono Emergency, 2 Warning
└── Priorità: rosso > arancione > giallo
3. PROIEZIONE — "Cosa succederà tra 5 minuti?"
└── Operatore sa: se la pressa non finisce il ciclo,
il KUKA rimarrà in attesa → ritardo produzione
└── Proiezione: trend + contesto
Obiettivo: portare l'operatore al Livello 3 di situation awareness con 1-2 secondi di osservazione.
| Elemento | ISA-101 / ASM | Implementazione Greenfield |
|---|---|---|
| Sfondo | Grigio chiaro (#F0F0F0) | Tema chiaro: #F0F0F0 |
| Attrezzatura normale | Grigio medio (#A0A0A0) o beige | #808080 — icona del device |
| Valori numerici | Nero su grigio, senza cornice | #000000 on #F0F0F0 |
| Processo in esecuzione | Nessun indicatore — è normale | Colore grigio. "Normale" non va evidenziato |
| Fuori specifica | Giallo (#FFD700) | Bordo giallo + icona triangolo |
| Allarme attivo | Rosso (#FF0000) lampeggiante | Bordo rosso + icona errore + testo |
| Trend anomali | Banda colore a sinistra del valore | Banda rossa/gialla a sinistra del TextBlock |
<!-- ASM-Compliant Value Display -->
<Grid ColumnDefinitions="4, Auto, Auto">
<!-- Banda laterale — colore solo per anomalie -->
<Rectangle Grid.Column="0" Width="4"
Fill="{Binding AlarmBandColor}"
IsVisible="{Binding HasAlarm}"/>
<!-- Nome parametro — sempre nero su grigio -->
<TextBlock Grid.Column="1" Text="{Binding Label}"
Foreground="#000000" Margin="4,0"/>
<!-- Valore — nero, senza decori -->
<TextBlock Grid.Column="2" Text="{Binding Value}"
Foreground="#000000"/>
</Grid>
Il principio cardine dell'HP-HMI è: mostra solo le eccezioni. In uno stato normale, l'HMI è quasi muto — l'operatore vede immediatamente ciò che è fuori norma.
flowchart TB
subgraph StatoNormale["🔇 STATO NORMALE — Schermo muto"]
A1["KUKA: 🟢 (grigio scuro)"]
A2["Pressa: 🟢 (grigio scuro)"]
A3["Produzione: 847 pezzi (testo nero)"]
A4["Allarmi: 0 (nessun testo)"]
end
subgraph StatoAnomalo["🔴 ALLARME — Schermo parla"]
B1["KUKA: 🔴 ROSSO LAMPEGGIANTE
Joint 3 overload"]
B2["Pressa: 🟢 (grigio — funziona)"]
B3["Produzione: 847 pezzi (nero)"]
B4["Allarmi: 1 Emergency, 2 Warning"]
end
StatoNormale -->|"Joint 3 overload"| StatoAnomalo
StatoAnomalo -->|"Risolto"| StatoNormale
Regola pratica: se un elemento non richiede attenzione, deve essere visivamente quasi invisibile. Un operatore che vede tutto colorato non vede nulla.
Le ricerche ASM mostrano che un valore numerico istantaneo non dà situation awareness: 87°C può essere normale o critico a seconda del trend.
NON ASM:
┌──────────────────┐
│ Temperatura: 87°C│ ← L'operatore sa cosa è ora, non cosa sarà
└──────────────────┘
ASM-COMPLIANT:
┌─────────────────────────┐
│ ┃ 87°C ▁▂▃▅▇███▇▆▄▃▂ │ ← Trend ultimi 30 minuti
│ ┃ Stabile │ + indicatore di trend
│ ┃ Soglia: 95°C │ + contesto
└─────────────────────────┘
Implementazione:
┌──────────────────────────┐
│ Banda colore | Valore │
│ (solo se anomalo) │
│ o │
│ Banda normale (invisib.) │
└──────────────────────────┘
| Metrica | Target ASM | Misurazione in Sistec |
|---|---|---|
| Time to Situation Awareness | < 2 secondi | Telemetry: tempo tra ingresso pagina e primo click |
| Abnormal Response Time | < 10 secondi per Emergency | Telemetry: tempo tra Raised e Acknowledged |
| Error Rate | < 1% delle operazioni | Audit log: azioni annullate o corrette |
| Alarm Flood Frequency | 0 flood/giorno (target) | AlarmAnalyticsService (da §24.3) |
| Screen Density | < 40% riempito | Verifica layout.json: max 7 elementi per vista |
[ ] HMI Philosophy document cita esplicitamente Endsley SA model
[ ] Colore di sfondo unico: grigio (#E8E8E8 o #F0F0F0)
[ ] Processo normale = invisibile (grigio su grigio)
[ ] Valori numerici in nero, allineati a destra, senza cornice
[ ] Colore riservato ESCLUSIVAMENTE ad anomalie e allarmi
[ ] Ogni valore numerico ha trend (minisparkline o banda) sulla sinistra
[ ] Layout eccezioni: mostra solo ciò che è fuori specifica
[ ] Navigazione ISA-101 4 livelli: Overview → Unit → Detail → Diagnostic
[ ] Allarmi integrati nel processo (non popup separati)
[ ] Test con operatori reali: trovano l'anomalia in <2 secondi?
Impatto: $21 già copre molti concetti HP-HMI. Questa checklist trasforma i suggerimenti in vincoli di design per Fase 3 (UI). Sforzo aggiuntivo: 2-3 giorni per applicare la filosofia grigia ai template layout.json e aggiornare Sistec.Theme con le regole ASM.
L'architettura a stack verticali è già predisposta per l'integrazione di intelligenza artificiale. Il documento prevede già OnnxRuntimeEngine per predictive maintenance (§9.6) e Sistec.Stack.Cloud per analytics cloud (§13.6). Questa sezione espande il quadro con un layer AI dedicato che si aggancia naturalmente all'esistente.
flowchart TB
subgraph AI_Stack["Sistec.Stack.AI (NuGet, opzionale)"]
direction TB
CLI_AI["Client/AI
OnnxInferenceEngine
LlmClient (locale/cloud)"]
DRV_AI["Driver/AI
IModelRegistry
IPromptTemplate"]
SVC_AI["Services/AI
PredictiveMaintenance
AnomalyDetector
ProcessOptimizer
AlarmAnalyzer
OperatorAssistant
DocumentGenerator"]
UI_AI["UI/AI
AiInsightsPanel
NaturalLanguageQuery"]
SIM_AI["Simulator/AI
AiSimulator (what-if)"]
end
subgraph Stacks_Esistenti["STACK ESISTENTI"]
Kuka["Sistec.Stack.Kuka
IMachineTelemetryProvider"]
Safan["Sistec.Stack.Safan
IMachineTelemetryProvider"]
Plc["Sistec.Stack.PLC
IMachineTelemetryProvider"]
Prod["Sistec.Stack.Production
IProductionAnalyticsService"]
Job["Sistec.Stack.JobManagement
IJobTracker"]
Alarm["Sistec.Stack.Alarms
IAlarmService"]
Maint["Sistec.Stack.Maintenance"]
Cloud["Sistec.Stack.Cloud"]
end
subgraph Config_AI["CONFIGURAZIONE AI"]
Models["models/*.onnx
modelli leggeri (<50MB)"]
Prompts["prompts/*.json
template prompt LLM"]
Thresholds["thresholds.json
soglie anomaly detection"]
end
UI_AI --> SVC_AI
SVC_AI --> CLI_AI
CLI_AI --> DRV_AI
DRV_AI --> Config_AI
Kuka -->|telemetria| SVC_AI
Safan -->|telemetria| SVC_AI
Plc -->|telemetria| SVC_AI
Prod -->|statistiche| SVC_AI
Job -->|storico job| SVC_AI
Alarm -->|storico allarmi| SVC_AI
Maint -->|RUL| SVC_AI
SVC_AI -->|analytics| Cloud
classDef ai fill:#e3f2fd,stroke:#1565c0
classDef stack fill:#e8f5e9,stroke:#2e7d32
classDef config fill:#f3e5f5,stroke:#7b1fa2
class CLI_AI,DRV_AI,SVC_AI,UI_AI,SIM_AI ai
class Kuka,Safan,Plc,Prod,Job,Alarm,Maint,Cloud stack
class Models,Prompts,Thresholds config
| Layer | Progetto | Contenuto | Dipende da |
|---|---|---|---|
| Client | Sistec.Stack.AI.Client |
OnnxInferenceEngine (ONNX Runtime CPU), LlmClient (llama.cpp locale o HTTP cloud) |
— |
| Driver | Sistec.Stack.AI.Driver |
IModelRegistry (caricamento modelli .onnx), IPromptTemplate (template prompt da JSON) |
Sistec.Stack.AI.Client, Sistec.Core |
| Services | Sistec.Stack.AI.Services |
AnomalyDetector, AlarmAnalyzer, ProcessOptimizer, OperatorAssistant, DocumentGenerator |
Sistec.Stack.AI.Driver, tutti gli stack macchina (via interfacce) |
| UI | Sistec.Stack.AI.UI |
AiInsightsPanel (sidebar), NaturalLanguageQuery (barra ricerca) |
Sistec.Stack.AI.Services, Sistec.Controls |
| Simulator | Sistec.Stack.AI.Simulator |
AiSimulator (scenari what-if su dati storici) |
Sistec.Stack.AI.Client |
Problema: l'operatore deve navigare 5+ schermate per trovare informazioni semplici.
Soluzione: barra di ricerca in linguaggio naturale che interroga i dati di produzione:
Operatore: "quanti pezzi ha prodotto oggi il turno notturno?"
AI: "Il turno notturno (00-06) ha prodotto 847 pezzi, 3.2% scarti.
Ultimo job completato: #4521, programma P123, durata 42s."
Operatore: "qual è il tempo medio di piegatura per il programma P456?"
AI: "Programma P456: tempo ciclo medio 38.2s (ultimi 200 cicli).
Deviazione standard: 1.8s. Trend: in miglioramento (-2.1% vs ieri)."
Operatore: "quanti allarmi KUKA ci sono stati stanotte?"
AI: "3 allarmi KUKA tra le 02:00 e 04:30:
- 2x 'Joint 2 Overload' (durata media 45s)
- 1x 'Connection timeout' (riconnesso automaticamente)"
flowchart LR
subgraph Input["INPUT OPERATORE"]
NL["Barra ricerca
linguaggio naturale
(Avalonia TextBox)"]
end
subgraph NLU["NLU LAYER"]
Intent["Intent Classifier
DistilBERT quantized
(ONNX, <70MB)"]
Entities["Entity Extractor
programId, timeRange,
machine, shift"]
end
subgraph Query["QUERY BUILDER"]
QB["QueryBuilder
Intent + Entities →
chiamate servizi"]
end
subgraph Services["SERVIZI DATI"]
PA["IProductionAnalyticsService"]
JA["IJobTracker"]
AS["IAlarmService"]
TP["IMachineTelemetryProvider"]
end
subgraph LLM["LLM RESPONSE"]
Resp["Response Generator
llama.cpp locale
o OpenAI cloud API"]
end
NL --> Intent --> Entities --> QB
QB --> PA
QB --> JA
QB --> AS
QB --> TP
PA --> Resp
JA --> Resp
AS --> Resp
TP --> Resp
Resp -->|"risposta testuale
all'operatore"| NL
| Intent | Esempio | Servizi consumati |
|---|---|---|
query_production |
"pezzi prodotti oggi" | IProductionAnalyticsService |
query_job |
"ultimo job completato" | IJobTracker |
query_alarm |
"allarmi di stanotte" | IAlarmService |
query_machine |
"stato KUKA" | IMachineTelemetryProvider |
query_scrap |
"tasso scarti settimana" | IProductionAnalyticsService |
query_downtime |
"fermi macchina oggi" | IAlarmService + IMachineTelemetryProvider |
compare |
"confronta turni oggi" | IProductionAnalyticsService |
trend |
"trend produzione ultima settimana" | IProductionAnalyticsService |
ai_query_log tabella MySQLDifferenza da predictive maintenance (§9.6): la maintenance predice quando si rompe (RUL). L'anomaly detection rileva comportamento anomalo in tempo reale, anche per componenti senza modello RUL addestrato.
Ogni stack espone IMachineTelemetryProvider (§9.6). L'AI sovrappone un modello di anomaly detection:
{
"detectorId": "kuka_vibration_anomaly",
"machine": "Kuka_0",
"modelFile": "models/kuka_vibration_isolation_forest.onnx",
"features": [
{ "tag": "Joint3.Vibration", "window": "5min", "statistic": "rms" },
{ "tag": "Joint3.Temperature", "window": "5min", "statistic": "max" },
{ "tag": "Joint3.Cycles", "window": "1h", "statistic": "sum" }
],
"alertOnAnomaly": true,
"severity": "warning",
"suggestedAction": "Controllare lubrificazione giunto 3"
}
| Caso | Modello | Dimensione | Latenza (CPU) | Addestramento |
|---|---|---|---|---|
| Anomaly detection | Isolation Forest (ONNX) | <5MB | <10ms | Non supervisionato, dati normali |
| Predictive maintenance | LightGBM (ONNX) | <10MB | <50ms | Supervisionato, storico rotture |
| NLU intent | DistilBERT quantized | <70MB | <100ms | Fine-tuning su domande operatore |
| Text generation | LLM locale (opzionale) | 1-4GB | 1-5s | Pre-addestrato, prompt tuning |
| Quality prediction | XGBoost (ONNX) | <5MB | <10ms | Supervisionato, storico scarti |
Problema: allarmi ricorrenti senza correlazione visibile. L'operatore vede lo stesso allarme ripetersi ma non capisce la causa.
Soluzione: AlarmAnalyzer analizza pattern negli allarmi storici:
Input: ultimi 500 allarmi (timestamp, tipo, sequenza, stato macchina)
Output: "L'allarme 'KUKA Joint 2 Overload' si verifica ogni 47±3 cicli.
Correlazione: appare dopo 3+ pieghe consecutive a angolo > 90°.
Suggerimento: considerare un cooldown di 5 cicli dopo pieghe acute."
flowchart LR
subgraph Input["STORICO ALLARMI"]
AH["IAlarmService
.GetHistory(7gg)"]
TM["IMachineTelemetryProvider
.CollectFeaturesAsync()"]
end
subgraph Analysis["ANALYSIS ENGINE"]
Corr["Correlation Engine
sequenze, frequenza,
finestre temporali"]
LLM_A["LLM Prompt
dati aggregati →
analisi in linguaggio naturale"]
end
subgraph Output["OUTPUT"]
Rep["Report periodico
salvato in DB"]
Not["Notifica operatore
(se pattern critico)"]
Dash["Dashboard allarmi
(insights nella sidebar)"]
end
AH --> Corr
TM --> Corr
Corr --> LLM_A
LLM_A --> Rep
LLM_A --> Not
LLM_A --> Dash
{
"patternId": "kuka_joint2_overload_correlation",
"alarmType": "KUKA Joint 2 Overload",
"frequency": "Ogni 47±3 cicli",
"correlation": "Appare dopo 3+ pieghe a angolo > 90°",
"rootCauseHypothesis": "Stress termico cumulativo su giunto 2",
"suggestedAction": "Cooldown 5 cicli dopo pieghe acute > 90°",
"confidence": 0.78,
"dataPoints": 500,
"timeRange": "2026-06-26 → 2026-07-03"
}
ProcessOptimizer analizza storico produzione e suggerisce ottimizzazioni parametriche:
// Input: storico job + parametri + esito
{
"programId": "P123",
"history": [
{ "params": { "speed": "fast", "pressure": 120 }, "cycleTime": 38.2, "scrapRate": 0.02 },
{ "params": { "speed": "normal", "pressure": 110 }, "cycleTime": 42.1, "scrapRate": 0.005 }
]
}
// Output: raccomandazione
{
"recommendation": "Ridurre speed a 'normal' per programmi > 90°",
"expectedImpact": "Scarti -60%, tempo ciclo +10%",
"confidence": 0.82,
"basedOn": "147 job storici con parametri simili"
}
La raccomandazione viene presentata come suggerimento, mai applicata automaticamente:
┌─────────────────────────────────────────────────┐
│ 💡 Suggerimento AI — Programma P123 │
│ │
│ Basandoci su 147 job storici con parametri │
│ simili, ridurre la velocità da 'fast' a │
│ 'normal' per angoli > 90° potrebbe ridurre │
│ gli scarti del 60% con un aumento ciclo │
│ del 10%. │
│ │
│ Confidenza: 82% │
│ │
│ [Applica] [Ignora] [Dettagli] │
└─────────────────────────────────────────────────┘
L'operatore preme "Applica" → i parametri vengono modificati con audit trail. "Ignora" → il suggerimento viene loggato per analysis futura.
Generazione automatica di report e documentazione:
| Report | Input | Output |
|---|---|---|
| Report giornaliero produzione | statistiche turno, allarmi, job completati | PDF/HTML con grafici e commenti |
| Report manutenzione | RUL componenti, allarmi correlati, storico manutenzione | Piano manutenzione prossimo mese |
| Report qualità | tasso scarti, trend, correlazioni parametri | Analisi causa radice + azioni correttive |
| Documentazione commessa | layout.json, manifest.json, config/ | Manuale operatore in PDF |
Il PC industriale (Celeron/ARM) esegue inferenza ONNX locale. Nessun dato produzione esce dalla rete industriale.
PC Industriale
├── HMI.exe (Avalonia)
├── Sistec.Stack.AI.dll
│ ├── models/*.onnx ← modelli leggeri (<50MB per anomaly/predictive)
│ ├── prompts/*.json ← template prompt per LLM
│ ├── OnnxInferenceEngine ← ONNX Runtime (CPU, no GPU richiesta)
│ └── LlmClient (opzionale) ← llama.cpp per NLU + generazione locale
└── Sistec.Stack.Cloud.dll (opzionale)
└── CloudModelUpdater ← scarica modelli aggiornati da cloud
| Componente | Minimo | Consigliato |
|---|---|---|
| CPU | Intel Celeron (2 core) | Intel Core i3 (4 core) |
| RAM | 4GB | 8GB |
| Storage | 1GB libero (modelli) | 5GB (con LLM locale) |
| GPU | Non richiesta | Opzionale per LLM più veloce |
| Rete | LAN industriale | LAN + WAN (per cloud sync modelli) |
L'layer AI si aggancia ai componenti già definiti:
| Componente esistente | Come l'AI lo consuma |
|---|---|
IMachineTelemetryProvider (§9.6) |
Anomaly detector + predictive maintenance |
IProductionAnalyticsService (§12.4) |
Process optimizer + operator assistant |
IAlarmService |
Alarm analyzer |
IJobTracker |
Process optimizer + operator assistant |
Sistec.Stack.Cloud (§13.6) |
CloudModelUpdater + export analytics |
layout.json (§8) |
AiInsightsPanel registrato nel ControlRegistry |
Sistec.Stack.Maintenance (§9.6) |
Predizione RUL ampliata con anomaly detection |
ILogger<T> (§9.7) |
Logging query AI, audit trail suggerimenti |
| Regola | Dettaglio |
|---|---|
| AI = solo lettura | Mai inviare comandi alle macchine. L'AI suggerisce, l'operatore conferma |
| Inferenza locale | Nessun dato produzione lascia il PC industriale (privacy + latency) |
| Fallback a regole | Se il modello ONNX non è disponibile, il sistema funziona con regole if/else |
| Audit trail | Ogni suggerimento AI salvato con timestamp, input, output, esito umano |
| Human-in-the-loop | Ogni azione derivata da suggerimento AI richiede conferma esplicita |
| Nessun PII nei prompt | PIN, badge RFID e password non compaiono mai nei prompt LLM |
| Rate limiting | Max 10 query/minuto per Operator Assistant |
| Model versioning | Ogni modello .onnx versionato, rollback possibile |
Fase 3 (aggiornata): Stack applicativi + AI (4-5 settimane)
├── Sistec.Stack.Production
├── Sistec.Stack.RecipeEngine
├── Sistec.Stack.PalletTracking
├── Sistec.Stack.Maintenance (predictive ML ampliato §9.6)
├── Sistec.Stack.AI (NUOVO)
│ ├── AnomalyDetector (Isolation Forest ONNX) ← priorità alta
│ ├── OperatorAssistant (NLU + query builder) ← priorità alta
│ ├── AlarmAnalyzer (correlation + LLM) ← priorità media
│ ├── ProcessOptimizer (storico + raccomandazioni) ← priorità media
│ ├── DocumentGenerator (report automatici) ← priorità bassa
│ └── AiInsightsPanel (UI Avalonia sidebar) ← priorità alta
└── Sistec.Stack.JobManagement, Alarms
Fase 4 (dopo deploy): Miglioramento continuo
├── Raccolta dati operatore su suggerimenti AI
├── Ri-addestramento modelli con dati reali
├── Aggiunta intent NLU basata su query frequenti
└── Ottimizzazione modelli per edge (quantizzazione)
| Aspetto | Senza AI | Con AI |
|---|---|---|
| Ricerca dati produzione | Navigare 5+ schermate | Barra ricerca: "pezzi oggi" → risposta immediata |
| Allarmi ricorrenti | Manutenzione reattiva | Correlazione automatica + suggerimento causa |
| Ottimizzazione parametri | Trial & error manuale | Raccomandazioni basate su storico |
| Manutenzione | Time-based (spreco) o rotture improvvise | Predictive + anomaly detection |
| Report | Manuale (ore di lavoro) | Generato automaticamente in PDF |
| Onboarding operatore | Formazione lunga | Assistente che risponde in linguaggio naturale |
| Documentazione commessa | Scritta a mano | Generata da layout.json + config/ |
| Limite | Motivo | Mitigazione |
|---|---|---|
| LLM locale su Celeron | Lento (1-5s per risposta) | Usare LLM cloud quando disponibile, locale come fallback |
| Modelli ONNX generici | Qualità inferiore a modelli custom | Addestrare modelli specifici Sistec con dati storici |
| Affidabilità NLU | Intent non sempre corretto | Fallback a query strutturate (dropdown, filtri) |
| Resistenza operatore | "L'AI non capisce il nostro impianto" | Partire da casi d'uso semplici (produzione, allarmi), dimostrare valore |
| Connettività cloud | Fabbriche con rete limitata | Tutto funziona offline (edge AI), cloud opzionale per sync modelli |
Le icone attuali delle codebase LAG/FAEL sono il prodotto di anni di sviluppo incrementale senza un design system unificato. Questa sezione documenta lo stato attuale, identifica i problemi e propone un'architettura greenfield conforme agli standard industriali (ISA-101, IEC 60617, WCAG 2.2).
| Metrica | LAG (5315) | FAEL (5309) | Note |
|---|---|---|---|
| PNG totali (incl. bin/) | ~4.484 | ~6.460 | Duplicazione massiccia |
| PNG unici in Resources/ | 248 | 436 | Cartelle sorgente |
| File identici per nome tra le due codebase | 242 | 242 | Stesse icone copiate due volte |
| Dimensione media | 9,1 KB | 9,8 KB | — |
| Dimensione minima | 1,2 KB | 0,2 KB (224 byte) | linked.png |
| Dimensione massima | 50,6 KB | 322,6 KB | Euro_5301_active.png (logo) |
| Dimensione mediana | 5,5 KB | 5,1 KB | — |
| File unici a questo codebase | 6 | 194 | FAEL ha più icone (3 varianti HMI) |
Il rapporto duplicazione è ~22:1 — per ogni icona unica esistono in media 22 copie nel filesystem (tra Sistec.Controls/Resources/, Sistec.UI/Resources/, Sistec.5315/HMI/Resources/, Sistec.HMI/C/Resources/, Sistec.HMI/AB/Resources/, e le cartelle bin/).
| Formato | LAG | FAEL | Impatto |
|---|---|---|---|
| 8-bit Indexed (palette) | 101 (41%) | 162 (37%) | Nessun alpha channel, transparency limitata |
| 16-bit RGBA | 114 (46%) | 141 (32%) | Buona qualità ma file grandi |
| 8-bit RGBA | 31 (12%) | 87 (20%) | Qualità standard |
| 1-bit / 2-bit / 4-bit Indexed | 1 | 43 | Palette estremamente limitata |
| SVG / Vettoriali | 0 | 1 | Solo templateHMI.svg (artefatto design, non usato) |
Zero icone vettoriali. Ogni icona è un PNG a risoluzione fissa. Su pannelli industriali con DPI variabili o scaling Windows, le icone si pixelano. Le dimensioni non sono potenze di 2 (410x410, 254x254) — suggeriscono design per densità pixel fissa, non scalabile.
| Dimensione | LAG | FAEL | Uso Tipico |
|---|---|---|---|
| 410x410 | 119 (48%) | 233 (53%) | Icone azione/navigazione (_Sistec_*_Enable/Disable) |
| 254x254 | 114 (46%) | 138 (32%) | Stato dispositivi, overlay, fallback (ROBOT_active.png, EmergencyActive.png) |
| 720x720 | 6 (2%) | 12 (3%) | LED indicator (GrayLed.png, GreenLed.png) |
| 794x794 | 4 (2%) | 4 (1%) | Reset (_Sistec_Reset_Enable/Disable) |
| 1230x410 | 4 (2%) | 4 (1%) | Banner/header |
Le LED a 720x720 sono rendering 3D fotorealistici — eccessivamente complessi per HMI industriale. Il Reset a 794x794 è un outlier dimensionale. Le dimensioni miste (410, 254, 720, 794) rendono impossibile un sistema di griglia uniforme.
| Tipo di Incoerenza | Esempi | Impatto |
|---|---|---|
| Typo | EngladFlag_Enable.png (England), Oreder_AZ (Order), Scrol_Down (Scroll), _Siste_WorkMode_X.png (Sistec) |
Confonde sviluppatori, errori di ricerca |
| Stato inconsistente | Disable vs Inactive vs Idle vs Off |
Nessuna convenzione standard |
| Suffisso ambiguo | StopModeActive.png vs StopModeActiveA.png — cosa significa "A"? |
Impossibile capire senza leggere il codice |
| Duplicati | _Sistec_Okay_Enable.png vs _Sistec_Okay_Enable1.png |
Quale usare? |
| Lingua mista | ControlloSpessore_Enable.png (italiano) accanto a RecipeSave (inglese) |
Incoerenza linguistica |
| Prefisso errato | _Siste_WorkMode_X.png (mancante 'c') |
Typo nel prefisso aziendale |
| Plurale errato | _Sistec_RecepiesEdit_Enable.png (Recepies ≠ Recipes) |
Errori di ortografia nel codice |
| Caso inconsistente | GateActive.png (PascalCase) vs _Sistec_Valve_Lock_Enable.png (underscore) |
Stili di naming misti |
Analisi dei 47 bottoni critici nelle schermate principali:
| Schermata | Pulsante | Icona Usata | Problema |
|---|---|---|---|
| Home | Nuovo Job | RecipeAdd (+ ricetta) |
Icona specifica ricetta per azione generica |
| Home | Cronologia Job | RecipeDB (database ricette) |
Icona ricetta per storico job |
| Home (FAEL) | Ispeziona Ordini | RecipeSearch |
Icona ricerca ricetta per ordini |
| Alarms | Reset Allarmi | Option (checkmark generico) |
Azione di sicurezza con icona non urgente |
| JobView | Stop Job | Delete (cestino) |
Stop ≠ Delete — azione completamente diversa |
| JobView | Esegui Job | Confirm (checkmark) |
Checkmark non comunica "avvia produzione" |
| Maintenance | Reset Contatore | Success (checkmark OK) |
Reset ≠ approvazione completata |
| Recipes | 4 Refresh diversi | Tutti RecipeLoad |
Nessuna distinzione visiva tra Robot/Punching/Bending/Program |
Risultato: 18/47 bottoni critici (38%) hanno un'icona semanticamente ambigua. L'operatore deve ricordare cosa fa ogni pulsante perché l'icona non lo comunica.
| Problema | Dettaglio |
|---|---|
| Dual-source pattern | ImageRepository (PNG da disco) + .resx (base64 embedded) coesistono — stesso pulsante caricato da fonti diverse |
| Overlay manuale | DisableOverlay.png e FocusOverlay.png sono PNG semi-trasparenti composti a runtime via GDI+ Graphics.DrawImage — non vettoriale, non scalabile |
| Bypass RoundSymbols | ToolStripButton nella main menu assegna icone direttamente, bypassando completamente RoundSymbols e ControlsExtensions |
| Fallback silenzioso | NotFound.png (254x254, 4,6 KB) viene mostrato quando un'icona manca — l'operatore vede un'icona generica senza sapere cosa manca |
| Nessun audit | Nessun tool verifica quali icone sono effettivamente usate nel codice — icone orfane accumulano nel tempo |
Le 6 LED indicator (GrayLed.png, BlueLed.png, GreenLed.png, YellowLed.png, OrangeLed.png, RedLed.png) sono 720x720 pixel, 8-bit RGBA, ~50 KB ciascuna. Sono rendering 3D con gradienti, ombre e riflessi — eccessivamente complessi per un HMI industriale dove il LED è un semplice indicatore circolare on/off.
FAEL ha inoltre duplicati italiani identici: Led_Grigio.png, Led_Blu.png, Led_Arancio.png, Led_Verde.png, Led_Rosso.png, Led_Giallo.png — stessi identici byte delle versioni inglesi.
| Principio | Applicazione Icone |
|---|---|
| Colore secondario, non primario | Icone in grigio di default; colore solo per stati anomali |
| Filosofia gray-scale | La maggior parte dell'HMI è grigia/muta. Il colore è riservato alle eccezioni |
| Gerarchia allarme a 4 livelli | Advisory → Warning → Critical → Emergency, ognuno con forma + dimensione + outline distinti |
| Situazione awareness | Icone devono supportare percezione (cosa succede), comprensione (cosa significa), proiezione (cosa succederà) |
| Standard | Ambito | Applicazione |
|---|---|---|
| IEC 60617 | Simboli per schemi elettrici | ~1.900 simboli: conduttori, componenti passivi, semiconduttori, interruttori, strumenti |
| ISO 7000 / IEC 60417 | Simboli su attrezzature fisiche | Simboli per etichette, nomina, pannelli |
| ISO 7010 | Segni di sicurezza | Pericolo, divieto, obbligo, salvataggio |
Per l'HMI serv ibrido: accuratezza tecnica da IEC 60617/ISO 7000, adattata per leggibilità su schermo.
| Criterio | Requisito | Applicazione |
|---|---|---|
| SC 1.4.11 | Contrasto non-testuale ≥ 3:1 | Icone interattive e di stato |
| SC 1.4.1 | Non affidarsi solo al colore | Ogni stato icona ha forma + colore (mai solo colore) |
| SC 2.5.5 | Target size ≥ 44x44px (AAA) | Touch target per guanti industriali: 64x64px |
| Griglia | Uso | Stroke | Note |
|---|---|---|---|
| 24x24px | Grid base per design | 2px | Standard industry (Phosphor, Material, Lucide) |
| 32x32px | Toolbar, bottoni secondari | 2,5px | HMI toolbar standard |
| 48x48px | Touch target primario | 3px | Bottoni principali, azioni critiche |
| 64x64px | Status indicator, dashboard | 3px | Dispositivi, allarmi |
Regola: ogni icona viene progettata su griglia 24x24 con padding 2px, poi esportata a 24/32/48/64px. Il design è sempre lo stesso, solo lo stroke scala.
Griglia 24x24 con padding 2px:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
0 ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░
1 ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░
2 ░░ ░░ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ░░ ░░
3 ░░ ░░ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ░░ ░░
...
22 ░░ ░░ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ░░ ░░
23 ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░
Area utile: 20x20 px (dal pixel 2 al pixel 21)
| Stile | Quando | Esempio |
|---|---|---|
| Outline (stroke) | Default, stato inattivo | Icona con bordo sottile, interno vuoto |
| Filled (pieno) | Stato attivo/selezionato | Icona piena, bordo non visibile |
| Duotone | Solo per elementi decorativi/dashboard | Due toni per profondità — mai per status critici |
Stroke width per dimensione:
| Dimensione | Stroke | Raggio angoli | Note |
|---|---|---|---|
| 24px | 2px | 1px | Standard Lucide/Material |
| 32px | 2,5px | 1,5px | Leggermente più spesso per visibilità |
| 48px | 3px | 2px | Touch target, alto contrasto |
| 64px | 3px | 2px | Stesso di 48px, area maggiore |
Regole di design:
| Stato | Colore Hex | Contrasto su #1A1A1A | Forma | Uso |
|---|---|---|---|---|
| Normale | #B0B0B0 (grigio chiaro) |
7,5:1 | Qualsiasi | Default, stato operativo normale |
| Attivo/In esecuzione | #66BB6A (verde chiaro) |
5,8:1 | Cerchio pieno | Robot in movimento, pressa attiva |
| Disabilitato | #757575 (grigio scuro) + opacity 0,4 |
Basso (intenzionale) | Opacity ridotta + desaturazione | Controlli non disponibili |
| Errore/Fault | #EF5350 (rosso chiaro) |
4,6:1 | Cerchio con X | Allarme, fault dispositivo |
| Warning | #FFA726 (arancione chiaro) |
5,2:1 | Triangolo con ! | Allarme avanzviso, soglia vicina |
| Successo/OK | #66BB6A (verde) |
5,8:1 | Cerchio con checkmark | Operazione completata |
| Info | #42A5F5 (blu chiaro) |
4,8:1 | Cerchio con i | Informazione contestuale |
| Emergenza | #FF1744 (rosso brillante) |
6,2:1 | Triangolo pieno rosso | Emergency stop, pericolo imminente |
Regola fondamentale ISA-101: il colore è SECONDARIO. L'icona deve essere riconoscibile per FORMA anche senza colore. Un operatore con daltonismo (8% degli uomini) deve poter distinguere tutti gli stati.
| Stato | Trattamento Visivo | Implementazione Avalonia |
|---|---|---|
| Normale/Abilitato | Opacity 1,0, colore standard | Opacity="1" Foreground="{DynamicResource IconNormal}" |
| Disabilitato | Opacity 0,35-0,45, desaturato a grigio | Opacity="0.4" Foreground="{DynamicResource IconDisabled}" con Blur opzionale |
| Hover/Focus | Luce incrementale + anello focus | Brightness 1,1 + FocusVisualStyle con border 2px |
| Premuto/Attivo | Leggero scurimento o scala | Brightness 0,9 o ScaleTransform 0,95 |
| In esecuzione | Spinner o animazione pulse | Path con Rotation animata o Opacity pulsante |
| Errore | Colore errore + shake opzionale | Foreground="{DynamicResource ColorError}" + TranslateTransform animato |
Transizioni di stato — mapping legacy → greenfield:
| Stato Legacy | Trattamento Legacy | Trattamento Greenfield |
|---|---|---|
ActiveImage |
PNG diverso caricato da disco | Cambio Foreground + Data via binding |
InactiveImage |
PNG diverso caricato da disco | Stesso Path con colore normale |
DisableOverlay.png |
PNG semi-trasparente composto via GDI+ | Opacity="0.4" + GrayscaleColorFilter |
FocusOverlay.png |
PNG highlight composto via GDI+ | FocusVisualStyle con border animato |
SuccessImage / FailImage |
PNG temporaneo mostrato per 500ms | Animazione Scale + ColorFlash per 500ms |
| Scenario | Touch Target Minimo | Spaziatura Minima | Note |
|---|---|---|---|
| Mano nuda | 48x48px | 8px | WCAG AAA (44px) + margine |
| Guanti industriali | 64x64px | 12px | Dito ingrossato ~2,5cm |
| Pannello industriale | 72x72px | 16px | Riflessi, angolo di visione |
Regole per touch:
Sistec.Icons (NuGet package)
├── Assets/
│ └── Icons/
│ ├── Nav/ ← Icone navigazione
│ │ ├── nav.home.svg
│ │ ├── nav.history.svg
│ │ ├── nav.settings.svg
│ │ ├── nav.alarms.svg
│ │ ├── nav.maintenance.svg
│ │ ├── nav.recipe.svg
│ │ └── nav.plc.svg
│ ├── Action/ ← Azioni utente
│ │ ├── action.add.svg
│ │ ├── action.delete.svg
│ │ ├── action.reset.svg
│ │ ├── action.play.svg
│ │ ├── action.stop.svg
│ │ ├── action.save.svg
│ │ ├── action.edit.svg
│ │ ├── action.search.svg
│ │ ├── action.download.svg
│ │ ├── action.refresh.svg
│ │ ├── action.filter.svg
│ │ └── action.undo.svg
│ ├── Device/ ← Dispositivi industriali
│ │ ├── device.robot.svg
│ │ ├── device.plc.svg
│ │ ├── device.press.svg
│ │ ├── device.punching.svg
│ │ ├── device.motor.svg
│ │ ├── device.motor.home.svg
│ │ ├── device.motor.autopos.svg
│ │ └── device.valve.svg
│ ├── Status/ ← Stato e allarmi
│ │ ├── status.ok.svg
│ │ ├── status.error.svg
│ │ ├── status.warning.svg
│ │ ├── status.info.svg
│ │ └── status.emergency.svg
│ ├── Mode/ ← Modalità operativa
│ │ ├── mode.auto.svg
│ │ ├── mode.manual.svg
│ │ └── mode.edit.svg
│ ├── Safety/ ← Sicurezza
│ │ ├── safety.gate.svg
│ │ ├── safety.light.svg
│ │ ├── safety.key.svg
│ │ └── safety.emergency.svg
│ └── Overlay/ ← Overlay (opzionale, per retrocompatibilità)
│ ├── overlay.disable.svg
│ └── overlay.focus.svg
├── IconRegistry.cs ← Mapping enum → SVG path
├── SistecIcon.cs ← Enum con tutte le icone
├── IconConverters.cs ← Avalonia value converters
└── Sistec.Icons.csproj
// Sistec.Icons/SistecIcon.cs
public enum SistecIcon
{
None,
// Navigation (7)
NavHome, NavHistory, NavSettings, NavAlarms,
NavMaintenance, NavRecipe, NavPlc,
// Action (12)
ActionAdd, ActionDelete, ActionReset, ActionPlay, ActionStop,
ActionSave, ActionEdit, ActionSearch, ActionDownload,
ActionRefresh, ActionFilter, ActionUndo,
// Device (8)
DeviceRobot, DevicePlc, DevicePress, DevicePunching,
DeviceMotor, DeviceMotorHome, DeviceMotorAutoPos, DeviceValve,
// Status (5)
StatusOk, StatusError, StatusWarning, StatusInfo, StatusEmergency,
// Mode (3)
ModeAuto, ModeManual, ModeEdit,
// Safety (4)
SafetyGate, SafetyLight, SafetyKey, SafetyEmergency
}
// Sistec.Icons/IconRegistry.cs
public static class IconRegistry
{
private static readonly Dictionary<SistecIcon, string> Paths = new()
{
[SistecIcon.NavHome] = "avares://Sistec.Icons/Assets/Icons/Nav/nav.home.svg",
[SistecIcon.ActionAdd] = "avares://Sistec.Icons/Assets/Icons/Action/action.add.svg",
[SistecIcon.DeviceRobot] = "avares://Sistec.Icons/Assets/Icons/Device/device.robot.svg",
[SistecIcon.StatusError] = "avares://Sistec.Icons/Assets/Icons/Status/status.error.svg",
[SistecIcon.ModeAuto] = "avares://Sistec.Icons/Assets/Icons/Mode/mode.auto.svg",
[SistecIcon.SafetyGate] = "avares://Sistec.Icons/Assets/Icons/Safety/safety.gate.svg",
// ... mapping completo per ogni SistecIcon
};
public static StreamGeometry GetGeometry(SistecIcon icon) =>
GetGeometry(Paths[icon]);
public static StreamGeometry GetGeometry(string svgPath)
{
// Parsing SVG path data → Avalonia StreamGeometry
var svg = AssetLoader.Open(new Uri(svgPath));
// Conversione build-time o runtime
return SvgConverter.ToGeometry(svg);
}
}
Mappatura RoundSymbols → SistecIcon (corretta):
| RoundSymbols Legacy | SistecIcon Greenfield | Motivo del Cambio |
|---|---|---|
None |
SistecIcon.None |
Invariato |
Success |
SistecIcon.StatusOk |
Generico "OK", non solo successo |
Fail |
SistecIcon.StatusError |
Errore, non solo fallimento |
Config |
SistecIcon.NavSettings |
Impostazioni = navigazione |
Reset |
SistecIcon.ActionReset |
Azione reset generica |
Locked / Unlocked |
SistecIcon.SafetyKey |
Chiave = accesso |
Minus / Plus |
SistecIcon.ActionAdd / ActionDelete |
Più = aggiungi, meno = rimuovi |
Motor |
SistecIcon.DeviceMotor |
Dispositivo, non azione |
MotorHome |
SistecIcon.DeviceMotorHome |
Dispositivo con azione |
Robot |
SistecIcon.DeviceRobot |
Dispositivo |
Calibration |
SistecIcon.ActionEdit |
Calibrazione = modifica parametri |
EditMode |
SistecIcon.ModeEdit |
Modalità |
ManualMode |
SistecIcon.ModeManual |
Modalità |
AutoMode |
SistecIcon.ModeAuto |
Modalità |
RecipeAdd |
SistecIcon.ActionAdd |
Azione generica, non specifica ricetta |
RecipeDelete |
SistecIcon.ActionDelete |
Azione generica |
RecipeSave |
SistecIcon.ActionSave |
Azione generica |
RecipeSearch |
SistecIcon.ActionSearch |
Azione generica |
RecipeDB |
SistecIcon.NavHistory |
Database = storico |
Delete |
SistecIcon.ActionDelete |
Elimina = azione |
Gate |
SistecIcon.SafetyGate |
Sicurezza |
LightCurtain |
SistecIcon.SafetyLight |
Sicurezza |
AccessKey |
SistecIcon.SafetyKey |
Sicurezza |
EmergencyStop |
SistecIcon.SafetyEmergency |
Sicurezza |
AlarmInhibit |
SistecIcon.StatusWarning |
Warning = allarme inibito |
<!-- Stile base per PathIcon industriale -->
<Style Selector="PathIcon.hmi-icon">
<Setter Property="Width" Value="32"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Foreground" Value="{DynamicResource IconNormal}"/>
</Style>
<!-- Stato errore -->
<Style Selector="PathIcon.hmi-icon.status-error">
<Setter Property="Foreground" Value="{DynamicResource ColorError}"/>
</Style>
<!-- Stato disabilitato -->
<Style Selector="PathIcon.hmi-icon.is-disabled">
<Setter Property="Opacity" Value="0.4"/>
</Style>
<!-- Touch target minimo 64x64 -->
<Style Selector="PathIcon.hmi-icon.touch-target">
<Setter Property="Width" Value="64"/>
<Setter Property="Height" Value="64"/>
<Setter Property="MinWidth" Value="64"/>
<Setter Property="MinHeight" Value="64"/>
</Style>
<!-- Uso in XAML -->
<PathIcon Data="{x:Static icons:IconRegistry.GetGeometry(SistecIcon.DeviceRobot)}"
Classes="hmi-icon touch-target"
Foreground="{DynamicResource ColorActive}"/>
Alternativa: Image con SVG (per icone complesse P&ID):
<!-- Per simboli IEC 60617 con colori multipli -->
<Image Source="avares://Sistec.Icons/Assets/Icons/Device/device.robot.svg"
Width="48" Height="48"
RenderOptions.BitmapInterpolationMode="HighQuality"/>
{Category}.{Action/Subject}.svg
| Categoria | Prefisso | Contenuto |
|---|---|---|
| Nav | nav. |
Navigazione, pagine, menu |
| Action | action. |
Azioni utente (add, delete, save, ecc.) |
| Device | device. |
Dispositivi industriali (robot, plc, press) |
| Status | status. |
Stato sistema (ok, error, warning) |
| Mode | mode. |
Modalità operativa (auto, manual, edit) |
| Safety | safety. |
Sicurezza (gate, light, key, emergency) |
| Overlay | overlay. |
Overlay semi-trasparenti (disable, focus) |
Regole di naming:
devices/, no actions/)| Pulsante Schermata | Icona Legacy (attuale) | Icona Greenfield (proposta) | Motivo |
|---|---|---|---|
| Home: Nuovo Job | RecipeAdd (+ ricetta) |
ActionAdd (+ generico) |
Job ≠ Ricetta |
| Home: Cronologia Job | RecipeDB (database) |
NavHistory (orologio/storico) |
Cronologia ≠ Database |
| Home: Ispeziona Ordini | RecipeSearch |
ActionSearch (lente) |
Ricerca generica |
| Home: Clean Program | RecipeClean |
ActionRefresh (frecce circolari) |
Clean ≠ Refresh |
| Alarms: Reset Allarmi | Option (checkmark) |
ActionReset (frecce circolari) |
Reset = frecce, non check |
| Alarms: Show Alarms | _Sistec_Sts1_AlarmV2 |
StatusError (cerchio X) |
Allarme = errore |
| Alarms: Show Warnings | _Sistec_Sts2_WarningV2 |
StatusWarning (triangolo !) |
Warning = triangolo |
| JobView: Esegui Job | Confirm (checkmark) |
ActionPlay (triangolo) |
Esegui = play, universale |
| JobView: Stop Job | Delete (cestino) |
ActionStop (quadrato) |
Stop ≠ Delete |
| JobView: Reset Job | Delete (cestino) |
ActionReset (frecce circolari) |
Reset ≠ Delete |
| Maintenance: Reset Contatore | Success (checkmark OK) |
ActionReset (frecce circolari) |
Reset ≠ Approvazione |
| Recipes: Refresh Robot | RecipeLoad |
ActionRefresh + DeviceRobot |
Combinazione refresh + device |
| Recipes: Refresh Punching | RecipeLoad |
ActionRefresh + DevicePunching |
Combinazione refresh + device |
| Recipes: Refresh Bending | RecipeLoad |
ActionRefresh + DevicePress |
Combinazione refresh + device |
| Recipes: Refresh Program | RecipeLoad |
ActionRefresh |
Refresh generico |
| Recipes: Save | RecipeSave |
ActionSave |
Salva generico |
| Recipes: Delete | RecipeDelete |
ActionDelete |
Elimina generico |
| Recipes: Edit Name | RecipeEdit |
ActionEdit |
Modifica generica |
| PressBrake: Auto | AutoMode (gear) |
ModeAuto (gear) |
Invariato |
| PressBrake: Manual | ManualMode (hand) |
ModeManual (hand) |
Invariato |
| PressBrake: Edit | EditMode (pen) |
ModeEdit (pen) |
Invariato |
| PressBrake: Program | Download |
ActionDownload |
Download generico |
| PressBrake: Reset | Reset |
ActionReset |
Invariato |
Per le icone che rappresentano componenti di processo (valvole, pompe, motori, serbatoi) servono simboli conformi a IEC 60617. Queste icone sono più complesse delle icone UI standard e richiedono un trattamento dedicato.
| Componente | Standard | Forma Base | Variazioni |
|---|---|---|---|
| Valvola a sfera | IEC 60617-05-04 | Cerchio con linea | Aperta/chiusa (colore) |
| Valvola a farfalla | IEC 60617-05-05 | Triangolo | Aperta/chiusa |
| Pompa | IEC 60617-06-01 | Cerchio con triangolo | On/off, allarme |
| Motore | IEC 60617-06-03 | Cerchio con M | On/off/fault |
| Scambiatore calore | IEC 60617-06-05 | Cerchio con linee | Attivo/inattivo |
| Serbatoio | IEC 60617-10-01 | Rettangolo aperto | Livello |
| Sensore di livello | IEC 60617-07-02 | Triangolo | High/low |
Implementazione: SVG in Sistec.Icons/Assets/Icons/Process/ con proprietà dinamiche via Avalonia binding. Per questi simboli, usare Image con SVG rendering (Svg.Skia) invece di PathIcon — sono troppo complessi per StreamGeometry.
<!-- Simbolo valvola dinamico -->
<Image Source="avares://Sistec.Icons/Assets/Icons/Process/valve.ball.svg"
Width="48" Height="48">
<Image.Styles>
<Style Selector="Image[IsOpen=True]">
<Setter Property="Source" Value="avares://Sistec.Icons/Assets/Icons/Process/valve.ball.open.svg"/>
</Style>
</Image.Styles>
</Image>
Fase 1 — Fondazioni (con Sistec.Theme)
├── Creare package Sistec.Icons (SVG base, ~39 icone)
│ ├── 7 Nav + 12 Action + 8 Device + 5 Status + 3 Mode + 4 Safety
│ ├── IconRegistry.cs + SistecIcon.cs
│ └── Conversione SVG → StreamGeometry (build tool)
├── Integrare in Sistec.Theme
│ ├── Colori DynamicResource per ogni stato
│ ├── Stili PathIcon base (hmi-icon, touch-target, status-*)
│ └── Sostituire LED 720x720 con SVG circolare semplice
└── Sforzo: 3-5 giorni
Fase 2 — Stack Macchina
├── Sostituire RoundSymbols in 1 stack (Kuka) come pilota
│ ├── Mapping: RoundSymbols → SistecIcon
│ ├── Test: tutti i bottoni funzionano con nuove icone
│ └── Verifica: leggibilità su pannello 15" industriale
└── Sforzo: 1-2 giorni
Fase 3 — Applicativi + UI
├── Sostituire icone in 3 schermate core
│ ├── Home: 4 bottoni (Nuovo Job, Cronologia, Ispeziona, Clean)
│ ├── Production: bottoni JobView (Esegui, Stop, Reset)
│ └── Maintenance: Reset contatore
├── Aggiungere icone P&ID per device status
│ ├── Robot, PLC, Press, Punching → SVG invece di PNG
│ └── Stati: active/inactive/error via Foreground binding
└── Sforzo: 3-5 giorni
Fase 4 — Deploy
├── Verifica leggibilità su pannello industriale reale
│ ├── Test con guanti industriali
│ ├── Test con riflessi ambientali
│ └── Test con angolo di visione 30/45/60 gradi
├── Eliminare PNG legacy non più usati
└── Sforzo: 1 giorno
| Problema Identificato | Soluzione | Componente Architetturale | Fase |
|---|---|---|---|
| PNG raster 100%, nessuna scalabilità | SVG vettoriali + StreamGeometry | Sistec.Icons NuGet |
Fase 1 |
| 10.900 file, duplicazione 22:1 | Singolo package NuGet, zero duplicazione | Sistec.Icons |
Fase 1 |
| Naming incoerente, typo | Convention {Category}.{Action}.svg uniforme |
Sistec.Icons/Assets/Icons/ |
Fase 1 |
| 38% bottoni critici ambigui | Mapping semantico corretto (§23.6) | IconRegistry.cs |
Fase 1-3 |
| Overlay PNG compositi manualmente | Opacity + Foreground via Avalonia theme | Sistec.Theme |
Fase 1 |
| LED 720x720 fotorealistici | SVG circolare 24px con colore dinamico | Sistec.Icons + Sistec.Theme |
Fase 1 |
| Nessun icon font | Phosphor Icons come base, SVG export | Sistec.Icons |
Fase 1 |
| Dual-source (ImageRepository + .resx) | Singola fonte: IconRegistry.GetGeometry() |
Sistec.Icons |
Fase 2-3 |
| Bypass RoundSymbols in ToolStrip | PathIcon con binding in tutta la UI |
Sistec.Theme |
Fase 3 |
| Touch target insufficienti per guanti | Minimo 64x64px, padding 12px | Sistec.Controls.Touch + Sistec.Theme |
Fase 1 |
| Nessun colore per stato anomalo | Palette ISA-101 con 7 stati + forma distintiva | Sistec.Theme Colors |
Fase 1 |
| Simboli P&ID inesistenti | SVG IEC 60617 in Process/ |
Sistec.Icons/Assets/Icons/Process/ |
Fase 3 |
Documento aggiornato il 2026-07-03 — Sezioni 22 (Integrazione AI), 23 (Icon Design System) e 24 (Standards Compliance) aggiunte.
Questa sezione definisce lo schema ER completo per la nuova commessa greenfield, migliorando radicalmente il design attuale (zero FK, zero indici, blob per dati strutturati, password in chiaro) e aggiungendo le tabelle necessarie per integrazione MES, analisi cloud e manutenzione predittiva.
| Principio | Applicazione |
|---|---|
| FK su ogni relazione | Niente orfani. ON DELETE RESTRICT per dati produttivi, ON DELETE CASCADE per log |
| Indici su ogni campo di ricerca/filtro | WHERE, JOIN, ORDER BY, GROUP BY devono essere sempre coperti da index |
| Niente blob per dati strutturati | cut_plan.Data → tabella cut_plan_items. persistent_vars.Blob → colonne tipizzate |
| Timestamp ovunque | created_at, updated_at, synced_at su ogni tabella per sync cloud |
| Partition-friendly | Chiavi primarie con timestamp embedded per partizionamento mensile |
| Niente dati cloud nel DB locale | Solo ID di riferimento + flag di sync. I dati analitici stanno nel cloud |
| Enum come TINYINT | Performance + integrità. Le lookup table servono solo per configurazione variabile |
| Password/PIN mai in chiaro | Solo hash + salt. VARCHAR(256) per BCrypt/Argon2 |
erDiagram
%% ── AUTH ──
roles {
int id PK
varchar name "Supervisore, Operatore, Manutenzione"
bigint permissions "Bitmask: 1=CanEditParams, 2=CanReset, 4=CanManageUsers"
bool is_active
datetime created_at
}
employees {
int badge_id PK "4-6 cifre, badge RFID"
varchar first_name
varchar last_name
tinyint shift "1=Mattina, 2=Pomeriggio, 3=Notte"
int role_id FK
varchar pin_hash
varchar pin_salt
bool is_active
datetime last_login
datetime created_at
}
%% ── PRODUZIONE ──
recipes {
int id PK
varchar program_code
tinyint working_mode
varchar robot_program
varchar punching_program
varchar bending_program
int punching_bay
int punching_items_per_sheet
varchar comment
datetime created_at
datetime updated_at
}
orders {
int id PK
int order_code
int recipe_id FK
int target_items_count
int items_completed_count
int scrap_items_count
varchar notes
datetime created_at
datetime updated_at
}
jobs {
int id PK
int order_id FK
int recipe_id FK
tinyint status "JobStatus enum: 0=Ready, 1=Running, 2=Completed..."
int target_items_count
int items_completed_count
int scrap_items_count
int operator_id FK "who started the job"
varchar notes
datetime created_at
datetime updated_at
datetime completed_at
}
job_items {
bigint id PK
int job_id FK
int sequence_number "1-based per job"
varchar item_code
varchar program_used
tinyint status "0=Pending, 1=Running, 2=Completed, 3=Scrapped"
datetime started_at
datetime completed_at
datetime created_at
}
production_cycles {
bigint id PK
int job_id FK
int job_item_id FK "nullable, NULL = setup/non-prod"
varchar machine_id "Kuka_0, Safan_0, Plc_0"
varchar cycle_type "Bend, Punch, Load, Unload, Transport, Wait"
int duration_ms
bool success
varchar error_code "NULL if success"
datetime started_at
datetime created_at
}
%% ── CUT PLAN ──
cut_plans {
int job_id PK "FK → jobs.id"
int plasterboard_length
int num_panels_per_portal
int num_of_panels_used
int scrap_into_pallet
int scrap_into_box
datetime created_at
}
cut_plan_items {
bigint id PK
int cut_plan_job_id FK
int sequence_number
int length_mm
int width_mm
tinyint type "0=Panel, 1=Scrap, 2=Waste"
tinyint quality "0=OK, 1=Defect, 2=Rework"
datetime created_at
}
%% ── SCRAP / QUALITY ──
scrap_events {
bigint id PK
int job_id FK
int job_item_id FK
tinyint scrap_type "1=Material, 2=Dimension, 3=Program, 4=Machine"
varchar reason
decimal weight_kg
int operator_id FK "who caused/scrapped it"
datetime created_at
}
piece_tracking {
bigint id PK
int job_id FK
int job_item_id FK
varchar serial_number
varchar qr_code
tinyint status "0=Produced, 1=QC_Passed, 2=QC_Failed, 3=Shipped, 4=Rework"
int qc_operator_id FK
datetime qc_timestamp
created_at
}
%% ── MANUTENZIONE PREDITTIVA ──
machine_components {
int id PK
varchar machine_id
varchar component_name "Joint_1, Cylinder_Main, Belt_Drive"
varchar model_file "kuka_joint_rul.onnx"
decimal rul_threshold_warning
decimal rul_threshold_critical
int fallback_hours "time-based schedule if ML unavailable"
tinyint unit "0=Hours, 1=Cycles, 2=Days"
bool is_active
datetime created_at
}
telemetry_windows {
bigint id PK
int component_id FK
datetime window_start "start of aggregation window"
datetime window_end "end of aggregation window"
json features "{\"avg_torque\": 12.3, \"max_temp\": 87.5, \"sum_cycles\": 150}"
decimal rul_predicted "NULL if no model"
decimal rul_confidence "0.0-1.0"
tinyint quality "0=Raw, 1=Validated, 2=Anomaly"
datetime synced_at "NULL if not synced to cloud"
}
rul_predictions {
bigint id PK
int component_id FK
decimal rul_hours
decimal confidence
tinyint model_version
json features_snapshot "features used for this prediction"
datetime predicted_at
}
maintenance_tasks {
int id PK
int component_id FK
int created_by_operator_id FK
varchar title
varchar description
tinyint priority "0=Low, 1=Medium, 2=High, 3=Critical"
tinyint source "0=TimeBased, 1=PredictiveML, 2=Manual, 3=Failure"
tinyint status "0=Open, 1=InProgress, 2=Completed, 3=Cancelled"
datetime due_at
datetime completed_at
int completed_by_operator_id FK
datetime created_at
}
maintenance_history {
bigint id PK
int component_id FK
int task_id FK "nullable"
int operator_id FK
tinyint intervention_type "0=Inspection, 1=Repair, 2=Replacement, 3=Lubrication, 4=Reset"
varchar notes
decimal cost_estimate
datetime performed_at
datetime created_at
}
component_failures {
bigint id PK
int component_id FK
int related_cycle_id FK "nullable, production_cycles.id"
varchar failure_mode "Overload, Wear, Jam, Overheat"
decimal downtime_hours
decimal repair_cost
tinyint root_cause "0=NormalWear, 1=OperatorError, 2=Design, 3=MaintenanceDelay"
datetime failed_at
datetime created_at
}
%% ── CLOUD SYNC ──
cloud_sync_log {
bigint id PK
varchar table_name "employees, telemetry_windows, rul_predictions..."
bigint record_id
tinyint operation "0=Insert, 1=Update, 2=Delete"
tinyint status "0=Pending, 1=Synced, 2=Failed"
int retry_count
varchar error_message
datetime created_at
datetime synced_at
}
plant_config {
varchar plant_id PK "sistec-lag-5315"
varchar plant_name "LAG 5315"
varchar cloud_endpoint "https://cloud.sistec.it/api/v1"
varchar mqtt_broker "192.168.1.50:1883"
varchar sparkplug_group "sistec"
tinyint sync_interval_minutes
datetime last_heartbeat
datetime created_at
}
%% ── RELAZIONI ──
roles ||--o{ employees : "has"
employees ||--o{ jobs : "started_by"
employees ||--o{ scrap_events : "caused_by"
employees ||--o{ piece_tracking : "qc_by"
employees ||--o{ maintenance_tasks : "created_by"
employees ||--o{ maintenance_tasks : "completed_by"
employees ||--o{ maintenance_history : "performed_by"
recipes ||--o{ orders : "referenced_by"
recipes ||--o{ jobs : "used_in"
orders ||--o{ jobs : "contains"
jobs ||--o{ job_items : "has_items"
jobs ||--o{ production_cycles : "has_cycles"
jobs ||--o{ scrap_events : "has_scraps"
jobs ||--o{ piece_tracking : "has_pieces"
jobs ||--o{ cut_plans : "has_cut_plan"
job_items ||--o{ production_cycles : "cycle_detail"
job_items ||--o{ scrap_events : "scrap_detail"
job_items ||--o{ piece_tracking : "tracking"
cut_plans ||--o{ cut_plan_items : "has_items"
machine_components ||--o{ telemetry_windows : "telemetry"
machine_components ||--o{ rul_predictions : "rul"
machine_components ||--o{ maintenance_tasks : "tasks"
machine_components ||--o{ maintenance_history : "history"
machine_components ||--o{ component_failures : "failures"
production_cycles ||--o{ component_failures : "failure_cycle"
maintenance_tasks ||--o{ maintenance_history : "task_execution"
-- Tabella ordini con FK e indici per MES
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_code INT NOT NULL,
recipe_id INT NOT NULL,
target_items_count INT NOT NULL DEFAULT 0,
items_completed_count INT NOT NULL DEFAULT 0,
scrap_items_count INT NOT NULL DEFAULT 0,
notes VARCHAR(1023),
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NOT NULL DEFAULT NOW() ON UPDATE NOW(),
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE RESTRICT,
INDEX idx_orders_order_code (order_code),
INDEX idx_orders_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Tabella job con FK a ordine, ricetta e operatore
CREATE TABLE jobs (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
recipe_id INT NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
target_items_count INT NOT NULL DEFAULT 0,
items_completed_count INT NOT NULL DEFAULT 0,
scrap_items_count INT NOT NULL DEFAULT 0,
operator_id INT,
notes VARCHAR(1023),
created_at DATETIME NOT NULL DEFAULT NOW(),
updated_at DATETIME NOT NULL DEFAULT NOW() ON UPDATE NOW(),
completed_at DATETIME,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE RESTRICT,
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE RESTRICT,
FOREIGN KEY (operator_id) REFERENCES employees(badge_id) ON DELETE SET NULL,
INDEX idx_jobs_status (status),
INDEX idx_jobs_operator (operator_id),
INDEX idx_jobs_created (created_at),
INDEX idx_jobs_order (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Singoli item di un job (addio blob)
CREATE TABLE job_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
job_id INT NOT NULL,
sequence_number INT NOT NULL,
item_code VARCHAR(100),
program_used VARCHAR(100),
status TINYINT NOT NULL DEFAULT 0 COMMENT '0=Pending, 1=Running, 2=Completed, 3=Scrapped',
started_at DATETIME,
completed_at DATETIME,
created_at DATETIME NOT NULL DEFAULT NOW(),
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE,
INDEX idx_items_job (job_id, sequence_number),
INDEX idx_items_status (job_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Ogni ciclo macchina registrato (analytics + OEE)
CREATE TABLE production_cycles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
job_id INT NOT NULL,
job_item_id BIGINT,
machine_id VARCHAR(50) NOT NULL COMMENT 'Kuka_0, Safan_0, Plc_0',
cycle_type VARCHAR(50) NOT NULL COMMENT 'Bend, Punch, Load, Unload, Wait',
duration_ms INT NOT NULL COMMENT 'cycle time in milliseconds',
success BOOLEAN NOT NULL DEFAULT TRUE,
error_code VARCHAR(100),
started_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT NOW(),
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE,
FOREIGN KEY (job_item_id) REFERENCES job_items(id) ON DELETE SET NULL,
INDEX idx_cycles_machine (machine_id, started_at),
INDEX idx_cycles_job (job_id, started_at),
INDEX idx_cycles_type (cycle_type, started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Singoli pezzi tagliati (addio longblob cut_plan.Data)
CREATE TABLE cut_plan_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
cut_plan_job_id INT NOT NULL,
sequence_number INT NOT NULL,
length_mm INT NOT NULL,
width_mm INT NOT NULL,
type TINYINT NOT NULL DEFAULT 0 COMMENT '0=Panel, 1=Scrap, 2=Waste',
quality TINYINT NOT NULL DEFAULT 0 COMMENT '0=OK, 1=Defect, 2=Rework',
created_at DATETIME NOT NULL DEFAULT NOW(),
FOREIGN KEY (cut_plan_job_id) REFERENCES cut_plans(job_id) ON DELETE CASCADE,
INDEX idx_cutplan_job (cut_plan_job_id, sequence_number)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Catalogo componenti monitorati
CREATE TABLE machine_components (
id INT PRIMARY KEY AUTO_INCREMENT,
machine_id VARCHAR(50) NOT NULL,
component_name VARCHAR(100) NOT NULL,
model_file VARCHAR(255) COMMENT 'kuka_joint_rul.onnx',
rul_threshold_warning DECIMAL(10,2) COMMENT 'RUL < this → yellow alert',
rul_threshold_critical DECIMAL(10,2) COMMENT 'RUL < this → red alert',
fallback_hours INT COMMENT 'time-based schedule if ML model unavailable',
unit TINYINT NOT NULL DEFAULT 0 COMMENT '0=Hours, 1=Cycles, 2=Days',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT NOW(),
UNIQUE KEY uq_component (machine_id, component_name),
INDEX idx_components_active (is_active, machine_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Finestre di telemetria aggregate (input per ML)
CREATE TABLE telemetry_windows (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
component_id INT NOT NULL,
window_start DATETIME NOT NULL,
window_end DATETIME NOT NULL,
features JSON NOT NULL COMMENT '{"avg_torque":12.3, "max_temp":87.5, "sum_cycles":150}',
rul_predicted DECIMAL(10,2) COMMENT 'RUL in hours, NULL if no model',
rul_confidence DECIMAL(5,4) COMMENT '0.0000-1.0000',
quality TINYINT NOT NULL DEFAULT 0 COMMENT '0=Raw, 1=Validated, 2=Anomaly',
synced_at DATETIME COMMENT 'NULL if not synced to cloud',
created_at DATETIME NOT NULL DEFAULT NOW(),
FOREIGN KEY (component_id) REFERENCES machine_components(id) ON DELETE CASCADE,
INDEX idx_telemetry_component (component_id, window_start DESC),
INDEX idx_telemetry_sync (synced_at),
INDEX idx_telemetry_quality (quality, window_start)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Predizioni RUL con snapshot features
CREATE TABLE rul_predictions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
component_id INT NOT NULL,
rul_hours DECIMAL(10,2) NOT NULL COMMENT 'predicted remaining useful life',
confidence DECIMAL(5,4) NOT NULL,
model_version TINYINT NOT NULL DEFAULT 1,
features_snapshot JSON COMMENT 'snapshot of features used for this prediction',
predicted_at DATETIME NOT NULL DEFAULT NOW(),
FOREIGN KEY (component_id) REFERENCES machine_components(id) ON DELETE CASCADE,
INDEX idx_rul_component (component_id, predicted_at DESC),
INDEX idx_rul_time (predicted_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Storico guasti reali (training data per ML cloud)
CREATE TABLE component_failures (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
component_id INT NOT NULL,
related_cycle_id BIGINT,
failure_mode VARCHAR(50) NOT NULL COMMENT 'Overload, Wear, Jam, Overheat',
downtime_hours DECIMAL(10,2) NOT NULL DEFAULT 0,
repair_cost DECIMAL(10,2) DEFAULT 0,
root_cause TINYINT NOT NULL DEFAULT 0 COMMENT '0=NormalWear, 1=OperatorError, 2=Design, 3=MaintenanceDelay',
failed_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT NOW(),
FOREIGN KEY (component_id) REFERENCES machine_components(id) ON DELETE CASCADE,
FOREIGN KEY (related_cycle_id) REFERENCES production_cycles(id) ON DELETE SET NULL,
INDEX idx_failures_component (component_id, failed_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Coda di sincronizzazione cloud (offline buffer)
CREATE TABLE cloud_sync_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
table_name VARCHAR(50) NOT NULL,
record_id BIGINT NOT NULL,
operation TINYINT NOT NULL COMMENT '0=Insert, 1=Update, 2=Delete',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0=Pending, 1=Synced, 2=Failed',
retry_count INT NOT NULL DEFAULT 0,
error_message VARCHAR(1023),
payload JSON COMMENT 'full record snapshot for conflict resolution',
created_at DATETIME NOT NULL DEFAULT NOW(),
synced_at DATETIME,
INDEX idx_sync_status (status, created_at),
INDEX idx_sync_table (table_name, record_id),
INDEX idx_sync_retry (status, retry_count)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Oltre agli indici già nelle DDL, aggiungere:
-- Performance query analitiche
ALTER TABLE production_cycles ADD INDEX idx_oee (machine_id, success, started_at);
ALTER TABLE job_items ADD INDEX idx_items_completed (job_id, completed_at);
ALTER TABLE telemetry_warnings ADD INDEX idx_quality_ml (quality, component_id, window_start);
-- Performance sincronizzazione cloud
ALTER TABLE cloud_sync_log ADD INDEX idx_cloud_failures (status, retry_count, created_at);
flowchart LR
subgraph Local["LOCALE (MySQL sul PC Industriale)"]
T1["orders, jobs, job_items
production_cycles, scrap_events
← dati produzione"]
T2["machine_components, telemetry_windows
rul_predictions, component_failures
← dati manutenzione"]
CQ["cloud_sync_log
coda: INSERT/UPDATE/DELETE
con retry e conflitto"]
end
subgraph Sync["SINCRONIZZAZIONE"]
SC["Sistec.Stack.Cloud
BackgroundService
legge coda ogni N minuti"]
BR["Broker MQTT
Sparkplug B
(realtime eventi)"]
HTTP["HTTP REST
batch upload
(dati analitici)"]
end
subgraph Cloud["CLOUD (Azure / AWS)"]
DB_C["Database cloud
PostgreSQL / TimescaleDB
(dati aggregati, storico)"]
ML["ML Pipeline
ri-addestramento modelli
con nuovi failure data"]
DASH["Dashboard centrale
OEE, trend, confronto
multi-impianto"]
end
T1 -->|INSERT| CQ
T2 -->|INSERT| CQ
CQ -->|ogni 5 min| SC
SC -->|batch HTTP| HTTP
SC -->|eventi realtime| BR
HTTP -->|INSERT| DB_C
BR -->|Sparkplug| DB_C
DB_C -->|features storiche| ML
ML -->|modelli .onnx aggiornati| T2
DB_C --> DASH
| Strategia | Dettaglio |
|---|---|
| Batch periodico | cloud_sync_log processato ogni 5 min. Record con status=Pending → upload HTTP → status=Synced |
| Realtime eventi | Eventi critici (job completato, allarme, failure) pubblicati subito via MQTT Sparkplug B |
| Conflitto | payload in cloud_sync_log salva snapshot completo. Conflict resolution: updated_at più recente vince |
| Retry | Fino a 10 tentativi. Dopo 10 → status=Failed. Alert operativo su cloud_sync non funzionante |
| Offline | Se cloud non raggiungibile, i record restano Pending. Appena torna online, la coda si svuota |
| Modelli ML | Cloud ri-addestra modelli ONNX con nuovi failure data. Download automatico via CloudModelUpdater |
| Query | Oggi (impossibile o lento) | Domani (indici + dati normalizzati) |
|---|---|---|
| Pezzi/h per operatore oggi | production_log grezzo, full scan |
production_cycles + idx_cycles_machine + jobs.operator_id |
| OEE macchina ultimo mese | Impossibile (nessuna tabella cicli) | production_cycles con idx_oee(machine_id, success, started_at) |
| Trend RUL per componente | persistent_vars.Blob (illegibile) |
telemetry_windows + idx_telemetry_component |
| Confronto multi-impianto | Impossibile (DB isolati) | cloud_sync_log → DB cloud centralizzato |
| Tempo ciclo medio programma X | cut_plan.Data blob (illegibile) |
production_cycles + job_items.program_used |
| Failure rate per macchina | Assente | component_failures + idx_failures_component |
| Tabella Legacy | Nuovo Schema | Note |
|---|---|---|
accounts |
employees + roles |
BCrypt, badge RFID, permessi granulari |
orders |
orders (migliorato) |
FK a recipes, timestamp, indici |
jobs |
jobs (migliorato) |
FK a orders, recipes, employees |
recipes |
recipes (invariato) |
Già ok |
cut_plan |
cut_plans + cut_plan_items |
Addio longblob, dati normalizzati |
alarm_journal |
Invariato | Già ok con FK a employees per ack |
persistent_vars |
telemetry_windows + machine_components |
Blob → JSON strutturato con unità |
maintenance_history |
maintenance_history (migliorato) |
FK a machine_components, employees |
| (nuova) | production_cycles |
Base per OEE, MES, analytics |
| (nuova) | job_items |
Tracciamento singolo pezzo |
| (nuova) | scrap_events |
Causa, peso, operatore, QR code |
| (nuova) | piece_tracking |
Genealogia pezzo (prodotto → QC → spedito) |
| (nuova) | machine_components |
Catalogo componenti monitorati |
| (nuova) | telemetry_windows |
Features aggregate per ML |
| (nuova) | rul_predictions |
Cache ultime predizioni RUL |
| (nuova) | component_failures |
Training data per ML cloud |
| (nuova) | cloud_sync_log |
Offline buffer per sync cloud |
Sforzo complessivo: 3-5 giorni
├── Giorno 1: Schema DDL + migration
├── Giorno 2: Adattamento repository esistenti (Order, Job, Recipe)
├── Giorno 3: Nuovi repository (JobItems, ProductionCycles, ScrapEvents)
├── Giorno 4: MachineComponents + TelemetryWindows + RulPredictions
└── Giorno 5: CloudSyncLog + sync strateg
Integrazione con stack esistenti:
├── Sistec.Stack.JobManagement → usa orders, jobs, job_items
├── Sistec.Stack.Production → usa production_cycles, scrap_events
├── Sistec.Stack.Maintenance → usa telemetry_windows, rul_predictions, component_failures
└── Sistec.Stack.Cloud → consuma cloud_sync_log
L'architettura a stack verticali e le scelte tecnologiche (Avalonia, DI, NuGet) risolvono i problemi di frammentazione del codice, ma non garantiscono compliance agli standard industriali che i clienti si aspettano e che i competitor (Siemens WinCC, Rockwell FactoryTalk, Ignition) già supportano nativamente.
Questa sezione formalizza l'adozione degli standard mancanti, trasformandoli da menzioni implicite a vincoli architetturali.
ISA-101.01-2015 (ora IEC 63303) è lo standard internazionale per il design delle interfacce uomo-macchina nei sistemi di automazione di processo. Definisce un ciclo di vita completo per l'HMI: Philosophy → Style Guide → Design → Implementation → Operation → Maintenance.
Il documento menziona ISA-101 solo in §21 (HCI) e §23 (Icone) come riferimento culturale, non come vincolo architetturale. Nessuna delle codebase legacy lo implementa. Nessun competitor lo ignora.
| Elemento ISA-101 | Dove nell'Architettura Greenfield | Stato |
|---|---|---|
| HMI Philosophy Document | docs/hmi-philosophy.md nel repo fondazione — dichiarazione formale di principi, palette, gerarchia |
📝 Da creare |
| Style Guide | Sistec.Theme package — colori, tipografia, spaziatura, comportamenti controlli |
✅ Parziale (preventivato in Fase 1) |
| Symbol Library | Sistec.Icons package — SVG vettoriali, mapping semantico |
✅ In Fase 1 |
| 4-Level Display Hierarchy | layout.json con vincolo: max 4 livelli di navigazione |
✅ Già definito in §8 |
| Color Philosophy | Palette muta ISA-101: sfondi grigi, colori solo per anomalie | ✅ In §21.4 |
| Alarm Prioritization | Integrazione con ISA-18.2 (§24.3) | 📝 Da allineare |
| Lifecycle Management | Fasi Roadmap: Filosofia → Design → Implementazione → Operatività → Manutenzione | ✅ Già nella Roadmap |
# HMI Philosophy — Sistec.<Commessa>
## Scopo
Fornire all'operatore la consapevolezza situazionale necessaria per
mantenere un controllo sicuro ed efficiente dell'impianto.
## Principi
1. Le situazioni anomale devono essere immediatamente evidenti
2. La navigazione deve essere intuitiva e consistente
3. Le informazioni critiche devono essere sempre visibili
4. Il carico cognitivo deve essere minimizzato
## Palette Colori
| Elemento | Colore | Uso |
|-------------------|-----------|----------------------------------|
| Sfondo | #F0F0F0 | Overview (Livello 1) |
| Sfondo | #D3D3D3 | Control (Livello 2) |
| Sfondo | #C0C0C0 | Detail (Livello 3) |
| Attrezzatura | #808080 | Stato normale |
| Valori | #000000 | Testo attivo |
| Allarme critico | #FF0000 | Rosso saturo, lampeggiante |
| Allarme alto | #FF8C00 | Arancione |
| Allarme medio | #FFD700 | Giallo |
| Allarme basso | #00BFFF | Ciano |
## Gerarchia Schermate
- **Livello 1 — Overview**: Impianto completo, KPI, allarmi attivi
- **Livello 2 — Control**: Unità di processo, comandi, trend
- **Livello 3 — Detail**: Diagnostica equipaggiamento, parametri
- **Livello 4 — Support**: Manutenzione, configurazione, log
Impatto: Un file hmi-philosophy.md obbligatorio in ogni repository di commessa. Viene compilato all'inizio della Fase 2 (dopo aver stabilito i principi del progetto).
IEC 62443 è lo standard internazionale per la cybersecurity dei sistemi di controllo industriale. Con l'entrata in vigore della direttiva NIS2 in Europa (2024), la compliance IEC 62443 diventa un requisito di legge per molti impianti industriali, non più una opzione.
| Aspetto | Stato Attuale | Rischio |
|---|---|---|
| Autenticazione | Password in chiaro, backdoor loginSistec.ls |
🔴 Critico |
| Autorizzazione | 4 livelli fissi (NoUser → Sistec), nessun permesso granulare | 🔴 Critico |
| Crittografia | Nessuna (OPC UA ha sicurezza ma non è configurata) | 🟠 Alto |
| Audit Log | Assente | 🟠 Alto |
| Secure Boot | Non considerato | 🟡 Medio |
| Firmware Signing | Non considerato | 🟡 Medio |
| Network Segmentation | Manuale (firewall rules in Ansible) | 🟢 Basso |
| Patch Management | UpdateAgent con rollback | 🟢 Basso |
IEC 62443 definisce 4 Security Levels. Per un HMI industriale Sistec:
| SL | Requisito | Implementazione |
|---|---|---|
| SL-1 | Protezione contro violazioni accidentali | ✅ Già coperto (autenticazione, autorizzazione base) |
| SL-2 | Protezione contro violazioni intenzionali con mezzi semplici | 📝 Target: BCrypt, TLS 1.3, audit trail, role-based access |
| SL-3 | Protezione contro violazioni intenzionali con mezzi sofisticati | 📝 Futuro: secure boot, firmware signing, HSM |
| SL-4 | Protezione contro violazioni intenzionali con mezzi estesi | ❌ Non necessario per HMI su pannello isolato |
# appsettings.json — sezione security
{
"Security": {
"Iec62443Level": "SL2",
"Authentication": {
"MinPasswordLength": 8,
"AccountLockout": { "MaxAttempts": 5, "LockoutMinutes": 15 },
"PasswordHistory": 5,
"SessionTimeout": 30
},
"Tls": {
"MinimumVersion": "1.3",
"CertificatePath": "Certs/sistec-hmi.pfx"
},
"Audit": {
"Enabled": true,
"Events": ["Login", "Logout", "ParamChange", "JobStart", "JobStop", "AlarmAck", "ConfigChange"],
"RetentionDays": 365
},
"Network": {
"ApiBindAddress": "127.0.0.1",
"ApiPort": 5000,
"AllowRemoteAccess": false
}
}
}
Schema già menzionato in §19.12, qui reso vincolo architetturale:
┌──────────────────────────────────────────────────────┐
│ PC Industriale │
│ │
│ eth0 ─── Rete Macchine ─── PLC · KUKA · Safan │
│ │ (192.168.0.0/24, isolata) │
│ │ Nessuna API HTTP su eth0 │
│ │ Solo OPC UA client, TCP client │
│ │
│ eth1 ─── Rete Aziendale ─── MES · Dashboard · IT │
│ (10.0.0.0/24, firewall) │
│ REST API su localhost (eth1) │
│ UpdateAgent verso update server │
└──────────────────────────────────────────────────────┘
Vincolo: Ogni PC industriale deve avere due interfacce di rete fisiche (o VLAN taggate). La rete macchine non deve mai essere raggiungibile dalla rete aziendale.
Fase 1 (aggiornata) — Fondazioni
├── Sistec.Infra.Authentication (BCrypt, session, audit)
├── Sistec.Infra.Audit (audit trail strutturato)
├── appsettings.json template con sezione Security
└── Vincolo architetturale: due schede di rete
Fase 3 (aggiornata) — Applicativi
├── Audit trail integrato in ogni stack (IStateAuditService)
├── API binding: solo localhost, solo eth1
└── TLS 1.3 per comunicazione UpdateAgent
Fase 5 (aggiornata) — Installer
├── Certificato TLS embedded nell'installer
└── Signature verification per aggiornamenti
ISA-18.2 (Management of Alarm Systems for the Process Industries) definisce il ciclo di vita completo della gestione allarmi: identificazione, razionalizzazione, progettazione, implementazione, operatività, manutenzione, monitoraggio e modifiche.
Sistec.Stack.Alarms è definito nell'architettura ma senza alcun riferimento a ISA-18.2. Non esiste:
| ISA-18.2 Elemento | Implementazione in Sistec.Stack.Alarms |
|---|---|
| Alarm Philosophy | Documento che definisce: cosa è un allarme, criteri di razionalizzazione, priorità, tempi di risposta attesi |
| Prioritization | 4 livelli: Emergency (🔴), High (🟠), Medium (🟡), Low (🔵) — mapping ISA-18.2 |
| Alarm Shelving | Soppressione temporanea con timeout (max 8 ore), audit trail |
| Alarm Flooding | Rilevamento automatico (>10 allarmi/minuto), visualizzazione aggregata |
| Alarm Analysis | Report periodici: top 10 allarmi per frequenza, alarm rate medio, operator response time |
| KPIs | Alarm Rate ≤ 5/hr (target ISA-18.2: ≤ 10/hr), Standing Alarms ≤ 15, Flood duration ≤ 10 min |
public enum AlarmPriority
{
Emergency = 1, // 🔴 Rosso saturo, lampeggiante — azione immediata richiesta
High = 2, // 🟠 Arancione — azione entro 5 minuti
Medium = 3, // 🟡 Giallo — azione entro 30 minuti
Low = 4 // 🔵 Ciano — informativo, nessuna azione obbligatoria
}
public enum AlarmState
{
Normal, // Condizione normale
Annunciated, // Allarme attivo, operatore non ha ancora risposto
Acknowledged, // Operatore ha preso visione
Shelved, // Soppresso temporaneamente (con timer)
OutOfService // Disabilitato per manutenzione
}
public record AlarmEvent(
string Id,
string TagName,
string Description,
AlarmPriority Priority,
AlarmState State,
DateTime RaisedAt,
DateTime? AcknowledgedAt,
DateTime? ClearedAt,
int? AckOperatorId,
int ShelveMinutesRemaining
);
ISA-18.2 e ISA-101 interagiscono: la visualizzazione allarmi nell'HMI deve seguire le regole di colore, forma e posizione definite dall'ISA-101 color philosophy. L'implementazione è già prevista in Sistec.Theme e layout.json — va solo allineata ai vincoli ISA-18.2:
// layout.json — alarm banner ISA-18.2 compliant
{
"id": "alarm-banner",
"control": "Alarms.Views.AlarmBanner",
"binding": {
"maxVisible": 5,
"showShelved": false,
"priorityFilter": ["Emergency", "High"],
"flashOnNew": true
}
}
| KPI | Formula | Target ISA-18.2 | Dove |
|---|---|---|---|
| Alarm Rate | Allarmi/h in stato annunciated | ≤ 10/hr (target: ≤ 5/hr) | Dashboard produzione |
| Annunciated Alarms | Allarmi attivi non riconosciuti | ≤ 15 | Home HMI |
| Standing Alarms | Allarmi attivi da > 24 ore | ≤ 10 | Report settimanale |
| Operator Response Time | Tempo medio tra RaisedAt e AcknowledgedAt | < 30 secondi per Emergency | Dashboard manutenzione |
| Flood Duration | Tempo con > 10 allarmi/minuto | < 10 minuti | Report mensile |
Con Redis opzionale, Sistec.Stack.Alarms usa Redis come hot state layer:
| Funzione | Redis Key/Pattern | Dettaglio |
|---|---|---|
| Stato allarme condiviso | sistec:{plant}:alarms:{alarmId} |
Hash con campi state, priority, raisedAt, ackOperatorId |
| Shelving timer | TTL su chiave allarme in shelved | Allarme riattivato automaticamente alla scadenza del TTL |
| Alarm Rate (flood detection) | sistec:{plant}:metrics:alarm.rate |
INCR + TTL 60s per sliding window — >10 in 1 min = flood |
| Pub/Sub notifica | channel:alarms:* |
Ogni cambio stato allarme pubblicato → tutti i pannelli aggiornati in tempo reale |
| Riconoscimento cross-pannello | Stato acknowledged in Redis |
Allarme riconosciuto su AB = silenziato anche su C |
MySQL resta audit trail definitivo. Redis tiene solo lo stato corrente per performance e consistenza multi-pannello.
Sistec.Stack.Alarms (esistente, da aggiornare)
├── AlarmModel ISA-18.2 compliant (sostituisce modello attuale)
├── AlarmShelveService (soppressione temporanea, TTL Redis)
├── AlarmFloodDetector (soglia configurabile, sliding window Redis)
├── AlarmAnalyticsService (report + KPIs)
├── IAlarmAudit (tracciamento risposte operatore)
├── RedisAlarmStore (stato condiviso cross-pannello, opzionale)
└── Test: flood simulation, shelve timeout, priority override
Sforzo: 2-3 settimane (da aggiungere alla Fase 3)
OPC UA è già presente nell'architettura come Sistec.Library.OpcUa, ma solo nella modalità Client/Server tradizionale. Le estensioni PubSub (Publish/Subscribe) e FX (Field Exchange) abilitano scenari che il Client/Server tradizionale non copre:
| Estensione | Cosa abilita | Perché serve |
|---|---|---|
| OPC UA PubSub | Comunicazione one-to-many senza connessione diretta | Scalabilità (un sensore pubblica, N subscribing applicazioni ricevono) |
| OPC UA FX | Controller-to-Controller real-time (<15ms) | Sincronizzazione PLC ↔ Robot ↔ Pressa senza hardwiring |
| OPC UA PubSub + MQTT | OPC UA su broker MQTT | Ponte OT/Cloud — dati OPC UA su infrastruttura MQTT standard |
flowchart TB
subgraph Edge["EDGE — Rete Macchine"]
PLC["PLC CODESYS
OPC UA Server"]
KUKA["KUKA KRC
OPC UA Server
(futuro)"]
HMI["Sistec HMI
OPC UA Client
+ MQTT Publisher"]
end
subgraph Brokered["BROKER MQTT"]
MQTT_B["Mosquitto / EMQX
MQTT Broker
(locale o edge)"]
end
subgraph Northbound["NORTHBOUND — Cloud / IT"]
Cloud["Sistec.Stack.Cloud
MQTT Subscriber
+ Forwarder"]
Dashboard["Dashboard
React / Grafana"]
ERP["ERP / MES"]
end
PLC -->|OPC UA Client/Server| HMI
KUKA -->|OPC UA Client/Server| HMI
HMI -->|MQTT Sparkplug B| MQTT_B
MQTT_B -->|MQTT Sparkplug B| Cloud
Cloud --> Dashboard
Cloud --> ERP
style Edge fill:#e8f5e9
style Brokered fill:#fff8e1
style Northbound fill:#e3f2fd
| Componente | Stato Attuale | Target |
|---|---|---|
IUAClient |
Client/Server subscription-based | ✅ Esistente |
MonitoredItem |
Subscription a tag specifici | ✅ Esistente |
| OPC UA PubSub | Non presente | 📝 Reader + Writer (nuovo) |
| OPC UA FX | Non presente | 📝 Valutare se PLC CODESYS lo supporta |
| OPC UA over MQTT | Non presente | 📝 Nuovo trasporto per Sistec.Stack.Cloud |
Sistec.Library.OpcUa (aggiornato)
├── Client/Server (esistente)
├── OPC UA PubSub Reader (nuovo, per ascolto eventi broadcast)
├── OPC UA PubSub Writer (nuovo, per pubblicazione eventi HMI)
└── OPC UA over MQTT (nuovo, per cloud forwarding)
Sforzo: 1-2 settimane di estensione (da schedulare in Fase 1)
L'Unified Namespace (UNS) è il pattern architetturale emergente per l'IIoT, dove tutti i dati di produzione convergono in un unico namespace accessibile da qualunque applicazione, eliminando le integrazioni point-to-point.
OGGI (point-to-point):
PLC ──► HMI
PLC ──► SCADA
HMI ──► MES
KUKA ──► HMI
KUKA ──► SCADA
DOMANI (Unified Namespace via MQTT Sparkplug):
Tutti i device pubblicano a un namespace unico:
┌─────────────────────────────────────┐
│ UNIFIED NAMESPACE │
│ (MQTT Broker + Sparkplug B) │
├─────────────────────────────────────┤
│ sistec/lag-5315/PLC/... │
│ sistec/lag-5315/KUKA/... │
│ sistec/lag-5315/Safan/... │
│ sistec/lag-5315/Production/... │
│ sistec/lag-5315/Alarms/... │
└─────────────────────────────────────┘
▲ ▲
│ │
Pubblicano Si sottoscrivono
HMI, PLC, KUKA Dashboard, MES, ERP
(Sparkplug B Edge (Sparkplug B
Node) Primary Application)
spBv1.0/
├── sistec-lag-5315/
│ ├── PLC/
│ │ ├── DATA/ ← metriche in tempo reale
│ │ ├── BIRTH/ ← stato all'avvio
│ │ └── DEATH/ ← stato quando si disconnette
│ ├── KUKA/
│ │ ├── DATA/
│ │ ├── BIRTH/
│ │ └── DEATH/
│ ├── Safan/
│ │ ├── DATA/
│ │ ├── BIRTH/
│ │ └── DEATH/
│ └── Production/
│ ├── DATA/
│ └── BIRTH/
└── sistec-fael-ab/
└── ...
| Vantaggio | Spiegazione |
|---|---|
| Discovery automatico | Ogni edge node (HMI, PLC) pubblica BIRTH con tutti i suoi tag all'avvio — nessuna configurazione manuale |
| Death detection | Se un edge node si disconnette, il broker pubblica DEATH — tutti i consumer sanno immediatamente che è offline |
| Decoupling | Produttori e consumatori non si conoscono — si agganciano al namespace |
| Scalabilità | Aggiungere un nuovo consumer (dashboard, MES, ERP) = sottoscriversi al topic |
| Standard | Sparkplug B è standard Eclipse Foundation, supportato da Ignition, Weintek, EMQX |
Sistec.Stack.Cloud (aggiornato con Sparkplug B)
├── SparkplugNode — implementa Edge Node (birth/death/data)
├── MqttPublisher — protocollo MQTT con payload Protobuf
├── OfflineBuffer — buffer Sparkplug-compatible (coda sequenziale)
├── UnifiedNamespaceConfig — topic prefix + group ID per commessa
└── Opzionale: Sparkplug Primary Application per dashboard cloud
Fase 1 — Sistec.Library.OpcUa + Sistec.Stack.Cloud
├── OPC UA PubSub Reader (ascolta eventi PLC)
├── Sparkplug B Edge Node (pubblica dati HMI + PLC)
├── MQTT Broker locale opzionale (Mosquitto in container)
└── Topic namespace standardizzato
Fase 3 — Unified Namespace completo
├── Tutti gli stack macchina pubblicano al namespace
├── Dashboard React legge dal namespace (non più da API HMI)
└── MES/ERP si agganciano al namespace invece che al DB diretto
| Standard | Riferimento | Sezione | Priorità | Sforzo |
|---|---|---|---|---|
| ISA-101 / IEC 63303 | HMI Design Standard | §24.1 + §21 | 🔴 Alta (requisito cliente) | 3-5gg (style guide + philosophy) |
| IEC 62443 | OT Cybersecurity | §24.2 + §12 | 🔴 Alta (regolatorio NIS2) | 5-7gg (auth + audit + network) |
| ISA-18.2 | Alarm Management | §24.3 + Alarms.Stack | 🟠 Media | 10-15gg (modello + analytics) |
| IEC 62541 PubSub/FX | OPC UA Extended | §24.4 + OpcUa.Library | 🟡 Media | 5-10gg (estensione libreria) |
| IEC 62541 + MQTT Sparkplug B | IIoT Communication | §24.4 + §13.6 + §24.5 | 🟡 Media | 5-7gg (Cloud.Stack update) |
| Unified Namespace (Sparkplug B) | IIoT Architecture | §24.5 | 🟢 Bassa (differenziante) | 5-7gg (dopo Cloud.Stack) |
| Standard | LAG (oggi) | FAEL (oggi) | Greenfield (target) |
|---|---|---|---|
| ISA-101 / IEC 63303 | ❌ Nulla | ❌ Nulla | ✅ HMI Philosophy + Style Guide + 4 livelli |
| IEC 62443 (SL-2) | ❌ No security | ❌ No security | ✅ BCrypt + TLS 1.3 + audit + 2 NIC |
| ISA-18.2 | ❌ Allarmi basici | ❌ Allarmi basici | ✅ 4 priorità + shelving + KPIs |
| OPC UA PubSub | ❌ Solo Client/Server | ❌ Solo Client/Server | ✅ Reader + Writer |
| OPC UA FX | ❌ | ❌ | ⚠️ Valutare |
| MQTT Sparkplug B | ❌ | ❌ | ✅ Edge Node + UNS |
| IEC 60617 (simboli) | ❌ PNG custom | ❌ PNG custom | ✅ SVG compliance in Sistec.Icons |
| WCAG 2.2 AA | ❌ | ❌ | ✅ contrasto 4.5:1 + target 48px + focus |