using IOB_UT; using MapoSDK; using MTConnect.Clients; using Newtonsoft.Json; using System; using System.Threading.Tasks; using Opc.Ua; using Opc.Ua.Configuration; using System.Collections.Generic; using System.IO; using System.Net.NetworkInformation; using System.Threading; using System.Windows.Forms; namespace IOB_WIN_NEXT { public class IobOpcUa : IobGeneric { #region Private Fields /// /// Struttura dove vengono memorizzati i dataitem ed i rispettivi valori x processing /// private Dictionary dataItemMem = new Dictionary(); /// /// Elenco degli items da monitorare come risultato del browse iniziale /// private Dictionary selectedItemList = new Dictionary(); #endregion Private Fields #region Protected Fields /// /// Abilitazione restart (da opt par...) /// protected bool enableCliRestart = false; /// /// Gestione filtraggio dati /// protected bool enableDataFilter = false; /// /// Determina se ha effettuata lettura items in memoria x confronto... /// protected bool hasReadItems = false; /// /// Ultimo current received x gestione update periodico... /// protected DateTime lastCurrent = DateTime.Now; /// /// Oggetto MAIN x connessione MTC /// protected UAClient UA_ref; /// /// Veto controllos tatus x log... /// protected DateTime vetoCheckStatus = DateTime.Now; #endregion Protected Fields #region Public Constructors /// /// Estende l'init della classe base, impiegando il pacchetto Nuget OPC-UA foundation /// https://github.com/OPCFoundation/UA-.NETStandard /// /// /// public IobOpcUa(AdapterForm caller, IobConfiguration IOBConf) : base(caller, IOBConf) { // gestione invio ritardato contapezzi pzCountDelay = utils.CRI("pzCountDelay"); // gestione data filtering... if (!string.IsNullOrEmpty(getOptPar("ENABLE_DATA_FILTER"))) { bool.TryParse(getOptPar("ENABLE_DATA_FILTER"), out enableDataFilter); } // gestione restart MTC client... if (!string.IsNullOrEmpty(getOptPar("ENABLE_CLI_RESTART"))) { bool.TryParse(getOptPar("ENABLE_CLI_RESTART"), out enableCliRestart); } // init datetime counters DateTime adesso = DateTime.Now; lastPzCountSend = adesso; lastWarnODL = adesso; lastCurrent = adesso; // ora leggo il file di conf specifico.... string jsonFileName = getOptPar("OPC_PARAM_CONF"); if (!string.IsNullOrEmpty(jsonFileName)) { // leggo il file... loadOpcUaConf(jsonFileName); } } #endregion Public Constructors #region Protected Properties /// /// Verifico se abbia ALMENO un errore... /// protected bool hasError { get { return checkMultiCondition(opcUaParams.condError); } } /// /// Indica se abbia emergenza premuta /// protected bool hasEStopTriggered { get { return checkMultiCondition(opcUaParams.condEStop); } } /// /// Indica se abbia stato POWER ON (multicondizione) /// protected bool hasPowerOn { get { return checkMultiCondition(opcUaParams.condPowerOn); } } /// /// Indica se abbia stato READY (condizioni varie, es ausiliari OK) /// protected bool isReady { get { return checkMultiCondition(opcUaParams.condReady); } } /// /// Indica se abbia stato READY (condizioni varie, es ausiliari OK) /// protected bool isWorking { get { return checkMultiCondition(opcUaParams.condWork); } } /// /// Parametri specifici MTC /// protected OpcUaParamConf opcUaParams { get; set; } /// /// URL x salvataggio elenco dataItems OpcUa /// protected string urlSaveDataItems { get { string answ = ""; try { string machineName = Environment.MachineName; answ = $@"http://{cIobConf.serverData.MPIP}{cIobConf.serverData.MPURL}{cIobConf.serverData.CMDALIVE}/saveDataItems/{cIobConf.codIOB}"; } catch (Exception exc) { lgError(exc, "Errore in composizione urlSaveDataItems"); } return answ; } } #endregion Protected Properties #region Private Methods /// /// Verifica ed invia variazioni /// /// /// private void checkAndSend(Opc.Ua.Client.MonitoredItem MonIt, MonitoredItemNotification Notify, bool forceSend) { if (MonIt != null) { if (Notify != null) { string sVal = ""; string descr = ""; DateTime locTStamp = DateTime.Now; descr = itemTranslation("OPC", MonIt.DisplayName); sVal = $"Change: {locTStamp.ToString()} | descr: {descr} | Id: {MonIt.StartNodeId} | | Val: {Notify.Value}"; if (isVerboseLog) { lgInfo(sVal); } // verifico se salvare bool changed = checkSaveValue(MonIt, Notify); // cerco se non sia un dato filtrato in FLUXLOG... bool isFiltered = opcUaParams.fluxLogVeto.Contains(MonIt.DisplayName); if (isFiltered) { if (isVerboseLog) { lgInfo($"NON ACCODATO sample per {MonIt.DisplayName} poiché trovato VETO in fluxLogVeto", false); } } else { if (changed || forceSend) { accodaFLog(sVal, qEncodeFLog(descr, $"{Notify.Value}")); } else { if (isVerboseLog) { lgInfo($"NON ACCODATO sample per {MonIt.DisplayName} poiché verifica variazione ha dato esito negativo", false); } } } } else { lgError("checkAndSend ERROR: Notify è null"); } } else { lgError("checkAndSend ERROR: MonIt è null"); } } /// /// Verifica condizione "multipla" secondo setup json /// /// /// private bool checkMultiCondition(List checkList) { bool answ = false; if (checkList != null && checkList.Count > 0) { int numCond = checkList.Count; int numCondOk = 0; // cerco nell'elenco delle condizioni che indicano lavora se sono ok faccio +1 conteggio...... foreach (var item in checkList) { if (getDataItemValue(item.keyName) == item.targetValue) { numCondOk++; } } answ = (numCond == numCondOk); } return answ; } /// /// Effettua decodifica aree memoria alla bitmap usata x MAPO /// private void decodeToBaseBitmap() { // init a zero... B_input = 0; /* ----------------------------------------------------- * bitmap MAPO * B0: POWER_ON * B1: RUN * B2: pzCount * B3: allarme * B4: manuale * B5: emergenza ----------------------------------------------------- */ // se valido il check ping lo eseguo... altrimenti lo do x buono bool checkPing = !opcUaParams.pingAsPowerOn; if (!checkPing) { checkPing = (testPingMachine == IPStatus.Success); } // bit 0 (poweron) imposto a 1 SE pingo o PowerOn=="ON"... B_input = (checkPing || hasPowerOn) ? 1 : 0; // variabili RUN... string currRun = getDataItemValue(opcUaParams.keyRunMode); if (isWorking) { // RUN = LAVORA! B_input += (1 << 1); } // se ho emergenza premuta --> emergenza! else if (hasEStopTriggered) { B_input += (1 << 5); } // se ho almeno 1 allarme E NON SONO IN AUTO --> ALARM! else if (hasError) { B_input += (1 << 3); } else if (isReady) { // se ho run mode != auto --> manual B_input += (1 << 4); } #if false DateTime adesso = DateTime.Now; int vFactor = 1; // controllo SE HO dati per fare verifiche... if (string.IsNullOrEmpty(currRun)) { // se ho parametro x gestione reset... if (enableCliRestart) { // controllo se ho ricevuto il current da OLTRE 1 minuto... if (lastCurrent.AddMinutes(3) < adesso) { lastCurrent = adesso; // stop... lgInfo("Fermato MTC_ref per mancanza dati current"); UA_ref.Disconnect(); Thread.Sleep(1000); // restart lgInfo("Riavviato MTC_ref per mancanza dati current"); var task = Task.Run(async () => { return await UA_ref.ConnectAsync().ConfigureAwait(false); }); } } } else { vFactor = 6; } // solo se non ho veto check if (vetoCheckStatus < adesso) { lgInfo($"Stato variabili: currRun: {currRun}"); // imposto veto per vetoSeconds... vetoCheckStatus = adesso.AddSeconds(vetoSeconds * vFactor); } #endif // log opzionale! if (verboseLog) { lgInfo($"Trasformazione B_input: {B_input} | currRun = {currRun}"); } } /// /// Vera connessione ad MTC /// /// private async Task doConnect() { IOutput console = new ConsoleOutput(); short esitoLink = 0; // reset memoria dataItem.. dataItemMem = new Dictionary(); // predisposizione conf oggetto di comunicazione MTC short port = 5000; short.TryParse(cIobConf.cncPort, out port); // ora avvio try { lgInfo("Start init OpcUa Client"); // Define the UA Client application ApplicationInstance application = new ApplicationInstance(); application.ApplicationName = "Steamware IOB-WIN Client"; application.ApplicationType = ApplicationType.Client; // load the application configuration. await application.LoadApplicationConfiguration("ConsoleReferenceClient.Config.xml", silent: false); // check the application certificate. await application.CheckApplicationInstanceCertificate(silent: false, minimumKeySize: 0); lgInfo($"Chiamata UAClient con configurazione standard: {application.ApplicationConfiguration.ApplicationName}"); UA_ref = new UAClient(application.ApplicationConfiguration, console, ClientBase.ValidateResponse); lgInfo($"Chiamata apertura OpcUa Client: {cIobConf.cncIpAddr}:{port}"); UA_ref.ServerUrl = $"opc.tcp://{cIobConf.cncIpAddr}:{port}"; var task = Task.Run(async () => { return await UA_ref.ConnectAsync(); }); bool connected = task.Result; if (connected) { // faccio un primo browse dei dati... selectedItemList = UA_ref.Browse(opcUaParams.BrowseNSIndex, opcUaParams.BrowseValue); // sottoscrivo a rilevazione cambio dati List subscribedItems = UA_ref.SubscribeToDataChanges(selectedItemList); // aggiungo come DataItems int dSamplePeriod = 0; int threshDBand = 0; string uuid = ""; foreach (var item in subscribedItems) { OpcUaDataItemExt newItem = formatDataItem(ref dSamplePeriod, ref threshDBand, ref uuid, item); dataItemMem.Add(uuid, newItem); } // gestione eventi change UA_ref.eh_MonItChange += UA_ref_eh_MonItChange; } esitoLink = 1; // fix tempi! DateTime adesso = DateTime.Now; lastPzCountSend = adesso; lastWarnODL = adesso; lastCurrent = adesso; } catch { } return esitoLink; } /// /// Formatta un dataitem x salvataggio in memoria locale /// /// /// /// /// /// private OpcUaDataItemExt formatDataItem(ref int dSamplePeriod, ref int threshDBand, ref string uuid, Opc.Ua.Client.MonitoredItem dataItem) { OpcUaDataItemExt currDataItem; // calcolo parametri uuid = $"OPC_{dataItem.DisplayName}"; // 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; } /// /// Effettua lettura file di conf specifico OPC-UA da oggetto serializzato json /// Nome file da cui leggere i parametri json /// private void loadOpcUaConf(string fileName) { string jsonFullPath = $"{Application.StartupPath}/DATA/CONF/{fileName}"; lgInfo($"Apertura file {jsonFullPath}"); StreamReader reader = new StreamReader(jsonFullPath); string jsonData = reader.ReadToEnd().Replace("\n", "").Replace("\r", ""); if (!string.IsNullOrEmpty(jsonData)) { lgInfo($"File json composto da {jsonData.Length} caratteri"); try { opcUaParams = JsonConvert.DeserializeObject(jsonData); lgInfo($"Decodifica aree OpcUaParamConf: trovati {opcUaParams.paramsEndThresh.Count} valori paramsEndThresh"); } catch (Exception exc) { lgError($"Eccezione in decodifica conf json OPC-UA:{Environment.NewLine}{exc}"); } } else { lgError("Errore in loadOpcUaConf: file json vuoto!"); } reader.Dispose(); } /// /// Effettua invio a MP/IO dell'elenco serializzato dei dataItems /// /// private void sendDataItemsList(List dataItems) { string rawData = JsonConvert.SerializeObject(dataItems); utils.callUrlNow($"{urlSaveDataItems}", rawData); } /// /// Evento rilevazione modifica valori --> chiamo checkSend /// /// /// private void UA_ref_eh_MonItChange(object sender, opcUaMonitItemChange e) { checkAndSend(e.CurrMonitoredItem, e.CurrNotify, false); } #endregion Private Methods #region Protected Methods /// /// Verifica un DataItem e se il valore corrisponde a quello indicato come "true value" restituisce true /// /// /// /// protected bool checkDataItem(string itemName, string trueVal) { bool answ = false; OpcUaDataItemExt currValue = null; try { currValue = dataItemMem[itemName]; answ = (currValue.value.Equals(trueVal)); } catch { lgError($"Errore in decodifica valore per {itemName} rispetto a {trueVal} | recuperato {currValue} / {currValue.value}"); } return answ; } /// /// Verifica / Salva valore e restitusice SE sia variato (e quindi da inviare...) /// /// /// protected bool checkSaveValue(Opc.Ua.Client.MonitoredItem dataItem, MonitoredItemNotification Notify) { bool answ = !enableDataFilter; double oldVal = 0; double newVal = 0; if (dataItem != null) { if (Notify != null) { if (isVerboseLog) { lgInfo($"Richiesta checkSaveSample per {dataItem.DisplayName} | id: {dataItem.StartNodeId} | Valore: {Notify.Value}"); } // verifico in memoria se ho l'oggetto condition ed il suo valore.. string uuid = $"OPC_{dataItem.DisplayName}"; 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 != $"{Notify.Value}") { // controllo SE ho DeadBand... if (dataItemMem[uuid].thresholdDeadBand > 0) { if (isVerboseLog) { lgInfo($"Test deadband: oldVal: {oldVal} | newVal: {newVal}"); } // recupero i valori e testo DeadBand... double.TryParse(dataItemMem[uuid].value.Replace(".", ","), out oldVal); double.TryParse($"{Notify.Value}".Replace(".", ","), out newVal); // test deadband! if (Math.Abs(newVal - oldVal) > dataItemMem[uuid].thresholdDeadBand) { // indico da salvare.. answ = true; } } } } if (answ) { // salvo! dataItemMem[uuid].value = $"{Notify.Value}"; dataItemMem[uuid].valueTimestamp = adesso; } } else { // registro non trovato da aggiungere... lgInfo($"DataItem non trovato in checkSaveSample: {dataItem.DisplayName}"); // provo a creare oggetto in memoria... try { List elencoDataItems = new List(); int dSamplePeriod = 0; int threshDBand = 0; uuid = ""; var currDataItem = formatDataItem(ref dSamplePeriod, ref threshDBand, ref uuid, dataItem); // aggiungo dataItemMem.Add(uuid, currDataItem); // salvo oggetto x registrazione su server MP-IO var currMapoDataItem = new machDataItem() { uuid = uuid, Category = DataItemCategory.EVENT, Name = dataItem.DisplayName, Type = $"{dataItem.NodeClass}", SubType = $"{dataItem.StartNodeId}" }; // aggiungo elencoDataItems.Add(currMapoDataItem); // invio il dataItem serializzato... sendDataItemsList(elencoDataItems); } catch (Exception exc) { lgError($"Eccezione in checkSaveSample{Environment.NewLine}{exc}"); } } } else { lgError("Attenzione: checkSaveItem con Notify null!"); } } else { lgError("Attenzione: checkSaveItem con MonIt null!"); } return answ; } /// /// Effettua traduzione ITEM da LUT parametrica (key: tipo+id) del file di conf, se non trovo uso key /// /// /// /// protected string itemTranslation(string tipo, string id) { string answ = ""; string 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; } #endregion Protected Methods #region Public Methods /// /// Processo i task richiesti e li elimino dalla coda 1:1 /// /// public override Dictionary executeTasks(Dictionary task2exe) { // uso metodo base x ora return base.executeTasks(task2exe); } /// /// Recupera uno specifico dataItem /// /// /// public string getDataItemValue(string diKey) { string answ = ""; try { var currDataItem = dataItemMem[diKey]; answ = currDataItem.value; } catch (Exception exc) { Logging.Instance.Error($"Errore in getDataItemValue per {diKey}{Environment.NewLine}{exc}"); } return answ; } /// /// Recupero dati dinamici... /// public override Dictionary getDynData() { Dictionary outVal = new Dictionary(); return outVal; } /// /// Effettua vero processing contapezzi /// public override void processContapezzi() { if (utils.CRB("enableContapezzi")) { // cerco parametro contapezzi... string currPzCount = getDataItemValue(opcUaParams.keyPartCount); // se ho un contapezzi... processo... if (!string.IsNullOrEmpty(currPzCount)) { int newVal = -1; Int32.TryParse(currPzCount, out newVal); contapezziPLC = newVal > -1 ? newVal : contapezziPLC; } } } /// /// Effettua lettura semafori principale /// Parametri da aggiornare x display in form /// public override void readSemafori(ref newDisplayData currDispData) { base.readSemafori(ref currDispData); try { if (verboseLog) { lgInfo("inizio read semafori"); } currDispData.semIn = Semaforo.SV; // decodifica e gestione decodeToBaseBitmap(); reportRawInput(ref currDispData); } catch (Exception exc) { currDispData.semIn = Semaforo.SR; lgError($"Eccezione in readSemafori:{Environment.NewLine}{exc}"); } } /// /// Effettua reset del contapezzi, NON POSSIBILE in questa versione /// /// public override bool resetcontapezziPLC() { bool answ = false; return answ; } /// /// Effettua IMPOSTAZIONE FORZATA del contapezzi, NON POSSIBILE in questa versione /// /// public override bool setcontapezziPLC(int newPzCount) { bool answ = false; return answ; } /// /// Override connessione /// public override void tryConnect() { if (!connectionOk) { // controllo che il ping sia stato tentato almeno pingTestSec fa... if (DateTime.Now.Subtract(lastPING).TotalSeconds > utils.CRI("pingTestSec")) { if (verboseLog || periodicLog) { lgInfo("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... parentForm.commPlcActive = true; var task = Task.Run(async () => { return await doConnect(); }); short esitoLink = task.Result; // use returned result from async method here lgInfo($"szStatusConnection OpcUa, esitoLink: {esitoLink}"); parentForm.commPlcActive = false; connectionOk = true; // refresh stato allarmi!!! if (connectionOk) { if (adpRunning) { lgInfo("Connessione OK"); } } else { lgError("Impossibile procedere, connessione mancante..."); } } catch (Exception exc) { lgFatal($"Errore nella connessione all'adapter OpcUa: {szStatusConnection}{Environment.NewLine}{exc}"); connectionOk = false; lgInfo($"Eccezione in TryConnect, Adapter OpcUa NON running, pausa di {utils.CRI("waitRecMSec")} msec prima di ulteriori tentativi di riconnessione"); } } else { // loggo no risposta ping ... connectionOk = false; if (verboseLog || periodicLog) { lgInfo($"Attenzione: OpcUa controllo PING fallito per IP {cIobConf.cncIpAddr}"); } } } } else { needRefresh = true; } // se non è ancora connesso faccio procesisng memoria caso disconnesso... if (!connectionOk) { // processo semafori ed invio... processMemoryDiscon(); } } /// /// Override disconnessione /// public override void tryDisconnect() { if (connectionOk) { string szStatusConnection = ""; try { UA_ref.Disconnect(); connectionOk = false; lgInfo(szStatusConnection); lgInfo("Effettuata disconnessione adapter OpcUa!"); } catch (Exception exc) { lgFatal(exc, "Errore nella disconnessione dall'adapter OpcUa"); } } else { lgError("IMPOSSIBILE effettuare disconnessione OpcUa: Connessione non disponibile..."); } } #endregion Public Methods } }