Merge branch 'Feature/SiemensTest_01' into develop

This commit is contained in:
Samuele Locatelli
2026-08-13 08:30:02 +02:00
9 changed files with 421 additions and 157 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ namespace IOB_UT_NEXT.Iob
/// <summary>
/// Struttura memoria PLC x lettura/scrittura da JSON file
/// </summary>
public plcMemMapExt memMap;
public plcMemMapExt memMap = new plcMemMapExt();
/// <summary>
/// Minimo numero di px da inviare in blocco
+8 -2
View File
@@ -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");
-20
View File
@@ -1856,26 +1856,6 @@ namespace IOB_WIN_FORM.Iob
return fatto;
}
#if false
/// <summary>
/// Effettua un trim della stringa al numero max di linee da mostrare a video
/// </summary>
/// <param name="newString"></param>
/// <returns></returns>
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
/// <summary>
/// riporta il log di tutti i dati di results temporali registrati
/// </summary>
-63
View File
@@ -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
+2 -3
View File
@@ -39,7 +39,6 @@ CLI_INST=SteamWareSim
;STARTLIST=PIZ03
;STARTLIST=3020
;STARTLIST=SIMUL_01
STARTLIST=3010
MAXCNC=10
STARTLIST=3017
MAXCNC=10
+23 -37
View File
@@ -195,10 +195,18 @@
<None Include="DATA\CONF\3010_alarm.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\3011.ini" />
<None Include="DATA\CONF\3011_alarm.json" />
<None Include="DATA\CONF\3013.ini" />
<None Include="DATA\CONF\3017.ini" />
<None Include="DATA\CONF\3011.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\3011_alarm.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\3013.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\3017.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\3020.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
@@ -217,47 +225,25 @@
<None Include="DATA\CONF\GT694.ini" />
<None Include="DATA\CONF\GT696.ini" />
<None Include="DATA\CONF\GT698.ini" />
<None Include="DATA\CONF\iBTB1P.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\iBTB1P.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\iBTB1P.ini" />
<None Include="DATA\CONF\iBTB1P.json" />
<None Include="DATA\CONF\INTERCL_01.ini" />
<None Include="DATA\CONF\INTERCL_01.json" />
<None Include="DATA\CONF\iOKU1P.ini" />
<None Include="DATA\CONF\iOKU1P.json" />
<None Include="DATA\CONF\MAIN.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\PIZ03.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\PIZ03.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\PIZ03_alarm.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\PIZ08.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\PIZ08.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\PIZ08_alarm.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\PIZ03.ini" />
<None Include="DATA\CONF\PIZ03.json" />
<None Include="DATA\CONF\PIZ03_alarm.json" />
<None Include="DATA\CONF\PIZ08.ini" />
<None Include="DATA\CONF\PIZ08.json" />
<None Include="DATA\CONF\PIZ08_alarm.json" />
<None Include="DATA\CONF\SIEMENS.ini" />
<None Include="DATA\CONF\SIMUL_01.ini">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\SIMUL_01.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\SIMUL_01_alarm.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="DATA\CONF\SIMUL_01.ini" />
<None Include="DATA\CONF\SIMUL_01.json" />
<None Include="DATA\CONF\SIMUL_01_alarm.json" />
<None Include="DATA\CONF\SIMUL_01_WD.ini" />
<None Include="DATA\CONF\VALV_SAET.ini" />
<None Include="DATA\CONF\VL20.ini" />
+191 -17
View File
@@ -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();
}
/// <summary>
@@ -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
}
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Override stopAdapter: ferma l'adapter e poi ferma il thread dedicato
/// (nessun leak sul restart che ricrea l'oggetto).
/// </summary>
public override async Task stopAdapter(bool tryRestart, bool forceDequeue)
{
await base.stopAdapter(tryRestart, forceDequeue);
StopCommThread();
}
/// <summary>
/// Converte direttamente un valore UInt16 su un oggetto byte[2]
/// </summary>
@@ -1138,9 +1166,9 @@ namespace IOB_WIN_SIEMENS.IobSiemens
#region Protected Fields
/// <summary>
/// Oggetto PLC da ri-utilizzare...
/// Oggetto PLC da ri-utilizzare... (PlcProxy: marshalling sul comm-thread)
/// </summary>
protected Plc currPLC;
protected PlcProxy currPLC;
/// <summary>
/// Esito ultimo ping
@@ -1162,6 +1190,12 @@ namespace IOB_WIN_SIEMENS.IobSiemens
/// </summary>
protected bool writePre = true;
// ---- Infrastruttura Single-Thread Proxy (S7.Net non thread-safe) ----
private BlockingCollection<Action> _commQueue = new BlockingCollection<Action>();
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)
/// <summary>
/// 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.
/// </summary>
private void StartCommThread()
{
if (_commThread != null && _commThread.IsAlive)
return;
_commQueue = new BlockingCollection<Action>();
_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)");
}
/// <summary>
/// Ferma il thread dedicato: completa la coda e attende (con timeout) la fine delle azioni in corso.
/// </summary>
private void StopCommThread()
{
if (_commThread == null)
return;
lgInfo($"[Thread: {Thread.CurrentThread.ManagedThreadId}] StopCommThread");
_commQueue.CompleteAdding();
_commThread.Join(3000);
_commThread = null;
}
/// <summary>
/// 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).
/// </summary>
private T ExecuteProxy<T>(Func<T> 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<T>();
_commQueue.Add(() =>
{
try
{
tcs.SetResult(action());
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
return tcs.Task.Result;
}
else
{
lock (_lockSync)
{
return action();
}
}
}
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// decodifica da bitmap il CURRENT MODE del controllo
/// </summary>
@@ -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
/// </summary>
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)
{
@@ -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
}
}
+196
View File
@@ -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<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 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<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 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.