Files
samuele ae748ebeb2 Update vers test DECODER
- cablata logica Cà
- test su WKST-SAM notturno
2023-03-30 19:16:15 +02:00

1313 lines
53 KiB
C#

using Microsoft.Extensions.Configuration;
using MP.MONO.Core;
using MP.MONO.Core.CONF;
using MP.MONO.Core.DTO;
using Newtonsoft.Json;
using NLog;
using Opc.Ua;
using Opc.Ua.Configuration;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MP.MONO.ADAPTER.OPC
{
/// <summary>
/// Classe di comunicazione OPC-UA
/// </summary>
public class IobOpcUa : IobGeneric
{
#region Public Constructors
/// <summary>
/// Avvia un oggetto IOB in grado di comunicare con server OPC-UA Impiega il pacchetto Nuget
/// OPC-UA foundation https://github.com/OPCFoundation/UA-.NETStandard
/// </summary>
/// <param name="confPath"></param>
/// <param name="config"></param>
public IobOpcUa(string confPath, IConfigurationRoot? config) : base(confPath, config)
{
// setup redis
redisConf = config.GetConnectionString("Redis");
ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true);
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(redisConf);
sub = redis.GetSubscriber();
redisDb = redis.GetDatabase();
confManager = new ConfigManager(redisConf, confPath);
msVetoRedCache = config.GetValue<int>("OptPar:VetoSendCache");
MinWait = config.GetValue<int>("OptPar:MinWait");
MaxWait = config.GetValue<int>("OptPar:MaxWait");
DataStaleSec = config.GetValue<int>("OptPar:DataStaleSec");
AlarmCleanPre = config.GetValue<string>("OptPar:AlarmCleanPre");
AlarmCleanPost = config.GetValue<string>("OptPar:AlarmCleanPost");
AlarmTrimWSpace = config.GetValue<bool>("OptPar:AlarmTrimWSpace");
logInfo($"VetoSendCache: {msVetoRedCache}s | MinWait: {MinWait} | MaxWait: {MaxWait} | AlarmCleanPre: {AlarmCleanPre} | AlarmCleanPost: {AlarmCleanPost}");
// gestione data filtering...
if (!string.IsNullOrEmpty(config.GetValue<string>("ENABLE_DATA_FILTER")))
{
bool.TryParse(config.GetValue<string>("ENABLE_DATA_FILTER"), out enableDataFilter);
}
// gestione restart OpcUa client...
if (!string.IsNullOrEmpty(config.GetValue<string>("ENABLE_CLI_RESTART")))
{
bool.TryParse(config.GetValue<string>("ENABLE_CLI_RESTART"), out enableCliRestart);
}
// init datetime counters
DateTime adesso = DateTime.Now;
lastCurrent = adesso;
// ora leggo il file di conf specifico....
if (!string.IsNullOrEmpty(cIobConf.ConfFile))
{
// leggo il file...
loadOpcUaConf(cIobConf.ConfFile);
}
}
#endregion Public Constructors
#region Public Properties
/// <summary>
/// Verifica SE si debba fare log periodico (ogni "verboseLogTOut" sec...)
/// </summary>
public bool periodicLog
{
get
{
bool answ = false;
answ = (DateTime.Now.Subtract(lastPeriodicLog).TotalSeconds > getCRI("verboseLogTOut"));
if (answ)
{
lastPeriodicLog = DateTime.Now;
}
return answ;
}
}
#endregion Public Properties
#region Public Methods
/// <summary>
/// Processo i task richiesti e li elimino dalla coda 1:1
/// </summary>
/// <param name="task2exe"></param>
public override Dictionary<string, string> executeTasks(Dictionary<string, string> task2exe)
{
// uso metodo base x ora
return base.executeTasks(task2exe);
}
///// <summary>
///// Effettua IMPOSTAZIONE FORZATA del contapezzi, NON POSSIBILE in questa versione
///// </summary>
///// <returns></returns>
//public override bool setcontapezziPLC(int newPzCount)
//{
// bool answ = false;
// return answ;
//}
/// <summary>
/// Override connessione
/// </summary>
public override void tryConnect()
{
if (!connectionOk)
{
// controllo che il ping sia stato tentato almeno pingTestSec fa...
if (DateTime.Now.Subtract(lastPING).TotalSeconds > getCRI("pingTestSec"))
{
if (verboseLog || periodicLog)
{
logInfo("OpcUa: ConnKO - tryConnect");
}
// in primis salvo data ping...
lastPING = DateTime.Now;
// se passa il ping faccio il resto...
if (testPingMachine == IPStatus.Success)
{
string szStatusConnection = "";
try
{
// ora provo connessione...
var task = Task.Run(async () =>
{
return await doConnect().ConfigureAwait(false);
});
short esitoLink = task.Result; // use returned result from async method here
logInfo($"szStatusConnection OpcUa, esitoLink: {esitoLink}");
connectionOk = true;
// refresh stato allarmi!!!
if (connectionOk)
{
if (adpRunning)
{
logInfo("Connessione OK");
}
}
else
{
logError("Impossibile procedere, connessione mancante...");
}
}
catch (Exception exc)
{
logFatal($"Errore nella connessione all'adapter OpcUa: {szStatusConnection}{Environment.NewLine}{exc}");
connectionOk = false;
logInfo($"Eccezione in TryConnect, Adapter OpcUa NON running, pausa di {getCRI("waitRecMSec")} msec prima di ulteriori tentativi di riconnessione");
}
}
else
{
// loggo no risposta ping ...
connectionOk = false;
if (verboseLog || periodicLog)
{
logInfo($"Attenzione: OpcUa controllo PING fallito per IP {cIobConf.IpAddress}");
}
}
}
}
else
{
needRefresh = true;
}
// se non è ancora connesso faccio procesisng memoria caso disconnesso...
if (!connectionOk)
{
// processo semafori ed invio...
processMemoryDiscon();
}
}
/// <summary>
/// Override disconnessione
/// </summary>
public override void tryDisconnect()
{
if (connectionOk)
{
if (UA_ref != null)
{
string szStatusConnection = "";
try
{
UA_ref.Disconnect();
connectionOk = false;
logInfo(szStatusConnection);
logInfo("Effettuata disconnessione adapter OpcUa!");
}
catch (Exception exc)
{
logFatal($"Errore nella disconnessione dall'adapter OpcUa{Environment.NewLine}{exc}");
}
}
else
{
logDebug("IMPOSSIBILE effettuare disconnessione OpcUa: UA_ref non disponibile...");
}
}
else
{
logError("IMPOSSIBILE effettuare disconnessione OpcUa: Connessione non disponibile...");
}
}
#endregion Public Methods
#region Internal Fields
/// <summary>
/// dataOra ultima verifica CNC disconnesso...
/// </summary>
protected DateTime lastDisconnCheck;
/// <summary>
/// Valore soglia in secondi x indicare che i dati sono stale = bloccati/non aggiornati
/// </summary>
protected int DataStaleSec = 120;
/// <summary>
/// Verifica se i dati siano stale = incastrati (ultimo dato letto oltre 2 minuti fa) e quindi sia encessario effettuare disconnect/reconnect
/// </summary>
public bool dataIsStale
{
get => DateTime.Now.Subtract(lastCurrent).TotalSeconds > DataStaleSec;
}
/// <summary>
/// Dizionario della DataOra ultimo invio x ogni valore
/// </summary>
internal Dictionary<string, DateTime> LastSend = new Dictionary<string, DateTime>();
#endregion Internal Fields
#region Internal Methods
/// <summary>
/// Verifica ed invia variazioni
/// </summary>
/// <param name="MonIt"></param>
/// <param name="NotifyValue"></param>
/// <param name="forceSend"></param>
internal bool checkAndSend(Opc.Ua.Client.MonitoredItem MonIt, string NotifyValue, bool forceSend)
{
bool changed = false;
if (MonIt != null)
{
if (NotifyValue != null)
{
string sVal = "";
string descr = "";
DateTime locTStamp = DateTime.Now;
descr = itemTranslation("OPC", MonIt.DisplayName);
sVal = $"C&S | Change: {locTStamp} | descr: {descr} | Id: {MonIt.StartNodeId} | Val: {NotifyValue} | forceSend: {forceSend}";
logInfo(sVal);
// verifico se salvare
changed = checkSaveValue(MonIt, NotifyValue);
// cerco se non sia un dato filtrato in FLUXLOG...
bool isFiltDisplName = opcUaParams.fluxLogVeto.Contains(MonIt.DisplayName);
bool isFiltNodeId = opcUaParams.fluxLogVeto.Contains($"{MonIt.StartNodeId}");
if (isFiltDisplName || isFiltNodeId)
{
logTrace($"C&S | NON ACCODATO sample per {MonIt.DisplayName} - trovato VETO in fluxLogVeto");
}
else
{
if (changed || forceSend)
{
routeAndSend($"{MonIt.StartNodeId}", $"{stringCleanup(NotifyValue)}");
}
else
{
logTrace($"C&S | NON ACCODATO sample per {MonIt.StartNodeId} - verifica variazione ha dato esito negativo");
}
}
}
else
{
logTrace($"C&S | checkAndSend ERROR | MonIt: {MonIt.StartNodeId} | NotifyValue Null!!!");
}
}
else
{
logTrace("C&S | checkAndSend ERROR: MonIt null");
}
return changed;
}
/// <summary>
/// Verifica ed invia variazioni DAL FORMATO RAW data (byte[])
/// </summary>
/// <param name="MonIt"></param>
/// <param name="NotifyValue"></param>
/// <param name="forceSend"></param>
internal virtual bool checkAndSendRaw(Opc.Ua.Client.MonitoredItem MonIt, byte[] NotifyValue, bool forceSend)
{
bool changed = false;
if (MonIt != null)
{
if (NotifyValue != null && NotifyValue.Length > 0)
{
// versione base: il valore è la stringa composta da TUTTI i valori in BYTE
// espressi come comma-sep-string
StringBuilder sb = new StringBuilder();
foreach (var bVal in NotifyValue)
{
sb.Append($"{bVal},");
}
string currVal = sb.ToString();
string sVal = "";
string descr = "";
DateTime locTStamp = DateTime.Now;
descr = itemTranslation("OPC", MonIt.DisplayName);
sVal = $"Change: {locTStamp.ToString()} | descr: {descr} | Id: {MonIt.StartNodeId} | Val: {currVal}";
logInfo(sVal);
// verifico se salvare
changed = checkSaveValue(MonIt, currVal) || forceSend;
// cerco se non sia un dato filtrato in FLUXLOG...
bool isFiltered = opcUaParams.fluxLogVeto.Contains(MonIt.DisplayName);
if (isFiltered)
{
logTrace($"NON ACCODATO sample per {MonIt.DisplayName} - trovato VETO in fluxLogVeto");
}
else
{
if (changed || forceSend)
{
routeAndSend($"{MonIt.StartNodeId}", $"{NotifyValue}");
//routeAndSend(sVal, qEncodeFLog(descr, $"{NotifyValue}"));
}
else
{
logTrace($"NON ACCODATO sample per {MonIt.DisplayName} - verifica variazione ha dato esito negativo");
}
}
}
else
{
logError($"checkAndSend ERROR | MonIt: {MonIt.DisplayName} | NotifyValue Null!!!");
}
}
else
{
logError("checkAndSend ERROR: MonIt null");
}
return changed;
}
///// <summary>
///// Effettua reset del contapezzi, NON POSSIBILE in questa versione
///// </summary>
///// <returns></returns>
//public override bool resetcontapezziPLC()
//{
// bool answ = false;
// return answ;
//}
/// <summary>
/// Processa gestione memoria x invio dati QUANDO IOB è disconnesso da CNC...
/// </summary>
internal void processMemoryDiscon()
{
// controllo contatore invio "keepalive"... invio solo a scadenza
if (DateTime.Now.Subtract(lastDisconnCheck).TotalSeconds > getCRI("disconMaxSec"))
{
// FIXME TODO !!! FARE da realizzare un sistema x inviare che il server OPC-UA è
// disonnesso e la macchina è in stato unknown / server disconnesso
#if false
// resetto tutti i valori BYTE IN/PREV/OUT... così invio macchina spenta...
B_input = 0;
B_output = 0;
B_previous = 0;
accodaSigIN(ref currDispData);
#endif
// update controllo
lastDisconnCheck = DateTime.Now;
}
}
//#region Internal Methods
/// <summary>
/// REDIS: salva in cache + invia tramite canale di notifica
/// </summary>
/// <param name="memKey">Scrive su Redis se !=""</param>
/// <param name="notifyChannel"></param>
/// <param name="message"></param>
internal void saveAndSendMessage(string memKey, string notifyChannel, string message)
{
// se ho una memoria di cache redis in cui scrivere...
if (!string.IsNullOrEmpty(memKey))
{
// effettuo la scrittura nell'area di memoria indicata SE passato intervallo minimo
bool doSend = true;
if (LastSend.ContainsKey(memKey))
{
if (DateTime.Now.Subtract(LastSend[memKey]).TotalMilliseconds < msVetoRedCache)
{
doSend = false;
}
}
else
{
LastSend.Add(memKey, DateTime.Now);
}
if (doSend)
{
redisDb.StringSetAsync(memKey, message);
LastSend[memKey] = DateTime.Now;
logInfo($"S&S | Redis Cache Key: {memKey}");
}
}
// invio notifica tramite il canale richiesto
sub.Publish(notifyChannel, message);
if (verboseLog)
{
logInfo($"S&S | [{notifyChannel}] key: {memKey} | val/message: {message}");
}
}
/// <summary>
/// Evento rilevazione modifica valori --&gt;; chiamo checkSend
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
internal virtual void UA_ref_eh_MonItChange(object sender, opcUaMonitItemChange e)
{
string currVal = "";
// da verificare decodifica valore byte...
if (doByteRead)
{
var currNot = e.CurrNotify;
if (currNot != null)
{
byteRawData = getByteRaw((DataValue)currNot.Value);
checkAndSendRaw(e.CurrMonitoredItem, byteRawData, false);
}
}
else
{
// prendo il valore bonificato dei caratteri che vogliamo ignorare
currVal = stringCleanup($"{e.CurrNotify.Value}");
checkAndSend(e.CurrMonitoredItem, currVal, false);
}
// aggiorno ultima lettura
lastCurrent = DateTime.Now;
}
#endregion Internal Methods
#region Protected Fields
/// <summary>
/// Struttura dove vengono memorizzati i dataitem ed i rispettivi valori x processing
/// </summary>
protected Dictionary<string, OpcUaDataItemExt> dataItemMem = new Dictionary<string, OpcUaDataItemExt>();
//#region Protected Fields
/// <summary>
/// Abilitazione restart (da opt par...)
/// </summary>
protected bool enableCliRestart = false;
//#endregion Internal Methods
/// <summary>
/// Gestione filtraggio dati
/// </summary>
protected bool enableDataFilter = false;
/// <summary>
/// Determina se ha effettuata lettura items in memoria x confronto...
/// </summary>
protected bool hasReadItems = false;
/// <summary>
/// Ultimo current received x gestione update periodico...
/// </summary>
protected DateTime lastCurrent = DateTime.Now;
/// <summary>
/// Oggetto MAIN x connessione OPC-UA
/// </summary>
protected UAClient UA_ref;
#endregion Protected Fields
#region Protected Properties
/// <summary>
/// Area dati raw per lettura encoded (SE presente)
/// </summary>
protected byte[] byteRawData { get; set; } = new byte[1];
//#region Protected Properties
/// <summary>
/// Indica se si debba leggere un area di tipo "encoded" (byte raw --&gt; obj)
/// </summary>
protected bool doByteRead { get; set; } = false;
/// <summary>
/// Parametri specifici OPC-UA
/// </summary>
protected OpcUaParamConf opcUaParams { get; set; } = new OpcUaParamConf();
#endregion Protected Properties
#region Protected Methods
/// <summary>
/// Verifica / Salva valore e restitusice SE sia variato (e quindi da inviare...)
/// </summary>
/// <param name="dataItem"></param>
/// <param name="NotifyValue"></param>
/// <returns></returns>
protected bool checkSaveValue(Opc.Ua.Client.MonitoredItem dataItem, string NotifyValue)
{
bool answ = !enableDataFilter;
double oldVal = 0;
double newVal = 0;
if (dataItem != null)
{
if (NotifyValue != null)
{
logTrace($"CSV | Richiesta checkSaveValue | id: {dataItem.StartNodeId} | {dataItem.DisplayName} | Valore: {NotifyValue}");
// verifico in memoria se ho l'oggetto condition ed il suo valore..
string uuid = $"{dataItem.StartNodeId}";
DateTime adesso = DateTime.Now;
if (dataItemMem.ContainsKey(uuid))
{
OpcUaDataItemExt currDataItemMem = dataItemMem[uuid];
// controllo SE SIA scaduto il tempo massimo...
if (Math.Abs(dataItemMem[uuid].valueTimestamp.Subtract(adesso).TotalSeconds) > currDataItemMem.samplePeriod)
{
answ = true;
}
else
{
// ALTRIMENTI controllo SE diverso
if (dataItemMem[uuid].value != $"{NotifyValue}")
{
logInfo($"CSV | uuid: {uuid} | old Val {dataItemMem[uuid].value} | NotifyValue: {NotifyValue}");
// controllo SE ho DeadBand...
if (dataItemMem[uuid].thresholdDeadBand > 0)
{
// recupero i valori e testo DeadBand...
bool isNum01 = double.TryParse(dataItemMem[uuid].value.Replace(".", ","), out oldVal);
bool isNum02 = double.TryParse($"{NotifyValue}".Replace(".", ","), out newVal);
// test deadband!
if (!(isNum01 && isNum02))
{
answ = true;
}
else
{
if (Math.Abs(newVal - oldVal) > dataItemMem[uuid].thresholdDeadBand)
{
// indico da salvare..
answ = true;
}
}
logInfo($"CSV | Test deadband: oldVal: {oldVal} | newVal: {newVal} | esito check: {answ}");
}
}
}
if (answ)
{
// salvo!
dataItemMem[uuid].value = $"{NotifyValue}";
dataItemMem[uuid].valueTimestamp = adesso;
}
}
else
{
// registro non trovato da aggiungere...
logInfo($"CSV | DataItem non trovato in checkSaveValue: {uuid} | {dataItem.DisplayName}");
// provo a creare oggetto in memoria...
try
{
int dSamplePeriod = 0;
int threshDBand = 0;
uuid = "";
var currDataItem = formatDataItem(ref dSamplePeriod, ref threshDBand, ref uuid, dataItem);
// sistemo valore/periodo
currDataItem.value = $"{NotifyValue}";
currDataItem.valueTimestamp = adesso;
// aggiungo
dataItemMem.Add(uuid, currDataItem);
}
catch (Exception exc)
{
logError($"CSV | Eccezione in checkSaveSample{Environment.NewLine}{exc}");
}
}
}
else
{
logError("CSV | Attenzione: checkSaveItem con Notify null!");
}
}
else
{
logError("CSV | Attenzione: checkSaveItem con MonIt null!");
}
return answ;
}
protected byte[] getByteRaw(DataValue obj)
{
if (obj == null)
return null;
byte[] rawByte = new byte[1];
if (obj != null)
{
try
{
var wrapVal = ((DataValue)obj).WrappedValue;
//rawByte = ObjectToByteArray(rawVal);
var bodyVal = ((Opc.Ua.ExtensionObject)wrapVal.Value).Body;
rawByte = (byte[])bodyVal;
}
catch
{ }
}
return rawByte;
}
/// <summary>
/// Effettua traduzione ITEM da LUT parametrica (key: tipo+id) del file di conf, se non
/// trovo uso key
/// </summary>
/// <param name="tipo"></param>
/// <param name="id"></param>
/// <returns></returns>
protected string itemTranslation(string tipo, string id)
{
string answ = "";
string lemma = id;
if (!string.IsNullOrEmpty(tipo))
{
lemma = $"{tipo}_{id}";
}
// cerco nel dizionario delle traduzioni SE esiste un valore e prendo quello, altrimenti
// uso il lemma...
if (opcUaParams.itemTranslation.ContainsKey(lemma))
{
answ = opcUaParams.itemTranslation[lemma];
}
else
{
answ = lemma;
}
return answ;
}
/// <summary>
/// Effettua lettura file di conf specifico OPC-UA da oggetto serializzato json <paramref
/// name="fileName">Nome file da cui leggere i parametri json</paramref>
/// </summary>
protected void loadOpcUaConf(string fileName)
{
string jsonFullPath = Path.Combine(configPath, fileName);
logInfo($"Apertura file {jsonFullPath}");
using (StreamReader reader = new StreamReader(jsonFullPath))
{
string jsonData = reader.ReadToEnd().Replace("\n", "").Replace("\r", "");
if (!string.IsNullOrEmpty(jsonData))
{
logDebug($"File json composto da {jsonData.Length} caratteri");
try
{
opcUaParams = JsonConvert.DeserializeObject<OpcUaParamConf>(jsonData);
logDebug($"Decodifica aree OpcUaParamConf: trovati {opcUaParams.paramsEndThresh.Count} valori paramsEndThresh");
// sistemo se ci sono dati memMap...
memMap = new plcMemMap();
if (opcUaParams.mMapWrite != null)
{
memMap.mMapWrite = opcUaParams.mMapWrite;
}
if (opcUaParams.mMapRead != null)
{
memMap.mMapRead = opcUaParams.mMapRead;
}
setupMemMap();
// faccio setup degli altri valori...
setupGroupConf();
}
catch (Exception exc)
{
logError($"Eccezione in decodifica conf json OPC-UA:{Environment.NewLine}{exc}");
}
}
else
{
logError("Errore in loadOpcUaConf: file json vuoto!");
}
}
}
/// <summary>
/// Routed send:
/// - verifica eventuale destinazione x valore ricevuto (se configurato su 1+ canali) + invia
/// - traduce (se configurato nei canali) il valore key da uuid (=NodeId) a StringKey
/// - invia nel generico canale eventi globale
/// </summary>
/// <param name="uuid">uuid chiave del parametro rilevato (NodeId), da tradurre se necessario</param>
/// <param name="rawDataVal">VALORE encoded da trasmettere (come string)</param>
protected void routeAndSend(string uuid, string rawDataVal)
{
// cerco se sia nella conf x invio in specifico canale RAW
// Activity Log
if (ActLogKList.Contains(uuid))
{
// recupero valore e verifico se sia numerico...
bool numOk = false;
double updVal = 0;
numOk = double.TryParse(rawDataVal, out updVal);
// aggiorno nella lista dei valori quello cambiato...
var currItem = ActLogStatus.FirstOrDefault(x => x.ExtCode == uuid);
if (numOk && currItem.IsNumeric)
{
currItem.ValueNum = updVal;
currItem.Value = updVal.ToString(currItem.DisplFormat);
}
else
{
currItem.Value = rawDataVal;
}
logTrace($"R&S | ActLogKList | {uuid} | {currItem.Value}");
// prendo tutti i valori del gruppo e li serializzo come DisplayDataDTO
string payloadActLog = JsonConvert.SerializeObject(ActLogStatus);
// invio
saveAndSendMessage(Constants.ACT_LOG_CURR_KEY, Constants.ACT_LOG_M_QUEUE, payloadActLog);
}
// Tools
if (ToolKList.Contains(uuid))
{
// recupero valore e verifico se sia numerico...
bool numOk = false;
double updVal = 0;
numOk = double.TryParse(rawDataVal, out updVal);
// aggiorno nella lista dei valori quello cambiato...
var currItem = ToolStatus.FirstOrDefault(x => x.ExtCode == uuid);
if (numOk && currItem != null && currItem.IsNumeric)
{
currItem.ValueNum = updVal;
currItem.Value = updVal.ToString(currItem.DisplFormat);
}
else
{
currItem.Value = rawDataVal;
}
logTrace($"R&S | ToolKList | {uuid} | {currItem.Value}");
#if false
// prendo tutti i valori del gruppo e li serializzo come DisplayDataDTO
string payloadTools = JsonConvert.SerializeObject(ToolStatus);
// invio
saveAndSendMessage(Constants.TOOLS_CURR_KEY, Constants.TOOLS_M_QUEUE, payloadTools);
#endif
// genero un dictionary di valori stringa / double e lo serializzo...
var dataDict = ToolStatus.ToDictionary(x => x.Title, x => x.Value);
string payloadTools = JsonConvert.SerializeObject(dataDict);
// invio
saveAndSendMessage(Constants.TOOLS_ACT_KEY, Constants.TOOLS_RAW_QUEUE, payloadTools);
}
// Parameters
if (ParamKList.Contains(uuid))
{
// recupero valore e verifico se sia numerico...
bool numOk = false;
double updVal = 0;
numOk = double.TryParse(rawDataVal, out updVal);
// aggiorno nella lista dei valori quello cambiato...
var currItem = ParamStatus.FirstOrDefault(x => x.ExtCode == uuid);
if (numOk && currItem.IsNumeric)
{
currItem.ValueNum = updVal;
currItem.Value = updVal.ToString(currItem.DisplFormat);
}
else
{
currItem.Value = rawDataVal;
}
logTrace($"R&S | ParamKList | {uuid} | {currItem.Value}");
// genero un dictionary di valori stringa / double e lo serializzo...
var dataDict = ParamStatus.ToDictionary(x => x.Title, x => x.Value);
string payloadParams = JsonConvert.SerializeObject(dataDict);
// invio
saveAndSendMessage(Constants.PARAMS_ACT_KEY, Constants.PARAMS_RAW_QUEUE, payloadParams);
#if false
// parametri: inviati come DTO
string payloadParams = JsonConvert.SerializeObject(ParamStatus);
// invio
saveAndSendMessage(Constants.PARAMS_CURR_KEY, Constants.PARAMS_M_QUEUE, payloadParams);
#endif
}
// Allarmi
if (AlarmsKList.Contains(uuid))
{
// allarmi: effettuo PRE processing x filtrare pre/post { }
if (!string.IsNullOrEmpty(AlarmCleanPre))
{
rawDataVal = rawDataVal.Replace(AlarmCleanPre, "");
}
if (!string.IsNullOrEmpty(AlarmCleanPost))
{
rawDataVal = rawDataVal.Replace(AlarmCleanPost, "");
}
if (AlarmTrimWSpace && string.IsNullOrWhiteSpace(rawDataVal))
{
rawDataVal = rawDataVal.Trim();
}
// aggiorno nella lista dei valori quello cambiato...
var currItem = AlarmsStatus.FirstOrDefault(x => x.ExtCode == uuid);
currItem.Value = rawDataVal;
// salvo in redis!
redisDb.StringSet(Constants.ALARMS_ADAP_CONF_KEY, JsonConvert.SerializeObject(AlarmsStatus));
// genero elenco allarmi attivi
List<string> adpActiveAlarm = AlarmsStatus
.Select(x => x.Value)
.Where(x => !string.IsNullOrEmpty(x))
.OrderBy(x => x)
.ToList();
logTrace($"R&S | AlarmsKList | {uuid} | {currItem.Value}");
var payloadAlarms = JsonConvert.SerializeObject(adpActiveAlarm);
// invio
saveAndSendMessage(Constants.ALARM_ACT_KEY, Constants.ALARM_RAW_QUEUE, payloadAlarms);
}
// Stato macchina / processo
if (MPKList.Contains(uuid))
{
// recupero valore e verifico se sia numerico...
bool numOk = false;
double updVal = 0;
numOk = double.TryParse(rawDataVal, out updVal);
// aggiorno nella lista dei valori quello cambiato...
var currItem = MPStatus.FirstOrDefault(x => x.ExtCode == uuid);
if (numOk && currItem.IsNumeric)
{
currItem.ValueNum = updVal;
currItem.Value = updVal.ToString(currItem.DisplFormat);
}
else
{
currItem.Value = rawDataVal;
}
logTrace($"R&S | MPKList | {uuid} | {currItem.Value}");
// genero un dictionary di valori stringa / double e lo serializzo...
var dataDict = MPStatus.ToDictionary(x => x.Title, x => x.Value);
string payloadParams = JsonConvert.SerializeObject(dataDict);
// invio
saveAndSendMessage(Constants.STATUS_ACT_KEY, Constants.STATUS_RAW_QUEUE, payloadParams);
}
// Dati raw x counters
if (CountKList.Contains(uuid))
{
// recupero valore e verifico se sia numerico...
bool numOk = false;
double updVal = 0;
numOk = double.TryParse(rawDataVal, out updVal);
// aggiorno nella lista dei valori quello cambiato...
var currItem = CountStatus.FirstOrDefault(x => x.ExtCode == uuid);
if (numOk && currItem.IsNumeric)
{
currItem.ValueNum = updVal;
currItem.Value = updVal.ToString(currItem.DisplFormat);
}
else
{
currItem.Value = rawDataVal;
}
logTrace($"R&S | CountKList | {uuid} | {currItem.Value}");
// genero un dictionary di valori stringa / double e lo serializzo...
var dataDict = CountStatus.ToDictionary(x => x.Title, x => x.Value);
string payloadParams = JsonConvert.SerializeObject(dataDict);
// invio
saveAndSendMessage(Constants.COUNT_CURR_KEY, Constants.COUNT_RAW_QUEUE, payloadParams);
}
}
#endregion Protected Methods
#region Private Fields
private static string redisConf = "";
/// <summary>
/// Char x bonifica stringa allarmi - POST
/// </summary>
private string AlarmCleanPost = "";
/// <summary>
/// Char x bonifica stringa allarmi - PRE
/// </summary>
private string AlarmCleanPre = "";
/// <summary>
/// Indica se forzare trim dei whitespace da allarmi
/// </summary>
private bool AlarmTrimWSpace = false;
private Logger Log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Elenco degli items da monitorare come risultato del browse iniziale
/// </summary>
private Dictionary<string, string> selectedItemList = new Dictionary<string, string>();
#endregion Private Fields
#region Private Properties
// // decodifica e gestione
// decodeToBaseBitmap();
// reportRawInput(ref currDispData);
// }
// catch (Exception exc)
// {
// currDispData.semIn = Semaforo.SR;
// logError($"Eccezione in readSemafori:{Environment.NewLine}{exc}");
// }
// // se > max errori --> disconnetto
// if (currReadErrors > maxReadErrors)
// {
// logError($"Superato limite errori Read ({currReadErrors}) --> tryDisconnect");
// currReadErrors = 0;
// tryDisconnect();
// }
// else
// {
// // altrimenti pausa forzata
// Thread.Sleep(300);
// }
//}
//#endregion Public Methods
/// <summary>
/// Elenco chiavi dei parametri gestiti come ActConf
/// </summary>
private List<string> ActLogKList { get; set; } = new List<string>();
// // verifico SE sia necessario forzare la lettura RAW if (doByteRead) { if
// (DateTime.Now.Subtract(lastCurrent).TotalSeconds > lastCurrentMaxElapsed) { // FIXME TODO
// !!! foreach (var item in dataItemMem) { // restituisce i 115 byte da deserializzare...
// qui HACK! var rawVal = UA_ref.ReadNodeRaw(item.Value.StartNodeId); if (rawVal != null) {
// byteRawData = getByteRaw((DataValue)rawVal); lastCurrent = DateTime.Now; currReadErrors =
// 0; } else { currReadErrors++; } } } }
/// <summary>
/// Elenco oggetti DisplayDTO da gestire come ActLog
/// </summary>
private List<DisplayDataDTO>? ActLogStatus { get; set; } = new List<DisplayDataDTO>();
/// <summary>
/// Elenco chiavi dei parametri gestiti come Allarmi
/// </summary>
private List<string> AlarmsKList { get; set; } = new List<string>();
/// <summary>
/// Elenco oggetti DisplayDTO da gestire come Allarmi
/// </summary>
private List<DisplayDataDTO>? AlarmsStatus { get; set; } = new List<DisplayDataDTO>();
private ConfigManager confManager { get; set; }
/// <summary>
/// Elenco chiavi dei parametri relativi ai contatori
/// </summary>
private List<string> CountKList { get; set; } = new List<string>();
/// <summary>
/// Elenco oggetti DisplayDTO relativi ai contatori
/// </summary>
private List<DisplayDataDTO>? CountStatus { get; set; } = new List<DisplayDataDTO>();
/// <summary>
/// Attesa massima (es x discovery)
/// </summary>
private int MaxWait { get; set; } = 250;
/// <summary>
/// Attesa minima (es x discovery)
/// </summary>
private int MinWait { get; set; } = 50;
/// <summary>
/// Elenco chiavi dei parametri relativi stato macchina/processo
/// </summary>
private List<string> MPKList { get; set; } = new List<string>();
/// <summary>
/// Elenco oggetti DisplayDTO relativi stato macchina/processo
/// </summary>
private List<DisplayDataDTO>? MPStatus { get; set; } = new List<DisplayDataDTO>();
/// <summary>
/// Durata in ms x veto (ri)scrittura verso REDIS cache
/// </summary>
private int msVetoRedCache { get; set; } = 300;
/// <summary>
/// Elenco chiavi dei parametri gestiti come Params
/// </summary>
private List<string> ParamKList { get; set; } = new List<string>();
/// <summary>
/// Elenco oggetti DisplayDTO da gestire come Params
/// </summary>
private List<DisplayDataDTO>? ParamStatus { get; set; } = new List<DisplayDataDTO>();
/// <summary>
/// Redis DB
/// </summary>
private IDatabase? redisDb { get; set; }
/// <summary>
/// Redis channel manager
/// </summary>
private ISubscriber sub { get; set; }
/// <summary>
/// Elenco chiavi dei parametri gestiti come Tool
/// </summary>
private List<string> ToolKList { get; set; } = new List<string>();
/// <summary>
/// Elenco oggetti DisplayDTO da gestire come Tool
/// </summary>
private List<DisplayDataDTO>? ToolStatus { get; set; } = new List<DisplayDataDTO>();
#endregion Private Properties
///// <summary>
///// Periodo massimo (in sec) per letture dati RAW in mancanza di eventi
///// </summary>
//protected int lastCurrentMaxElapsed { get; set; } = 120;
#region Private Methods
/// <summary>
/// Vera connessione ad OpcUa
/// </summary>
/// <returns></returns>
private async Task<short> doConnect()
{
short esitoLink = 0;
// reset memoria dataItem..
dataItemMem = new Dictionary<string, OpcUaDataItemExt>();
// ora avvio
try
{
logInfo("Start init OpcUa Client");
// Define the UA Client application
ApplicationInstance application = new ApplicationInstance();
application.ApplicationName = "EgalWare MP.MONO Adapter Client";
application.ApplicationType = ApplicationType.Client;
// load the application configuration.
string confPath = Path.Combine(Directory.GetCurrentDirectory(), "CONF", "MpMonoAdapterClient.Config.xml");
await application.LoadApplicationConfiguration(confPath, silent: false).ConfigureAwait(false);
// check the application certificate.
await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0).ConfigureAwait(false);
logInfo($"New UAClient stasrted with std config: {application.ApplicationConfiguration.ApplicationName}");
UA_ref = new UAClient(application.ApplicationConfiguration, opcUaParams.Identity.UserName, opcUaParams.Identity.Passwd, ClientBase.ValidateResponse);
// se nullo calcolo il fullUrl
if (string.IsNullOrEmpty(cIobConf.FullUrl))
{
cIobConf.FullUrl = $"opc.tcp://{cIobConf.IpAddress}:{cIobConf.Port}";
}
logInfo($"Call to OpcUa Server at address: {cIobConf.FullUrl}");
UA_ref.ServerUrl = cIobConf.FullUrl;
//logInfo($"Call to OpcUa Server at address: opc.tcp://{cIobConf.IpAddress}:{cIobConf.Port}");
//UA_ref.ServerUrl = $"opc.tcp://{cIobConf.IpAddress}:{cIobConf.Port}";
var task = Task.Run(async () =>
{
return await UA_ref.ConnectAsync().ConfigureAwait(false);
});
bool connected = task.Result;
if (connected)
{
// faccio un primo browse dei dati...
Dictionary<string, string> nodeIdNameList = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(opcUaParams.BrowseFullVal))
{
try
{
UA_ref.Browse(opcUaParams.BrowseFullVal, opcUaParams.filterItemsNodeId, ref nodeIdNameList);
}
catch
{ }
}
else
{
UA_ref.Browse(opcUaParams.BrowseNSIndex, opcUaParams.BrowseValue, opcUaParams.filterItemsNodeId, ref nodeIdNameList);
}
// loggo elenco degli item sottocrivibili...
logDebug("---------- AVAILABLE FOR SUBSCRIBE ----------");
foreach (var item in nodeIdNameList)
{
logDebug(item.Key);
}
logDebug("---------- END LIST ----------");
// se ho un insieme non vuoto degli item sottoscritti carico solo quelli
if (opcUaParams.subscribedItems != null && opcUaParams.subscribedItems.Count > 0)
{
// cerco e aggiungo SOLO quelle indicati
foreach (var currItem in opcUaParams.subscribedItems)
{
var foundItems = nodeIdNameList.Where(x => x.Key.Contains(currItem)).ToList();
if (foundItems != null && foundItems.Count > 0)
{
foreach (var fItem in foundItems)
{
// verifico di NON duplicare...
if (!selectedItemList.ContainsKey(fItem.Key))
{
selectedItemList.Add(fItem.Key, fItem.Value);
}
}
}
else
{
logDebug($"subscribedItems non trovato: {currItem}");
}
}
logDebug($"Aggiunti {selectedItemList.Count} items!");
}
// altrimenti tutti!
else
{
selectedItemList = nodeIdNameList;
}
// loggo elenco degli item sottocrivibili...
logInfo("---------- SUBSCRIBED NODES ----------");
foreach (var item in selectedItemList)
{
logInfo(item.Key);
}
logInfo("---------- END LIST ----------");
// sottoscrivo a rilevazione cambio dati solo l'incrocio degli insiemi
List<Opc.Ua.Client.MonitoredItem> subscribedItems = UA_ref.SubscribeToDataChanges(selectedItemList);
// aggiungo come DataItems
int dSamplePeriod = 0;
int threshDBand = 0;
string uuid = "";
Random rnd = new Random();
foreach (var item in subscribedItems)
{
bool changed = false;
OpcUaDataItemExt newItem = formatDataItem(ref dSamplePeriod, ref threshDBand, ref uuid, item);
// controllo non sia già stato aggiunto
if (dataItemMem.ContainsKey(uuid))
{
logDebug($"CONN | Item ALREADY subscribed: {uuid} | NOT re-adding");
}
else
{
dataItemMem.Add(uuid, newItem);
logDebug($"CONN | Item subscribed: {uuid}");
// verifico se ho i dati complessi by design
string currVal = "";
if (doByteRead)
{
// restituisce i 115 byte da deserializzare... qui HACK!
var rawVal = UA_ref.ReadNodeRaw(item.StartNodeId);
if (rawVal != null)
{
byteRawData = getByteRaw((DataValue)rawVal);
changed = checkAndSendRaw(item, byteRawData, true);
}
}
else
{
currVal = UA_ref.ReadNode(item.StartNodeId);
var fixString = stringCleanup(currVal);
changed = checkAndSend(item, fixString, true);
}
}
// attesa x evitare problemi
logDebug(" --------------- ");
Thread.Sleep(rnd.Next(MinWait, MaxWait));
}
// gestione eventi change
UA_ref.eh_MonItChange += UA_ref_eh_MonItChange;
logInfo("CONN | eh_MonItChange event registered");
}
esitoLink = 1;
// fix tempi!
DateTime adesso = DateTime.Now;
lastCurrent = adesso;
}
catch (Exception exc)
{
logError($"Eccezione in doConnect{Environment.NewLine}{exc}");
}
return esitoLink;
}
//#region Private Methods
/// <summary>
/// Formatta un dataitem x salvataggio in memoria locale
/// </summary>
/// <param name="dSamplePeriod"></param>
/// <param name="threshDBand"></param>
/// <param name="uuid"></param>
/// <param name="dataItem"></param>
/// <returns></returns>
private OpcUaDataItemExt formatDataItem(ref int dSamplePeriod, ref int threshDBand, ref string uuid, Opc.Ua.Client.MonitoredItem dataItem)
{
OpcUaDataItemExt currDataItem;
// calcolo parametri
uuid = dataItem.StartNodeId.ToString();
// SOLO SE è abilitato il datafiltering...
if (enableDataFilter)
{
threshDBand = 1;
// controllo SE ho conf x deadband...
if (opcUaParams.paramsEndThresh.Count > 0)
{
// ciclo su tutti i parametri indicati...
foreach (var item in opcUaParams.paramsEndThresh)
{
if (uuid.EndsWith(item.Key))
{
threshDBand = item.Value;
}
}
}
}
else
{
threshDBand = 0;
}
dSamplePeriod = 60;
// salvo oggetto x "uso interno"
currDataItem = new OpcUaDataItemExt(dataItem)
{
uid = uuid,
thresholdDeadBand = threshDBand,
samplePeriod = dSamplePeriod
};
return currDataItem;
}
/// <summary>
/// Effettua setup delle conf dei gruppi di dati
/// </summary>
private void setupGroupConf()
{
// leggo e salvo conf varie (dati x decodifica e gestione)
ActLogStatus = confManager.getActLog();
ActLogKList = ActLogStatus.Select(x => x.ExtCode).ToList();
ToolStatus = confManager.getTools();
ToolKList = ToolStatus.Select(x => x.ExtCode).ToList();
ParamStatus = confManager.getParamsConf();
ParamKList = ParamStatus.Select(x => x.ExtCode).ToList();
AlarmsStatus = confManager.getAlarmsConf();
AlarmsKList = AlarmsStatus.Select(x => x.ExtCode).ToList();
MPStatus = confManager.getMPStatusConf();
MPKList = MPStatus.Select(x => x.ExtCode).ToList();
CountStatus = confManager.getCountersConf();
CountKList = CountStatus.Select(x => x.ExtCode).ToList();
}
#endregion Private Methods
}
}