Merge branch 'Feature/SiemensTest_01' into develop
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,6 @@ CLI_INST=SteamWareSim
|
||||
;STARTLIST=PIZ03
|
||||
;STARTLIST=3020
|
||||
;STARTLIST=SIMUL_01
|
||||
STARTLIST=3010
|
||||
|
||||
MAXCNC=10
|
||||
STARTLIST=3017
|
||||
|
||||
MAXCNC=10
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user