diff --git a/IOB-UT-NEXT/Iob/BaseObj.cs b/IOB-UT-NEXT/Iob/BaseObj.cs index 6eba73dc..5979c008 100644 --- a/IOB-UT-NEXT/Iob/BaseObj.cs +++ b/IOB-UT-NEXT/Iob/BaseObj.cs @@ -166,7 +166,7 @@ namespace IOB_UT_NEXT.Iob /// /// Struttura memoria PLC x lettura/scrittura da JSON file /// - public plcMemMapExt memMap; + public plcMemMapExt memMap = new plcMemMapExt(); /// /// Minimo numero di px da inviare in blocco diff --git a/IOB-WIN-FORM/AdapterForm.cs b/IOB-WIN-FORM/AdapterForm.cs index ae041ba8..96d3e14e 100644 --- a/IOB-WIN-FORM/AdapterForm.cs +++ b/IOB-WIN-FORM/AdapterForm.cs @@ -1183,8 +1183,14 @@ namespace IOB_WIN_FORM { IOBConfFull = new IobConfTree(); } - await loadIobType(); - displayTaskAndLog("Waiting for config file selection"); + // se loadIniFile/loadYamlFile (autoLoadConf=true) hanno già creato iobObj, NON ricrearlo: + // evita la doppia costruzione dell'adapter (e che l'AvviaAdapter auto agisca sul 1° oggetto + // orfanizzato invece che su quello attivo) + if (iobObj == null) + { + await loadIobType(); + displayTaskAndLog("Waiting for config file selection"); + } // ricarica stats da redis string callKey = iobObj.redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CallStats"); diff --git a/IOB-WIN-FORM/Iob/Generic.cs b/IOB-WIN-FORM/Iob/Generic.cs index f524307b..f5e4a52e 100644 --- a/IOB-WIN-FORM/Iob/Generic.cs +++ b/IOB-WIN-FORM/Iob/Generic.cs @@ -1856,26 +1856,6 @@ namespace IOB_WIN_FORM.Iob return fatto; } - -#if false - /// - /// Effettua un trim della stringa al numero max di linee da mostrare a video - /// - /// - /// - public string limitLine2show(string newString) - { - // se num righe superiore a limite trimmo... - if (newString.Split('\n').Length > parentForm.nLine2show) - { - //int idx = newString.LastIndexOf('\r'); - int idx = newString.LastIndexOf(Environment.NewLine); - newString = newString.Substring(0, idx); - } - return newString; - } -#endif - /// /// riporta il log di tutti i dati di results temporali registrati /// diff --git a/IOB-WIN-SIEMENS/DATA/CONF/3023-PING.ini b/IOB-WIN-SIEMENS/DATA/CONF/3023-PING.ini deleted file mode 100644 index ec3d2e6d..00000000 --- a/IOB-WIN-SIEMENS/DATA/CONF/3023-PING.ini +++ /dev/null @@ -1,63 +0,0 @@ -;Configurazione IOB-WIN -[IOB] -CNCTYPE=PingWatchdog -PING_MS_TIMEOUT=500 -IOB_NAME=3023 - -[MACHINE] -VENDOR=STEAMWARE -MODEL=WATCHDOG - -[CNC] -;IP=192.168.1.151 -IP=192.168.1.8 -PORT=0000 - -[SERVER] -MPIP=http://192.168.1.7 -MPURL=/MP/RIOC/api -APICLASS=IOB -CMDBASE=/IOB/input/ -CMDFLOG=/IOB/flog/ -CMDULOG=/IOB/ulog/ -CMDALIVE=/IOB -CMDENABLED=/IOB/enabled/ -CMDADV1=?valore= -CMDREBO=/sendReboot.aspx?idxMacchina= - -[MEMORY] - -[BLINK] -MAX_COUNTER_BLINK = 15 -BLINK_FILT=0 - -[OPTPAR] -MAX_ERR_CHECK=10 -timerIntMs=300 -VETO_QUEUE_IN=15 -AUTO_CHANGE_ODL=false -POWEROFF_TIMEOUT_SEC=60 -DISABLE_PZCOUNT=TRUE -;; gestioni PING -;MAX_TRY_PING=3 -;VETO_QUEUE_IN=4 -;VETO_PING_SEC=4 -;VETO_CHECKDIR_SEC=10 -;MAX_ELAPSED_TIME_SEC=60 -;VETO_SEND_SNAPSHOT=3 -VETO_SIG_IN=false -B_IN_OFF=0 -B_IN_ON=129 -VETO_PING_SEC=6 -VETO_SCADUTO=TRUE - -[BRANCH] -NAME=master - -; Tags manuali -[TAGS] -Customer=JETCO -HostOS=WIN -HostName=IOB-WIN-09 -HostAddr=192.168.1.133 - diff --git a/IOB-WIN-SIEMENS/DATA/CONF/MAIN.ini b/IOB-WIN-SIEMENS/DATA/CONF/MAIN.ini index babafa34..6d2fdde2 100644 --- a/IOB-WIN-SIEMENS/DATA/CONF/MAIN.ini +++ b/IOB-WIN-SIEMENS/DATA/CONF/MAIN.ini @@ -39,7 +39,6 @@ CLI_INST=SteamWareSim ;STARTLIST=PIZ03 ;STARTLIST=3020 ;STARTLIST=SIMUL_01 -STARTLIST=3010 - -MAXCNC=10 +STARTLIST=3017 +MAXCNC=10 \ No newline at end of file diff --git a/IOB-WIN-SIEMENS/IOB-WIN-SIEMENS.csproj b/IOB-WIN-SIEMENS/IOB-WIN-SIEMENS.csproj index 73cd5e90..497cc6db 100644 --- a/IOB-WIN-SIEMENS/IOB-WIN-SIEMENS.csproj +++ b/IOB-WIN-SIEMENS/IOB-WIN-SIEMENS.csproj @@ -195,10 +195,18 @@ Always - - - - + + Always + + + Always + + + Always + + + Always + Always @@ -217,47 +225,25 @@ - - Always - - - Always - + + - PreserveNewest - - - Always - - - Always - - - Always - - - Always - - - Always - - Always + + + + + + - - Always - - - Always - - - Always - + + + diff --git a/IOB-WIN-SIEMENS/IobSiemens/Siemens.cs b/IOB-WIN-SIEMENS/IobSiemens/Siemens.cs index ec89d272..c4c1c5f0 100644 --- a/IOB-WIN-SIEMENS/IobSiemens/Siemens.cs +++ b/IOB-WIN-SIEMENS/IobSiemens/Siemens.cs @@ -7,12 +7,14 @@ using MapoSDK; using Newtonsoft.Json; using S7.Net; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.NetworkInformation; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace IOB_WIN_SIEMENS.IobSiemens { @@ -127,6 +129,7 @@ namespace IOB_WIN_SIEMENS.IobSiemens public void Dispose() { currPLC.Dispose(); + StopCommThread(); } /// @@ -170,7 +173,7 @@ namespace IOB_WIN_SIEMENS.IobSiemens try { // processo x ogni valore configurato... - if (memMap.mMapRead.Count > 0) + if (memMap != null && memMap.mMapRead != null && memMap.mMapRead.Count > 0) { // inizializzo i valori bool valBool = false; @@ -233,7 +236,7 @@ namespace IOB_WIN_SIEMENS.IobSiemens } else { - lgInfo($"getDynData: {memMap.mMapRead.Count} record in mMapRead"); + lgInfo($"getDynData: {(memMap != null && memMap.mMapRead != null ? memMap.mMapRead.Count : 0)} record in mMapRead"); } } catch (Exception exc) @@ -1111,6 +1114,31 @@ namespace IOB_WIN_SIEMENS.IobSiemens } } + /// + /// Override startAdapter: se il thread dedicato non è vivo (es. dopo uno stop) lo riavvia + /// e ricrea il Plc sul nuovo thread (affinità S7: creatore == comm-thread). + /// Sul path di avvio normale (subito dopo il costruttore) il thread è già vivo e non fa nulla. + /// + public override void startAdapter(bool resetQueue) + { + if (_commThread == null || !_commThread.IsAlive) + { + StartCommThread(); + currPLC = new PlcProxy(this, parametri.tipoCpu, parametri.ipAdrr, parametri.rack, parametri.slot); + } + base.startAdapter(resetQueue); + } + + /// + /// Override stopAdapter: ferma l'adapter e poi ferma il thread dedicato + /// (nessun leak sul restart che ricrea l'oggetto). + /// + public override async Task stopAdapter(bool tryRestart, bool forceDequeue) + { + await base.stopAdapter(tryRestart, forceDequeue); + StopCommThread(); + } + /// /// Converte direttamente un valore UInt16 su un oggetto byte[2] /// @@ -1138,9 +1166,9 @@ namespace IOB_WIN_SIEMENS.IobSiemens #region Protected Fields /// - /// Oggetto PLC da ri-utilizzare... + /// Oggetto PLC da ri-utilizzare... (PlcProxy: marshalling sul comm-thread) /// - protected Plc currPLC; + protected PlcProxy currPLC; /// /// Esito ultimo ping @@ -1162,6 +1190,12 @@ namespace IOB_WIN_SIEMENS.IobSiemens /// protected bool writePre = true; + // ---- Infrastruttura Single-Thread Proxy (S7.Net non thread-safe) ---- + private BlockingCollection _commQueue = new BlockingCollection(); + private Thread _commThread; + private readonly object _lockSync = new object(); + private bool _isProxyActive = false; + #endregion Protected Fields #region Protected Properties @@ -1176,6 +1210,152 @@ namespace IOB_WIN_SIEMENS.IobSiemens #region Protected Methods + #region Single-Thread Proxy Infrastructure (S7.Net non thread-safe) + + /// + /// Avvia (o riavvia) il thread dedicato, unico proprietario di currPLC (PlcProxy). + /// Se il thread è già vivo non fa nulla; altrimenti crea una coda nuova e lo avvia. + /// + private void StartCommThread() + { + if (_commThread != null && _commThread.IsAlive) + return; + + _commQueue = new BlockingCollection(); + _commThread = new Thread(() => + { + while (true) + { + Action a; + try + { + a = _commQueue.Take(); + } + catch (InvalidOperationException) + { + // coda completata (CompleteAdding) --> esci + break; + } + catch (Exception) + { + break; + } + try + { + a(); + } + catch (Exception ex) + { + lgError($"[CommThread {Thread.CurrentThread.ManagedThreadId}] azione fallita: {ex}"); + } + } + }) + { + IsBackground = true, + Name = "S7-Comm-Thread", + Priority = ThreadPriority.AboveNormal + }; + _commThread.Start(); + lgInfo($"[Thread: {_commThread.ManagedThreadId}] CommThread avviato (unico owner di currPLC)"); + } + + /// + /// Ferma il thread dedicato: completa la coda e attende (con timeout) la fine delle azioni in corso. + /// + private void StopCommThread() + { + if (_commThread == null) + return; + + lgInfo($"[Thread: {Thread.CurrentThread.ManagedThreadId}] StopCommThread"); + _commQueue.CompleteAdding(); + _commThread.Join(3000); + _commThread = null; + } + + /// + /// Esegue un'azione sul comm-thread (marshalling sincrono per il chiamante). + /// Se il thread non è vivo (o flag off) esegue inline sotto lock (fallback anti-hang). + /// + private T ExecuteProxy(Func action) + { + if (IOBConfFull.General.MachWLoopSingleThread && _commThread != null && _commThread.IsAlive) + { + // se siamo già sul thread dedicato esegui inline (nessun rientro/deadlock) + if (Thread.CurrentThread == _commThread) + { + return action(); + } + + var tcs = new TaskCompletionSource(); + _commQueue.Add(() => + { + try + { + tcs.SetResult(action()); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + return tcs.Task.Result; + } + else + { + lock (_lockSync) + { + return action(); + } + } + } + + /// + /// Wrapper thread-safe su S7.Net Plc: marshalla ogni chiamata sul comm-thread (unico owner). + /// Il Plc REALE nasce sul comm-thread (affinità S7). Nessuna sottoclasse va modificata. + /// + protected sealed class PlcProxy + { + private readonly Siemens _owner; + private readonly Plc _real; + + public PlcProxy(Siemens owner, CpuType cpu, string ip, short rack, short slot) + { + _owner = owner; + // il Plc REALE nasce sul comm-thread (affinità S7) + _real = owner.ExecuteProxy(() => new Plc(cpu, ip, rack, slot)); + } + + public byte[] ReadBytes(DataType dataType, int db, int startByte, int length) + => _owner.ExecuteProxy(() => _real.ReadBytes(dataType, db, startByte, length)); + + public ErrorCode WriteBytes(DataType dataType, int db, int startByte, byte[] value) + => _owner.ExecuteProxy(() => _real.WriteBytes(dataType, db, startByte, value)); + + public void Open() + => _owner.ExecuteProxy(() => { _real.Open(); return true; }); + + public void Close() + => _owner.ExecuteProxy(() => { _real.Close(); return true; }); + + public void ClearLastError() + => _owner.ExecuteProxy(() => { _real.ClearLastError(); return true; }); + + public bool IsConnected => _owner.ExecuteProxy(() => _real.IsConnected); + public bool IsAvailable => _owner.ExecuteProxy(() => _real.IsAvailable); + public ErrorCode LastErrorCode => _owner.ExecuteProxy(() => _real.LastErrorCode); + public string LastErrorString => _owner.ExecuteProxy(() => _real.LastErrorString); + public CpuType CPU => _owner.ExecuteProxy(() => _real.CPU); + public short MaxPDUSize => _owner.ExecuteProxy(() => _real.MaxPDUSize); + public short Rack => _owner.ExecuteProxy(() => _real.Rack); + public short Slot => _owner.ExecuteProxy(() => _real.Slot); + + public void Dispose() + => _owner.ExecuteProxy(() => { _real.Dispose(); return true; }); + } + + #endregion + /// /// decodifica da bitmap il CURRENT MODE del controllo /// @@ -1343,7 +1523,7 @@ namespace IOB_WIN_SIEMENS.IobSiemens int valInt = 0; uint valUInt = 0; // cerco in area memMapWrite... - if (memMap.mMapWrite.ContainsKey(item.uid)) + if (memMap != null && memMap.mMapWrite != null && memMap.mMapWrite.ContainsKey(item.uid)) { // recupero! currMem = memMap.mMapWrite[item.uid]; @@ -1450,6 +1630,11 @@ namespace IOB_WIN_SIEMENS.IobSiemens /// protected override void setParamPlc() { + // forzo single-thread comm x SIEMENS (S7.Net non thread-safe): attiva il ramo proxy + IOBConfFull.General.MachWLoopSingleThread = true; + // avvio il thread dedicato (unico owner di currPLC) PRIMA della creazione PLC + StartCommThread(); + // Creo oggetto connessione NC parentForm.commPlcActive = true; lgInfoStartup($"Start init Adapter SIEMENS all'IP {IOBConfFull.Device.Connect.IpAddr} | CPU: {IOBConfFull.Special.SiemensConf.CpuType} | R/S: {IOBConfFull.Special.SiemensConf.Rack}/{IOBConfFull.Special.SiemensConf.Slot} | --> IOB {IOBConfFull.General.CodIOB}"); @@ -1469,17 +1654,6 @@ namespace IOB_WIN_SIEMENS.IobSiemens parametri.rack = IOBConfFull.Special.SiemensConf.Rack; parametri.tipoCpu = (CpuType)Enum.Parse(typeof(CpuType), IOBConfFull.Special.SiemensConf.CpuType); parametri.ipAdrr = IOBConfFull.Device.Connect.IpAddr; -#if false - // leggo file init... - lgInfoStartup("Reading ini file..."); - IniFile fIni = new IniFile(cIobConf.iniFileName); - // ora leggo valori speciali - parametri.memAddrRead = fIni.ReadString("MEMORY", "ADDR_READ", ""); - parametri.memAddrWrite = fIni.ReadString("MEMORY", "ADDR_WRITE", ""); - parametri.memSizeRead = fIni.ReadInteger("MEMORY", "SIZE_READ", 0); - parametri.memSizeWrite = fIni.ReadInteger("MEMORY", "SIZE_WRITE", 0); -#endif - parametri.memAddrRead = IOBConfFull.Special.SiemensConf.MemAddrRead; parametri.memAddrWrite = IOBConfFull.Special.SiemensConf.MemAddrWrite; parametri.memSizeRead = IOBConfFull.Special.SiemensConf.MemSizeRead; @@ -1498,7 +1672,7 @@ namespace IOB_WIN_SIEMENS.IobSiemens needRefresh = false; try { - currPLC = new Plc(parametri.tipoCpu, parametri.ipAdrr, parametri.rack, parametri.slot); + currPLC = new PlcProxy(this, parametri.tipoCpu, parametri.ipAdrr, parametri.rack, parametri.slot); // disconnetto e connetto... if (isVerboseLog) { diff --git a/IOB-WIN-SIEMENS/IobSiemens/SiemensTorri.cs b/IOB-WIN-SIEMENS/IobSiemens/SiemensTorri.cs index 40ab6823..0e543d30 100644 --- a/IOB-WIN-SIEMENS/IobSiemens/SiemensTorri.cs +++ b/IOB-WIN-SIEMENS/IobSiemens/SiemensTorri.cs @@ -483,20 +483,6 @@ namespace IOB_WIN_SIEMENS.IobSiemens trovati += checkAlarmBank(item.memAddr); } } - // legacy... tolto! -#if false - else - { - // leggo i banchi allarmi : cablato D700.DBDW2 --> D700 DBDW14, sono 4 banchi a 32 bit - // da verificare - - // ciclo nei 4 banchi... - trovati += checkAlarmBank("DB700.DBDW2"); - trovati += checkAlarmBank("DB700.DBDW6"); - trovati += checkAlarmBank("DB700.DBDW10"); - trovati += checkAlarmBank("DB700.DBDW14"); - } -#endif } } diff --git a/IOB-WIN-SIEMENS/SiemensSingleThread.md b/IOB-WIN-SIEMENS/SiemensSingleThread.md new file mode 100644 index 00000000..afd2402f --- /dev/null +++ b/IOB-WIN-SIEMENS/SiemensSingleThread.md @@ -0,0 +1,196 @@ +# 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 (`tryConnect` → `currPLC.Open()` `:1042`) sul thread UI (via `loadIobType`). +- **Thread macchina dedicato** `WorkerLoopMachine` (`AdapterForm.cs`) → `doMachineTask` + (`Generic.cs:723`) → `readSemafori`/`process*` → `S7ReadBB` → `currPLC.ReadBytes` (`:469`). +- **Thread worker server** (`Task.Run` pool) → `doServerTaskAsync` (`Generic.cs:853`) → + `S7WriteBB` → `currPLC.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` coda thread-safe. +- **`_commThread`**: thread unico e persistente, avviato durante l'inizializzazione, unico + proprietario di `currPLC` (inclusa la creazione `new Plc(...)`). +- **`ExecuteProxy(Func)`**: 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(Func)` 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 currPLC` → `protected 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()` (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 currPLC` → `protected 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 == null` → `memMap = 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**: +```csharp +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. + + +