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.
+
+
+