using Microsoft.Extensions.Configuration; using MP.MONO.Core; using MP.MONO.Core.CONF; using Newtonsoft.Json; using NLog; using System; using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; using System.Text.RegularExpressions; using static MP.MONO.Core.CONF.OpcUaParamConf; using static MP.MONO.Core.Enums; namespace MP.MONO.ADAPTER.OPC { public class IobGeneric { #region Public Fields /// /// Struttura memoria PLC x lettura/scrittura da JSON file /// public plcMemMap memMap; #endregion Public Fields #region Public Constructors /// /// Avvia generico IOB /// /// /// public IobGeneric(string confPath, IConfigurationRoot? config) { lg = LogManager.GetCurrentClassLogger(); connectionOk = false; configPath = confPath; confMan = config; if (confMan != null) { // carico dati endpoint var selection = confMan.GetSection("Endpoint"); if (selection.Exists()) { // recupero dati endpoint cIobConf = selection.Get(); } // carico dati OptPar var optPar = confMan.GetSection("OptPar"); if (optPar.Exists()) { // configurazione gestione log Log2Console = optPar.GetValue("Log2Console"); Log2File = optPar.GetValue("Log2File"); // configurazione gestione split/regexp dati ForceSplit = optPar.GetValue("ForceSplit"); CleanupRegexpPre = optPar.GetValue("CleanupRegexpPre"); if (optPar.GetValue("FiltCrLf")) { FiltSplitChar = new char[] { '\n', '\r', '\0' }; } else { FiltSplitChar = optPar.GetValue("FiltSplitChar"); } CleanupRegexpPost = optPar.GetValue("CleanupRegexpPost"); } } } #endregion Public Constructors #region Public Properties /// /// indica se ping disabilitato da optPar /// public bool pingDisabled { get => getCRB("NO_PING"); } /// /// Verifica SE si debba fare log verboso (verboso + ogni tot letture IN) /// public bool verboseLog { get { bool answ = false; int logEvery = getCRI("logEvery"); if (logEvery < 1) { logEvery = 10; } answ = getCRB("verbose") && (nReadIN % logEvery == 0); return answ; } } #endregion Public Properties #region Public Methods /// /// Esecuzione dei task richiesti e pulizia coda richieste eseguite /// /// public virtual Dictionary executeTasks(Dictionary task2exe) { // Verificare il protocollo: dovrebbe togliere SOLO i task eseguiti... Dictionary taskDone = new Dictionary(); if (task2exe != null) { // controllo se memMap != null... if (memMap != null) { bool taskOk = false; string taskVal = ""; // cerco task specifici: se ho startSetup --> imposto bit DBB701.DBB0.4 foreach (var item in task2exe) { taskOk = false; taskVal = ""; // converto richiesta in enum... taskType tName = taskType.nihil; Enum.TryParse(item.Key, out tName); // controllo sulla KEY... switch (tName) { case taskType.setArt: case taskType.setComm: case taskType.setProg: case taskType.setPzComm: // recupero dati da memMap... if (memMap != null && memMap.mMapWrite != null) { if (memMap.mMapWrite.ContainsKey(item.Key)) { dataConf currMem = memMap.mMapWrite[item.Key]; string addr = currMem.memAddr; taskVal = $"SET task: {item.Key} --> {item.Value} | mem: {currMem.memAddr} - {currMem.size} byte"; // salvo il nuovo valore nella memoria... così prox invio lo trasmetterà memMap.mMapWrite[item.Key].value = item.Value; } else { taskVal = $"NO DATA MEM, SET task: {item.Key} --> {item.Value}"; } } else { taskVal = $"NO MemMap found, SET task: {item.Key} --> {item.Value}"; } // salvo in currProd.. saveProdData(new KeyValuePair(item.Key, item.Value)); break; case taskType.forceResetPzCount: // reset contapezzi inizio setup taskOk = resetcontapezziPLC(); taskVal = taskOk ? "RESET PZ COUNT OK" : "PZ RESET DISABLED | NO EXEC"; logInfo($"Chiamata forceResetPzCount: taskOk: {taskOk} | taskVal: {taskVal}"); break; case taskType.startSetup: // reset contapezzi inizio setup taskOk = resetcontapezziPLC(); taskVal = taskOk ? "RESET: SETUP START" : "PZ RESET DISABLED | NO EXEC"; logInfo($"Chiamata startSetup: taskOk: {taskOk} | taskVal: {taskVal}"); break; case taskType.stopSetup: // reset contapezzi fine setup SE ESPLICITAMENTE IMPOSTATO if (getCRB("ENABLE_PZ_RESET_stopSetup")) { taskOk = resetcontapezziPLC(); } taskVal = taskOk ? "RESET: SETUP END" : "PZ RESET DISABLED | NO EXEC"; logInfo($"Chiamata stopSetup: taskOk: {taskOk} | taskVal: {taskVal}"); break; case taskType.setParameter: // richiedo da URL i parametri WRITE da popolare logInfo("Chiamata setParameter --> processMemWriteRequests"); taskVal = processMemWriteRequests(); // se restituiscce "" faccio altra prova... if (string.IsNullOrEmpty(taskVal)) { // i parametri me li aspetto come stringa composta paramName|paramvalue if (item.Value.Contains("|")) { string[] paramsJob = item.Value.Split('|'); taskVal = $"REQUEST SET PARAMETERS: {paramsJob[0]} --> {paramsJob[1]}"; } else { taskVal = $"WRONG REQUEST FOR SET PARAMETERS: {item.Value} doesnt contain pipe for splitting key/value"; } } break; default: taskVal = $"taskReq: {tName} | key: {item.Key} | val: {item.Value} | SKIPPED | NO EXEC"; logInfo($"Chiamata senza processing: taskOk: {taskOk} | taskVal: {taskVal}"); break; } // aggiungo task! taskDone.Add(item.Key, taskVal); } } else { logError($"Attenzione! memMap è nullo, non posso eseguire task2exe!"); } } return taskDone; } /// /// Fornisce il valore di flusso e valore in formato valido x messa in coda nel formato /// dtEve#flusso#valore#cont Flusso datiValore da salvare /// public string qEncodeFLog(string flusso, string valore) { string answ = ""; // fixme todo !!! fare tenere? cambiare? modificare? si dovrebbe cambiare con invio su // channel REDIS dei dati ricevuti, eventualmente divisi x canale secondo setup #if false try { answ = $"{DateTime.Now:yyyyMMddHHmmssfff}#{flusso}#{valore}#{counterULog}"; } catch { } #endif return answ; } /// /// Metodo generico di reset contapezzi... /// /// public virtual bool resetcontapezziPLC() { return false; } /// /// Salva valori indicati in prod data /// /// Item KVP di cui salvare i dati in currProdData come chiave/valore /// public void saveProdData(KeyValuePair item) { // imposto i valori... if (currProdData.ContainsKey(item.Key)) { currProdData[item.Key] = item.Value; } else { currProdData.Add(item.Key, item.Value); } } /// /// Metodo base connessione... /// public virtual void tryConnect() { dtAvvioAdp = DateTime.Now; } /// /// Metodo base disconnessione... /// public virtual void tryDisconnect() { } #endregion Public Methods #region Internal Fields /// /// valore booleano di check se sia stato AVVIATO l'adapter (Running) /// internal bool adpRunning = false; /// /// Data/ora ultimo avvio adapter /// internal DateTime dtAvvioAdp = DateTime.Now; /// /// Data/ora ultimo spegnimento adapter /// internal DateTime dtStopAdp = DateTime.Now; /// /// dataOra ultimo log periodico... /// internal DateTime lastPeriodicLog; /// /// dataOra ultimo PING inviato verso il PLC... /// internal DateTime lastPING = DateTime.Now.AddHours(-1); /// /// indica se serva refresh parametri e quindi PLC... /// internal bool needRefresh = true; #endregion Internal Fields #region Internal Properties /// /// COnfiguraizone Endpoint corrente /// internal EndpointData cIobConf { get; set; } = new EndpointData(); /// /// Stringa regexp da usare x bonifica caratteri, POST split /// internal string CleanupRegexpPost { get; set; } = ""; /// /// Stringa regexp da usare x bonifica caratteri, PRE split /// internal string CleanupRegexpPre { get; set; } = ""; internal string configPath { get; set; } = ""; internal IConfigurationRoot? confMan { get; set; } = null!; /// /// Salva verifica stato connessione OK /// /// internal virtual bool connectionOk { get { return _connOk; } set { _connOk = value; } } /// /// Carattere x eventuale split/filt valori notificati /// internal char[] FiltSplitChar { get; set; } = new char[1]; /// /// Indica se forzare split caratteri letti /// internal bool ForceSplit { get; set; } = false; /// /// Abilitazione al log su console /// internal bool Log2Console { get; set; } = false; /// /// Abilitazione al log su file /// internal bool Log2File { get; set; } = false; #endregion Internal Properties #region Internal Methods /// /// Effettua log INFO su file e se richiesto su console /// internal static void logInfo(string msg, bool log2file = true, bool log2console = true) { if (log2console) { Console.WriteLine("INFO | " + msg); } if (log2file) { lg.Info(msg); } } internal bool getCRB(string keyName) { bool answ = false; answ = confMan.GetValue(keyName); return answ; } internal int getCRI(string keyName) { int answ = -1; answ = confMan.GetValue(keyName); return answ; } internal string getCRS(string keyName) { string answ = ""; answ = confMan.GetValue(keyName); return answ; } /// /// Effettua log DEBUG su file e se richiesto su console /// internal void logDebug(string msg) { if (Log2Console) { Console.WriteLine("DEBUG | " + msg); } if (Log2File) { lg.Debug(msg); } } /// /// Effettua log ERROR su file e se richiesto su console /// internal void logError(string msg) { if (Log2Console) { Console.WriteLine("ERROR | " + msg); } if (Log2File) { lg.Error(msg); } } /// /// Effettua log FATAL su file e se richiesto su console /// internal void logFatal(string msg) { if (Log2Console) { Console.WriteLine("FATAL | " + msg); } if (Log2File) { lg.Fatal(msg); } } /// /// Effettua log TRACE su file e se richiesto su console /// internal void logTrace(string msg) { if (Log2Console) { Console.WriteLine("TRACE | " + msg); } if (Log2File) { lg.Trace(msg); } } /// /// Effettua pulizia caratteri da valori non ammessi... /// /// /// internal string stringCleanup(string rawData) { string currVal = ""; string temp = ""; // Step 01: pulizia dati preliminare (SE impostato cleanup...) if (!string.IsNullOrEmpty(CleanupRegexpPre)) { Regex rgxPre = new Regex(CleanupRegexpPre); temp = rgxPre.Replace(rawData, ""); } else { temp = rawData; } // Step 02: se è abilitato filt/split... if (ForceSplit) { var dataArray = temp.Split(FiltSplitChar); if (dataArray.Length > 0) { currVal = dataArray[0]; } } // Step 03: se non ok copio x intero if (string.IsNullOrEmpty(currVal)) { currVal = temp; } // Step 04: pulizia dati finale (SE impostato cleanup...) if (!string.IsNullOrEmpty(CleanupRegexpPost)) { Regex rgxPost = new Regex(CleanupRegexpPost); currVal = rgxPost.Replace(currVal, ""); } return currVal; } #endregion Internal Methods #region Protected Fields /// /// wrapper di log /// protected static Logger lg; protected bool _connOk = false; /// /// Dizionario valori impostati x produzione /// protected Dictionary currProdData = new Dictionary(); /// /// Dizionario ultimi valori (double) delle TSVC /// protected Dictionary LastTSVC = new Dictionary(); /// /// Dizionario di VC da trattare come TimeSeries (con conf decodificata + processing successivo...) /// protected Dictionary TSVC_Data = new Dictionary(); #endregion Protected Fields #region Protected Properties /// /// Numero letture IN da avvio /// protected int nReadIN { get; set; } /// /// test ping all'indirizzo PLC/CNC impostato nei parametri /// /// protected IPStatus testPingMachine { get { IPStatus answ = IPStatus.Unknown; // se disabilitato salto... if (pingDisabled) { answ = IPStatus.Success; } else { IPAddress address; PingReply reply; using (Ping pingSender = new Ping()) { address = IPAddress.Loopback; int pingMsTimeout = cIobConf.PingMsTimeout; IPAddress.TryParse(cIobConf.IpAddress, out address); try { // se != null --> uso address... if (address != null) { reply = pingSender.Send(address, pingMsTimeout); } else { reply = pingSender.Send(cIobConf.IpAddress, pingMsTimeout); } } catch { reply = pingSender.Send(IPAddress.Loopback, pingMsTimeout); } answ = reply.Status; } } return answ; } } #endregion Protected Properties #region Protected Methods /// /// Stringa raw dei parametri da scrivere... /// /// protected string getParams2write() { string answ = ""; // recuperare da una apposita area REDIS... #if false string url2call = $"{urlGetParams2Write}"; if (verboseLog) { lgInfo("chiamata URL " + url2call); } answ = utils.callUrlNow(url2call); // se vuoto faccio seconda prova... if (string.IsNullOrEmpty(answ)) { answ = utils.callUrlNow(url2call); } #endif return answ; } /// /// Metodo da overridare x scrivere DAVVERO i parametri sul PLC /// /// protected virtual void plcWriteParams(ref List updatedPar) { // non faccio nulla di base... } /// /// Processa le richieste di scrittura memoria /// /// protected string processMemWriteRequests() { string answ = ""; // li salvo nei parametri in memoria locale (ogni adapter DOVREBBE salvare POI sul VERO PLC) List writeList = new List(); List updatedPar = new List(); // recupero elenco delle cose da fare string resp = getParams2write(); if (!string.IsNullOrEmpty(resp)) { try { writeList = JsonConvert.DeserializeObject>(resp); // se ho da fare chiamo esecuzione.. if (writeList.Count > 0) { foreach (var item in writeList) { // scrivo in memoria if (memMap.mMapWrite.ContainsKey(item.uid)) { memMap.mMapWrite[item.uid].value = item.reqValue; // accodo in stringa taskVal... answ += $" | Parameter {item.uid} --> {item.reqValue}"; // sistemo valori item.value = item.reqValue; logInfo($"Richiesta update parametro {item.uid} | actVal = {item.value} | reqVal = {item.reqValue}"); item.reqValue = ""; // salvo in lista da ritrasmettere updatedPar.Add(item); } else { answ += $" | Error: parameter {item.uid} not found"; } } // richiamo scrittura parametri su PLC plcWriteParams(ref updatedPar); // invio su cloud parametri! string rawData = JsonConvert.SerializeObject(updatedPar); logInfo("Notifica a server scrittura parametri"); #if false utils.callUrl($"{urlUpdateWriteParams}", rawData); #endif } } catch (Exception exc) { logError($"Eccezione in processMemWriteRequests:{Environment.NewLine}{exc}"); } } else { logError("Non è stata ricevuta risposta x task da eseguire"); } return answ; } /// /// setup parametri da file di conf /// protected void setupMemMap() { logInfo($"setupMemMap | trovati {memMap.mMapRead.Count} parametri Read (TSVC)"); logInfo($"setupMemMap | trovati {memMap.mMapWrite.Count} parametri Write"); if (getCRB("verbose")) { string rawMemConf = JsonConvert.SerializeObject(memMap, Formatting.Indented); logDebug($"setupMemMap | configurazione memoria R/W:{Environment.NewLine}{rawMemConf}"); } // se ho variabili read --> genero dati TSVC... if (memMap.mMapRead.Count > 0) { TSVC_Data.Clear(); LastTSVC.Clear(); VCData currConf; int periodo = 0; VC_func funz = VC_func.POINT; // accodo nella conf... foreach (var item in memMap.mMapRead) { funz = item.Value.func; periodo = item.Value.period; currConf = new VCData() { Funzione = funz, Period = periodo, DTStart = DateTime.Now.AddHours(-1), dataArray = new List() }; TSVC_Data.Add(item.Key, currConf); } // documento... foreach (var item in TSVC_Data) { logTrace($"TSVC: {item.Key} | periodo: {item.Value.Period} | funz: {item.Value.Funzione}"); // salvo i valori PREC... LastTSVC.Add(item.Key, 0); } } } #endregion Protected Methods #region Private Fields private IConfigurationRoot config; private string confPath; #endregion Private Fields } }