using IOB_UT_NEXT.Config; using IOB_UT_NEXT.Config.Mem; using IOB_UT_NEXT.Objects; using IOB_UT_NEXT.Services.Cache; using IOB_UT_NEXT.Services.Core; using IOB_UT_NEXT.Services.Data; using IOB_UT_NEXT.Services.Files; using MapoSDK; using Newtonsoft.Json; using NLog; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.NetworkInformation; using System.Runtime.Serialization.Formatters.Binary; using System.Text.Json; using System.Text.Json.Serialization; using static IOB_UT_NEXT.Config.BaseAlarmConf; namespace IOB_UT_NEXT.Iob { /// /// Classe di base per IOB /// public class BaseObj { #region Public Fields /// /// valore booleano di check se sia stato AVVIATO l'adapter (Running) /// public bool adpRunning = false; /// /// valore booleano di check se l'adapter STIA SALVANDO /// public bool adpSaving = false; /// /// valore booleano (richiesta di riavvio automatico) /// public bool adpTryRestart; /// /// Livello minimo allarmi da considerare x invio /// public AlarmLevel alarmLevelMin = AlarmLevel.Alarm; /// /// Struttura allarmi mappati /// public List alarmMaps = new List(); /// /// Tipo di allarmi gestiti /// rif: BaseAlarmConf.AlarmBlockType.[Bitmap/ActiveList] /// public AlarmBlockType alarmType = AlarmBlockType.Bitmap; /// /// Conteggio ATTUALE ore macchina IN LAVORO /// public double contOreMaccLav; /// /// Conteggio ATTUALE ore macchina ON /// public double contOreMaccOn; /// /// contatore x simulazione valori input /// public int countSim = 0; /// /// ODL attualmente sulla macchina /// public Int32 currIdxODL = 0; /// /// Modo corrente (da classe ENUM) /// public CNC_MODE currMode; /// /// ODL corrente caricato sulla macchina (stringa, da chiamata MP/IO) /// public string currODL = ""; /// /// Indica se sia richiesto campionamento memoria PERIODICO /// public bool doSampleMemory; /// /// Indica se si debba leggere e fare DUMP delle aree di memoria (1 volta solo all'avvio x debug...) /// public bool doStartMemDump; /// /// Collettore di tutte le variabili scadenza DateTime /// public DateTimeHelper DtHelp; /// /// Abilitazione lettura PrgName /// public bool enablePrgName = true; /// /// Abilitazione invio pezzi "in blocco" per recupero contapezzi /// public bool enableSendPzCountBlock = false; /// /// Determina se sia encessario convertire valori little/big endian (SIEMENS=true, OSAI=FALSE) /// public bool hasBigEndian = false; /// /// Configurazione gerarchica completa (v 4.x.x.x) /// public IobConfTree IOBConfFull; /// /// dataOra ultima verifica CNC disconnesso... /// public DateTime lastDisconnCheck; /// /// Data/ora ultima volta che IOB è stato dichiarato online /// public DateTime lastIobOnline = DateTime.Now.AddHours(-1); /// /// Ultima verifica status IOB x forzare display status SRV /// public DateTime lastIobStatusDisplUpdate = DateTime.Now; /// /// dataOra ultimo log periodico... /// public DateTime lastPeriodicLog; /// /// dataOra ultimo PING inviato verso il PLC... /// public DateTime lastPING = DateTime.Now.AddHours(-1); /// /// DataOra ultima lettura da PLC /// public DateTime lastReadPLC; /// /// ULtimo valore inviato (in caso di disconnessione lo reinvia x garantire watchdog...) /// public string lastSignInVal = ""; /// /// DateTime Ultimo valore simulazione generato /// public DateTime lastSim; /// /// dataOra ultimo segnale inviato al SERVER... /// public DateTime lastWatchDog; /// /// dataOra ultimo segnale inviato a macchina/PLC... /// public DateTime lastWatchDogPLC = DateTime.Now; /// /// Massimo numero di px da inviare in blocco /// public int maxSendPzCountBlock = 10; /// /// Struttura memoria PLC x lettura/scrittura da JSON file /// public plcMemMapExt memMap; /// /// Minimo numero di px da inviare in blocco /// public int minSendPzCountBlock = 1; /// /// Variabile booleana che indica se sia necessario fare refresh del contapezzi /// public bool needRefreshPzCount = true; /// /// Determina se utilizzare blocchi di memoria IOT contigui (e quindi processing /// "monoblocco" semplificato"= /// public bool procIotMem = false; /// /// Coda valori ALLARMI ove gestiti... /// public DataQueue QueueAlarm; /// /// Oggetto della coda degli elementi letti di tipo FluxLog (e non ancora trasmessi) /// public DataQueue QueueFLog; /// /// Oggetto della coda degli elementi letti (e non ancora trasmessi) /// public DataQueue QueueIN; /// /// Coda valori MESSAGGI/EVENTI (da non sottocampionare come samples)... /// public DataQueue QueueMessages; /// /// Coda degli esiti di ping x calcolo stato macchina /// public DataQueue QueuePing; /// /// Oggetto della coda degli elementi di tipo RawTransf (e non ancora trasmessi) /// NB: sono salvati serializzati come stringhe /// public DataQueue QueueRawTransf; /// /// Coda delle richieste dal server (Task2Exe) /// public DataQueue QueueSrvReq; // = new DataQueue("000", "QueueRawTransf", false); /// /// Coda delle risposte al server (Task2Exe) /// public DataQueue QueueSrvResp; /// /// Coda valori LOG UTENTE (da non sottocampionare come samples)... /// public DataQueue QueueULog; /// /// alias booleano false = R /// public bool R = false; // = new DataQueue("000", "QueueULog", false); /// /// 32 byte input base (es strobe, 8 word da 32 bit di flags...) /// public byte[] RawInput = new byte[32]; /// /// 32 byte output base (es ack, 8 word da 32 bit di flags...) /// public byte[] RawOutput = new byte[32]; /// /// Oggetto connessione REDIS /// public RedisIobCache redisMan; /// /// Oggetto cronometro x campionamento durate chiamate /// public Stopwatch sw = new Stopwatch(); /// /// Imposta veto lettura dati (es per DB a 2 sec) /// public DateTime vetoDataRead = DateTime.Now; /// /// Imposta veto SYNC dati (es per DB 2 DB a 10 sec) /// public DateTime vetoDataSync = DateTime.Now; /// /// Veto (in sec) a nuovi ping (x IOB PING, FTP...) /// public int vetoPingSec = 1; /// /// Veto specifico per la gestione (svuotamento) coda contapezzi (es in fase di attrezzaggio e reset lenti) /// Tipicamente impostato a DelayReadPzCountSetup dopo attrezzaggio... /// public DateTime vetoQueuePzCount = DateTime.Now; /// /// Imposta veto chiamata split (durante chiamata, per 60 sec) /// public DateTime vetoSplit = DateTime.Now.AddMinutes(1); /// /// alias booleano true = W /// public bool W = true; #endregion Public Fields #region Public Properties /// /// Verifica se sia in modalità DEMO avanzata (campionamento da set di valori ammessi...) /// public static bool DemoInSample => baseUtils.CRB("DemoInSample"); /// /// Verifica se sia in modalità DEMO x dati OUTPUT /// public static bool DemoOut => utils.CRB("DemoOut"); /// /// Indicazione VETO PING a server sino alla data-ora indicata /// public static DateTime dtVetoPing { get => utils.dtVetoPing; set => utils.dtVetoPing = value; } /// /// Indicazione VETO accodamento valori INGRESSI/EVENTI sino alla data-ora indicata /// public static DateTime dtVetoQueueIN { get => utils.dtVetoQueueIN; set => utils.dtVetoQueueIN = value; } /// /// Indicazione VETO invio a server sino alla data-ora indicata /// public static DateTime dtVetoSend { get => utils.dtVetoSend; set => utils.dtVetoSend = value; } /// /// Verifica se sia abilitato test lettura blocchi memoria all'avvio /// public static bool EnableTest => baseUtils.CRB("enableTest"); /// /// stato Online/Offline del server MP IO (su REDIS) /// public static bool MPOnline { get => utils.MPIO_Online; set => utils.MPIO_Online = value; } #endregion Public Properties #region Public Methods /// /// processa dataLayer e se necessario salva/mostra /// public static void checkSavePersDataLayer() { } public static string GetMACAddress() { NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces(); String sMacAddress = string.Empty; foreach (NetworkInterface adapter in nics) { if (string.IsNullOrEmpty(sMacAddress))// only return MAC Address from first card { IPInterfaceProperties properties = adapter.GetIPProperties(); //sMacAddress = adapter.GetPhysicalAddress().ToString(); sMacAddress = string.Join(":", (from z in adapter.GetPhysicalAddress().GetAddressBytes() select z.ToString("X2")).ToArray()); } } return sMacAddress; } #endregion Public Methods #region Protected Fields /// /// Variabile numero errori vari (in lettura) --> se supera soglia maxErroriCheck --> disconnette /// protected static int numErroriCheck = 0; /// /// Valore di attesa (random) dopo ogni invio x evitare congestione send... /// protected static int urlRandWait = 0; /// /// Stato connessione con macchina gestita /// protected bool _connOk = false; /// /// valore booleano di check se sia in fase di COMUNICAZIONE ATTIVA con il PLC/NC /// protected bool adpCommAct; /// /// porta x adapter (x restart) /// protected int adpPortNum; /// /// DataOra ultimo avvio adapter x watchdog /// protected DateTime adpStartRun; /// /// Vettore 32 BIT valori in ingresso al filtro /// protected int B_input; /// /// Vettore 32 BIT valori in uscita dal filtro /// protected int B_output; /// /// Vettore 32 BIT valori precedenti /// protected int B_previous = -1; /// /// Cod grupo IOB x creazione PODL al volo /// protected string CodGruppoIob = "ND-00"; /// /// Num errori check alive /// protected int currAliveErrors = 0; /// /// Dizionario valori impostati x produzione /// protected Dictionary currProdData = new Dictionary(); /// /// num corrente errori read PLC /// protected int currReadErrors = 0; /// /// num errori send /// protected int currSendErrors = 0; /// /// Tempo di attesa in minuti x lettura contapezzi standard (da .ini / OptPar) /// protected double delayMinReadPzCount = 0; /// /// Fattore di demoltiplicazione dei DynData x ridurre campionamento /// protected int demFactDynData = 1; /// /// Disabilitazione gestione ODL (lettura e gestione) /// protected bool disableOdl = false; /// /// Boolean x indicare contapezzi disabilitato forzatamente da IOB /// protected bool disablePzCountByIob = false; /// /// Veto x esecuzione task AutoDossier /// protected DateTime dtVetoAutoDossier = DateTime.Now; /// /// Veto x lettura contapezzi (in caso di autoODL con attesa reset ad esempio...) /// protected DateTime dtVetoReadPzCount = DateTime.Now; /// /// DataOra x veto all'invio dataItem /// protected DateTime dtVetoSenDataItem = DateTime.Now; /// /// Veto x esecuzione Task2Exe troppo frequenti /// protected DateTime dtVetoTask2Exe = DateTime.Now; /// /// Determina se sia prevista gestione PODL (creazione/avvio/chiusura) come Soitaab /// protected bool EnabelPodlManFull = false; /// /// Abilita riscrittura memoria se trova differenze lettura/richiesti /// protected bool ENABLE_MEM_REWRITE = true; /// /// Abilitazione restart (da opt par...) /// protected bool enableCliRestart = false; /// /// Boolean x indicare contapezzi abilitato a livello di conf applicazione /// protected bool enablePzCountByApp = true; /// /// Abilitazione invio dataitem /// protected bool enableSendDataItem = true; /// /// Abilitazione gestione slow data /// protected bool enableSlowData = false; //protected static Logger lg = LogManager.GetCurrentClassLogger(); /// /// Abilitazione invio conf macchine /// protected bool enabSendMachineConf = true; /// /// dizionario (opzionale) xz decodifica file da importare /// protected Dictionary FileDecod = new Dictionary(); /// /// DeadBand x riduzione dati FluxLog (se 0 non gestita) /// protected double fluxLogRedDeadBand = 0; /// /// Determina se sia gestita riduzione dati FluxLog /// protected bool fluxLogReduce = false; /// /// Dizionario dei valori FluxLog ultimi verificati x veto /// protected Dictionary fluxLogReduceLast = new Dictionary(); /// /// Dizionario dei valori FluxLog ultimi tipo STRING verificati x veto /// protected Dictionary fluxLogReduceLastString = new Dictionary(); /// /// Dizionario dei veto send x ogni variabile quando non variata /// protected Dictionary fluxLogReduceVeto = new Dictionary(); /// /// Finestra in minuti x invio dati FluxLog quando invariati /// protected int fluxLogResendPeriod = 60; /// /// indica che è richiesto forzatamente reset contapezzi /// protected bool forcePzReset = false; /// /// indica che è richiesto forzatamente reset contapezzi /// protected DateTime forcePzResetUntil = DateTime.Now.AddMinutes(-1); /// /// DEADBAND globale se impostata (es x gestire variazioni minime valori FluxLog da inviare...) /// protected float GLOBAL_DBAND = 0; /// /// Determina se siano gestite le ricette /// protected bool hasRecipe = false; /// /// Array dei contatori x segnali blinking /// protected int[] i_counters; /// /// Indica impianto IN SETUP (fino a quando SMETTE di esserlo...) /// protected bool inSetup = false; /// /// ultimo tentativo connessione... /// protected DateTime lastConnectTry; /// /// Ultimo LOG registrazione avvio (x ridurre log notturni...) /// protected DateTime lastLogStartup = DateTime.Today.AddHours(-1); /// /// Dizionario ULTIMI valori impostati x produzione /// protected Dictionary lastProdData = new Dictionary(); /// /// Ultimo invio contapezzi (x invio delayed) /// protected DateTime lastPzCountSend; /// /// Dizionario ultimi valori (string) delle TSS /// protected Dictionary LastTSS = new Dictionary(); /// /// Dizionario ultimi valori (string) delle TSS /// protected Dictionary LastTSSSend = new Dictionary(); /// /// Dizionario ultimi valori (double) delle TSVC /// protected Dictionary LastTSVC = new Dictionary(); /// /// Ultima registrazione warning x ODL mancante (x scrivere solo ogni 15 secondi) /// protected DateTime lastWarnODL; /// /// ultima data-ora invio parametri write x ribadire status /// protected DateTime lastWriteParamsUpsert = DateTime.Now; /// /// Separatore linea (tipicamente x commenti) /// protected string lineSep = "--------------------------"; /// /// Elenco parametri calcolati da inviare alla macchina (es ricetta con traduzione, RAMA/TFT) /// protected List list2Write = new List(); /// /// Elenco delle eventuali condizioni di veto conditions attive /// protected List ListVetoCond = new List(); /// /// Num massimo di errori in funzionalità check alive /// protected int maxAliveErrors = utils.CRI("maxAliveErrors"); /// /// Soglia massima errori prima della disconnessione automatica in check /// protected int maxErroriCheck = utils.CRI("maxErroriCheck"); /// /// Quantità massima per singola ricetta (per determinare num ricette da inviare) /// protected int maxQtyPerFile = 0; /// /// Dimensione coda di ping x valutazione /// protected int maxQueuePing = 11; /// /// Num massimo di errori di rete read (dal PLC) /// protected int maxReadErrors = utils.CRI("maxReadErrors"); /// /// Periodo massimo (in sec) per letture dati in mancanza di eventi di aggiornamento /// protected int MaxSecReload = 120; /// /// Num massimodi errori di rete send al server /// protected int maxSendErrors = utils.CRI("maxSendErrors"); /// /// Numero massimo di tentativi x test ping preliminare /// protected int maxTryPing = 1; protected string mem2trace = ""; /// /// Tempo minimo ammissibile di risposta (es x errori ModBus che risponde troppo in fretta) in millisec /// protected int minRespTimeMs = 5; protected int minVetoSendDataItem = 60; /// /// indica se serva refresh parametri e quindi PLC... /// protected bool needRefresh = true; /// /// Indica se usare la parte numerica di un articolo come codice INT /// protected bool numArtCharTrim = false; /// /// Timeout x ping al server /// protected int pingServerMsTimeout = utils.CRI("PingMsTimeout"); /// /// DataOra avvio Programma x check avvio /// protected DateTime prgStarted = DateTime.Now; /// /// Ritardo minimo x invio contapezzi /// protected int pzCountDelay = 2500; /// /// Indica se resettare allarmi all'avvio e inviare il reset appena parte adapter /// protected bool resetAlarmOnStart = false; /// /// Periodo di campionamento x refresh info /// protected int samplePeriod = 1000; /// /// MsSample Period base x campionamenti tpo OCP-UA /// protected int samplePeriodBase = 600; /// /// Dizionario di VC da trattare come TimeSeries (con conf decodificata + processing successivo...) /// protected Dictionary TSVC_Data = new Dictionary(); /// /// Indica se usare archivio locale ricette vs scarico http/REST /// protected bool useLocalRecipe = true; /// /// Dizionario di VARIABILI da trattare come eventi (da inviare quando cambiano oppure a /// scadenza periodo...) /// protected Dictionary VarArray = new Dictionary(); /// /// Dizionario dei divieti di invio x ogni counter gestito /// protected Dictionary VetoCounterSend = new Dictionary(); /// /// Veto per registrazione completa log di startup (minuti) /// protected int vetoLogStartupDuration = 60; /// /// Durata in secondi del divieto accodamento segnali IN alla fase di startup /// protected int vetoQueueIn = 10; /// /// Periodo di veto prima di invio nuova conferma dei valori write correnti, default ogni 5 min /// protected double vetoSendWriteUpsert = 1; /// /// Periodo wathdog di default (2 sec se non specificato) /// protected int watchDogPeriod = 2; #endregion Protected Fields #region Protected Properties /// /// Dizionario condizioni di veto log x ogni tipo di messaggio (periodo differente secondo livello) /// protected Dictionary VetoLog { get; set; } = new Dictionary(); /// /// Dizionario conteggio numero volte che si fa veto log x ogni tipo di messaggio /// protected Dictionary VetoLogCount { get; set; } = new Dictionary(); #endregion Protected Properties #region Protected Methods /// /// Decodifica file MAP (caso .bit) /// /// /// /// indirizzo Byte: indirizzo di partenza memoria /// dimensione singolo slot in byte /// indirizzo bit: numero riga x calcolo indice bit /// protected static otherData decodeBitData(string linea, char separator, int ByteNum, int memSize, int BitNum) { if (linea != null) { string[] valori = linea.Split(separator); int shift = 0; try { shift = Convert.ToInt32(valori[0]) - 1; } catch { } int resto = 0; Math.DivRem(BitNum, 8, out resto); string memAddr = string.Format("{0}.{1}", ByteNum + shift * memSize, resto); return new otherData(valori[0], memAddr, valori[1].Trim(), valori[2].Trim()); } else { return null; } } /// /// Decodifica file MAP generico /// /// /// /// /// /// /// protected static otherData decodeOtherData(string linea, char separator, string memPre, int baseAddr, int memSize) { if (linea != null) { string[] valori = linea.Split(separator); int shift = 0; try { shift = Convert.ToInt32(valori[0]) - 1; } catch { } string memAddr = string.Format("{0}{1}", memPre, baseAddr + shift * memSize); return new otherData(valori[0], memAddr, valori[1].Trim(), valori[2].Trim()); } else { return null; } } protected static long GetObjectSize(object genObj, bool isSerializable) { long result = 0; if (isSerializable) { using (var stream = new MemoryStream()) { var formatter = new BinaryFormatter(); formatter.Serialize(stream, genObj); result = stream.Length; } } else { string rawVal = System.Text.Json.JsonSerializer.Serialize(genObj, options); result = rawVal.Length; } return result; } /// /// Verifica se ci sia veto log attivo e gestisce casistiche /// /// /// /// protected bool checkLogVeto(int vetoSec, ref string message) { // verifico SE si debba fare log, altrimenti metto in coda... bool doVeto = true; try { DateTime adesso = DateTime.Now; if (VetoLog.ContainsKey(message)) { // conteggio num veto a +1... if (VetoLogCount.ContainsKey(message)) { VetoLogCount[message]++; } else { VetoLogCount.Add(message, 1); } // controllo scadenza, quando superata soglia aggiorno messaggio con {n} x {messaggio} if (adesso.Subtract(VetoLog[message]).TotalSeconds > vetoSec) { doVeto = false; string newMessage = $"{VetoLogCount[message]} x {message}"; VetoLog.Remove(message); VetoLogCount.Remove(message); message = newMessage; } } else { // primo --> loggo doVeto = false; VetoLog.Add(message, adesso.AddSeconds(vetoSec)); if (VetoLogCount.ContainsKey(message)) { VetoLogCount[message]++; } else { VetoLogCount.Add(message, 0); } } } catch (Exception exc) { lg.Error($"Eccezione in checkLogVeto: reset contatori VetoLog/VetoLogCount | message: {message}"); lg.Error($"Raised Exc:{Environment.NewLine}{exc}"); doVeto = false; // svuoto tutti i log veto... VetoLog = new Dictionary(); VetoLogCount = new Dictionary(); lg.Error($"Effettuato reset contatori VetoLog/VetoLogCount"); } // restituisco esito! return doVeto; } /// /// Recupera la chiave per le statistiche delle chiamate. /// protected string GetCallStatsKey() => redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CallStats"); /// /// Recupera la chiave per i dati di produzione correnti. /// protected string GetCurrProdDataKey() => redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:CurrProdData"); /// /// Recupera la chiave per il flusso di memoria. /// protected string GetFluxMemKey() => redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:FluxMem"); /// /// Recupera la chiave per l'invio dei PODL. /// protected string GetPOdlSentKey() => redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:POdlSent"); /// /// Recupera un valore specifico dal hash dello stato dell'IOB. /// protected string GetStatusField(string field) { string baseKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}"); return redisMan.redGetHashField(baseKey, field); } /// /// Recupera la chiave per le statistiche settimanali. /// protected string GetWeekStatsKey() => redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}:WeekStats"); /// /// Deserializza una stringa JSON in un oggetto. /// protected T JsonDeserialize(string json) => DataSerializer.Deserialize(json); /// /// Serializza un oggetto in formato JSON. /// protected string JsonSerialize(T obj) => DataSerializer.Serialize(obj); /// /// Serializza un oggetto in formato JSON con opzioni serializzazione. /// protected string JsonSerialize(T obj, Formatting reqFormat) => DataSerializer.Serialize(obj, reqFormat); /// /// Imposta un valore nel hash dello stato dell'IOB. /// protected void SetStatusField(string field, string value) { string baseKey = redisMan.redHash($"IOB:Status:{IOBConfFull.General.FilenameIOB}"); redisMan.redSetHashField(baseKey, field, value); } /// /// Setup di tutti gli oggetti Queue, ma solo alcuni hanno coda REDIS (quelli senza sono "sacrificabili" /// protected void SetupQueue() { string codIob = IOBConfFull.General.FilenameIOB; bool useRedis = IOBConfFull.General.EnabRedisQue; // valutare se portare di nuovo in redis... QueueIN = new DataQueue(codIob, "QueueIN", useRedis, redisMan); QueueSrvResp = new DataQueue(codIob, "QueueServResp", useRedis, redisMan); // fix a NON redis QueueAlarm = new DataQueue(codIob, "QueueAlarm", false, redisMan); QueueFLog = new DataQueue(codIob, "QueueFLog", false, redisMan); //QueueFLog = new DataQueue(codIob, "QueueFLog", useRedis, redisMan); QueueMessages = new DataQueue(codIob, "QueueMessages", false, redisMan); QueuePing = new DataQueue(codIob, "QueuePing", false, redisMan); QueueRawTransf = new DataQueue(codIob, "QueueRawTransf", false, redisMan); QueueSrvReq = new DataQueue(codIob, "QueueServReq", false, redisMan); QueueULog = new DataQueue(codIob, "QueueULog", false, redisMan); } /// /// Deserializza una stringa XML in un oggetto. /// protected T XmlDeserialize(string xml) => XmlDataSerializer.Deserialize(xml); /// /// Serializza un oggetto in formato XML. /// protected string XmlSerialize(T obj) => XmlDataSerializer.Serialize(obj); #endregion Protected Methods #region Private Fields /// /// wrapper di log /// private static readonly Logger lg = LogManager.GetCurrentClassLogger(); private static readonly JsonSerializerOptions options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles, WriteIndented = false // Optional: set to true for pretty-printing }; #endregion Private Fields } }