Files
Mapo-IOB-WIN/IOB-WIN-SIEMENS/SiemensSingleThread.md
2026-08-13 08:07:18 +02:00

12 KiB

Piano di Implementazione: Single-Thread Communication Proxy (Siemens S7)

1. Obiettivo

Garantire che tutte le interazioni con l'oggetto currPLC (libreria S7.Net / S7netplus, Plc) avvengano esclusivamente su un unico thread proprietario, eliminando i fallimenti di comunicazione e le eccezioni causate dall'accesso concorrente a una libreria non thread-safe, quando il parametro MachWLoopSingleThread è true.

2. Diagnosi del problema (verificata sul codice)

2.1 currPLC viene toccato da ≥3 thread diversi

  • Thread di init/UI: il costruttore Siemens(AdapterFormNext, IobConfTree) chiama setParamPlc() (Siemens.cs:1451) che crea currPLC = new Plc(...) (:1501) e apre la connessione (tryConnectcurrPLC.Open() :1042) sul thread UI (via loadIobType).
  • Thread macchina dedicato WorkerLoopMachine (AdapterForm.cs) → doMachineTask (Generic.cs:723) → readSemafori/process*S7ReadBBcurrPLC.ReadBytes (:469).
  • Thread worker server (Task.Run pool) → doServerTaskAsync (Generic.cs:853) → S7WriteBBcurrPLC.WriteBytes (:573, :636) e tryConnect (:1042).

Il _plcLock (SemaphoreSlim) in AdapterForm serializza solo i tempi di esecuzione tra macchina e server, non l'affinità di thread: chi vince il lock esegue su thread diversi nel tempo. S7.Net Plc non è thread-safe → Open/ReadBytes/WriteBytes/Close concorrenti su thread diversi → errori di connessione / dati corrotti / mai comunicazione.

2.2 Verdetto sull'ipotesi "forzare MachWLoopSingleThread=true"

Necessario ma NON sufficiente. Verificato con grep su tutto il repo: il flag MachWLoopSingleThread (IobDto.cs:101, caricato da IobConfTree.cs:132) non ha alcun consumer nel codice condiviso (Generic.cs/AdapterForm.cs). I _singleFactory / _singleScheduler (SingleThreadTaskScheduler) sono dichiarati in AdapterForm.cs:868/873 ma mai usati; doMachineTask/doServerTaskAsync girano sul thread chiamante. Quindi forzare il flag da solo non cambia il threading. Serve il proxy (che è l'unico consumer del flag, come in FANUC Fanuc.cs:1828). Il flag va comunque forzato a true per attivare il ramo proxy.

2.3 Particolarità Siemens: sottoclassi accedono currPLC direttamente

A differenza di FANUC (tutti gli accessi dentro Fanuc.cs), currPLC è un campo protected Plc acceduto direttamente da 17+ sottoclassi (SiemensAprochim, SiemensTorri, SiemensAt2001, ...) con currPLC.IsConnected, LastErrorCode, LastErrorString, CPU, MaxPDUSize, Rack, Slot, Open, Close, ClearLastError, IsAvailable. C'è anche una classe legacy IobSiemensTorri_legacy.cs con un proprio currPLC.

3. Architettura Tecnica: Pattern Proxy

Componenti (interni alla classe Siemens):

  • _commQueue: BlockingCollection<Action> coda thread-safe.
  • _commThread: thread unico e persistente, avviato durante l'inizializzazione, unico proprietario di currPLC (inclusa la creazione new Plc(...)).
  • ExecuteProxy<T>(Func<T>): marshalling sincrono per il chiamante (tcs.Task.Result), con guardia di rientro (Thread.CurrentThread == _commThread → inline) e fallback anti-hang sotto lock quando il thread è morto (stesso pattern di FANUC).

Strategia di marshalling

Dato l'accesso diretto di currPLC da base + sottoclassi, la soluzione a punto unico è un PlcProxy: un wrapper che espone la stessa superficie usata di S7.Net Plc e marshalla internamente ogni chiamata sul comm-thread. Così nessuna sottoclasse va modificata.

Superficie da replicare nel PlcProxy (verificata dai call-site): ReadBytes(DataType,int,int,int)→byte[], WriteBytes(DataType,int,int,byte[])→ErrorCode, Open(), Close(), IsConnected(bool), IsAvailable(bool), LastErrorCode(ErrorCode), LastErrorString(string), ClearLastError(), CPU(CpuType), MaxPDUSize(int), Rack(int), Slot(int), Dispose().

Gestione ref/out

S7.Net restituisce byte[]/ErrorCode come valori di ritorno (niente ref nei call-site di Siemens), quindi il marshalling è semplice: ExecuteProxy(() => currPLC.Real.ReadBytes(...)). Nessun copy-in/copy-out necessario (a differenza di FANUC).

4. Modifiche ai Componenti

A. Infrastruttura (interna a Siemens.cs)

  1. _commQueue, _commThread, _lockSync, _isProxyActive.
  2. StartCommThread() / StopCommThread() (come FANUC: CompleteAdding + Join(timeout)).
  3. ExecuteProxy<T>(Func<T>) con guardia di rientro e fallback inline sotto lock.

B. PlcProxy (nuova classe interna)

  • Espone la superficie sopra; internamente tiene il Plc reale e marshalla ogni chiamata: ExecuteProxy(() => _real.ReadBytes(...)), ExecuteProxy(() => _real.Open()), ecc.
  • I costruttori / new Plc(...) vengono marshallati sul comm-thread (il Plc nasce sul thread proprietario).
  • IsConnected, IsAvailable, LastErrorCode/LastErrorString, CPU... → proprietà marshalled (ExecuteProxy(() => _real.IsConnected)).

C. Lifecycle del thread

  • Costruttore/setParamPlc: StartCommThread() PRIMA di new Plc(...); la creazione + assegnazione currPLC passano dal proxy.
  • tryConnect/tryDisconnect: Open/Close marshallati (già coperti dal PlcProxy).
  • startAdapter override: se _commThread non vivo → riavvio + ricreazione Plc sul nuovo thread (path stop→start), senza duplicare new Plc sull'avvio normale.
  • stopAdapter/Dispose: fermare il thread; Dispose() marshalla currPLC.Dispose().
  • MachWLoopSingleThread forzato a true nel costruttore (per attivare il ramo proxy).

D. Classe legacy IobSiemensTorri_legacy.cs

Ha un proprio currPLC con lo stesso problema. Se non è più usata in produzione: documentare e lasciare. Se è usata: applicare lo stesso PlcProxy.

5. Validazione e Test

  • Log: tutte le chiamate a currPLC devono presentare lo stesso ManagedThreadId del comm-thread.
  • Test di connessione: tryConnect (Open) e read/write devono funzionare attraverso il proxy.
  • Test stop→start e restart: _commThread ricreato con ricreazione Plc, senza double-create.

6. Note di sicurezza (pitfall)

  • Il flag da solo non basta: senza proxy, MachWLoopSingleThread=true non cambia il threading.
  • Reentrancy: guardia Thread.CurrentThread == _commThread in ExecuteProxy.
  • Hang: fallback inline sotto lock se il thread è morto.
  • Shutdown: StopCommThread() su stopAdapter/Dispose; niente CompleteAdding durante il run.
  • Sottoclassi: con PlcProxy nessuna modifica necessaria; il campo cambia tipo (protected Plc currPLCprotected PlcProxy currPLC).

7. Stato implementazione (aggiornato a build Release passata)

Implementato in IOB-WIN-SIEMENS\IobSiemens\Siemens.cs

  1. Infrastruttura proxy: _commQueue, _commThread, _lockSync, _isProxyActive + StartCommThread() / StopCommThread() / ExecuteProxy<T>() (guardia di rientro + fallback inline anti-hang).
  2. PlcProxy: classe annidata che marshalla ogni chiamata sul comm-thread; il Plc reale nasce sul comm-thread (ExecuteProxy(() => new Plc(...))). Copre tutta la superficie usata da base + sottoclassi: ReadBytes, WriteBytes, Open, Close, ClearLastError, IsConnected, IsAvailable, LastErrorCode, LastErrorString, CPU, MaxPDUSize, Rack, Slot, Dispose.
  3. Campo: protected Plc currPLCprotected PlcProxy currPLC.
  4. Costruttore: MachWLoopSingleThread = true + StartCommThread() prima di setParamPlc().
  5. setParamPlc: currPLC = new PlcProxy(this, ...) (creazione marshalled).
  6. Lifecycle: override startAdapter (riavvia thread + ricrea PlcProxy se non vivo) e stopAdapter (base + StopCommThread()); Dispose() marshalla currPLC.Dispose() + StopCommThread().

Deviazioni riscontrate durante l'implementazione

  • CS0052 ("tipo di campo meno accessibile del campo"): PlcProxy deve essere protected sealed class (coerente col campo protected), non internal.
  • Tipi esatti S7.Net (verificati con reflection sulla S7.Net.dll): CPU è CpuType, Rack/Slot/MaxPDUSize sono short (Int16), Open() ritorna ErrorCode (nel wrapper Open() è void, il ritorno viene scartato), LastErrorCode è ErrorCode, IsConnected/IsAvailable sono bool.

Verifica

  • Build Release IOB-WIN-SIEMENS: exit 0, 0 errori CS (base + tutte le sottoclassi che accedono a currPLC.IsConnected ecc. compilano contro PlcProxy).
  • Non è stato modificato nessun altro file (sottoclassi, config, csproj).

8. Fix successivi scoperti durante la verifica runtime (memMap nullo + affinità)

8.1 Bug di affinità introdotto inizialmente (corretto)

setParamPlc() è chiamato due volte: nel costruttore Generic (Generic.cs:75) e nel costruttore Siemens. La prima chiamata crea currPLC = new PlcProxy(...) prima che StartCommThread() parta (avviato nel corpo del costruttore Siemens, che gira DOPO base(...)), quindi ExecuteProxy usava il fallback inline → il Plc reale nasceva sul thread sbagliato, e la seconda setParamPlc (con needRefresh=false) NON ricreava currPLC → affinità S7 rotta.

Fix: MachWLoopSingleThread=true + StartCommThread() spostati all'inizio dell'override setParamPlc (Siemens.cs:1636), così la prima chiamata (dal costruttore Generic) avvia il comm-thread e crea currPLC marshalled sul thread corretto; la seconda è un no-op (thread già vivo).

8.2 memMap nullo all'avvio (fixato)

Per molte config Siemens IOBConfFull.Memory è null (nessuna memory map: mMapRead/mMapWrite non configurati). memMap è un campo public plcMemMapExt con default null (BaseObj.cs:169). loadMemConf() lo setta, ma getDynData (Siemens.cs:181) legge memMap.mMapRead.Count senza guardia → NRE all'avvio se memMap resta null.

Fix:

  • Nel costruttore Siemens, dopo setParamPlc(): se memMap == nullmemMap = new plcMemMapExt() (mappa vuota, non serve per molte config Siemens) + lgWarn.
  • Guardie difensive nelle letture: getDynData (memMap != null && mMapRead != null) e plcWriteParams (memMap != null && mMapWrite != null).

Verifica finale

  • Build Release IOB-WIN-SIEMENS: exit 0, 0 errori CS.
  • Il costruttore ora garantisce memMap non-null e currPLC creato sul comm-thread.

9. Fix della doppia costruzione + mancato auto-avvio (AdapterForm.cs, classe condivisa)

9.1 Problema

AdapterForm_Load (AdapterForm.cs:1186) chiamava await loadIobType() incondizionatamente. Con autoLoadConf=true, loadIniFile creava già iobObj (Siemens #1) e poi AdapterForm_Load ne creava un secondo (Siemens #2) sostituendo iobObj:

  • il costruttore Siemens girava 2 volte;
  • l'AvviaAdapter automatico (autoStartOnLoad=true in App.config) avviava il 1° oggetto orfanizzato, mentre l'oggetto attivo (2°) restava non avviato → nessuna comunicazione PLC, e l'utente doveva premere start_Click.
  • Inoltre la connessione del 1° Siemens restava aperta (leak): il Open() del 2° poteva fallire (connessioni PLC limitate) → connectionOk=false → nessuna comunicazione PLC.

9.2 Fix

In AdapterForm_Load, loadIobType() viene chiamato solo se iobObj è ancora null:

if (iobObj == null)
{
    await loadIobType();
    displayTaskAndLog("Waiting for config file selection");
}

Con autoLoadConf=true (iobObj già creato da loadIniFile) non si ricrea → singola costruzione, e l'AvviaAdapter automatico agisce sull'oggetto attivo. Il fix è nella classe condivisa e vale per tutti gli adapter (FANUC, SIEMENS, ecc.).

9.3 Verifica

  • Build Release IOB-WIN-FORM e IOB-WIN-SIEMENS: exit 0, 0 errori CS.
  • BaseObj.cs (init memMap) è la tua modifica, lasciata invariata.